/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */


#include "EditorDefs.h"

#include "ExportManager.h"

// Qt
#include <QMessageBox>

// Maestro
#include <Maestro/Types/AnimParamType.h>

// Editor
#include "ViewManager.h"
#include "OBJExporter.h"
#include "OCMExporter.h"
#include "FBXExporterDialog.h"
#include "TrackViewExportKeyTimeDlg.h"
#include "AnimationContext.h"
#include "TrackView/DirectorNodeAnimator.h"
#include "Util/AutoDirectoryRestoreFileDialog.h"
#include "QtUI/WaitCursor.h"
#include "WaitProgress.h"
#include "Objects/SelectionGroup.h"
#include "Include/IObjectManager.h"
#include "TrackView/TrackViewTrack.h"
#include "TrackView/TrackViewSequenceManager.h"
#include "Resource.h"
#include "Plugins/ComponentEntityEditorPlugin/Objects/ComponentEntityObject.h"

#include <IEntityRenderState.h>
#include <IStatObj.h>

namespace
{
    inline Export::Vector3D Vec3ToVector3D(const Vec3& vec)
    {
        Export::Vector3D ret;
        ret.x = vec.x;
        ret.y = vec.y;
        ret.z = vec.z;
        return ret;
    }

    const float kTangentDelta = 0.01f;
    const float kAspectRatio = 1.777778f;
    const int kReserveCount = 7; // x,y,z,rot_x,rot_y,rot_z,fov
    const QString kPrimaryCameraName = "PrimaryCamera";
} // namespace



//////////////////////////////////////////////////////////
// CMesh
Export::CMesh::CMesh()
{
    ::ZeroMemory(&material, sizeof(material));
    material.opacity = 1.0f;
}


//////////////////////////////////////////////////////////
// CObject
Export::CObject::CObject(const char* pName)
    : m_MeshHash(0)
{
    pos.x = pos.y = pos.z = 0;
    rot.v.x = rot.v.y = rot.v.z = 0;
    rot.w = 1.0f;
    scale.x = scale.y = scale.z = 1.0f;

    nParent = -1;

    azstrcpy(name, AZ_ARRAY_SIZE(name), pName);

    materialName[0] = '\0';

    entityType = Export::eEntity;

    cameraTargetNodeName[0] = '\0';

    m_pLastObject = nullptr;
}


void Export::CObject::SetMaterialName(const char* pName)
{
    azstrcpy(materialName, AZ_ARRAY_SIZE(materialName), pName);
}


// CExportData
void Export::CData::Clear()
{
    m_objects.clear();
}


// CExportManager
CExportManager::CExportManager()
    : m_isPrecaching(false)
    , m_pBaseObj(nullptr)
    , m_FBXBakedExportFPS(0.0f)
    , m_fScale(100.0f)
    ,                 // this scale is used by CryEngine RC
    m_bAnimationExport(false)
    , m_bExportLocalCoords(false)
    , m_numberOfExportFrames(0)
    , m_pivotEntityObject(nullptr)
    , m_bBakedKeysSequenceExport(true)
    , m_animTimeExportPrimarySequenceCurrentTime(0.0f)
    , m_animKeyTimeExport(true)
    , m_soundKeyTimeExport(true)
    , m_bExportOnlyPrimaryCamera(false)
{
    RegisterExporter(new COBJExporter());
    RegisterExporter(new COCMExporter());
}


CExportManager::~CExportManager()
{
    m_data.Clear();
    for (TExporters::iterator ppExporter = m_exporters.begin(); ppExporter != m_exporters.end(); ++ppExporter)
    {
        (*ppExporter)->Release();
    }
}


bool CExportManager::RegisterExporter(IExporter* pExporter)
{
    if (!pExporter)
    {
        return false;
    }

    m_exporters.push_back(pExporter);
    return true;
}

inline f32 Sandbox2MayaFOVDeg(const f32 fov, const f32 ratio) { return RAD2DEG(2.0f * atan_tpl(tan(DEG2RAD(fov) / 2.0f) * ratio)); };
inline f32 Sandbox2MayaFOVRad2Deg(const f32 fov, const f32 ratio) { return RAD2DEG(2.0f * atan_tpl(tan(fov / 2.0f) * ratio)); };

