#include "CDrawUtil.h"
#include "CGraphics.h"
#include "Core/GameProject/CResourceStore.h"
#include <Common/Log.h>
#include <Common/Math/CTransform4f.h>
#include <iostream>

// ************ MEMBER INITIALIZATION ************
std::optional<CVertexBuffer> CDrawUtil::mGridVertices;
CIndexBuffer CDrawUtil::mGridIndices;

std::optional<CDynamicVertexBuffer> CDrawUtil::mSquareVertices;
CIndexBuffer CDrawUtil::mSquareIndices;

std::optional<CDynamicVertexBuffer> CDrawUtil::mLineVertices;
CIndexBuffer CDrawUtil::mLineIndices;

TResPtr<CModel> CDrawUtil::mpCubeModel;

std::optional<CVertexBuffer> CDrawUtil::mWireCubeVertices;
CIndexBuffer CDrawUtil::mWireCubeIndices;

TResPtr<CModel> CDrawUtil::mpSphereModel;
TResPtr<CModel> CDrawUtil::mpDoubleSidedSphereModel;

TResPtr<CModel> CDrawUtil::mpWireSphereModel;

CShader *CDrawUtil::mpColorShader;
CShader *CDrawUtil::mpColorShaderLighting;
CShader *CDrawUtil::mpBillboardShader;
CShader *CDrawUtil::mpLightBillboardShader;
CShader *CDrawUtil::mpTextureShader;
CShader *CDrawUtil::mpCollisionShader;
CShader *CDrawUtil::mpTextShader;

TResPtr<CTexture> CDrawUtil::mpCheckerTexture;

TResPtr<CTexture> CDrawUtil::mpLightTextures[4];
TResPtr<CTexture> CDrawUtil::mpLightMasks[4];

bool CDrawUtil::mDrawUtilInitialized = false;

// ************ PUBLIC ************
void CDrawUtil::DrawGrid(CColor LineColor, CColor BoldLineColor)
{
    Init();

    mGridVertices->Bind();

    CGraphics::sMVPBlock.ModelMatrix = CMatrix4f::skIdentity;
    CGraphics::UpdateMVPBlock();

    glBlendFunc(GL_ONE, GL_ZERO);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDepthMask(GL_TRUE);

    glLineWidth(1.0f);
    LineColor.A = 0.f;
    UseColorShader(LineColor);
    mGridIndices.DrawElements(0, mGridIndices.GetSize() - 4);

    glLineWidth(1.5f);
    BoldLineColor.A = 0.f;
    UseColorShader(BoldLineColor);
    mGridIndices.DrawElements(mGridIndices.GetSize() - 4, 4);
}

void CDrawUtil::DrawSquare()
{
    // Overload with default tex coords
    CVector2f TexCoords[4] = { CVector2f(0.f, 1.f), CVector2f(1.f, 1.f), CVector2f(1.f, 0.f), CVector2f(0.f, 0.f) };
    DrawSquare(&TexCoords[0].X);
}

void CDrawUtil::DrawSquare(const CVector2f& TexUL, const CVector2f& TexUR, const CVector2f& TexBR, const CVector2f& TexBL)
{
    // Overload with tex coords specified via parameters
    // I don't think that parameters are guaranteed to be contiguous in memory, so:
    CVector2f TexCoords[4] = { TexUL, TexUR, TexBR, TexBL };
    DrawSquare(&TexCoords[0].X);
}

void CDrawUtil::DrawSquare(const float *pTexCoords)
{
    Init();

    // Set tex coords
    for (uint32 iTex = 0; iTex < 8; iTex++)
    {
        EVertexAttribute TexAttrib = (EVertexAttribute) ((uint) (EVertexAttribute::Tex0) << iTex);
        mSquareVertices->BufferAttrib(TexAttrib, pTexCoords);
    }

    // Draw
    mSquareVertices->Bind();
    mSquareIndices.DrawElements();
    mSquareVertices->Unbind();
}

void CDrawUtil::DrawLine(const CVector3f& PointA, const CVector3f& PointB)
{
    DrawLine(PointA, PointB, CColor::skWhite);
}

void CDrawUtil::DrawLine(const CVector2f& PointA, const CVector2f& PointB)
{
    // Overload for 2D lines
    DrawLine(CVector3f(PointA.X, PointA.Y, 0.f), CVector3f(PointB.X, PointB.Y, 0.f), CColor::skWhite);
}

