/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */

#include "CubismPose.hpp"
#include "Id/CubismIdManager.hpp"

using namespace Live2D::Cubism::Framework;

namespace Live2D { namespace Cubism { namespace Framework {

namespace {
const csmFloat32 Epsilon= 0.001f;
const csmFloat32 DefaultFadeInSeconds = 0.5f;

// Pose.jsonのタグ
const csmChar*   FadeIn = "FadeInTime";
const csmChar*   Link   = "Link";
const csmChar*   Groups = "Groups";
const csmChar*   Id     = "Id";
}

CubismPose::PartData::PartData()
{ }

CubismPose::PartData::~PartData()
{ }

CubismPose::PartData::PartData(const PartData& v)
                                            : ParameterIndex(0)
                                            , PartIndex(0)
{
    PartId = v.PartId;

    for (csmVector<PartData>::const_iterator ite = v.Link.Begin(); ite != v.Link.End(); ++ite)
    {
        Link.PushBack(*ite);
    }
}

CubismPose::PartData& CubismPose::PartData::operator=(const PartData& v)
{
    PartId = v.PartId;

    for (csmVector<PartData>::const_iterator ite = v.Link.Begin(); ite != v.Link.End(); ++ite)
    {
        Link.PushBack(*ite);
    }

    return (*this);
}

void CubismPose::PartData::Initialize(CubismModel* model)
{
    ParameterIndex = model->GetParameterIndex(PartId);
    PartIndex = model->GetPartIndex(PartId);

    model->SetParameterValue(ParameterIndex, 1);
}

CubismPose::CubismPose() : _fadeTimeSeconds(DefaultFadeInSeconds)
                         , _lastModel(NULL)
{ }

CubismPose::~CubismPose()
{ }

CubismPose* CubismPose::Create(const csmByte* pose3json, csmSizeInt size)
{
    Utils::CubismJson*  json = Utils::CubismJson::Create(pose3json, size);
    if (!json)
    {
        return NULL;
    }
    CubismPose* ret = CSM_NEW CubismPose();
    Utils::Value&       root = json->GetRoot();

    // フェード時間の指定
    if (!root[FadeIn].IsNull())
    {
        ret->_fadeTimeSeconds = root[FadeIn].ToFloat(DefaultFadeInSeconds);

        if (ret->_fadeTimeSeconds < 0.0f)
        {
            ret->_fadeTimeSeconds = DefaultFadeInSeconds;
        }
    }

    // パーツグループ
    Utils::Value&      poseListInfo = root[Groups];
    const csmInt32     poseCount = poseListInfo.GetSize();

    for (csmInt32 poseIndex = 0; poseIndex < poseCount; ++poseIndex)
    {
        Utils::Value&      idListInfo = poseListInfo[poseIndex];
        const csmInt32    idCount = idListInfo.GetSize();
        csmInt32    groupCount = 0;

        for (csmInt32 groupIndex = 0; groupIndex < idCount; ++groupIndex)
        {
            Utils::Value&   partInfo = idListInfo[groupIndex];
            PartData        partData;
            const CubismIdHandle parameterId = CubismFramework::GetIdManager()->GetId(partInfo[Id].GetRawString());

            partData.PartId = parameterId;

            // リンクするパーツの設定
            if (!partInfo[Link].IsNull())
            {
                Utils::Value&   linkListInfo = partInfo[Link];
                const csmInt32        linkCount = linkListInfo.GetSize();

                for (csmInt32 linkIndex = 0; linkIndex < linkCount; ++linkIndex)
                {
                    PartData             linkPart;
                    const CubismIdHandle linkId = CubismFramework::GetIdManager()->GetId(linkListInfo[linkIndex].GetString());

                    linkPart.PartId = linkId;

                    partData.Link.PushBack(linkPart);
                }
            }

            ret->_partGroups.PushBack(partData);

            ++groupCount;
        }

        ret->_partGroupCounts.PushBack(groupCount);

    }

    Utils::CubismJson::Delete(json);

    return ret;
}

void CubismPose::Delete(CubismPose* pose)
{
    CSM_DELETE_SELF(CubismPose, pose);
}

void CubismPose::Reset(CubismModel* model)
{
    csmInt32 beginIndex = 0;

    for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); ++i)
    {
        const csmInt32 groupCount = _partGroupCounts[i];

        for (csmInt32 j = beginIndex; j < beginIndex + groupCount; ++j)
        {
            _partGroups[j].Initialize(model);

            const csmInt32 partsIndex = _partGroups[j].PartIndex;
            const csmInt32 paramIndex = _partGroups[j].ParameterIndex;

            if (partsIndex < 0)
            {
                continue;
            }

            model->SetPartOpacity( partsIndex, (j == beginIndex ? 1.0f : 0.0f));
            model->SetParameterValue(   paramIndex, (j == beginIndex ? 1.0f : 0.0f));

            for (csmUint32 k = 0; k < _partGroups[j].Link.GetSize(); ++k)
            {
                _partGroups[j].Link[k].Initialize(model);
            }
        }

        beginIndex += groupCount;

    }

}

void CubismPose::CopyPartOpacities(CubismModel* model)
{
    for (csmUint32 groupIndex = 0; groupIndex < _partGroups.GetSize(); ++groupIndex)
    {
        PartData& partData = _partGroups[groupIndex];

        if (partData.Link.GetSize() == 0)
        {
            continue; // 連動するパラメータはない
        }

        const csmInt32    partIndex = _partGroups[groupIndex].PartIndex;
        const csmFloat32  opacity = model->GetPartOpacity(partIndex);

        for (csmUint32 linkIndex = 0; linkIndex < partData.Link.GetSize(); ++linkIndex)
        {
            PartData&   linkPart = partData.Link[linkIndex];
            const csmInt32    linkPartIndex = linkPart.PartIndex;

            if (linkPartIndex < 0)
            {
                continue;
            }

            model->SetPartOpacity(linkPartIndex, opacity);
        }
    }
}

void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount)
{
    csmInt32    visiblePartIndex = -1;
    csmFloat32  newOpacity = 1.0f;

    const csmFloat32 Phi = 0.5f;
    const csmFloat32 BackOpacityThreshold = 0.15f;

    // 現在、表示状態になっているパーツを取得
    for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i)
    {
        const csmInt32 partIndex = _partGroups[i].PartIndex;
        const csmInt32 paramIndex = _partGroups[i].ParameterIndex;

        if (model->GetParameterValue(paramIndex) > Epsilon)
        {
            if (visiblePartIndex >= 0)
            {
                break;
            }

            visiblePartIndex = i;
            if (_fadeTimeSeconds == 0.0f)
            {
                newOpacity = 1.0f;
                continue;
            }

            newOpacity = model->GetPartOpacity(partIndex);

            // 新しい不透明度を計算
            newOpacity += (deltaTimeSeconds / _fadeTimeSeconds);

            if (newOpacity > 1.0f)
            {
                newOpacity = 1.0f;
            }
        }
    }

    if (visiblePartIndex < 0)
    {
        visiblePartIndex = 0;
        newOpacity = 1.0f;
    }

    //  表示パーツ、非表示パーツの不透明度を設定する
    for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i)
    {
        const csmInt32 partsIndex = _partGroups[i].PartIndex;

        //  表示パーツの設定
        if (visiblePartIndex == i)
        {
            model->SetPartOpacity(partsIndex, newOpacity); // 先に設定
        }
        // 非表示パーツの設定
        else
        {
            csmFloat32 opacity = model->GetPartOpacity(partsIndex);
            csmFloat32 a1;          // 計算によって求められる不透明度

            if (newOpacity < Phi)
            {
                a1 = newOpacity * (Phi - 1) / Phi + 1.0f; // (0,1),(phi,phi)を通る直線式
            }
            else
            {
                a1 = (1 - newOpacity) * Phi / (1.0f - Phi); // (1,0),(phi,phi)を通る直線式
            }

            // 背景の見える割合を制限する場合
            const csmFloat32 backOpacity = (1.0f - a1) * (1.0f - newOpacity);

            if (backOpacity > BackOpacityThreshold)
            {
                a1 = 1.0f - BackOpacityThreshold / (1.0f - newOpacity);
            }

            if (opacity > a1)
            {
                opacity = a1; // 計算の不透明度よりも大きければ（濃ければ）不透明度を上げる
            }

            model->SetPartOpacity(partsIndex, opacity);
        }
    }
}

void CubismPose::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds)
{
    // 前回のモデルと同じではないときは初期化が必要
    if (model != _lastModel)
    {
        // パラメータインデックスの初期化
        Reset(model);
    }

    _lastModel = model;

    // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。
    if (deltaTimeSeconds < 0.0f)
    {
        deltaTimeSeconds = 0.0f;
    }

    csmInt32 beginIndex = 0;

    for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); i++)
    {
        const csmInt32 partGroupCount = _partGroupCounts[i];

        DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount);

        beginIndex += partGroupCount;
    }

    CopyPartOpacities(model);
}

}}}