void CExportManager::AddEntityAnimationData(const CTrackViewTrack* pTrack, Export::CObject* pObj, AnimParamType entityTrackParamType)
{
    int keyCount = pTrack->GetKeyCount();
    pObj->m_entityAnimData.reserve(keyCount * kReserveCount);

    for (int keyNumber = 0; keyNumber < keyCount; keyNumber++)
    {
        const CTrackViewKeyConstHandle currentKeyHandle = pTrack->GetKey(keyNumber);
        const float fCurrentKeytime = currentKeyHandle.GetTime();

        float trackValue = 0.0f;
        pTrack->GetValue(fCurrentKeytime, trackValue);

        ISplineInterpolator* pSpline = pTrack->GetSpline();

        if (pSpline)
        {
            Export::EntityAnimData entityData;
            entityData.dataType = (Export::AnimParamType)pTrack->GetParameterType().GetType();
            entityData.keyValue = trackValue;
            entityData.keyTime = fCurrentKeytime;

            if (entityTrackParamType == AnimParamType::Position)
            {
                entityData.keyValue *= 100.0f;
            }
            else if (entityTrackParamType == AnimParamType::FOV)
            {
                entityData.keyValue = Sandbox2MayaFOVDeg(entityData.keyValue, kAspectRatio);
            }


            ISplineInterpolator::ValueType tin;
            ISplineInterpolator::ValueType tout;
            ZeroStruct(tin);
            ZeroStruct(tout);
            pSpline->GetKeyTangents(keyNumber, tin, tout);

            float fInTantentX = tin[0];
            float fInTantentY = tin[1];

            float fOutTantentX = tout[0];
            float fOutTantentY = tout[1];

            if (fInTantentX == 0.0f)
            {
                fInTantentX = kTangentDelta;
            }

            if (fOutTantentX == 0.0f)
            {
                fOutTantentX = kTangentDelta;
            }

            entityData.leftTangent = fInTantentY / fInTantentX;
            entityData.rightTangent = fOutTantentY / fOutTantentX;

            if (entityTrackParamType == AnimParamType::Position)
            {
                entityData.leftTangent *= 100.0f;
                entityData.rightTangent *= 100.0f;
            }

            float fPrevKeyTime = 0.0f;
            float fNextKeyTime = 0.0f;

            bool bIsFirstKey = false;
            bool bIsMiddleKey = false;
            bool bIsLastKey = false;

            if (keyNumber == 0 && keyNumber < (keyCount - 1))
            {
                const CTrackViewKeyConstHandle nextKeyHandle = pTrack->GetKey(keyNumber + 1);
                fNextKeyTime = nextKeyHandle.GetTime();

                if (fNextKeyTime != 0.0f)
                {
                    bIsFirstKey = true;
                }
            }
            else if (keyNumber > 0)
            {
                const CTrackViewKeyConstHandle prevKeyHandle = pTrack->GetKey(keyNumber - 1);
                fPrevKeyTime = prevKeyHandle.GetTime();

                if (keyNumber < (keyCount - 1))
                {
                    const CTrackViewKeyConstHandle nextKeyHandle = pTrack->GetKey(keyNumber + 1);
                    fNextKeyTime = nextKeyHandle.GetTime();

                    if (fNextKeyTime != 0.0f)
                    {
                        bIsMiddleKey = true;
                    }
                }
                else
                {
                    bIsLastKey = true;
                }
            }

            float fLeftTangentWeightValue = 0.0f;
            float fRightTangentWeightValue = 0.0f;

            if (bIsFirstKey)
            {
                fRightTangentWeightValue = fOutTantentX / fNextKeyTime;
            }
            else if (bIsMiddleKey)
            {
                fLeftTangentWeightValue = fInTantentX / (fCurrentKeytime - fPrevKeyTime);
                fRightTangentWeightValue = fOutTantentX / (fNextKeyTime - fCurrentKeytime);
            }
            else if (bIsLastKey)
            {
                fLeftTangentWeightValue = fInTantentX / (fCurrentKeytime - fPrevKeyTime);
            }

            entityData.leftTangentWeight = fLeftTangentWeightValue;
            entityData.rightTangentWeight = fRightTangentWeightValue;

            pObj->m_entityAnimData.push_back(entityData);
        }
    }
}


void CExportManager::ProcessEntityAnimationTrack(
    const AZ::EntityId entityId, Export::CObject* pObj, AnimParamType entityTrackParamType)
{
    CTrackViewAnimNode* pEntityNode = GetIEditor()->GetSequenceManager()->GetActiveAnimNode(entityId);
    CTrackViewTrack* pEntityTrack = (pEntityNode ? pEntityNode->GetTrackForParameter(entityTrackParamType) : nullptr);

    if (!pEntityTrack)
    {
        return;
    }

    if (pEntityTrack->GetParameterType() == AnimParamType::FOV)
    {
        AddEntityAnimationData(pEntityTrack, pObj, entityTrackParamType);
        return;
    }

    for (unsigned int trackNumber = 0; trackNumber < pEntityTrack->GetChildCount(); ++trackNumber)
    {
        CTrackViewTrack* pSubTrack = static_cast<CTrackViewTrack*>(pEntityTrack->GetChild(trackNumber));

        if (pSubTrack)
        {
            AddEntityAnimationData(pSubTrack, pObj, entityTrackParamType);
        }
    }
}


void CExportManager::AddEntityAnimationData(AZ::EntityId entityId)
{
    Export::CObject* pObj = new Export::CObject(m_pBaseObj->GetName().toUtf8().data());

    ProcessEntityAnimationTrack(entityId, pObj, AnimParamType::Position);
    ProcessEntityAnimationTrack(entityId, pObj, AnimParamType::Rotation);
}


