#include "ui/simple/CSimpleModel.hpp"
#include "gx/Camera.hpp"
#include "gx/Coordinate.hpp"
#include "gx/Draw.hpp"
#include "gx/Shader.hpp"
#include "gx/Transform.hpp"
#include "math/Utils.hpp"
#include "model/CM2Shared.hpp"
#include "model/M2Data.hpp"
#include "ui/LoadXML.hpp"
#include "ui/simple/CSimpleModelScript.hpp"
#include <common/XML.hpp>
#include <tempest/Math.hpp>

int32_t CSimpleModel::s_metatable;
int32_t CSimpleModel::s_objectType;

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

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

    return CSimpleModel::s_objectType;
}

void CSimpleModel::LightingCallback(CM2Model* model, CM2Lighting* lighting, void* userArg) {
    CSimpleModel* simpleModel = static_cast<CSimpleModel*>(userArg);

    if (simpleModel->m_flags & 0x1) {
        lighting->SetFog(simpleModel->m_fogColor, simpleModel->m_fogNear, simpleModel->m_fogFar);
    }

    if (simpleModel->m_light.m_visible) {
        lighting->AddLight(&simpleModel->m_light);
    }
}

void CSimpleModel::ModelLoaded(CM2Model* model, void* arg) {
    auto simpleModel = static_cast<CSimpleModel*>(arg);
    simpleModel->OnModelLoaded(model);
}

void CSimpleModel::RegisterScriptMethods(lua_State* L) {
    CSimpleFrame::RegisterScriptMethods(L);
    FrameScript_Object::FillScriptMethodTable(L, SimpleModelMethods, NUM_SIMPLE_MODEL_SCRIPT_METHODS);
}

void CSimpleModel::RenderModel(void* arg) {
    auto simpleModel = static_cast<CSimpleModel*>(arg);

    if (!simpleModel->m_model) {
        return;
    }

    CRect viewRect;

    if (!simpleModel->GetRect(&viewRect)) {
        return;
    }

    C44Matrix proj;
    GxXformProjection(proj);

    C44Matrix view;
    GxXformView(view);

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

    C3Vector cameraPos;
    C3Vector cameraTarg;

    if (simpleModel->m_camera) {
        DataMgrGetCoord(simpleModel->m_camera, 7, &cameraPos);
        DataMgrGetCoord(simpleModel->m_camera, 8, &cameraTarg);
        CameraSetupWorldProjection(simpleModel->m_camera, viewRect, 0x0);
    } else {
        C2Vector v19 = { viewRect.minX, viewRect.minY };
        CameraSetupScreenProjection(viewRect, v19, 0.0f, 1);
    }

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

    viewRect.minX = std::max(0.0f, std::min(viewRect.minX, 1.0f));
    viewRect.maxX = std::max(0.0f, std::min(viewRect.maxX, 1.0f));
    viewRect.minY = std::max(0.0f, std::min(viewRect.minY, 1.0f));
    viewRect.maxY = std::max(0.0f, std::min(viewRect.maxY, 1.0f));

    // Handle zero width or zero height
    if (AreEqual(viewRect.minX, viewRect.maxX, WHOA_EPSILON_1) || AreEqual(viewRect.minY, viewRect.maxY, WHOA_EPSILON_1)) {
        GxXformSetProjection(proj);
        GxXformSetView(view);
        GxXformSetViewport(minX, maxX, minY, maxY, minZ, maxZ);

        return;
    }

    float v11 = viewRect.minX * maxX;
    float v12 = viewRect.maxX * maxX;
    float v13 = viewRect.minY * maxY;
    float v14 = viewRect.maxY * maxY;

    GxXformSetViewport(v11, v12, v13, v14, 0.0f, 1.0f);
    CImVector clearColor = { 0x00, 0x00, 0x00, 0xFF };
    GxSceneClear(0x2, clearColor);
    CShaderEffect::UpdateProjMatrix();

    simpleModel->UpdateModel();

    if (simpleModel->m_pendingCameraIndex == -1u) {
        simpleModel->GetScene()->Animate(cameraPos);
        simpleModel->GetScene()->Draw(M2PASS_0);
        simpleModel->GetScene()->Draw(M2PASS_1);
    }

    GxXformSetProjection(proj);
    GxXformSetView(view);
    GxXformSetViewport(minX, maxX, minY, maxY, minZ, maxZ);
    CShaderEffect::UpdateProjMatrix();
}

