/*  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 <dinput.h>
#include "InputManager.h"

#include "VKey.h"
#include "DirectInput.h"
#include "DeviceEnumerator.h"
#include "PS2Etypes.h"

// All for getting GUIDs of XInput devices....
#include <wbemidl.h>
#include <oleauto.h>
// MS's code imports wmsstd.h, thus requiring the entire windows
// media SDK also be installed for a simple macro.  This is
// simpler and less silly.
#ifndef SAFE_RELEASE
#define SAFE_RELEASE(p)      { if(p) { (p)->Release(); (p)=NULL; } }
#endif

// Aka htons, without the winsock dependency.
inline static u16 flipShort(u16 s) {
	return (s>>8) | (s<<8);
}

// Aka htonl, without the winsock dependency.
inline static u32 flipLong(u32 l) {
	return (((u32)flipShort((u16)l))<<16) | flipShort((u16)(l>>16));
}

static void GUIDtoString(wchar_t *data, const GUID *pg) {
	wsprintfW(data, L"%08X-%04X-%04X-%04X-%04X%08X",
		pg->Data1, (u32)pg->Data2, (u32)pg->Data3,
		flipShort(((u16*)pg->Data4)[0]),
		flipShort(((u16*)pg->Data4)[1]),
		flipLong(((u32*)pg->Data4)[1]));
}

struct DirectInput8Data {
	IDirectInput8* lpDI8;
	int refCount;
	int deviceCount;
};

DirectInput8Data di8d = {0,0,0};

IDirectInput8* GetDirectInput() {
	if (!di8d.lpDI8) {
		if (FAILED(DirectInput8Create(hInst, 0x800, IID_IDirectInput8, (void**) &di8d.lpDI8, 0))) return 0;
	}
	di8d.refCount++;
	return di8d.lpDI8;
}
void ReleaseDirectInput() {
	if (di8d.refCount) {
		di8d.refCount--;
		if (!di8d.refCount) {
			di8d.lpDI8->Release();
			di8d.lpDI8 = 0;
		}
	}
}

static int StringToGUID(GUID *pg, wchar_t *dataw) {
	char data[100];
	if (wcslen(dataw) > 50) return 0;
	int w = 0;
	while (dataw[w]) {
		data[w] = (char) dataw[w];
		w++;
	}
	data[w] = 0;
	u32 temp[5];
	sscanf(data, "%08X-%04X-%04X-%04X-%04X%08X",
		&pg->Data1, temp, temp+1,
		temp+2, temp+3, temp+4);
	pg->Data2 = (u16) temp[0];
	pg->Data3 = (u16) temp[1];
	((u16*)pg->Data4)[0] = flipShort((u16)temp[2]);
	((u16*)pg->Data4)[1] = flipShort((u16)temp[3]);
	((u32*)pg->Data4)[1] = flipLong(temp[4]);
	return 1;
}


struct DI8Effect {
	IDirectInputEffect *die;
	int scale;
};

BOOL CALLBACK EnumDeviceObjectsCallback (LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef);
BOOL CALLBACK EnumEffectsCallback(LPCDIEFFECTINFOW pdei, LPVOID pvRef);
class DirectInputDevice : public Device {
public:
	DI8Effect *diEffects;

	IDirectInputDevice8 *did;
	GUID guidInstance;
	DirectInputDevice(DeviceType type, IDirectInputDevice8* did, wchar_t *displayName, wchar_t *instanceID, wchar_t *productID, GUID guid) : Device(DI, type, displayName, instanceID, productID) {
		diEffects = 0;
		guidInstance = guid;
		this->did = 0;
		did->EnumEffects(EnumEffectsCallback, this, DIEFT_ALL);
		did->EnumObjects(EnumDeviceObjectsCallback, this, DIDFT_ALL);
		did->Release();
	}

	void SetEffect(ForceFeedbackBinding *binding, unsigned char force) {
		int index = 0;
		if (!diEffects) {
			return;
		}
		for (int port=0; port<2; port++) {
			for (int slot=0; slot<4; slot++) {
				unsigned int diff = binding - pads[port][slot].ffBindings;
				if (diff < (unsigned int)pads[port][slot].numFFBindings) {
					index += diff;
					port = 2;
					break;
				}
				index += pads[port][slot].numFFBindings;
			}
		}
		IDirectInputEffect *die = diEffects[index].die;
		if (die) {
			DIEFFECT dieffect;
			memset(&dieffect, 0, sizeof(dieffect));
			union {
				DIPERIODIC periodic;
				DIRAMPFORCE ramp;
				DICONSTANTFORCE constant;
			};

			dieffect.dwSize = sizeof(dieffect);
			dieffect.lpvTypeSpecificParams = &periodic;
			int magnitude = abs((int)((force*10000*(__int64)diEffects[index].scale)/BASE_SENSITIVITY/255));
			if (magnitude > 10000) magnitude = 10000;
			int type = ffEffectTypes[binding->effectIndex].type;
			if (type == EFFECT_CONSTANT) {
				dieffect.cbTypeSpecificParams = sizeof(DICONSTANTFORCE);
				constant.lMagnitude = magnitude;
			}
			else if (type == EFFECT_PERIODIC) {
				dieffect.cbTypeSpecificParams = sizeof(DIPERIODIC);
				periodic.dwMagnitude = 0;
				periodic.lOffset = magnitude;
				periodic.dwPhase = 0;
				periodic.dwPeriod = 2000000;
			}
			else if (type == EFFECT_RAMP) {
				dieffect.cbTypeSpecificParams = sizeof(DIRAMPFORCE);
				ramp.lEnd = ramp.lStart = magnitude;
			}
			dieffect.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTIDS;
			dieffect.dwDuration = 2000000;
			die->SetParameters(&dieffect, DIEP_TYPESPECIFICPARAMS | DIEP_START);
		}
	}

	int Activate(InitInfo *initInfo) {
		int i;
		IDirectInput8 *di8 = GetDirectInput();
		Deactivate();
		if (!di8) return 0;
		if (DI_OK != di8->CreateDevice(guidInstance, &did, 0)) {
			ReleaseDirectInput();
			did = 0;
			return 0;
		}

		{
			DIOBJECTDATAFORMAT *formats = (DIOBJECTDATAFORMAT*)calloc(numPhysicalControls, sizeof(DIOBJECTDATAFORMAT));
			for (i=0; i<numPhysicalControls; i++) {
				formats[i].dwType = physicalControls[i].type | DIDFT_MAKEINSTANCE(physicalControls[i].id);
				formats[i].dwOfs = 4*i;
			}
			DIDATAFORMAT format;
			format.dwSize = sizeof(format);
			format.dwDataSize = 4 * numPhysicalControls;
			format.dwObjSize = sizeof(DIOBJECTDATAFORMAT);
			format.dwFlags = 0;
			format.dwNumObjs = numPhysicalControls;
			format.rgodf = formats;
			int res = did->SetDataFormat(&format);
			for (i=0; i<numPhysicalControls; i++) {
				if (physicalControls[i].type == ABSAXIS) {
					DIPROPRANGE prop;
					prop.diph.dwHeaderSize = sizeof(DIPROPHEADER);
					prop.diph.dwSize = sizeof(DIPROPRANGE);
					prop.diph.dwObj = formats[i].dwType;
					prop.diph.dwHow = DIPH_BYID;
					prop.lMin = -FULLY_DOWN;
					prop.lMax = FULLY_DOWN;
					did->SetProperty(DIPROP_RANGE, &prop.diph);

					// May do something like this again, if there's any need.
					/*
					if (FAILED(DI->did->SetProperty(DIPROP_RANGE, &prop.diph))) {
						if (FAILED(DI->did->GetProperty(DIPROP_RANGE, &prop.diph))) {
							// ????
							DI->objects[i].min = prop.lMin;
							DI->objects[i].max = prop.lMax;
							continue;
						}
					}
					DI->objects[i].min = prop.lMin;
					DI->objects[i].max = prop.lMax;
					//*/
				}
			}
			free(formats);
		}
		// Note:  Have to use hWndTop to properly hide cursor for mouse device.
		if (type == OTHER) {
			did->SetCooperativeLevel(initInfo->hWndTop, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
		}
		else if (type == KEYBOARD) {
			did->SetCooperativeLevel(initInfo->hWndTop, DISCL_FOREGROUND);
		}
		else {
			did->SetCooperativeLevel(initInfo->hWndTop, DISCL_FOREGROUND | DISCL_EXCLUSIVE);
		}
		if (did->Acquire() != DI_OK) {
			did->Release();
			did = 0;
			ReleaseDirectInput();
			return 0;
		}
		AllocState();
		int count = GetFFBindingCount();
		diEffects = (DI8Effect*) calloc(count, sizeof(DI8Effect));
		i = 0;
		for (int port=0; port<2; port++) {
			for (int slot=0; slot<4; slot++) {
				int subIndex = i;
				for (int j=0; j<pads[port][slot].numFFBindings; j++) {
					ForceFeedbackBinding *b = 0;
					b = &pads[port][slot].ffBindings[i-subIndex];
					ForceFeedbackEffectType *eff = ffEffectTypes + b->effectIndex;
					GUID guid;
					if (!StringToGUID(&guid, eff->effectID)) continue;

					DIEFFECT dieffect;
					memset(&dieffect, 0, sizeof(dieffect));
					dieffect.dwSize = sizeof(dieffect);
					dieffect.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTIDS;
					dieffect.dwDuration = 1000000;
					dieffect.dwGain = 10000;
					dieffect.dwTriggerButton = DIEB_NOTRIGGER;
					union {
						DIPERIODIC pediodic;
						DIRAMPFORCE ramp;
						DICONSTANTFORCE constant;
					} stuff = {0,0,0,0};

					if (eff->type == EFFECT_CONSTANT) {
						dieffect.cbTypeSpecificParams = sizeof(DICONSTANTFORCE);
					}
					else if (eff->type == EFFECT_PERIODIC) {
						dieffect.cbTypeSpecificParams = sizeof(DIPERIODIC);
					}
					else if (eff->type == EFFECT_RAMP) {
						dieffect.cbTypeSpecificParams = sizeof(DIRAMPFORCE);
					}
					dieffect.lpvTypeSpecificParams = &stuff;

					int maxForce = 0;
					int numAxes = 0;
					int *axes = (int*) malloc(sizeof(int) * 3 * numFFAxes);
					DWORD *axisIDs = (DWORD*)(axes + numFFAxes);
					LONG *dirList = (LONG*)(axisIDs + numFFAxes);
					dieffect.rgdwAxes = axisIDs;
					dieffect.rglDirection = dirList;
					for (int k=0; k<numFFAxes; k++) {
						if (b->axes[k].force) {
							int force = abs(b->axes[k].force);
							if (force > maxForce) {
								maxForce = force;
							}
							axes[numAxes] = k;
							axisIDs[numAxes] = ffAxes[k].id;
							dirList[numAxes] = b->axes[k].force;
							numAxes++;
						}
					}
					if (!numAxes) {
						free(axes);
						continue;
					}
					dieffect.cAxes = numAxes;
					diEffects[i].scale = maxForce;
					if (!SUCCEEDED(did->CreateEffect(guid, &dieffect, &diEffects[i].die, 0))) {
						diEffects[i].die = 0;
						diEffects[i].scale = 0;
					}

					free(axes);
					axes = 0;
					i++;
				}
			}
		}
		active = 1;
		return 1;
	}

	int Update() {
		if (!active) return 0;
		if (numPhysicalControls) {
			HRESULT res = did->Poll();
			// ??
			if ((res != DI_OK && res != DI_NOEFFECT) ||
				DI_OK != did->GetDeviceState(4*numPhysicalControls, physicalControlState)) {
					Deactivate();
					return 0;
			}
			for (int i=0; i<numPhysicalControls; i++) {
				if (physicalControls[i].type & RELAXIS) {
					physicalControlState[i] *= (FULLY_DOWN/3);
				}
				else if (physicalControls[i].type & BUTTON) {
					physicalControlState[i] = (physicalControlState[i]&0x80) * FULLY_DOWN / 128;
				}
			}
		}
		return 1;
	}

	int GetFFBindingCount() {
		int count = 0;
		for (int port = 0; port<2; port++) {
			for (int slot = 0; slot<4; slot++) {
				count += pads[port][slot].numFFBindings;
			}
		}
		return count;
	}

	void Deactivate() {
		FreeState();
		if (diEffects) {
			int count = GetFFBindingCount();
			for (int i=0; i<count; i++) {
				if (diEffects[i].die) {
					diEffects[i].die->Stop();
					diEffects[i].die->Release();
				}
			}
			free(diEffects);
			diEffects = 0;
		}
		if (active) {
			did->Unacquire();
			did->Release();
			ReleaseDirectInput();
			did = 0;
			active = 0;
		}
	}

	~DirectInputDevice() {
	}
};