void CExportManager::AddMesh(Export::CObject* pObj, const IIndexedMesh* pIndMesh, Matrix34A* pTm)
{
    if (m_isPrecaching || !pObj)
    {
        return;
    }

    pObj->m_MeshHash    =   reinterpret_cast<size_t>(pIndMesh);
    IIndexedMesh::SMeshDescription meshDesc;
    pIndMesh->GetMeshDescription(meshDesc);

    // if we have subset of meshes we need to duplicate vertices,
    // keep transformation of submesh,
    // and store new offset for indices
    int newOffsetIndex = pObj->GetVertexCount();

    if (meshDesc.m_nVertCount)
    {
        pObj->m_vertices.reserve(meshDesc.m_nVertCount + newOffsetIndex);
        pObj->m_normals.reserve(meshDesc.m_nVertCount + newOffsetIndex);
    }

    for (int v = 0; v < meshDesc.m_nVertCount; ++v)
    {
        Vec3 n = meshDesc.m_pNorms[v].GetN();
        Vec3 tmp = (meshDesc.m_pVerts ? meshDesc.m_pVerts[v] : meshDesc.m_pVertsF16[v].ToVec3());
        if (pTm)
        {
            tmp = pTm->TransformPoint(tmp);
        }

        pObj->m_vertices.push_back(Vec3ToVector3D(tmp * m_fScale));
        pObj->m_normals.push_back(Vec3ToVector3D(n));
    }

    if (meshDesc.m_nCoorCount)
    {
        pObj->m_texCoords.reserve(meshDesc.m_nCoorCount + newOffsetIndex);
    }

    for (int v = 0; v < meshDesc.m_nCoorCount; ++v)
    {
        Vec2 uv = meshDesc.m_pTexCoord[v].GetUV();
        uv.y = 1.0f - uv.y;
        pObj->m_texCoords.push_back({uv.x,uv.y});
    }

    if (pIndMesh->GetSubSetCount() && !(pIndMesh->GetSubSetCount() == 1 && pIndMesh->GetSubSet(0).nNumIndices == 0))
    {
        for (int i = 0; i < pIndMesh->GetSubSetCount(); ++i)
        {
            Export::CMesh* pMesh = new Export::CMesh();

            const SMeshSubset& sms = pIndMesh->GetSubSet(i);
            const vtx_idx* pIndices = &meshDesc.m_pIndices[sms.nFirstIndexId];
            int nTris = sms.nNumIndices / 3;
            pMesh->m_faces.reserve(nTris);
            for (int f = 0; f < nTris; ++f)
            {
                Export::Face face;
                face.idx[0] = *(pIndices++) + newOffsetIndex;
                face.idx[1] = *(pIndices++) + newOffsetIndex;
                face.idx[2] = *(pIndices++) + newOffsetIndex;
                pMesh->m_faces.push_back(face);
            }

            pObj->m_meshes.push_back(pMesh);
        }
    }
    else
    {
        Export::CMesh* pMesh = new Export::CMesh();
        if (meshDesc.m_nFaceCount == 0 && meshDesc.m_nIndexCount != 0 && meshDesc.m_pIndices != nullptr)
        {
            const vtx_idx* pIndices = &meshDesc.m_pIndices[0];
            int nTris = meshDesc.m_nIndexCount / 3;
            pMesh->m_faces.reserve(nTris);
            for (int f = 0; f < nTris; ++f)
            {
                Export::Face face;
                face.idx[0] = *(pIndices++) + newOffsetIndex;
                face.idx[1] = *(pIndices++) + newOffsetIndex;
                face.idx[2] = *(pIndices++) + newOffsetIndex;
                pMesh->m_faces.push_back(face);
            }
        }
        else
        {
            pMesh->m_faces.reserve(meshDesc.m_nFaceCount);
            for (int f = 0; f < meshDesc.m_nFaceCount; ++f)
            {
                Export::Face face;
                face.idx[0] = meshDesc.m_pFaces[f].v[0];
                face.idx[1] = meshDesc.m_pFaces[f].v[1];
                face.idx[2] = meshDesc.m_pFaces[f].v[2];
                pMesh->m_faces.push_back(face);
            }
        }

        pObj->m_meshes.push_back(pMesh);
    }
}


bool CExportManager::AddStatObj(Export::CObject* pObj, IStatObj* pStatObj, Matrix34A* pTm)
{
    IIndexedMesh* pIndMesh = nullptr;

    if (pStatObj->GetSubObjectCount())
    {
        for (int i = 0; i < pStatObj->GetSubObjectCount(); i++)
        {
            IStatObj::SSubObject* pSubObj = pStatObj->GetSubObject(i);
            if (pSubObj && pSubObj->nType == STATIC_SUB_OBJECT_MESH && pSubObj->pStatObj)
            {
                pIndMesh = nullptr;
                if (m_isOccluder)
                {
                    if (pSubObj->pStatObj->GetLodObject(2))
                    {
                        pIndMesh = pSubObj->pStatObj->GetLodObject(2)->GetIndexedMesh(true);
                    }
                    if (!pIndMesh && pSubObj->pStatObj->GetLodObject(1))
                    {
                        pIndMesh = pSubObj->pStatObj->GetLodObject(1)->GetIndexedMesh(true);
                    }
                }
                if (!pIndMesh)
                {
                    pIndMesh = pSubObj->pStatObj->GetIndexedMesh(true);
                }
                if (pIndMesh)
                {
                    AddMesh(pObj, pIndMesh, pTm);
                }
            }
        }
    }

    if (!pIndMesh)
    {
        if (m_isOccluder)
        {
            if (pStatObj->GetLodObject(2))
            {
                pIndMesh = pStatObj->GetLodObject(2)->GetIndexedMesh(true);
            }
            if (!pIndMesh && pStatObj->GetLodObject(1))
            {
                pIndMesh = pStatObj->GetLodObject(1)->GetIndexedMesh(true);
            }
        }
        if (!pIndMesh)
        {
            pIndMesh = pStatObj->GetIndexedMesh(true);
        }
        if (pIndMesh)
        {
            AddMesh(pObj, pIndMesh, pTm);
        }
    }

    return true;
}

