#include "ui/simple/CSimpleScrollFrame.hpp"
#include "gx/Camera.hpp"
#include "gx/Coordinate.hpp"
#include "gx/Transform.hpp"
#include "ui/CRenderBatch.hpp"
#include "ui/FrameXML.hpp"
#include "ui/simple/CSimpleScrollFrameScript.hpp"
#include "util/CStatus.hpp"
#include "util/Lua.hpp"
#include <common/XML.hpp>
#include <tempest/Matrix.hpp>
#include <cmath>
#include <limits>

int32_t CSimpleScrollFrame::s_metatable;
int32_t CSimpleScrollFrame::s_objectType;

void CSimpleScrollFrame::CreateScriptMetaTable() {
    lua_State* L = FrameScript_GetContext();
    int32_t ref = FrameScript_Object::CreateScriptMetaTable(L, &CSimpleScrollFrame::RegisterScriptMethods);
    CSimpleScrollFrame::s_metatable = ref;
}

int32_t CSimpleScrollFrame::GetObjectType() {
    if (!CSimpleScrollFrame::s_objectType) {
        CSimpleScrollFrame::s_objectType = ++FrameScript_Object::s_objectTypes;
    }

    return CSimpleScrollFrame::s_objectType;
}

void CSimpleScrollFrame::RegisterScriptMethods(lua_State* L) {
    CSimpleFrame::RegisterScriptMethods(L);
    FrameScript_Object::FillScriptMethodTable(L, SimpleScrollFrameMethods, NUM_SIMPLE_SCROLL_FRAME_SCRIPT_METHODS);
}

void CSimpleScrollFrame::RenderScrollChild(void* param) {
    auto frame = static_cast<CSimpleScrollFrame*>(param);

    CRect viewRect;
    if (!frame->GetHitRect(viewRect)) {
        return;
    }

    auto scrollChild = frame->m_scrollChild;
    if (!scrollChild) {
        return;
    }

    if (!scrollChild->m_visible) {
        return;
    }

    C44Matrix savedProj;
    GxXformProjection(savedProj);

    C44Matrix savedView;
    GxXformView(savedView);

    float minX, maxX, minY, maxY, minZ, maxZ;
    GxXformViewport(minX, maxX, minY, maxY, minZ, maxZ);

    // TODO clean up
    float v34 = 0.0f;
    float v35 = 0.0f;
    float v36 = 0.0f;
    float v37 = 0.0f;
    NDCToDDC(minX, minY, &v35, &v34);
    NDCToDDC(maxX, maxY, &v37, &v36);

    auto v4 = viewRect.minX;
    if (v35 > viewRect.minX) {
        v4 = v35;
        viewRect.minX = v35;
    }
    auto v5 = viewRect.maxX;
    if (v37 < viewRect.maxX) {
        v5 = v37;
        viewRect.maxX = v37;
    }
    auto v6 = viewRect.minY;
    if (v34 > viewRect.minY) {
        v6 = v34;
        viewRect.minY = v34;
    }
    auto v7 = viewRect.maxY;
    if (v36 < viewRect.maxY) {
        v7 = v36;
        viewRect.maxY = v36;
    }

    if (v4 != v5 && v6 != v7) {
        C2Vector screenPoint = { 0.0f, 0.0f };
        CameraSetupScreenProjection(viewRect, screenPoint, 0.0f, 0);

        DDCToNDC(viewRect.minX, viewRect.minY, &viewRect.minX, &viewRect.minY);
        DDCToNDC(viewRect.maxX, viewRect.maxY, &viewRect.maxX, &viewRect.maxY);

        float v11, v12, v13, v14;

        if (viewRect.minX >= 0.0f) {
            v12 = viewRect.minX;
            v11 = viewRect.maxX;
        } else {
            v11 = viewRect.maxX - viewRect.minX;
            viewRect.maxX = v11;
            v12 = 0.0f;
            viewRect.minX = 0.0f;
        }

        if (v11 > 1.0f) {
            v12 = v12 - (v11 - 1.0f);
            viewRect.minX = v12;
            viewRect.maxX = 1.0f;
            v11 = 1.0f;
        }

        if (viewRect.minY >= 0.0f) {
            v14 = viewRect.minY;
            v13 = viewRect.maxY;
        } else {
            v13 = viewRect.maxY - viewRect.minY;
            viewRect.maxY = v13;
            v14 = 0.0f;
            viewRect.minY = 0.0f;
        }

        float v15 = v14;
        float v16 = v13;
        float v17 = v15;

        if (v16 > 1.0f) {
            v17 = v17 - (v16 - 1.0f);
            viewRect.minY = v17;
            v16 = 1.0f;
            viewRect.maxY = 1.0f;
        }

        float v18 = v16;
        float v19 = v12;
        float v20 = v18;

        if (v19 < v11) {
            float v21 = v19;
            float v22 = v17;
            float v23 = v21;

            if (v22 < v20) {
                float v27 = v20;
                float v26 = v22;
                float v25 = v11;
                float v24 = v23;

                GxXformSetViewport(v24, v25, v26, v27, 0.0f, 1.0f);
                scrollChild->OnFrameRender();
            }
        }

        GxXformSetProjection(savedProj);
        GxXformSetView(savedView);
        GxXformSetViewport(minX, maxX, minY, maxY, minZ, maxZ);
    }
}

