#include "ui/CSimpleTop.hpp"
#include "gx/Camera.hpp"
#include "gx/Coordinate.hpp"
#include "gx/Device.hpp"
#include "gx/Font.hpp"
#include "gx/Screen.hpp"
#include "ui/CSimpleFrame.hpp"
#include "ui/CSimpleRender.hpp"
#include "ui/CSimpleTexture.hpp"
#include "ui/CSimpleTitleRegion.hpp"
#include "cursor/Cursor.hpp"
#include <cstring>
#include <tempest/Math.hpp>

CSimpleTop* CSimpleTop::s_instance;
uint32_t CSimpleTop::m_eventTime;

void PaintScreen(void* param, const RECTF* rect, const RECTF* visible, float elapsedSec) {
    CSimpleTop::s_instance->OnLayerUpdate(elapsedSec);
    CSimpleTop::s_instance->OnLayerRender();
}

int32_t CSimpleTop::OnChar(const EVENT_DATA_CHAR* pCharData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);

    int32_t eaten = 0;

    CCharEvent charEvent;
    charEvent = *pCharData;
    charEvent.id = 0x40060067;

    for (int32_t strata = FRAME_STRATA_TOOLTIP; strata >= FRAME_STRATA_WORLD; strata--) {
        if (eaten) {
            break;
        }

        auto priorities = &top->m_eventqueue[strata][SIMPLE_EVENT_CHAR];

        for (uint32_t i = 0; i < priorities->Count(); i++) {
            auto priority = priorities->operator[](i);

            if (!priority || eaten) {
                break;
            }

            auto frame = priority->frame;

            eaten = frame->OnLayerChar(charEvent);
        }
    }

    return eaten == 0;
}

int32_t CSimpleTop::OnDisplaySizeChanged(const EVENT_DATA_SIZE* pSizeData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);

    GxuFontWindowSizeChanged();

    for (auto frame = top->m_frames.Head(); frame; frame = top->m_frames.Link(frame)->Next()) {
        frame->OnScreenSizeChanged();
    }

    if (top->m_displaySizeCallback) {
        CSizeEvent sizeEvent;
        sizeEvent = *pSizeData;
        sizeEvent.id = 0x40040064;

        top->m_displaySizeCallback(sizeEvent);
    }

    return 1;
}

int32_t CSimpleTop::OnFocusChanged(const void* a1, void* a2) {
    // TODO
    return 0;
}

int32_t CSimpleTop::OnIme(const void* a1, void* a2) {
    // TODO
    return 0;
}

int32_t CSimpleTop::OnKeyDown(const EVENT_DATA_KEY* pKeyData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);
    CSimpleTop::m_eventTime = pKeyData->time;

    CKeyEvent keyEvent;
    keyEvent = *pKeyData;
    keyEvent.id = 0x40060064;

    // TODO
    // if (top->unk1239) {
    //     top->unk1239(keyEvent);
    // }

    int32_t eaten = 0;

    for (int32_t strata = FRAME_STRATA_TOOLTIP; strata >= FRAME_STRATA_WORLD; strata--) {
        if (eaten) {
            break;
        }

        auto priorities = &top->m_eventqueue[strata][SIMPLE_EVENT_KEY];

        for (uint32_t i = 0; i < priorities->Count(); i++) {
            auto priority = priorities->operator[](i);

            if (!priority || eaten) {
                break;
            }

            auto frame = priority->frame;

            eaten = frame->OnLayerKeyDown(keyEvent);

            if (eaten) {
                top->m_keydownCapture[keyEvent.key] = frame;
            }
        }
    }

    return eaten == 0;
}

int32_t CSimpleTop::OnKeyDownRepeat(const void* a1, void* a2) {
    // TODO
    return 0;
}

