 #region License
 //
 // The Open Toolkit Library License
 //
 // Copyright (c) 2006 - 2010 the Open Toolkit library.
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
 // in the Software without restriction, including without limitation the rights to 
 // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 // the Software, and to permit persons to whom the Software is furnished to do
 // so, subject to the following conditions:
 //
 // The above copyright notice and this permission notice shall be included in all
 // copies or substantial portions of the Software.
 //
 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 // OTHER DEALINGS IN THE SOFTWARE.
 //
 #endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using OpenTK.Input;

namespace OpenTK.Platform.X11
{
    sealed class XI2MouseKeyboard : IKeyboardDriver2, IMouseDriver2, IDisposable
    {
        static readonly IntPtr ExitAtom;
        readonly object Sync = new object();
        readonly Thread ProcessingThread;
        readonly X11KeyMap KeyMap;
        bool disposed;

        class XIMouse
        {
            public MouseState State;
            public XIDeviceInfo DeviceInfo;
            public XIScrollClassInfo ScrollX = new XIScrollClassInfo { number = -1 };
            public XIScrollClassInfo ScrollY = new XIScrollClassInfo { number = -1 };
            public XIValuatorClassInfo MotionX = new XIValuatorClassInfo { number = -1 };
            public XIValuatorClassInfo MotionY = new XIValuatorClassInfo { number = -1 };
            public string Name;
        }

        class XIKeyboard
        {
            public KeyboardState State;
            public XIDeviceInfo DeviceInfo;
            public string Name;
        }

        long cursor_x, cursor_y; // For GetCursorState()
        List<XIMouse> devices = new List<XIMouse>(); // list of connected mice
        Dictionary<int, int> rawids = new Dictionary<int, int>(); // maps hardware device ids to XIMouse ids

        List<XIKeyboard> keyboards = new List<XIKeyboard>(); // list of connected keybords
        Dictionary<int, int> keyboard_ids = new Dictionary<int, int>(); // maps hardware device ids to XIKeyboard ids

        internal readonly X11WindowInfo window;
        internal static int XIOpCode { get; private set; }
        internal static int XIVersion { get; private set; }

        static readonly Functions.EventPredicate PredicateImpl = IsEventValid;
        readonly IntPtr Predicate = Marshal.GetFunctionPointerForDelegate(PredicateImpl);

        static XI2MouseKeyboard()
        {
            using (new XLock(API.DefaultDisplay))
            {
                ExitAtom = Functions.XInternAtom(API.DefaultDisplay, "Exit Input Thread Message", false);
            }
        }

        public XI2MouseKeyboard()
        {
            window = new X11WindowInfo();

            window.Display = Functions.XOpenDisplay(IntPtr.Zero);
            using (new XLock(window.Display))
            {
                XSetWindowAttributes attr = new XSetWindowAttributes();

                window.Screen = Functions.XDefaultScreen(window.Display);
                window.RootWindow = Functions.XRootWindow(window.Display, window.Screen);
                window.Handle = Functions.XCreateWindow(window.Display, window.RootWindow,
                    0, 0, 1, 1, 0, 0,
                    CreateWindowArgs.InputOnly, IntPtr.Zero,
                    SetWindowValuemask.Nothing, attr);

                KeyMap = new X11KeyMap(window.Display);
            }

            if (!IsSupported(window.Display))
                throw new NotSupportedException("XInput2 not supported.");

            // Enable XI2 mouse/keyboard events
            // Note: the input event loop blocks waiting for these events
            // *or* a custom ClientMessage event that instructs us to exit.
            // See SendExitEvent() below.
            using (new XLock(window.Display))
            using (XIEventMask mask = new XIEventMask(1,
                XIEventMasks.RawKeyPressMask |
                XIEventMasks.RawKeyReleaseMask |
                XIEventMasks.RawButtonPressMask |
                XIEventMasks.RawButtonReleaseMask |
                XIEventMasks.RawMotionMask |
                XIEventMasks.MotionMask |
                XIEventMasks.DeviceChangedMask))
            {
                XI.SelectEvents(window.Display, window.RootWindow, mask);
                UpdateDevices();
            }

            ProcessingThread = new Thread(ProcessEvents);
            ProcessingThread.IsBackground = true;
            ProcessingThread.Start();
        }

