///////////////////////////////////////////////////////////////////////////////////////////
// DirectInput.cpp
//
//  Implementation of the DirectInput class, which wraps a DirectInput 8 object.
//
//  Enumerating DirectInput devices involves a callback. .NET provides a nice way to 
// perform the callbacks: it automatically adds an unmanaged callback thunk to a delegate
// when you pass the delegate to an unmanaged function. However, all the arguments must be 
// ints (!). This explains the curious signature of EnumDevicesBySemanticsCallback. This 
// conveniently removes the need to pass structures or object pointers to an unmanaged 
// callback function - never a good idea, particularly since you can't cast a __gc pointer 
// to void * anyway.
//  This is the theory. So much for theory... In practice, this works fine *provided* you
// are calling a [DllImport] function. We're not - we're calling a COM interface. In order
// to tell .NET that we want to pass the delegate as a function pointer, we need to give it
// a MarshalAsAttribute. The receiving function, however, must see a C function pointer...
// To get around this, we have a .NET declaration for CallEnumDevicesBySemantics here, with
// the attribute. In DirectInputEnum.cpp, we have the unmanaged code, with a C function 
// pointer parameter. Note that the same has to be done for the Configure and Display 
// methods.
//
//  Here, we are using the groovy System.Runtime.InteropServices.Marshal.Copy method, which
// allows us to copy between managed and unmanaged memory, and the even groovier
// Marshal.StringToCoTaskMemXXX, which copies the string to COM task allocated memory.
//
//  When the application is deactivated, good practice dictates we unacquire all devices,
// then re-acquire them when we are activated again. The DirectInput class therefore hooks
// the ParentWindow's Activated and Deactivate events, and calls Acquire and Unacquire
// appropriately.
//
//  The devices are stored in an untyped ArrayList. It's not really something an 
// application should need, but if you do, they are wrapped by the DirectInput.Device 
// subclass (which primarily performs lifetime management). Change the contents of the list 
// at your peril...
//
//  Lots of DirectInput functions take a pointer to the DIACTIONFORMAT structure, for which
// a lot of pinning goes on...

#include "StdAfx.h"
#include "DirectInput.h"

#pragma comment(lib, "dinput8.lib")
#pragma comment(lib, "dxguid.lib")

using namespace System::Runtime::InteropServices;
using namespace System::Windows::Forms;

#pragma unmanaged
// Helper function for calling IDirectInput8::EnumDevicesBySemantics from managed code.
extern "C" HRESULT CallEnumDevicesBySemantics(IDirectInput8 *pDI,
                                           LPCTSTR lpsz, DIACTIONFORMAT *pDIAF,
                                           [MarshalAs(UnmanagedType::FunctionPtr)] Sunlight::DirectX::Input::DirectInput::DIEnumDevicesBySemanticsCallbackDelegate *pd,
                                           LPVOID pvRef, DWORD dwFlags);
// Helper function for calling IDirectInput8::ConfigureDevices from managed code.
extern "C" HRESULT CallConfigureDevices(IDirectInput8 *pDI, 
                                        [MarshalAs(UnmanagedType::FunctionPtr)] Sunlight::DirectX::Input::DirectInput::DIConfigureDevicesCallbackDelegate *pCallback, 
                                        LPDICONFIGUREDEVICESPARAMS lpdiCDParams, DWORD dwFlags, LPVOID pvRef);
#pragma managed

namespace Sunlight
{
    namespace DirectX
    {
        namespace Input
        {
            // Callback function to map appropriate devices
            bool DirectInput::EnumDevicesBySemanticsCallback(int nddi, int ndid, int /*dwFlags*/, int dwRemaining, int /*pvRef*/)
            {
                HRESULT                 h;
                DIACTIONFORMAT  __pin   *pDIAF = &m_diaf;

                LPCDIDEVICEINSTANCE lpddi = (LPCDIDEVICEINSTANCE)nddi;
                IDirectInputDevice8 __nogc *lpdid = (IDirectInputDevice8 __nogc *)ndid;

                // Devices of type DI8DEVTYPE_DEVICECTRL are specialized devices not generally
                // considered appropriate to control game actions. We just ignore these.
                if (GET_DIDEVICE_TYPE(lpddi->dwDevType) == DI8DEVTYPE_DEVICECTRL)
                    return DIENUM_CONTINUE;

                // Build the action map for the device. This will map each action to
                //  the most appropriate function on the device.
                h = lpdid->BuildActionMap(pDIAF, NULL, DIDBAM_DEFAULT);
                if (FAILED(h))
                    return DIENUM_CONTINUE;

                for (DWORD i = 0; i < pDIAF->dwNumActions; i++)
                {
                    if (pDIAF->rgoAction[i].dwHow != DIAH_UNMAPPED)
                    {
                        // If any controls were mapped, assign exclusive control of this device to us.
                        h = lpdid->SetCooperativeLevel((HWND)m_pParentWindow->Handle.ToPointer(), DISCL_EXCLUSIVE | DISCL_FOREGROUND);
                        if (FAILED(h))
                            return DIENUM_CONTINUE;

                        // Set the action map on this device.
                        h = lpdid->SetActionMap(pDIAF, NULL, DIDSAM_DEFAULT);
                        if (FAILED(h))
                            return DIENUM_CONTINUE;

                        m_pDeviceArray->Add(new Device(lpdid));
                        return DIENUM_CONTINUE;
                    }
                }

                return DIENUM_CONTINUE;
            }