void CDrawUtil::DrawLine(const CVector3f& PointA, const CVector3f& PointB, const CColor& LineColor)
{
    Init();

    // Copy vec3s into an array to ensure they are adjacent in memory
    CVector3f Points[2] = { PointA, PointB };
    mLineVertices->BufferAttrib(EVertexAttribute::Position, Points);

    // Draw
    UseColorShader(LineColor);
    mLineVertices->Bind();
    mLineIndices.DrawElements();
    mLineVertices->Unbind();
}

void CDrawUtil::DrawLine(const CVector2f& PointA, const CVector2f& PointB, const CColor& LineColor)
{
    // Overload for 2D lines
    DrawLine(CVector3f(PointA.X, PointA.Y, 0.f), CVector3f(PointB.X, PointB.Y, 0.f), LineColor);
}

void CDrawUtil::DrawCube()
{
    Init();
    mpCubeModel->Draw(ERenderOption::NoMaterialSetup, 0);
}

void CDrawUtil::DrawCube(const CColor& Color)
{
    Init();
    UseColorShader(Color);
    DrawCube();
}

void CDrawUtil::DrawCube(const CVector3f& Position, const CColor& Color)
{
    CGraphics::sMVPBlock.ModelMatrix = CTransform4f::TranslationMatrix(Position);
    CGraphics::UpdateMVPBlock();
    UseColorShader(Color);
    DrawCube();
}

void CDrawUtil::DrawShadedCube(const CColor& Color)
{
    Init();
    UseColorShaderLighting(Color);
    DrawCube();
}

void CDrawUtil::DrawWireCube()
{
    Init();
    glLineWidth(1.f);
    mWireCubeVertices->Bind();
    mWireCubeIndices.DrawElements();
    mWireCubeVertices->Unbind();
}

void CDrawUtil::DrawWireCube(const CAABox& kAABox, const CColor& kColor)
{
    Init();

    // Calculate model matrix
    CTransform4f Transform;
    Transform.Scale(kAABox.Size());
    Transform.Translate(kAABox.Center());
    CGraphics::sMVPBlock.ModelMatrix = Transform;
    CGraphics::UpdateMVPBlock();

    UseColorShader(kColor);
    DrawWireCube();
}

void CDrawUtil::DrawSphere(bool DoubleSided)
{
    Init();

    if (!DoubleSided)
        mpSphereModel->Draw(ERenderOption::NoMaterialSetup, 0);
    else
        mpDoubleSidedSphereModel->Draw(ERenderOption::NoMaterialSetup, 0);
}

void CDrawUtil::DrawSphere(const CColor &kColor)
{
    Init();
    UseColorShader(kColor);
    DrawSphere(false);
}

void CDrawUtil::DrawWireSphere(const CVector3f& Position, float Radius, const CColor& Color /*= CColor::skWhite*/)
{
    Init();

    // Create model matrix
    CTransform4f Transform;
    Transform.Scale(Radius);
    Transform.Translate(Position);
    CGraphics::sMVPBlock.ModelMatrix = Transform;
    CGraphics::UpdateMVPBlock();

    // Set other render params
    UseColorShader(Color);
    CMaterial::KillCachedMaterial();
    glBlendFunc(GL_ONE, GL_ZERO);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDepthMask(GL_TRUE);

    // Draw
    mpWireSphereModel->Draw(ERenderOption::NoMaterialSetup, 0);
}

void CDrawUtil::DrawBillboard(CTexture* pTexture, const CVector3f& Position, const CVector2f& Scale /*= CVector2f::skOne*/, const CColor& Tint /*= CColor::skWhite*/)
{
    Init();

    // Create translation-only model matrix
    CGraphics::sMVPBlock.ModelMatrix = CTransform4f::TranslationMatrix(Position);
    CGraphics::UpdateMVPBlock();

    // Set uniforms
    mpBillboardShader->SetCurrent();

    static GLuint ScaleLoc = mpBillboardShader->GetUniformLocation("BillboardScale");
    glUniform2f(ScaleLoc, Scale.X, Scale.Y);

    static GLuint TintLoc = mpBillboardShader->GetUniformLocation("TintColor");
    glUniform4f(TintLoc, Tint.R, Tint.G, Tint.B, Tint.A);

    pTexture->Bind(0);

    // Set other properties
    CMaterial::KillCachedMaterial();
    glBlendFunc(GL_ONE, GL_ZERO);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDepthMask(GL_TRUE);

    // Draw
    DrawSquare();
}

