using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HidLibrary
{
    public class HidDevice : IHidDevice, IDisposable
    {
        protected delegate HidDeviceData ReadDelegate();

        protected delegate HidReport ReadReportDelegate();

        private delegate bool WriteDelegate(byte[] data);

        private delegate bool WriteReportDelegate(HidReport report);

        private readonly string _description;

        private readonly string _devicePath;

        private readonly HidDeviceAttributes _deviceAttributes;

        private readonly HidDeviceCapabilities _deviceCapabilities;

        private DeviceMode _deviceReadMode = DeviceMode.NonOverlapped;

        private DeviceMode _deviceWriteMode = DeviceMode.NonOverlapped;

        private readonly HidDeviceEventMonitor _deviceEventMonitor;

        private bool _monitorDeviceEvents;

        public IntPtr ReadHandle
        {
            get;
            private set;
        }

        public IntPtr WriteHandle
        {
            get;
            private set;
        }

        public bool IsOpen
        {
            get;
            private set;
        }

        public bool IsConnected => HidDevices.IsConnected(_devicePath);

        public string Description => _description;

        public HidDeviceCapabilities Capabilities => _deviceCapabilities;

        public HidDeviceAttributes Attributes => _deviceAttributes;

        public string DevicePath => _devicePath;

        public bool MonitorDeviceEvents
        {
            get
            {
                return _monitorDeviceEvents;
            }
            set
            {
                if (value & !_monitorDeviceEvents)
                {
                    _deviceEventMonitor.Init();
                }
                _monitorDeviceEvents = value;
            }
        }

        public event InsertedEventHandler Inserted;

        public event RemovedEventHandler Removed;

        internal HidDevice(string devicePath, string description = null)
        {
            _deviceEventMonitor = new HidDeviceEventMonitor(this);
            _deviceEventMonitor.Inserted += DeviceEventMonitorInserted;
            _deviceEventMonitor.Removed += DeviceEventMonitorRemoved;
            _devicePath = devicePath;
            _description = description;
            try
            {
                IntPtr intPtr = OpenDeviceIO(_devicePath, 0u);
                _deviceAttributes = GetDeviceAttributes(intPtr);
                _deviceCapabilities = GetDeviceCapabilities(intPtr);
                CloseDeviceIO(intPtr);
            }
            catch (Exception innerException)
            {
                throw new Exception($"Error querying HID device '{devicePath}'.", innerException);
            }
        }

        public override string ToString()
        {
            return $"VendorID={_deviceAttributes.VendorHexId}, ProductID={_deviceAttributes.ProductHexId}, Version={_deviceAttributes.Version}, DevicePath={_devicePath}";
        }

        public void OpenDevice()
        {
            OpenDevice(DeviceMode.NonOverlapped, DeviceMode.NonOverlapped);
        }

        public void OpenDevice(DeviceMode readMode, DeviceMode writeMode)
        {
            if (!IsOpen)
            {
                _deviceReadMode = readMode;
                _deviceWriteMode = writeMode;
                try
                {
                    ReadHandle = OpenDeviceIO(_devicePath, readMode, 2147483648u);
                    WriteHandle = OpenDeviceIO(_devicePath, writeMode, 1073741824u);
                }
                catch (Exception innerException)
                {
                    IsOpen = false;
                    throw new Exception("Error opening HID device.", innerException);
                }
                IntPtr intPtr = ReadHandle;
                bool num = intPtr.ToInt32() != -1;
                intPtr = WriteHandle;
                IsOpen = (num & (intPtr.ToInt32() != -1));
            }
        }

        public void CloseDevice()
        {
            if (IsOpen)
            {
                CloseDeviceIO(ReadHandle);
                CloseDeviceIO(WriteHandle);
                IsOpen = false;
            }
        }

        public HidDeviceData Read()
        {
            return Read(0);
        }

        public void Read(ReadCallback callback)
        {
            ReadDelegate readDelegate = Read;
            HidAsyncState @object = new HidAsyncState(readDelegate, callback);
            readDelegate.BeginInvoke(EndRead, @object);
        }

        public HidDeviceData Read(int timeout)
        {
            if (IsConnected)
            {
                if (!IsOpen)
                {
                    OpenDevice();
                }
                try
                {
                    return ReadData(timeout);
                }
                catch
                {
                    return new HidDeviceData(HidDeviceData.ReadStatus.ReadError);
                }
            }
            return new HidDeviceData(HidDeviceData.ReadStatus.NotConnected);
        }

        public void ReadReport(ReadReportCallback callback)
        {
            ReadReportDelegate readReportDelegate = ReadReport;
            HidAsyncState @object = new HidAsyncState(readReportDelegate, callback);
            readReportDelegate.BeginInvoke(EndReadReport, @object);
        }

        public HidReport ReadReport(int timeout)
        {
            return new HidReport(Capabilities.InputReportByteLength, Read(timeout));
        }

        public HidReport ReadReport()
        {
            return ReadReport(0);
        }

        public bool ReadFeatureData(out byte[] data, byte reportId = 0)
        {
            if (_deviceCapabilities.FeatureReportByteLength > 0)
            {
                data = new byte[_deviceCapabilities.FeatureReportByteLength];
                byte[] array = CreateFeatureOutputBuffer();
                array[0] = reportId;
                IntPtr intPtr = IntPtr.Zero;
                bool flag = false;
                try
                {
                    intPtr = OpenDeviceIO(_devicePath, 0u);
                    flag = NativeMethods.HidD_GetFeature(intPtr, array, array.Length);
                    if (flag)
                    {
                        Array.Copy(array, 0, data, 0, Math.Min(data.Length, _deviceCapabilities.FeatureReportByteLength));
                    }
                }
                catch (Exception innerException)
                {
                    throw new Exception($"Error accessing HID device '{_devicePath}'.", innerException);
                }
                finally
                {
                    if (intPtr != IntPtr.Zero)
                    {
                        CloseDeviceIO(intPtr);
                    }
                }
                return flag;
            }
            data = new byte[0];
            return false;
        }

        public bool ReadProduct(out byte[] data)
        {
            data = new byte[64];
            IntPtr intPtr = IntPtr.Zero;
            bool result = false;
            try
            {
                intPtr = OpenDeviceIO(_devicePath, 0u);
                result = NativeMethods.HidD_GetProductString(intPtr, ref data[0], data.Length);
            }
            catch (Exception innerException)
            {
                throw new Exception($"Error accessing HID device '{_devicePath}'.", innerException);
            }
            finally
            {
                if (intPtr != IntPtr.Zero)
                {
                    CloseDeviceIO(intPtr);
                }
            }
            return result;
        }

        public bool ReadManufacturer(out byte[] data)
        {
            data = new byte[64];
            IntPtr intPtr = IntPtr.Zero;
            bool result = false;
            try
            {
                intPtr = OpenDeviceIO(_devicePath, 0u);
                result = NativeMethods.HidD_GetManufacturerString(intPtr, ref data[0], data.Length);
            }
            catch (Exception innerException)
            {
                throw new Exception($"Error accessing HID device '{_devicePath}'.", innerException);
            }
            finally
            {
                if (intPtr != IntPtr.Zero)
                {
                    CloseDeviceIO(intPtr);
                }
            }
            return result;
        }

        public bool ReadSerialNumber(out byte[] data)
        {
            data = new byte[64];
            IntPtr intPtr = IntPtr.Zero;
            bool result = false;
            try
            {
                intPtr = OpenDeviceIO(_devicePath, 0u);
                result = NativeMethods.HidD_GetSerialNumberString(intPtr, ref data[0], data.Length);
            }
            catch (Exception innerException)
            {
                throw new Exception($"Error accessing HID device '{_devicePath}'.", innerException);
            }
            finally
            {
                if (intPtr != IntPtr.Zero)
                {
                    CloseDeviceIO(intPtr);
                }
            }
            return result;
        }

        public void Write(byte[] data, WriteCallback callback)
        {
            WriteDelegate writeDelegate = Write;
            HidAsyncState @object = new HidAsyncState(writeDelegate, callback);
            writeDelegate.BeginInvoke(data, EndWrite, @object);
        }

        public bool Write(byte[] data)
        {
            return Write(data, 0);
        }

        public bool Write(byte[] data, int timeout)
        {
            if (IsConnected)
            {
                if (!IsOpen)
                {
                    OpenDevice();
                }
                try
                {
                    return WriteData(data, timeout);
                }
                catch
                {
                    return false;
                }
            }
            return false;
        }

        public void WriteReport(HidReport report, WriteCallback callback)
        {
            WriteReportDelegate writeReportDelegate = WriteReport;
            HidAsyncState @object = new HidAsyncState(writeReportDelegate, callback);
            writeReportDelegate.BeginInvoke(report, EndWriteReport, @object);
        }

        public bool WriteReport(HidReport report)
        {
            return WriteReport(report, 0);
        }

        public bool WriteReport(HidReport report, int timeout)
        {
            return Write(report.GetBytes(), timeout);
        }

        public HidReport CreateReport()
        {
            return new HidReport(Capabilities.OutputReportByteLength);
        }

        public bool WriteFeatureData(byte[] data)
        {
            if (_deviceCapabilities.FeatureReportByteLength > 0)
            {
                byte[] array = CreateFeatureOutputBuffer();
                Array.Copy(data, 0, array, 0, Math.Min(data.Length, _deviceCapabilities.FeatureReportByteLength));
                IntPtr intPtr = IntPtr.Zero;
                bool result = false;
                try
                {
                    intPtr = OpenDeviceIO(_devicePath, 0u);
                    result = NativeMethods.HidD_SetFeature(intPtr, array, array.Length);
                }
                catch (Exception innerException)
                {
                    throw new Exception($"Error accessing HID device '{_devicePath}'.", innerException);
                }
                finally
                {
                    if (intPtr != IntPtr.Zero)
                    {
                        CloseDeviceIO(intPtr);
                    }
                }
                return result;
            }
            return false;
        }

        protected static void EndRead(IAsyncResult ar)
        {
            HidAsyncState hidAsyncState = (HidAsyncState)ar.AsyncState;
            ReadDelegate readDelegate = (ReadDelegate)hidAsyncState.CallerDelegate;
            ReadCallback readCallback = (ReadCallback)hidAsyncState.CallbackDelegate;
            HidDeviceData data = readDelegate.EndInvoke(ar);
            readCallback?.Invoke(data);
        }

        protected static void EndReadReport(IAsyncResult ar)
        {
            HidAsyncState hidAsyncState = (HidAsyncState)ar.AsyncState;
            ReadReportDelegate readReportDelegate = (ReadReportDelegate)hidAsyncState.CallerDelegate;
            ReadReportCallback readReportCallback = (ReadReportCallback)hidAsyncState.CallbackDelegate;
            HidReport report = readReportDelegate.EndInvoke(ar);
            readReportCallback?.Invoke(report);
        }

        private static void EndWrite(IAsyncResult ar)
        {
            HidAsyncState hidAsyncState = (HidAsyncState)ar.AsyncState;
            WriteDelegate writeDelegate = (WriteDelegate)hidAsyncState.CallerDelegate;
            WriteCallback writeCallback = (WriteCallback)hidAsyncState.CallbackDelegate;
            bool success = writeDelegate.EndInvoke(ar);
            writeCallback?.Invoke(success);
        }

        private static void EndWriteReport(IAsyncResult ar)
        {
            HidAsyncState hidAsyncState = (HidAsyncState)ar.AsyncState;
            WriteReportDelegate writeReportDelegate = (WriteReportDelegate)hidAsyncState.CallerDelegate;
            WriteCallback writeCallback = (WriteCallback)hidAsyncState.CallbackDelegate;
            bool success = writeReportDelegate.EndInvoke(ar);
            writeCallback?.Invoke(success);
        }

        private byte[] CreateInputBuffer()
        {
            return CreateBuffer(Capabilities.InputReportByteLength - 1);
        }

        private byte[] CreateOutputBuffer()
        {
            return CreateBuffer(Capabilities.OutputReportByteLength - 1);
        }

        private byte[] CreateFeatureOutputBuffer()
        {
            return CreateBuffer(Capabilities.FeatureReportByteLength - 1);
        }

        private static byte[] CreateBuffer(int length)
        {
            byte[] array = null;
            Array.Resize(ref array, length + 1);
            return array;
        }

        private static HidDeviceAttributes GetDeviceAttributes(IntPtr hidHandle)
        {
            NativeMethods.HIDD_ATTRIBUTES attributes = default(NativeMethods.HIDD_ATTRIBUTES);
            attributes.Size = Marshal.SizeOf((object)attributes);
            NativeMethods.HidD_GetAttributes(hidHandle, ref attributes);
            return new HidDeviceAttributes(attributes);
        }

        private static HidDeviceCapabilities GetDeviceCapabilities(IntPtr hidHandle)
        {
            NativeMethods.HIDP_CAPS capabilities = default(NativeMethods.HIDP_CAPS);
            IntPtr preparsedData = default(IntPtr);
            if (NativeMethods.HidD_GetPreparsedData(hidHandle, ref preparsedData))
            {
                NativeMethods.HidP_GetCaps(preparsedData, ref capabilities);
                NativeMethods.HidD_FreePreparsedData(preparsedData);
            }
            return new HidDeviceCapabilities(capabilities);
        }

        private bool WriteData(byte[] data, int timeout)
        {
            if (_deviceCapabilities.OutputReportByteLength > 0)
            {
                byte[] array = CreateOutputBuffer();
                uint lpNumberOfBytesWritten = 0u;
                Array.Copy(data, 0, array, 0, Math.Min(data.Length, _deviceCapabilities.OutputReportByteLength));
                if (_deviceWriteMode != DeviceMode.Overlapped)
                {
                    try
                    {
                        NativeOverlapped lpOverlapped = default(NativeOverlapped);
                        return NativeMethods.WriteFile(WriteHandle, array, (uint)array.Length, out lpNumberOfBytesWritten, ref lpOverlapped);
                    }
                    catch
                    {
                        return false;
                    }
                }
                NativeMethods.SECURITY_ATTRIBUTES securityAttributes = default(NativeMethods.SECURITY_ATTRIBUTES);
                NativeOverlapped lpOverlapped2 = default(NativeOverlapped);
                int dwMilliseconds = (timeout <= 0) ? 65535 : timeout;
                securityAttributes.lpSecurityDescriptor = IntPtr.Zero;
                securityAttributes.bInheritHandle = true;
                securityAttributes.nLength = Marshal.SizeOf((object)securityAttributes);
                lpOverlapped2.OffsetLow = 0;
                lpOverlapped2.OffsetHigh = 0;
                lpOverlapped2.EventHandle = NativeMethods.CreateEvent(ref securityAttributes, Convert.ToInt32(false), Convert.ToInt32(true), "");
                try
                {
                    NativeMethods.WriteFile(WriteHandle, array, (uint)array.Length, out lpNumberOfBytesWritten, ref lpOverlapped2);
                }
                catch
                {
                    return false;
                }
                switch (NativeMethods.WaitForSingleObject(lpOverlapped2.EventHandle, dwMilliseconds))
                {
                    case 0u:
                        return true;
                    case 258u:
                        return false;
                    case uint.MaxValue:
                        return false;
                    default:
                        return false;
                }
            }
            return false;
        }

        protected HidDeviceData ReadData(int timeout)
        {
            byte[] array = new byte[0];
            HidDeviceData.ReadStatus status = HidDeviceData.ReadStatus.NoDataRead;
            if (_deviceCapabilities.InputReportByteLength > 0)
            {
                uint lpNumberOfBytesRead = 0u;
                array = CreateInputBuffer();
                if (_deviceReadMode != DeviceMode.Overlapped)
                {
                    try
                    {
                        NativeOverlapped lpOverlapped = default(NativeOverlapped);
                        NativeMethods.ReadFile(ReadHandle, array, (uint)array.Length, out lpNumberOfBytesRead, ref lpOverlapped);
                        status = HidDeviceData.ReadStatus.Success;
                    }
                    catch
                    {
                        status = HidDeviceData.ReadStatus.ReadError;
                    }
                }
                else
                {
                    NativeMethods.SECURITY_ATTRIBUTES securityAttributes = default(NativeMethods.SECURITY_ATTRIBUTES);
                    NativeOverlapped lpOverlapped2 = default(NativeOverlapped);
                    int dwMilliseconds = (timeout <= 0) ? 65535 : timeout;
                    securityAttributes.lpSecurityDescriptor = IntPtr.Zero;
                    securityAttributes.bInheritHandle = true;
                    securityAttributes.nLength = Marshal.SizeOf((object)securityAttributes);
                    lpOverlapped2.OffsetLow = 0;
                    lpOverlapped2.OffsetHigh = 0;
                    lpOverlapped2.EventHandle = NativeMethods.CreateEvent(ref securityAttributes, Convert.ToInt32(false), Convert.ToInt32(true), string.Empty);
                    try
                    {
                        NativeMethods.ReadFile(ReadHandle, array, (uint)array.Length, out lpNumberOfBytesRead, ref lpOverlapped2);
                        switch (NativeMethods.WaitForSingleObject(lpOverlapped2.EventHandle, dwMilliseconds))
                        {
                            case 0u:
                                status = HidDeviceData.ReadStatus.Success;
                                break;
                            case 258u:
                                status = HidDeviceData.ReadStatus.WaitTimedOut;
                                array = new byte[0];
                                break;
                            case uint.MaxValue:
                                status = HidDeviceData.ReadStatus.WaitFail;
                                array = new byte[0];
                                break;
                            default:
                                status = HidDeviceData.ReadStatus.NoDataRead;
                                array = new byte[0];
                                break;
                        }
                    }
                    catch
                    {
                        status = HidDeviceData.ReadStatus.ReadError;
                    }
                    finally
                    {
                        CloseDeviceIO(lpOverlapped2.EventHandle);
                    }
                }
            }
            return new HidDeviceData(array, status);
        }

        private static IntPtr OpenDeviceIO(string devicePath, uint deviceAccess)
        {
            return OpenDeviceIO(devicePath, DeviceMode.NonOverlapped, deviceAccess);
        }

        private static IntPtr OpenDeviceIO(string devicePath, DeviceMode deviceMode, uint deviceAccess)
        {
            NativeMethods.SECURITY_ATTRIBUTES lpSecurityAttributes = default(NativeMethods.SECURITY_ATTRIBUTES);
            int dwFlagsAndAttributes = 0;
            if (deviceMode == DeviceMode.Overlapped)
            {
                dwFlagsAndAttributes = 1073741824;
            }
            lpSecurityAttributes.lpSecurityDescriptor = IntPtr.Zero;
            lpSecurityAttributes.bInheritHandle = true;
            lpSecurityAttributes.nLength = Marshal.SizeOf((object)lpSecurityAttributes);
            return NativeMethods.CreateFile(devicePath, deviceAccess, 3, ref lpSecurityAttributes, 3, dwFlagsAndAttributes, 0);
        }

        private static void CloseDeviceIO(IntPtr handle)
        {
            if (Environment.OSVersion.Version.Major > 5)
            {
                NativeMethods.CancelIoEx(handle, IntPtr.Zero);
            }
            NativeMethods.CloseHandle(handle);
        }

        private void DeviceEventMonitorInserted()
        {
            if (IsOpen)
            {
                OpenDevice();
            }
            if (this.Inserted != null)
            {
                this.Inserted();
            }
        }

        private void DeviceEventMonitorRemoved()
        {
            if (IsOpen)
            {
                CloseDevice();
            }
            if (this.Removed != null)
            {
                this.Removed();
            }
        }

        public void Dispose()
        {
            if (MonitorDeviceEvents)
            {
                MonitorDeviceEvents = false;
            }
            if (IsOpen)
            {
                CloseDevice();
            }
        }
    }
}