CSimpleScrollFrame::CSimpleScrollFrame(CSimpleFrame* parent) : CSimpleFrame(parent) {
    // TODO
}

int32_t CSimpleScrollFrame::GetBoundsRect(CRect& bounds) {
    if (this->IsResizePending()) {
        this->Resize(1);
    }

    CRect rect = { 0.0f, 0.0f, 0.0f, 0.0f };

    if (this->GetRect(&rect)) {
        bounds.minX = rect.minX >= bounds.minX ? bounds.minX : rect.minX;
        bounds.maxX = rect.maxX <= bounds.maxX ? bounds.maxX : rect.maxX;
        bounds.minY = rect.minY >= bounds.minY ? bounds.minY : rect.minY;
        bounds.maxY = rect.maxY <= bounds.maxY ? bounds.maxY : rect.maxY;
    }

    for (auto region = this->m_regions.Head(); region; region = this->m_regions.Next(region)) {
        if (region->IsShown()) {
            if (region->IsResizePending()) {
                region->Resize(1);
            }

            if (region->GetRect(&rect)) {
                bounds.minX = rect.minX >= bounds.minX ? bounds.minX : rect.minX;
                bounds.maxX = rect.maxX <= bounds.maxX ? bounds.maxX : rect.maxX;
                bounds.minY = rect.minY >= bounds.minY ? bounds.minY : rect.minY;
                bounds.maxY = rect.maxY <= bounds.maxY ? bounds.maxY : rect.maxY;
            }
        }
    }

    for (auto child = this->m_children.Head(); child; child = this->m_children.Next(child)) {
        if (child->frame != this->m_scrollChild && child->frame->m_shown) {
            child->frame->GetBoundsRect(bounds);
        }
    }

    return bounds.maxY > bounds.minY && bounds.maxX > bounds.minX;
}

FrameScript_Object::ScriptIx* CSimpleScrollFrame::GetScriptByName(const char* name, ScriptData& data) {
    auto parentScript = CSimpleFrame::GetScriptByName(name, data);

    if (parentScript) {
        return parentScript;
    }

    if (!SStrCmpI(name, "OnHorizontalScroll", STORM_MAX_STR)) {
        data.wrapper = "return function(self,offset) %s end";
        return &this->m_onHorizontalScroll;
    }

    if (!SStrCmpI(name, "OnVerticalScroll", STORM_MAX_STR)) {
        data.wrapper = "return function(self,offset) %s end";
        return &this->m_onVerticalScroll;
    }

    if (!SStrCmpI(name, "OnScrollRangeChanged", STORM_MAX_STR)) {
        data.wrapper = "return function(self,xrange,yrange) %s end";
        return &this->m_onScrollRangeChanged;
    }

    return nullptr;
}

int32_t CSimpleScrollFrame::GetScriptMetaTable() {
    return CSimpleScrollFrame::s_metatable;
}

bool CSimpleScrollFrame::IsA(int32_t type) {
    return type == CSimpleScrollFrame::s_objectType
        || type == CSimpleFrame::s_objectType
        || type == CScriptRegion::s_objectType
        || type == CScriptObject::s_objectType;
}

void CSimpleScrollFrame::LoadXML(const XMLNode* node, CStatus* status) {
    CSimpleFrame::LoadXML(node, status);

    auto scrollChildNode = node->GetChildByName("ScrollChild");

    if (scrollChildNode) {
        XMLNode* childNode = scrollChildNode->m_child;

        if (childNode) {
            CSimpleFrame* childFrame = FrameXML_CreateFrame(childNode, this, status);

            if (childFrame) {
                this->SetScrollChild(childFrame);
            }
        } else {
            const char* frameName = this->GetName();

            if (!frameName) {
                frameName = "<unnamed>";
            }

            status->Add(
                STATUS_WARNING,
                "Frame %s: Scroll frame created without scroll child",
                frameName
            );
        }
    }
}