BOOL CALLBACK EnumEffectsCallback(LPCDIEFFECTINFOW pdei, LPVOID pvRef) {
	DirectInputDevice * did = (DirectInputDevice*) pvRef;
	EffectType type;
	int diType = DIEFT_GETTYPE(pdei->dwEffType);
	if (diType == DIEFT_CONSTANTFORCE) {
		type = EFFECT_CONSTANT;
	}
	else if (diType == DIEFT_RAMPFORCE) {
		type = EFFECT_RAMP;
	}
	else if (diType == DIEFT_PERIODIC) {
		type = EFFECT_PERIODIC;
	}
	else {
		return DIENUM_CONTINUE;
	}
	wchar_t guidString[50];
	GUIDtoString(guidString, &pdei->guid);
	did->AddFFEffectType(pdei->tszName, guidString, type);

	return DIENUM_CONTINUE;
}

BOOL CALLBACK EnumDeviceObjectsCallback (LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef) {
	DirectInputDevice * did = (DirectInputDevice*) pvRef;
	if (lpddoi->dwType & DIDFT_FFACTUATOR) {
		did->AddFFAxis(lpddoi->tszName, lpddoi->dwType);
	}

	ControlType type;

	if (lpddoi->dwType & DIDFT_POV)
		type = POV;
	else if (lpddoi->dwType & DIDFT_ABSAXIS)
		type = ABSAXIS;
	else if (lpddoi->dwType & DIDFT_RELAXIS)
		type = RELAXIS;
	else if (lpddoi->dwType & DIDFT_PSHBUTTON)
		type = PSHBTN;
	else if (lpddoi->dwType & DIDFT_TGLBUTTON)
		type = TGLBTN;
	else
		return DIENUM_CONTINUE;
	// If too many objects, ignore extra buttons.
	if ((did->numPhysicalControls > 255 && DIDFT_GETINSTANCE(lpddoi->dwType) > 255) && (type & (DIDFT_PSHBUTTON | DIDFT_TGLBUTTON))) {
		int i;
		for (i = did->numPhysicalControls-1; i>did->numPhysicalControls-4; i--) {
			if (!lpddoi->tszName[0]) break;
			const wchar_t *s1 = lpddoi->tszName;
			const wchar_t *s2 = did->physicalControls[i].name;
			while (*s1 && *s1 == *s2) {
				s1++;
				s2++;
			}
			// If perfect match with one of last 4 names, break.
			if (!*s1 && !*s2) break;

			while (s1 != lpddoi->tszName && (s1[-1] >= '0' && s1[-1] <= '9')) s1--;
			int check = 0;
			while (*s1 >= '0' && *s1 <= '9') {
				check = check*10 + *s1 - '0';
				s1++;
			}
			while (*s2 >= '0' && *s2 <= '9') {
				s2++;
			}
			// If perfect match other than final number > 30, then break.
			// takes care of "button xx" case without causing issues with F keys.
			if (!*s1 && !*s2 && check > 30) break;
		}
		if (i != did->numPhysicalControls-4) {
			return DIENUM_CONTINUE;
		}
	}

	int vkey = 0;
	if (lpddoi->tszName[0] && did->type == KEYBOARD) {
		for (u32 i = 0; i<256; i++) {
			wchar_t *t = GetVKStringW((u8)i);
			if (!wcsicmp(lpddoi->tszName, t)) {
				vkey = i;
				break;
			}
		}
	}
	did->AddPhysicalControl(type, DIDFT_GETINSTANCE(lpddoi->dwType), vkey, lpddoi->tszName);
	return DIENUM_CONTINUE;
}

