using System;
using System.IO;
using System.IO.Ports;
using System.Text;
using Microsoft.SPOT;
//using Microsoft.SPOT.IO;

//using GHI.Utilities;
//using GHI.IO;

namespace Zmodem
{
    public class ZmodemSender : Zmodem
    {
        //Constants
        private const int ZRQ_MAX = 10;
        private const int ZRQ_TIMEOUT = 2;
        private static TimeSpan tsZRQTimeout = new TimeSpan(0, 0, ZRQ_TIMEOUT);

        //Variables
        private string fileName;
        private SerialPort sPort;
        private bool inTransfer;
        private bool allDone;
        private TimeSpan tsStart;


        private int zrqinits_sent;
        private byte[] txbuff;
        private byte[] rxbuff;
        private int count;
        private UInt16 MAX_BLOCK = 8192;
        private UInt16 MAX_TXFRAME = 1024;

        private bool Wantfcs32; /*we want to send 32bit fcs*/
        private UInt16 Rxbuflen;  /* Receiver's max buffer length (from ZRINIT) */
        private UInt16 Txwindow;  /* Control the size of the transmitted windows */
        private UInt16 Tframlen;
        //private UInt16 Txwspac;   /*spacing between zcrcq requests */

        private UInt16 blklen; /*subpacket block lenghth*/
        private UInt16 blkopt; /*override value for zmodem blklen */


        //Transfer settings
        private byte Lzconv; /* Local ZMODEM file conversion request */
        private byte Lzmanag; /* Loval ZMODEM file management request */
        private byte Lztrans;
        private byte Lskipnocor;

        
        //Constructor
        /// <summary>
        /// Constructor in which is specified the serialport object to operate on, and the filename.
        /// </summary>
        /// <param name="port">SerialPort Object reference, port is assumed to be open</param>
        /// <param name="filename">Absolute path filename or director for multiple file transfer</param>
        /// <example>
        /// This sample shows you how to implement the class for basic use. 
        /// <code>
        /// SerialPort sp = new SerialPort("COM1", 115200, Parity.None, 8, StopBits.One);
        /// ZmodemSender zs = new ZmodemSender(sp, "\\SD\\sample.txt");
        /// while (zs.doTasks()) {
        ///     //Do other things here. We only return here between major transfer 
        ///     //stage steps.
        /// }
        /// </code>
        /// </example>
        public ZmodemSender(SerialPort port, string filename)
        {
            this.sPort = port;
            this.fileName = filename;
            this.inTransfer = false;
            this.allDone = false;
            this.zrqinits_sent = 0;
            this.txbuff = new byte[MAX_BLOCK];//MAX_BLOCK

            this.rxbuff = new byte[128];
            this.count = 0;
            this.tsStart = DateTime.Now.TimeOfDay;
            this.Rxbuflen = 16384;
            this.Tframlen = MAX_TXFRAME;
            this.blklen = 128;
            this.blkopt = 0;
            this.Wantfcs32 = true;

            //init file xfer settings
            this.Lzconv = 0;
            this.Lzmanag = 0;
            this.Lztrans = 0;
            this.Lskipnocor = 0;

            //init the static Class

            Init(false);

        }

        /// <summary>
        /// Run a repeat task for zmodem transmission
        /// </summary>
        /// <returns>
        /// Status of the loop:
        /// 0 = OK
        /// 1 = DONE, SUCCESS
        /// -1 = FAILURE IN TRANSMISSION
        /// -2 = FILE DOES NOT EXIST
        /// </returns>
        public int doTasks()
        {

            //first read port
            //read();

            //

            if (!inTransfer & zrqinits_sent < ZRQ_MAX) {

                //Guess we should send another zrqinit, it will check if its time. 

                if (sendZRQInit() > 0) {

                    //Wait for a reply
                    if (getZRInit()) {
                        Debug.Print("Got ZRINIT Message.");
                        inTransfer = true;
                        // optional response
                        byte[] myattn = Encoding.UTF8.GetBytes("0");
                        if (sendZSInit(ref myattn, (UInt16)myattn.Length) == OK) {
                        inTransfer = true;
                        Debug.Print("Got ACK to our ZSINIT");
                        }
                    }

                }
            }

            if (zrqinits_sent >= ZRQ_MAX) {
                Debug.Print("Too Many Attempts, quitting.");
                return -1;
            }

            if (inTransfer && !allDone) {
                //figure out what file we're on....
                //send file info

                if (!File.Exists(fileName)) {
                    Debug.Print("File Does Not Exist, exiting");
                    return -2;
                }


                FileInfo fi = new FileInfo(fileName);

                if (sendZFile(fi) == OK) {
                    //Transfer successful.
                    allDone = true;

                }
                else { zrqinits_sent++; }
            }

            if (allDone) {
                if (sendZFIN() == OK) {
                    Debug.Print("OVER AND OUT");
                    return 1;
                }
                else { zrqinits_sent++; }
            }


            return 0;

        }