void CDrawUtil::DrawLightBillboard(ELightType Type, const CColor& LightColor, const CVector3f& Position, const CVector2f& Scale /*= CVector2f::skOne*/, const CColor& Tint /*= CColor::skWhite*/)
{
    Init();

    // Create translation-only model matrix
    CGraphics::sMVPBlock.ModelMatrix = CTransform4f::TranslationMatrix(Position);
    CGraphics::UpdateMVPBlock();

    // Set uniforms
    mpLightBillboardShader->SetCurrent();

    static GLuint ScaleLoc = mpLightBillboardShader->GetUniformLocation("BillboardScale");
    glUniform2f(ScaleLoc, Scale.X, Scale.Y);

    static GLuint ColorLoc = mpLightBillboardShader->GetUniformLocation("LightColor");
    glUniform4f(ColorLoc, LightColor.R, LightColor.G, LightColor.B, LightColor.A);

    static GLuint TintLoc = mpLightBillboardShader->GetUniformLocation("TintColor");
    glUniform4f(TintLoc, Tint.R, Tint.G, Tint.B, Tint.A);

    CTexture *pTexA = GetLightTexture(Type);
    CTexture *pTexB = GetLightMask(Type);
    pTexA->Bind(0);
    pTexB->Bind(1);

    static GLuint TextureLoc = mpLightBillboardShader->GetUniformLocation("Texture");
    static GLuint MaskLoc    = mpLightBillboardShader->GetUniformLocation("LightMask");
    glUniform1i(TextureLoc, 0);
    glUniform1i(MaskLoc, 1);

    // Set other properties
    CMaterial::KillCachedMaterial();
    glBlendFunc(GL_ONE, GL_ZERO);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDepthMask(GL_TRUE);

    // Draw
    DrawSquare();

}

void CDrawUtil::UseColorShader(const CColor& kColor)
{
    Init();
    mpColorShader->SetCurrent();

    static GLuint ColorLoc = mpColorShader->GetUniformLocation("ColorIn");
    glUniform4f(ColorLoc, kColor.R, kColor.G, kColor.B, kColor.A);

    CMaterial::KillCachedMaterial();
}

void CDrawUtil::UseColorShaderLighting(const CColor& kColor)
{
    Init();
    mpColorShaderLighting->SetCurrent();

    static GLuint NumLightsLoc = mpColorShaderLighting->GetUniformLocation("NumLights");
    glUniform1i(NumLightsLoc, CGraphics::sNumLights);

    static GLuint ColorLoc = mpColorShaderLighting->GetUniformLocation("ColorIn");
    glUniform4f(ColorLoc, kColor.R, kColor.G, kColor.B, kColor.A);

    CMaterial::KillCachedMaterial();
}

void CDrawUtil::UseTextureShader()
{
    UseTextureShader(CColor::skWhite);
}

void CDrawUtil::UseTextureShader(const CColor& TintColor)
{
    Init();
    mpTextureShader->SetCurrent();

    static GLuint TintColorLoc = mpTextureShader->GetUniformLocation("TintColor");
    glUniform4f(TintColorLoc, TintColor.R, TintColor.G, TintColor.B, TintColor.A);

    CMaterial::KillCachedMaterial();
}

void CDrawUtil::UseCollisionShader(bool IsFloor, bool IsUnstandable, const CColor& TintColor /*= CColor::skWhite*/)
{
    Init();
    mpCollisionShader->SetCurrent();

    // Force blend mode to opaque + set alpha to 0 to ensure collision geometry isn't bloomed
    glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ZERO, GL_ZERO);

    static GLuint TintColorLoc = mpCollisionShader->GetUniformLocation("TintColor");
    glUniform4f(TintColorLoc, TintColor.R, TintColor.G, TintColor.B, TintColor.A);

    static GLuint IsFloorLoc = mpCollisionShader->GetUniformLocation("IsFloor");
    glUniform1i(IsFloorLoc, IsFloor ? 1 : 0);

    static GLuint IsUnstandableLoc = mpCollisionShader->GetUniformLocation("IsUnstandable");
    glUniform1i(IsUnstandableLoc, IsUnstandable ? 1 : 0);

    CMaterial::KillCachedMaterial();
}

CShader* CDrawUtil::GetTextShader()
{
    Init();
    return mpTextShader;
}

void CDrawUtil::LoadCheckerboardTexture(uint32 GLTextureUnit)
{
    Init();
    mpCheckerTexture->Bind(GLTextureUnit);
}

CTexture* CDrawUtil::GetLightTexture(ELightType Type)
{
    Init();
    return mpLightTextures[(int) Type];
}

CTexture* CDrawUtil::GetLightMask(ELightType Type)
{
    Init();
    return mpLightMasks[(int) Type];
}

CModel* CDrawUtil::GetCubeModel()
{
    Init();
    return mpCubeModel;
}

