#include <utility>
#include <QMatrix4x4>
#include <QOpenGLFunctions>
#include <QOpenGLContext>
#include "XC.h"
#include "gl/Global.h"
#include "gl/ExtendShader.h"
#include "gl/Util.h"
#include "img/ResourceNode.h"
#include "img/BlendMode.h"
#include "core/LayerNode.h"
#include "core/ObjectNodeUtil.h"
#include "core/TimeKeyExpans.h"
#include "core/DepthKey.h"
#include "core/ResourceEvent.h"
#include "core/ResourceUpdatingWorkspace.h"
#include "core/FFDKeyUpdater.h"
#include "core/ImageKeyUpdater.h"
#include "core/ClippingFrame.h"
#include "core/DestinationTexturizer.h"

namespace core
{

LayerNode::LayerNode(const QString& aName, ShaderHolder& aShaderHolder)
    : mName(aName)
    , mIsVisible(true)
    , mIsSlimmedDown()
    , mInitialRect()
    , mTimeLine()
    , mShaderHolder(aShaderHolder)
    , mIsClipped()
    , mMeshTransformer("./data/shader/MeshTransform.glslex")
    , mCurrentMesh()
    , mClippees()
{
}

void LayerNode::setDefaultImage(const img::ResourceHandle& aHandle)
{
    setDefaultImage(aHandle, aHandle->blendMode());
}

void LayerNode::setDefaultImage(const img::ResourceHandle& aHandle, img::BlendMode aBlendMode)
{
    XC_ASSERT(aHandle);
    XC_PTR_ASSERT(aHandle->image().data());
    XC_ASSERT(aHandle->image().pixelSize().isValid());
    if (!aHandle || !aHandle->image().data()) return; // fail safe

    auto key = new ImageKey();
    mTimeLine.grabDefaultKey(TimeKeyType_Image, key);
    key->setImage(aHandle, aBlendMode);
    key->resetGridMesh();
    key->setImageOffsetByCenter();

    mShaderHolder.reserveShaders(aBlendMode);
    mShaderHolder.reserveGridShader();
    mShaderHolder.reserveClipperShaders();
}

void LayerNode::setDefaultPosture(const QVector2D& aPos)
{
    {
        auto key = (MoveKey*)mTimeLine.defaultKey(TimeKeyType_Move);
        if (!key)
        {
            key = new MoveKey();
            mTimeLine.grabDefaultKey(TimeKeyType_Move, key);
        }
        key->data().setPos(aPos);
    }
    {
        auto key = (RotateKey*)mTimeLine.defaultKey(TimeKeyType_Rotate);
        if (!key)
        {
            key = new RotateKey();
            mTimeLine.grabDefaultKey(TimeKeyType_Rotate, key);
        }
    }
    {
        auto key = (ScaleKey*)mTimeLine.defaultKey(TimeKeyType_Scale);
        if (!key)
        {
            key = new ScaleKey();
            mTimeLine.grabDefaultKey(TimeKeyType_Scale, key);
        }
    }
}

void LayerNode::setDefaultDepth(float aValue)
{
    auto key = (DepthKey*)mTimeLine.defaultKey(TimeKeyType_Depth);
    if (!key)
    {
        key = new DepthKey();
        mTimeLine.grabDefaultKey(TimeKeyType_Depth, key);
    }
    key->setDepth(aValue);
}

void LayerNode::setDefaultOpacity(float aValue)
{
    auto key = (OpaKey*)mTimeLine.defaultKey(TimeKeyType_Opa);
    if (!key)
    {
        key = new OpaKey();
        mTimeLine.grabDefaultKey(TimeKeyType_Opa, key);
    }
    key->setOpacity(aValue);
}

float LayerNode::initialDepth() const
{
    auto key = (DepthKey*)mTimeLine.defaultKey(TimeKeyType_Depth);
    return key ? key->depth() : 0.0f;
}

void LayerNode::setClipped(bool aIsClipped)
{
    mIsClipped = aIsClipped;
}

bool LayerNode::isClipper() const
{
    if (mIsClipped) return false;

    auto prev = this->prevSib();
    if (!prev || !prev->renderer() || !prev->renderer()->isClipped())
    {
        return false;
    }
    return true;
}

img::BlendMode LayerNode::blendMode() const
{
    auto key = (ImageKey*)mTimeLine.defaultKey(TimeKeyType_Image);
    return key ? key->data().blendMode() : img::BlendMode_Normal;
}

void LayerNode::setBlendMode(img::BlendMode aMode)
{
    auto key = (ImageKey*)mTimeLine.defaultKey(TimeKeyType_Image);
    if (key)
    {
        key->data().setBlendMode(aMode);
        mShaderHolder.reserveShaders(aMode);
    }
}

void LayerNode::prerender(const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor)
{
    mCurrentMesh = nullptr;

    if (!mIsVisible) return;

    // transform shape by current keys
    transformShape(aInfo, aAccessor);
}

void LayerNode::render(const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor)
{
    if (!mIsVisible) return;

    if (aAccessor.get(mTimeLine).opa().isZero()) return;

    // render shape
    renderShape(aInfo, aAccessor);

    if (aInfo.isGrid) return;

    // render clippees
    renderClippees(aInfo, aAccessor);
}

void LayerNode::renderClippees(
        const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor)
{
    if (!aInfo.clippingFrame || !isClipper()) return;

    // reset clippees
    ObjectNodeUtil::collectRenderClippees(*this, mClippees, aAccessor);

    // clipping frame
    auto& frame = *aInfo.clippingFrame;

    const uint8 clippingId = frame.forwardClippingId();

    RenderInfo childInfo = aInfo;
    childInfo.clippingId = clippingId;

    uint32 stamp = frame.renderStamp() + 1;

    for (auto clippee : mClippees)
    {
        XC_PTR_ASSERT(clippee.renderer);

        // write clipper as necessary
        if (stamp != frame.renderStamp())
        {
            renderClipper(aInfo, aAccessor, clippingId);
            stamp = frame.renderStamp();
        }

        // render child
        clippee.renderer->render(childInfo, aAccessor);
    }
}

void LayerNode::renderClipper(
        const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor, uint8 aClipperId)
{
    if (!mIsVisible || !aInfo.clippingFrame) return;
    if (!mCurrentMesh) return;

    gl::Global::Functions& ggl = gl::Global::functions();
    auto& shader = mShaderHolder.clipperShader(aInfo.clippingId != 0);
    auto& expans = aAccessor.get(mTimeLine);

    if (!expans.areaTexture()) return;
    auto textureId = expans.areaTexture()->id();
    auto textureSize = expans.areaTexture()->size();
    auto texCoordOffset = mCurrentMesh->originOffset() - expans.imageOffset();

    core::ClippingFrame& frame = *aInfo.clippingFrame;
    frame.updateRenderStamp();

    // bind framebuffer
    frame.bind();
    frame.setupDrawBuffers();

    // bind textures
    ggl.glActiveTexture(GL_TEXTURE0);
    ggl.glBindTexture(GL_TEXTURE_2D, textureId);
    ggl.glActiveTexture(GL_TEXTURE1);
    ggl.glBindTexture(GL_TEXTURE_2D, frame.texture().id());

    // view matrix
    const QMatrix4x4 viewMatrix = aInfo.camera.viewMatrix();
    // color
    const float opacity = expans.worldOpacity();
    QColor color(255, 255, 255, xc_clamp((int)(255 * opacity), 0, 255));
    {
        shader.bind();

        shader.setAttributeBuffer("inPosition", mMeshTransformer.positions(), GL_FLOAT, 3);
        shader.setAttributeArray("inTexCoord", mCurrentMesh->texCoords(), mCurrentMesh->vertexCount());

        shader.setUniformValue("uViewMatrix", viewMatrix);
        shader.setUniformValue("uScreenSize", QSizeF(aInfo.camera.deviceScreenSize()));
        shader.setUniformValue("uImageSize", QSizeF(textureSize));
        shader.setUniformValue("uTexCoordOffset", texCoordOffset);
        shader.setUniformValue("uColor", color);
        shader.setUniformValue("uClipperId", (int)aClipperId);
        shader.setUniformValue("uTexture", 0);
        shader.setUniformValue("uDestTexture", 1);

        if (aInfo.clippingId != 0)
        {
            shader.setUniformValue("uClippingId", (int)aInfo.clippingId);
        }

        gl::Util::drawElements(
                    mCurrentMesh->primitiveMode(),
                    GL_UNSIGNED_INT,
                    mCurrentMesh->getIndexBuffer());

        shader.release();
    }
    // unbind texture
    ggl.glActiveTexture(GL_TEXTURE0);
    ggl.glBindTexture(GL_TEXTURE_2D, 0);

    // release framebuffer
    frame.release();

    // bind default framebuffer
    ggl.glBindFramebuffer(GL_FRAMEBUFFER, aInfo.framebuffer);

    ggl.glFlush();
}

void LayerNode::transformShape(
        const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor)
{
    auto& expans = aAccessor.get(mTimeLine);

    // current mesh
    LayerMesh* mesh = expans.ffdMesh();
    if (!mesh || mesh->vertexCount() <= 0) return;

    // positions
    util::ArrayBlock<const gl::Vector3> positions;
    bool useInfluence = true;

    if (aInfo.originMesh) // ignore mesh deforming
    {
        useInfluence = false;
        if (expans.areaImageKey())
        {
            mesh = &(expans.areaImageKey()->data().gridMesh());
            if (!mesh || mesh->vertexCount() <= 0) return;
        }
        positions = util::ArrayBlock<const gl::Vector3>(
                    mesh->positions(), mesh->vertexCount());
    }
    else
    {
        useInfluence = (mesh == expans.bone().targetMesh());
        positions = util::ArrayBlock<const gl::Vector3>(
                    expans.ffd().positions(), expans.ffd().count());
        XC_MSG_ASSERT(mesh->vertexCount() == positions.count(),
                      "vtx count = %d, %d", mesh->vertexCount(), positions.count());
    }
    XC_ASSERT(positions);

    // transform
    mMeshTransformer.callGL(
                expans, mesh->getMeshBuffer(), mesh->originOffset(),
                positions, aInfo.nonPosed, useInfluence);

    mCurrentMesh = mesh;
}

void LayerNode::renderShape(
        const RenderInfo& aInfo, const TimeCacheAccessor& aAccessor)
{
    if (!mCurrentMesh) return;

    gl::Global::Functions& ggl = gl::Global::functions();
    const bool isClippee = (aInfo.clippingFrame && aInfo.clippingId != 0);

    auto& expans = aAccessor.get(mTimeLine);
    if (!expans.areaImageKey() || !expans.areaTexture()) return;

    auto textureId = expans.areaTexture()->id();
    auto textureSize = expans.areaTexture()->size();
    auto texCoordOffset = mCurrentMesh->originOffset() - expans.imageOffset();
    auto blendMode = expans.blendMode();
    const QMatrix4x4 viewMatrix = aInfo.camera.viewMatrix();


    auto& shader = aInfo.isGrid ?
                mShaderHolder.gridShader() :
                mShaderHolder.shader(blendMode, isClippee);

    // update destination color
    XC_PTR_ASSERT(aInfo.destTexturizer);
    auto destTextureId = aInfo.destTexturizer->texture().id();
    if (!aInfo.isGrid && blendMode != img::BlendMode_Normal)
    {
        aInfo.destTexturizer->update(
                    aInfo.framebuffer, aInfo.dest, viewMatrix,
                    *mCurrentMesh, mMeshTransformer.positions());
    }

    if (aInfo.isGrid)
    {
        ggl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    }
    else
    {
        // blend func
        ggl.glEnable(GL_BLEND);
        ggl.glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);

        ggl.glActiveTexture(GL_TEXTURE0);
        ggl.glBindTexture(GL_TEXTURE_2D, textureId);
        ggl.glActiveTexture(GL_TEXTURE1);
        ggl.glBindTexture(GL_TEXTURE_2D, destTextureId);

        if (isClippee)
        {
            ggl.glActiveTexture(GL_TEXTURE2);
            ggl.glBindTexture(GL_TEXTURE_2D, aInfo.clippingFrame->texture().id());
        }
    }

