#include "Win32Common.hpp"
#include <Windowsx.h>
#include "boo/IWindow.hpp"
#include "boo/IGraphicsContext.hpp"
#include <LogVisor/LogVisor.hpp>

#include "boo/graphicsdev/D3D11.hpp"
#include "boo/graphicsdev/D3D12.hpp"

namespace boo
{
static LogVisor::LogModule Log("WindowWin32");
class WindowWin32;
IGraphicsCommandQueue* _NewD3D12CommandQueue(D3D12Context* ctx, D3D12Context::Window* windowCtx, IGraphicsContext* parent);
IGraphicsCommandQueue* _NewD3D11CommandQueue(D3D11Context* ctx, D3D11Context::Window* windowCtx, IGraphicsContext* parent);

struct GraphicsContextWin32 : IGraphicsContext
{

    EGraphicsAPI m_api;
    EPixelFormat m_pf;
    WindowWin32* m_parentWindow;
    D3DAppContext& m_d3dCtx;

    ComPtr<IDXGISwapChain1> m_swapChain;
    ComPtr<IDXGIOutput> m_output;

    IGraphicsCommandQueue* m_commandQueue = nullptr;
    IGraphicsDataFactory* m_dataFactory = nullptr;

public:
    IWindowCallback* m_callback;

    GraphicsContextWin32(EGraphicsAPI api, WindowWin32* parentWindow, D3DAppContext& d3dCtx)
    : m_api(api),
      m_pf(PF_RGBA8),
      m_parentWindow(parentWindow),
      m_d3dCtx(d3dCtx)
    {
        /* Create Swap Chain */
        DXGI_SWAP_CHAIN_DESC1 scDesc = {};
        scDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        scDesc.SampleDesc.Count = 1;
        scDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        scDesc.BufferCount = 2;
        scDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
        scDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

#if _WIN32_WINNT_WIN10
        IUnknown* dev;
        if (d3dCtx.m_ctx12.m_dev)
        {
            scDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
            dev = static_cast<IUnknown*>(d3dCtx.m_ctx12.m_dev.Get());
        }
        else
            dev = static_cast<IUnknown*>(d3dCtx.m_ctx11.m_dev.Get());
#else
        IUnknown* dev = static_cast<IUnknown*>(d3dCtx.m_ctx11.m_dev.Get());
#endif
        if (FAILED(d3dCtx.m_dxFactory->CreateSwapChainForHwnd(dev, 
            parentWindow->m_hwnd, &scDesc, nullptr, nullptr, &m_swapChain)))
            Log.report(LogVisor::FatalError, "unable to create swap chain");

        if (FAILED(m_swapChain->GetContainingOutput(&m_output)))
            Log.report(LogVisor::FatalError, "unable to get DXGI output");

#if _WIN32_WINNT_WIN10
        if (d3dCtx.m_ctx12.m_dev)
        {
            auto insIt = d3dCtx.m_ctx12.m_windows.emplace(std::make_pair(parentWindow, D3D12Context::Window()));
            D3D12Context::Window& w = insIt.first->second;
            m_swapChain.As<IDXGISwapChain3>(&w.m_swapChain);
            m_swapChain->GetBuffer(0, __uuidof(ID3D12Resource), &w.m_fb[0]);
            m_swapChain->GetBuffer(1, __uuidof(ID3D12Resource), &w.m_fb[1]);
            w.m_backBuf = w.m_swapChain->GetCurrentBackBufferIndex();
            D3D12_RESOURCE_DESC resDesc = w.m_fb[0]->GetDesc();
            w.width = resDesc.Width;
            w.height = resDesc.Height;
            m_dataFactory = new D3D12DataFactory(this, &d3dCtx.m_ctx12);
            m_commandQueue = _NewD3D12CommandQueue(&d3dCtx.m_ctx12, &w, this);
        }
        else
#endif
        {
            auto insIt = d3dCtx.m_ctx11.m_windows.emplace(std::make_pair(parentWindow, D3D11Context::Window()));
            D3D11Context::Window& w = insIt.first->second;
            ComPtr<ID3D11Texture2D> fbRes;
            m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), &fbRes);
            D3D11_TEXTURE2D_DESC resDesc;
            fbRes->GetDesc(&resDesc);
            w.width = resDesc.Width;
            w.height = resDesc.Height;
            m_dataFactory = new D3D11DataFactory(this, &d3dCtx.m_ctx11);
            m_commandQueue = _NewD3D11CommandQueue(&d3dCtx.m_ctx11, &insIt.first->second, this);
        }
    }

    ~GraphicsContextWin32()
    {
#if _WIN32_WINNT_WIN10
        if (m_d3dCtx.m_ctx12.m_dev)
        {
            m_d3dCtx.m_ctx12.m_windows.erase(m_parentWindow);
        }
        else
#endif
        {
        }
    }

    void _setCallback(IWindowCallback* cb)
    {
        m_callback = cb;
    }

    EGraphicsAPI getAPI() const
    {
        return m_api;
    }

    EPixelFormat getPixelFormat() const
    {
        return m_pf;
    }

    void setPixelFormat(EPixelFormat pf)
    {
        if (pf > PF_RGBAF32_Z24)
            return;
        m_pf = pf;
    }

    void initializeContext() {}

    void makeCurrent() {}

    void postInit() {}

    void present() {}

    IGraphicsCommandQueue* getCommandQueue()
    {

    }

    IGraphicsDataFactory* getDataFactory()
    {

    }

    /* Creates a new context on current thread!! Call from client loading thread */
    IGraphicsDataFactory* getLoadContextDataFactory()
    {

    }
};