            DirectInput::DirectInput() :
                Direct3DDevice(NULL),
                m_pDI(NULL),
                m_pDeviceArray(new Collections::ArrayList(5)),
                Actions(new ActionMap()),
                ActionMapName(NULL),
                Genre(Genres::None),
                AppID(Guid::Empty),
                m_bCreated(false)
            {
            }

            DirectInput::~DirectInput()
            {
                if (m_pDI != NULL)
                {
                    m_pDI->Release();
                    m_pDI = NULL;
                }
                if (m_pActions != NULL)
                {
                    for (DWORD i = 0; i < m_diaf.dwNumActions; i++)
                        CoTaskMemFree((LPVOID)m_pActions[i].lptszActionName);
                    delete m_pActions;
                    m_pActions = NULL;
                }
            }

            // Creates DirectInput device objects and configure action maps
            void DirectInput::Create()
            {
                if (m_bCreated)
                    return;

                if (m_pParentWindow == NULL)
                    throw new ArgumentNullException(S"ParentWindow");

                IDirectInput8 __nogc *pDI;

                HRESULT h;

                // Create an IDirectInput8 object that we will use to create devices.
                h = ::DirectInput8Create(::GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8,
                    (void __nogc * __nogc *)&pDI, NULL);
                if (FAILED(h))
                    throw new Sunlight::DirectX::DirectXException(S"::DirectInput8Create", h);

                m_pDI = pDI;

                BuildDIAF();

                EnumDevices();

                m_bCreated = true;
            }

            // Builds the DIACTIONFORMAT structure for this object.
            void DirectInput::BuildDIAF()
            {
                DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

                ZeroMemory(pDIAF, sizeof(DIACTIONFORMAT));
                m_diaf.dwSize = sizeof(DIACTIONFORMAT);
                m_diaf.dwActionSize = sizeof(DIACTION);
                m_diaf.dwNumActions = Actions->Count;
                m_diaf.dwDataSize = m_diaf.dwNumActions * sizeof(DWORD);

                // Copy the GUID across.
                Marshal::Copy(AppID.ToByteArray(), 0, &pDIAF->guidActionMap, sizeof(GUID));

                m_pActions = new DIACTION[m_diaf.dwNumActions];
                ZeroMemory(m_pActions, sizeof(DIACTION) * m_diaf.dwNumActions);

                m_diaf.rgoAction = m_pActions;

                for (DWORD i = 0; i < m_diaf.dwNumActions; i++)
                {
                    ActionMap::Entry *pEntry = Actions->Item(i);
                    m_pActions[i].uAppData = pEntry->ID;
                    m_pActions[i].dwSemantic = pEntry->Semantic;
#ifdef _UNICODE
                    LPWSTR  p = (LPWSTR)(void *)Marshal::StringToCoTaskMemUni(pEntry->Name);
#else
                    LPSTR   p = (LPSTR)(void *)Marshal::StringToCoTaskMemAnsi(pEntry->Name);
#endif
                    m_pActions[i].lptszActionName = p;
                }

                m_diaf.dwGenre = Genre;
                m_diaf.dwBufferSize = 16;
                m_diaf.lAxisMin = -100;
                m_diaf.lAxisMax = 100;

#ifdef _UNICODE
                LPWSTR  p = (LPWSTR)(void *)Marshal::StringToCoTaskMemUni(ActionMapName);
#else
                LPSTR   p = (LPSTR)(void *)Marshal::StringToCoTaskMemAnsi(ActionMapName);
#endif
                lstrcpy(pDIAF->tszActionMap, p);
                CoTaskMemFree(p);
            }