bool CExportManager::AddMeshes(Export::CObject* pObj)
{
    if (m_pBaseObj->GetType() == OBJTYPE_AZENTITY)
    {
        CEntityObject* pEntityObject = (CEntityObject*)m_pBaseObj;
        IRenderNode* pEngineNode = pEntityObject->GetEngineNode();

        if (pEngineNode)
        {
            if (!m_isPrecaching)
            {
                for (int i = 0; i < pEngineNode->GetSlotCount(); ++i)
                {
                    Matrix34A tm;
                    IStatObj* pStatObj = pEngineNode->GetEntityStatObj(i, 0, &tm);
                    if (pStatObj)
                    {
                        Matrix34A objTM = m_pBaseObj->GetWorldTM();
                        objTM.Invert();
                        tm = objTM * tm;
                        AddStatObj(pObj, pStatObj, &tm);
                    }
                }
            }
        }
    }

    return true;
}


bool CExportManager::AddObject(CBaseObject* pBaseObj)
{
    if (m_isOccluder)
    {
        return false;
    }

    m_pBaseObj = pBaseObj;

    if (m_bAnimationExport)
    {
        if (pBaseObj->GetType() == OBJTYPE_AZENTITY)
        {
            const auto componentEntityObject = static_cast<CComponentEntityObject*>(pBaseObj);
            AddEntityAnimationData(componentEntityObject->GetAssociatedEntityId());
            return true;
        }
    }

    if (m_isPrecaching)
    {
        AddMeshes(nullptr);
        return true;
    }

    Export::CObject* pObj = new Export::CObject(m_pBaseObj->GetName().toUtf8().data());

    AddPosRotScale(pObj, pBaseObj);
    m_data.m_objects.push_back(pObj);

    m_objectMap[pBaseObj] = int(m_data.m_objects.size() - 1);

    AddMeshes(pObj);
    m_pBaseObj = nullptr;

    return true;
}


void CExportManager::AddPosRotScale(Export::CObject* pObj, const CBaseObject* pBaseObj)
{
    Vec3 pos = pBaseObj->GetPos();
    pObj->pos.x = pos.x * m_fScale;
    pObj->pos.y = pos.y * m_fScale;
    pObj->pos.z = pos.z * m_fScale;

    Quat rot = pBaseObj->GetRotation();
    pObj->rot.v.x = rot.v.x;
    pObj->rot.v.y = rot.v.y;
    pObj->rot.v.z = rot.v.z;
    pObj->rot.w = rot.w;

    Vec3 scale = pBaseObj->GetScale();
    pObj->scale.x = scale.x;
    pObj->scale.y = scale.y;
    pObj->scale.z = scale.z;
}

void CExportManager::AddEntityData(Export::CObject* pObj, Export::AnimParamType dataType, const float fValue, const float fTime)
{
    Export::EntityAnimData entityData;
    entityData.dataType = dataType;
    entityData.leftTangent = kTangentDelta;
    entityData.rightTangent = kTangentDelta;
    entityData.rightTangentWeight = 0.0f;
    entityData.leftTangentWeight = 0.0f;
    entityData.keyValue = fValue;
    entityData.keyTime = fTime;
    pObj->m_entityAnimData.push_back(entityData);
}


void CExportManager::SolveHierarchy()
{
    for (TObjectMap::iterator it = m_objectMap.begin(); it != m_objectMap.end(); ++it)
    {
        CBaseObject* pObj = it->first;
        int index = it->second;
        if (pObj && pObj->GetParent())
        {
            CBaseObject* pParent = pObj->GetParent();
            TObjectMap::iterator itFind = m_objectMap.find(pParent);
            if (itFind != m_objectMap.end())
            {
                int indexOfParent = itFind->second;
                if (indexOfParent >= 0 && index >= 0)
                {
                    m_data.m_objects[index]->nParent = indexOfParent;
                }
            }
        }
    }

    m_objectMap.clear();
}

bool CExportManager::ShowFBXExportDialog()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();

    if (!pSequence)
    {
        return false;
    }

    CFBXExporterDialog fpsDialog;

    CTrackViewNode* pivotObjectNode = pSequence->GetFirstSelectedNode();

    if (pivotObjectNode && !pivotObjectNode->IsGroupNode())
    {
        m_pivotEntityObject = static_cast<CEntityObject*>(GetIEditor()->GetObjectManager()->FindObject(pivotObjectNode->GetName().c_str()));

        if (m_pivotEntityObject)
        {
            fpsDialog.SetExportLocalCoordsCheckBoxEnable(true);
        }
    }

    if (fpsDialog.exec() != QDialog::Accepted)
    {
        return false;
    }

    SetFBXExportSettings(fpsDialog.GetExportCoordsLocalToTheSelectedObject(), fpsDialog.GetExportOnlyPrimaryCamera(), fpsDialog.GetFPS());

    return true;
}