        // Checks whether XInput2 is supported on the specified display.
        // If a display is not specified, the default display is used.
        internal static bool IsSupported(IntPtr display)
        {
            try
            {
                if (display == IntPtr.Zero)
                {
                    display = API.DefaultDisplay;
                }

                using (new XLock(display))
                {
                    int major, ev, error;
                    if (Functions.XQueryExtension(display, "XInputExtension", out major, out ev, out error) != 0)
                    {
                        XIOpCode = major;

                        int minor = 2;
                        while (minor >= 0)
                        {
                            if (XI.QueryVersion(display, ref major, ref minor) == ErrorCodes.Success)
                            {
                                XIVersion = major * 100 + minor * 10;
                                return true;
                            }
                            minor--;
                        }
                    }
                }
            }
            catch (DllNotFoundException e)
            {
                Debug.Print(e.ToString());
                Debug.Print("XInput2 extension not supported. Mouse support will suffer.");
            }

            return false;
        }

        #region IKeyboardDriver2 Members

        KeyboardState IKeyboardDriver2.GetState()
        {
            lock (Sync)
            {
                KeyboardState state = new KeyboardState();
                foreach (XIKeyboard k in keyboards)
                {
                    state.MergeBits(k.State);
                }
                return state;
            }
        }

        KeyboardState IKeyboardDriver2.GetState(int index)
        {
            lock (Sync)
            {
                if (index >= 0 && index < keyboards.Count)
                {
                    return keyboards[index].State;
                }
                return new KeyboardState();
            }
        }

        string IKeyboardDriver2.GetDeviceName(int index)
        {
            lock (Sync)
            {
                if (index >= 0 && index < keyboards.Count)
                {
                    return keyboards[index].Name;
                }
                return String.Empty;
            } 
        }

        #endregion

        #region IMouseDriver2 Members

        MouseState IMouseDriver2.GetState()
        {
            lock (Sync)
            {
                MouseState master = new MouseState();
                foreach (var d in devices)
                {
                    master.MergeBits(d.State);
                }
                return master;
            }
        }

        MouseState IMouseDriver2.GetState(int index)
        {
            lock (Sync)
            {
                if (index >= 0 && index < devices.Count)
                {
                    return devices[index].State;
                }
                return new MouseState();
            }
        }

        MouseState IMouseDriver2.GetCursorState()
        {
            lock (Sync)
            {
                MouseState master = (this as IMouseDriver2).GetState();
                master.X = (int)Interlocked.Read(ref cursor_x);
                master.Y = (int)Interlocked.Read(ref cursor_y);
                return master;
            }
        }

        void IMouseDriver2.SetPosition(double x, double y)
        {
            // Note: we cannot use window.Display here, because
            // that will deadlock the input thread, which is
            // blocking inside XIfEvent
            using (new XLock(API.DefaultDisplay))
            {
                Functions.XWarpPointer(API.DefaultDisplay,
                    IntPtr.Zero, window.RootWindow, 0, 0, 0, 0, (int)x, (int)y);
                Functions.XFlush(API.DefaultDisplay);
                Interlocked.Exchange(ref cursor_x, (long)x);
                Interlocked.Exchange(ref cursor_y, (long)y);
            }
        }

        #endregion

        #region Private Members