// ************ PRIVATE ************
CDrawUtil::CDrawUtil()
{
}

void CDrawUtil::Init()
{
    if (!mDrawUtilInitialized)
    {
        debugf("Initializing CDrawUtil");
        InitGrid();
        InitSquare();
        InitLine();
        InitCube();
        InitWireCube();
        InitSphere();
        InitWireSphere();
        InitShaders();
        InitTextures();
        mDrawUtilInitialized = true;
    }
}

void CDrawUtil::InitGrid()
{
    debugf("Creating grid");

    const int kGridSize = 501; // must be odd
    const float kGridSpacing = 1.f;
    int MinIdx = (kGridSize - 1) / -2;
    int MaxIdx = (kGridSize - 1) / 2;

    mGridVertices.emplace();
    mGridVertices->SetVertexDesc(EVertexAttribute::Position);
    mGridVertices->Reserve(kGridSize * 4);

     for (int32 i = MinIdx; i <= MaxIdx; i++)
     {
         if (i == 0) continue;
         mGridVertices->AddVertex(CVector3f(MinIdx * kGridSpacing, i * kGridSpacing, 0.0f));
         mGridVertices->AddVertex(CVector3f(MaxIdx * kGridSpacing, i * kGridSpacing, 0.0f));
         mGridVertices->AddVertex(CVector3f(i * kGridSpacing, MinIdx * kGridSpacing, 0.0f));
         mGridVertices->AddVertex(CVector3f(i * kGridSpacing, MaxIdx * kGridSpacing, 0.0f));
     }

     mGridVertices->AddVertex(CVector3f(MinIdx * kGridSpacing, 0, 0.0f));
     mGridVertices->AddVertex(CVector3f(MaxIdx * kGridSpacing, 0, 0.0f));
     mGridVertices->AddVertex(CVector3f(0, MinIdx * kGridSpacing, 0.0f));
     mGridVertices->AddVertex(CVector3f(0, MaxIdx * kGridSpacing, 0.0f));

     int NumIndices = kGridSize * 4;
     mGridIndices.Reserve(NumIndices);
     for (uint16 i = 0; i < NumIndices; i++) mGridIndices.AddIndex(i);
     mGridIndices.SetPrimitiveType(GL_LINES);
}

void CDrawUtil::InitSquare()
{
    debugf("Creating square");
    mSquareVertices.emplace();
    mSquareVertices->SetActiveAttribs(EVertexAttribute::Position |
                                      EVertexAttribute::Normal |
                                      EVertexAttribute::Tex0 |
                                      EVertexAttribute::Tex1 |
                                      EVertexAttribute::Tex2 |
                                      EVertexAttribute::Tex3 |
                                      EVertexAttribute::Tex4 |
                                      EVertexAttribute::Tex5 |
                                      EVertexAttribute::Tex6 |
                                      EVertexAttribute::Tex7 );
    mSquareVertices->SetVertexCount(4);

    CVector3f SquareVertices[] = {
        CVector3f(-1.f,  1.f, 0.f),
        CVector3f( 1.f,  1.f, 0.f),
        CVector3f( 1.f, -1.f, 0.f),
        CVector3f(-1.f, -1.f, 0.f)
    };

    CVector3f SquareNormals[] = {
        CVector3f(0.f, 0.f, 1.f),
        CVector3f(0.f, 0.f, 1.f),
        CVector3f(0.f, 0.f, 1.f),
        CVector3f(0.f, 0.f, 1.f)
    };

    CVector2f SquareTexCoords[] = {
        CVector2f(0.f, 1.f),
        CVector2f(1.f, 1.f),
        CVector2f(1.f, 0.f),
        CVector2f(0.f, 0.f)
    };

    mSquareVertices->BufferAttrib(EVertexAttribute::Position, SquareVertices);
    mSquareVertices->BufferAttrib(EVertexAttribute::Normal, SquareNormals);

    for (uint32 iTex = 0; iTex < 8; iTex++)
    {
        EVertexAttribute Attrib = (EVertexAttribute) (EVertexAttribute::Tex0 << iTex);
        mSquareVertices->BufferAttrib(Attrib, SquareTexCoords);
    }

    mSquareIndices.Reserve(4);
    mSquareIndices.SetPrimitiveType(GL_TRIANGLE_STRIP);
    mSquareIndices.AddIndex(3);
    mSquareIndices.AddIndex(2);
    mSquareIndices.AddIndex(0);
    mSquareIndices.AddIndex(1);
}