bool CExportManager::ProcessObjectsForExport()
{
    Export::CObject* pObj = new Export::CObject(kPrimaryCameraName.toUtf8().data());
    pObj->entityType = Export::eCamera;
    m_data.m_objects.push_back(pObj);

    float fpsTimeInterval = 1.0f / m_FBXBakedExportFPS;
    float timeValue = 0.0f;

    GetIEditor()->GetAnimation()->SetRecording(false);
    GetIEditor()->GetAnimation()->SetPlaying(false);

    int startFrame = 0;
    timeValue = startFrame * fpsTimeInterval;

    for (int frameID = startFrame; frameID <= m_numberOfExportFrames; ++frameID)
    {
        GetIEditor()->GetAnimation()->SetTime(timeValue);

        for (size_t objectID = 0; objectID < m_data.m_objects.size(); ++objectID)
        {
            Export::CObject* pObj2 =  m_data.m_objects[objectID];
            CBaseObject* pObject = nullptr;

            if (QString::compare(pObj2->name, kPrimaryCameraName) == 0)
            {
                pObject = GetIEditor()->GetObjectManager()->FindObject(GetIEditor()->GetViewManager()->GetCameraObjectId());
            }
            else
            {
                if (m_bExportOnlyPrimaryCamera && pObj2->entityType != Export::eCameraTarget)
                {
                    continue;
                }

                pObject = pObj2->GetLastObjectPtr();
            }

            if (!pObject)
            {
                continue;
            }

            Quat rotation(pObject->GetRotation());

            if (pObject->GetParent())
            {
                CBaseObject* pParentObject = pObject->GetParent();
                Quat parentWorldRotation;

                const Vec3& parentScale = pParentObject->GetScale();
                float threshold = 0.0003f;

                bool bParentScaled = false;

                if ((fabsf(parentScale.x - 1.0f) + fabsf(parentScale.y - 1.0f) + fabsf(parentScale.z - 1.0f)) >= threshold)
                {
                    bParentScaled = true;
                }

                if (bParentScaled)
                {
                    Matrix34 tm = pParentObject->GetWorldTM();
                    tm.OrthonormalizeFast();
                    parentWorldRotation = Quat(tm);
                }
                else
                {
                    parentWorldRotation = Quat(pParentObject->GetWorldTM());
                }

                rotation = parentWorldRotation * rotation;
            }

            Vec3 objectPos = pObject->GetWorldPos();

            if (m_bExportLocalCoords && m_pivotEntityObject && m_pivotEntityObject != pObject)
            {
                Matrix34 currentObjectTM = pObject->GetWorldTM();
                Matrix34 invParentTM = m_pivotEntityObject->GetWorldTM();
                invParentTM.Invert();
                currentObjectTM = invParentTM * currentObjectTM;

                objectPos = currentObjectTM.GetTranslation();
                rotation = Quat(currentObjectTM);
            }

            Ang3 worldAngles = RAD2DEG(Ang3::GetAnglesXYZ(Matrix33(rotation)));

            Export::EntityAnimData entityData;
            entityData.keyTime = timeValue;
            entityData.leftTangentWeight = 0.0f;
            entityData.rightTangentWeight = 0.0f;
            entityData.leftTangent = 0.0f;
            entityData.rightTangent = 0.0f;

            entityData.keyValue = objectPos.x;
            entityData.keyValue *= 100.0f;
            entityData.dataType = (Export::AnimParamType)AnimParamType::PositionX;
            pObj2->m_entityAnimData.push_back(entityData);

            entityData.keyValue = objectPos.y;
            entityData.keyValue *= 100.0f;
            entityData.dataType = (Export::AnimParamType)AnimParamType::PositionY;
            pObj2->m_entityAnimData.push_back(entityData);

            entityData.keyValue = objectPos.z;
            entityData.keyValue *= 100.0f;
            entityData.dataType = (Export::AnimParamType)AnimParamType::PositionZ;
            pObj2->m_entityAnimData.push_back(entityData);

            entityData.keyValue = worldAngles.x;
            entityData.dataType = (Export::AnimParamType)AnimParamType::RotationX;
            pObj2->m_entityAnimData.push_back(entityData);

            entityData.keyValue = worldAngles.y;
            entityData.dataType = (Export::AnimParamType)AnimParamType::RotationY;
            pObj2->m_entityAnimData.push_back(entityData);

            entityData.dataType = (Export::AnimParamType)AnimParamType::RotationZ;
            entityData.keyValue = worldAngles.z;
            pObj2->m_entityAnimData.push_back(entityData);
        }

        timeValue += fpsTimeInterval;
    }

    return true;
}

bool CExportManager::IsDuplicateObjectBeingAdded(const QString& newObjectName)
{
    for (int objectID = 0; objectID < m_data.m_objects.size(); ++objectID)
    {
        if (newObjectName.compare(m_data.m_objects[objectID]->name, Qt::CaseInsensitive) == 0)
        {
            return true;
        }
    }

    return false;
}

