#region --- License ---
/* Copyright (c) 2007 Stefanos Apostolopoulos
 * See license.txt for license info
 */
#endregion

#region --- Using directives ---

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Microsoft.Win32;
using OpenTK.Input;

#endregion

namespace OpenTK.Platform.Windows
{
    internal class WinRawKeyboard : IKeyboardDriver, IDisposable
    {
        private List<KeyboardDevice> keyboards = new List<KeyboardDevice>();
        private IntPtr window;

        #region internal static Dictionary<VirtualKeys, Input.Key> KeyMap

        internal static Dictionary<VirtualKeys, Input.Key> KeyMap =
            new Dictionary<VirtualKeys, Input.Key>((int)VirtualKeys.Last);

        private static bool keyMapExists;
        /// <summary>
        /// Initializes the map between VirtualKeys and OpenTK.Key
        /// </summary>
        private static void InitKeyMap()
        {
            if (!keyMapExists)
            {
                try
                {
                    KeyMap.Add(VirtualKeys.ESCAPE, Key.Escape);

                    // Function keys
                    for (int i = 0; i < 24; i++)
                    {
                        KeyMap.Add((VirtualKeys)((int)VirtualKeys.F1 + i), Key.F1 + i);
                    }

                    // Number keys (0-9)
                    for (int i = 0; i <= 9; i++)
                    {
                        KeyMap.Add((VirtualKeys)(0x30 + i), Key.Number0 + i);
                    }

                    // Letters (A-Z)
                    for (int i = 0; i < 26; i++)
                    {
                        KeyMap.Add((VirtualKeys)(0x41 + i), Key.A + i);
                    }

                    KeyMap.Add(VirtualKeys.TAB, Key.Tab);
                    KeyMap.Add(VirtualKeys.CAPITAL, Key.CapsLock);
                    KeyMap.Add(VirtualKeys.LCONTROL, Key.ControlLeft);
                    KeyMap.Add(VirtualKeys.LSHIFT, Key.ShiftLeft);
                    KeyMap.Add(VirtualKeys.LWIN, Key.WinLeft);
                    KeyMap.Add(VirtualKeys.LMENU, Key.AltLeft);
                    KeyMap.Add(VirtualKeys.SPACE, Key.Space);
                    KeyMap.Add(VirtualKeys.RMENU, Key.AltRight);
                    KeyMap.Add(VirtualKeys.RWIN, Key.WinRight);
                    KeyMap.Add(VirtualKeys.APPS, Key.Menu);
                    KeyMap.Add(VirtualKeys.RCONTROL, Key.ControlRight);
                    KeyMap.Add(VirtualKeys.RSHIFT, Key.ShiftRight);
                    KeyMap.Add(VirtualKeys.RETURN, Key.Enter);
                    KeyMap.Add(VirtualKeys.BACK, Key.BackSpace);

                    KeyMap.Add(VirtualKeys.OEM_1, Key.Semicolon);      // Varies by keyboard, ;: on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_2, Key.Slash);          // Varies by keyboard, /? on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_3, Key.Tilde);          // Varies by keyboard, `~ on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_4, Key.BracketLeft);    // Varies by keyboard, [{ on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_5, Key.BackSlash);      // Varies by keyboard, \| on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_6, Key.BracketRight);   // Varies by keyboard, ]} on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_7, Key.Quote);          // Varies by keyboard, '" on Win2K/US
                    KeyMap.Add(VirtualKeys.OEM_PLUS, Key.Plus);        // Invariant: +
                    KeyMap.Add(VirtualKeys.OEM_COMMA, Key.Comma);      // Invariant: ,
                    KeyMap.Add(VirtualKeys.OEM_MINUS, Key.Minus);      // Invariant: -
                    KeyMap.Add(VirtualKeys.OEM_PERIOD, Key.Period);    // Invariant: .

                    KeyMap.Add(VirtualKeys.HOME, Key.Home);
                    KeyMap.Add(VirtualKeys.END, Key.End);
                    KeyMap.Add(VirtualKeys.DELETE, Key.Delete);
                    KeyMap.Add(VirtualKeys.PRIOR, Key.PageUp);
                    KeyMap.Add(VirtualKeys.NEXT, Key.PageDown);
                    KeyMap.Add(VirtualKeys.PRINT, Key.PrintScreen);
                    KeyMap.Add(VirtualKeys.PAUSE, Key.Pause);
                    KeyMap.Add(VirtualKeys.NUMLOCK, Key.NumLock);

                    KeyMap.Add(VirtualKeys.SCROLL, Key.ScrollLock);
                    KeyMap.Add(VirtualKeys.SNAPSHOT, Key.PrintScreen);
                    KeyMap.Add(VirtualKeys.CLEAR, Key.Clear);
                    KeyMap.Add(VirtualKeys.INSERT, Key.Insert);

                    KeyMap.Add(VirtualKeys.SLEEP, Key.Sleep);

                    // Keypad
                    for (int i = 0; i <= 9; i++)
                    {
                        KeyMap.Add((VirtualKeys)((int)VirtualKeys.NUMPAD0 + i), Key.Keypad0 + i);
                    }
                    KeyMap.Add(VirtualKeys.DECIMAL, Key.KeypadDecimal);
                    KeyMap.Add(VirtualKeys.ADD, Key.KeypadAdd);
                    KeyMap.Add(VirtualKeys.SUBTRACT, Key.KeypadSubtract);
                    KeyMap.Add(VirtualKeys.DIVIDE, Key.KeypadDivide);
                    KeyMap.Add(VirtualKeys.MULTIPLY, Key.KeypadMultiply);

                    // Navigation
                    KeyMap.Add(VirtualKeys.UP, Key.Up);
                    KeyMap.Add(VirtualKeys.DOWN, Key.Down);
                    KeyMap.Add(VirtualKeys.LEFT, Key.Left);
                    KeyMap.Add(VirtualKeys.RIGHT, Key.Right);
                }
                catch (ArgumentException e)
                {
                    Debug.Print("Exception while creating keymap: '{0}'.", e.ToString());
                    System.Windows.Forms.MessageBox.Show(
                        String.Format("Exception while creating keymap: '{0}'.", e.ToString()));
                }
                finally
                {
                    keyMapExists = true;
                }
            }
        }

