/*
* High Speed modem to transfer data in a 2,7kHz SSB channel
* =========================================================
* Author: DJ0ABR
*
*   (c) DJ0ABR
*   www.dj0abr.de
*
*   This program is free software; you can redistribute it and/or modify
*   it under the terms of the GNU General Public License as published by
*   the Free Software Foundation; either version 2 of the License, or
*   (at your option) any later version.
*
*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*   GNU General Public License for more details.
*
*   You should have received a copy of the GNU General Public License
*   along with this program; if not, write to the Free Software
*   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* 
* File restauration process
* =========================
* 1) file header is received, get filename and file ID (which is the CRC16 over the complete file contents)
* 2) is this file already existing ? yes-> 3)
*    this file does not exist -> 4)
* 
* 3) file with this name and ID exists is already, cancel reception, show file
* 
* 4) file doews not exist, receive the blocks
* 5) a block is missing -> find a previous block-file with this name+ID. If exists, take the block from this file
* 6) reception complete, the files is OK -> save the file, delete a block file
* 7) reception complete, the files is incomplete -> save the block file for late use
* 
* Filenames:
* ----------
* transmitter:
* the sender takes any filename, but the ID is the C&C16 over the file contents (to identify different files with the same name)
* 
* receiver:
* use the filename as transmitted for the good original file
* the blockfile's filename is the ID in Ascii-Hex representation, i.e.: ID= 0x45 0xfe ... filename is: 45FE.blk
*/

using System;
using System.Drawing;
using System.IO;

namespace oscardata
{
    class receivefile
    {
        int rxtype;
        int rxfrmnum;
        int minfo;
        int rxstat;
        int speed;
        int maxlevel;
        int dummy4;
        int dummy5;
        Byte[] rxdata = new byte[statics.PayloadLen];

        // file buffer, we have max 2^10=1024 blocks with 219 bytes each = 224.256kB
        Byte[,] blockbuf = new byte[1024, statics.PayloadLen];
        bool[] blockvalid = new bool[1024];
        int blockidx;
        Byte[] firstblock;

        bool[] lastblockvalid = new bool[1024];
        int lastblockidx;

        bool receiving = false;
        public String filename = null;
        public String StatusText = "";
        public long filesize = 0;
        public Bitmap pbmp = null;
        DateTime starttime;
        public TimeSpan runtime;
        public int rxbytes = 0;
        bool autoRXnum = false;
        String blockFilename = "";

        //  2 ... file OK, file already exist
        //  1 ... file OK
        //  0 ... continue receiving
        // -1 ... invalid header
        // -2 ... first frame invalid
        // -3 ... invalid data in cont frame
        // -4 ... invalid data in last frame
        // -5 ... file ends, but bad blocks

        public int receive(Byte []rxdp)
        {
            // read frame header
            if(!getFrameHeader(rxdp))
            {
                // invalid situation
                blockidx = 0;
                receiving = false;
                return -1;
            }

            // receive first frame of a transmission
            if (minfo == statics.FirstFrame || minfo == statics.SingleFrame)
            {
                receiving = false;
                starttime = DateTime.UtcNow;
                rxbytes = 0;
                filesize = 0;
                filename = "";
                if (!StartFileRX()) return -2;   // invalid file
            }

            if (receiving && minfo != statics.FirstFrame)
                runtime = DateTime.UtcNow - starttime;

            // receive continous frames of a transmission
            if (minfo == statics.NextFrame)
            {
                // there are more frames for this file
                if(!FileRX())
                {
                    // invalid situation
                    blockidx = 0;
                    receiving = false;
                    return -3;
                }
            }

            rxbytes += statics.PayloadLen;

            // receive last file of a transmission
            if (minfo == statics.LastFrame || minfo == statics.SingleFrame)
            {
                if (!FileRX())
                {
                    // invalid situation
                    blockidx = 0;
                    receiving = false;
                    return -4;
                }

                // the last block was received ok
                // save file if all blocks valid
                bool fileisok = SaveFile();
                if(fileisok == false)
                {
                    // file not complete, do not to unzip
                    return -5;
                }
                blockidx = 0;
                receiving = false;

                if (rxtype == statics.AsciiFile || rxtype == statics.HTMLFile || rxtype == statics.BinaryFile)
                {
                    // these file type must be unzipped
                    int r = 1;
                    if (handleZIPfiles() == 1) r = 2;
                    receiving = false;
                    return r;
                }
            }

            if (rxtype == statics.Image)
            {
                // build bitmap from received data
                pbmp = buildBitmap();
                return 1;
            }

            return 0;
        }

