#include "ui/game/CGCamera.hpp"
#include "common/Time.hpp"
#include "console/CVar.hpp"
#include "object/Client.hpp"
#include "object/client/CVehicleCamera_C.hpp"
#include "ui/game/Types.hpp"
#include "world/World.hpp"
#include <algorithm>
#include <storm/String.hpp>
#include <tempest/Math.hpp>

static CVar* s_cameraView;

CGCamera::CameraViewData CGCamera::s_cameraViewDataDefault[MAX_CAMERA_VIEWS] = {
    {  "0.0",   "0.0", "0.0" },     // VIEW_FIRST_PERSON
    {  "0.0",   "0.0", "0.0" },     // VIEW_THIRD_PERSON_A
    {  "5.55", "10.0", "0.0" },     // VIEW_THIRD_PERSON_B
    {  "5.55", "20.0", "0.0" },     // VIEW_THIRD_PERSON_C
    { "13.88", "30.0", "0.0" },     // VIEW_THIRD_PERSON_D
    { "13.88", "10.0", "0.0" },     // VIEW_THIRD_PERSON_E
    {  "0.0",   "0.0", "0.0" },     // VIEW_COMMENTATOR
    {  "5.0",  "10.0", "0.0" },     // VIEW_BARBER_SHOP
};

namespace {

bool ValidateCameraView(CVar* var, const char* oldValue, const char* value, void* arg) {
    auto view = SStrToFloat(value);
    auto min = static_cast<float>(VIEW_FIRST_PERSON);
    auto max = static_cast<float>(VIEW_BARBER_SHOP);

    if (view >= min && view <= max) {
        return true;
    }

    // TODO ConsoleWriteA("Value out of range (%f - %f)\n", DEFAULT_COLOR, min, max);

    return false;
}

}

int32_t CGCamera::UpdateCallback(const void*, void* param) {
    auto camera = static_cast<CGCamera*>(param);

    if (!camera) {
        return true;
    }

    auto timestamp = OsGetAsyncTimeMsPrecise();

    camera->m_nearZ = CWorld::GetNearClip();
    camera->m_farZ = CWorld::GetFarClip();

    // Model camera

    if (camera->HasModel()) {
        camera->CalcModelCamera(timestamp);
        camera->CheckUnderwater();

        return true;
    }

    // Target camera

    auto target = ClntObjMgrObjectPtr(camera->m_target, TYPE_OBJECT, __FILE__, __LINE__);

    if (target) {
        camera->CalcTargetCamera(target, timestamp);
        camera->CheckUnderwater();

        return true;
    }

    // Unknown camera

    auto object90 = ClntObjMgrObjectPtr(camera->guid90, TYPE_OBJECT, __FILE__, __LINE__);

    if (object90) {
        // TODO

        return true;
    }

    return true;
}

CGCamera::CGCamera() : CSimpleCamera(CWorld::GetNearClip(), CWorld::GetFarClip(), 90.0f * CMath::DEG2RAD) {
    this->m_model = nullptr;

    this->m_target = 0;
    this->guid90 = 0;
    this->m_relativeTo = 0;

    this->m_view = s_cameraView->GetInt();

    this->m_distance = SStrToFloat(CGCamera::s_cameraViewDataDefault[this->m_view].m_distance);
    this->m_yaw = 0.0f;
    this->m_pitch = SStrToFloat(CGCamera::s_cameraViewDataDefault[this->m_view].m_pitch);
    this->m_roll = 0.0f;

    this->m_fovOffset = 0.0f;
}

void CGCamera::CalcModelCamera(uint32_t timestamp) {
    // TODO
}

void CGCamera::CalcTargetCamera(CGObject_C* target, uint32_t timestamp) {
    // TODO
}

void CGCamera::CheckUnderwater() {
    // TODO
}

float CGCamera::FOV() const {
    // Clamp offset-adjusted FOV between 0pi and 1pi
    return std::min(std::max(this->m_fov + this->m_fovOffset, 0.0f), CMath::PI);
}

C3Vector CGCamera::Forward() const {
    if (this->m_relativeTo) {
        return this->CSimpleCamera::Forward() * this->ParentToWorld();
    }

    return this->CSimpleCamera::Forward();
}

const WOWGUID& CGCamera::GetTarget() const {
    return this->m_target;
}

int32_t CGCamera::HasModel() const {
    return this->m_model != nullptr;
}

C33Matrix CGCamera::ParentToWorld() const {
    auto relativeTo = ClntObjMgrObjectPtr(this->m_relativeTo, TYPE_OBJECT, __FILE__, __LINE__);

    if (!relativeTo) {
        return {};
    }

    float facing;

    if (relativeTo->IsA(TYPE_UNIT)) {
        facing = static_cast<CGUnit_C*>(relativeTo)->GetRawSmoothFacing();
        auto transport = ClntObjMgrObjectPtr(relativeTo->GetTransportGUID(), TYPE_OBJECT, __FILE__, __LINE__);
        CVehicleCamera_C::ConvertSmoothFacingFromRawToWorld(facing, transport);
    } else {
        facing = relativeTo->GetFacing();
    }

    return C33Matrix::RotationAroundZ(facing);
}

C3Vector CGCamera::Right() const {
    if (this->m_relativeTo) {
        return this->CSimpleCamera::Right() * this->ParentToWorld();
    }

    return this->CSimpleCamera::Right();
}

void CGCamera::SetupWorldProjection(const CRect& projRect) {
    this->SetGxProjectionAndView(projRect);
}

C3Vector CGCamera::Target() const {
    return this->m_position + this->Forward();
}

C3Vector CGCamera::Up() const {
    if (this->m_relativeTo) {
        return this->CSimpleCamera::Up() * this->ParentToWorld();
    }

    return this->CSimpleCamera::Up();
}

void CameraRegisterCVars() {
    // TODO

    s_cameraView = CVar::Register("cameraView", nullptr, 0x10, "2", &ValidateCameraView, DEFAULT);

    // TODO
}