    {
        const float opacity = expans.worldOpacity();
        QColor color(255, 255, 255, xc_clamp((int)(255 * opacity), 0, 255));
        if (aInfo.isGrid) color = QColor(Qt::black);

        shader.bind();

        shader.setAttributeBuffer("inPosition", mMeshTransformer.positions(), GL_FLOAT, 3);
        shader.setAttributeArray("inTexCoord", mCurrentMesh->texCoords(), mCurrentMesh->vertexCount());

        shader.setUniformValue("uViewMatrix", viewMatrix);
        shader.setUniformValue("uScreenSize", QSizeF(aInfo.camera.deviceScreenSize()));
        shader.setUniformValue("uImageSize", QSizeF(textureSize));
        shader.setUniformValue("uTexCoordOffset", texCoordOffset);
        shader.setUniformValue("uColor", color);
        shader.setUniformValue("uTexture", 0);
        shader.setUniformValue("uDestTexture", 1);

        if (isClippee)
        {
            shader.setUniformValue("uClippingId", (int)aInfo.clippingId);
            shader.setUniformValue("uClippingTexture", 2);
        }

        gl::Util::drawElements(
                    mCurrentMesh->primitiveMode(),
                    GL_UNSIGNED_INT,
                    mCurrentMesh->getIndexBuffer());

        shader.release();
    }