        bool getFrameHeader(Byte[] rxd)
        {
            rxtype = rxd[0];
            rxfrmnum = rxd[1];
            rxfrmnum <<= 8;
            rxfrmnum += rxd[2];
            minfo = rxd[3];
            rxstat = rxd[4];
            speed = rxd[5];
            speed <<= 8;
            speed += rxd[6];
            maxlevel = rxd[7];
            dummy4 = rxd[8];
            dummy5 = rxd[9];

            if (rxfrmnum >= 1024) return false;
            if (!autoRXnum)
                blockidx = rxfrmnum;

            //Console.WriteLine("minfo:" + minfo + " framenum:" + rxfrmnum + " blockidx:" + blockidx);

            Array.Copy(rxd, 10, rxdata, 0, rxd.Length - 10);
            return true;
        }

        bool StartFileRX()
        {
            if (receiving) return true; // already open

            //Console.WriteLine("first block");

            // store first block
            filename = null;
            if (rxfrmnum != 0)
            {
                Console.WriteLine("blockidx auto increment");
                autoRXnum = true;    // for old compatibility, increment blockidx by ourself
            }
            blockidx = 0;
            if (rxdata.Length > statics.PayloadLen)
            {
                Console.WriteLine("wrong payload size: " + rxdata.Length + " expected:" + statics.PayloadLen);
                return false; // wrong size
            }

            // read file header
            ArraySend.rxFilename = "";
            firstblock = ArraySend.GetAndRemoveHeader(rxdata);
            if (firstblock == null)
            {
                Console.WriteLine("invalid File header");
                return false;   // cannot read header, file is corrupted, ignore
            }

            // get block filename for this file
            blockFilename = idToFilename(ArraySend.FileID);

            // init valid blocks table
            for(int i=0; i<blockvalid.Length; i++)
                blockvalid[i] = false;

            // insert previously received blocks
            readBlocks();

            // insert first block
            for (int i = 0; i < rxdata.Length; i++)
                blockbuf[blockidx, i] = rxdata[i]; // contains file info header and file data

            // mark first block as valid
            blockvalid[blockidx] = true;
            
            receiving = true;

            return true;
        }

        String idToFilename(int id)
        {
            char hexToChar(Byte h)
            {
                if (h >= 0 && h <= 9) return (char)(0x30 + h);
                return (char)(0x41 + h - 10);
            }

            Byte b0 = (Byte)((id >> 12) & 0x0f);
            Byte b1 = (Byte)((id >> 8) & 0x0f);
            Byte b2 = (Byte)((id >> 4) & 0x0f);
            Byte b3 = (Byte)(id & 0x0f);

            String s = "";
            s += hexToChar(b0);
            s += hexToChar(b1);
            s += hexToChar(b2);
            s += hexToChar(b3);
            s += ".blk";

            return statics.getHomePath("blocks", s);
        }

        public bool blockstat(int[] result)
        {
            // return if no blocks received
            if (blockidx == 0) return false;

            // count the number of good blocks
            int ok = 0;
            for (int i = 0; i <= blockidx; i++)
            {
                if (blockvalid[i]) ok++;
            }
            result[0] = blockidx+1; // +1 because we start with block 0
            result[1] = ok;

            // store in lastblockvalid for later evaluation for status display on the screen
            Array.Copy(blockvalid, lastblockvalid, blockvalid.Length);
            lastblockidx = blockidx;

            return true;
        }