            // Enumerates devices and builds the device array.
            void DirectInput::EnumDevices()
            {
                DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

                HRESULT h = CallEnumDevicesBySemantics(m_pDI, NULL, pDIAF, 
                    new DIEnumDevicesBySemanticsCallbackDelegate(this, &DirectInput::EnumDevicesBySemanticsCallback),
                    NULL, DIEDBSFL_ATTACHEDONLY);
                if (FAILED(h))
                    throw new Sunlight::DirectX::DirectXException(S"IDirectInput8::EnumDevicesBySemantics", h);
            }

            // Called when the ParentWindow is activated.
            void DirectInput::OnFormActivated(Object *sender, EventArgs *e)
            {
                Acquire();
            }

            // Called when the ParentWindow is deactivated.
            void DirectInput::OnFormDeactivated(Object *sender, EventArgs *e)
            {
                Unacquire();
            }

            // Acquires the devices for use by this application.
            void DirectInput::Acquire()
            {
                Create();

                Cursor::Hide();

                for (int iDevice = 0; iDevice < m_pDeviceArray->Count; iDevice++)
                    static_cast<Device *>(m_pDeviceArray->Item[iDevice])->m_pDevice->Acquire();
            }
            // Release the devices for use by other applications.
            void DirectInput::Unacquire()
            {
                for (int iDevice = 0; iDevice < m_pDeviceArray->Count; iDevice++)
                    static_cast<Device *>(m_pDeviceArray->Item[iDevice])->m_pDevice->Unacquire();

                Cursor::Show();
            }

            // The Device object with which this texture will be created.
            Form *DirectInput::get_ParentWindow()
            {
                return m_pParentWindow;
            }
            void DirectInput::set_ParentWindow(Form *pParentWindow)
            {
                m_pParentWindow = pParentWindow;

                // Attach to the parent window Activated and Deactivated events, to acquire and unacquire.
                __hook(&System::Windows::Forms::Form::Activated, m_pParentWindow, &DirectInput::OnFormActivated, this);
                __hook(&System::Windows::Forms::Form::Deactivate, m_pParentWindow, &DirectInput::OnFormDeactivated, this);
                __hook(&System::Windows::Forms::Form::Closed, m_pParentWindow, &DirectInput::OnFormDeactivated, this);
            }

#define INPUT_DATA_LIMIT    20

            // Checks the devices for updated data.
            void DirectInput::Check()
            {
                DIDEVICEOBJECTDATA  pdidod[INPUT_DATA_LIMIT];
                DWORD               dwObjCount;

                if (m_pDeviceArray == NULL)
                    return;

                for (int iDevice = 0; iDevice < m_pDeviceArray->Count; iDevice++)
                {
                    Device *pDev = static_cast<Device *>(m_pDeviceArray->Item[iDevice]);
                    // Poll the device for data.
                    pDev->m_pDevice->Poll();
               
                    // Retrieve the data.
                    dwObjCount = INPUT_DATA_LIMIT;
                    pDev->m_pDevice->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), pdidod, &dwObjCount, 0);

                    if (Action != NULL)
                    {
                        for (DWORD i = 0; i < dwObjCount; i++)
                            __raise Action(this, new DirectInputEventArgs(pdidod[i].uAppData, (int)pdidod[i].dwData));
                    }
                }
            }

