/*
* 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 "LmbrCentral_precompiled.h"
#include "EditorGeomCacheComponent.h"

#include <AzCore/RTTI/BehaviorContext.h>
#include <LmbrCentral/Rendering/MeshComponentBus.h>

namespace LmbrCentral
{
    void EditorGeometryCacheCommon::Reflect(AZ::ReflectContext* context)
    {
        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serializeContext->Class<EditorGeometryCacheCommon, GeometryCacheCommon>()
                ->Version(1)
                ;

            if (AZ::EditContext* editContext = serializeContext->GetEditContext())
            {
                editContext->Class<EditorGeometryCacheCommon>("Editor Geom Cache Common Configuration", "")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ;

                editContext->Class<GeometryCacheCommon>("Geom Cache Common Configuration", "")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_visible, "Visible", "Should the GeomCache be rendered.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnRenderOptionsChanged)

                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &GeometryCacheCommon::m_minSpec, "Min Spec", "The minimum graphics spec where this GeomCache will be rendered.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnRenderOptionsChanged)
                    ->EnumAttribute(EngineSpec::Never, "Never")
                    ->EnumAttribute(EngineSpec::VeryHigh, "Very high")
                    ->EnumAttribute(EngineSpec::High, "High")
                    ->EnumAttribute(EngineSpec::Medium, "Medium")
                    ->EnumAttribute(EngineSpec::Low, "Low")

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_geomCacheAsset, "Geom Cache", "The Alembic Geometry Cache asset.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnGeomCacheAssetChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_materialOverrideAsset, "Material Override", "Optional material override asset.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnMaterialOverrideChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_loop, "Loop", "Should the animation loop.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnLoopChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_playOnStart, "Play on Start", "Should the alembic animation play when the component activates.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnPlayOnStartChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_startTime, "Start Time", "The time point that the animation should start at.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnStartTimeChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_streamInDistance, "Stream In Distance", "How close does the viewer need to be for the GeomCache to begin streaming into memory.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnStreamInDistanceChanged)

                    ->ClassElement(AZ::Edit::ClassElements::Group, "Stand-in Settings")
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, false)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_firstFrameStandin, "First Frame Stand-in", "The entity that should stand in for this GeomCache before playback begins.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnFirstFrameStandinChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_lastFrameStandin, "Last Frame Stand-in", "The entity that should stand in for this GeomCache after playback has ended.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnLastFrameStandinChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_standin, "Stand-in", "The entity that should stand in for this GeomCache when the viewer is past the Stand-in Distance.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnStandinChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_standinDistance, "Stand-in Distance", "How close does the viewer need to be before the GeomCache replaces the Stand-in.")

                    ->ClassElement(AZ::Edit::ClassElements::Group, "Options")
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, false)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_maxViewDistance, "Max View Distance", "That maximum distance that this GeomCache can be viewed from.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnMaxViewDistanceChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_viewDistanceMultiplier, "View Distance Multiplier", "Multiplied to the Max View Distance to get the final max view distance.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnViewDistanceMultiplierChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Slider, &GeometryCacheCommon::m_lodDistanceRatio, "LOD Distance Ratio", "Controls LOD ratio over distance.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnLODDistanceRatioChanged)
                    ->Attribute(AZ::Edit::Attributes::Min, 0)
                    ->Attribute(AZ::Edit::Attributes::Max, 255)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_castShadows, "Cast Shadows", "Should the GeomCache cast shadows.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnRenderOptionsChanged)

                    ->DataElement(AZ::Edit::UIHandlers::Default, &GeometryCacheCommon::m_useVisAreas, "Use Vis Areas", "Should the GeomCache be affected by VisAreas.")
                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &GeometryCacheCommon::OnRenderOptionsChanged)
                    ;
            }
        }
    }

    void EditorGeometryCacheCommon::Activate()
    {
        GeometryCacheCommon::Activate();

        //Store the serialized results here so that we can properly react to stand-in changes after level load
        m_prevFirstFrameStandin = m_firstFrameStandin;
        m_prevLastFrameStandin = m_lastFrameStandin;
        m_prevStandin = m_standin;
    }

    void EditorGeometryCacheCommon::Deactivate()
    {
        GeometryCacheCommon::Deactivate();
    }
    
    void EditorGeometryCacheCommon::OnFirstFrameStandinChanged()
    {
        //We only need to remove the prev stand-in from the geom cache's hierarchy if it's not used anywhere else
        bool removePrevFirstFrameStandinTransform = false;
        if (m_prevFirstFrameStandin != m_prevStandin && m_prevFirstFrameStandin != m_prevLastFrameStandin)
        {
            removePrevFirstFrameStandinTransform = true;
        }
        HandleStandinChanged(m_prevFirstFrameStandin, m_firstFrameStandin, removePrevFirstFrameStandinTransform);

        m_prevFirstFrameStandin = m_firstFrameStandin;
    }

    void EditorGeometryCacheCommon::OnLastFrameStandinChanged()
    {
        //We only need to remove the prev stand-in from the geom cache's hierarchy if it's not used anywhere else
        bool removePrevLastFrameStandinTransform = false;
        if (m_prevLastFrameStandin != m_prevStandin && m_prevLastFrameStandin != m_prevFirstFrameStandin)
        {
            removePrevLastFrameStandinTransform = true;
        }
        HandleStandinChanged(m_prevLastFrameStandin, m_lastFrameStandin, removePrevLastFrameStandinTransform);

        m_prevLastFrameStandin = m_lastFrameStandin;
    }

    void EditorGeometryCacheCommon::OnStandinChanged()
    {
        //We only need to remove the prev stand-in from the geom cache's hierarchy if it's not used anywhere else
        bool removePrevStandinTransform = false;
        if (m_prevStandin != m_prevLastFrameStandin && m_prevStandin != m_prevFirstFrameStandin)
        {
            removePrevStandinTransform = true;
        }
        HandleStandinChanged(m_prevStandin, m_standin, removePrevStandinTransform);
        m_prevStandin = m_standin;
    }

    void EditorGeometryCacheCommon::HandleStandinChanged(const AZ::EntityId& prevStandinId, const AZ::EntityId& newStandinId, bool evictPrevStandinTransform)
    {
        //Undo modifications we've made to the stand-in entity
        if (prevStandinId.IsValid() && evictPrevStandinTransform)
        {
            //If this stand-in isn't currently active it may be set to invisible. If it's not going to be a stand-in anymore we want it to be visible
            MeshComponentRequestBus::Event(prevStandinId, &MeshComponentRequestBus::Events::SetVisibility, true);
            //If it's not going to be a stand-in anymore it also doesn't need to be parented. User can re-parent if needed
            AZ::TransformBus::Event(prevStandinId, &AZ::TransformBus::Events::SetParent, AZ::EntityId()); 
        }
        
        //Parent new stand-in to geometry cache transform
        AZ::TransformBus::Event(newStandinId, &AZ::TransformBus::Events::SetParent, m_entityId);

        //Standin will be made visible if necessary in GeomCacheCommon::OnTick
        MeshComponentRequestBus::Event(m_standin, &MeshComponentRequestBus::Events::SetVisibility, false);
        MeshComponentRequestBus::Event(m_firstFrameStandin, &MeshComponentRequestBus::Events::SetVisibility, false);
        MeshComponentRequestBus::Event(m_lastFrameStandin, &MeshComponentRequestBus::Events::SetVisibility, false);

        //Force stand-in logic to refresh
        m_currentStandinType = StandinType::None;
    }

    void EditorGeometryCacheCommon::SetMaterial(_smart_ptr<IMaterial> material)
    {
        GeometryCacheCommon::SetMaterial(material);

        AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast(
            &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay,
            AzToolsFramework::Refresh_AttributesAndValues);
    }

    void EditorGeometryCacheComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provides)
    {
        provides.push_back(AZ_CRC("GeomCacheService", 0x3d2bc48c));
    }

    void EditorGeometryCacheComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& requires)
    {
        requires.push_back(AZ_CRC("TransformService", 0x8ee22c50));
    }

    void EditorGeometryCacheComponent::Reflect(AZ::ReflectContext* context)
    {
        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serializeContext->Class<EditorGeometryCacheComponent, AzToolsFramework::Components::EditorComponentBase>()
                ->Version(1)
                ->Field("Common", &EditorGeometryCacheComponent::m_common)
                ;

            if (AZ::EditContext* editContext = serializeContext->GetEditContext())
            {
                editContext->Class<EditorGeometryCacheComponent>("Geometry Cache", "Controls playback of baked vertex animations.")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::Category, "Rendering")
                    ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/GeometryCache.svg")
                    ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/GeometryCache.png")
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c))
                    ->Attribute(AZ::Edit::Attributes::HelpPageURL, "http://docs.aws.amazon.com/console/lumberyard/userguide/geom-cache-component")
                    ->DataElement(AZ::Edit::UIHandlers::Default, &EditorGeometryCacheComponent::m_common, "Common", "No Description")
                    ;
            }
        }

        if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
        {
            behaviorContext->Class<EditorGeometryCacheComponent>()
                ->RequestBus("GeometryCacheComponentRequestBus")
                ->RequestBus("GeometryCacheComponentNotificationBus")
                ;
        }

        EditorGeometryCacheCommon::Reflect(context);
    }

    void EditorGeometryCacheComponent::Init()
    {
        m_common.Init(GetEntityId());
    }

    void EditorGeometryCacheComponent::Activate()
    {
        m_common.Activate();

        AzToolsFramework::Components::EditorComponentBase::Activate();

        AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
        AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());

        //Get initial world transform for debug rendering
        m_currentWorldTransform = AZ::Transform::CreateIdentity();
        AZ::TransformBus::EventResult(m_currentWorldTransform, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM);
    }

    void EditorGeometryCacheComponent::Deactivate()
    {
        AzToolsFramework::Components::EditorComponentBase::Deactivate();

        AZ::TransformNotificationBus::Handler::BusDisconnect(GetEntityId());
        AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect(GetEntityId());

        m_common.Deactivate();
    }

    void EditorGeometryCacheComponent::SetPrimaryAsset(const AZ::Data::AssetId& assetId)
    {
        m_common.SetGeomCacheAsset(assetId);
    }

    void EditorGeometryCacheComponent::BuildGameEntity(AZ::Entity* gameEntity)
    {
        gameEntity->CreateComponent<GeometryCacheComponent>(&m_common);
    }

    void EditorGeometryCacheComponent::DisplayEntityViewport(
        const AzFramework::ViewportInfo& viewportInfo,
        AzFramework::DebugDisplayRequests& debugDisplay)
    {
        // Don't draw extra visualization unless selected.
        if (!IsSelected())
        {
            return;
        }

        debugDisplay.PushMatrix(m_currentWorldTransform);

        debugDisplay.SetColor(AZ::Vector4(1.0f, 1.0f, 1.0f, 1.0f));

        debugDisplay.DrawWireSphere(AZ::Vector3::CreateZero(), m_common.GetStandInDistance());
        debugDisplay.DrawWireSphere(AZ::Vector3::CreateZero(), m_common.GetStreamInDistance());

        debugDisplay.PopMatrix();
    }

    void EditorGeometryCacheComponent::OnTransformChanged(const AZ::Transform& /*local*/, const AZ::Transform& world)
    {
        m_currentWorldTransform = world;
    }

} //namespace LmbrCentral