static void genFrameDefault(MONITORINFO* screen, int& xOut, int& yOut, int& wOut, int& hOut)
{
    float width = screen->rcMonitor.right * 2.0 / 3.0;
    float height = screen->rcMonitor.bottom * 2.0 / 3.0;
    xOut = (screen->rcMonitor.right - width) / 2.0;
    yOut = (screen->rcMonitor.bottom - height) / 2.0;
    wOut = width;
    hOut = height;
}

static uint32_t translateKeysym(WPARAM sym, int& specialSym, int& modifierSym)
{
    specialSym = KEY_NONE;
    modifierSym = MKEY_NONE;
    if (sym >= VK_F1 && sym <= VK_F12)
        specialSym = KEY_F1 + sym - VK_F1;
    else if (sym == VK_ESCAPE)
        specialSym = KEY_ESC;
    else if (sym == VK_RETURN)
        specialSym = KEY_ENTER;
    else if (sym == VK_BACK)
        specialSym = KEY_BACKSPACE;
    else if (sym == VK_INSERT)
        specialSym = KEY_INSERT;
    else if (sym == VK_DELETE)
        specialSym = KEY_DELETE;
    else if (sym == VK_HOME)
        specialSym = KEY_HOME;
    else if (sym == VK_END)
        specialSym = KEY_END;
    else if (sym == VK_PRIOR)
        specialSym = KEY_PGUP;
    else if (sym == VK_NEXT)
        specialSym = KEY_PGDOWN;
    else if (sym == VK_LEFT)
        specialSym = KEY_LEFT;
    else if (sym == VK_RIGHT)
        specialSym = KEY_RIGHT;
    else if (sym == VK_UP)
        specialSym = KEY_UP;
    else if (sym == VK_DOWN)
        specialSym = KEY_DOWN;
    else if (sym == VK_LSHIFT || sym == VK_RSHIFT)
        modifierSym = MKEY_SHIFT;
    else if (sym == VK_LCONTROL || sym == VK_RCONTROL)
        modifierSym = MKEY_CTRL;
    else if (sym == VK_MENU)
        modifierSym = MKEY_ALT;
    else
        return MapVirtualKey(sym, MAPVK_VK_TO_CHAR);
    return 0;
}

static int translateModifiers()
{
    int retval = 0;
    if (GetKeyState(VK_LSHIFT) & 0x8000 != 0 || GetKeyState(VK_RSHIFT) & 0x8000 != 0)
        retval |= MKEY_SHIFT;
    if (GetKeyState(VK_LCONTROL) & 0x8000 != 0 || GetKeyState(VK_RCONTROL) & 0x8000 != 0)
        retval |= MKEY_CTRL;
    if (GetKeyState(VK_MENU) & 0x8000 != 0)
        retval |= MKEY_ALT;
    return retval;
}
    