        #endregion

        #region --- Constructors ---

        internal WinRawKeyboard()
            : this(IntPtr.Zero)
        {
        }

        internal WinRawKeyboard(IntPtr windowHandle)
        {
            Debug.WriteLine("Initializing keyboard driver (WinRawKeyboard).");
            Debug.Indent();

            this.window = windowHandle;
            InitKeyMap();

            UpdateKeyboardList();

            Debug.Unindent();
        }

        #endregion

        #region internal static void UpdateKeyboardList()

        internal void UpdateKeyboardList()
        {
            int count = WinRawInput.DeviceCount;
            RawInputDeviceList[] ridl = new RawInputDeviceList[count];
            for (int i = 0; i < count; i++)
                ridl[i] = new RawInputDeviceList();
            Functions.GetRawInputDeviceList(ridl, ref count, API.RawInputDeviceListSize);

            // Discover keyboard devices:
            for (int i = 0; i < count; i++)
            {
                uint size = 0;
                Functions.GetRawInputDeviceInfo(ridl[i].Device, RawInputDeviceInfoEnum.DEVICENAME, IntPtr.Zero, ref size);
                IntPtr name_ptr = Marshal.AllocHGlobal((IntPtr)size);
                Functions.GetRawInputDeviceInfo(ridl[i].Device, RawInputDeviceInfoEnum.DEVICENAME, name_ptr, ref size);
                string name = Marshal.PtrToStringAnsi(name_ptr);
                Marshal.FreeHGlobal(name_ptr);
                if (name.ToLower().Contains("root"))
                {
                    // This is a terminal services device, skip it.
                    continue;
                }
                else if (ridl[i].Type == RawInputDeviceType.KEYBOARD || ridl[i].Type == RawInputDeviceType.HID)
                {
                    // This is a keyboard or USB keyboard device. In the latter case, discover if it really is a
                    // keyboard device by qeurying the registry.

                    // remove the \??\
                    name = name.Substring(4);

                    string[] split = name.Split('#');

                    string id_01 = split[0];    // ACPI (Class code)
                    string id_02 = split[1];    // PNP0303 (SubClass code)
                    string id_03 = split[2];    // 3&13c0b0c5&0 (Protocol code)
                    // The final part is the class GUID and is not needed here

                    string findme = string.Format(
                        @"System\CurrentControlSet\Enum\{0}\{1}\{2}",
                        id_01, id_02, id_03);

                    RegistryKey regkey = Registry.LocalMachine.OpenSubKey(findme);

                    string deviceDesc =
                        (string)regkey.GetValue("DeviceDesc");
                    string deviceClass =
                        (string)regkey.GetValue("Class");
                    if (!String.IsNullOrEmpty(deviceClass) && deviceClass.ToLower().Equals("keyboard"))
                    {
                        KeyboardDevice kb = new KeyboardDevice();
                        kb.Description = deviceDesc;

                        // Register the keyboard:
                        RawInputDeviceInfo info = new RawInputDeviceInfo();
                        int devInfoSize = API.RawInputDeviceInfoSize;
                        Functions.GetRawInputDeviceInfo(ridl[i].Device, RawInputDeviceInfoEnum.DEVICEINFO,
                                info, ref devInfoSize);

                        kb.NumberOfLeds = info.Device.Keyboard.NumberOfIndicators;
                        kb.NumberOfFunctionKeys = info.Device.Keyboard.NumberOfFunctionKeys;
                        kb.NumberOfKeys = info.Device.Keyboard.NumberOfKeysTotal;
                        //kb.DeviceID = (info.Device.Keyboard.Type << 32) + info.Device.Keyboard.SubType;
                        kb.DeviceID = ridl[i].Device;

                        //if (!keyboards.Contains(kb))
                        //{
                            this.RegisterKeyboardDevice(kb);
                            keyboards.Add(kb);
                        //}
                    }
                }
            }
        }

