#ifdef _WIN32

#undef NDEBUG
#include "win32-joystick.hpp"
#include "compat/sleep.hpp"
#include <cassert>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <objbase.h>

#include <QDebug>

QMutex win32_joy_ctx::enum_state::mtx;
win32_joy_ctx::enum_state win32_joy_ctx::enumerator;

void win32_joy_ctx::poll(fn f)
{
    //refresh(false);

    QMutexLocker l(&enumerator.mtx);

    auto& joys = enumerator.get_joys();

    for (auto& j : joys)
    {
        j.second->poll(f);
    }
}

bool win32_joy_ctx::poll_axis(const QString &guid, int* axes)
{
    QMutexLocker l(&enumerator.mtx);

    for (int k = 0; k < 10; k++)
    {
        if (k > 0)
            enumerator.refresh();

        const joys_t& joys = enumerator.get_joys();
        auto iter = joys.find(guid);

        if (iter == joys.end())
            return false;

        auto& j = iter->second;

        auto& joy_handle = j->joy_handle;
        bool ok = false;
        HRESULT hr;

        if (!FAILED(hr = joy_handle->Poll()))
        {
            ok = true;
        }

        if (!ok && FAILED(hr = joy_handle->Acquire()))
        {
            //qDebug() << "joy acquire failed" << hr;
        }

        if (!ok)
        {
            portable::sleep(25);
            (void) joy_handle->Unacquire();
            continue;
        }

        DIJOYSTATE2 js;
        std::memset(&js, 0, sizeof(js));

        if (FAILED(hr = joy_handle->GetDeviceState(sizeof(js), &js)))
        {
            //qDebug() << "joy get state failed" << guid << hr;
            portable::sleep(50);
            continue;
        }

        const int values[] =
        {
            js.lX,
            js.lY,
            js.lZ,
            js.lRx,
            js.lRy,
            js.lRz,
            js.rglSlider[0],
            js.rglSlider[1]
        };

        for (int i = 0; i < 8; i++)
            axes[i] = values[i];

        return true;
    }

    return false;
}

std::vector<win32_joy_ctx::joy_info> win32_joy_ctx::get_joy_info()
{
    std::vector<joy_info> ret;
    QMutexLocker l(&enumerator.mtx);
    auto& joys = enumerator.get_joys();
    ret.reserve(joys.size());

    for (auto& j : joys)
        ret.push_back(joy_info { j.second->name, j.first });

    std::sort(ret.begin(), ret.end(), [&](const joy_info& fst, const joy_info& snd) -> bool { return fst.name < snd.name; });

    return ret;
}

win32_joy_ctx::win32_joy_ctx()
{
    refresh();
}

void win32_joy_ctx::refresh()
{
    QMutexLocker l(&enumerator.mtx);
    enumerator.refresh();
}

QString win32_joy_ctx::guid_to_string(const GUID& guid)
{
    char buf[40] = {0};
    wchar_t szGuidW[40] = {0};

    StringFromGUID2(guid, szGuidW, 40);
    WideCharToMultiByte(0, 0, szGuidW, -1, buf, 40, NULL, NULL);

    return QString(buf);
}

using fn = win32_joy_ctx::fn;

void win32_joy_ctx::joy::release()
{
    if (joy_handle)
    {
        (void) joy_handle->Unacquire();
        joy_handle->Release();
        joy_handle = nullptr;
    }
}

bool win32_joy_ctx::joy::poll(fn f)
{
    HRESULT hr;
    bool ok = false;

    (void) joy_handle->Acquire();

    if (!FAILED(hr = joy_handle->Poll()))
        ok = true;

    if (!ok)
    {
        qDebug() << "joy acquire failed" << guid << hr;
        (void) joy_handle->Unacquire();
        return false;
    }

    DIJOYSTATE2 js;
    std::memset(&js, 0, sizeof(js));

    if (FAILED(hr = joy_handle->GetDeviceState(sizeof(js), &js)))
    {
        qDebug() << "joy get state failed" << guid << hr;
        return false;
    }

    for (unsigned i = 0; i < 4; i++)
    {
        using std::round;

        unsigned char pos;
        unsigned pos_ = js.rgdwPOV[i];
        if ((pos_ & 0xffff) == 0xffff)
            pos = 0;
        else if (pos_ == ~0u)
            pos = 0;
        else
        {
            using uc = unsigned char;
            pos = uc(((pos_ / 9000u) % 4u) + 1u);
        }

        const bool state[] =
        {
            pos == 1,
            pos == 2,
            pos == 3,
            pos == 4
        };

        unsigned idx = 128u + i * 4u;

        for (unsigned j = 0; j < 4; j++, idx++)
        {
            if (state[j] != pressed[idx])
            {
                f(guid, int(idx), state[j]);
                pressed[idx] = state[j];
            }
        }
    }

    for (int i = 0; i < 128; i++)
    {
        const bool state = !!(js.rgbButtons[i] & 0x80);
        if (state != pressed[i])
        {
            f(guid, i, state);
        }
        pressed[i] = state;
    }

    return true;
}