    if (aInfo.isGrid)
    {
        ggl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
    else
    {
        ggl.glActiveTexture(GL_TEXTURE0);
        ggl.glBindTexture(GL_TEXTURE_2D, 0);

        // blend func
        ggl.glDisable(GL_BLEND);
    }

    ggl.glFlush();
}

cmnd::Vector LayerNode::createResourceUpdater(const ResourceEvent& aEvent)
{
    cmnd::Vector result;

    if (aEvent.type() != ResourceEvent::Type_Reload)
    {
        return result;
    }

    ResourceUpdatingWorkspacePtr workspace = std::make_shared<ResourceUpdatingWorkspace>();
    const bool createTransitions = !mTimeLine.isEmpty(TimeKeyType_FFD);

    // image key
    result.push(ImageKeyUpdater::createResourceUpdater(*this, aEvent, workspace, createTransitions));

    // ffd key should be called finally
    if (createTransitions)
    {
        result.push(FFDKeyUpdater::createResourceUpdater(*this, workspace));
    }

    return result;
}

bool LayerNode::serialize(Serializer& aOut) const
{
    static const std::array<uint8, 8> kSignature =
        { 'L', 'a', 'y', 'e', 'r', 'N', 'd', '_' };

    // block begin
    auto pos = aOut.beginBlock(kSignature);

    // name
    aOut.write(mName);
    // visibility
    aOut.write(mIsVisible);
    // slim-down
    aOut.write(mIsSlimmedDown);
    // initial rect
    aOut.write(mInitialRect);
    // clipping
    aOut.write(mIsClipped);
    // timeline
    if (!mTimeLine.serialize(aOut))
    {
        return false;
    }

    // block end
    aOut.endBlock(pos);

    return !aOut.failure();
}

bool LayerNode::deserialize(Deserializer& aIn)
{
    // check block begin
    if (!aIn.beginBlock("LayerNd_"))
        return aIn.errored("invalid signature of layer node");

    // name
    aIn.read(mName);
    // visibility
    aIn.read(mIsVisible);
    // slim-down
    aIn.read(mIsSlimmedDown);
    // initial rect
    aIn.read(mInitialRect);
    // clipping
    aIn.read(mIsClipped);
    // timeline
    if (!mTimeLine.deserialize(aIn))
        return aIn.errored("failed to deserialize time line");

    // reserve shaders
    {
        mShaderHolder.reserveGridShader();
        mShaderHolder.reserveClipperShaders();

        auto defaultKey = (ImageKey*)mTimeLine.defaultKey(TimeKeyType_Image);
        if (defaultKey)
        {
            mShaderHolder.reserveShaders(defaultKey->data().blendMode());
        }

        auto& map = mTimeLine.map(TimeKeyType_Image);
        for (auto key : map)
        {
            mShaderHolder.reserveShaders(((ImageKey*)key)->data().blendMode());
        }
    }

    // check block end
    if (!aIn.endBlock())
        return aIn.errored("invalid end of layer node");

    return aIn.checkStream();
}

} // namespace core
