using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HidSharp;
using HidSharp.Reports;
using HidSharp.Reports.Encodings;

namespace arDev.Joystick
{
    public class JoystickRaw : IDisposable
    {
        HidDevice _dev;
        HidStream _hidStream;

        public Boolean IsOpen { get; private set; }
        public event EventHandler Disconnected;
        public event EventHandler Connected;
        public event EventHandler Changed;
        public delegate void MessageHandler(string msg);
        public event MessageHandler Message;
        public int InputLength { get; private set; }
        public int OutputLength { get; private set; }
        public int FeatureLength { get; private set; }
        public int vid { get; set; }
        public int pid { get; set; }
        public Boolean[] Buttons = new bool[64];    //버튼정보를 기록한다
        Boolean bListUpdate = false;

        public JoystickRaw()
        {
            vid = -1;
            pid = -1;
            this.InputLength = 0;
            this.OutputLength = 0;
            this.FeatureLength = 0;
            this.IsOpen = false;
            this._dev = null;
            this._hidStream = null;
            DeviceList.Local.Changed += Local_Changed;
            Job = new Task(JobMain, ct);
            Job.Start();
            for (int i = 0; i < Buttons.Length; i++)
                Buttons[i] = false;

        }
        CancellationToken  ct ;
        public IEnumerable<HidDevice> GetHidDevices()
        {
            return DeviceList.Local.GetHidDevices();
        }
        public void Connect(int vid, int pid)
        {
            this.vid = vid;
            this.pid = pid;
        }
        public void Connect(HidDevice device)
        {
            this.vid = device.VendorID;
            this.pid = device.ProductID;
        }

        Task Job = null;
        private bool disposed = false;
        DateTime LastConnTime = DateTime.Now;
        void JobMain()
        {
            //개체가 소멸하기전까지 진행한다
            while (this.disposed == false)
            {
                if (IsOpen == false)
                {
                    if (this.vid == -1 || this.pid == -1)
                    {
                        //아직설정되지 않았으니 대기를한다
                        System.Threading.Thread.Sleep(3000);
                    }
                    else
                    {
                        //연결작업을 시작해야한다
                        if (bListUpdate || (DateTime.Now - LastConnTime).TotalMilliseconds >= 3000)
                        {
                            LastConnTime = DateTime.Now;

                            //연결작업
                            var list = this.GetHidDevices();
                            this._dev = list.Where(t => t.VendorID == this.vid && t.ProductID == this.pid).FirstOrDefault();
                            if (_dev != null)
                            {
                                try
                                {
                                    IsOpen = _dev.TryOpen(out _hidStream);
                                    if (IsOpen)
                                    {
                                        this.InputLength = _dev.GetMaxInputReportLength();
                                        this.OutputLength = _dev.GetMaxOutputReportLength();
                                        this.FeatureLength = _dev.GetMaxFeatureReportLength();
                                        this._hidStream.ReadTimeout = Timeout.Infinite;

                                        Connected?.Invoke(this, null);
                                        var rawReportDescriptor = _dev.GetRawReportDescriptor();
                                        var msg = ("Report Descriptor:");
                                        msg += string.Format("  {0} ({1} bytes)", string.Join(" ", rawReportDescriptor.Select(d => d.ToString("X2"))), rawReportDescriptor.Length);
                                        Message?.Invoke(msg);
                                    }
                                    else
                                    {
                                        if (this._hidStream != null) this._hidStream.Dispose();
                                    }
                                }
                                catch { System.Threading.Thread.Sleep(1500); }
                            }
                            else System.Threading.Thread.Sleep(1500);
                        }
                        else System.Threading.Thread.Sleep(1500);
                    }
                }
                else
                {
                    //데이터를 가지고 온다
                    using (_hidStream)
                    {
                        try
                        {
                            reportDescriptor = _dev.GetReportDescriptor();
                            deviceItem = reportDescriptor.DeviceItems[0];
                            var inputReportBuffer = new byte[_dev.GetMaxInputReportLength()];
                            var inputReceiver = reportDescriptor.CreateHidDeviceInputReceiver();
                            var inputParser = deviceItem.CreateDeviceItemInputParser();

                            inputReceiver.Start(_hidStream);

                            while (true)
                            {
                                if (!inputReceiver.IsRunning) { break; } // Disconnected?

                                Report report; //
                                while (inputReceiver.TryRead(inputReportBuffer, 0, out report))
                                {
                                    if (inputParser.TryParseReport(inputReportBuffer, 0, report))
                                    {
                                        WriteDeviceItemInputParserResult(inputParser);
                                    }
                                }
                                System.Threading.Thread.Sleep(20);
                            }

                            inputReceiver = null;
                            IsOpen = false;
                        }
                        catch (Exception ex)
                        {

                            Message?.Invoke(ex.Message);

                        }
                        finally
                        {
                            Disconnected?.Invoke(this, null);
                            IsOpen = false;
                        }
                    }
                }
            }
        }

        void WriteDeviceItemInputParserResult(HidSharp.Reports.Input.DeviceItemInputParser parser)
        {
            while (parser.HasChanged)
            {
                int changedIndex = parser.GetNextChangedIndex();
                var previousDataValue = parser.GetPreviousValue(changedIndex);
                var dataValue = parser.GetValue(changedIndex);

                var oVal = previousDataValue.GetPhysicalValue();
                var nVal = dataValue.GetPhysicalValue();

                if (double.IsNaN(oVal)) oVal = 0.0;
                if (double.IsNaN(nVal)) nVal = 0.0;

                var InputMethod = (Usage)dataValue.Usages.FirstOrDefault();
                if (InputMethod.ToString().StartsWith("Button"))
                {
                    var butNo = int.Parse(InputMethod.ToString().Substring(6));
                    this.Buttons[butNo - 1] = nVal > 0; //버튼값은 기록 210107
                }

                //이벤트발생
                InputChanged?.Invoke(this, new InputChangedEventHandler(
                    InputMethod, oVal, nVal));

                //메세지도 발생함
                //var msg = (string.Format("  {0}: {1} -> {2}",
                //                  (Usage)dataValue.Usages.FirstOrDefault(),
                //                  previousDataValue.GetPhysicalValue(),
                //                  dataValue.GetPhysicalValue()));
                //Message?.Invoke(msg);

            }
        }

        public event EventHandler<InputChangedEventHandler> InputChanged;

        public class InputChangedEventHandler : EventArgs
        {
            public Usage input { get; set; }
            public double oldValue { get; set; }
            public double newValue { get; set; }
            public InputChangedEventHandler(Usage input_, double oValue_, double nValue_)
            {
                this.input = input_;
                this.oldValue = oValue_;
                this.newValue = nValue_;
            }
        }

        ReportDescriptor reportDescriptor;
        DeviceItem deviceItem;
        ~JoystickRaw()
        {

            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this.disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.

                if (disposing)
                {
                    // Dispose managed resources.
                    this._dev = null;
                }


                Job.Wait();
                Job.Dispose();

                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // If disposing is false,
                // only the following code is executed.
                //CloseHandle(handle);
                //handle = IntPtr.Zero;

                // Note disposing has been done.
                DeviceList.Local.Changed -= Local_Changed;
                disposed = true;
            }
        }

        private void Local_Changed(object sender, DeviceListChangedEventArgs e)
        {
            Changed?.Invoke(sender, e);
            bListUpdate = true;
        }

    }
}