QString CExportManager::CleanXMLText(const QString& text)
{
    QString outText(text);
    outText.replace("\\", "_");
    outText.replace("/", "_");
    outText.replace(" ", "_");
    outText.replace(":", "-");
    outText.replace(";", "-");
    return outText;
}

void CExportManager::FillAnimTimeNode(XmlNodeRef writeNode, CTrackViewAnimNode* pObjectNode, [[maybe_unused]] CTrackViewSequence* currentSequence)
{
    if (!writeNode)
    {
        return;
    }

    CTrackViewTrackBundle allTracks = pObjectNode->GetAllTracks();
    const unsigned int numAllTracks = allTracks.GetCount();
    bool bCreatedSubNodes = false;

    if (numAllTracks > 0)
    {
        XmlNodeRef objNode = writeNode->createNode(CleanXMLText(pObjectNode->GetName().c_str()).toUtf8().data());
        writeNode->setAttr("time", m_animTimeExportPrimarySequenceCurrentTime);

        for (unsigned int trackID = 0; trackID < numAllTracks; ++trackID)
        {
            CTrackViewTrack* childTrack = allTracks.GetTrack(trackID);

            AnimParamType trackType = childTrack->GetParameterType().GetType();

            if (trackType == AnimParamType::Animation || trackType == AnimParamType::Sound)
            {
                QString childName = CleanXMLText(childTrack->GetName().c_str());

                if (childName.isEmpty())
                {
                    continue;
                }

                XmlNodeRef subNode = objNode->createNode(childName.toUtf8().data());
                CTrackViewKeyBundle keyBundle = childTrack->GetAllKeys();
                uint keysNumber = keyBundle.GetKeyCount();

                for (uint keyID = 0; keyID < keysNumber; ++keyID)
                {
                    const CTrackViewKeyHandle& keyHandle = keyBundle.GetKey(keyID);

                    QString keyContentName;
                    float keyStartTime = 0.0f;
                    float keyEndTime = 0.0f;
                    ;
                    float keyTime = 0.0f;
                    float keyDuration = 0.0f;

                    if (trackType == AnimParamType::Animation)
                    {
                        if (!m_animKeyTimeExport)
                        {
                            continue;
                        }

                        ICharacterKey animationKey;
                        keyHandle.GetKey(&animationKey);
                        keyStartTime = animationKey.m_startTime;
                        keyEndTime = animationKey.m_endTime;
                        keyTime = animationKey.time;
                        keyContentName = CleanXMLText(animationKey.m_animation.c_str());
                        keyDuration = animationKey.GetActualDuration();
                    }
                    else if (trackType == AnimParamType::Sound)
                    {
                        if (!m_soundKeyTimeExport)
                        {
                            continue;
                        }

                        ISoundKey soundKey;
                        keyHandle.GetKey(&soundKey);
                        keyTime = soundKey.time;
                        keyContentName = CleanXMLText(soundKey.sStartTrigger.c_str());
                        keyDuration = soundKey.fDuration;
                    }

                    if (keyContentName.isEmpty())
                    {
                        continue;
                    }

                    XmlNodeRef keyNode = subNode->createNode(keyContentName.toUtf8().data());

                    float keyGlobalTime = m_animTimeExportPrimarySequenceCurrentTime + keyTime;
                    keyNode->setAttr("keyTime", keyGlobalTime);

                    if (keyStartTime > 0)
                    {
                        keyNode->setAttr("startTime", keyStartTime);
                    }

                    if (keyEndTime > 0)
                    {
                        keyNode->setAttr("endTime", keyEndTime);
                    }

                    if (keyDuration > 0)
                    {
                        keyNode->setAttr("duration", keyDuration);
                    }

                    subNode->addChild(keyNode);
                    objNode->addChild(subNode);
                    bCreatedSubNodes = true;
                }
            }
        }

        if (bCreatedSubNodes)
        {
            writeNode->addChild(objNode);
        }
    }
}