        public void oldblockinfo(int[] result)
        {
            int ok = 0;
            for (int i = 0; i <= lastblockidx; i++)
                if (lastblockvalid[i]) ok++;
            result[0] = lastblockidx + 1; // +1 because we start with block 0
            result[1] = ok;
        }

        public String missingBlockString()
        {
            String s = "";
            bool f = true;
            for (int i = 0; i <= lastblockidx; i++)
            {
                if (!lastblockvalid[i])
                {
                    if (!f)
                        s += ", ";
                    else
                        f = false;

                    s += i.ToString();
                }
            }

            return s;
        }

        void saveBlocks()
        {
            try
            {
                using (BinaryWriter writer = new BinaryWriter(File.Open(blockFilename, FileMode.Create)))
                {
                    for (int i = 0; i <= blockidx; i++)
                    {
                        // first byte of a block: valid/invalid block
                        Byte[] blk = new byte[statics.PayloadLen + 1];
                        for (int j = 0; j < statics.PayloadLen; j++)
                            blk[j + 1] = blockbuf[i, j];

                        blk[0] = blockvalid[i] ? (Byte)1 : (Byte)0;

                        writer.Write(blk);
                    }
                    writer.Close();
                }
            }
            catch { }
        }

        void readBlocks()
        {
            Byte[] blk = new byte[statics.PayloadLen + 1];
            int idx = 0;

            try
            {
                using (BinaryReader reader = new BinaryReader(File.Open(blockFilename, FileMode.Open)))
                {
                    while (true)
                    {
                        int ret = reader.Read(blk, 0, statics.PayloadLen + 1);
                        if (ret != statics.PayloadLen + 1) break;

                        for (int j = 0; j < statics.PayloadLen; j++)
                        {
                            if (blk[0] == 1)
                            {
                                blockbuf[idx, j] = blk[j + 1];
                                blockvalid[idx] = true;
                            }
                        }
                        idx++;
                    }
                    reader.Close();
                }
            }
            catch { }
        }

        bool FileRX()
        {
            if (!receiving)
            {
                //Console.WriteLine("next/last block: not receiving, first block missing?");
                return false;
            }

            //Console.WriteLine("next/last block");

            // store next block
            if (rxdata.Length != statics.PayloadLen)
            {
                Console.WriteLine("wrong payload size: " + rxdata.Length + " expected:" + statics.PayloadLen);
                return false; // wrong size
            }

            if (autoRXnum)
            {
                // for compatibility with old implementation which did not send a block number
                blockidx++;
                Console.WriteLine("blockidx: do auto increment " + blockidx);
            }

            // insert the received payload into the block buffer and set this block's status to "valid"
            for (int i = 0; i < rxdata.Length; i++)
                blockbuf[blockidx, i] = rxdata[i];

            blockvalid[blockidx] = true;

            return true;
        }

        // build full path+filename from received filename
        String makeRXfilename()
        {
            String fn = "";
            // remove possible path from filename
            String fname = ArraySend.rxFilename;
            int idx = fname.IndexOfAny(new char[] { '\\', '/' });
            if (idx != -1)
            {
                try
                {
                    fname = fname.Substring(idx + 1);
                }
                catch { }
            }
            if (rxtype == statics.Image)
                fn = statics.getHomePath(statics.RXimageStorage, fname);
            else
                fn = statics.getHomePath("", fname);
            return fn;
        }