        void UpdateDevices()
        {
            lock (Sync)
            {
                devices.Clear();
                keyboards.Clear();

                int count;
                unsafe
                {
                    XIDeviceInfo* list = (XIDeviceInfo*)XI.QueryDevice(window.Display,
                        XI.XIAllDevices, out count);

                    Debug.Print("Refreshing input device list");
                    Debug.Print("{0} input devices detected", count);

                    for (int i = 0; i < count; i++)
                    {
                        switch ((list + i)->use)
                        {
                            case XIDeviceType.MasterKeyboard:
                            //case XIDeviceType.SlaveKeyboard:
                                {
                                    XIKeyboard k = new XIKeyboard();
                                    k.DeviceInfo = *(list + i);
                                    k.State.SetIsConnected(k.DeviceInfo.enabled);
                                    k.Name = Marshal.PtrToStringAnsi(k.DeviceInfo.name);
                                    int id = k.DeviceInfo.deviceid;
                                    if (!keyboard_ids.ContainsKey(id))
                                    {
                                        keyboard_ids.Add(k.DeviceInfo.deviceid, 0);
                                    }
                                    keyboard_ids[id] = keyboards.Count;
                                    keyboards.Add(k);
                                }
                                break;

                            case XIDeviceType.MasterPointer:
                            //case XIDeviceType.SlavePointer:
                            case XIDeviceType.FloatingSlave:
                                {
                                    XIMouse d = new XIMouse();
                                    d.DeviceInfo = *(list + i);
                       
                                    d.State.SetIsConnected(d.DeviceInfo.enabled);
                                    d.Name = Marshal.PtrToStringAnsi(d.DeviceInfo.name);
                                    Debug.Print("Device {0} \"{1}\" is {2} and has:",
                                        i, d.Name, d.DeviceInfo.enabled ? "enabled" : "disabled");

                                    // Decode the XIDeviceInfo to axes, buttons and scroll types
                                    for (int j = 0; j < d.DeviceInfo.num_classes; j++)
                                    {
                                        XIAnyClassInfo* class_info = *((XIAnyClassInfo**)d.DeviceInfo.classes + j);
                                        switch (class_info->type)
                                        {
                                            case XIClassType.Button:
                                                {
                                                    XIButtonClassInfo* button = (XIButtonClassInfo*)class_info;
                                                    Debug.Print("\t{0} buttons", button->num_buttons);
                                                }
                                                break;

                                            case XIClassType.Scroll:
                                                {
                                                    XIScrollClassInfo* scroll = (XIScrollClassInfo*)class_info;
                                                    switch (scroll->scroll_type)
                                                    {
                                                        case XIScrollType.Vertical:
                                                            Debug.WriteLine("\tSmooth vertical scrolling");
                                                            d.ScrollY = *scroll;
                                                            break;

                                                        case XIScrollType.Horizontal:
                                                            Debug.WriteLine("\tSmooth horizontal scrolling");
                                                            d.ScrollX = *scroll;
                                                            break;

                                                        default:
                                                            Debug.Print("\tUnknown scrolling type {0}", scroll->scroll_type);
                                                            break;
                                                    }
                                                }
                                                break;

                                            case XIClassType.Valuator:
                                                {
                                                    // We use relative x/y valuators for mouse movement.
                                                    // Iff these are not available, we fall back to
                                                    // absolute x/y valuators.
                                                    XIValuatorClassInfo* valuator = (XIValuatorClassInfo*)class_info;
                                                    if (valuator->label == XI.RelativeX)
                                                    {
                                                        Debug.WriteLine("\tRelative X movement");
                                                        d.MotionX = *valuator;
                                                    }
                                                    else if (valuator->label == XI.RelativeY)
                                                    {
                                                        Debug.WriteLine("\tRelative Y movement");
                                                        d.MotionY = *valuator;
                                                    }
                                                    else if (valuator->label == XI.AbsoluteX)
                                                    {
                                                        Debug.WriteLine("\tAbsolute X movement");
                                                        if (d.MotionX.number == -1)
                                                            d.MotionX = *valuator;
                                                    }
                                                    else if (valuator->label == XI.AbsoluteY)
                                                    {
                                                        Debug.WriteLine("\tAbsolute X movement");
                                                        if (d.MotionY.number == -1)
                                                            d.MotionY = *valuator;
                                                    }
                                                    else
                                                    {
                                                        IntPtr label = Functions.XGetAtomName(window.Display, valuator->label);
                                                        Debug.Print("\tUnknown valuator {0}",
                                                            Marshal.PtrToStringAnsi(label));
                                                        Functions.XFree(label);
                                                    }
                                                }
                                                break;
                                        }
                                    }

                                    // Map the hardware device id to the current XIMouse id
                                    int id = d.DeviceInfo.deviceid;
                                    if (!rawids.ContainsKey(id))
                                    {
                                        rawids.Add(id, 0);
                                    }
                                    rawids[id] = devices.Count;
                                    devices.Add(d);
                                }
                                break;
                        }
                    }
                    XI.FreeDeviceInfo((IntPtr)list);
                }
            }
        }

