using System;
using System.Collections.Generic;
using System.Windows;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
using System.Windows.Controls;
using System.ComponentModel;

namespace smx_config
{
    // The state and configuration of a pad.
    public struct LoadFromConfigDelegateArgsPerController
    {
        public SMX.SMXInfo info;
        public SMX.SMXConfig config;
        public SMX.SMXSensorTestModeData test_data;

        // The panels that are activated.  Note that to receive notifications from OnConfigChange
        // when inputs change state, set RefreshOnInputChange to true.  Otherwise, this field will
        // be filled in but notifications won't be sent due to only inputs changing.
        public bool[] inputs;
    }

    public struct LoadFromConfigDelegateArgs
    {
        // This indicates which fields changed since the last call.
        public bool ConnectionsChanged, ConfigurationChanged, InputChanged, TestDataChanged;

        // Data for each of two controllers:
        public LoadFromConfigDelegateArgsPerController[] controller;

        // If we have more than one connected controller, we expect them to be the same version.
        // Return the newest firmware version that's connected.
        public int firmwareVersion()
        {
            int result = 1;
            foreach(var data in controller)
            {
                if(data.info.connected && data.info.m_iFirmwareVersion > result)
                    result = data.info.m_iFirmwareVersion;

            }
            return result;
        }

        // The control that changed the configuration (passed to FireConfigurationChanged).
        public object source;
    };

    // This class tracks the device we're currently configuring, and runs a callback when
    // it changes.
    class CurrentSMXDevice
    {
        public static CurrentSMXDevice singleton;

        // This is fired when FireConfigurationChanged is called, and when the current device
        // changes.
        public delegate void ConfigurationChangedDelegate(LoadFromConfigDelegateArgs args);
        public event ConfigurationChangedDelegate ConfigurationChanged;

        private bool[] WasConnected = new bool[2] { false, false };
        private bool[][] LastInputs = new bool[2][];
        private SMX.SMXSensorTestModeData[] LastTestData = new SMX.SMXSensorTestModeData[2];
        private Dispatcher MainDispatcher;

        public CurrentSMXDevice()
        {
            // Grab the main thread's dispatcher, so we can invoke into it.
            MainDispatcher = Dispatcher.CurrentDispatcher;

            // Set our update callback.  This will be called when something happens: connection or disconnection,
            // inputs changed, configuration updated, test data updated, etc.  It doesn't specify what's changed,
            // we simply check the whole state.
            SMX.SMX.Start(delegate(int PadNumber, SMX.SMX.SMXUpdateCallbackReason reason) {
                // Console.WriteLine("... " + reason);
                // This is called from a thread, with SMX's internal mutex locked.  We must not call into SMX
                // or do anything with the UI from here.  Just queue an update back into the UI thread.
                MainDispatcher.InvokeAsync(delegate() {
                    switch(reason)
                    {
                    case SMX.SMX.SMXUpdateCallbackReason.Updated:
                        CheckForChanges();
                        break;
                    case SMX.SMX.SMXUpdateCallbackReason.FactoryResetCommandComplete:
                        Console.WriteLine("SMX_FactoryResetCommandComplete");
                        FireConfigurationChanged(null);
                        break;
                    }
                });
            });
        }

        public void Shutdown()
        {
            SMX.SMX.Stop();
        }

        private void CheckForChanges()
        {
            LoadFromConfigDelegateArgs args = GetState();

            // Mark which parts have changed.
            //
            // For configuration, we only check for connection state changes.  Actual configuration
            // changes are fired by controls via FireConfigurationChanged.
            for(int pad = 0; pad < 2; ++pad)
            {
                LoadFromConfigDelegateArgsPerController controller = args.controller[pad];
                if(WasConnected[pad] != controller.info.connected)
                {
                    args.ConfigurationChanged = true;
                    args.ConnectionsChanged = true;
                    WasConnected[pad] = controller.info.connected;
                }

                if(LastInputs[pad] == null || !Enumerable.SequenceEqual(controller.inputs, LastInputs[pad]))
                {
                    args.InputChanged = true;
                    LastInputs[pad] = controller.inputs;
                }

                if(!controller.test_data.Equals(LastTestData[pad]))
                {
                    args.TestDataChanged = true;
                    LastTestData[pad] = controller.test_data;
                }
            }

            // Only fire the delegate if something has actually changed.
            if(args.ConfigurationChanged || args.InputChanged || args.TestDataChanged)
                ConfigurationChanged?.Invoke(args);
        }

        public void FireConfigurationChanged(object source)
        {
            LoadFromConfigDelegateArgs args = GetState();
            args.ConfigurationChanged = true;
            args.source = source;
            ConfigurationChanged?.Invoke(args);
        }