        bool SaveFile()
        {
            //Console.WriteLine("save file");

            // check if all blocks ok
            for (int i = 0; i <= blockidx; i++)
            {
                if (blockvalid[i] == false)
                {
                    Console.WriteLine("save file: not all blocks ok");
                    // save block file
                    saveBlocks();
                    return false;
                }
            }

            // reception was ok, blockfile no more needed, delete it
            // file is OK, blockfile no more needed, delete it
            try { statics.FileDelete(blockFilename); } catch { }

            // make filename
            filename = makeRXfilename();

            //Console.WriteLine("save at " + filename);

            try
            {
                using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Create)))
                {
                    for (int i = 0; i <= blockidx; i++)
                    {
                        if (i == 0)
                            writer.Write(firstblock);
                        else
                        {
                            Byte[] blk = new byte[statics.PayloadLen];
                            for (int j = 0; j < statics.PayloadLen; j++)
                                blk[j] = blockbuf[i, j];

                            writer.Write(blk);
                        }
                    }
                    writer.Close();
                }
            }
            catch
            {
            }

            return true;
        }

        Bitmap buildBitmap()
        {
            Bitmap bmp = null;

            using (MemoryStream memStream = new MemoryStream())
            {
                for (int i = 0; i <= blockidx; i++)
                {
                    if (i == 0)
                        memStream.Write(firstblock, 0, firstblock.Length);
                    else
                    {
                        Byte[] blk = new byte[statics.PayloadLen];
                        for (int j = 0; j < statics.PayloadLen; j++)
                            blk[j] = blockbuf[i, j];

                        memStream.Write(blk, 0, blk.Length);
                    }
                }
                try
                {
                    // first assign it to an "Image", this checks if the image is valid
                    Image img = Image.FromStream(memStream);
                    bmp = new Bitmap(img);
                }
                catch
                {
                    return null;
                }
            }

            return bmp;
        }

        // returns:
        // -1 ... error
        // 0 ... ok
        // 1 ... ok, but file exists already
        int handleZIPfiles()
        {
            int ret = -1;

            if (filename == null)
            {
                Console.WriteLine("handleZIPfile: no filename");
                return ret;
            }

            try
            {
                // filename has the received data, but maybe too long (multiple of payload length)
                // reduce for the real file length
                Byte[] fc = File.ReadAllBytes(filename);
                Byte[] fdst = new byte[ArraySend.FileSize];
                if (fc.Length < ArraySend.FileSize)
                {
                    Console.WriteLine("file not complete: got len=" + fc.Length + " expected len=" + ArraySend.FileSize);
                    return ret;
                }
                Array.Copy(fc, 0, fdst, 0, ArraySend.FileSize);
                File.WriteAllBytes(statics.zip_RXtempfilename, fdst);   // the received file (still zipped) is here

                // unzip received data and store result in file: unzipped_RXtempfilename
                ZipStorer zs = new ZipStorer();
                String fl = zs.unzipFile(statics.zip_RXtempfilename);
                if (fl != null)
                {
                    // save file
                    ret = 0;
                    // fl is the filename of the file inside the zip file, so the originally zipped file
                    // remove path to get just the filename
                    int idx = fl.LastIndexOf('/');
                    if (idx == -1) idx = fl.LastIndexOf('\\');
                    String fdest = fl.Substring(idx + 1);
                    fdest = statics.getHomePath("", fdest);
                    // fdest is the file in the oscardata's user home directory

                    // check if file already exists
                    if (File.Exists(fdest))
                    {
                        ret = 1;
                        // remove old file with same name
                        try { statics.FileDelete(fdest); } catch { }
                    }

                    // move the unzipped file to the final location
                    File.Move(fl, fdest);
                    filesize = statics.GetFileSize(fdest);
                    StatusText = "unzip OK";
                    sendBulletin(fdest);
                }
                else
                    StatusText = "unzip failed";

                statics.FileDelete(statics.zip_RXtempfilename);
            }
            catch
            {
                StatusText = "unzip failed";
            }

            return ret;
        }

        // send a bulletin file to hsmodem
        // which sends it via websocket to a browser
        void sendBulletin(String fn)
        {
            if (Path.GetExtension(fn) != ".blt") return;    // no bulletin, ignore

            String s = File.ReadAllText(fn);
            Byte[] tarr = statics.StringToByteArrayUtf8(s);

            Byte[] tsend = new byte[tarr.Length + 1];

            tsend[0] = statics.BulletinFile;
            for (int i = 0; i < tarr.Length; i++)
                tsend[i + 1] = tarr[i];

            Udp.UdpSendCtrl(tsend);
        }
    }
}