        private int sendZRQInit()
        {
            TimeSpan tsNow = DateTime.Now.TimeOfDay;

            //test if its time
            if ((tsNow - tsStart) < tsZRQTimeout)
                return -1;
            else
                tsStart = tsNow;

            Debug.Print("Sending ZRQINIT...");

            zshhdr(sPort, ZRQINIT, ref Txhdr);


            zrqinits_sent++;

            if (zrqinits_sent == ZRQ_MAX)
                Debug.Print("Too Many ZRQinits issued, aborting zmodem transfer");

            return 1;

        }


        int Rxflags = 0;
        int Rxflags2 = 0;
        private bool rxBuffIsSet = false;
        private bool getZRInit()
        {
            UInt32 rxpos = 0;

            switch (zgethdr(sPort, ref Rxhdr, 0, ref rxpos)) {
                case ZCHALLENGE: /* Echo receiver's challenge number */
                    stohdr(rxpos);
                    zshhdr(sPort, ZACK, ref Txhdr);

                    return false;
                case ZCOMMAND:
                    return false;
                case ZRINIT: /*this is what we want*/
                    Rxflags = Rxhdr[ZF0];
                    Rxflags2 = Rxhdr[ZF1];

                    Txfcs32 = Wantfcs32 && ((Rxflags & CANFC32) == CANFC32);
                    bool oldZctl = Zctlesc;
                    Zctlesc = (Rxflags & TESCCTL) == TESCCTL;

                    /* update table, we initialized with a different escape method */
                    if (Zctlesc != oldZctl)
                        zsendline_init();

                    /* Receive buffer size flags */
                    Rxbuflen = (UInt16)(Rxhdr[ZP0] + (Rxhdr[ZP1] << 8));

                    if (Rxbuflen > 0) rxBuffIsSet = true;

                    /* Full Duplex */
                    if ((Rxflags & CANFDX) != CANFDX) //cannot fdx
                        Txwindow = 0;


                    if (Rxbuflen < 32 || Rxbuflen > MAX_BLOCK) {
                        Rxbuflen = MAX_BLOCK;
                    }

                    if (Rxbuflen > Tframlen)
                        Rxbuflen = Tframlen;
                    if (Rxbuflen == 0)
                        Rxbuflen = MAX_TXFRAME;

                    Debug.Print("getZRinit(): Rxbuflen= " + Rxbuflen.ToString());

                    if (blklen < 1024) {
                        if (sPort.BaudRate > 300)
                            blklen = 256;
                        if (sPort.BaudRate > 1200)
                            blklen = 512;
                        if (sPort.BaudRate > 2400)
                            blklen = 1024;
                    }

                    if (blklen > Rxbuflen)
                        blklen = Rxbuflen;
                    if (blkopt > 0 && blklen > blkopt)
                        blklen = blkopt;

                    Debug.Print("getZRinit(): blklen= " + blklen.ToString());
                    Debug.Print("getZRinit(): Txwindow=" + Txwindow.ToString());

                    return true;

                case ZCAN:
                case TIMEOUT:
                case ZRQINIT: /* we got an echo back*/
                    return false;
                default: /*something totally scrobbled came in */
                    zshhdr(sPort, ZNAK, ref Txhdr);
                    return false;

            }

        }
        private int errors;
        private int sendZSInit(ref byte[] fname, UInt16 fname_len)
        {
            int c;
            UInt32 rxpos = 0;
            errors = 0;

            for (; ; ) {
                stohdr(0); // reset the flag header
                if (Zctlesc) {
                    Txhdr[ZF0] |= TESCCTL;
                    zshhdr(sPort, ZSINIT, ref Txhdr);
                }
                else {
                    zsbhdr(sPort, ZSINIT, ref Txhdr);

                }

                //Send data subpacket
                ZSDATA(ref fname, fname_len, ZCRCW);

                c = zgethdr(sPort, ref Rxhdr, 0, ref rxpos);
                switch (c) {
                    case ZCAN:
                        Debug.Print("sendZSInit(): GOT ZCAN");
                        return ERROR;
                    case ZACK:
                        Debug.Print("sendZSInit(): GOT ZACK");
                        return OK;
                    case ZRINIT:
                        Debug.Print("sendZSInit(): GOT ZRINIT");
                        return ERROR;
                    default:
                        Debug.Print("sendZSInit(): IN DEFAULT" + c.ToString());

                        if (++errors > 5) {
                            Debug.Print("sendZSInit(): GOT TOO MANY ERROR");
                            return ERROR;
                        }
                        continue;

                }

            }
        }