CSimpleModel::CSimpleModel(CSimpleFrame* parent) : CSimpleFrame(parent) {
    this->m_light.SetVisible(0);
    this->m_light.SetLightType(M2LIGHT_1);
    this->m_light.m_ambColor = { 1.0f, 1.0f, 1.0f };
    this->m_light.m_dirColor = { 1.0f, 1.0f, 1.0f };

    this->m_fogColor = { 0xFF, 0xFF, 0xFF, 0xFF };
}

CM2Scene* CSimpleModel::GetScene() {
    if (!this->m_scene) {
        this->m_scene = M2CreateScene();
    }

    return this->m_scene;
}

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

    if (parentScript) {
        return parentScript;
    }

    if (!SStrCmpI(name, "OnUpdateModel", STORM_MAX_STR)) {
        return &this->m_onUpdateModel;
    }

    if (!SStrCmpI(name, "OnAnimFinished", STORM_MAX_STR)) {
        return &this->m_onAnimFinished;
    }

    return nullptr;
}

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

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

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

    const char* fileAttr = node->GetAttributeByName("file");
    if (fileAttr && *fileAttr) {
        this->SetModel(fileAttr);

        if (!this->m_model) {
            status->Add(
                STATUS_WARNING,
                "Frame %s: Bad model file: %s",
                this->GetDisplayName(),
                fileAttr
            );
        }
    }

    const char* scaleAttr = node->GetAttributeByName("scale");
    if (scaleAttr && *scaleAttr) {
        float scale = SStrToFloat(scaleAttr);

        if (scale <= 0.0f) {
            status->Add(
                STATUS_WARNING,
                "Frame %s: Invalid model scale: %s",
                this->GetDisplayName(),
                scaleAttr
            );
        } else {
            this->SetScale(scale);
        }
    }

    const char* fogNearAttr = node->GetAttributeByName("fogNear");
    if (fogNearAttr && *fogNearAttr) {
        float fogNear = SStrToFloat(fogNearAttr);

        if (fogNear < 0.0f) {
            fogNear = 0.0f;
        }

        this->SetFogNear(fogNear);
    }

    const char* fogFarAttr = node->GetAttributeByName("fogFar");
    if (fogFarAttr && *fogFarAttr) {
        float fogFar = SStrToFloat(fogFarAttr);

        if (fogFar < 0.0f) {
            fogFar = 0.0f;
        }

        this->SetFogFar(fogFar);
    }

    for (XMLNode* child = node->m_child; child; child = child->m_next) {
        if (!SStrCmpI(child->GetName(), "FogColor", STORM_MAX_STR)) {
            CImVector color;
            LoadXML_Color(child, color);
            this->SetFogColor(color);
        }
    }
}

void CSimpleModel::OnFrameRender(CRenderBatch* batch, uint32_t layer) {
    // TODO
}

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

    if (this->m_model && this->m_visible && this->m_pendingCameraIndex == -1u) {
        this->m_model->SetAnimating(1);
        this->m_model->SetVisible(1);

        if (this->m_model->m_attachParent) {
            this->m_model->m_flag20000 = 1;
        } else {
            this->m_model->m_flag10000 = 1;
        }
    }

    uint32_t advance = CMath::fuint(elapsedSec * 1000.f);
    this->GetScene()->AdvanceTime(advance);
}

