/*  LilyPad - Pad plugin for PS2 Emulator
 *  Copyright (C) 2002-2014  PCSX2 Dev Team/ChickenLiver
 *
 *  PCSX2 is free software: you can redistribute it and/or modify it under the
 *  terms of the GNU Lesser General Public License as published by the Free
 *  Software Found- ation, either version 3 of the License, or (at your option)
 *  any later version.
 *
 *  PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY
 *  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 *  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with PCSX2.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Global.h"
#include "InputManager.h"
#include "WindowsMessaging.h"
#include "VKey.h"
#include "DeviceEnumerator.h"
#include "WndProcEater.h"
#include "WindowsKeyboard.h"
#include "WindowsMouse.h"

ExtraWndProcResult RawInputWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *output);

int GetRawKeyboards(HWND hWnd)
{
    RAWINPUTDEVICE Rid;
    Rid.hwndTarget = hWnd;

    Rid.dwFlags = 0;
    Rid.usUsagePage = 0x01;
    Rid.usUsage = 0x06;
    return RegisterRawInputDevices(&Rid, 1, sizeof(Rid));
}

void ReleaseRawKeyboards()
{
    RAWINPUTDEVICE Rid;
    Rid.hwndTarget = 0;

    Rid.dwFlags = RIDEV_REMOVE;
    Rid.usUsagePage = 0x01;
    Rid.usUsage = 0x06;
    RegisterRawInputDevices(&Rid, 1, sizeof(Rid));
}

int GetRawMice(HWND hWnd)
{
    RAWINPUTDEVICE Rid;
    Rid.hwndTarget = hWnd;

    Rid.dwFlags = RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE;
    Rid.usUsagePage = 0x01;
    Rid.usUsage = 0x02;
    return RegisterRawInputDevices(&Rid, 1, sizeof(Rid));
}

void ReleaseRawMice()
{
    RAWINPUTDEVICE Rid;
    Rid.hwndTarget = 0;

    Rid.dwFlags = RIDEV_REMOVE;
    Rid.usUsagePage = 0x01;
    Rid.usUsage = 0x02;
    RegisterRawInputDevices(&Rid, 1, sizeof(Rid));
}

// Count of active raw keyboard devices.
// when it gets to 0, release them all.
static int rawKeyboardActivatedCount = 0;
// Same for mice.
static int rawMouseActivatedCount = 0;

class RawInputKeyboard : public WindowsKeyboard
{
public:
    HANDLE hDevice;

    RawInputKeyboard(HANDLE hDevice, wchar_t *name, wchar_t *instanceID = 0)
        : WindowsKeyboard(RAW, name, instanceID)
    {
        this->hDevice = hDevice;
    }

    int Activate(InitInfo *initInfo)
    {
        Deactivate();

        hWndProc = initInfo->hWndProc;

        active = 1;
        if (!rawKeyboardActivatedCount++) {
            if (!rawMouseActivatedCount)
                hWndProc->Eat(RawInputWndProc, EATPROC_NO_UPDATE_WHILE_UPDATING_DEVICES);

            if (!GetRawKeyboards(hWndProc->hWndEaten)) {
                Deactivate();
                return 0;
            }
        }

        InitState();
        return 1;
    }

    void Deactivate()
    {
        FreeState();
        if (active) {
            active = 0;
            rawKeyboardActivatedCount--;
            if (!rawKeyboardActivatedCount) {
                ReleaseRawKeyboards();
                if (!rawMouseActivatedCount)
                    hWndProc->ReleaseExtraProc(RawInputWndProc);
            }
        }
    }
};

class RawInputMouse : public WindowsMouse
{
public:
    HANDLE hDevice;

    RawInputMouse(HANDLE hDevice, wchar_t *name, wchar_t *instanceID = 0, wchar_t *productID = 0)
        : WindowsMouse(RAW, 0, name, instanceID, productID)
    {
        this->hDevice = hDevice;
    }

    int Activate(InitInfo *initInfo)
    {
        Deactivate();

        hWndProc = initInfo->hWndProc;

        active = 1;

        // Have to be careful with order.  At worst, one unmatched call to ReleaseRawMice on
        // EatWndProc fail.  In all other cases, no unmatched initialization/cleanup
        // lines.
        if (!rawMouseActivatedCount++) {
            GetMouseCapture(hWndProc->hWndEaten);
            if (!rawKeyboardActivatedCount)
                hWndProc->Eat(RawInputWndProc, EATPROC_NO_UPDATE_WHILE_UPDATING_DEVICES);

            if (!GetRawMice(hWndProc->hWndEaten)) {
                Deactivate();
                return 0;
            }
        }

        AllocState();
        return 1;
    }

    void Deactivate()
    {
        FreeState();
        if (active) {
            active = 0;
            rawMouseActivatedCount--;
            if (!rawMouseActivatedCount) {
                ReleaseRawMice();
                ReleaseMouseCapture();
                if (!rawKeyboardActivatedCount) {
                    hWndProc->ReleaseExtraProc(RawInputWndProc);
                }
            }
        }
    }
};

ExtraWndProcResult RawInputWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *output)
{
    if (uMsg == WM_INPUT) {
        if (GET_RAWINPUT_CODE_WPARAM(wParam) == RIM_INPUT) {
            RAWINPUT in;
            unsigned int size = sizeof(RAWINPUT);
            if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &in, &size, sizeof(RAWINPUTHEADER)) > 0) {
                for (int i = 0; i < dm->numDevices; i++) {
                    Device *dev = dm->devices[i];
                    if (dev->api != RAW || !dev->active)
                        continue;
                    if (in.header.dwType == RIM_TYPEKEYBOARD && dev->type == KEYBOARD) {
                        RawInputKeyboard *rik = (RawInputKeyboard *)dev;
                        if (rik->hDevice != in.header.hDevice)
                            continue;

                        u32 uMsg = in.data.keyboard.Message;
                        if (!(in.data.keyboard.VKey >> 8))
                            rik->UpdateKey((u8)in.data.keyboard.VKey, (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN));
                    } else if (in.header.dwType == RIM_TYPEMOUSE && dev->type == MOUSE) {
                        RawInputMouse *rim = (RawInputMouse *)dev;
                        if (rim->hDevice != in.header.hDevice)
                            continue;
                        if (in.data.mouse.usFlags) {
                            // Never been set for me, and specs on what most of them
                            // actually mean is sorely lacking.  Also, specs erroneously
                            // indicate MOUSE_MOVE_RELATIVE is a flag, when it's really
                            // 0...
                            continue;
                        }

                        unsigned short buttons = in.data.mouse.usButtonFlags & 0x3FF;
                        int button = 0;
                        while (buttons) {
                            if (buttons & 3) {
                                // 2 is up, 1 is down.  Up takes precedence over down.
                                rim->UpdateButton(button, !(buttons & 2));
                            }
                            button++;
                            buttons >>= 2;
                        }
                        if (in.data.mouse.usButtonFlags & RI_MOUSE_WHEEL) {
                            rim->UpdateAxis(2, ((short)in.data.mouse.usButtonData) / WHEEL_DELTA);
                        }
                        if (in.data.mouse.lLastX || in.data.mouse.lLastY) {
                            rim->UpdateAxis(0, in.data.mouse.lLastX);
                            rim->UpdateAxis(1, in.data.mouse.lLastY);
                        }
                    }
                }
            }
        }
    } else if (uMsg == WM_ACTIVATE) {
        for (int i = 0; i < dm->numDevices; i++) {
            Device *dev = dm->devices[i];
            if (dev->api != RAW || dev->physicalControlState == 0)
                continue;
            memset(dev->physicalControlState, 0, sizeof(int) * dev->numPhysicalControls);
        }
    } else if (uMsg == WM_SIZE && rawMouseActivatedCount) {
        // Doesn't really matter for raw mice, as I disable legacy stuff, but shouldn't hurt.
        WindowsMouse::WindowResized(hWnd);
    }

    return CONTINUE_BLISSFULLY;
}

void EnumRawInputDevices()
{
    int count = 0;
    if (GetRawInputDeviceList(0, (unsigned int *)&count, sizeof(RAWINPUTDEVICELIST)) != (UINT)-1 && count > 0) {
        wchar_t *instanceID = (wchar_t *)malloc(41000 * sizeof(wchar_t));
        wchar_t *keyName = instanceID + 11000;
        wchar_t *displayName = keyName + 10000;
        wchar_t *productID = displayName + 10000;

        RAWINPUTDEVICELIST *list = (RAWINPUTDEVICELIST *)malloc(sizeof(RAWINPUTDEVICELIST) * count);
        int keyboardCount = 1;
        int mouseCount = 1;
        count = GetRawInputDeviceList(list, (unsigned int *)&count, sizeof(RAWINPUTDEVICELIST));

        // Not necessary, but reminder that count is -1 on failure.
        if (count > 0) {
            for (int i = 0; i < count; i++) {
                if (list[i].dwType != RIM_TYPEKEYBOARD && list[i].dwType != RIM_TYPEMOUSE)
                    continue;

                UINT bufferLen = 10000;
                int nameLen = GetRawInputDeviceInfo(list[i].hDevice, RIDI_DEVICENAME, instanceID, &bufferLen);
                if (nameLen >= 4) {
                    // nameLen includes terminating null.
                    nameLen--;

                    // Strip out GUID parts of instanceID to make it a generic product id,
                    // and reformat it to point to registry entry containing device description.
                    wcscpy(productID, instanceID);
                    wchar_t *temp = 0;
                    for (int j = 0; j < 3; j++) {
                        wchar_t *s = wcschr(productID, '#');
                        if (!s)
                            break;
                        *s = '\\';
                        if (j == 2) {
                            *s = 0;
                        }
                        if (j == 1)
                            temp = s;
                    }

                    wsprintfW(keyName, L"SYSTEM\\CurrentControlSet\\Enum%s", productID + 3);
                    if (temp)
                        *temp = 0;
                    int haveDescription = 0;
                    HKEY hKey;
                    if (ERROR_SUCCESS == RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyName, 0, KEY_QUERY_VALUE, &hKey)) {
                        DWORD type;
                        DWORD len = 10000 * sizeof(wchar_t);
                        if (ERROR_SUCCESS == RegQueryValueExW(hKey, L"DeviceDesc", 0, &type, (BYTE *)displayName, &len) &&
                            len && type == REG_SZ) {
                            wchar_t *temp2 = wcsrchr(displayName, ';');
                            if (!temp2)
                                temp2 = displayName;
                            else
                                temp2++;
                            // Could do without this, but more effort than it's worth.
                            wcscpy(keyName, temp2);
                            haveDescription = 1;
                        }
                        RegCloseKey(hKey);
                    }
                    if (list[i].dwType == RIM_TYPEKEYBOARD) {
                        if (!haveDescription)
                            wsprintfW(displayName, L"Raw Keyboard %i", keyboardCount++);
                        else
                            wsprintfW(displayName, L"Raw KB: %s", keyName);
                        dm->AddDevice(new RawInputKeyboard(list[i].hDevice, displayName, instanceID));
                    } else if (list[i].dwType == RIM_TYPEMOUSE) {
                        if (!haveDescription)
                            wsprintfW(displayName, L"Raw Mouse %i", mouseCount++);
                        else
                            wsprintfW(displayName, L"Raw MS: %s", keyName);
                        dm->AddDevice(new RawInputMouse(list[i].hDevice, displayName, instanceID, productID));
                    }
                }
            }
        }
        free(list);
        free(instanceID);
        dm->AddDevice(new RawInputKeyboard(0, L"Simulated Keyboard"));
        dm->AddDevice(new RawInputMouse(0, L"Simulated Mouse"));
    }
}