        private void ZSDATA(ref byte[] buff, UInt16 length, byte frameend)
        {
            if (Txfcs32) {
                zsda32(sPort, ref buff, length, frameend);
            }
            else {
                zsdata(sPort, ref buff, length, frameend);
            }

        }

        private string makeOctalString(UInt32 num)
        {
            StringBuilder sb = new StringBuilder();
            UInt32 mod = 0;
            while (num > 0) {
                mod = num % 8;
                sb.Insert(0, mod.ToString(), 1);
                num /= 8;

            }
            return (sb.ToString());

        }

        private DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
        private int sendZFile(FileInfo fi)
        {
            int c;
            UInt32 rxpos = 0;
            StringBuilder sb = new StringBuilder();
            StringBuilder sbdata = new StringBuilder();
            UInt32 unixTimeStamp = (UInt32)(fi.LastWriteTimeUtc.Subtract(epoch).Ticks / TimeSpan.TicksPerSecond);
            /*
             * we are going to send a Zfile 
             */

            for (; ; ) {
                stohdr(0);
                Txhdr[ZF0] = Lzconv;
                Txhdr[ZF1] = Lzmanag;
                if (Lskipnocor > 0)
                    Txhdr[ZF1] |= ZF1_ZMSKNOLOC;
                Txhdr[ZF2] = Lztrans;
                Txhdr[ZF3] = 0;

                // Send binary header
                zsbhdr(sPort, ZFILE, ref Txhdr);


                //Assemble File info
                sb.Append(fi.Name+ "\0\0");
                
                /*sb.Append('\0');
                sb.Append(" ");
                sb.Append(fi.Length.ToString() + " ");
                sb.Append(makeOctalString(unixTimeStamp) + " ");//seconds from epoch mod date octal rep
                sb.Append("0 ");//file mode
                sb.Append("0 ");// Serial number
                sb.Append("1 ");//number of files left
                sb.Append(fi.Length.ToString());
                sb.Append(" \0");
                */
                
                //Copy into the buffer
                byte[] fdata = new byte[sb.Length + 2];
                Array.Copy(Encoding.UTF8.GetBytes(sb.ToString()), fdata, Encoding.UTF8.GetBytes(sb.ToString()).Length);

                
                Debug.Print("Sending ZDATA with string: " + sb.ToString());

                if (fdata.Length > 1024) {
                    //Filename is too long, close it down. 
                    Debug.Print("sendZFile(): File string is too long. Aborting");
                    return ERROR;
                }





                //Transmit file info
                ZSDATA(ref fdata, (ushort)fdata.Length, ZCRCW);
            again:
                c = zgethdr(sPort, ref Rxhdr, 0, ref rxpos);

                switch (c) {
                    case ZRINIT://this is what we get when we finish the file...
                        Debug.Print("sendZFile(): got ZRINIT time to move on.");
                        try { 
                            if (fs != null) 
                                fs.Close();
                        }
                        catch (IOException e) {
                            Debug.Print("Problem Closing file after transfer. " + e.ToString());
                        }
                        fsOpen = false;
                        return OK;
                    default: continue;

                    case ZRQINIT:
                        Debug.Print("sendZFile(): got ZRQINIT remote site is sender!");
                        return ERROR;
                    case ZCAN:
                        Debug.Print("sendZFile(): got ZCAN");
                        return ERROR;
                    case TIMEOUT:
                        Debug.Print("sendZFile(): got TIMEOUT");
                        return ERROR;
                    case ZABORT:
                        Debug.Print("sendZFile(): got ABORT");
                        return ERROR;
                    case ZFIN:
                        Debug.Print("sendZFile(): got ZFIN");
                        return ERROR;
                    case ZCRC:
                        Debug.Print("sendZFile(): got ZCRC");
                        /*
                         * The receiver has a file with the same name and length, and they
                         * responded with a ZCRC header with a a byte count, which requires
                         * us, the sender, to perform a 32bit crc on the number of btes in 
                         * the file and transmit complement of the CRC in an answering the 
                         * ZCRC header. 
                         */
                        goto again;
                    case ZSKIP:
                        Debug.Print("sendZFile(): got ZSKIP");
                        return OK;
                    case ZACK:
                        Debug.Print("sendZFile(): got ZACK " + c.ToString() + " with offset " + rxpos.ToString());
                        if (sendFData(fi, rxpos, ZACK) == OK)
                            goto again;
                        else
                            return ERROR;

                    case ZRPOS://this is what we need to get started
                        Debug.Print("sendZFile(): got ZRPOS " + c.ToString() + " with offset " + rxpos.ToString());
                        if (sendFData(fi, rxpos, ZRPOS) == OK)
                            goto again;
                        else
                            return ERROR;

                }

            }

        }