void CSimpleModel::OnModelLoaded(CM2Model* model) {
    model->SetAnimating(1);

    C3Vector cameraPos;
    model->m_scene->Animate(cameraPos);

    if (this->m_pendingCameraIndex != -1u) {
        this->SetCameraByIndex(this->m_pendingCameraIndex);
    } else if (this->m_pendingCameraId != -1u) {
        this->SetCameraByID(this->m_pendingCameraId);
    }

    CAaBox bounds;
    this->m_bounds = model->GetBoundingBox(bounds);

    this->Resize(0);
}

void CSimpleModel::SetCamera(HCAMERA camera) {
    if (camera) {
        camera = HandleDuplicate(camera);
    }

    if (this->m_camera) {
        HandleClose(this->m_camera);
    }

    this->m_camera = camera;
    this->m_pendingCameraIndex = -1u;
    this->m_pendingCameraId = -1u;
}

void CSimpleModel::SetCameraByID(uint32_t id) {
    // TODO
}

void CSimpleModel::SetCameraByIndex(uint32_t index) {
    if (!this->m_model || !this->m_model->IsLoaded(0, 0)) {
        this->m_pendingCameraIndex = index;

        return;
    }

    if (!this->m_model->m_loaded) {
        this->m_model->WaitForLoad(nullptr);
    }

    if (index >= this->m_model->m_shared->m_data->cameras.Count()) {
        if (this->m_camera) {
            HandleClose(this->m_camera);
        }

        this->m_camera = nullptr;
        this->m_pendingCameraIndex = -1u;
        this->m_pendingCameraId = -1u;

        return;
    }

    HCAMERA camera = this->m_model->GetCameraByIndex(index);
    this->SetCamera(camera);
}

void CSimpleModel::SetFogColor(CImVector& fogColor) {
    this->m_fogColor = fogColor;
    this->m_flags |= 0x1;
}

void CSimpleModel::SetFogFar(float fogFar) {
    this->m_fogFar = fogFar;
}

void CSimpleModel::SetFogNear(float fogNear) {
    this->m_fogNear = fogNear;
}

void CSimpleModel::SetModel(const char* sourcefile) {
    CM2Model* model = nullptr;

    if (sourcefile && *sourcefile) {
        if (!this->m_scene) {
            this->m_scene = M2CreateScene();
        }

        model = this->m_scene->CreateModel(sourcefile, 1);
    }

    this->SetModel(model);

    if (model) {
        model->Release();
    }
}

void CSimpleModel::SetModel(CM2Model* model) {
    if (model) {
        model->m_refCount++;
    }

    if (this->m_model) {
        this->m_model->Release();
    }

    this->m_model = model;

    if (model) {
        // TODO
        this->m_model->SetLightingCallback(&CSimpleModel::LightingCallback, this);
        this->m_model->SetLoadedCallback(&CSimpleModel::ModelLoaded, this);
    }

    this->NotifyDrawLayerChanged(DRAWLAYER_ARTWORK);
}

void CSimpleModel::SetScale(float scale) {
    this->m_scale = scale;
}

void CSimpleModel::SetSequence(uint32_t sequenceId) {
    if (this->m_model) {
        this->m_model->SetBoneSequence(-1, sequenceId, -1, 0, 1.0f, 0, 1);
    }
}

int32_t CSimpleModel::SetSequenceTime(uint32_t sequence, int32_t time) {
    if (this->m_model) {
        this->m_model->SetBoneSequence(-1, sequence, -1, time, 1.0f, 0, 1);
    }

    return 1;
}

void CSimpleModel::UpdateModel() {
    if (this->m_onUpdateModel.luaRef) {
        this->RunScript(this->m_onUpdateModel, 0, nullptr);
    }

    C3Vector position = {
        this->m_position.x * this->m_layoutScale,
        this->m_position.y * this->m_layoutScale,
        this->m_position.z * this->m_layoutScale
    };

    float scale = NDCToDDCHeight(1.0f) * 1.6666666 * (this->m_scale * this->m_layoutScale);

    this->m_model->SetWorldTransform(position, this->m_facing, scale);
}