        void ProcessEvents()
        {
            while (!disposed)
            {
                XEvent e = new XEvent();
                XGenericEventCookie cookie;

                using (new XLock(window.Display))
                {
                    Functions.XIfEvent(window.Display, ref e, Predicate, new IntPtr(XIOpCode));

                    if (e.type == XEventName.ClientMessage && e.ClientMessageEvent.ptr1 == ExitAtom)
                    {
                        Functions.XDestroyWindow(window.Display, window.Handle);
                        window.Handle = IntPtr.Zero;
                        break;
                    }

                    if (e.type == XEventName.GenericEvent && e.GenericEvent.extension == XIOpCode)
                    {
                        IntPtr dummy;
                        int x, y, dummy2;
                        Functions.XQueryPointer(window.Display, window.RootWindow,
                            out dummy, out dummy, out x, out y,
                            out dummy2, out dummy2, out dummy2);
                        Interlocked.Exchange(ref cursor_x, (long)x);
                        Interlocked.Exchange(ref cursor_y, (long)y);

                        cookie = e.GenericEventCookie;
                        if (Functions.XGetEventData(window.Display, ref cookie) != 0)
                        {
                            switch ((XIEventType)cookie.evtype)
                            {
                                case XIEventType.Motion:
                                // Nothing to do
                                    break;

                                case XIEventType.RawKeyPress:
                                case XIEventType.RawKeyRelease:
                                case XIEventType.RawMotion:
                                case XIEventType.RawButtonPress:
                                case XIEventType.RawButtonRelease:
                                // Delivered to all XIMouse instances
                                    ProcessRawEvent(ref cookie);
                                    break;

                                case XIEventType.DeviceChanged:
                                    UpdateDevices();
                                    break;
                            }
                        }
                        Functions.XFreeEventData(window.Display, ref cookie);
                    }
                }
            }
            Debug.WriteLine("[X11] Exiting input event loop.");
        }

        void ProcessRawEvent(ref XGenericEventCookie cookie)
        {
            lock (Sync)
            {
                unsafe
                {
                    XIRawEvent raw = *(XIRawEvent*)cookie.data;
                    XIMouse mouse;
                    XIKeyboard keyboard;

                    switch (raw.evtype)
                    {
                        case XIEventType.RawMotion:
                            if (GetMouseDevice(raw.deviceid, out mouse))
                            {
                                ProcessRawMotion(mouse, ref raw);
                            }
                            break;

                        case XIEventType.RawButtonPress:
                        case XIEventType.RawButtonRelease:
                            if (GetMouseDevice(raw.deviceid, out mouse))
                            {
                                float dx, dy;
                                MouseButton button = X11KeyMap.TranslateButton(raw.detail, out dx, out dy);
                                mouse.State[button] = raw.evtype == XIEventType.RawButtonPress;
                                if (mouse.ScrollX.number == -1 && mouse.ScrollY.number == -1)
                                    mouse.State.SetScrollRelative(dx, dy);
                            }
                            break;

                        case XIEventType.RawKeyPress:
                        case XIEventType.RawKeyRelease:
                            if (GetKeyboardDevice(raw.deviceid, out keyboard))
                            {
                                Key key;
                                if (KeyMap.TranslateKey(raw.detail, out key))
                                {
                                    keyboard.State[key] = raw.evtype == XIEventType.RawKeyPress;
                                }
                            }
                            break;
                    }
                }
            }
        }

        bool GetMouseDevice(int deviceid, out XIMouse mouse)
        {
            if (!rawids.ContainsKey(deviceid))
            {
                Debug.Print("Unknown mouse device {0} encountered, ignoring.", deviceid);
                mouse = null;
                return false;
            }
            mouse = devices[rawids[deviceid]];
            return true;
        }

        bool GetKeyboardDevice(int deviceid, out XIKeyboard keyboard)
        {
            if (!keyboard_ids.ContainsKey(deviceid))
            {
                Debug.Print("Unknown keyboard device {0} encountered, ignoring.", deviceid);
                keyboard = null;
                return false;
            }
            keyboard = keyboards[keyboard_ids[deviceid]];
            return true;
        }