        #endregion

        #region internal void RegisterKeyboardDevice(Keyboard kb)

        internal void RegisterKeyboardDevice(KeyboardDevice kb)
        {
            RawInputDevice[] rid = new RawInputDevice[1];
            // Keyboard is 1/6 (page/id). See http://www.microsoft.com/whdc/device/input/HID_HWID.mspx
            rid[0] = new RawInputDevice();
            rid[0].UsagePage = 1;
            rid[0].Usage = 6;
            rid[0].Flags = RawInputDeviceFlags.INPUTSINK;
            rid[0].Target = window;

            if (!Functions.RegisterRawInputDevices(rid, 1, API.RawInputDeviceSize))
            {
                throw new ApplicationException(
                    String.Format(
                        "Raw input registration failed with error: {0}. Device: {1}",
                        Marshal.GetLastWin32Error(),
                        rid[0].ToString())
                );
            }
            else
            {
                Debug.Print("Registered keyboard {0}", kb.ToString());
            }
        }

        #endregion

        #region internal bool ProcessKeyboardEvent(API.RawInput rin)

        /// <summary>
        /// Processes raw input events.
        /// </summary>
        /// <param name="rin"></param>
        /// <returns></returns>
        internal bool ProcessKeyboardEvent(RawInput rin)
        {
            //Keyboard key = keyboards[0];
            //rin.Header.Device;
            switch (rin.Header.Type)
            {
                case RawInputDeviceType.KEYBOARD:
                    bool pressed =
                        rin.Data.Keyboard.Message == (int)WindowMessage.KEYDOWN ||
                        rin.Data.Keyboard.Message == (int)WindowMessage.SYSKEYDOWN;
                    
                    // Find the device where the button was pressed. It can be that the input notification
                    // came not from a physical keyboard device but from a code-generated input message - in
                    // that case, the event goes to the default (first) keyboard.
                    // TODO: Send the event to all keyboards instead of the default one.
                    // TODO: Optimize this! The predicate allocates way to much memory.
                    //int index = keyboards.FindIndex(delegate(KeyboardDevice kb)
                    //{
                    //    return kb.DeviceID == rin.Header.Device;
                    //});
                    //if (index == -1) index = 0;
                    int index;
                    if (keyboards.Count > 0) index = 0;
                    else return false;

                    // Generic control, shift, alt keys may be sent instead of left/right.
                    // It seems you have to explicitly register left/right events.
                    switch (rin.Data.Keyboard.VKey)
                    {
                        case VirtualKeys.SHIFT:
                            keyboards[index][Input.Key.ShiftLeft] = keyboards[index][Input.Key.ShiftRight] = pressed;
                            return true;

                        case VirtualKeys.CONTROL:
                            keyboards[index][Input.Key.ControlLeft] = keyboards[index][Input.Key.ControlRight] = pressed;
                            return true;

                        case VirtualKeys.MENU:
                            keyboards[index][Input.Key.AltLeft] = keyboards[index][Input.Key.AltRight] = pressed;
                            return true;

                        default:
                            if (!WinRawKeyboard.KeyMap.ContainsKey(rin.Data.Keyboard.VKey))
                            {
                                Debug.Print("Virtual key {0} ({1}) not mapped.",
                                    rin.Data.Keyboard.VKey, (int)rin.Data.Keyboard.VKey);
                            }
                            else
                            {
                                keyboards[index][WinRawKeyboard.KeyMap[rin.Data.Keyboard.VKey]] = pressed;
                            }
                            return false;
                    }

                default:
                    throw new ApplicationException("Windows raw keyboard driver received invalid data.");
            }
        }

        #endregion

        #region --- IInputDevice Members ---

        public string Description
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public Input.InputDeviceType DeviceType
        {
            get { return Input.InputDeviceType.Keyboard; }
        }

        #endregion

        #region --- IKeyboardDriver Members ---

        public IList<KeyboardDevice> Keyboard
        {
            get { return keyboards; }
        }

        #endregion

        #region --- IDisposable Members ---

        private bool disposed;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool manual)
        {
            if (!disposed)
            {
                if (manual)
                {
                    keyboards.Clear();
                }
                disposed = true;
            }
        }

        ~WinRawKeyboard()
        {
            Dispose(false);
        }

        #endregion
    }
}