int32_t CSimpleTop::OnKeyUp(const EVENT_DATA_KEY* pKeyData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);
    CSimpleTop::m_eventTime = pKeyData->time;

    int32_t eaten = 0;

    CSimpleFrame* frame = top->m_keydownCapture[pKeyData->key];

    if (frame) {
        CKeyEvent keyEvent;
        keyEvent = *pKeyData;
        keyEvent.id = 0x40060066;

        frame->OnLayerKeyUp(keyEvent);

        eaten = 1;
    } else if (pKeyData->key == KEY_PRINTSCREEN) {
        eaten = CSimpleTop::OnKeyDown(pKeyData, param);
    }

    top->m_keydownCapture[pKeyData->key] = nullptr;

    return eaten == 0;
}

int32_t CSimpleTop::OnMouseDown(const EVENT_DATA_MOUSE* pMouseData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);

    CMouseEvent mouseEvent;
    mouseEvent = *pMouseData;
    mouseEvent.id = 0x400500C8;

    CSimpleTop::m_eventTime = pMouseData->time;

    // TODO
    // bool v4 = top->m_layout.enabled;
    bool enabled = false;

    memcpy(&top->m_mousePosition, pMouseData, sizeof(top->m_mousePosition));

    if (enabled && (EventIsKeyDown(KEY_LCONTROL) || EventIsKeyDown(KEY_LALT))) {
        int32_t v5 = EventIsKeyDown(KEY_LCONTROL);
        top->StartMoveOrResizeFrame(mouseEvent, MOVERESIZE_REASON1, v5);

        return 0;
    }

    auto mouseButtonCallback = top->m_mouseButtonCallback;

    if (!top->m_mouseCapture && mouseButtonCallback && mouseButtonCallback(&mouseEvent)) {
        return 0;
    }

    CSimpleFrame* target = top->m_mouseCapture;

    if (!target) {
        target = top->m_mouseFocus;
    }

    if (!target) {
        return 1;
    }

    target->Raise();

    CSimpleTitleRegion* titleRegion = target->m_titleRegion;

    C2Vector pt = { mouseEvent.x, mouseEvent.y };

    if (titleRegion && titleRegion->PtInFrameRect(pt)) {
        top->StartMoveOrResizeFrame(target, MOVERESIZE_REASON2, mouseEvent.x, mouseEvent.y, 4);
    } else {
        top->m_mouseCapture = target;
        target->OnLayerMouseDown(mouseEvent, nullptr);
    }

    return 0;
}

int32_t CSimpleTop::OnMouseMove(const EVENT_DATA_MOUSE* pMouseData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);

    CMouseEvent mouseEvent;
    mouseEvent = *pMouseData;
    mouseEvent.id = 0x400500CA;

    int32_t v18 = pMouseData != &top->m_mousePosition;

    if (v18) {
        if (pMouseData->x == top->m_mousePosition.x && pMouseData->y == top->m_mousePosition.y) {
            return 0;
        }

        CSimpleTop::m_eventTime = pMouseData->time;
    }

    memcpy(&top->m_mousePosition, pMouseData, sizeof(top->m_mousePosition));

    CSimpleFrame* lastFocus = top->m_mouseFocus;
    CSimpleFrame* nextFocus = nullptr;

    if (top->m_layout.frame) {
        CMouseEvent evt;
        evt = *pMouseData;

        top->MoveOrResizeFrame(evt);

        return 0;
    }

    auto mousePositionCallback = top->m_mousePositionCallback;

    if (mousePositionCallback && mousePositionCallback(&mouseEvent)) {
        return 0;
    }

    CSimpleFrame* mouseCapture = top->m_mouseCapture;

    if (mouseCapture) {
        mouseCapture->OnLayerTrackUpdate(mouseEvent);
    }

    for (int32_t strata = FRAME_STRATA_DIALOG; strata >= FRAME_STRATA_WORLD; strata--) {
        auto priorities = &top->m_eventqueue[strata][SIMPLE_EVENT_MOUSE];

        for (int32_t i = 0; i < priorities->Count(); i++) {
            auto priority = priorities->operator[](i);
            auto frame = priority->frame;

            if (frame->OnLayerTrackUpdate(mouseEvent)) {
                nextFocus = frame;
                break;
            }
        }

        if (nextFocus) {
            break;
        }
    }

    if (nextFocus != lastFocus) {
        top->m_mouseFocus = nextFocus;

        if (lastFocus) {
            lastFocus->OnLayerCursorExit(v18, 0);
        }

        if (nextFocus) {
            nextFocus->OnLayerCursorEnter(v18);
        }
    }

    return nextFocus == nullptr;
}