// Evil code from MS's site.  If only they'd just made a way to get
// an XInput device's GUID directly in the first place...
BOOL IsXInputDevice( const GUID* pGuidProductFromDirectInput )
{
    IWbemLocator*           pIWbemLocator  = NULL;
    IEnumWbemClassObject*   pEnumDevices   = NULL;
    IWbemClassObject*       pDevices[20]   = {0};
    IWbemServices*          pIWbemServices = NULL;
    BSTR                    bstrNamespace  = NULL;
    BSTR                    bstrDeviceID   = NULL;
    BSTR                    bstrClassName  = NULL;
    DWORD                   uReturned      = 0;
    bool                    bIsXinputDevice= false;
    UINT                    iDevice        = 0;
    VARIANT                 var;
    HRESULT                 hr;

    // CoInit if needed
    hr = CoInitialize(NULL);
    bool bCleanupCOM = SUCCEEDED(hr);

    // Create WMI
    hr = CoCreateInstance( __uuidof(WbemLocator),
                           NULL,
                           CLSCTX_INPROC_SERVER,
                           __uuidof(IWbemLocator),
                           (LPVOID*) &pIWbemLocator);
    if( FAILED(hr) || pIWbemLocator == NULL )
        goto LCleanup;

    bstrNamespace = SysAllocString( L"\\\\.\\root\\cimv2" );if( bstrNamespace == NULL ) goto LCleanup;
    bstrClassName = SysAllocString( L"Win32_PNPEntity" );   if( bstrClassName == NULL ) goto LCleanup;
    bstrDeviceID  = SysAllocString( L"DeviceID" );          if( bstrDeviceID == NULL )  goto LCleanup;

    // Connect to WMI
    hr = pIWbemLocator->ConnectServer( bstrNamespace, NULL, NULL, 0L,
                                       0L, NULL, NULL, &pIWbemServices );
    if( FAILED(hr) || pIWbemServices == NULL )
        goto LCleanup;

    // Switch security level to IMPERSONATE.
    CoSetProxyBlanket( pIWbemServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
                       RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE );

    hr = pIWbemServices->CreateInstanceEnum( bstrClassName, 0, NULL, &pEnumDevices );
    if( FAILED(hr) || pEnumDevices == NULL )
        goto LCleanup;

    // Loop over all devices
    for( ;; )
    {
        // Get 20 at a time
        hr = pEnumDevices->Next( 10000, 20, pDevices, &uReturned );
        if( FAILED(hr) )
            goto LCleanup;
        if( uReturned == 0 )
            break;

        for( iDevice=0; iDevice<uReturned; iDevice++ )
        {
            // For each device, get its device ID
            hr = pDevices[iDevice]->Get( bstrDeviceID, 0L, &var, NULL, NULL );
            if( SUCCEEDED( hr ) && var.vt == VT_BSTR && var.bstrVal != NULL )
            {
                // Check if the device ID contains "IG_".  If it does, then it's an XInput device
				    // This information can not be found from DirectInput
                if( wcsstr( var.bstrVal, L"IG_" ) )
                {
                    // If it does, then get the VID/PID from var.bstrVal
                    DWORD dwPid = 0, dwVid = 0;
                    WCHAR* strVid = wcsstr( var.bstrVal, L"VID_" );
					if (strVid) {
						dwVid = wcstoul(strVid+4, 0, 16);
					}
                    WCHAR* strPid = wcsstr( var.bstrVal, L"PID_" );
					if (strPid) {
						dwPid = wcstoul(strPid+4, 0, 16);
					}

                    // Compare the VID/PID to the DInput device
                    DWORD dwVidPid = MAKELONG( dwVid, dwPid );
                    if( dwVidPid == pGuidProductFromDirectInput->Data1 )
                    {
                        bIsXinputDevice = true;
                        goto LCleanup;
                    }
                }
            }
            SAFE_RELEASE( pDevices[iDevice] );
        }
    }

LCleanup:
    if(bstrNamespace)
        SysFreeString(bstrNamespace);
    if(bstrDeviceID)
        SysFreeString(bstrDeviceID);
    if(bstrClassName)
        SysFreeString(bstrClassName);
    for( iDevice=0; iDevice<20; iDevice++ )
        SAFE_RELEASE( pDevices[iDevice] );
    SAFE_RELEASE( pEnumDevices );
    SAFE_RELEASE( pIWbemLocator );
    SAFE_RELEASE( pIWbemServices );

    if( bCleanupCOM )
        CoUninitialize();

    return bIsXinputDevice;
}


