using System;
using Microsoft.SPOT;

using Microsoft.SPOT.Hardware;

using System.IO;
using System.IO.Ports;
using System.Threading;

using GHI.Utilities;

namespace CPF
{
    public class XModem
    {

        // TERMINAL PROGRAM OBSERVATIONS:
        //
        // TeraTerm UTF-8 Pro:
        // XModem-1K ALWAYS sends 1024 data bytes/packet.
        // When Receiver cancels, only 1 <CAN> byte is sent. The <CAN> byte is sent as a standalone byte outside a packet. The Sender simply stops transmitting
        // at the nearest whole packet, and no acknowledgement is returned.
        // When Sender cancels, no control bytes are sent. Transmission simply stops at the nearest whole packet. Only the Receiver interpacket timeout will abort 
        // the transfer on the Receiver side.
        //
        // HyperTerminal 7.0:
        // XModem-1K USUALLY sends 1024 data bytes/packet. However, it can also switch to 128-byte packets if the portion to send is short.
        // When Receiver cancels, 5 <CAN> bytes are sent. The Sender simply stops transmitting at the nearest whole packet, and no acknowledgement is returned.
        // When Sender cancels, 5 <CAN> bytes are sent. The Receiver does not acknowledge. (An <ACK> is sent, but it's actually in response to the final
        // whole packet sent, not the cancellation notification.)
        //
        // This Implementation:
        // For both Sender and Receiver, requesting a cancellation is done by sending <CAN> 5 times. If the Sender or Receiver receives at least 1 <CAN> byte, 
        // transfer will abort. If the Sender receives the cancellation, it will stop at the nearest whole packet. No acknowledgement is sent under any 
        // circumstances by any party, in response to a cancellation.  When transmitting over a a long distance noisy channel, such as a telephone line, 
        // aborting on a single <CAN> byte can be problemmatic due to other standalone control bytes possibly being corrupted into <CAN> and triggering an 
        // unintended abort. However, this implementation is intended to be operated by devices that are directly connected via a serial cable, and so the risk
        // of byte corruption is much less. Aborting on a single <CAN> allows this implementation to respond to terminal software that only sends 1 <CAN> 
        // like TeraTerm. However, if this is problematic, NumCancellationBytesRequired can be used to set the minimum number of cancellation bytes that
        // will result in cancellation.
        //
        // On G120 largest byte array size permissible = 786364 bytes


        // *********************************** BEGIN: RECEIVER CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// The Receiver is responsible for initiating a file transfer.
        /// It does this by sending a FileInitiationByte, which is NAK for XModem-Checksum and C for XModem-CRC and XModem-1K.
        /// If the receiver has sent a FileInitiationByte, yet it has not received the first packet within the timeout specified
        /// below, it should resend the FileInitiationByte.
        /// </summary>
        public int ReceiverFileInitiationRetryMillisec = 250;

        /// <summary>
        /// This is the maximum number of times the Receiver will send the FileInitiationByte if the Sender has not sent the first packet.
        /// If this limit is reached, what happens next will depend on the Receiver's variant.
        ///
        /// SITUATION 1A: The Receiver is requesting XModem-CRC or XModem-1K --AND-- FallBackAllowed == True
        /// A Receiver wishing to use the newer CRC or 1K protocol requests a file transfer by sending <C>.
        /// The Sender is supposed to respond by sending the first packet. However, if the Sender does not support either of these new variants,
        /// it will not recognize the <C> and will not respond. If the maximum number of file initiation attempts is reached, the Receiver 
        /// will fall back to the older XModem-Checksum protocol and send <NAK> as its FileInitiationByte instead. 
        /// The number of attempts is reset. If the limit is reached again and the Sender still has not sent the first packet, then the file 
        /// transfer is aborted.
        ///
        /// SITUATION 1B: The Receiver is requesting XModem-CRC or XModem-1K --AND-- FallBackAllowed == False
        /// If this limit is reached and the first packet has not been received, the Receiver will abort the file transfer.
        ///
        /// SITUATION 2: The Receiver is requesting XModem-Checksum
        /// If this limit is reached and the first packet has not been received, the Receiver will abort the file transfer.
        /// </summary>
        public int ReceiverFileInitiationMaxAttempts = 240;

        /// <summary>
        /// XModem-CRC and XModem-1K evolved from XModem-Checksum.
        /// The official specification states that a Receiver which is requesting XModem-CRC or XModem-1K must have the
        /// ability to "fall back" to XModem-Checksum in case the Sender does not support newer formats.
        ///
        /// When True, this flag allows fallback to occur.
        /// When False, fallback will not occur, and the Receiver will simply abort the file transfer if the newer protocols aren't supported.
        /// This flag only has an effect if the Receiver is configured for XModem-CRC or XModem-1K. It has no impact if the Receiver is 
        /// already configured for XModem-Checksum.
        /// </summary>
        public bool ReceiverFallBackAllowed = true;

        /// <summary>
        /// If the Receiver is expecting data from the Sender, this is the maximum amount of time it will wait before sending NAK to prompt 
        /// the Sender to either resend the packet or finish the file.
        /// </summary>
        public int ReceiverTimeoutMillisec = 10000;

        /// <summary>
        /// If the Sender has not responded to repeated NAK nagging after this number of attempts, then the Receiver should abort the transfer.
        /// </summary>
        public int ReceiverMaxConsecutiveRetries = 10;

        private int _NumCancellationBytesRequired = 1;
        /// This is the minimum number of cancellation bytes that a Sender or Receiver must receive in order to cancel the file transfer.
        /// If 0 or less is specified, CAN bytes are ignored and will not result in cancellation, in which case cancellation will depend 
        /// entirely on the timeout mechanism.
        public int NumCancellationBytesRequired
        {
            get { return _NumCancellationBytesRequired; }
            set
            {
                _NumCancellationBytesRequired = value;
            }
        }

        private int _NumCancellationBytesToSend = 5;
        /// <summary>
        /// This is the number of CAN bytes that a Sender or Receiver will transmit to the connected party if the user requests a cancellation.
        /// </summary>
        public int NumCancellationBytesToSend
        {
            get { return _NumCancellationBytesToSend; }
            set
            {
                if (value >= 0)
                    _NumCancellationBytesToSend = value;
                else
                    NumCancellationBytesToSend = 5;
            }
        }

        // *********************************** END: RECEIVER CUSTOMIZABLE PARAMETERS ***********************************

        // *********************************** BEGIN: SENDER CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// The Receiver checks each packet for errors. If a packet contains errors, the Receiver is supposed to send NAK, telling the Sender to resend the packet.
        /// This is the total number of times the Sender will resend the same packet whenever consecutive NAKs are received.
        /// If this limit is reached and the Receiver is still sending NAK, the communication channel is assumed to be terminally unreliable
        /// and file transfer should abort.
        /// </summary>
        public int MaxSenderRetries = 10;

        /// <summary>
        /// The Receiver is supposed to validate each packet with ACK or NAK.
        /// If validation has not been received from the Receiver for this amount of time, the packet is resent.
        /// </summary>
        public int SenderPacketRetryTimeoutMillisec = 15000;

        /// <summary>
        /// If the Sender has not heard ANYTHING from the Receiver after this amount of time has elapsed, the file transfer is aborted.
        /// </summary>
        public int SendInactivityTimeoutMillisec = 60000;

        // *********************************** END: SENDER CUSTOMIZABLE PARAMETERS ***********************************

        // *********************************** BEGIN: COMMON CUSTOMIZABLE PARAMETERS ***********************************

