/*
 * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
 * its licensors.
 *
 * For complete copyright and license terms please see the LICENSE at the root of this
 * distribution (the "License"). All use of this software is governed by the License,
 * or, if provided, by the license below or the license accompanying this file. Do not
 * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 */
#include "StdAfx.h"

#include <Editor/EditorBlastMeshDataComponent.h>
#include <Editor/EditorBlastSliceAssetHandler.h>

#include <AzCore/Component/Entity.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/GenericStreams.h>
#include <AzCore/Serialization/ObjectStream.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/Slice/SliceComponent.h>
#include <AzCore/StringFunc/StringFunc.h>

#include <AzFramework/StringFunc/StringFunc.h>

#include <SceneAPI/SceneCore/Containers/RuleContainer.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/DataTypes/Groups/IMeshGroup.h>
#include <SceneAPI/SceneData/Groups/MeshGroup.h>
#include <SceneAPI/SceneData/ManifestBase/SceneNodeSelectionList.h>
#include <SceneAPI/SceneData/Rules/MaterialRule.h>

#include <GFxFramework/MaterialIO/Material.h>

namespace Blast
{
    // BlastSliceAssetStorageComponent

    void BlastSliceAssetStorageComponent::Reflect(AZ::ReflectContext* context)
    {
        using namespace AZ::Edit;

        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serialize->Class<BlastSliceAssetStorageComponent, AzToolsFramework::Components::EditorComponentBase>()
                ->Version(2)
                ->Field("Mesh Data", &BlastSliceAssetStorageComponent::m_meshAssetIdList)
                ->Field("Mesh Path List", &BlastSliceAssetStorageComponent::m_meshAssetPathList);

            if (AZ::EditContext* ec = serialize->GetEditContext())
            {
                ec->Class<BlastSliceAssetStorageComponent>(
                      "Blast Slice Storage Component", "Used process blast slice data")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::Category, "Physics")
                    ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/Box.png")
                    ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/Box.png")
                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game"))
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ->Attribute(AZ::Edit::Attributes::AddableByUser, false)                    
                    ->DataElement(
                        AZ::Edit::UIHandlers::Default, &BlastSliceAssetStorageComponent::m_meshAssetIdList, "Mesh Data",
                        "Slice data to fill out the mesh list")
                    ->DataElement(
                        AZ::Edit::UIHandlers::Default, &BlastSliceAssetStorageComponent::m_meshAssetPathList,
                        "Mesh Paths", "The mesh path list");
            }
        }

        if (AZ::BehaviorContext* behavior = azrtti_cast<AZ::BehaviorContext*>(context))
        {
            behavior->Class<BlastSliceAssetStorageComponent>("BlastSliceAssetStorageComponent")
                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
                ->Attribute(AZ::Script::Attributes::Module, "blast")
                ->Method("GenerateAssetInfo", &BlastSliceAssetStorageComponent::GenerateAssetInfo)
                ->Method("WriteMaterialFile", &BlastSliceAssetStorageComponent::WriteMaterialFile);
        }
    }

    bool BlastSliceAssetStorageComponent::GenerateAssetInfo(
        const AZStd::vector<AZStd::string>& chunkNames, AZStd::string_view blastFilename,
        AZStd::string_view assetinfoFilename)
    {
        AZ::SerializeContext* serializeContext = nullptr;
        AZ::ComponentApplicationBus::BroadcastResult(
            serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
        if (serializeContext == nullptr)
        {
            return false;
        }
        using namespace AZ::SceneAPI::Containers;
        using namespace AZ::SceneAPI::SceneData;

        AZStd::string filename;
        AZ::StringFunc::Path::Split(blastFilename.data(), nullptr, nullptr, &filename, nullptr);

        AZStd::any sceneManifestPointer(serializeContext->CreateAny(azrtti_typeid<SceneManifest>()));
        SceneManifest* sceneManifest = AZStd::any_cast<SceneManifest>(&sceneManifestPointer);

        AZStd::vector<AZStd::any> meshGroupData;
        meshGroupData.reserve(chunkNames.size());

        AZStd::vector<AZStd::any> materialRuleData;
        materialRuleData.reserve(chunkNames.size());

        for (const AZStd::string& chunkName : chunkNames)
        {
            meshGroupData.emplace_back(serializeContext->CreateAny(azrtti_typeid<MeshGroup>()));
            AZStd::any& meshGroupPointer = meshGroupData.back();
            MeshGroup* meshGroup = AZStd::any_cast<MeshGroup>(&meshGroupPointer);

            // make selection list
            meshGroup->GetSceneNodeSelectionList().RemoveSelectedNode("RootNode");
            for (const AZStd::string& node : chunkNames)
            {
                meshGroup->GetSceneNodeSelectionList().RemoveSelectedNode(
                    AZStd::string::format("RootNode.%s", node.c_str()));
            }
            meshGroup->GetSceneNodeSelectionList().AddSelectedNode(
                AZStd::string::format("RootNode.%s", chunkName.c_str()));

            // create a default material for the mesh group
            materialRuleData.emplace_back(serializeContext->CreateAny(azrtti_typeid<MaterialRule>()));
            AZStd::any& materialRulePointer = materialRuleData.back();
            MaterialRule* materialRule = AZStd::any_cast<MaterialRule>(&materialRulePointer);

            // override the deleter since the AZStd::any will clean up later on
            AZStd::shared_ptr<MaterialRule> materialRuleEntry = AZStd::shared_ptr<MaterialRule>(
                materialRule,
                [](auto)
                {
                });
            meshGroup->GetRuleContainer().AddRule(materialRuleEntry);

            // construct the asset name for the chunk's mesh group
            AZStd::string meshGroupName(filename);
            meshGroupName.append("-");
            meshGroupName.append(chunkName);
            meshGroup->OverrideId(AZ::Uuid::CreateName(meshGroupName.c_str()));
            meshGroup->SetName(AZStd::move(meshGroupName));

            // override the deleter since the AZStd::any will clean up later on
            AZStd::shared_ptr<MeshGroup> meshGroupEntry = AZStd::shared_ptr<MeshGroup>(
                meshGroup,
                [](auto)
                {
                });
            sceneManifest->AddEntry(AZStd::move(meshGroupEntry));
        }

        return sceneManifest->SaveToFile(assetinfoFilename.data());
    }

    bool BlastSliceAssetStorageComponent::WriteMaterialFile(
        AZStd::string_view materialGroupName,
        const AZStd::vector<AZStd::string>& materialNames,
        AZStd::string_view materialFilename)
    {
        AZStd::vector<AZStd::shared_ptr<IMaterial>> materialList;
        materialList.reserve(materialNames.size());

        AZ::GFxFramework::MaterialGroup group;
        for (const auto& texture : materialNames)
        {
            auto mat = AZStd::make_shared<AZ::GFxFramework::Material>();
            mat->SetName(texture);
            mat->SetTexture(AZ::GFxFramework::TextureMapType::Diffuse, "EngineAssets/Textures/white.dds");
            group.AddMaterial(mat);
        }
        group.SetMtlName(materialGroupName);
        return group.WriteMtlFile(materialFilename.data());
    }

    //
    // EditorBlastSliceAssetHandler
    //

    EditorBlastSliceAssetHandler::~EditorBlastSliceAssetHandler()
    {
        Unregister();
    }

    AZ::Data::AssetPtr EditorBlastSliceAssetHandler::CreateAsset(
        const AZ::Data::AssetId& id, const AZ::Data::AssetType& type)
    {
        if (type != GetAssetType())
        {
            AZ_Error("Blast", type == GetAssetType(), "Invalid asset type! We only handle 'BlastAsset'");
            return {};
        }

        if (!CanHandleAsset(id))
        {
            return nullptr;
        }

        return aznew BlastSliceAsset;
    }

    bool EditorBlastSliceAssetHandler::LoadAssetData(
        const AZ::Data::Asset<AZ::Data::AssetData>& asset, AZ::IO::GenericStream* stream,
        const AZ::Data::AssetFilterCB& assetLoadFilterCB)
    {
        BlastSliceAsset* blastSliceAssetData = asset.GetAs<BlastSliceAsset>();
        AZ_Error(
            "blast", blastSliceAssetData,
            "This should be a BlastSliceAsset type, as this is the only type we process!");
        AZ::SerializeContext* serializeContext = nullptr;
        AZ::ComponentApplicationBus::BroadcastResult(
            serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
        if (blastSliceAssetData && serializeContext)
        {
            AZ::ObjectStream::FilterDescriptor filter(assetLoadFilterCB);
            AZStd::unique_ptr<AZ::Entity> baseEntity(
                AZ::Utils::LoadObjectFromStream<AZ::Entity>(*stream, serializeContext, filter));
            AZ_Error("Blast", baseEntity, "Could not load slice root entity {asset id}");
            if (!baseEntity)
            {
                return false;
            }

            auto&& sliceComponent = baseEntity->FindComponent<AZ::SliceComponent>();
            AZ_Error("Blast", sliceComponent, "blast_slice entity missing SliceComponent!");
            if (sliceComponent == nullptr)
            {
                return false;
            }

            AZStd::vector<AZ::Entity*> enityList;
            sliceComponent->GetEntities(enityList);
            for (auto&& entity : enityList)
            {
                // the base element type to store Blast mesh data is the BlastSliceAssetStorageComponent
                auto&& blastSliceAssetStorage = entity->FindComponent<BlastSliceAssetStorageComponent>();
                if (blastSliceAssetStorage)
                {
                    if (blastSliceAssetStorage->GetMeshData().empty() == false)
                    {
                        blastSliceAssetData->SetMeshIdList(blastSliceAssetStorage->GetMeshData());
                        return true;
                    }
                    else if (blastSliceAssetStorage->GetMeshPathList().empty() == false)
                    {
                        AZStd::vector<AZ::Data::AssetId> meshAssetIdList;
                        meshAssetIdList.reserve(blastSliceAssetStorage->GetMeshPathList().size());

                        for (auto&& assetPath : blastSliceAssetStorage->GetMeshPathList())
                        {
                            AZ::Data::AssetId meshAssetId;
                            AZ::Data::AssetCatalogRequestBus::BroadcastResult(
                                meshAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
                                assetPath.c_str(), AZ::Data::s_invalidAssetType, false);

                            if (meshAssetId.IsValid())
                            {
                                meshAssetIdList.emplace_back(meshAssetId);
                            }
                        }
                        blastSliceAssetData->SetMeshIdList(meshAssetIdList);
                        return true;
                    }
                }

                // back up logic to load blast data for the EditorBlastMeshDataComponent
                auto&& meshDataComponent = entity->FindComponent<EditorBlastMeshDataComponent>();
                if (meshDataComponent)
                {
                    auto&& innerBlastSliceAsset = meshDataComponent->GetBlastSliceAsset();
                    if (innerBlastSliceAsset.IsReady())
                    {
                        blastSliceAssetData->SetMeshIdList(innerBlastSliceAsset.Get()->GetMeshIdList());
                        blastSliceAssetData->SetMaterialId(innerBlastSliceAsset.Get()->GetMaterialId());
                        return true;
                    }
                    else
                    {
                        auto&& meshDataList = meshDataComponent->GetMeshAssets();
                        AZStd::vector<AZ::Data::AssetId> meshAssetIdList;
                        meshAssetIdList.reserve(meshDataList.size());
                        for (auto&& meshData : meshDataList)
                        {
                            LmbrCentral::MeshAsset* meshAsset = meshData.Get();
                            if (meshAsset)
                            {
                                meshAssetIdList.push_back(meshAsset->GetId());
                            }
                        }
                        blastSliceAssetData->SetMeshIdList(meshAssetIdList);
                        return true;
                    }
                }
            }
            AZ_Error(
                "Blast", false, "blast_slice assetId:%s missing EditorBlastMeshDataComponent!",
                asset->GetId().ToString<AZStd::string>().c_str());
        }
        return false;
    }

    bool EditorBlastSliceAssetHandler::LoadAssetData(
        const AZ::Data::Asset<AZ::Data::AssetData>& asset, const char* assetPath,
        const AZ::Data::AssetFilterCB& assetLoadFilterCB)
    {
        AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
        if (fileIO)
        {
            AZ::IO::FileIOStream stream(assetPath, AZ::IO::OpenMode::ModeRead);
            if (stream.IsOpen())
            {
                return LoadAssetData(asset, &stream, assetLoadFilterCB);
            }
        }
        return false;
    }

    void EditorBlastSliceAssetHandler::DestroyAsset(AZ::Data::AssetPtr ptr)
    {
        delete ptr;
    }

    void EditorBlastSliceAssetHandler::GetHandledAssetTypes(AZStd::vector<AZ::Data::AssetType>& assetTypes)
    {
        assetTypes.push_back(azrtti_typeid<BlastSliceAsset>());
    }

    void EditorBlastSliceAssetHandler::Register()
    {
        AZ_Assert(AZ::Data::AssetManager::IsReady(), "Asset manager isn't ready!");
        AZ::Data::AssetManager::Instance().RegisterHandler(this, azrtti_typeid<BlastSliceAsset>());
        AZ::AssetTypeInfoBus::Handler::BusConnect(azrtti_typeid<BlastSliceAsset>());
    }

    void EditorBlastSliceAssetHandler::Unregister()
    {
        AZ::AssetTypeInfoBus::Handler::BusDisconnect(azrtti_typeid<BlastSliceAsset>());
        if (AZ::Data::AssetManager::IsReady())
        {
            AZ::Data::AssetManager::Instance().UnregisterHandler(this);
        }
    }

    AZ::Data::AssetType EditorBlastSliceAssetHandler::GetAssetType() const
    {
        return azrtti_typeid<BlastSliceAsset>();
    }

    const char* EditorBlastSliceAssetHandler::GetAssetTypeDisplayName() const
    {
        return "Blast Slice Asset";
    }

    const char* EditorBlastSliceAssetHandler::GetGroup() const
    {
        return "Blast";
    }

    const char* EditorBlastSliceAssetHandler::GetBrowserIcon() const
    {
        return "Editor/Icons/Components/Box.png";
    }

    void EditorBlastSliceAssetHandler::GetAssetTypeExtensions(AZStd::vector<AZStd::string>& extensions)
    {
        extensions.push_back("blast_slice");
    }

} // namespace Blast