int32_t CSimpleTop::OnMouseMoveRelative(const EVENT_DATA_MOUSE* pMouseData, void* param) {
    // TODO
    return 0;
}

int32_t CSimpleTop::OnMouseUp(const EVENT_DATA_MOUSE* pMouseData, void* param) {
    CSimpleTop* top = static_cast<CSimpleTop*>(param);

    CMouseEvent mouseEvent;
    mouseEvent = *pMouseData;
    mouseEvent.id = 0x400500C9;

    CSimpleTop::m_eventTime = pMouseData->time;

    if (top->m_layout.anchor == 1 || top->m_layout.anchor == 2) {
        if (top->m_layout.frame) {
            top->m_layout.frame->UnflattenFrame(top);
        }

        top->m_layout.frame = nullptr;
        top->m_layout.anchor = FRAMEPOINT_TOPLEFT;

        return 0;
    }

    // TODO
    // if (v11 && !top->m_mouseCapture && v11(mouseEvent)) {
    //     return 0;
    // }

    if (!top->m_mouseCapture) {
        return 1;
    }

    top->m_mouseCapture->OnLayerMouseUp(mouseEvent, nullptr);

    if (!mouseEvent.buttonState) {
        top->m_mouseCapture = nullptr;
    }

    return 0;
}

int32_t CSimpleTop::OnMouseWheel(const EVENT_DATA_MOUSE* pMouseData, void* param) {
    // TODO
    return 0;
}

float CSimpleTop::RoundToPixelHeight(float ddcHeight) {
    if (abs(ddcHeight) < 0.00000023841858) {
        return ddcHeight;
    }

    CRect windowRect = { 0.0f, 0.0f, 0.0f, 0.0f };
    g_theGxDevicePtr->CapsWindowSizeInScreenCoords(windowRect);
    float windowHeight = windowRect.maxY - windowRect.minY;

    if (abs(windowHeight) < 0.00000023841858) {
        return ddcHeight;
    }

    auto ndcHeight = DDCToNDCHeight(ddcHeight);
    auto pixelHeight = ndcHeight * windowHeight;
    auto roundedPixelHeight = CMath::fuint_pi(pixelHeight);
    auto roundedNdcHeight = static_cast<float>(roundedPixelHeight) / windowHeight;
    auto roundedDdcHeight = NDCToDDCHeight(roundedNdcHeight);

    return roundedDdcHeight;
}

CSimpleTop::CSimpleTop() : CLayoutFrame() {
    // TODO

    CSimpleTop::s_instance = this;

    ScrnLayerCreate(nullptr, 1.0, 0x4, nullptr, &PaintScreen, &this->m_screenLayer);

    this->EnableEvents();

    // TODO
    // memset(this->m_keydownCapture, 0, sizeof(this->m_keydownCapture));

    NDCToDDC(1.0f, 1.0f, &this->m_rect.maxX, &this->m_rect.maxY);

    this->m_flags |= 0x1;

    // TODO

    for (int32_t s = 0; s < NUM_FRAME_STRATA; s++) {
        auto m = SMemAlloc(sizeof(CFrameStrata), __FILE__, __LINE__, 0x0);
        this->m_strata[s] = new (m) CFrameStrata();
    }

    // TODO

    CSimpleRender::Init();
    CSimpleTexture::Init();
}

void CSimpleTop::CompressStrata(int32_t strata) {
    // TODO
}