void CDrawUtil::InitLine()
{
    debugf("Creating line");
    mLineVertices.emplace();
    mLineVertices->SetActiveAttribs(EVertexAttribute::Position);
    mLineVertices->SetVertexCount(2);

    mLineIndices.Reserve(2);
    mLineIndices.SetPrimitiveType(GL_LINES);
    mLineIndices.AddIndex(0);
    mLineIndices.AddIndex(1);
}

void CDrawUtil::InitCube()
{
    debugf("Creating cube");
    mpCubeModel = gpEditorStore->LoadResource("Cube.CMDL");
}

void CDrawUtil::InitWireCube()
{
    debugf("Creating wire cube");
    mWireCubeVertices.emplace();
    mWireCubeVertices->SetVertexDesc(EVertexAttribute::Position);
    mWireCubeVertices->Reserve(8);
    mWireCubeVertices->AddVertex(CVector3f(-0.5f, -0.5f, -0.5f));
    mWireCubeVertices->AddVertex(CVector3f(-0.5f,  0.5f, -0.5f));
    mWireCubeVertices->AddVertex(CVector3f( 0.5f,  0.5f, -0.5f));
    mWireCubeVertices->AddVertex(CVector3f( 0.5f, -0.5f, -0.5f));
    mWireCubeVertices->AddVertex(CVector3f(-0.5f, -0.5f,  0.5f));
    mWireCubeVertices->AddVertex(CVector3f( 0.5f, -0.5f,  0.5f));
    mWireCubeVertices->AddVertex(CVector3f( 0.5f,  0.5f,  0.5f));
    mWireCubeVertices->AddVertex(CVector3f(-0.5f,  0.5f,  0.5f));

    uint16 Indices[] = {
        0, 1,
        1, 2,
        2, 3,
        3, 0,
        4, 5,
        5, 6,
        6, 7,
        7, 4,
        0, 4,
        1, 7,
        2, 6,
        3, 5
    };
    mWireCubeIndices.AddIndices(Indices, sizeof(Indices) / sizeof(uint16));
    mWireCubeIndices.SetPrimitiveType(GL_LINES);
}

void CDrawUtil::InitSphere()
{
    debugf("Creating sphere");
    mpSphereModel = gpEditorStore->LoadResource("Sphere.CMDL");
    mpDoubleSidedSphereModel = gpEditorStore->LoadResource("SphereDoubleSided.CMDL");
}

void CDrawUtil::InitWireSphere()
{
    debugf("Creating wire sphere");
    mpWireSphereModel = gpEditorStore->LoadResource("WireSphere.CMDL");
}

void CDrawUtil::InitShaders()
{
    debugf("Creating shaders");
    mpColorShader          = CShader::FromResourceFile("ColorShader");
    mpColorShaderLighting  = CShader::FromResourceFile("ColorShaderLighting");
    mpBillboardShader      = CShader::FromResourceFile("BillboardShader");
    mpLightBillboardShader = CShader::FromResourceFile("LightBillboardShader");
    mpTextureShader        = CShader::FromResourceFile("TextureShader");
    mpCollisionShader      = CShader::FromResourceFile("CollisionShader");
    mpTextShader           = CShader::FromResourceFile("TextShader");
}

void CDrawUtil::InitTextures()
{
    debugf("Loading textures");
    mpCheckerTexture = gpEditorStore->LoadResource("Checkerboard.TXTR");

    mpLightTextures[0] = gpEditorStore->LoadResource("LightAmbient.TXTR");
    mpLightTextures[1] = gpEditorStore->LoadResource("LightDirectional.TXTR");
    mpLightTextures[2] = gpEditorStore->LoadResource("LightCustom.TXTR");
    mpLightTextures[3] = gpEditorStore->LoadResource("LightSpot.TXTR");

    mpLightMasks[0] = gpEditorStore->LoadResource("LightAmbientMask.TXTR");
    mpLightMasks[1] = gpEditorStore->LoadResource("LightDirectionalMask.TXTR");
    mpLightMasks[2] = gpEditorStore->LoadResource("LightCustomMask.TXTR");
    mpLightMasks[3] = gpEditorStore->LoadResource("LightSpotMask.TXTR");
}

void CDrawUtil::Shutdown()
{
    if (mDrawUtilInitialized)
    {
        debugf("Shutting down");
        mGridVertices = std::nullopt;
        mSquareVertices = std::nullopt;
        mLineVertices = std::nullopt;
        mWireCubeVertices = std::nullopt;
        delete mpColorShader;
        delete mpColorShaderLighting;
        delete mpTextureShader;
        delete mpCollisionShader;
        delete mpTextShader;
        mDrawUtilInitialized = false;
    }
}