class WindowWin32 : public IWindow
{
    friend GraphicsContextWin32;
    HWND m_hwnd;
    std::unique_ptr<GraphicsContextWin32> m_gfxCtx;
    IWindowCallback* m_callback = nullptr;
    
public:
    
    WindowWin32(const SystemString& title, D3DAppContext& d3dCtx)
    {
        m_hwnd = CreateWindowW(L"BooWindow", title.c_str(), WS_OVERLAPPEDWINDOW,
                               CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                               NULL, NULL, NULL, NULL);
        m_gfxCtx.reset(new GraphicsContextWin32(IGraphicsContext::API_D3D11, this, d3dCtx));
    }
    
    ~WindowWin32()
    {
        
    }
    
    void setCallback(IWindowCallback* cb)
    {
        m_callback = cb;
    }
    
    void showWindow()
    {
        ShowWindow(m_hwnd, SW_SHOW);
    }
    
    void hideWindow()
    {
        ShowWindow(m_hwnd, SW_HIDE);
    }
    
    SystemString getTitle()
    {
         wchar_t title[256];
         int c = GetWindowTextW(m_hwnd, title, 256);
         return SystemString(title, c);
    }
    
    void setTitle(const SystemString& title)
    {
        SetWindowTextW(m_hwnd, title.c_str());
    }
    