bool CExportManager::AddObjectsFromSequence(CTrackViewSequence* pSequence, XmlNodeRef seqNode)
{
    CTrackViewAnimNodeBundle allNodes = pSequence->GetAllAnimNodes();
    const unsigned int numAllNodes = allNodes.GetCount();

    for (unsigned int nodeID = 0; nodeID < numAllNodes; ++nodeID)
    {
        CTrackViewAnimNode* pAnimNode = allNodes.GetNode(nodeID);

        if (seqNode && pAnimNode)
        {
            FillAnimTimeNode(seqNode, pAnimNode, pSequence);
        }

        AZ::Entity* entity = nullptr;
        AZ::ComponentApplicationBus::BroadcastResult(
            entity, &AZ::ComponentApplicationBus::Events::FindEntity, pAnimNode->GetAzEntityId());

        if (entity)
        {
            QString addObjectName = entity->GetName().c_str();

            if (IsDuplicateObjectBeingAdded(addObjectName))
            {
                continue;
            }

            Export::CObject* pObj = new Export::CObject(entity->GetName().c_str());

            pObj->m_entityAnimData.reserve(m_numberOfExportFrames * kReserveCount);
            m_data.m_objects.push_back(pObj);
        }
    }

    CTrackViewTrackBundle trackBundle = pSequence->GetTracksByParam(AnimParamType::Sequence);

    const uint numSequenceTracks = trackBundle.GetCount();
    for (uint i = 0; i < numSequenceTracks; ++i)
    {
        CTrackViewTrack* pSequenceTrack = trackBundle.GetTrack(i);
        if (pSequenceTrack->IsDisabled())
        {
            continue;
        }

        const uint numKeys = pSequenceTrack->GetKeyCount();
        for (uint keyIndex = 0; keyIndex < numKeys; ++keyIndex)
        {
            const CTrackViewKeyHandle& keyHandle = pSequenceTrack->GetKey(keyIndex);
            ISequenceKey sequenceKey;
            keyHandle.GetKey(&sequenceKey);

            CTrackViewSequence* pSubSequence = CDirectorNodeAnimator::GetSequenceFromSequenceKey(sequenceKey);

            if (pSubSequence)
            {
                if (pSubSequence && !pSubSequence->IsDisabled())
                {
                    XmlNodeRef subSeqNode = nullptr;

                    if (!seqNode)
                    {
                        AddObjectsFromSequence(pSubSequence);
                    }
                    else
                    {
                        // In case of exporting animation/sound times data
                        const QString sequenceName = QString::fromUtf8(pSubSequence->GetName().c_str());
                        XmlNodeRef subSeqNode2 = seqNode->createNode(sequenceName.toUtf8().data());

                        if (sequenceName == m_animTimeExportPrimarySequenceName)
                        {
                            m_animTimeExportPrimarySequenceCurrentTime = sequenceKey.time;
                        }
                        else
                        {
                            m_animTimeExportPrimarySequenceCurrentTime += sequenceKey.time;
                        }

                        AddObjectsFromSequence(pSubSequence, subSeqNode2);
                        seqNode->addChild(subSeqNode2);
                    }
                }
            }
        }
    }

    return false;
}

bool CExportManager::AddSelectedEntityObjects()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();
    if (!pSequence)
    {
        return false;
    }

    CTrackViewAnimNodeBundle selectedNodes = pSequence->GetSelectedAnimNodes();
    const unsigned int numSelectedNodes = selectedNodes.GetCount();

    for (unsigned int nodeNumber = 0; nodeNumber < numSelectedNodes; ++nodeNumber)
    {
        if (m_bAnimationExport)
        {
            CTrackViewAnimNode* pAnimNode = selectedNodes.GetNode(nodeNumber);
            AddEntityAnimationData(pAnimNode->GetAzEntityId());
        }
    }

    return true;
}

bool CExportManager::AddSelectedRegionObjects()
{
    AABB box;
    GetIEditor()->GetSelectedRegion(box);
    if (box.IsEmpty())
    {
        return false;
    }

    std::vector<CBaseObject*> objects;
    GetIEditor()->GetObjectManager()->FindObjectsInAABB(box, objects);

    const size_t numObjects = objects.size();
    if (numObjects > m_data.m_objects.size())
    {
        m_data.m_objects.reserve(numObjects + 1); // +1 for terrain
    }
    // First run pipeline to precache geometry
    m_isPrecaching = true;
    for (size_t i = 0; i < numObjects; ++i)
    {
        AddObject(objects[i]);
    }

    // Repeat pipeline to collect geometry
    m_isPrecaching = false;
    for (size_t i = 0; i < numObjects; ++i)
    {
        AddObject(objects[i]);
    }

    return true;
}

bool CExportManager::ExportToFile(const char* filename, bool bClearDataAfterExport)
{
    bool bRet = false;
    QString ext = PathUtil::GetExt(filename);

    if (m_data.GetObjectCount() == 0)
    {
        QMessageBox::warning(QApplication::activeWindow(), QString(), QObject::tr("Track View selection does not exist as an object."));
        return false;
    }

    for (int i = 0; i < m_exporters.size(); ++i)
    {
        IExporter* pExporter = m_exporters[i];
        if (!QString::compare(ext, pExporter->GetExtension(), Qt::CaseInsensitive))
        {
            bRet = pExporter->ExportToFile(filename, &m_data);
            break;
        }
    }

    if (bClearDataAfterExport)
    {
        m_data.Clear();
    }
    return bRet;
}