            // Callback function to display configuration interface
            void DirectInput::ConfigureDevicesCallback(int lpDDSTarget, int /*pvRef*/)
            {
                IDirect3DSurface8   *pSurface, *pBackBuffer;

                if (!Direct3DDevice->Paused)
                {
                    ((::IUnknown *)lpDDSTarget)->QueryInterface(IID_IDirect3DSurface8, (void **)&pSurface);
                    ((LPDIRECT3DDEVICE8)Direct3DDevice->Direct3DDevice)->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &pBackBuffer);

                    ((LPDIRECT3DDEVICE8)Direct3DDevice->Direct3DDevice)->CopyRects(pSurface, NULL, 0, pBackBuffer, NULL);
                    Direct3DDevice->Flip();

                    pSurface->Release();
                    pBackBuffer->Release();
                }
            }

            // Opens a dialog box allowing the user to configure input devices.
            void DirectInput::Configure()
            {
                Create();

                HRESULT h;

                DICONFIGUREDEVICESPARAMS dicdp;
                DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

                ZeroMemory(&dicdp, sizeof(dicdp));

                dicdp.dwSize = sizeof(dicdp);
                dicdp.dwcUsers = 1;
                dicdp.lptszUserNames = NULL;
                dicdp.dwcFormats = 1;
                dicdp.lprgFormats = pDIAF;
                dicdp.hwnd = (HWND)m_pParentWindow->Handle.ToPointer();
                dicdp.lpUnkDDSTarget = NULL;

                if (Direct3DDevice != NULL)
                {
                    IDirect3DSurface8 *pSurface;

                    h = ((LPDIRECT3DDEVICE8)Direct3DDevice->Direct3DDevice)->CreateImageSurface(Direct3DDevice->Width, Direct3DDevice->Height,
                        (D3DFORMAT)Direct3DDevice->Format, &pSurface);
                    if (FAILED(h))
                        throw new DirectXException(S"IDirect3DDevice8::CreateImageSurface", h);
        
                    pSurface->QueryInterface(IID_IUnknown, (void **)&dicdp.lpUnkDDSTarget);
                    pSurface->Release();
                }

                // Set up a colour set, which will allow us to make the 
                //  configuration box look like the rest of our program.
                //  If this is initialised to zero, DirectInput will use 
                //  the default colour set.
                dicdp.dics.dwSize = sizeof(DICOLORSET);

                // Let go of any devices so that the configuration 
                //  interface can have a go
                Unacquire();

                // Display action configuration
                if (dicdp.lpUnkDDSTarget == NULL)
                    h = m_pDI->ConfigureDevices(NULL, &dicdp, DICD_EDIT, NULL);
                else
                {
                    h = CallConfigureDevices(m_pDI, 
                        new DIConfigureDevicesCallbackDelegate(this, &DirectInput::ConfigureDevicesCallback), 
                        &dicdp, DICD_EDIT, NULL);
                    dicdp.lpUnkDDSTarget->Release();
                }
                if (FAILED(h))
                {
                    Acquire();
                    throw new DirectXException(S"IDirectInput8::ConfigureDevices", h);
                }

                // Devices are no longer valid, so reinitialise.
                m_pDeviceArray->Clear();

                EnumDevices();

                Acquire();
            }

            // Opens a dialog box allowing the user to view the configuration of input devices.
            void DirectInput::Display()
            {
                Create();

                HRESULT h;

                DICONFIGUREDEVICESPARAMS dicdp;
                DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

                ZeroMemory(&dicdp, sizeof(dicdp));

                dicdp.dwSize = sizeof(dicdp);
                dicdp.dwcUsers = 1;
                dicdp.lptszUserNames = NULL;
                dicdp.dwcFormats = 1;
                dicdp.lprgFormats = pDIAF;
                dicdp.hwnd = (HWND)m_pParentWindow->Handle.ToPointer();
                dicdp.lpUnkDDSTarget = NULL;

                if (Direct3DDevice != NULL)
                {
                    IDirect3DSurface8 *pSurface;

                    h = ((LPDIRECT3DDEVICE8)Direct3DDevice->Direct3DDevice)->CreateImageSurface(Direct3DDevice->Width, Direct3DDevice->Height,
                        (D3DFORMAT)Direct3DDevice->Format, &pSurface);
                    if (FAILED(h))
                        throw new DirectXException(S"IDirect3DDevice8::CreateImageSurface", h);
        
                    pSurface->QueryInterface(IID_IUnknown, (void **)&dicdp.lpUnkDDSTarget);
                    pSurface->Release();
                }

                // Set up a colour set, which will allow us to make the configuration box
                //  look like the rest of our program.
                // If this is initialised to zero, DirectInput will use the default colour set.
                dicdp.dics.dwSize = sizeof(DICOLORSET);

                // Let go of any devices so that the configuration interface can have a go
                Unacquire();

                // Display action configuration
                if (dicdp.lpUnkDDSTarget == NULL)
                    h = m_pDI->ConfigureDevices(NULL, &dicdp, DICD_DEFAULT, NULL);
                else
                {
                    h = CallConfigureDevices(m_pDI, 
                        new DIConfigureDevicesCallbackDelegate(this, &DirectInput::ConfigureDevicesCallback), 
                        &dicdp, DICD_DEFAULT, NULL);
                    dicdp.lpUnkDDSTarget->Release();
                }

                Acquire();

                if (FAILED(h))
                    throw new DirectXException(S"IDirectInput8::ConfigureDevices", h);
            }
        }
    }
}