        // ASCII codes for common character constants.
        // These are exposed as public fields in case the user needs to customize them with nonstandard values.
        public byte SOH = 1;    // Sender begins each 128-byte packet with this header
        public byte STX = 2;    // Sender begins each 1024-byte packet with this header
        public byte ACK = 6;    // Receiver sends this to indicate packet was received successfully with no errors
        public byte NAK = 21;   // Receiver sends this to initiate XModem-Checksum file transfer -- OR -- indicate packet errors
        public byte C = 67;     // Receiver sends this to initiate XModem-CRC or XModem-1K file transfer
        public byte EOT = 4;    // Sender sends this to mark the end of file. Receiver must acknowledge receipt of this byte with <ACK>, otherwise Sender resends <EOT> 
        public byte SUB = 26;   // This is used as a padding byte in the original specification
        public byte CAN = 24;   // [Commonly used but unofficial] Sender or Receiver sends this byte to abort file transfer
        public byte EOF = 26;   // [Commonly used but unofficial] MS-DOS version of <EOT>

        /// <summary>
        /// Defines the number of data bytes in a nominal 1024-byte packet.
        /// This allows the user to redefine a custom packet size for non-standard XModem implementations.
        /// </summary>
        private int _Packet1024NominalSize = 1024;
        public int Packet1024NominalSize
        {
            get { return _Packet1024NominalSize; }
            set
            {
                if (value > 0)
                {
                    _Packet1024NominalSize = value;
                    DefineDataPacketTemplate();
                }
            }
        }

        /// <summary>
        /// Defines the number of data bytes in a nominal 128-byte packet.
        /// This allows the user to redefine a custom packet size for non-standard XModem implementations.
        /// </summary>
        private int _Packet128NominalSize = 128;
        public int Packet128NominalSize
        {
            get { return _Packet128NominalSize; }
            set
            {
                if (value > 0)
                {
                    _Packet128NominalSize = value;
                    DefineDataPacketTemplate();
                }
            }
        }

        // *********************************** END: COMMON CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// CONSTRUCTOR.
        /// </summary>
        /// <param name="port">SerialPort to use when sending or receiving.</param>
        /// <param name="variant">
        /// The particular flavor of XModem to use.
        /// See Variants enumeration for a description of each XModem variant.
        /// </param>
        /// <param name="paddingByte">
        /// If sending, this is the byte value that will be used to pad a packet if the data being sent is shorter than
        /// the required packet length. If omitted, defaults to SUB (byte decimal 26).
        /// </param>
        /// <param name="endOfFileByteToSend">
        /// If sending, this is the byte value that will be sent to indicate that the end-of-file has been reached.
        /// Of omitted, defaults to EOT (byte decimal 4). Some XModem receiver implementations may require that EOF
        /// is used instead to indicate end-of-file.
        /// </param>
        public XModem(SerialPort port, Variants variant, byte paddingByte = 26, byte endOfFileByteToSend = 4)
        {
            // Field initialization
            PaddingByte = paddingByte;
            EndOfFileByteToSend = endOfFileByteToSend;

            // Property Initialization
            Port = port;
            Variant = variant;
        }

        /// <summary>
        /// Cancels the file transfer operation (send or receive) and notifies the other party of the
        /// cancellation by transmitting CAN bytes.
        /// </summary>
        public void CancelFileTransfer()
        {
            // Abort what we're currently doing
            Abort();

            // Send cancellation bytes
            for (int k = 1; k <= _NumCancellationBytesToSend; k++)
            {
                Port.WriteByte(CAN);
            }

            _TerminationReason = TerminationReasonEnum.UserCancelled;
        }

        /// <summary>
        /// Describes the various flavors of XModem.
        /// 
        /// XModemChecksum: This is the original, classic version of XModem. It is also the slowest and most error-prone.
        /// 128 data bytes per packet with checksum error-detection 
        /// 132 bytes/packet total = 3 header bytes + 128 data bytes + 1 error-detection byte
        /// 
        /// XModemCRC: This similar to the original, except with CRC-16 error detection instead of a simple checksum for more reliability.
        /// 128 data bytes per packet with CRC-16 error-detection 
        /// 133 bytes/packet total = 3 header bytes + 128 data bytes + 2 error-detection bytes
        /// 
        /// XModem1K: This is an updated version of XModem-CRC, expanded to 1024 data bytes (though 128 data bytes are still accepted).
        /// 128 and/or 1024 data bytes per packet and CRC-16 error-detection 
        /// 133 bytes/packet total = 3 header bytes + 128 data bytes + 2 error-detection bytes
        /// 1029 bytes/packet total = 3 header bytes + 1024 data bytes + 2 error-detection bytes
        /// </summary>
        public enum Variants
        {
            XModemChecksum,
            XModemCRC,
            XModem1K
        };

        private Variants _Variant;
        /// <summary>
        /// Gets/sets the XModem flavor that this implementation adheres to.
        /// </summary>
        public Variants Variant
        {
            get { return _Variant; }
            set
            {
                _Variant = value;
                DefineDataPacketTemplate(); // In case user wants to send data
            }
        }

        /// <summary>
        /// XModem's possible operational states.
        /// </summary>
        private enum States
        {
            Inactive,                           // The object is neither Sending nor Receiving
            ReceiverFileInitiation,             // Receiver is sending the file initiation byte at regular intervals
            ReceiverHeaderSearch,               // Receiver is expecting SOH or STX packet header
            ReceiverBlockNumSearch,             // Receiver is expecting the block number
            ReceiverBlockNumComplementSearch,   // Receiver is expecting the block number complement
            ReceiverDataBytesSearch,            // Receiver is populating data bytes
            ReceiverErrorCheckSearch,           // Receiver is expecting 1-byte or 2-byte check value(s)
            SenderAwaitingFileInitiation,       // Sender is expecting file transmission request from Receiver
            SenderPacketSent,                   // Sender has sent a packet and is waiting for Receiver to validate it
            SenderAlertForPossibleCancellation, // Sender is not expecting anything in particular but can respond to a cancellation request if needed
            SenderAwaitingEndOfFileConfirmation // Sender has transmitted the end-of-file byte and is waiting for Receiver to acknowledge receipt of that byte
        }

        private States CurrentState = States.Inactive;

        /// <summary>
        /// Describes how the current XModem session has ended.
        /// </summary>
        public enum TerminationReasonEnum
        {
            TransferStillActiveNotTerminated,   // File transfer is still active and has not terminated yet
            UserCancelled,                      // User has cancelled the file transfer
            EndOfFile,                          // End-of-file was reached. This is the ideal outcome when sending or receiving.
            FileInitiationTimeout,              // Receiver has repeatedly requested file transfer to begin, but Sender has not responded
            CancelNotificationReceived,         // Transfer aborted because cancellation bytes were detected from the Sender or Receiver
            TooManyRetries,                     // Too many erroneous packets have been sent or received. Indicates corruption or total communication loss.
            NoResponseFromReceiver              // When Sending a file, the Receiver has become completely silent for an extended period.
        }

        public TerminationReasonEnum _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
        /// <summary>
        /// Describes under what conditions the file transfer has terminated.
        /// </summary>
        public TerminationReasonEnum TerminationReason
        {
            get { return _TerminationReason; }
        }