void CSimpleTop::EnableEvents() {
    EventRegisterEx(EVENT_ID_CHAR, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnChar), this, 1.0);
    EventRegisterEx(EVENT_ID_IME, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnIme), this, 1.0);
    EventRegisterEx(EVENT_ID_KEYDOWN, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnKeyDown), this, 1.0);
    EventRegisterEx(EVENT_ID_KEYUP, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnKeyUp), this, 1.0);
    EventRegisterEx(EVENT_ID_KEYDOWN_REPEATING, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnKeyDownRepeat), this, 1.0);
    EventRegisterEx(EVENT_ID_MOUSEMOVE, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnMouseMove), this, 1.0);
    EventRegisterEx(EVENT_ID_MOUSEMOVE_RELATIVE, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnMouseMoveRelative), this, 1.0);
    EventRegisterEx(EVENT_ID_MOUSEDOWN, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnMouseDown), this, 1.0);
    EventRegisterEx(EVENT_ID_MOUSEUP, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnMouseUp), this, 1.0);
    EventRegisterEx(EVENT_ID_MOUSEWHEEL, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnMouseWheel), this, 1.0);
    EventRegisterEx(EVENT_ID_SIZE, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnDisplaySizeChanged), this, 1.0);
    EventRegisterEx(EVENT_ID_FOCUS, reinterpret_cast<EVENTHANDLERFUNC>(CSimpleTop::OnFocusChanged), this, 1.0);
}

void CSimpleTop::HideFrame(CSimpleFrame* frame, int32_t a4) {
    if (this->m_layout.frame == frame && !a4) {
        if (this->m_layout.frame) {
            this->m_layout.frame->UnflattenFrame(this);
        }

        this->m_layout.frame = nullptr;
        this->m_layout.anchor = FRAMEPOINT_TOPLEFT;
    }

    frame->UnregisterForEvents(a4);
    this->m_strata[frame->m_strata]->RemoveFrame(frame);
}

void CSimpleTop::OnLayerUpdate(float elapsedSec) {
    // TODO
    // - walk m_destroyed and perform some cleanup

    CLayoutFrame::ResizePending();

    for (int32_t s = 0; s < NUM_FRAME_STRATA; s++) {
        auto strata = this->m_strata[s];
        strata->OnLayerUpdate(elapsedSec);
    }

    CLayoutFrame::ResizePending();

    if (this->m_checkFocus) {
        this->m_checkFocus = 0;
        CSimpleTop::OnMouseMove(&this->m_mousePosition, this);
    }
}

void CSimpleTop::OnLayerRender() {
    C2Vector v13 = { 0.0f, 0.0f };
    CameraSetupScreenProjection(this->m_rect, v13, 0.0f, 0);

    for (int32_t s = 0; s < NUM_FRAME_STRATA; s++) {
        auto strata = this->m_strata[s];
        strata->BuildBatches(this->m_mouseCapture == nullptr);
        strata->RenderBatches();
    }
}

void CSimpleTop::MoveOrResizeFrame(const CMouseEvent& evt) {
    // TODO
}

void CSimpleTop::NotifyFrameLayerChanged(CSimpleFrame* frame, uint32_t layer) {
    auto strata = this->m_strata[frame->m_strata];
    auto level = strata->levels[frame->m_level];
    level->batchDirty |= 1 << layer;
    strata->batchDirty = 1;
}

void CSimpleTop::NotifyFrameMovedOrResized(CSimpleFrame* frame) {
    auto strata = this->m_strata[frame->m_strata];
    strata->levelsDirty = 1;

    if (this->m_layout.frame) {
        this->RaiseFrame(this->m_layout.frame, 0);
    }

    this->m_checkFocus = 1;
}