        unsafe static void ProcessRawMotion(XIMouse d, ref XIRawEvent raw)
        {
            // Note: we use the raw values here, without pointer
            // ballistics and any other modification.
            double x = 0;
            double y = 0;
            double h = 0;
            double v = 0;
            if (d.MotionX.number != -1)
                x = ReadRawValue(ref raw, d.MotionX.number);
            if (d.MotionY.number != -1)
                y = ReadRawValue(ref raw, d.MotionY.number);
            if (d.ScrollX.number != -1)
                h = ReadRawValue(ref raw, d.ScrollX.number) / d.ScrollX.increment;
            if (d.ScrollY.number != -1)
                v = ReadRawValue(ref raw, d.ScrollY.number) / d.ScrollY.increment;

            if (d.MotionX.mode == XIMode.Relative)
                d.State.X += (int)Math.Round(x);
            else
                d.State.X = (int)Math.Round(x);
            if (d.MotionY.mode == XIMode.Relative)
                d.State.Y += (int)Math.Round(y);
            else
                d.State.Y = (int)Math.Round(y);

            // Note: OpenTK follows the windows scrolling convention where
            // (+h, +v) = (right, up). XI2 uses (+h, +v) = (right, down)
            // instead, so we need to flip the vertical offset.
            d.State.SetScrollRelative((float)h, (float)(-v));
        }

        unsafe static double ReadRawValue(ref XIRawEvent raw, int bit)
        {
            double value = 0;
            if (IsBitSet(raw.valuators.mask, bit))
            {
                // Find the offset where this value is stored.
                // The offset is equal to the number of bits
                // set in raw.valuators.mask between [0, bit)
                int offset = 0;
                for (int i = 0; i < bit; i++)
                {
                    if (IsBitSet(raw.valuators.mask, i))
                    {
                        offset++;
                    }
                }
                value = *((double*)raw.raw_values + offset);
            }
            return value;
        }

        static bool IsEventValid(IntPtr display, ref XEvent e, IntPtr arg)
        {
            bool valid = false;
            switch (e.type)
            {
                case XEventName.GenericEvent:
                {
                    long extension = (long)e.GenericEventCookie.extension;
                    valid =
                        extension == arg.ToInt64() &&
                        (e.GenericEventCookie.evtype == (int)XIEventType.RawKeyPress ||
                        e.GenericEventCookie.evtype == (int)XIEventType.RawKeyRelease ||
                        e.GenericEventCookie.evtype == (int)XIEventType.RawMotion ||
                        e.GenericEventCookie.evtype == (int)XIEventType.RawButtonPress ||
                        e.GenericEventCookie.evtype == (int)XIEventType.RawButtonRelease ||
                        e.GenericEventCookie.evtype == (int)XIEventType.DeviceChanged);
                    valid |= extension == 0;
                    break;
                }

                case XEventName.ClientMessage:
                    valid =
                        e.ClientMessageEvent.ptr1 == ExitAtom;
                    break;
            }

            return valid;
        }

        static bool IsBitSet(IntPtr mask, int bit)
        {
            unsafe
            {
                return bit >= 0 && (*((byte*)mask + (bit >> 3)) & (1 << (bit & 7))) != 0;
            }
        }

        void SendExitEvent()
        {
            // Send a ClientMessage event to unblock XIfEvent
            // and exit the input event loop.
            using (new XLock(API.DefaultDisplay))
            {
                XEvent ev = new XEvent();
                ev.type = XEventName.ClientMessage;
                ev.ClientMessageEvent.display = window.Display;
                ev.ClientMessageEvent.window = window.Handle;
                ev.ClientMessageEvent.format = 32;
                ev.ClientMessageEvent.ptr1 = ExitAtom;
                Functions.XSendEvent(API.DefaultDisplay, window.Handle, false, 0, ref ev);
                Functions.XFlush(API.DefaultDisplay);
            }
        }

        #endregion

        #region IDisposable Members

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

        void Dispose(bool disposing)
        {
            if (!disposed)
            {
                disposed = true;
                SendExitEvent();
            }
        }

        ~XI2MouseKeyboard()
        {
            Dispose(false);
        }

        #endregion
    }
}