    void setWindowFrameDefault()
    {
        MONITORINFO monInfo;
        GetMonitorInfo(MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTOPRIMARY), &monInfo);
        int x, y, w, h;
        genFrameDefault(&monInfo, x, y, w, h);
        setWindowFrame(x, y, w, h);
    }
    
    void getWindowFrame(float& xOut, float& yOut, float& wOut, float& hOut) const
    {
        RECT rct;
        GetWindowRect(m_hwnd, &rct);
        xOut = rct.left;
        yOut = rct.top;
        wOut = rct.right;
        hOut = rct.bottom;
    }

    void getWindowFrame(int& xOut, int& yOut, int& wOut, int& hOut) const
    {
        RECT rct;
        GetWindowRect(m_hwnd, &rct);
        xOut = rct.left;
        yOut = rct.top;
        wOut = rct.right;
        hOut = rct.bottom;
    }
    
    void setWindowFrame(float x, float y, float w, float h)
    {
        MoveWindow(m_hwnd, x, y, w, h, true);
    }

    void setWindowFrame(int x, int y, int w, int h)
    {
        MoveWindow(m_hwnd, x, y, w, h, true);
    }
    
    float getVirtualPixelFactor() const
    {
        return 1.0;
    }
    
    bool isFullscreen() const
    {
        return false;
    }
    
    void setFullscreen(bool fs)
    {
        
    }

    void waitForRetrace()
    {
        m_gfxCtx->m_output->WaitForVBlank();
    }

    uintptr_t getPlatformHandle() const
    {
        return uintptr_t(m_hwnd);
    }

    void buttonDown(HWNDEvent& e, EMouseButton button)
    {
        if (m_callback)
        {
            int x, y, w, h;
            getWindowFrame(x, y, w, h);
            int modifierMask = translateModifiers();
            SWindowCoord coord =
            {
                {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                {float(GET_X_LPARAM(e.lParam)) / float(w), float(GET_Y_LPARAM(e.lParam)) / float(h)}
            };
            m_callback->mouseDown(coord, button, EModifierKey(modifierMask));
        }
    }

    void buttonUp(HWNDEvent& e, EMouseButton button)
    {
        if (m_callback)
        {
            int x, y, w, h;
            getWindowFrame(x, y, w, h);
            int modifierMask = translateModifiers();
            SWindowCoord coord =
            {
                {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                {float(GET_X_LPARAM(e.lParam)) / float(w), float(GET_Y_LPARAM(e.lParam)) / float(h)}
            };
            m_callback->mouseUp(coord, button, EModifierKey(modifierMask));
        }
    }
    
    void _incomingEvent(void* ev)
    {
        HWNDEvent& e = *static_cast<HWNDEvent*>(ev);
        switch (e.uMsg)
        {
        case WM_SIZE:
        {
            if (m_callback)
            {
                SWindowRect rect;
                int x, y, w, h;
                getWindowFrame(x, y, w, h);
                rect.location[0] = x;
                rect.location[1] = y;
                rect.size[0] = LOWORD(e.lParam);
                rect.size[1] = HIWORD(e.lParam);
                m_callback->resized(rect);
            }
            return;
        }
        case WM_KEYDOWN:
        {
            if (m_callback)
            {
                int specialKey;
                int modifierKey;
                uint32_t charCode = translateKeysym(e.wParam, specialKey, modifierKey);
                int modifierMask = translateModifiers();
                if (charCode)
                    m_callback->charKeyDown(charCode, EModifierKey(modifierMask), e.lParam & 0xffff != 0);
                else if (specialKey)
                    m_callback->specialKeyDown(ESpecialKey(specialKey), EModifierKey(modifierMask), e.lParam & 0xffff != 0);
                else if (modifierKey)
                    m_callback->modKeyDown(EModifierKey(modifierKey), e.lParam & 0xffff != 0);
            }
            return;
        }
        case WM_KEYUP:
        {
            if (m_callback)
            {
                int specialKey;
                int modifierKey;
                uint32_t charCode = translateKeysym(e.wParam, specialKey, modifierKey);
                int modifierMask = translateModifiers();
                if (charCode)
                    m_callback->charKeyUp(charCode, EModifierKey(modifierMask));
                else if (specialKey)
                    m_callback->specialKeyUp(ESpecialKey(specialKey), EModifierKey(modifierMask));
                else if (modifierKey)
                    m_callback->modKeyUp(EModifierKey(modifierKey));
            }
            return;
        }
        case WM_LBUTTONDOWN:
        {
            buttonDown(e, BUTTON_PRIMARY);
            return;
        }
        case WM_LBUTTONUP:
        {
            buttonUp(e, BUTTON_PRIMARY);
            return;
        }
        case WM_RBUTTONDOWN:
        {
            buttonDown(e, BUTTON_SECONDARY);
            return;
        }
        case WM_RBUTTONUP:
        {
            buttonUp(e, BUTTON_SECONDARY);
            return;
        }
        case WM_MBUTTONDOWN:
        {
            buttonDown(e, BUTTON_MIDDLE);
            return;
        }
        case WM_MBUTTONUP:
        {
            buttonUp(e, BUTTON_MIDDLE);
            return;
        }
        case WM_XBUTTONDOWN:
        {
            if (HIWORD(e.wParam) == XBUTTON1)
                buttonDown(e, BUTTON_AUX1);
            else if (HIWORD(e.wParam) == XBUTTON2)
                buttonDown(e, BUTTON_AUX2);
            return;
        }
        case WM_XBUTTONUP:
        {
            if (HIWORD(e.wParam) == XBUTTON1)
                buttonUp(e, BUTTON_AUX1);
            else if (HIWORD(e.wParam) == XBUTTON2)
                buttonUp(e, BUTTON_AUX2);
            return;
        }
        case WM_MOUSEMOVE:
        {
            if (m_callback)
            {
                int x, y, w, h;
                getWindowFrame(x, y, w, h);
                SWindowCoord coord =
                {
                    {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                    {(unsigned)GET_X_LPARAM(e.lParam), (unsigned)GET_Y_LPARAM(e.lParam)},
                    {float(GET_X_LPARAM(e.lParam)) / float(w), float(GET_Y_LPARAM(e.lParam)) / float(h)}
                };
                m_callback->mouseMove(coord);
            }
            return;
        }
        default: break;
        }
    }

    ETouchType getTouchType() const
    {
        return TOUCH_NONE;
    }

    IGraphicsCommandQueue* getCommandQueue()
    {
        return m_gfxCtx->getCommandQueue();
    }
    IGraphicsDataFactory* getDataFactory()
    {
        return m_gfxCtx->getDataFactory();
    }

    /* Creates a new context on current thread!! Call from client loading thread */
    IGraphicsDataFactory* getLoadContextDataFactory()
    {
        return m_gfxCtx->getLoadContextDataFactory();
    }
    
};

IWindow* _WindowWin32New(const SystemString& title, D3DAppContext& d3dCtx)
{
    return new WindowWin32(title, d3dCtx);
}
    
}