win32_joy_ctx::enum_state::enum_state() : di(dinput_handle::make_di())
{
}

win32_joy_ctx::enum_state::~enum_state()
{
    QMutexLocker l(&mtx);

    joys = std::unordered_map<QString, std::shared_ptr<joy>>();
}

void win32_joy_ctx::enum_state::refresh()
{
    all.clear();

    if (!di)
    {
        qDebug() << "can't create dinput";
        return;
    }

    HRESULT hr;

    if(FAILED(hr = di->EnumDevices(DI8DEVCLASS_GAMECTRL,
                                   EnumJoysticksCallback,
                                   this,
                                   DIEDFL_ATTACHEDONLY)))
    {
        qDebug() << "failed enum joysticks" << hr;
        return;
    }

    for (auto it = joys.begin(); it != joys.end(); )
    {
        if (std::find_if(all.cbegin(), all.cend(), [&](const QString& guid2) -> bool { return it->second->guid == guid2; }) == all.end())
        {
            it = joys.erase(it);
        }
        else
        {
            ++it;
        }
    }
}

const win32_joy_ctx::joys_t& win32_joy_ctx::enum_state::get_joys() const { return joys; }

BOOL CALLBACK win32_joy_ctx::enum_state::EnumJoysticksCallback(const DIDEVICEINSTANCE *pdidInstance, void *pContext)
{
    enum_state& state = *reinterpret_cast<enum_state*>(pContext);
    const QString guid = guid_to_string(pdidInstance->guidInstance);
    const QString name = QString(pdidInstance->tszInstanceName);

    const bool exists = state.joys.find(guid) != state.joys.end();

    state.all.push_back(guid);

    if (exists)
        goto end;

    {
        HRESULT hr;
        LPDIRECTINPUTDEVICE8 h;
        if (FAILED(hr = state.di->CreateDevice(pdidInstance->guidInstance, &h, nullptr)))
        {
            qDebug() << "createdevice" << guid << hr;
            goto end;
        }
        if (FAILED(h->SetDataFormat(&c_dfDIJoystick2)))
        {
            qDebug() << "format";
            h->Release();
            goto end;
        }

        // not a static member - need main() to run for some time first
        static const QWidget fake_window;

        if (FAILED(h->SetCooperativeLevel(reinterpret_cast<HWND>(fake_window.winId()), DISCL_NONEXCLUSIVE | DISCL_BACKGROUND)))
        {
            qDebug() << "coop";
            h->Release();
            goto end;
        }
        if (FAILED(hr = h->EnumObjects(EnumObjectsCallback, h, DIDFT_ALL)))
        {
            qDebug() << "enum-objects";
            h->Release();
            goto end;
        }

        state.joys[guid] = std::make_shared<joy>(h, guid, name);
    }
end:
    return DIENUM_CONTINUE;
}

BOOL CALLBACK win32_joy_ctx::enum_state::EnumObjectsCallback(const DIDEVICEOBJECTINSTANCE *pdidoi, void *ctx)
{
    if (pdidoi->dwType & DIDFT_AXIS)
    {
        DIPROPRANGE diprg;
        std::memset(&diprg, 0, sizeof(diprg));
        diprg.diph.dwSize = sizeof( DIPROPRANGE );
        diprg.diph.dwHeaderSize = sizeof( DIPROPHEADER );
        diprg.diph.dwHow = DIPH_BYID;
        diprg.diph.dwObj = pdidoi->dwType;
        diprg.lMax = joy_axis_size;
        diprg.lMin = -joy_axis_size;

        HRESULT hr;

        if (FAILED(hr = reinterpret_cast<LPDIRECTINPUTDEVICE8>(ctx)->SetProperty(DIPROP_RANGE, &diprg.diph)))
        {
            qDebug() << "DIPROP_RANGE" << hr;
            return DIENUM_STOP;
        }
    }

    return DIENUM_CONTINUE;
}

win32_joy_ctx::joy::joy(LPDIRECTINPUTDEVICE8 handle, const QString &guid, const QString &name)
    : joy_handle(handle), guid(guid), name(name)
{
    //qDebug() << "make joy" << guid << name << joy_handle;
    std::memset(pressed, 0, sizeof(pressed));
}

win32_joy_ctx::joy::~joy()
{
    //qDebug() << "nix joy" << guid;
    release();
}

#endif