        private FileStream fs;
        private bool fsOpen = false;
        private UInt32 bytes_sent;

        private int sendFData(FileInfo fi, UInt32 rxpos, byte lastHdr)
        {
            int n;

            if (!fsOpen) {
                try {
                    fs = File.OpenRead(fi.FullName);
                }
                catch (Exception e) {
                    fsOpen = false;
                    Debug.Print(e.ToString());
                    return ERROR;
                }
                fsOpen = true;
                bytes_sent = 0;
            }

            fs.Seek((long)rxpos, SeekOrigin.Begin);

            if (rxpos != bytes_sent) {// looks like we missed a step

                Debug.Print("sendFData(): Jumping (missed pckt or resume)");
                bytes_sent = rxpos;
            }

            //write the block to the txbuff
            n = fs.Read(txbuff, 0, Rxbuflen);

            if (n == 0) { //
                Debug.Print("N bytes returned.. is this an empty file?");
                //return ERROR;
            }

            // determine terminating character
            byte endl;

            if (n < Rxbuflen) { //eof encountered
                endl = ZCRCE;
                Debug.Print("End of file seen");
            }
            else if (((Rxflags & CANOVIO) != CANOVIO) || rxBuffIsSet)
                endl = ZCRCW;
            else if ((Rxflags & CANFDX) == CANFDX)
                endl = ZCRCQ;
            else
                endl = ZCRCG;

            //Debug.Print("Line Ending is: " + endl.ToString());

            // We are in a streaming mode don't resend header
            if (!((endl == ZCRCQ) && (lastHdr == ZACK))) {
                stohdr(bytes_sent);
                zsbhdr(sPort, ZDATA, ref Txhdr);
            }

            ZSDATA(ref txbuff, (ushort)n, endl);
            bytes_sent += (UInt16)n;

            if (endl == ZCRCE) {
                stohdr(bytes_sent);
                zsbhdr(sPort, ZEOF, ref Txhdr);
            }
            return OK;
        }

        private int sendZFIN()
        {
            stohdr(0);
            zshhdr(sPort, ZFIN, ref Txhdr);
            UInt32 rxpos = 0;
            switch (zgethdr(sPort, ref Rxhdr, 0, ref rxpos)) {
                case ZFIN: /* This is what we want */
                    sPort.WriteByte(0x4F); //O
                    sPort.WriteByte(0x4F); //O
                    return OK;
                case ZCAN:
                    Debug.Print("sendZFIN(): got zcan instead of zfin");
                    goto default;
                case TIMEOUT:
                    Debug.Print("sendZFIN(): got TIMEOUT instead of zfin");
                    goto default;
                default:
                    return ERROR;
            }

        }

        ~ZmodemSender()
        {
            try {
                fs.Close();
            }
            catch (Exception e) {
                Debug.Print("Closing object exception in File Close.");
            }
        }


        //public int zsendfile(FileInfo zm_fileinfo, byte[] buf, UInt16 blen) 
        //{
        //}

        //public int getnak(){
        //}

        //public int wtxpn(FileInfo zm_fileinfo) {
        //}

        //public int wcs(string oname, string remotename) {
        //}

        //public int zfilbuff(FileInfo zm_fileinfo) {
        //}

        //public int filbuf(byte[] buf, UInt16 count) {
        //}

        //public int getzrxinit (void) {
        //}

        //public int calc_blklen (long int total_sent) {
        //}

        //public int sendzsinit (void) {
        //}

        //public int wctx(FileInfo zm_fileinfo) {
        //}

        //public int zsendfdata(FileInfo zm_fileinfo) {
        //}

        //public int getinsync (FileInfo zm_fileinfo) {
        //}

        //public void 

    }
}