        public LoadFromConfigDelegateArgs GetState()
        {
            LoadFromConfigDelegateArgs args = new LoadFromConfigDelegateArgs();
            args.controller = new LoadFromConfigDelegateArgsPerController[2];

            for(int pad = 0; pad < 2; ++pad)
            {
                LoadFromConfigDelegateArgsPerController controller;
                controller.test_data = new SMX.SMXSensorTestModeData();

                // Expand the inputs mask to an array.
                UInt16 Inputs = SMX.SMX.GetInputState(pad);
                controller.inputs = new bool[9];
                for(int i = 0; i < 9; ++i)
                    controller.inputs[i] = (Inputs & (1 << i)) != 0;
                SMX.SMX.GetInfo(pad, out controller.info);
                SMX.SMX.GetConfig(pad, out controller.config);
                SMX.SMX.GetTestData(pad, out controller.test_data);
                args.controller[pad] = controller;
            }

            return args;
        }

    }

    // Call a delegate on configuration change.  Configuration changes are notified by calling
    // FireConfigurationChanged.  Listeners won't receive notifications for changes that they
    // fired themselves.
    public class OnConfigChange
    {
        public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args);
        private readonly Control Owner;
        private readonly LoadFromConfigDelegate Callback;
        private bool _RefreshOnInputChange = false;

        // If set to true, the callback will be invoked on input changes in addition to configuration
        // changes.  This can cause the callback to be run at any time, such as while the user is
        // interacting with the control.
        public bool RefreshOnInputChange {
            get { return _RefreshOnInputChange; }
            set {_RefreshOnInputChange = value; }
        }

        private bool _RefreshOnTestDataChange = false;

        // Like RefreshOnInputChange, but enables callbacks when test data changes.
        public bool RefreshOnTestDataChange {
            get { return _RefreshOnTestDataChange; }
            set { _RefreshOnTestDataChange = value; }
        }

        // Owner is the Control that we're calling.  This callback will be disable when the
        // control is unloaded, and we won't call it if it's the same control that fired
        // the change via FireConfigurationChanged.
        //
        // In addition, the callback is called when the control is Loaded, to load the initial
        // state.
        public OnConfigChange(Control owner, LoadFromConfigDelegate callback)
        {
            Owner = owner;
            Callback = callback;

            Owner.Loaded += delegate(object sender, RoutedEventArgs e)
            {
                if(CurrentSMXDevice.singleton != null)
                    CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged;
                Refresh();
            };

            Owner.Unloaded += delegate(object sender, RoutedEventArgs e)
            {
                if(CurrentSMXDevice.singleton != null)
                    CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged;
            };
        }

        private void ConfigurationChanged(LoadFromConfigDelegateArgs args)
        {
            if(args.ConfigurationChanged ||
                (RefreshOnInputChange && args.InputChanged) ||
                (RefreshOnTestDataChange && args.TestDataChanged))
            {
                Callback(args);
            }
        }

        private void Refresh()
        {
            if(CurrentSMXDevice.singleton != null)
                Callback(CurrentSMXDevice.singleton.GetState());
        }
    };


    public class OnInputChange
    {
        public delegate void LoadFromConfigDelegate(LoadFromConfigDelegateArgs args);
        private readonly Control Owner;
        private readonly LoadFromConfigDelegate Callback;

        // Owner is the Control that we're calling.  This callback will be disable when the
        // control is unloaded, and we won't call it if it's the same control that fired
        // the change via FireConfigurationChanged.
        //
        // In addition, the callback is called when the control is Loaded, to load the initial
        // state.
        public OnInputChange(Control owner, LoadFromConfigDelegate callback)
        {
            Owner = owner;
            Callback = callback;

            // This is available when the application is running, but will be null in the XAML designer.
            if(CurrentSMXDevice.singleton == null)
                return;

            Owner.Loaded += delegate(object sender, RoutedEventArgs e)
            {
                CurrentSMXDevice.singleton.ConfigurationChanged += ConfigurationChanged;
                Refresh();
            };

            Owner.Unloaded += delegate(object sender, RoutedEventArgs e)
            {
                CurrentSMXDevice.singleton.ConfigurationChanged -= ConfigurationChanged;
            };
        }

        private void ConfigurationChanged(LoadFromConfigDelegateArgs args)
        {
            Callback(args);
        }

        private void Refresh()
        {
            if(CurrentSMXDevice.singleton != null)
                Callback(CurrentSMXDevice.singleton.GetState());
        }
    };
}