void CSimpleScrollFrame::OnFrameRender(CRenderBatch* batch, uint32_t layer) {
    CSimpleFrame::OnFrameRender(batch, layer);

    if (layer == DRAWLAYER_HIGHLIGHT) {
        batch->QueueCallback(CSimpleScrollFrame::RenderScrollChild, this);
    }
}

void CSimpleScrollFrame::OnFrameSizeChanged(float w, float h) {
    CSimpleFrame::OnFrameSizeChanged(w, h);
    this->m_updateScrollChild = 1;
}

void CSimpleScrollFrame::OnLayerUpdate(float elapsedSec) {
    CSimpleFrame::OnLayerUpdate(elapsedSec);

    if (this->m_updateScrollChild) {
        CRect rect = { 0.0f, 0.0f, 0.0f, 0.0f };

        if (this->GetRect(&rect)) {
            float w = rect.maxX - rect.minX;
            float h = rect.maxY - rect.minY;
            this->UpdateScrollChildRect(w, h);
        }

        this->m_updateScrollChild = 0;
    }
}

void CSimpleScrollFrame::RunOnScrollRangeChangedScript() {
    if (!this->m_onScrollRangeChanged.luaRef) {
        return;
    }

    auto L = FrameScript_GetContext();

    auto rangeX = this->m_scrollRange.x;
    auto ddcRangeX = CoordinateGetAspectCompensation() * 1024.0f * rangeX;
    auto ndcRangeX = DDCToNDCWidth(ddcRangeX);
    lua_pushnumber(L, ndcRangeX);

    auto rangeY = this->m_scrollRange.y;
    auto ddcRangeY = CoordinateGetAspectCompensation() * 1024.0f * rangeY;
    auto ndcRangeY = DDCToNDCWidth(ddcRangeY);
    lua_pushnumber(L, ndcRangeY);

    this->RunScript(this->m_onScrollRangeChanged, 2, nullptr);
}

void CSimpleScrollFrame::RunOnVerticalScrollScript() {
    if (!this->m_onVerticalScroll.luaRef) {
        return;
    }

    auto L = FrameScript_GetContext();

    auto offsetY = this->m_scrollOffset.y;
    auto ddcOffsetY = CoordinateGetAspectCompensation() * 1024.0f * offsetY;
    auto ndcOffsetY = DDCToNDCWidth(ddcOffsetY);
    lua_pushnumber(L, ndcOffsetY);

    this->RunScript(this->m_onVerticalScroll, 1, nullptr);
}

void CSimpleScrollFrame::SetScrollChild(CSimpleFrame* frame) {
    if (this->m_scrollChild) {
        this->m_scrollChild->SetBeingScrolled(0, 0);
        this->m_scrollChild->SetParent(nullptr);
    }

    this->m_scrollChild = frame;

    if (this->m_scrollChild) {
        this->m_scrollChild->SetParent(this);
        this->m_scrollChild->SetBeingScrolled(1, 1);
    }

    this->UpdateScrollChild();

    this->m_updateScrollChild = 1;
}

void CSimpleScrollFrame::SetVerticalScroll(float offset) {
    if (fabs(offset - this->m_scrollOffset.y) >= 0.00000095367432) {
        this->m_scrollOffset.y = offset;
        this->UpdateScrollChild();
        this->RunOnVerticalScrollScript();
    }
}

void CSimpleScrollFrame::UpdateScrollChild() {
    if (!this->m_scrollChild) {
        return;
    }

    this->m_scrollChild->ClearAllPoints();
    this->m_scrollChild->SetPoint(
        FRAMEPOINT_TOPLEFT,
        this,
        FRAMEPOINT_TOPLEFT,
        -this->m_scrollOffset.x,
        this->m_scrollOffset.y,
        1
    );
}

void CSimpleScrollFrame::UpdateScrollChildRect(float w, float h) {
    if (!this->m_scrollChild) {
        return;
    }

    float rangeX = this->m_scrollRange.x;
    float rangeY = this->m_scrollRange.y;

    CRect childBounds = {
        std::numeric_limits<float>::max(),
        std::numeric_limits<float>::max(),
        0.0f,
        0.0f
    };

    this->m_scrollChild->GetBoundsRect(childBounds);

    auto ooScale = 1.0f / this->m_layoutScale;

    auto v7 = std::max(childBounds.maxX - childBounds.minX - w, 0.0f);
    this->m_scrollRange.x = v7 * ooScale;

    auto v12 = std::max(childBounds.maxY - childBounds.minY - h, 0.0f);
    this->m_scrollRange.y = v12 * ooScale;

    if (
        fabs(this->m_scrollRange.x - rangeX) >= 0.00000095367432
        || fabs(this->m_scrollRange.y - rangeY) >= 0.00000095367432
    ) {
        this->RunOnScrollRangeChangedScript();
    }
}