        /// <summary>
        /// PacketReceived event handler.
        /// </summary>
        /// <param name="sender">The XModem object that raised the event.</param>
        /// <param name="packet">The complete, validated data packet received.</param>
        /// <param name="endOfFileDetected">
        /// Boolean flag that indicates if this packet is the final packet expected.
        /// True if the end-of-file byte was received, and the current packet is therefore the last one.
        /// False if the end-of-file byte has not been received, and more packets are still expected.
        /// </param>
        public delegate void PacketReceivedEventHandler(XModem sender, byte[] packet, bool endOfFileDetected);

        /// <summary>
        /// Raised whenever a complete packet has been received.
        /// </summary>
        public event PacketReceivedEventHandler PacketReceived;

        /// <summary>
        /// Communication port that will be used to send and receive.
        /// </summary>
        public SerialPort Port;

        /// <summary>
        /// Performs blocking when the Receive() method is called by the user.
        /// </summary>
        private ManualResetEvent ReceiverUserBlock = new ManualResetEvent(false);

        private int _NumCancellationBytesReceived = 0;
        /// <summary>
        /// Tracks the number of CAN bytes that have currently been received by this Sender or Receiver.
        /// </summary>
        public int NumCancellationBytesReceived
        {
            get { return _NumCancellationBytesRequired; }
        }

        /// <summary>
        /// MemoryStream used to store all data received if user wants to receive data in one big lump.
        /// This remains null if user wants to receive data packet-by-packet instead.
        /// </summary>
        private MemoryStream AllDataReceivedBuffer;

        /// <summary>
        /// Initiates the file receive process. This method blocks until the file transfer has terminated.
        /// </summary>
        /// <param name="allDataReceivedBuffer">
        /// Optional MemoryStream object, which may be omitted. 
        /// By instantiating an empty MemoryStream object and supplying it as an argument, the user can collect all data
        /// received into a single data structure and process it in one big lump once the transfer has finished. 
        /// This is NOT recommended if the expected file size can exceed the contiguous memory capacity of the device. 
        /// If the expected file size is large, subscribing to the PacketReceived event is recommended so that processing
        /// can be done one packet at a time within the available memory.
        /// </param>
        /// <returns>
        /// A TerminationReasonEnum that describes under what conditions the receive process has terminated. This may
        /// be due to a successful EndOfFile being reached, or due to timeouts, errors, etc.
        /// </returns>
        public TerminationReasonEnum Receive(MemoryStream allDataReceivedBuffer = null)
        {
            // Initialize control variables
            _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
            Aborted = false;
            BlockNumExpected = 1;
            _NumFileInitiationBytesSent = 0;
            _NumCancellationBytesReceived = 0;
            Remainder = new byte[0];
            DataPacketNumBytesStored = 0;
            ExpectingFirstPacket = true;
            ValidPacketReceived = false;

            // Define file initiation byte according to variant
            if (_Variant == Variants.XModemChecksum)
                FileInitiationByteToSend = NAK;
            else
                FileInitiationByteToSend = C;

            // Initialize state
            CurrentState = States.ReceiverFileInitiation;

            // Open port if it isn't open already
            if (Port.IsOpen == false)
                Port.Open();

            // Clear out serial port buffers
            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();

            // If user wants to get all data in one big lump, initialize the buffer with whatever MemoryStream object
            // is passed. If a memorystream is omitted, this defaults to null, which means received data
            // will not be stored.
            AllDataReceivedBuffer = allDataReceivedBuffer;

            // Attach event handler
            Port.DataReceived += new SerialDataReceivedEventHandler(Port_DataReceived);

            // Begin file initiation
            if (ReceiverFileInitiationTimer == null)
                ReceiverFileInitiationTimer = new Timer(ReceiverFileInitiationRoutine, null, 0, ReceiverFileInitiationRetryMillisec);
            else
                ReceiverFileInitiationTimer.Change(0, ReceiverFileInitiationRetryMillisec);

            // Block here
            ReceiverUserBlock.Reset();
            ReceiverUserBlock.WaitOne();
            ReceiverUserBlock.Reset();

            return TerminationReason;
        }

        // The byte value that a Receiver sends when requesting file transfer:
        // XModem Checksum = <NAK>
        // XModem CRC = <C>
        // XModem 1K = <C>
        private byte FileInitiationByteToSend;

        /// <summary>
        /// Sends the file initiation byte at regular intervals to request start of transfer from Sender.
        /// </summary>
        private Timer ReceiverFileInitiationTimer;

        /// <summary>
        /// Timer callback for file initiation timer.
        /// </summary>
        /// <param name="notUsed"></param>
        private void ReceiverFileInitiationRoutine(object notUsed)
        {
            Port.WriteByte(FileInitiationByteToSend);
            NumFileInitiationBytesSent += 1;
        }

        // Tracks how many file initiation bytes have already been sent to the Sender
        private int _NumFileInitiationBytesSent = 0;
        public int NumFileInitiationBytesSent
        {
            get { return _NumFileInitiationBytesSent; }
            private set
            {
                _NumFileInitiationBytesSent = value;

                // Determine if Receiver should fall back to an older variant
                if (_NumFileInitiationBytesSent > ReceiverFileInitiationMaxAttempts)
                {
                    if (FileInitiationByteToSend == C && ReceiverFallBackAllowed == true)
                    {
                        // Fall back to older variant
                        FileInitiationByteToSend = NAK;
                        _NumFileInitiationBytesSent = 0;
                    }
                    else
                    {
                        Abort();
                        _TerminationReason = TerminationReasonEnum.FileInitiationTimeout;
                        ReceiverUserBlock.Set();
                    }
                }
            }
        }

        // Indicates whether a valid packet has been received and is fit to be forwarded to user
        private bool ValidPacketReceived = false;

        /// <summary>
        /// State machine dispatcher.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = sender as SerialPort;
            int numBytes = sp.BytesToRead;
            byte[] recv = new byte[numBytes];
            sp.Read(recv, 0, numBytes);

            if (numBytes > 0)
            {
                switch (CurrentState)
                {
                    // RECEIVER STATES:
                    case States.ReceiverFileInitiation:
                        // A response was received from the Sender after 1 or more file initiation bytes have been sent.

                        // Halt the file initiation sender
                        if (ReceiverFileInitiationTimer != null)
                            ReceiverFileInitiationTimer.Change(Timeout.Infinite, Timeout.Infinite);

                        // Start the timeout/NAK watchdog
                        if (ReceiverNAKWatchdog == null)
                            ReceiverNAKWatchdog = new Timer(NAKNag, null, ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);
                        else
                            ReceiverNAKWatchdog.Change(ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);

                        // Advance to the next stage and process the current data received
                        CurrentState = States.ReceiverHeaderSearch;
                        ReceiverPacketBuilder(recv);
                        break;

                    case States.ReceiverHeaderSearch:
                    case States.ReceiverBlockNumSearch:
                    case States.ReceiverBlockNumComplementSearch:
                    case States.ReceiverDataBytesSearch:
                    case States.ReceiverErrorCheckSearch:
                        ResetNAKWatchdog();
                        ReceiverPacketBuilder(recv);
                        break;


                    // SENDER STATES:
                    case States.SenderAwaitingFileInitiation:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            // Determine if file initiation byte for CRC or Checksum error-checking received.
                            if (Array.IndexOf(recv, C) > -1 || Array.IndexOf(recv, NAK) > -1)
                            {
                                // If user originally specified older XModem-Checksum but <C> was received instead,
                                // upgrade variant to new XModem-1K (which can handle both 128 and 1024 data bytes)
                                // for maximum flexibility.
                                if (_Variant == Variants.XModemChecksum && Array.IndexOf(recv, C) > -1)
                                    Variant = Variants.XModem1K;

                                if (Array.IndexOf(recv, NAK) > -1)
                                    Variant = Variants.XModemChecksum;

                                CurrentState = States.SenderAlertForPossibleCancellation;
                                WaitForResponseFromReceiver.Set();
                            }
                        }
                        break;

                    case States.SenderPacketSent:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            if (Array.IndexOf(recv, ACK) > -1)
                            {
                                // ACK received
                                ResetSenderPacketResponseWatchdog();
                                _SenderConsecutiveRetryAttempts = 0;
                                PacketSuccessfullySent = true;
                                _BlockNumToSend += 1;
                                WaitForResponseFromReceiver.Set();
                            }
                            else if (Array.IndexOf(recv, NAK) > -1)
                            {
                                // NAK received
                                ResetSenderPacketResponseWatchdog();
                                _SenderConsecutiveRetryAttempts += 1;
                                WaitForResponseFromReceiver.Set();
                            }
                        }
                        break;

