#include "egg/gfx/eggScreen.h"

#include "common.h"
#include "egg/gfx/eggDrawGX.h"
#include "egg/gfx/eggStateGX.h"
#include "rvl/SC/scapi.h"

// ported from OGWS, though this class has evolved a fair bit

using namespace nw4r;

namespace EGG {

Screen::TVMode Screen::sTVMode;
Screen::TVModeInfo Screen::sTVModeInfo[TV_MODE_MAX] = {
    {
     608, 456,
     nw4r::math::VEC2(1.0526316f, 1.0f),
     },
    {
     812, 456,
     nw4r::math::VEC2(0.7881773, 1.0f),
     },
    {
     640, 456,
     nw4r::math::VEC2(1.0f, 1.0f),
     }
};

Screen *Screen::spRoot = nullptr;

Screen::ChangeTVModeFunc Screen::sChangeTVModeFunc = nullptr;
void *Screen::spChangeTVModeFuncInfo = nullptr;

math::VEC2 Screen::sCanvasScale(1.0f, 1.0f);
math::VEC2 Screen::sCanvasOffset(0.0f, 0.0f);

void Screen::Initialize(const u16 *maxX, const u16 *maxY, Screen *userRoot) {
    sTVModeInfo[TV_MODE_4_3].width = maxX[0];
    sTVModeInfo[TV_MODE_4_3].height = maxY[0];

    sTVModeInfo[TV_MODE_4_3].ratios.x = (f32)StateGX::s_widthEfb / (f32)sTVModeInfo[TV_MODE_4_3].width;
    sTVModeInfo[TV_MODE_4_3].ratios.y = (f32)StateGX::s_heightEfb / (f32)sTVModeInfo[TV_MODE_4_3].height;

    sTVModeInfo[TV_MODE_16_9].width = maxX[1];
    sTVModeInfo[TV_MODE_16_9].height = maxY[1];

    sTVModeInfo[TV_MODE_16_9].ratios.x = (f32)StateGX::s_widthEfb / (f32)sTVModeInfo[TV_MODE_16_9].width;
    sTVModeInfo[TV_MODE_16_9].ratios.y = (f32)StateGX::s_heightEfb / (f32)sTVModeInfo[TV_MODE_16_9].height;

    sTVModeInfo[TV_MODE_UNK_3].width = StateGX::s_widthEfb;
    sTVModeInfo[TV_MODE_UNK_3].height = StateGX::s_heightEfb;

    sTVModeInfo[TV_MODE_UNK_3].ratios.x = 1.0f;
    sTVModeInfo[TV_MODE_UNK_3].ratios.y = 1.0f;

    static Screen defaultRoot;
    defaultRoot.SetProjectionType(PROJ_ORTHO);
    defaultRoot.SetCanvasMode(CANVASMODE_1);
    defaultRoot.SetNearZ(0.0f);
    defaultRoot.SetFarZ(1.0f);
    defaultRoot.SetSize(math::VEC2(sTVModeInfo[sTVMode].width, sTVModeInfo[sTVMode].height));

    userRoot = (userRoot == nullptr) ? &defaultRoot : userRoot;
    spRoot = userRoot;
    userRoot->mParent = nullptr;
    SetTVMode(sTVMode);
}

Screen::Screen()
    : Frustum(
          PROJ_PERSP, math::VEC2(sTVModeInfo[sTVMode].width, sTVModeInfo[sTVMode].height), 10.0f, 100000.0f,
          CANVASMODE_1
      ) {
    mParent = nullptr;
    mPosition.x = 0.0f;
    mPosition.y = 0.0f;
    field_0x48.x = 0.0f;
    field_0x48.y = 0.0f;
    field_0x50.x = 0.0f;
    field_0x50.y = 0.0f;
    SetParent(nullptr);
}

Screen::Screen(f32 x, f32 y, f32 w, f32 h, const Screen *userRoot, CanvasMode canvasMode)
    : Frustum(PROJ_PERSP, math::VEC2(w, h), 10.0f, 100000.0f, canvasMode) {
    mParent = nullptr;
    mPosition.x = x;
    mPosition.y = y;
    field_0x48.x = 0.0f;
    field_0x48.y = 0.0f;
    field_0x50.x = 0.0f;
    field_0x50.y = 0.0f;
    SetParent(userRoot);
}

Screen::Screen(const Screen &other)
    : Frustum(other),
      mPosition(other.mPosition),
      mDataEfb(other.mDataEfb),
      field_0x48(other.field_0x48),
      field_0x50(other.field_0x50) {
    SetParent(other.mParent);
}

void Screen::SetProjectionGX() const {
    if (!(mFlags & 0x40)) {
        f32 sx, sy, ox, oy;
        GetGlobalScaleOffset(&sx, &sy, &ox, &oy);

        SetGlobalScaleOffset(sCanvasScale.x, sCanvasScale.y, sCanvasOffset.x, sCanvasOffset.y);
        Frustum::SetProjectionGX();

        SetGlobalScaleOffset(sx, sy, ox, oy);
    } else {
        Frustum::SetProjectionGX();
    }

    const DataEfb &efb = GetDataEfb();

    StateGX::GXSetViewport_(efb.vp.x1, efb.vp.y1, efb.vp.x2, efb.vp.y2, efb.vp.z1, efb.vp.z2);
    StateGX::GXSetScissor_(efb.sc_x, efb.sc_y, efb.sc_w, efb.sc_h);
    StateGX::GXSetScissorBoxOffset_(efb.sc_ox, efb.sc_oy);
}

void Screen::CopyToG3D(g3d::Camera cam) const {
    if (!(mFlags & 0x40)) {
        f32 sx, sy, ox, oy;
        GetGlobalScaleOffset(&sx, &sy, &ox, &oy);

        SetGlobalScaleOffset(sCanvasScale.x, sCanvasScale.y, sCanvasOffset.x, sCanvasOffset.y);
        Frustum::CopyToG3D(cam);

        SetGlobalScaleOffset(sx, sy, ox, oy);
    } else {
        Frustum::CopyToG3D(cam);
    }

    const DataEfb &efb = GetDataEfb();

    f32 x1, x2, y1, y2;
    f32 z1, z2;

    y2 = efb.vp.y2;
    x2 = efb.vp.x2;

    y1 = efb.vp.y1;
    x1 = efb.vp.x1;

    z2 = efb.vp.z2;
    z1 = efb.vp.z1;

    cam.SetViewport(x1, y1, x2, y2);
    cam.SetViewportZRange(z1, z2);
    cam.SetScissor(efb.sc_x, efb.sc_y, efb.sc_w, efb.sc_h);
    cam.SetScissorBoxOffset(efb.sc_ox, efb.sc_oy);
}

void Screen::CopyFromAnother(const Screen &other) {
    Frustum::CopyFromAnother(other);

    mPosition = other.mPosition;
    field_0x48 = other.field_0x48;
    field_0x50 = other.field_0x50;
    mDataEfb = other.mDataEfb;
    mParent = other.mParent;
}

void Screen::Reset(f32 x, f32 y, f32 w, f32 h, CanvasMode mode) {
    mPosition.x = x;
    mPosition.y = y;
    mSize.x = w;
    SetDirty(true);
    mSize.y = h;
    SetCanvasMode(mode);
}

void Screen::SetParent(const Screen *parent) {
    const Screen *newParent = parent != nullptr ? parent : spRoot;

    if (mParent != newParent) {
        SetDirty(true);
        mParent = newParent;
    }
}

void Screen::SetUnkFlag8() {
    if ((mFlags & FLAG_0x08) == 0) {
        SetDirty(true);
    }
    mFlags |= FLAG_0x08;
}

void Screen::GetPosSizeInEfb() const {
    f32 &x1 = mDataEfb.vp.x1;
    f32 &y1 = mDataEfb.vp.y1;
    if (mParent == nullptr) {
        x1 = mPosition.x * sCanvasScale.x;
        y1 = mPosition.y * sCanvasScale.y;
    } else {
        mParent->ConvertToEfb(mPosition.x, mPosition.y, &x1, &y1);
    }

    // TODO: Make this work without temporaries?

    mDataEfb.sc_x = x1 + ScaleByX(field_0x48.x);
    mDataEfb.sc_y = y1 + ScaleByY(field_0x48.y);

    mDataEfb.sc_oy = 0;
    mDataEfb.sc_ox = 0;
    if (x1 < 0.0f) {
        int x1_i = x1;
        int x1_in = -x1_i;
        mDataEfb.sc_ox = (x1_in - (x1_in & 1));
        x1 = x1 - x1_i;
    }
    if (y1 < 0.0f) {
        int y1_i = y1;
        int y1_in = -y1_i;
        mDataEfb.sc_oy = (y1_in - (y1_in & 1));
        y1 = y1 - y1_i;
    }

    if ((mFlags & FLAG_0x02) != 0) {
        x1 = x1 - (((int)x1) & 1);
        y1 = y1 - (((int)y1) & 1);
    }

    mDataEfb.vp.x2 = ScaleByX(mSize.x);
    mDataEfb.vp.y2 = ScaleByY(mSize.y);

    if ((mFlags & FLAG_0x04) != 0) {
        mDataEfb.vp.x2 -= ((int)mDataEfb.vp.x2 & 3);
        mDataEfb.vp.y2 -= ((int)mDataEfb.vp.y2 & 3);
    }
    mDataEfb.vp.x2 = (int)mDataEfb.vp.x2;
    mDataEfb.vp.y2 = (int)mDataEfb.vp.y2;

    mDataEfb.sc_w = mDataEfb.vp.x2 - ScaleByX(field_0x48.x + field_0x50.x);
    mDataEfb.sc_h = mDataEfb.vp.y2 - ScaleByY(field_0x48.y + field_0x50.y);
}

const Screen::DataEfb &Screen::GetDataEfb() const {
    if (IsChangeEfb()) {
        GetPosSizeInEfb();
        mDataEfb.vp.z1 = 0.0f;
        mDataEfb.vp.z2 = 1.0f;
        SetDirty(false);
    }

    return mDataEfb;
}

bool Screen::IsChangeEfb() const {
    if (mFlags & FLAG_DIRTY) {
        return true;
    }

    if (mParent != nullptr) {
        return mParent->IsChangeEfb();
    }

    return false;
}

void Screen::CalcMatrixForDrawQuad(math::MTX34 *mtx, f32 x, f32 y, f32 sx, f32 sy) const {
    MTXScale(*mtx, sx, sy, 1.0f);

    mtx->m[0][3] = x;
    mtx->m[1][3] = mCanvasMode == CANVASMODE_0 ? y - sy : y;
    mtx->m[2][3] = 0.0f;
}

void Screen::FillBufferGX(u32 flags, GXColor color, u32 r6) const {
    if (flags != 0) {
        math::MTX34 drawMtx;
        Screen clone(mPosition.x, mPosition.y, mSize.x, mSize.y, mParent, CANVASMODE_1);

        if ((mFlags & 0x8) != 0) {
            clone.SetUnkFlag8();
        }

        clone.SetNearZ(0.0f);
        clone.SetFarZ(1.0f);
        clone.SetFlag(0x40);
        clone.SetProjectionType(PROJ_ORTHO);

        clone.SetProjectionGX();
        clone.CalcMatrixForDrawQuad(&drawMtx, 0.0f, 0.0f, mSize.x, mSize.y);
        DrawGX::ClearEfb(
            drawMtx, (flags & 1) ? true : false, (flags & 2) ? true : false, (flags & 4) ? true : false, color, true
        );
    }
}

void Screen::GetGlobalPos(f32 *ox, f32 *oy) const {
    f32 px, py;
    const Screen *parent = GetParent();

    if (parent != nullptr) {
        parent->GetGlobalPos(&px, &py);
        parent->ConvertToCanvasLU(mPosition.x, mPosition.y, ox, oy);

        *ox += px;
        *oy += py;
    } else {
        *ox = mPosition.x;
        *oy = mPosition.y;
    }
}

void Screen::SetTVMode(TVMode tvMode) {
    sTVMode = tvMode;
    if (spRoot != nullptr) {
        spRoot->SetSizeX(GetSizeXMax());
        spRoot->SetSizeY(GetSizeYMax());
    }

    if (sChangeTVModeFunc != nullptr) {
        sChangeTVModeFunc(spChangeTVModeFuncInfo);
    }
}

void Screen::SetTVModeDefault() {
    SetTVMode(SCGetAspectRatio() == SC_ASPECT_STD ? TV_MODE_4_3 : TV_MODE_16_9);
}

void Screen::ConvertToEfb(f32 a, f32 b, f32 *ox, f32 *oy) const {
    GetGlobalPos(ox, oy);
    ConvertToCanvasLU(a, b, &a, &b);
    *ox = ScaleByX((*ox + a));
    *oy = ScaleByY((*oy + b));
}

} // namespace EGG
