#include "CStaticModel.h"
#include "Core/Render/CDrawUtil.h"
#include "Core/Render/CRenderer.h"
#include "Core/OpenGL/GLCommon.h"

CStaticModel::CStaticModel()
    : CBasicModel(nullptr)
    , mpMaterial(nullptr)
    , mTransparent(false)
{
}

CStaticModel::CStaticModel(CMaterial *pMat)
    : CBasicModel()
    , mpMaterial(pMat)
    , mTransparent((pMat->Options() & EMaterialOption::Transparent) != 0)
{
}

CStaticModel::~CStaticModel()
{
}

void CStaticModel::AddSurface(SSurface *pSurface)
{
    mSurfaces.push_back(pSurface);
    mAABox.ExpandBounds(pSurface->AABox);

    mVertexCount += pSurface->VertexCount;
    mTriangleCount += pSurface->TriangleCount;
}

void CStaticModel::BufferGL()
{
    if (!mBuffered)
    {
        mVBO.Clear();
        mIBOs.clear();

        for (uint32 iSurf = 0; iSurf < mSurfaces.size(); iSurf++)
        {
            SSurface *pSurf = mSurfaces[iSurf];

            uint16 VBOStartOffset = (uint16) mVBO.Size();
            mVBO.Reserve((uint16) pSurf->VertexCount);

            for (uint32 iPrim = 0; iPrim < pSurf->Primitives.size(); iPrim++)
            {
                SSurface::SPrimitive *pPrim = &pSurf->Primitives[iPrim];
                CIndexBuffer *pIBO = InternalGetIBO(pPrim->Type);
                pIBO->Reserve(pPrim->Vertices.size() + 1); // Allocate enough space for this primitive, plus the restart index

                // Next step: add new vertices to the VBO and create a small index buffer for the current primitive
                std::vector<uint16> Indices(pPrim->Vertices.size());
                for (uint32 iVert = 0; iVert < pPrim->Vertices.size(); iVert++)
                    Indices[iVert] = mVBO.AddIfUnique(pPrim->Vertices[iVert], VBOStartOffset);

                // then add the indices to the IBO. We convert some primitives to strips to minimize draw calls.
                switch (pPrim->Type)
                {
                    case EPrimitiveType::Triangles:
                        pIBO->TrianglesToStrips(Indices.data(), Indices.size());
                        break;
                    case EPrimitiveType::TriangleFan:
                        pIBO->FansToStrips(Indices.data(), Indices.size());
                        break;
                    case EPrimitiveType::Quads:
                        pIBO->QuadsToStrips(Indices.data(), Indices.size());
                        break;
                    default:
                        pIBO->AddIndices(Indices.data(), Indices.size());
                        pIBO->AddIndex(0xFFFF); // primitive restart
                        break;
                }
            }

            // Make sure the number of submesh offset vectors matches the number of IBOs, then add the offsets
            while (mIBOs.size() > mSurfaceEndOffsets.size())
                mSurfaceEndOffsets.emplace_back(std::vector<uint32>(mSurfaces.size()));

            for (uint32 iIBO = 0; iIBO < mIBOs.size(); iIBO++)
                mSurfaceEndOffsets[iIBO][iSurf] = mIBOs[iIBO].GetSize();
        }

        mVBO.Buffer();

        for (uint32 iIBO = 0; iIBO < mIBOs.size(); iIBO++)
            mIBOs[iIBO].Buffer();

        mBuffered = true;
    }
}

void CStaticModel::GenerateMaterialShaders()
{
    if (mpMaterial)
        mpMaterial->GenerateShader(false);
}

void CStaticModel::ClearGLBuffer()
{
    mVBO.Clear();
    mIBOs.clear();
    mSurfaceEndOffsets.clear();
    mBuffered = false;
}

void CStaticModel::Draw(FRenderOptions Options)
{
    if (!mBuffered) BufferGL();

    mVBO.Bind();
    glLineWidth(1.f);

    auto DoDraw = [this]()
    {
        // Draw IBOs
        for (uint32 iIBO = 0; iIBO < mIBOs.size(); iIBO++)
        {
            CIndexBuffer *pIBO = &mIBOs[iIBO];
            pIBO->Bind();
            glDrawElements(pIBO->GetPrimitiveType(), pIBO->GetSize(), GL_UNSIGNED_SHORT, (void*) 0);
            pIBO->Unbind();
            gDrawCount++;
        }
    };

    // Bind material
    if ((Options & ERenderOption::NoMaterialSetup) == 0)
    {
        for (CMaterial* passMat = mpMaterial; passMat; passMat = passMat->GetNextDrawPass())
        {
            passMat->SetCurrent(Options);
            DoDraw();
        }
    }
    else
    {
        DoDraw();
    }

    mVBO.Unbind();
}

void CStaticModel::DrawSurface(FRenderOptions Options, uint32 Surface)
{
    if (!mBuffered) BufferGL();

    mVBO.Bind();
    glLineWidth(1.f);

    auto DoDraw = [this, Surface]()
    {
        for (uint32 iIBO = 0; iIBO < mIBOs.size(); iIBO++)
        {
            // Since there is a shared IBO for every mesh, we need two things to draw a single one: an offset and a size
            uint32 Offset = 0;
            if (Surface > 0) Offset = mSurfaceEndOffsets[iIBO][Surface - 1];
            uint32 Size = mSurfaceEndOffsets[iIBO][Surface] - Offset;

            if (!Size) continue; // The chosen submesh doesn't use this IBO

            // Now we have both, so we can draw
            mIBOs[iIBO].DrawElements(Offset, Size);
            gDrawCount++;
        }
    };

    // Bind material
    if ((Options & ERenderOption::NoMaterialSetup) == 0)
    {
        for (CMaterial* passMat = mpMaterial; passMat; passMat = passMat->GetNextDrawPass())
        {
            passMat->SetCurrent(Options);
            DoDraw();
        }
    }
    else
    {
        DoDraw();
    }

    mVBO.Unbind();
}

void CStaticModel::DrawWireframe(FRenderOptions Options, CColor WireColor /*= CColor::skWhite*/)
{
    if (!mBuffered) BufferGL();

    // Set up wireframe
    WireColor.A = 0;
    CDrawUtil::UseColorShader(WireColor);
    Options |= ERenderOption::NoMaterialSetup;
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    glBlendFunc(GL_ONE, GL_ZERO);

    // Draw surfaces
    for (uint32 iSurf = 0; iSurf < mSurfaces.size(); iSurf++)
        DrawSurface(Options, iSurf);

    // Cleanup
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

CMaterial* CStaticModel::GetMaterial()
{
    return mpMaterial;
}

void CStaticModel::SetMaterial(CMaterial *pMat)
{
    mpMaterial = pMat;
    mTransparent = pMat->Options().HasFlag(EMaterialOption::Transparent);
}

bool CStaticModel::IsTransparent()
{
    return mTransparent;
}

bool CStaticModel::IsOccluder()
{
    return mpMaterial->Options().HasFlag(EMaterialOption::Occluder);
}

CIndexBuffer* CStaticModel::InternalGetIBO(EPrimitiveType Primitive)
{
    GLenum type = GXPrimToGLPrim(Primitive);

    for (uint32 iIBO = 0; iIBO < mIBOs.size(); iIBO++)
    {
        if (mIBOs[iIBO].GetPrimitiveType() == type)
            return &mIBOs[iIBO];
    }

    mIBOs.emplace_back(CIndexBuffer(type));
    return &mIBOs.back();
}