                    case States.SenderAlertForPossibleCancellation:
                        ResetReceiverStillAliveWatchdog();
                        DetectCancellation(recv);
                        break;

                    case States.SenderAwaitingEndOfFileConfirmation:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            if (Array.IndexOf(recv, ACK) > -1)
                            {
                                // ACK received
                                EndOfFileAcknowledgementReceived = true;
                                Abort();
                            }
                        }
                        break;

                }
            }
        }

        /// <summary>
        /// Enforces timeouts if no data is received or the packet repeatedly fails the checksum.
        /// This timer must be reset within a set amount of time or NAK is transmitted.
        /// </summary>
        private Timer ReceiverNAKWatchdog;

        /// <summary>
        /// Resets the NAK watchdog.
        /// </summary>
        private void ResetNAKWatchdog()
        {
            if (ReceiverNAKWatchdog != null)
                ReceiverNAKWatchdog.Change(ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);

            ReceiverNumConsecutiveNAKSent = 0;
        }

        /// <summary>
        /// Timer callback for the NAK watchdog.
        /// </summary>
        /// <param name="notUsed"></param>
        private void NAKNag(object notUsed)
        {
            SendNAK();
        }

        private void SendNAK()
        {
            Port.WriteByte(NAK);
            ReceiverNumConsecutiveNAKSent += 1;
        }

        private void SendACK()
        {
            Port.WriteByte(ACK);
            ReceiverNumConsecutiveNAKSent = 0;
        }

        // Tracks how many consecutive <NAK> bytes have been sent
        public int _ReceiverNumConsecutiveNAKSent = 0;
        public int ReceiverNumConsecutiveNAKSent
        {
            get { return _ReceiverNumConsecutiveNAKSent; }
            set
            {
                _ReceiverNumConsecutiveNAKSent = value;

                if (_ReceiverNumConsecutiveNAKSent >= ReceiverMaxConsecutiveRetries)
                {
                    Abort();
                    _TerminationReason = TerminationReasonEnum.TooManyRetries;
                    ReceiverUserBlock.Set();
                }
            }
        }

        /// <summary>
        /// The expected data packet size as specified by the incoming packet header.
        /// </summary>
        private int ExpectedDataPacketSize;

        // Stores the value of a candidate block number
        private byte BlockNumReceived;

        // Stores the value of a candidate block number complement (255 minus block number)
        private byte BlockNumComplementCandidateReceived;

        private bool ExpectingFirstPacket = true;
        private byte BlockNumExpected = 1;

        private byte[] BytesToParse;
        private byte[] Remainder = new byte[0];

        // Stores data bytes received. It is instantiated to be the same length as the expected data packet size.
        private byte[] DataPacketReceived;

        // Keeps track of the number of bytes actually placed inside the current data packet
        private int DataPacketNumBytesStored = 0;

        // Stores the check value. This will contain 1 byte for XModem-Checksum, and 2 bytes for XModem-CRC or XModem-1K.
        private byte[] ErrorCheck;

        private void ReceiverPacketBuilder(byte[] freshBytes)
        {
            // The DataReceived event is occasionally triggered even when there are no bytes to read, so guard against this
            if (freshBytes.Length == 0)
                return;

            // We only want this to run once per DataReceived event and only if we are searching for a valid header byte
            if (Remainder.Length > 0 && CurrentState == States.ReceiverHeaderSearch)
                BytesToParse = Utility.CombineArrays(Remainder, freshBytes);
            else
                BytesToParse = freshBytes;

            // This keeps track of the index positions that searches begin at
            int headerByteSearchStartIndex = 0;
            int searchStartIndex = 0;

            // Loop while we are within bounds of the bytes to parse
            while (searchStartIndex < BytesToParse.Length && headerByteSearchStartIndex < BytesToParse.Length)
            {

                if (CurrentState == States.ReceiverHeaderSearch)
                {
                    // Empty the remainder if populated, since it should have fulfilled its purpose prior to us
                    // reaching this point. We don't want the remainder to carry over into the next DataReceived event
                    // if we are in the header byte search state.
                    if (Remainder.Length > 0)
                        Remainder = new byte[0];

                    // Check for file termination tokens
                    if (BytesToParse[headerByteSearchStartIndex] == EOT ||
                        BytesToParse[headerByteSearchStartIndex] == EOF)
                    {
                        SendACK();
                        Abort();
                        _TerminationReason = TerminationReasonEnum.EndOfFile;

                        // If subscribed to, raise packet received event and indicate end of file reached
                        if (PacketReceived != null && ValidPacketReceived == true && DataPacketReceived != null && DataPacketReceived.Length > 0)
                            PacketReceived(this, DataPacketReceived, true);

                        ReceiverUserBlock.Set();
                        return;
                    }
                    else
                    {
                        // If subscribed to, raise packet received event and indicate end of file NOT yet reached
                        if (PacketReceived != null && ValidPacketReceived == true && DataPacketReceived != null && DataPacketReceived.Length > 0)
                            PacketReceived(this, DataPacketReceived, false);
                    }

                    // Reset packet received validation flag
                    ValidPacketReceived = false;

                    // Check for cancellation bytes
                    if (_NumCancellationBytesRequired > 0)
                    {
                        // Check if current byte is a cancellation request
                        if (BytesToParse[headerByteSearchStartIndex] == CAN)
                        {
                            _NumCancellationBytesReceived += 1;

                            if (_NumCancellationBytesReceived >= _NumCancellationBytesRequired)
                            {
                                // CANCEL THE FILE TRANSFER
                                Abort();
                                _TerminationReason = TerminationReasonEnum.CancelNotificationReceived;
                                ReceiverUserBlock.Set();
                                return;
                            }
                            else
                            {
                                // Move on to the next byte
                                headerByteSearchStartIndex += 1;
                                continue;
                            }
                        }
                        else
                        {
                            // If not a cancellation byte, reset the cancellation byte counter
                            _NumCancellationBytesReceived = 0;
                        }
                    }

                    // Determine if we have a candidate header byte based on our XModem variant
                    if (_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                    {
                        // XModem-Checksum and XModem-CRC should always have 128 data byte packets headed by <SOH>.
                        // Determine the location of this header byte, if it's present.                        
                        int foundIndex = Array.IndexOf(BytesToParse, SOH, headerByteSearchStartIndex);

                        if (foundIndex == -1) // No header byte found
                        {
                            // Quit this search and ignore the remaining bytes. 
                            // Look for a header byte in the next transmission.
                            return;
                        }
                        else if (foundIndex > -1)
                        {
                            // Save the index position where we THINK a valid header byte resides at.
                            // The candidate header byte may be discovered to be invalid later on, and we
                            // need an index to return to in case we need to repeat the header search
                            // starting from the next index.
                            headerByteSearchStartIndex = foundIndex + 1;

                            // Specify the packet size corresponding to this header
                            ExpectedDataPacketSize = _Packet128NominalSize;

                            // Move on to the next stage
                            searchStartIndex = foundIndex + 1;
                            CurrentState = States.ReceiverBlockNumSearch;
                            continue;
                        }
                    }
                    else if (_Variant == Variants.XModem1K)
                    {
                        // XModem-1K can receive 1024 data byte packets headed by <STX> --OR-- 128 data byte packets headed by <SOH>.
                        // The official standard allows a Sender to send a mixture of packet sizes, so a Receiver has to look for both.
                        int packetStartIndexSTX = Array.IndexOf(BytesToParse, STX, headerByteSearchStartIndex);
                        int packetStartIndexSOH = Array.IndexOf(BytesToParse, SOH, headerByteSearchStartIndex);

                        // Look for the packet header byte
                        int foundIndex = 0;
                        if (packetStartIndexSTX > -1 && packetStartIndexSOH > -1)
                        {
                            // There is a (slim) possibility that both <SOH> and <STX> bytes may be found.                      
                            // If both are found, our candidate packet start index should be the earlier of the two. 
                            // If we're wrong, we can always get to the later one during the next iteration.
                            if (packetStartIndexSTX <= packetStartIndexSOH)
                            {
                                // Header for 1024 data bytes/packet
                                ExpectedDataPacketSize = _Packet1024NominalSize;
                                foundIndex = packetStartIndexSTX;
                            }
                            else
                            {
                                // Header for 128 data bytes/packet
                                ExpectedDataPacketSize = _Packet128NominalSize;
                                foundIndex = packetStartIndexSOH;
                            }
                        }
                        else if (packetStartIndexSTX > -1)  // Only 1 found
                        {
                            // Header for 1024 data bytes/packet
                            ExpectedDataPacketSize = _Packet1024NominalSize;
                            foundIndex = packetStartIndexSTX;
                        }
                        else if (packetStartIndexSOH > -1)  // Only 1 found
                        {
                            // Header for 128 data bytes/packet
                            ExpectedDataPacketSize = _Packet128NominalSize;
                            foundIndex = packetStartIndexSOH;
                        }
                        else
                        {
                            // If neither candidate headers were found, quit this search and ignore the remaining bytes.
                            // Look for a header byte in the next transmission.
                            return;
                        }

                        // Save the index position where we THINK a valid header byte resides at.
                        // The candidate header byte may be discovered to be invalid later on, and we
                        // need an index to return to in case we need to repeat the header search
                        // starting from the next index.
                        headerByteSearchStartIndex = foundIndex + 1;

                        // Move on to the next stage
                        searchStartIndex = foundIndex + 1;
                        CurrentState = States.ReceiverBlockNumSearch;
                        continue;
                    }
                }

                if (CurrentState == States.ReceiverBlockNumSearch)
                {
                    // The block number should be immediately after the packet header byte
                    BlockNumReceived = BytesToParse[searchStartIndex];

                    // If the candidate block number is equal to a header byte, there is a slim possibility that what we currently believe
                    // is the block number may actually be the true header byte, especially if the current candidate header byte is discovered
                    // to be invalid later. Therefore, we should save the "block number" as a remainder so we can re-examine it later if 
                    // the current header byte canididate does not pan out and another DataReceived event is raised.
                    if ((_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                        && BlockNumReceived == SOH)
                    {
                        Remainder = new byte[] { BlockNumReceived };   // Initialize remainder
                    }
                    else if (_Variant == Variants.XModem1K && (BlockNumReceived == SOH || BlockNumReceived == STX))
                    {
                        Remainder = new byte[] { BlockNumReceived };   // Initialize remainder
                    }

                    // Move on to the next stage
                    searchStartIndex += 1;
                    CurrentState = States.ReceiverBlockNumComplementSearch;
                    continue;
                }

                if (CurrentState == States.ReceiverBlockNumComplementSearch)
                {
                    // The complement of the block number should be right after the block number
                    BlockNumComplementCandidateReceived = BytesToParse[searchStartIndex];

                    // Determine if we have received a valid block number and complement.
                    // This will determine if we have a valid packet header and can proceed to the next stage.
                    if (BlockNumComplementCandidateReceived == 255 - BlockNumReceived)
                    {
                        // Valid packet header....

                        // The packet header is valid, so we don't need to re-examine the remainder and can therefore
                        // discard it
                        if (Remainder.Length > 0)
                            Remainder = new byte[0];

                        // Instantiate the data packet array which will hold incoming data
                        DataPacketReceived = new byte[ExpectedDataPacketSize];
                        DataPacketNumBytesStored = 0;

                        // Move on to the next stage
                        searchStartIndex += 1;
                        CurrentState = States.ReceiverDataBytesSearch;
                        continue;
                    }
                    else
                    {
                        // Not a valid packet header....

                        // Add the candidate block number complement to the remainder if: 
                        // 1.) the remainder already contains an alternate candidate header byte --OR--
                        // 2.) the candidate block number complement has the same value as a header byte, and could therefore be a valid header byte itself
                        if (Remainder.Length > 0 || ((_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                            && BlockNumReceived == SOH))
                        {
                            Remainder = Utility.CombineArrays(Remainder, new byte[] { BlockNumComplementCandidateReceived });   // Add to remainder
                        }
                        else if (Remainder.Length > 0 || (_Variant == Variants.XModem1K && (BlockNumReceived == SOH || BlockNumReceived == STX)))
                        {
                            Remainder = Utility.CombineArrays(Remainder, new byte[] { BlockNumComplementCandidateReceived });   // Add to remainder
                        }

                        // Search for another header byte
                        CurrentState = States.ReceiverHeaderSearch;
                        continue;
                    }
                }

                if (CurrentState == States.ReceiverDataBytesSearch)
                {
                    // Begin filling the data packet....

                    // Determine if there are enough unparsed bytes to fill the data packet
                    int numUnparsedBytesRemaining = BytesToParse.Length - searchStartIndex;
                    int numDataPacketBytesStillMissing = DataPacketReceived.Length - DataPacketNumBytesStored;

                    int numDataBytesToPull;
                    if (numUnparsedBytesRemaining >= numDataPacketBytesStillMissing)
                        numDataBytesToPull = numDataPacketBytesStillMissing;
                    else
                        numDataBytesToPull = numUnparsedBytesRemaining;

                    Array.Copy(BytesToParse, searchStartIndex, DataPacketReceived, DataPacketNumBytesStored, numDataBytesToPull);
                    DataPacketNumBytesStored += numDataBytesToPull;
                    searchStartIndex += numDataBytesToPull;

                    if (DataPacketNumBytesStored >= ExpectedDataPacketSize)
                    {
                        // If all expected data bytes have been gathered, move on to the next stage
                        CurrentState = States.ReceiverErrorCheckSearch;
                        ErrorCheck = new byte[0];
                    }

                    continue;
                }

                if (CurrentState == States.ReceiverErrorCheckSearch)
                {
                    if (_Variant == Variants.XModemChecksum)
                    {
                        // 1 error-check byte expected.
                        ErrorCheck = new byte[] { BytesToParse[searchStartIndex] };

                        // Validate packet
                        ValidatePacket();

                        // Start over               
                        headerByteSearchStartIndex = searchStartIndex + 1;
                        CurrentState = States.ReceiverHeaderSearch;
                    }
                    else  // XModem-CRC or XModem-1K
                    {
                        // 2 error-check bytes expected.

                        // If we have not yet gathered the required number of error check bytes, append to the error check
                        // array.
                        if (ErrorCheck.Length < 2)
                        {
                            ErrorCheck = Utility.CombineArrays(ErrorCheck, new byte[] { BytesToParse[searchStartIndex] });
                        }

                        if (ErrorCheck.Length >= 2)
                        {
                            // We have enough error-check bytes, so validate packet
                            ValidatePacket();

                            // Return to the initial state and start over               
                            headerByteSearchStartIndex = searchStartIndex + 1;
                            CurrentState = States.ReceiverHeaderSearch;
                        }
                        else
                        {
                            // Advance to the next byte
                            searchStartIndex += 1;
                        }
                    }
                } // End if
            } // End while
        } // End method

        private void ValidatePacket()
        {
            // In order for a packet to be accepted, it must be an expected block number, and the transmitted
            // check value must match the calculated check value.

            // Dealing with a block number that is out of sequence:
            //
            // Normally, successive block numbers must be monotonically increasing (accounting for binary wraparound) in order
            // to be valid. <NAK> is normally sent if a block number is out of sequence. However, there's an exception.
            //
            // Exception:
            // If the current block number has been duplicated (it has the same value as a block number that was
            // received previously), <ACK> is sent anyway on the theory that the Sender may not have received
            // the previous <ACK> and decided to send the same packet again. <ACK> will prompt the Sender to move on to the
            // next packet, which is what we want. (<NAK> will make it resend the packet yet again, which is unwanted.)
            //
            // Exception to the exception:
            // If this is the very first packet, the block number MUST be 1. Otherwise, <NAK> will be sent.

            if (BlockNumReceived == BlockNumExpected)
            {
                if (ValidateChecksum() == true)
                {
                    // Update control variables
                    BlockNumExpected += 1;
                    ExpectingFirstPacket = false;

                    // If user wants all received data to be outputed in one lump, add this packet to the buffer
                    if (AllDataReceivedBuffer != null)
                        AllDataReceivedBuffer.Write(DataPacketReceived, 0, DataPacketReceived.Length);

                    ValidPacketReceived = true;

                    // Notify Sender to send the next packet
                    SendACK();
                }
                else
                {
                    // Inform sender that checksum invalid
                    SendNAK();
                    ValidPacketReceived = false;
                }
            }
            else if (ExpectingFirstPacket == false && BlockNumReceived == (byte)(BlockNumExpected - 1))
            {
                // Receiver got a duplicate packet.
                // Send <ACK> to prompt the Sender to advance to the next packet. Ignore the current (redundant) packet.
                SendACK();
                ValidPacketReceived = false;
            }
            else
            {
                // The block number is completely out of sequence, so send NAK
                SendNAK();
                ValidPacketReceived = false;
            }
        }

        /// <summary>
        /// Calculates the check value (simple checksum or CRC-16) appended to the end of a packet.
        /// </summary>
        /// <returns>
        /// True if the calculated check value and received check value match.
        /// False if there is a mismatch between the calculated and received check values.
        /// </returns>
        private bool ValidateChecksum()
        {
            switch (_Variant)
            {
                // Arithmetic checksum:
                case Variants.XModemChecksum:
                    byte checksum = CheckSum(DataPacketReceived);
                    if (checksum == ErrorCheck[0])
                        return true;
                    else
                        return false;

                // CRC-16:
                case Variants.XModemCRC:
                case Variants.XModem1K:
                    ushort crcChecksumCalculated = Crc.Crc16(DataPacketReceived, 0, DataPacketReceived.Length, 0);
                    ushort crcChecksumReceived = BytesToUShort(ErrorCheck[0], ErrorCheck[1]);
                    if (crcChecksumCalculated == crcChecksumReceived)
                        return true;
                    else
                        return false;

                // Just so VS2010 doesn't complain:
                default:
                    return false;
            }
        }

        private ushort BytesToUShort(byte highByte, byte lowByte)
        {
            return (ushort)((highByte << 8) + lowByte);
        }

        private byte[] UShortToBytes(ushort val)
        {
            byte highByte = (byte)(val / 256);
            byte lowByte = (byte)(val % 256);

            return new byte[] { highByte, lowByte };
        }

        /// <summary>
        /// Calculates the simple checksum by summing all values in a byte array and returning the remainder.
        /// </summary>
        /// <param name="seq">
        /// Byte array whose checksum to calculate.
        /// </param>
        /// <returns>
        /// Modulo-256 checksum.
        /// </returns>
        private byte CheckSum(byte[] seq)
        {
            byte sum = 0;
            for (int k = 0; k < seq.Length; k++)
            {
                sum += seq[k];
            }
            return sum;
        }

        private bool Aborted = false;

        /// <summary>
        /// Internal method used to cancel the file transfer.
        /// </summary>
        private void Abort()
        {
            CurrentState = States.Inactive;
            TerminateSend = true;
            SenderInitialized = false;
            Aborted = true;

            // Detach event handler
            Port.DataReceived -= Port_DataReceived;

            // If we are sending data, tell Sender not to expect any more responses from Receiver.
            // This has no ill effect if we are receiving instead.
            WaitForResponseFromReceiver.Set();

            // Deactivate Send and Receive watchdogs
            if (ReceiverNAKWatchdog != null)
                ReceiverNAKWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            if (ReceiverFileInitiationTimer != null)
                ReceiverFileInitiationTimer.Change(Timeout.Infinite, Timeout.Infinite);

            if (SenderPacketResponseWatchdog != null)
                SenderPacketResponseWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            if (ReceiverStillAliveWatchdog != null)
                ReceiverStillAliveWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            // Flush serial data so they don't contaminate a future session
            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();
        }

        // ************************************* SENDER SENDER SENDER SENDER SENDER SENDER SENDER ***********************************

        private ManualResetEvent WaitForResponseFromReceiver = new ManualResetEvent(false);

        private Timer SenderPacketResponseWatchdog;

        private Timer ReceiverStillAliveWatchdog;

        /// <summary>
        /// This is an array that will be instantiated to the specified data packet size and filled with padding bytes.
        /// The intent is for this array to be copied for each outgoing data packet and populated with data bytes.
        /// This avoids the overhead of having to populate new arrays with padding bytes each time.
        /// </summary>
        private byte[] SenderDataPacketMasterTemplate;

        private byte[] DataPacketToSend;

        private bool TerminateSend = false;

        /// <summary>
        /// Tracks the number of data bytes that have currently been added to the outbound packet.
        /// </summary>
        private int NumUserDataBytesAddedToCurrentPacket = 0;

        /// <summary>
        /// The byte value used to pad a packet in order to meet its 128-byte or 1024-byte required length.
        /// </summary>
        public byte PaddingByte;

        /// <summary>
        /// When using this XModem to send data, the official specification requires that EOT is transmitted to signal the end of file.
        /// However, some programs may require a different byte value, such as EOF instead. This allows the user to specify
        /// a custom byte value to transmit to the Receiver when the file is complete.
        /// </summary>
        public byte EndOfFileByteToSend;

        /// <summary>
        /// Defines a master outbound data packet that will be filled with padding bytes.
        /// The purpose of this template is to be copied for each new outbound packet and subsequently
        /// populated with actual data.
        /// </summary>
        private void DefineDataPacketTemplate()
        {
            int dataPacketSize;
            if (_Variant == Variants.XModem1K)
                dataPacketSize = _Packet1024NominalSize;
            else
                dataPacketSize = _Packet128NominalSize;

            SenderDataPacketMasterTemplate = new byte[dataPacketSize];

            // Fill the template with padding bytes
            for (int k = 0; k < SenderDataPacketMasterTemplate.Length; k++)
                SenderDataPacketMasterTemplate[k] = PaddingByte;
        }

        private byte _BlockNumToSend = 1;
        public byte BlockNumToSend
        {
            get { return _BlockNumToSend; }
        }

        /// <summary>
        /// Initializes the modem send process.
        /// </summary>
        /// <param name="dataToSend">
        /// Optional argument. 
        /// If provided, this is the file that should be transmitted in its entirety.
        /// The EndOfFile byte is automatically transmitted once this array is finished.
        /// If omitted, the file may be sent piece-by-piece using the AddToOutboundPacket() method.
        /// </param>
        /// <returns>
        /// The number of data bytes successfully transmitted.
        /// </returns>
        public int Send(byte[] dataToSend = null)
        {
            // Initialize control variables
            _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
            Aborted = false;
            _BlockNumToSend = 1;    // Current outbound block number
            _SenderConsecutiveRetryAttempts = 0;
            PacketSuccessfullySent = false;
            DataPacketToSend = null;
            NumUserDataBytesAddedToCurrentPacket = 0;
            _TotalUserDataBytesPacketized = 0;
            _TotalUserDataBytesSent = 0;
            _NumCancellationBytesReceived = 0;
            TerminateSend = false;
            EndOfFileAcknowledgementReceived = false;
            SenderInitialized = true;   // Ensures that this method is called first before AddToOutboundPacket()

            WaitForResponseFromReceiver.Reset();
            CurrentState = States.SenderAwaitingFileInitiation;

            // Open port if it isn't open already
            if (Port.IsOpen == false)
                Port.Open();

            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();

            if (ReceiverStillAliveWatchdog == null)
                ReceiverStillAliveWatchdog = new Timer(ReceiverStillAliveWatchdogRoutine, null, SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);
            else
                ReceiverStillAliveWatchdog.Change(SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);

            if (SenderPacketResponseWatchdog == null)
                SenderPacketResponseWatchdog = new Timer(SenderPacketResponseWatchdogRoutine, null, SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);
            else
                SenderPacketResponseWatchdog.Change(SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);

            // Attach event handler
            Port.DataReceived += new SerialDataReceivedEventHandler(Port_DataReceived);

            // Wait here for file initiation byte to be received from Receiver
            WaitForResponseFromReceiver.WaitOne();
            WaitForResponseFromReceiver.Reset();

            if (dataToSend != null && Aborted == false)
            {
                AddToOutboundPacket(dataToSend);
                if (TerminateSend == false)
                    EndFile();

                return _TotalUserDataBytesSent;
            }
            else
                return 0;
        }

        private int _TotalUserDataBytesPacketized = 0;
        /// <summary>
        /// Returns the total number of user data bytes that have been incorporated into outbound packets since the current
        /// send session was started. This counts data bytes that have already been successfully transmitted as well as
        /// those data bytes that have been added to a packet that is still waiting for completion. Header bytes, padding bytes,
        /// checksum bytes (any overhead incurred by the XModem infrastructure) are NOT included. 
        /// 
        /// This allows the user, for example, to calculate the proper file offset if reading an extremely large file
        /// from persistent storage. 
        /// </summary>
        public int TotalUserDataBytesPacketized
        {
            get { return _TotalUserDataBytesPacketized; }
        }

        private int _TotalUserDataBytesSent = 0;
        /// <summary>
        /// Returns the total number of user data bytes that have been successfully transmitted since the current send session
        /// was started. Only data bytes are included in this count. Header bytes, padding bytes, checksum bytes (any overhead 
        /// incurred by the XModem infrastructure) are NOT included. 
        /// 
        /// When the transfer has terminated, this can also tell the user if the file was successfully sent in its entirety.
        /// </summary>
        public int TotalUserDataBytesSent
        {
            get { return _TotalUserDataBytesSent; }
        }

        /// <summary>
        /// Sentinel variable that verifies that Send() method is called before AddToOutboundPacket().
        /// </summary>
        private bool SenderInitialized = false;

        /// <summary>
        /// Assembles one or more packets from the bytes passed to this method and automatically transmits them when the
        /// packet size has been satisfied.
        /// 
        /// The number of bytes supplied by the user during each call may be completely arbitrary. Supplied arguments may be 
        /// extremely short or extremely long. This method automatically parses the supplied byte arrays and builds packets
        /// on the fly. Once enough data has been received for a packet, that packet is automatically transmitted.
        /// 
        /// This method is useful when reading a very large file piece-by-piece from a source, and for packetizing those
        /// pieces automatically.
        /// </summary>
        /// <param name="dataToSend">
        /// The data bytes that should be added to the current pending packet. This array may be any length.
        /// </param>
        /// <returns>
        /// The number of data bytes successfully sent during this particular method call. 
        /// This is 0 if the packet is still too short to send using the data bytes collected thus far. By knowing how many data 
        /// bytes were successfully transmitted during this method invocation, the user can calculate the correct file offset
        /// if the source file is extremely lage and is being read from persistent storage, for example.
        /// 
        /// Note that only the data bytes successfully transmitted during THIS method call are counted. Use the
        /// TotalUserDataBytesSent property to keep track of the accumulated total of all user data bytes sent during the
        /// current send session.
        /// </returns>
        public int AddToOutboundPacket(byte[] dataToSend)
        {
            // Tracks the number of user data bytes that have been successfully sent during this method invocation
            int numUnpaddedDataBytesSentThisCall = 0;

            // Ensure that the Send() method is first called before this method
            if (SenderInitialized == false)
            {
                throw new ArgumentException("The XModem.Send() method must first be called before XModem.AddToOutboundPacket() is used.");
            }

            int dataOffset = 0;
            while (dataOffset < dataToSend.Length && TerminateSend == false)
            {

                // Instantiate outbound data packet if empty
                if (DataPacketToSend == null)
                {
                    if (_Variant == Variants.XModem1K)
                        DataPacketToSend = new byte[_Packet1024NominalSize];
                    else
                        DataPacketToSend = new byte[_Packet128NominalSize];

                    Array.Copy(SenderDataPacketMasterTemplate, DataPacketToSend, DataPacketToSend.Length);
                }

                int numUnparsedDataBytes = dataToSend.Length - dataOffset;
                int numPacketDataBytesNeeded = DataPacketToSend.Length - NumUserDataBytesAddedToCurrentPacket;

                int numBytesToAdd;
                if (numPacketDataBytesNeeded >= numUnparsedDataBytes)
                    numBytesToAdd = numUnparsedDataBytes;
                else
                    numBytesToAdd = numPacketDataBytesNeeded;

                Array.Copy(dataToSend, dataOffset, DataPacketToSend, NumUserDataBytesAddedToCurrentPacket, numBytesToAdd);

                NumUserDataBytesAddedToCurrentPacket += numBytesToAdd;
                dataOffset += numBytesToAdd;
                _TotalUserDataBytesPacketized += numBytesToAdd;

                if (NumUserDataBytesAddedToCurrentPacket >= DataPacketToSend.Length)
                {
                    TransmitPacket();

                    // Determine if packet transmission was successful, or the maximum number of retries has been exhausted:
                    if (PacketSuccessfullySent == true)
                    {
                        // If packet successfully transmitted, keep a running tally of data bytes sent out.
                        // Only count actual user-provided data. Padding bytes are not counted.
                        _TotalUserDataBytesSent += numBytesToAdd;
                        numUnpaddedDataBytesSentThisCall += numBytesToAdd;

                        // Reset control variables
                        NumUserDataBytesAddedToCurrentPacket = 0;

                        // Re-initialize a new packet
                        DataPacketToSend = null;
                    }
                    else if (TerminateSend == false)
                    {
                        // Terminal condition if ACK not received even after multiple attempts
                        Abort();
                        _TerminationReason = TerminationReasonEnum.TooManyRetries;
                        break;
                    }
                }
            }

            return numUnpaddedDataBytesSentThisCall;
        }

        private bool PacketSuccessfullySent = false;

        private int _SenderConsecutiveRetryAttempts = 0;
        public int SenderConsecutiveRetryAttempts
        {
            get { return _SenderConsecutiveRetryAttempts; }
        }

        private void TransmitPacket()
        {
            // Calculate check-value
            byte[] checkValueBytes;
            if (_Variant == Variants.XModemChecksum)
                checkValueBytes = new byte[] { CheckSum(DataPacketToSend) };
            else
            {
                ushort checkValueShort = Crc.Crc16(DataPacketToSend, 0, DataPacketToSend.Length, 0);
                checkValueBytes = UShortToBytes(checkValueShort);
            }

            // Determine packet size header
            byte packetSizeHeader;
            if (_Variant == Variants.XModem1K)
                packetSizeHeader = STX;
            else
                packetSizeHeader = SOH;

            PacketSuccessfullySent = false;
            while (PacketSuccessfullySent == false && _SenderConsecutiveRetryAttempts < MaxSenderRetries && TerminateSend == false)
            {
                // Send packet size header
                Port.WriteByte(packetSizeHeader);

                // Send block number
                Port.WriteByte(_BlockNumToSend);

                // Send block number complement
                Port.WriteByte((byte)(255 - _BlockNumToSend));

                // Send data packet
                Port.Write(DataPacketToSend, 0, DataPacketToSend.Length);

                // Update state. Do this just before the Receiver is expected to respond so its response
                // doesn't fall through the cracks if it replies extremely quickly.
                WaitForResponseFromReceiver.Reset();
                CurrentState = States.SenderPacketSent;

                // Send check-value. This completes the packet. A response from the Receiver is expected after this.
                Port.Write(checkValueBytes, 0, checkValueBytes.Length);

                // Wait for ACK or NAK or CAN
                WaitForResponseFromReceiver.WaitOne();
                WaitForResponseFromReceiver.Reset();

                // Once we've gotten a response, the only message expected from the Receiver at this point is a possible cancellation
                CurrentState = States.SenderAlertForPossibleCancellation;
            }
        }

        /// <summary>
        /// Determines if the minimum number of consecutive cancellation bytes are present in a byte array.
        /// </summary>
        /// <param name="recv">
        /// The byte array to search for consecutive cancellation bytes.
        /// </param>
        /// <returns>
        /// True if cancellation condition has been met.
        /// False if cancellation request is absent.
        /// </returns>
        private bool DetectCancellation(byte[] recv)
        {
            if (NumCancellationBytesRequired > 0)
            {
                int foundIndex = Array.IndexOf(recv, CAN);
                if (foundIndex > -1)
                {
                    for (int indexToCheck = foundIndex; indexToCheck < recv.Length; indexToCheck++)
                    {
                        // If more than 1 CAN byte is required for cancellation, they must be consecutive
                        // for cancellation to occur.
                        if (recv[indexToCheck] == CAN)
                            _NumCancellationBytesReceived += 1;
                        else
                            _NumCancellationBytesReceived = 0;

                        if (_NumCancellationBytesReceived >= NumCancellationBytesRequired)
                        {
                            Abort();
                            _TerminationReason = TerminationReasonEnum.CancelNotificationReceived;
                            return true;    // Cancellation detected
                        }
                    }
                }
                else
                {
                    _NumCancellationBytesReceived = 0;
                }
            }
            return false;   // No cancellation detected
        }

        private bool EndOfFileAcknowledgementReceived = false;

        /// <summary>
        /// Informs the Receiver that the transmitted file is complete.
        /// Any pending packets are sent, followed by the end-of-file byte.
        /// </summary>
        /// <returns>
        /// The number of user data bytes successfully transmitted during this method call.
        /// </returns>
        public int EndFile()
        {
            // Check if there are unsent data bytes remaining in a pending packet, and if so, send the packet
            // containing the unsent bytes
            if (NumUserDataBytesAddedToCurrentPacket > 0)
            {
                TransmitPacket();
                _TotalUserDataBytesSent += NumUserDataBytesAddedToCurrentPacket;
            }

            CurrentState = States.SenderAwaitingEndOfFileConfirmation;

            int numEndOfFileBytesSent = 0;
            while (EndOfFileAcknowledgementReceived == false && numEndOfFileBytesSent <= MaxSenderRetries)
            {
                WaitForResponseFromReceiver.Reset();
                Port.WriteByte(EndOfFileByteToSend);
                numEndOfFileBytesSent += 1;
                WaitForResponseFromReceiver.WaitOne();
            }

            if (EndOfFileAcknowledgementReceived == true)
            {
                Abort();
                _TerminationReason = TerminationReasonEnum.EndOfFile;
                return NumUserDataBytesAddedToCurrentPacket;
            }
            else
            {
                Abort();
                _TerminationReason = TerminationReasonEnum.TooManyRetries;
                return 0;
            }
        }

        private void SenderPacketResponseWatchdogRoutine(object notUsed)
        {
            // <ACK>, <NAK> or <CAN> is expected from the Receiver after each packet sent.
            // If an appropriate response is not received within the expected timeout, release the WaitHandle which is enforcing
            // the wait-for-response. This will make the Sender resend the packet once more (until the retry limit is reached).
            if (CurrentState == States.SenderPacketSent)
                WaitForResponseFromReceiver.Set();
        }

        private void ResetSenderPacketResponseWatchdog()
        {
            if (SenderPacketResponseWatchdog != null)
                SenderPacketResponseWatchdog.Change(SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);
        }

        private void ReceiverStillAliveWatchdogRoutine(object notUsed)
        {
            Abort();
            _TerminationReason = TerminationReasonEnum.NoResponseFromReceiver;
        }

        private void ResetReceiverStillAliveWatchdog()
        {
            if (ReceiverStillAliveWatchdog != null)
                ReceiverStillAliveWatchdog.Change(SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);
        }

        /// <summary>
        /// Removes one or more padding bytes that may exist at the end of a byte array.
        /// </summary>
        /// <param name="input">
        /// The byte array to trim.
        /// </param>
        /// <param name="paddingByteToRemove">
        /// The byte value which defines a padding byte.
        /// If omitted, this defaults to SUB (byte decimal 26).
        /// </param>
        /// <returns>
        /// A byte array without trailing padding bytes (if any are found).
        /// </returns>
        public byte[] TrimPaddingBytesFromEnd(byte[] input, byte paddingByteToRemove = 26)
        {
            int numBytesToDiscard = 0;

            for (int k = input.Length - 1; k >= 0; k--)
            {
                if (input[k] == paddingByteToRemove)
                    numBytesToDiscard += 1;
                else
                    break;
            }

            int numBytesToKeep = input.Length - numBytesToDiscard;

            byte[] output = new byte[numBytesToKeep];
            Array.Copy(input, output, numBytesToKeep);

            return output;
        }

    } // End class

} // End namespace
