using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System.Windows.Data;
using System.Threading;
using System.Windows.Threading;

namespace TwoMbit
{
    public class Input
    {
        //sms
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setHwnd")]
        static extern void smsSetHwnd(IntPtr wih);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "initInput")]
        [return: MarshalAs(UnmanagedType.I1)]
        static extern bool smsInit();
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceIdList")]
        static extern uint smsGetInputDeviceIdList(byte[] deviceIdList);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceName")]
        static extern void smsGetInputDeviceName(uint deviceId, byte[] name);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDevicePort")]
        static extern Port smsGetInputDevicePort(uint deviceId);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceObjectIdList")]
        static extern uint smsGetInputDeviceObjectIdList(uint deviceId, byte[] iobjectIdList);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceObjectEntry")]
        static extern void smsGetInputDeviceObjectEntry(uint deviceId, uint iobjectId, byte[] entry);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "updateInputMap")]
        static extern void smsUpdateInputMap(uint deviceId, uint iobjectId, uint element);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "readInputMap")]
        static extern uint smsReadInputMap(uint deviceId, uint iobjectId);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "unmapInput")]
        static extern void smsUnmapInput(uint deviceId);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setInputPort")]
        static extern void smsSetInputPort(Port port, uint deviceId);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "captureReady")]
        static extern void smsCaptureReady(uint deviceId, uint iobjectId);
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "isInputCapturing")]
        [return: MarshalAs(UnmanagedType.I1)]
        static extern bool smsIsInputCapturing();
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "endInputCapturing")]
        static extern void smsEndInputCapturing();
        [DllImport(@"SmsCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "pollInput")]
        static extern void smsPollInput();

        //snes
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setHwnd")]
        static extern void snesSetHwnd(IntPtr wih);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "initInput")]
        [return: MarshalAs(UnmanagedType.I1)]
        static extern bool snesInit();
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceIdList")]
        static extern uint snesGetInputDeviceIdList(byte[] deviceList);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceName")]
        static extern void snesGetInputDeviceName(uint deviceId, byte[] name);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDevicePort")]
        static extern Port snesGetInputDevicePort(uint deviceId);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceObjectIdList")]
        static extern uint snesGetInputDeviceObjectIdList(uint deviceId, byte[] iobjectIdList);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getInputDeviceObjectEntry")]
        static extern void snesGetInputDeviceObjectEntry(uint deviceId, uint iobjectId, byte[] entry);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "updateInputMap")]
        static extern void snesUpdateInputMap(uint deviceId, uint iobjectId, uint element);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "readInputMap")]
        static extern uint snesReadInputMap(uint deviceId, uint iobjectId);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "unmapInput")]
        static extern void snesUnmapInput(uint deviceId);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setInputPort")]
        static extern void snesSetInputPort(Port port, uint deviceId);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "captureReady")]
        static extern void snesCaptureReady(uint deviceId, uint iobjectId);
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "isInputCapturing")]
        [return: MarshalAs(UnmanagedType.I1)]
        static extern bool snesIsInputCapturing();
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "endInputCapturing")]
        static extern void snesEndInputCapturing();
        [DllImport(@"SnesCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "pollInput")]
        static extern void snesPollInput();

        delegate void SetHwnd(IntPtr wih);
        delegate bool InitInput();
        delegate uint GetInputDeviceIdList(byte[] deviceList);
        delegate void GetInputDeviceName(uint deviceId, byte[] name);
        delegate Port GetInputDevicePort(uint deviceId);
        delegate uint GetInputDeviceObjectIdList(uint deviceId, byte[] iobjectIdList);
        delegate void GetInputDeviceObjectEntry(uint deviceId, uint iobjectId, byte[] entry);
        delegate void UpdateInputMap(uint deviceId, uint iobjectId, uint element);
        delegate uint ReadInputMap(uint deviceId, uint iobjectId);
        delegate void UnmapInput(uint deviceId);
        delegate void SetInputPort(Port port, uint deviceId);
        delegate void CaptureReady(uint deviceId, uint iobjectId);
        delegate bool IsInputCapturing();
        delegate void EndInputCapturing();
        delegate void PollInput();

        SetHwnd setHwnd;
        InitInput initInput;
        GetInputDeviceIdList getInputDeviceIdList;
        GetInputDeviceName getInputDeviceName;
        GetInputDevicePort getInputDevicePort;
        GetInputDeviceObjectIdList getInputDeviceObjectIdList;
        GetInputDeviceObjectEntry getInputDeviceObjectEntry;
        UpdateInputMap updateInputMap;
        ReadInputMap readInputMap;
        UnmapInput unmapInput;
        SetInputPort setInputPort;
        CaptureReady captureReady;
        IsInputCapturing isInputCapturing;
        EndInputCapturing endInputCapturing;
        PollInput pollInput;

        public struct GridData {
            public string Button { set; get; }
            public string Device { set; get; }
            public string Map { set; get; }
        }

        public static ComboBox comboDevices;
        public static DataGrid gridMap;
        public static Button buttonEmpty;
        public static TextBlock textMap;
        public static ComboBox comboPort1;
        public static ComboBox comboPort2;

        static List<Input> list = null;
        static Input inputObject;
        static public App app;

        enum Port { System = 0, Port1 = 1, Port2 = 2 };
        const uint MAX_DEVICE_ELEMENTS = 20;
        const uint MAX_INPUT_ELEMENTS = 20;
        
        string sysName;
        Config cfg;
        Cart._System system;
        App _app;
        Thread pollthread;
        volatile bool stopPoll;

        bool coreInit;

        struct IObject {
            public uint id;
            public string button;
        };

        struct Devices {
            public Port port;
            public uint id;
            public string name;
            public IObject[] iobjectList;
            public uint iobjectIndex;
            public uint iobjectCount;
        };
        Devices[] deviceList;
        uint deviceIndex;
        uint deviceCount;

        public Input(Config cfg, App _app, Cart._System system) {
            if (list == null) list = new List<Input>();
            list.Add(this);
            this._app = _app;
            this.cfg = cfg;
            this.system = system;
            this.coreInit = false;
            this.deviceCount = 0;
            this.deviceIndex = 0;
            Input.inputObject = null;
            this.pollthread = null;
            this.stopPoll = false;

            this.deviceList = new Devices[MAX_DEVICE_ELEMENTS];
            
            prepareDelegates(system);
        }

        void prepareDelegates(Cart._System system) {
            this.sysName = App.systemIdent[system];
            switch (system) {
                case Cart._System.SMS:
                    setHwnd = new SetHwnd(smsSetHwnd);
                    initInput = new InitInput(smsInit);
                    getInputDeviceIdList = new GetInputDeviceIdList(smsGetInputDeviceIdList);
                    getInputDeviceName = new GetInputDeviceName(smsGetInputDeviceName);
                    getInputDevicePort = new GetInputDevicePort(smsGetInputDevicePort);
                    getInputDeviceObjectIdList = new GetInputDeviceObjectIdList(smsGetInputDeviceObjectIdList);
                    getInputDeviceObjectEntry = new GetInputDeviceObjectEntry(smsGetInputDeviceObjectEntry);
                    updateInputMap = new UpdateInputMap(smsUpdateInputMap);
                    readInputMap = new ReadInputMap(smsReadInputMap);
                    unmapInput = new UnmapInput(smsUnmapInput);
                    setInputPort = new SetInputPort(smsSetInputPort);
                    captureReady = new CaptureReady(smsCaptureReady);
                    isInputCapturing = new IsInputCapturing(smsIsInputCapturing);
                    endInputCapturing = new EndInputCapturing(smsEndInputCapturing);
                    pollInput = new PollInput(smsPollInput);
                    break;
                case Cart._System.SNES:
                    setHwnd = new SetHwnd(snesSetHwnd);
                    initInput = new InitInput(snesInit);
                    getInputDeviceIdList = new GetInputDeviceIdList(snesGetInputDeviceIdList);
                    getInputDeviceName = new GetInputDeviceName(snesGetInputDeviceName);
                    getInputDevicePort = new GetInputDevicePort(snesGetInputDevicePort);
                    getInputDeviceObjectIdList = new GetInputDeviceObjectIdList(snesGetInputDeviceObjectIdList);
                    getInputDeviceObjectEntry = new GetInputDeviceObjectEntry(snesGetInputDeviceObjectEntry);
                    updateInputMap = new UpdateInputMap(snesUpdateInputMap);
                    readInputMap = new ReadInputMap(snesReadInputMap);
                    unmapInput = new UnmapInput(snesUnmapInput);
                    setInputPort = new SetInputPort(snesSetInputPort);
                    captureReady = new CaptureReady(snesCaptureReady);
                    isInputCapturing = new IsInputCapturing(snesIsInputCapturing);
                    endInputCapturing = new EndInputCapturing(snesEndInputCapturing);
                    pollInput = new PollInput(snesPollInput);
                    break;
            }
        }

        //init functions
        public void init() { //one time for each core
            if (this.coreInit) return;
            setHwnd(this._app.mainHwndHandle);
            if (!initInput()) {
                app.showError(app.lang.get("din_error"));
                app.kill();
            }
            readDevices();
            transferCfgToCore();
            this.coreInit = true;
        }

        public static Input getInputByIdent(Cart._System ident) {
            foreach (Input _input in list) {
                if (_input.system == ident) return _input;
            }
            return null;
        }

        void readDevices() {
            byte[] deviceIdList = new byte[MAX_DEVICE_ELEMENTS];
            this.deviceCount = getInputDeviceIdList(deviceIdList);

            for (uint i = 0; i < this.deviceCount; i++) {
                uint deviceId = deviceIdList[i];
                this.deviceList[i].id = deviceId;
                this.deviceList[i].port = getInputDevicePort(deviceId);
                byte[] deviceName = new byte[30];
                getInputDeviceName(deviceId, deviceName);
                System.Text.Encoding enc = System.Text.Encoding.Default;
                this.deviceList[i].name = enc.GetString(deviceName).Replace("\0", string.Empty);
            }
        }

        void transferCfgToCore() {
            setDeviceByPortFromCfg(Port.Port1);
            setDeviceByPortFromCfg(Port.Port2);
            for (uint i = 0; i < this.deviceCount; i++) {
                readObjectList(ref deviceList[i]);
                setObjectsFromCfg(deviceList[i]);
            }
        }

        void setDeviceByPortFromCfg(Port port) {
            uint deviceId = (uint) cfg.get<int>(this.sysName + "_port_" + Convert.ToString((int)port));
            for (uint i = 0; i < this.deviceCount; i++) {
                if (deviceList[i].id == deviceId && deviceList[i].port == port) {
                    setInputPort(port, deviceId);
                    break;
                }
            }
        }

        void readObjectList(ref Devices device) {
            byte[] iobjectIdList = new byte[MAX_INPUT_ELEMENTS];
            device.iobjectCount = getInputDeviceObjectIdList(device.id, iobjectIdList);

            device.iobjectList = new IObject[MAX_INPUT_ELEMENTS];
            for (uint i = 0; i < device.iobjectCount; i++) {
                device.iobjectList[i].id = iobjectIdList[i];
                string[] words = getObjectEntry(device.id, device.iobjectList[i].id);
                device.iobjectList[i].button = words[0];
            }
        }

        string[] getObjectEntry(uint deviceId, uint iobjectId) {
            byte[] iobjectEntry = new byte[70];
            getInputDeviceObjectEntry(deviceId, iobjectId, iobjectEntry);
            System.Text.Encoding enc = System.Text.Encoding.Default;
            string entry = enc.GetString(iobjectEntry).Replace("\0", string.Empty);
            return entry.Split('|');
        }

        void setObjectsFromCfg(Devices device) {
            for (uint i = 0; i < device.iobjectCount; i++) {
                try {
                    uint element = (uint)cfg.get<int>(getCfgIdent(device, device.iobjectList[i].button));
                    updateInputMap(device.id, device.iobjectList[i].id, element);
                } catch {
                    continue;
                }
            }
        }

        string getCfgIdent(Devices device, string button) {
            string port = Convert.ToString((int)device.port);
            return (this.sysName + "_" + device.name + "_" + port + "_" + button).ToLower();
        }

        //UI generation
        public void prepare() {
            endAllPolling();
            Input.inputObject = this;
            try {
                clearUI();
                init();
                setDeviceComboByPort(Port.Port1);
                setDeviceComboByPort(Port.Port2);
                for (uint i = 0; i < this.deviceCount; i++) {
                    ComboBoxItem item = new ComboBoxItem();
                    item.Content = deviceList[i].port + " : " + deviceList[i].name;
                    if (i == 0) item.IsSelected = true;
                    item.TabIndex = (int)i;
                    item.Selected += new RoutedEventHandler(setDevice);
                    comboDevices.Items.Add(item);
                }
                comboDevices.IsEnabled = true;
                buttonEmpty.IsEnabled = true;
                this.deviceIndex = 0;
                setGrid( deviceList[0] );
            } catch (DllNotFoundException) {
                Input.inputObject = null;
                app.showWarning(app.lang.get(this.sysName + "_core_dll") );
            }
        }

        void setDeviceComboByPort(Port port) {
            ComboBox box = null;
            switch (port) {
                case Port.Port1: box = Input.comboPort1; break;
                case Port.Port2: box = Input.comboPort2; break;
            }
            if (box == null) return;
            uint deviceId = (uint)cfg.get<int>(this.sysName + "_port_" + Convert.ToString((int)port));
            
            box.IsEnabled = true;
            ComboBoxItem item = new ComboBoxItem();
            item.IsSelected = true;
            item.TabIndex = 0;
            item.Content = app.lang.get("none");
            item.Selected += new RoutedEventHandler(setDeviceByPort);
            box.Items.Add(item);

            for (uint i = 0; i < this.deviceCount; i++) {
                if (deviceList[i].port != port) continue;
                item = new ComboBoxItem();
                if (deviceList[i].id == deviceId) item.IsSelected = true;
                item.TabIndex = (int)deviceList[i].id;
                item.Content = deviceList[i].name;
                item.Selected += new RoutedEventHandler(setDeviceByPort);
                box.Items.Add(item);
            }
        }

        void setDeviceByPort(object sender, RoutedEventArgs e) {
            ComboBoxItem item = (ComboBoxItem)sender;
            var comboBox = ItemsControl.ItemsControlFromItemContainer(item) as ComboBox;

            Port port = Port.Port1;
            switch (comboBox.Name) {
                case "InputPort1Box": port = Port.Port1; break;
                case "InputPort2Box": port = Port.Port2; break;
            }
            uint deviceId = (uint)item.TabIndex;

            cfg.set<int>(this.sysName + "_port_" + Convert.ToString((int)port), (int)deviceId);
            setInputPort(port, deviceId);
        }

        void setDevice(object sender, RoutedEventArgs e) {
            endPolling();
            ComboBoxItem ci = (ComboBoxItem)sender;
            this.deviceIndex = (uint)ci.TabIndex;
            setGrid(deviceList[this.deviceIndex]);
        }

        void setGrid(Devices device) {
            endPolling();

            gridMap.Dispatcher.Invoke(DispatcherPriority.Normal,
                new DispatcherOperationCallback(delegate {
                clearGrid();
                Input.textMap.Text = app.lang.get("register_input");
                for (uint i = 0; i < device.iobjectCount; i++) {
                    string[] words = getObjectEntry(device.id, device.iobjectList[i].id);
                    gridMap.Items.Add(new GridData() { Button = translate(words[0]), Device = words[1], Map = words[2] });     
                }
                return null;
                }), null);
        }

        //unmap
        public static void UNMAP() {
            if (Input.inputObject == null) return;
            Input.inputObject.unmapDevice();
        }

        void unmapDevice() {
            Devices device = deviceList[this.deviceIndex];
            unmapInput(device.id);
            setGrid(device);

            for (uint i = 0; i < device.iobjectCount; i++) {
                IObject iobject = device.iobjectList[i];
                uint element = readInputMap(device.id, iobject.id);
                cfg.set<int>(getCfgIdent(device, iobject.button), (int)element);
            }
        }

        //polling
        public static void endAllPolling() {
            if (list == null) return;
            foreach (Input _input in list) {
                _input.endPolling();
            }
        }

        void endPolling() {
            try {
                endInputCapturing();
            } catch (DllNotFoundException) {}
            if (pollthread != null) stopPoll = true;
        }

        public static void MAP(uint index) {
            if (Input.inputObject == null) return;
            Input.inputObject.oMap(index);
        }

        void oMap(uint index) {
            this.deviceList[this.deviceIndex].iobjectIndex = index;
            Devices device = this.deviceList[this.deviceIndex];
            endPolling();

            Input.textMap.Text = app.lang.get("press_key") + " " + translate(device.iobjectList[index].button);
            captureReady(this.deviceList[this.deviceIndex].id, device.iobjectList[index].id);
            do {
                System.Threading.Thread.Sleep(1);
                if (pollthread == null) {
                    pollthread = new Thread(new ThreadStart(poll));
                    break;
                }
            } while (pollthread != null);
            pollthread.IsBackground = true;
            pollthread.Start();
        }

        void poll() { //polling in separate thread, keep the UI responsive
            Devices device = this.deviceList[this.deviceIndex];
            stopPoll = false;
            for (;;) {
                System.Threading.Thread.Sleep(16);
                if (stopPoll) break;
                pollInput();
                if (!isInputCapturing()) {
                    IObject iobject = device.iobjectList[device.iobjectIndex];
                    uint element = readInputMap(device.id, iobject.id);
                    cfg.set<int>(getCfgIdent(device, iobject.button), (int)element);
                    setGrid(device);
                    break;
                }
            }
            pollthread = null;
        }

        //UI clear 
        public static void clearUI() {
            clearControls();
            clearGrid();
        }

        public static void clearControls() {
            comboDevices.Items.Clear();
            comboDevices.IsEnabled = false;
            comboPort1.Items.Clear();
            comboPort1.IsEnabled = false;
            comboPort2.Items.Clear();
            comboPort2.IsEnabled = false;
            buttonEmpty.IsEnabled = false;
        }

        public static void clearGrid() {
            gridMap.Items.Clear();
            gridMap.Columns.Clear();
            gridMap.Columns.Add(new DataGridTextColumn { Width = 80, Header = "Button", Binding = new Binding("Button") });
            gridMap.Columns.Add(new DataGridTextColumn { Width = 200, Header = app.lang.get("device"), Binding = new Binding("Device") });
            gridMap.Columns.Add(new DataGridTextColumn { Width = 170, Header = app.lang.get("map"), Binding = new Binding("Map") });
        }

        static string translate(string src) {
            try {
                return app.lang.get(src.ToLower());
            } catch {}
            return src;
        }
    }
}
