/*
 * 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 <Components/BlastSystemComponent.h>

#include <Asset/BlastAsset.h>
#include <Asset/BlastAssetHandler.h>
#include <AzCore/Jobs/JobCompletion.h>
#include <AzCore/Jobs/JobFunction.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/SourceControl/SourceControlAPI.h>
#include <Blast/BlastActorData.h>
#include <Blast/BlastDebug.h>
#include <Blast/BlastFamilyComponentBus.h>
#include <Blast/BlastMaterial.h>
#include <IConsole.h>
#include <NvBlastExtPxSerialization.h>
#include <NvBlastExtTkSerialization.h>
#include <NvBlastPxCallbacks.h>
#include <NvBlastTkJoint.h>
#include <PhysX/SystemComponentBus.h>
#include <PxPhysics.h>
#include <task/PxCpuDispatcher.h>
#include <platform_impl.h>
#ifdef BLAST_EDITOR
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#endif

namespace Blast
{
    static const char* const DefaultConfigurationPath = "default.blastconfiguration";

    void BlastSystemComponent::Reflect(AZ::ReflectContext* context)
    {
        BlastGlobalConfiguration::Reflect(context);

        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serialize->Class<BlastSystemComponent, AZ::Component>()->Version(1);

            if (AZ::EditContext* ec = serialize->GetEditContext())
            {
                ec->Class<BlastSystemComponent>("Blast", "Adds support for the NVIDIA Blast destruction system")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
            }
        }
    }

    void BlastGlobalConfiguration::Reflect(AZ::ReflectContext* context)
    {
        BlastMaterialLibraryAsset::Reflect(context);
        BlastMaterialConfiguration::Reflect(context);

        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serialize->Class<BlastGlobalConfiguration>()
                ->Field("BlastMaterialLibrary", &BlastGlobalConfiguration::m_materialLibrary)
                ->Field("StressSolverIterations", &BlastGlobalConfiguration::m_stressSolverIterations)
                ->Version(1);

            if (AZ::EditContext* ec = serialize->GetEditContext())
            {
                ec->Class<BlastGlobalConfiguration>(
                      "Blast global configuration", "Set of configuration that are applied globally within Blast gem.")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ->DataElement(
                        AZ::Edit::UIHandlers::Default, &BlastGlobalConfiguration::m_materialLibrary,
                        "Blast material library", "Material library asset to be used globally.")
                    ->DataElement(
                        AZ::Edit::UIHandlers::Default, &BlastGlobalConfiguration::m_stressSolverIterations,
                        "Stress solver iterations",
                        "Number of iterations stress solver on each family runs for each tick.")
                    ->Attribute(AZ::Edit::Attributes::Min, 0)
                    ->Attribute(AZ::Edit::Attributes::Max, 50000);
            }
        }
    }

    void BlastSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
    {
        provided.push_back(AZ_CRC_CE("BlastService"));
    }

    void BlastSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
    {
        incompatible.push_back(AZ_CRC_CE("BlastService"));
    }

    void BlastSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
    {
        required.push_back(AZ_CRC_CE("PhysXService"));
    }

    void BlastSystemComponent::Init()
    {
        // Route NvBlast allocations through the AZ system allocator
        NvBlastGlobalSetAllocatorCallback(&m_blastAllocatorCallback);
        m_debugRenderMode = DebugRenderDisabled;
    }

    void BlastSystemComponent::Activate()
    {
        AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::System);
        auto blastAssetHandler = aznew BlastAssetHandler();
        blastAssetHandler->Register();
        m_assetHandlers.emplace_back(blastAssetHandler);

        auto materialAsset = aznew AzFramework::GenericAssetHandler<BlastMaterialLibraryAsset>(
            "Blast Material", "Blast", "blastmaterial");
        materialAsset->Register();
        m_assetHandlers.emplace_back(materialAsset);

        // Add asset types and extensions to AssetCatalog. Uses "AssetCatalogService".
        auto assetCatalog = AZ::Data::AssetCatalogRequestBus::FindFirstHandler();
        if (assetCatalog)
        {
            assetCatalog->EnableCatalogForAsset(AZ::AzTypeInfo<BlastAsset>::Uuid());
            assetCatalog->AddExtension("blast");
        }

        m_registered = false;

        BlastSystemRequestBus::Handler::BusConnect();
        AZ::TickBus::Handler::BusConnect();
        CrySystemEventBus::Handler::BusConnect();

        InitPhysics();
    }

    void BlastSystemComponent::Deactivate()
    {
        AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::System);
        CrySystemEventBus::Handler::BusDisconnect();
        AZ::TickBus::Handler::BusDisconnect();
        BlastSystemRequestBus::Handler::BusDisconnect();

        SaveConfiguration();
        DeactivatePhysics();

        m_assetHandlers.clear();
    };

    void BlastSystemComponent::InitPhysics()
    {
        // Create blast singletons
        m_tkFramework.reset(NvBlastTkFrameworkCreate());
        physx::PxCpuDispatcher* dispatcher;
        PhysX::SystemRequestsBus::BroadcastResult(dispatcher, &PhysX::SystemRequests::GetCpuDispatcher);

        physx::PxCooking* cooking = nullptr;
        PhysX::SystemRequestsBus::BroadcastResult(cooking, &PhysX::SystemRequests::GetCooking);

        m_defaultTaskManager.reset(physx::PxTaskManager::createTaskManager(NvBlastGetPxErrorCallback(), dispatcher));
        m_extSerialization.reset(NvBlastExtSerializationCreate());

        if (m_extSerialization != nullptr)
        {
            NvBlastExtPxSerializerLoadSet(*m_tkFramework, PxGetPhysics(), *cooking, *m_extSerialization);
            NvBlastExtTkSerializerLoadSet(*m_tkFramework, *m_extSerialization);
        }

        NvBlastProfilerSetCallback(&m_blastProfilerCallback);
        NvBlastProfilerSetDetail(Nv::Blast::ProfilerDetail::HIGH);
    }

    void BlastSystemComponent::DeactivatePhysics()
    {
        m_extSerialization = nullptr;
        m_defaultTaskManager = nullptr;
        m_tkFramework = nullptr;
    }

    void BlastSystemComponent::OnTick(float deltaTime, AZ::ScriptTimePoint time)
    {
        AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Physics);

        AZ::JobCompletion jobCompletion;

        BlastFamilyComponentRequestBus::EnumerateHandlers(
            [&jobCompletion](BlastFamilyComponentRequests* handler)
            {
                auto stressDamageJob = AZ::CreateJobFunction(
                    [handler]() -> void
                    {
                        handler->ApplyStressDamage();
                    },
                    true);
                stressDamageJob->SetDependent(&jobCompletion);
                stressDamageJob->Start();

                return true;
            });

        BlastFamilyComponentRequestBus::EnumerateHandlers(
            [](BlastFamilyComponentRequests* handler)
            {
                handler->SyncMeshes();

                return true;
            });

        jobCompletion.StartAndWaitForCompletion();

        // Run groups
        for (auto i = 0; i < m_groups.size(); ++i)
        {
            if (m_groups[i].m_tkGroup->getActorCount() == 0)
            {
                AZStd::swap(m_groups[i], m_groups.back());
                m_groups.pop_back();
            }
        }

        for (auto& group : m_groups)
        {
            AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Physics, "ExtGroupTaskManager::process");
            group.m_extGroupTaskManager->process();
        }
        for (auto& group : m_groups)
        {
            AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Physics, "ExtGroupTaskManager::wait");
            group.m_extGroupTaskManager->wait();
        }

        // Clean up damage descriptions and program params now that groups have run.
        {
            AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Physics, "BlastSystemComponent::OnTick::Cleanup");
            m_radialDamageDescs.clear();
            m_capsuleDamageDescs.clear();
            m_shearDamageDescs.clear();
            m_triangleDamageDescs.clear();
            m_impactDamageDescs.clear();
            m_programParams.clear();
        }

        if (gEnv && m_debugRenderMode)
        {
            AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Physics, "BlastSystemComponent::OnTick::DebugRender");
            DebugRenderBuffer buffer;
            BlastFamilyComponentRequestBus::Broadcast(
                &BlastFamilyComponentRequests::FillDebugRenderBuffer, buffer, m_debugRenderMode);
            for (DebugLine& line : buffer.m_lines)
            {
                Vec3 p0(line.m_p0.GetX(), line.m_p0.GetY(), line.m_p0.GetZ());
                Vec3 p1(line.m_p1.GetX(), line.m_p1.GetY(), line.m_p1.GetZ());

                ColorF color(line.m_color.GetR(), line.m_color.GetG(), line.m_color.GetB(), line.m_color.GetA());

                gEnv->pRenderer->GetIRenderAuxGeom()->DrawLine(p0, color, p1, color);
            }
        }
    }

    void BlastSystemComponent::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
    {
        if (m_configuration.m_materialLibrary == asset)
        {
            m_configuration.m_materialLibrary = asset;
        }
    }

    void BlastSystemComponent::LoadConfiguration()
    {
        BlastGlobalConfiguration globalConfiguration;
        bool loaded = AZ::Utils::LoadObjectFromFileInPlace<BlastGlobalConfiguration>(
            DefaultConfigurationPath, globalConfiguration);
        AZ_Warning("Blast", loaded, "Failed to load Blast configuration, initializing with default configs.");

        SetGlobalConfiguration(globalConfiguration);
        SaveConfiguration();
    }

    void BlastSystemComponent::SaveConfiguration()
    {
        auto assetRoot = AZ::IO::FileIOBase::GetInstance()->GetAlias("@devassets@");

        if (!assetRoot)
        {
            return;
        }

        AZStd::string fullPath;
        AzFramework::StringFunc::Path::Join(assetRoot, DefaultConfigurationPath, fullPath);

        bool saved = AZ::Utils::SaveObjectToFile<BlastGlobalConfiguration>(
            fullPath.c_str(), AZ::DataStream::ST_XML, &m_configuration);
        AZ_Warning("BlastSystemComponent", saved, "Failed to save Blast configuration");
    }

    void BlastSystemComponent::CheckoutConfiguration()
    {
        const auto assetRoot = AZ::IO::FileIOBase::GetInstance()->GetAlias("@devassets@");

        AZStd::string fullPath;
        AzFramework::StringFunc::Path::Join(assetRoot, DefaultConfigurationPath, fullPath);

        AzToolsFramework::SourceControlCommandBus::Broadcast(
            &AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, fullPath.c_str(), true,
            [](bool /*success*/, const AzToolsFramework::SourceControlFileInfo& info)
            {
                // File is checked out
            });
    }

    void BlastSystemComponent::OnCrySystemInitialized(ISystem&, const SSystemInitParams&)
    {
        LoadConfiguration();
        RegisterCommands();
    }

    void BlastSystemComponent::OnCryEditorInitialized()
    {
        CheckoutConfiguration();
    }

    Nv::Blast::TkFramework* BlastSystemComponent::GetTkFramework() const
    {
        return m_tkFramework.get();
    }

    Nv::Blast::ExtSerialization* BlastSystemComponent::GetExtSerialization() const
    {
        return m_extSerialization.get();
    }

    Nv::Blast::TkGroup* BlastSystemComponent::GetTkGroup()
    {
        m_groups.emplace_back();
        Nv::Blast::TkGroupDesc groupDesc;
        groupDesc.workerCount = m_defaultTaskManager->getCpuDispatcher()->getWorkerCount();
        m_groups.back().m_tkGroup.reset(m_tkFramework->createGroup(groupDesc));
        m_groups.back().m_extGroupTaskManager.reset(
            Nv::Blast::ExtGroupTaskManager::create(*m_defaultTaskManager, m_groups.back().m_tkGroup.get()));

        return m_groups.back().m_tkGroup.get();
    }

    void BlastSystemComponent::AddDamageDesc(AZStd::unique_ptr<NvBlastExtRadialDamageDesc> desc)
    {
        m_radialDamageDescs.push_back(AZStd::move(desc));
    }

    void BlastSystemComponent::AddDamageDesc(AZStd::unique_ptr<NvBlastExtCapsuleRadialDamageDesc> desc)
    {
        m_capsuleDamageDescs.push_back(AZStd::move(desc));
    }

    void BlastSystemComponent::AddDamageDesc(AZStd::unique_ptr<NvBlastExtShearDamageDesc> desc)
    {
        m_shearDamageDescs.push_back(AZStd::move(desc));
    }

    void BlastSystemComponent::AddDamageDesc(AZStd::unique_ptr<NvBlastExtTriangleIntersectionDamageDesc> desc)
    {
        m_triangleDamageDescs.push_back(AZStd::move(desc));
    }

    void BlastSystemComponent::AddDamageDesc(AZStd::unique_ptr<NvBlastExtImpactSpreadDamageDesc> desc)
    {
        m_impactDamageDescs.push_back(AZStd::move(desc));
    }

    void BlastSystemComponent::AddProgramParams(AZStd::unique_ptr<NvBlastExtProgramParams> program)
    {
        m_programParams.emplace_back(AZStd::move(program));
    }

    const BlastGlobalConfiguration& BlastSystemComponent::GetGlobalConfiguration() const
    {
        return m_configuration;
    }

    void BlastSystemComponent::SetGlobalConfiguration(const BlastGlobalConfiguration& globalConfiguration)
    {
        m_configuration = globalConfiguration;
        SaveConfiguration();

        {
            AZ::Data::Asset<Blast::BlastMaterialLibraryAsset>& materialLibrary = m_configuration.m_materialLibrary;

            if (!materialLibrary.GetId().IsValid())
            {
                AZ_Warning("Blast", false, "LoadDefaultMaterialLibrary: Default Material Library asset ID is invalid.");
                return;
            }

            const bool queueLoadData = true;
            const AZ::Data::AssetFilterCB assetLoadFilterCB = nullptr;
            const bool loadBlocking = true;
            materialLibrary = AZ::Data::AssetManager::Instance().GetAsset<Blast::BlastMaterialLibraryAsset>(
                materialLibrary.GetId(), queueLoadData, assetLoadFilterCB, loadBlocking);

            // Listen for material library asset modification events
            if (!AZ::Data::AssetBus::MultiHandler::BusIsConnectedId(materialLibrary.GetId()))
            {
                AZ::Data::AssetBus::MultiHandler::BusDisconnect();
                AZ::Data::AssetBus::MultiHandler::BusConnect(materialLibrary.GetId());
            }

            AZ_Warning(
                "Blast", (materialLibrary.GetData() != nullptr),
                "LoadDefaultMaterialLibrary: Default Material Library asset data is invalid.");
        }
#ifdef BLAST_EDITOR
        AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast(
            &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay, AzToolsFramework::Refresh_EntireTree);
#endif
    }

    void BlastSystemComponent::AZBlastProfilerCallback::zoneStart(const char* eventName)
    {
        AZ_PROFILE_EVENT_BEGIN(AZ::Debug::ProfileCategory::Physics, eventName);
    }

    void BlastSystemComponent::AZBlastProfilerCallback::zoneEnd()
    {
        AZ_PROFILE_EVENT_END(AZ::Debug::ProfileCategory::Physics);
    }

    static void CmdToggleBlastDebugVisualization(IConsoleCmdArgs* args)
    {
        using namespace CryStringUtils;

        const int argumentCount = args->GetArgCount();

        if (argumentCount == 2)
        {
            const auto userPreference = static_cast<DebugRenderMode>(strtol(args->GetArg(1), nullptr, 10));
            BlastSystemRequestBus::Broadcast(&BlastSystemRequests::SetDebugRenderMode, userPreference);
        }
        else
        {
            AZ_Warning(
                "Blast", false,
                "Invalid blast_debug Arguments. Please use blast_debug 1 to enable, blast_debug 0 to disable.");
        }
    }

    void BlastSystemComponent::RegisterCommands()
    {
        if (m_registered)
        {
            return;
        }

        if (gEnv)
        {
            IConsole* console = gEnv->pSystem->GetIConsole();
            if (console)
            {
                console->AddCommand("blast_debug", CmdToggleBlastDebugVisualization);
            }

            m_registered = true;
        }
    }

    void BlastSystemComponent::SetDebugRenderMode(DebugRenderMode debugRenderMode)
    {
        m_debugRenderMode = debugRenderMode;
    }
} // namespace Blast