bool CExportManager::Export(const char* defaultName, const char* defaultExt, const char* defaultPath, [[maybe_unused]] bool isSelectedObjects, bool isSelectedRegionObjects, bool isOccluder, bool bAnimationExport)
{
    m_bAnimationExport = bAnimationExport;

    m_isOccluder    =   isOccluder;
    const float OldScale    =   m_fScale;
    if (isOccluder)
    {
        m_fScale    =   1.f;
    }

    m_data.Clear();
    m_objectMap.clear();

    QString filters;
    for (TExporters::iterator ppExporter = m_exporters.begin(); ppExporter != m_exporters.end(); ++ppExporter)
    {
        IExporter* pExporter = (*ppExporter);
        if (pExporter)
        {
            const QString ext = pExporter->GetExtension();
            const QString newFilter = QString("%1 (*.%2)").arg(pExporter->GetShortDescription(), ext);
            if (filters.isEmpty())
            {
                filters = newFilter;
            }
            else if (ext == defaultExt) // the default extension should be the first item in the list, so it's the default export options
            {
                filters = newFilter + ";;" + filters;
            }
            else
            {
                filters = filters + ";;" + newFilter;
            }
        }
    }
    filters += ";;All files (*)";

    bool returnRes = false;

    QString newFilename(defaultName);
    if (m_bAnimationExport || CFileUtil::SelectSaveFile(filters, defaultExt, defaultPath, newFilename))
    {
        WaitCursor wait;
        if (isSelectedRegionObjects)
        {
            AddSelectedRegionObjects();
        }

        if (!bAnimationExport)
        {
            SolveHierarchy();
        }

        if (m_bAnimationExport)
        {
            CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();

            if (pSequence)
            {
                // Save To FBX custom selected nodes
                if (!m_bBakedKeysSequenceExport)
                {
                    returnRes = AddSelectedEntityObjects();
                }
                else
                {
                    // Export the whole sequence with baked keys
                    if (ShowFBXExportDialog())
                    {
                        m_numberOfExportFrames = static_cast<int>(pSequence->GetTimeRange().end * m_FBXBakedExportFPS);

                        if (!m_bExportOnlyPrimaryCamera)
                        {
                            AddObjectsFromSequence(pSequence);
                        }

                        returnRes = ProcessObjectsForExport();
                        SolveHierarchy();
                    }
                }
            }

            if (returnRes)
            {
                returnRes = ExportToFile(defaultName);
            }
        }
        else
        {
            returnRes = ExportToFile(newFilename.toStdString().c_str());
        }
    }

    m_fScale = OldScale;
    m_bBakedKeysSequenceExport = true;
    m_FBXBakedExportFPS = 0.0f;

    return returnRes;
}

void CExportManager::SetFBXExportSettings(bool bLocalCoordsToSelectedObject, bool bExportOnlyPrimaryCamera, const float fps)
{
    m_bExportLocalCoords = bLocalCoordsToSelectedObject;
    m_bExportOnlyPrimaryCamera = bExportOnlyPrimaryCamera;
    m_FBXBakedExportFPS = fps;
}

Export::Object* Export::CData::AddObject(const char* objectName)
{
    for (size_t objectID = 0; objectID < m_objects.size(); ++objectID)
    {
        if (strcmp(m_objects[objectID]->name, objectName) == 0)
        {
            return m_objects[objectID];
        }
    }

    CObject* pObj = new CObject(objectName);
    m_objects.push_back(pObj);
    return m_objects[m_objects.size() - 1];
}

bool CExportManager::ImportFromFile(const char* filename)
{
    bool bRet = false;
    QString ext = PathUtil::GetExt(filename);

    m_data.Clear();

    for (size_t handlerID = 0; handlerID < m_exporters.size(); ++handlerID)
    {
        IExporter* pExporter = m_exporters[handlerID];
        if (!QString::compare(ext, pExporter->GetExtension(), Qt::CaseInsensitive))
        {
            bRet = pExporter->ImportFromFile(filename, &m_data);
            break;
        }
    }

    return bRet;
}

bool CExportManager::ExportSingleStatObj(IStatObj* pStatObj, const char* filename)
{
    Export::CObject* pObj = new Export::CObject(Path::GetFileName(filename).toUtf8().data());
    AddStatObj(pObj, pStatObj);
    m_data.m_objects.push_back(pObj);
    ExportToFile(filename, true);
    return true;
}

void CExportManager::SaveNodeKeysTimeToXML()
{
    CTrackViewSequence* pSequence = GetIEditor()->GetAnimation()->GetSequence();
    if (!pSequence)
    {
        return;
    }

    CTrackViewExportKeyTimeDlg exportDialog;

    if (exportDialog.exec() == QDialog::Accepted)
    {
        m_animKeyTimeExport = exportDialog.IsAnimationExportChecked();
        m_soundKeyTimeExport = exportDialog.IsSoundExportChecked();

        QString filters = "All files (*.xml)";
        QString defaultName = QString::fromUtf8(pSequence->GetName().c_str()) + ".xml";

        QtUtil::QtMFCScopedHWNDCapture cap;
        CAutoDirectoryRestoreFileDialog dlg(QFileDialog::AcceptSave, QFileDialog::AnyFile, "xml", defaultName, filters, {}, {}, cap);
        if (dlg.exec())
        {
            m_animTimeNode = XmlHelpers::CreateXmlNode(pSequence->GetName().c_str());
            m_animTimeExportPrimarySequenceName = QString::fromUtf8(pSequence->GetName().c_str());

            m_data.Clear();
            m_animTimeExportPrimarySequenceCurrentTime = 0.0;

            AddObjectsFromSequence(pSequence, m_animTimeNode);

            m_animTimeNode->saveToFile(dlg.selectedFiles().constFirst().toStdString().c_str());
            QMessageBox::information(QApplication::activeWindow(), QString(), QObject::tr("Export Finished"));
        }
    }
}
