// Created with MonoDevelop
//
//    Copyright (C) 2023 Hiperscan GmbH Dresden, Germany
//
//    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 3 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, see <http://www.gnu.org/licenses/>.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Hiperscan.Common.SerialPorts;
using Hiperscan.SGS.Splitter;
using Hiperscan.Unix;

namespace Hiperscan.SGS.SerialInterface.USB
{
    public partial class RaspianCom : ISerialInterface
    {
        private const int NO_TIMEOUT = 0;
        private const int BLOCK_SIZE = 4096;
        private const uint MIN_TIMEOUT = 500;


        public RaspianCom(DeviceInfo device_info, bool ask_for_protocol = false)
        {
            this.DeviceInfo = device_info;
            this.IsAskingForProtocol = ask_for_protocol;
        }

        public void OpenBySerialNumber(string serial)
        {
            this.Port = new ComPort(this.DeviceInfo.SerialPortName, ComPort.PortBaudRate.Baud115200);
            this.Port.DataReceived += this.OnDataReceived;
            this.Port.WriteBlockInterval = SplitterProtocol.BLOCK_INTERVAL;
            this.Port.ReadBlockInterval = 50;
            this.Reset();

            this.Port.StartListening();
            this.SetTimeouts();
        }

        public void Close()
        {
            this.Reset();
            this.Port.Dispose();
        }

        private void Reset()
        {
            this.ResetIsExecuting = true;
            this.Port.StopListening();
            this.ClearBuffer();
            this.SetTimeouts();
            this.ResetIsExecuting = false;
        }

        private void ClearBuffer()
        {
            Thread.Sleep(200);
            lock (this.ReadBufferSync)
            {
                this.Port.Clear();
                this.ReadBuffer.Clear();
            }
        }

        public void ResetDevice()
        {
            this.Reset();
            this.Close();
            Thread.Sleep(1000);
            this.OpenBySerialNumber(this.DeviceInfo.SerialNumber);
        }

        public void SetTimeouts(int read_timeout, int write_timeout)
        {
            this.SendUsbTimeoutsCommand(read_timeout, write_timeout);
        }

        public void SetTimeouts()
        {
            this.SetTimeouts(this.DefaultTimeouts.ReadTimeout, this.DefaultTimeouts.WriteTimeout);
        }

        public void Purge()
        {
        }

        public void SetLatency()
        {

        }

        protected void OnDataReceived(object sender, ComPort.ReceivedArgs e)
        {
            lock(this.ReadBufferSync)
            {
                this.ReadBuffer.AddRange(e.Buffer);
            }
        }

        public uint Read(out byte[] rx_buf, uint size)
        {
            rx_buf = new byte[0];
            int length = (int)size;

            int block_size = length > RaspianCom.BLOCK_SIZE ? RaspianCom.BLOCK_SIZE : length;
            DateTime end = DateTime.Now.AddMilliseconds(this.GetTransferTimeout(length));

            for (; ; )
            {
                if (DateTime.Now > end)
                    break;

                byte[] data = null;
 
                lock (this.ReadBufferSync)
                {
                    int tmp_length = this.ReadBuffer.Count;
                    if (tmp_length > block_size)
                        tmp_length = block_size;

                    if (tmp_length > 0)
                    {
                        data = this.ReadBuffer.GetRange(0, tmp_length).ToArray();
                        this.ReadBuffer.RemoveRange(0, tmp_length);
                    }
                }

                if (data == null)
                    continue;

                rx_buf = rx_buf.Concat(data).ToArray();
                length = length - data.Length;

                if (length == 0)
                    break;

                block_size = length > RaspianCom.BLOCK_SIZE ? RaspianCom.BLOCK_SIZE : length;
            }

            return (uint)rx_buf.Length;
        }

        public uint Read(out byte[] data)
        {
            List<byte> rx_buf = new List<byte>();

            while (true)
            {
                byte[] buf = null;

                this.Read(out buf, RaspianCom.BLOCK_SIZE);

                if (buf?.Length > 0)
                    rx_buf.AddRange(buf);
                else
                    break;
            }

            data = rx_buf.ToArray();
            return (uint)rx_buf.Count;
        }

        public byte[] ReadBlocking(uint size, int timeout, IdleTaskHandler idle_task)
        {
            List<byte[]> buffers = new List<byte[]>();
            DateTime start = DateTime.Now;

            int length = (int)size;
            timeout += this.GetTransferTimeout(length) + SplitterProtocol.BLOCK_INTERVAL;

            for (; ; )
            {
                int  number_of_recieved_bytes = (int)this.Read(out byte[] recieved_buffer, (uint)length); 

                if (number_of_recieved_bytes > 0)
                {
                    Console.WriteLine("ReadBlocking: {0}/{1}", number_of_recieved_bytes, length);
                    buffers.Add(recieved_buffer);

                    length = length - number_of_recieved_bytes;

                    if(length == 0)
                        break;
                }

                TimeSpan span = DateTime.Now - start;

                if (span.TotalMilliseconds > timeout)
                {
                    if (timeout > 0)
                    {
                        Console.WriteLine("Timeout while ReadBlocking: {0}/{1}", number_of_recieved_bytes, size - number_of_recieved_bytes);
                        foreach (byte[] buffer in buffers)
                        {
                            Console.Write("\talready read:");
                            foreach (byte b in buffer)
                            {
                                Console.Write(" 0x{0:X2}", b);
                            }
                            Console.WriteLine();
                        }

                        throw new TimeoutException(Catalog.GetString("Timeout while blocking read operation."));
                    }
                    break;
                }

                System.Threading.Thread.Sleep(10);
                idle_task?.Invoke();
            }

            return buffers.SelectMany(b => b).ToArray();
        }

        public uint Write(byte[] data)
        {
            //lock (this.ReadBufferSync)
            //   this.ReadBuffer.Clear();
            int length = this.Port.Write(data);
            return (uint)length;
        }

        private int GetTransferTimeout(int number_of_bytes)
        {
            double time_per_byte = 1 / (this.Port.BaudRate / 8);
            double margin_factor = 2;

            int time_out = (int)(time_per_byte * margin_factor);
            if (time_out < RaspianCom.MIN_TIMEOUT)
                time_out = (int)RaspianCom.MIN_TIMEOUT;

            return time_out;
        }

        public DriverType DriverType
        {
            get
            {
                switch (Environment.OSVersion.Platform)
                {

                    case PlatformID.Win32NT:
                    case PlatformID.Win32S:
                    case PlatformID.Win32Windows:
                    case PlatformID.WinCE:
                        return DriverType.WinUSB;
                    default:
                        return DriverType.LibUSB;

                }
            }
        }

        private DeviceInfo DeviceInfo { get; set; }
        private ComPort Port { get; set; }

        private LibUsbDevice.Config DefaultTimeouts { get; set; } = new LibUsbDevice.Config();
        private List<byte> ReadBuffer { get; set; } = new List<byte>();

        private object ReadBufferSync { get; set; } = new object();
    }
}
