/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensor's.
*
* 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 "RoadSurfaceDataComponent.h"

#include <AzCore/Debug/Profiler.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
#include <RoadsAndRivers/RoadsAndRiversBus.h>
#include <SurfaceData/SurfaceDataSystemRequestBus.h>
#include <SurfaceData/Utility/SurfaceDataUtility.h>

namespace RoadsAndRivers
{
    namespace Constants
    {
        static const AZ::Crc32 s_roadTagCrc = AZ::Crc32(s_roadTagName);
    }

    void RoadSurfaceDataConfig::Reflect(AZ::ReflectContext* context)
    {
        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
        if (serialize)
        {
            serialize->Class<RoadSurfaceDataConfig, AZ::ComponentConfig>()
                ->Version(1)
                ->Field("ProviderTags", &RoadSurfaceDataConfig::m_providerTags)
                ->Field("SnapToTerrain", &RoadSurfaceDataConfig::m_snapToTerrain)
                ;

            AZ::EditContext* edit = serialize->GetEditContext();
            if (edit)
            {
                edit->Class<RoadSurfaceDataConfig>(
                    "Road Surface Tag Emitter", "")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ->DataElement(0, &RoadSurfaceDataConfig::m_providerTags, "Generated Tags", "Surface tags to add to created points")
                    ->DataElement(0, &RoadSurfaceDataConfig::m_snapToTerrain, "Snap To Terrain", "Determine whether emitted surface follows the terrain or the spline geometry")
                    ;
            }
        }
    }

    void RoadSurfaceDataComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
    {
        services.push_back(AZ_CRC("SurfaceDataProviderService", 0xfe9fb95e));
    }

    void RoadSurfaceDataComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
    {
        services.push_back(AZ_CRC("SurfaceDataProviderService", 0xfe9fb95e));
    }

    void RoadSurfaceDataComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
    {
        services.push_back(AZ_CRC("RoadService", 0x8e6d7b29));
    }

    void RoadSurfaceDataComponent::Reflect(AZ::ReflectContext* context)
    {
        RoadSurfaceDataConfig::Reflect(context);

        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
        if (serialize)
        {
            serialize->Class<RoadSurfaceDataComponent, AZ::Component>()
                ->Version(0)
                ->Field("Configuration", &RoadSurfaceDataComponent::m_configuration)
                ;
        }
    }

    RoadSurfaceDataComponent::RoadSurfaceDataComponent(const RoadSurfaceDataConfig& configuration)
        : m_configuration(configuration)
    {
    }

    void RoadSurfaceDataComponent::Activate()
    {
        AddItemIfNotFound(m_configuration.m_providerTags, Constants::s_roadTagCrc);

        SurfaceData::SurfaceDataRegistryEntry registryEntry;
        registryEntry.m_entityId = GetEntityId();
        registryEntry.m_bounds = m_shapeBounds;
        registryEntry.m_tags = m_configuration.m_providerTags;

        m_providerHandle = SurfaceData::InvalidSurfaceDataRegistryHandle;

        AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
        LmbrCentral::SplineComponentNotificationBus::Handler::BusConnect(GetEntityId());
        RoadNotificationBus::Handler::BusConnect(GetEntityId());
        RoadsAndRiversGeometryNotificationBus::Handler::BusConnect(GetEntityId());

        // Update the cached shape data and bounds, then register the surface data provider
        UpdateShapeData();

        // This needs to connect after the call to UpdateShapeData() so that the handle is valid
        AZ_Assert((m_providerHandle != SurfaceData::InvalidSurfaceDataRegistryHandle), "Invalid surface data handle");
        SurfaceData::SurfaceDataProviderRequestBus::Handler::BusConnect(m_providerHandle);

        m_refresh = false;
    }

    void RoadSurfaceDataComponent::Deactivate()
    {
        m_refresh = false;
        AZ::TickBus::Handler::BusDisconnect();
        AZ::TransformNotificationBus::Handler::BusDisconnect();
        RoadsAndRiversGeometryNotificationBus::Handler::BusDisconnect();
        RoadNotificationBus::Handler::BusDisconnect();
        LmbrCentral::SplineComponentNotificationBus::Handler::BusDisconnect();

        SurfaceData::SurfaceDataProviderRequestBus::Handler::BusDisconnect();
        SurfaceData::SurfaceDataSystemRequestBus::Broadcast(&SurfaceData::SurfaceDataSystemRequestBus::Events::UnregisterSurfaceDataProvider, m_providerHandle);
        m_providerHandle = SurfaceData::InvalidSurfaceDataRegistryHandle;
    }

    bool RoadSurfaceDataComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
    {
        if (auto config = azrtti_cast<const RoadSurfaceDataConfig*>(baseConfig))
        {
            m_configuration = *config;
            return true;
        }
        return false;
    }

    bool RoadSurfaceDataComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const
    {
        if (auto config = azrtti_cast<RoadSurfaceDataConfig*>(outBaseConfig))
        {
            *config = m_configuration;
            return true;
        }
        return false;
    }