struct DeviceEnumInfo {
	IDirectInput8 *di8;
	int ignoreXInput;
};

BOOL CALLBACK EnumCallback (LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef) {
	IDirectInput8* di8 = ((DeviceEnumInfo*)pvRef)->di8;
	const wchar_t *name;
	wchar_t temp[40];
	//if (((DeviceEnumInfo*)pvRef)->ignoreXInput && lpddi->
	if (lpddi->tszInstanceName[0]) {
		name = lpddi->tszInstanceName;
	}
	else if (lpddi->tszProductName[0]) {
		name = lpddi->tszProductName;
	}
	else {
		wsprintfW (temp, L"Device %i", di8d.deviceCount);
		name = temp;
	}
	di8d.deviceCount++;
	wchar_t *fullName = (wchar_t *) malloc((wcslen(name) + 4) * sizeof(wchar_t));
	wsprintf(fullName, L"DX %s", name);
	wchar_t instanceID[100];
	wchar_t productID[100];
	GUIDtoString(instanceID, &lpddi->guidInstance);
	GUIDtoString(productID, &lpddi->guidProduct);
	DeviceType type = OTHER;
	if ((lpddi->dwDevType & 0xFF) == DI8DEVTYPE_KEYBOARD) {
		type = KEYBOARD;
	}
	else if ((lpddi->dwDevType & 0xFF) == DI8DEVTYPE_MOUSE) {
		type = MOUSE;
	}
	IDirectInputDevice8 *did;
	if (DI_OK == di8->CreateDevice(lpddi->guidInstance, &did, 0)) {
		DirectInputDevice *dev = new DirectInputDevice(type, did, fullName, instanceID, productID, lpddi->guidInstance);
		if (dev->numPhysicalControls || dev->numFFAxes) {
			dm->AddDevice(dev);
		}
		else {
			delete dev;
		}
	}
	free(fullName);
	return DIENUM_CONTINUE;
}

void EnumDirectInputDevices(int ignoreXInput) {
	DeviceEnumInfo enumInfo;
	enumInfo.di8 = GetDirectInput();
	if (!enumInfo.di8) return;
	enumInfo.ignoreXInput = ignoreXInput;
	di8d.deviceCount = 0;
	enumInfo.di8->EnumDevices(DI8DEVCLASS_ALL, EnumCallback, &enumInfo, DIEDFL_ATTACHEDONLY);
	ReleaseDirectInput();
}

