// Created with MonoDevelop
//
//    Copyright (C) 2017-2019 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.Unix;

using LibUsbDotNet;
using LibUsbDotNet.Main;


namespace Hiperscan.SGS.SerialInterface.USB
{

    public abstract partial class LibUsbDevice
    {
        public class Config
        {
            public int  ReadTimeout    { get; set; } = 5000;      // 5000ms USB Timeout (rxd)
            public int  WriteTimeout   { get; set; } = 5000;      // 5000ms USB Timeout (txd)
            public uint ReadBufferSize { get; set; } = 20 * 1024; // read large chunks in 20k blocks (we need to read the complete frame in one operation!)
        }

        protected Tuple<uint,uint>[] vendor_product_ids;
        protected UsbDevice usb_device;
        protected UsbEndpointReader endpoint_reader;
        protected UsbEndpointWriter endpoint_writer;
        protected readonly Config config = new Config();
        protected string serial;


        public uint GetNumberOfDevices()
        {
            return (uint)UsbDevice.AllDevices.FindAll((dev) =>
                                                      this.vendor_product_ids.Any(v => 
                                                                                  v.Item1 == (uint)dev.Vid && 
                                                                                  v.Item2 == (uint)dev.Pid))
                                  .Count;
        }

        public IEnumerable<DeviceInfo> GetDeviceList()
        {
            return GetDeviceList(UsbDevice.AllDevices);
        }

        public IEnumerable<DeviceInfo> GetDeviceList(UsbRegDeviceList all_usb_regs)
        {
            var infos = new List<DeviceInfo>();

            var usb_regs = all_usb_regs.FindAll((dev) =>
                                                      this.vendor_product_ids.Any(v =>
                                                                                  v.Item1 == (uint)dev.Vid &&
                                                                                  v.Item2 == (uint)dev.Pid));
            foreach (UsbRegistry usb_reg in usb_regs)
            {
                try
                {
                    string manufacturer = usb_reg.DeviceProperties["Mfg"]?.ToString() ?? Catalog.GetString("Unknown");
                    string description = usb_reg.DeviceProperties["DeviceDesc"]?.ToString() ?? Catalog.GetString("Unknown");
                    string serial      = LibUsbDevice.GetSerialNumber(usb_reg);

                    infos.Add(new DeviceInfo(manufacturer, serial, description, (uint)usb_reg.Vid, (uint)usb_reg.Pid));
                }
                catch (UsbException ex) when (ex.Status == ErrorCode.NotSupported.ToString())
                {
                    Console.WriteLine($"Device {usb_reg.Vid.ToString("X4")}:{usb_reg.Pid.ToString("X4")}: {ex.Message}");
                }
            }

            return infos;
        }

        public void OpenBySerialNumber(string serial)
        {
            this.serial = serial;

            try
            {
                this.OpenUsbDevice(serial);

                try
                {
                    this.InitUsbDevice();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Cannot open USB device: " + ex.Message);
                    Console.WriteLine("Trying to reset device and open it again...");
                    this.ResetDevice();
                }
            }
            catch (Exception ex)
            {
                string msg = string.Format(Catalog.GetString("Cannot open USB device with serial " +
                                                             "number {0}: Device may already be in use."),
                                           serial);
                throw new UsbException(ErrorCode.ResourceBusy, msg + " (" + ex.Message + ")");
            }
        }

        public void Close()
        {
            if (this.usb_device == null)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot close USB device which has not been initialized."));
            
            // is this a real "libusb" device, not WinUSB ?
            if (this.usb_device is IUsbDevice iusb_device)
            {
                if (iusb_device.ReleaseInterface(0) == false)
                    throw new Exception(Catalog.GetString("Cannot release interface."));
            }
            try
            {
                this.usb_device.Close();
            }
            catch (Exception ex)
            {
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot close USB device:") + " " + ex.Message);
            }
        }

        public void SetTimeouts(int read_timeout, int write_timeout)
        {
            this.config.ReadTimeout = read_timeout;
            this.config.WriteTimeout = write_timeout;
        }

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

        public void SetLatency()
        {
        }

        public void Purge()
        {
        }

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

            uint actually_read = 0;

            while (actually_read < len)
            {
                uint currently_read;

                currently_read = this.Read(out byte[] rx_buf, len - actually_read);

                if (currently_read > 0)
                {
                    Console.WriteLine("ReadBlocking: {0}/{1}", currently_read, len - actually_read);
                    buffers.Add(rx_buf);
                    actually_read += currently_read;
                    continue;
                }

                TimeSpan span = DateTime.Now - then;

                if (timeout > 0 && span.TotalMilliseconds > timeout)
                {
                    Console.WriteLine("Timeout while ReadBlocking: {0}/{1}", currently_read, len - actually_read);
                    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."));
                }

                idle_task?.Invoke();

                Thread.Sleep(50);
            }

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

        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;

                }
            }
        }

        protected static string GetSerialNumber(UsbRegistry usb_reg)
        {
            if (usb_reg.DeviceProperties.ContainsKey("SerialNumber") && usb_reg.DeviceProperties["SerialNumber"] != null)
            {
                return usb_reg.DeviceProperties["SerialNumber"].ToString();
            }
            else if (usb_reg.DeviceProperties.ContainsKey("DeviceID") && usb_reg.DeviceProperties["DeviceID"] != null)
            {
                string device_id = usb_reg.DeviceProperties["DeviceID"].ToString();
                return device_id.Substring(device_id.LastIndexOf('\\')).Trim('\\');
            }
            else
            {
                throw new UsbException(ErrorCode.NotSupported, Catalog.GetString("Cannot determine serial number of device."));
            }
        }

        abstract public void ResetDevice();

        protected abstract void OpenUsbDevice(string serial);
        protected abstract void InitUsbDevice();
        protected abstract void ResetUsbDevice();

        public abstract uint Read(out byte[] rx_buf, uint size);
    }
}