    void RoadSurfaceDataComponent::GetSurfacePoints(const AZ::Vector3& inPosition, SurfaceData::SurfacePointList& surfacePointList) const
    {
        AZStd::lock_guard<decltype(m_cacheMutex)> lock(m_cacheMutex);

        if (m_shapeBoundsIsValid)
        {
            const AZ::Vector3 rayOrigin(inPosition.GetX(), inPosition.GetY(), m_shapeBounds.GetMax().GetZ());
            const AZ::Vector3 rayDirection = -AZ::Vector3::CreateAxisZ();
            AZ::Vector3 resultPosition = rayOrigin;
            AZ::Vector3 resultNormal = AZ::Vector3::CreateAxisZ();
            if (SurfaceData::GetQuadListRayIntersection(m_shapeVertices, rayOrigin, rayDirection, m_shapeBounds.GetDepth(), resultPosition, resultNormal))
            {
                SurfaceData::SurfacePoint point;
                point.m_entityId = GetEntityId();

                if (m_configuration.m_snapToTerrain)
                {
                    const float x = inPosition.GetX();
                    const float y = inPosition.GetY();
                    auto engine = GetISystem()->GetI3DEngine();
                    bool isTerrainActive = false;
                    float terrainHeight = AzFramework::Terrain::TerrainDataRequests::GetDefaultTerrainHeight();
                    bool isPointInTerrain = false;
                    bool terrainHeightIsValid = false;
                    auto enumerationCallback = [&](AzFramework::Terrain::TerrainDataRequests* terrain) -> bool
                    {
                        isTerrainActive = true;
                        terrainHeight = terrain->GetHeightFromFloats(x, y, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &terrainHeightIsValid);
                        isPointInTerrain = SurfaceData::AabbContains2D(terrain->GetTerrainAabb(), inPosition);
                        const bool isHole = !terrainHeightIsValid;

                        if (isPointInTerrain)
                        {
                            if (m_ignoreTerrainHoles || !isHole)
                            {
                                point.m_position = AZ::Vector3(x, y, terrainHeight);
                                point.m_normal = terrain->GetNormal(inPosition);
                                AddMaxValueForMasks(point.m_masks, m_configuration.m_providerTags, 1.0f);
                                surfacePointList.push_back(point);
                            }
                        }
      
                        // Only one handler should exist.
                        return false;
                    };
                    AzFramework::Terrain::TerrainDataRequestBus::EnumerateHandlers(enumerationCallback);
                }
                else
                {
                    point.m_position = resultPosition;
                    point.m_normal = resultNormal;
                    AddMaxValueForMasks(point.m_masks, m_configuration.m_providerTags, 1.0f);
                    surfacePointList.push_back(point);
                }

            }
        }
    }

    void RoadSurfaceDataComponent::OnTransformChanged(const AZ::Transform& /*local*/, const AZ::Transform& /*world*/)
    {
        OnCompositionChanged();
    }

    void RoadSurfaceDataComponent::OnSplineChanged()
    {
        OnCompositionChanged();
    }

    void RoadSurfaceDataComponent::OnWidthChanged()
    {
        OnCompositionChanged();
    }

    void RoadSurfaceDataComponent::OnSegmentLengthChanged(float /*segmentLength*/)
    {
        OnCompositionChanged();
    }

    void RoadSurfaceDataComponent::OnIgnoreTerrainHolesChanged(bool ignoreHoles)
    {
        OnCompositionChanged();
    }

    void RoadSurfaceDataComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
    {
        if (m_refresh)
        {
            UpdateShapeData();
            m_refresh = false;
        }
        AZ::TickBus::Handler::BusDisconnect();
    }

    void RoadSurfaceDataComponent::OnCompositionChanged()
    {
        if (!m_refresh)
        {
            m_refresh = true;
            AZ::TickBus::Handler::BusConnect();
        }
    }

    void RoadSurfaceDataComponent::UpdateShapeData()
    {
        AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity);

        {
            AZStd::lock_guard<decltype(m_cacheMutex)> lock(m_cacheMutex);

            m_shapeWorldTM = AZ::Transform::CreateIdentity();
            AZ::TransformBus::EventResult(m_shapeWorldTM, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM);

            RoadsAndRivers::RoadRequestBus::EventResult(m_ignoreTerrainHoles, GetEntityId(), &RoadsAndRivers::RoadRequestBus::Events::GetIgnoreTerrainHoles);


            m_shapeVertices.clear();
            RoadsAndRivers::RoadsAndRiversGeometryRequestsBus::EventResult(m_shapeVertices, GetEntityId(), &RoadsAndRivers::RoadsAndRiversGeometryRequestsBus::Events::GetQuadVertices);

            m_shapeBounds = AZ::Aabb::CreateNull();
            for (AZ::Vector3& vertex : m_shapeVertices)
            {
                vertex = m_shapeWorldTM * vertex;
                m_shapeBounds.AddPoint(vertex);
            }
            m_shapeBoundsIsValid = m_shapeBounds.IsValid();
        }

        SurfaceData::SurfaceDataRegistryEntry registryEntry;
        registryEntry.m_entityId = GetEntityId();
        registryEntry.m_bounds = m_shapeBounds;
        registryEntry.m_tags = m_configuration.m_providerTags;
        if (m_providerHandle == SurfaceData::InvalidSurfaceDataRegistryHandle)
        {
            // First time this is called, register the provider and save off the provider handle
            AZ_Assert(m_shapeBoundsIsValid, "Road Surface Geometry isn't correctly initialized.");
            SurfaceData::SurfaceDataSystemRequestBus::BroadcastResult(m_providerHandle, &SurfaceData::SurfaceDataSystemRequestBus::Events::RegisterSurfaceDataProvider, registryEntry);
        }
        else
        {
            // On subsequent calls, just update the provider information with the saved provider handle
            SurfaceData::SurfaceDataSystemRequestBus::Broadcast(&SurfaceData::SurfaceDataSystemRequestBus::Events::UpdateSurfaceDataProvider, m_providerHandle, registryEntry);
        }
    }
}