int32_t CSimpleTop::RaiseFrame(CSimpleFrame* frame, int32_t checkOcclusion) {
    while (frame && frame->m_flags & 0x01) {
        frame = frame->m_parent;
    }

    if (!frame) {
        return 0;
    }

    if (checkOcclusion) {
        if (!(static_cast<CLayoutFrame*>(frame)->m_flags & 0x01) || this->IsResizePending()) {
            frame->Resize(1);
        }

        int32_t occluded = this->m_strata[frame->m_strata]->FrameOccluded(frame);
        frame->SetFrameFlag(0x10, occluded);
    }

    if (frame->m_flags & 0x10) {
        this->CompressStrata(frame->m_strata);
        auto strata = this->m_strata[frame->m_strata];
        frame->SetFrameLevel(strata->topLevel, 1);
        this->m_checkFocus = 1;
    }

    return 1;
}

void CSimpleTop::RegisterForEvent(CSimpleFrame* frame, CSimpleEventType event, int32_t a4, uint32_t priority) {
    auto& queue = this->m_eventqueue[frame->m_strata][event];

    auto m = SMemAlloc(sizeof(FRAMEPRIORITY), __FILE__, __LINE__, 0x0);
    auto framePriority = new (m) FRAMEPRIORITY();

    framePriority->frame = frame;
    framePriority->priority = priority == -1 ? frame->m_level : priority;

    queue.Insert(framePriority);

    if (!a4 || event == SIMPLE_EVENT_MOUSE) {
        this->m_checkFocus = 1;
    }
}

void CSimpleTop::RegisterFrame(CSimpleFrame* frame) {
    this->m_frames.LinkToTail(frame);
}

void CSimpleTop::SetCursor(MipBits* image) {
    if (image) {
        auto cursor = g_theGxDevicePtr->CursorLock();
        memcpy(cursor, image->mip[0], CURSOR_IMAGE_BYTES);
        g_theGxDevicePtr->CursorUnlock(0, 0);
        g_theGxDevicePtr->CursorSetVisible(1);
    }
}

void CSimpleTop::ShowFrame(CSimpleFrame* frame, int32_t a3) {
    this->m_strata[frame->m_strata]->AddFrame(frame);
    frame->RegisterForEvents(a3);
}

int32_t CSimpleTop::StartMoveOrResizeFrame(const CMouseEvent& start, MOVERESIZE_REASON reason, int32_t resize) {
    // TODO
    return 0;
}

int32_t CSimpleTop::StartMoveOrResizeFrame(CSimpleFrame* frame, MOVERESIZE_REASON reason, float startx, float starty, int32_t a6) {
    // TODO
    return 0;
}

int32_t CSimpleTop::StartMoveOrResizeFrame(CSimpleFrame* frame, MOVERESIZE_REASON reason, float startx, float starty, FRAMEPOINT a6) {
    // TODO
    return 0;
}

void CSimpleTop::UnregisterForEvent(CSimpleFrame* frame, CSimpleEventType event, int32_t a4) {
    auto& queue = this->m_eventqueue[frame->m_strata][event];

    int32_t found = 0;

    for (int32_t i = 0; i < queue.Count(); i++) {
        if (queue[i]->frame == frame) {
            queue.Remove(i);
            found = 1;

            break;
        }
    }

    if (!found) {
        return;
    }

    if (!a4) {
        if (event == SIMPLE_EVENT_KEY) {
            for (int32_t i = 0; i < 787; i++) {
                if (frame == this->m_keydownCapture[i]) {
                    this->m_keydownCapture[i] = nullptr;
                }
            }
        } else if (event == SIMPLE_EVENT_MOUSE) {
            if (frame == this->m_mouseCapture) {
                this->m_mouseCapture = nullptr;
            }

            if (frame == this->m_mouseFocus) {
                this->m_mouseFocus = nullptr;
                this->m_checkFocus = 1;

                if (frame->m_intAC != 3) {
                    frame->OnLayerCursorExit(0, 1);
                }
            }
        }
    }
}

void CSimpleTop::UnregisterFrame(CSimpleFrame* frame) {
    this->HideFrame(frame, 0);

    auto framesLink = this->m_frames.Link(frame);
    if (framesLink) {
        framesLink->Unlink();
    }
}
