// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "SpotLight.h"
#include "Engine/Graphics/RenderView.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Content/Assets/IESProfile.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Level/Scene/SceneRendering.h"

SpotLight::SpotLight(const SpawnParams& params)
    : LightWithShadow(params)
    , _radius(1000.0f)
    , _outerConeAngle(43.0f)
    , _innerConeAngle(10.0f)
{
    CastVolumetricShadow = false;
    ShadowsDistance = 2000.0f;
    ShadowsFadeDistance = 100.0f;
    ShadowsDepthBias = 0.5f;

    _direction = Vector3::Forward;
    _cosOuterCone = Math::Cos(_outerConeAngle * DegreesToRadians);
    _cosInnerCone = Math::Cos(_innerConeAngle * DegreesToRadians);
    _invCosConeDifference = 1.0f / (_cosInnerCone - _cosOuterCone);
    const float boundsRadius = Math::Sqrt(1.25f * _radius * _radius - _radius * _radius * _cosOuterCone);
    _sphere = BoundingSphere(GetPosition() + 0.5f * GetDirection() * _radius, boundsRadius);
    BoundingBox::FromSphere(_sphere, _box);
}

float SpotLight::ComputeBrightness() const
{
    float result = Brightness;

    if (IESTexture)
    {
        if (UseIESBrightness)
        {
            result = IESTexture->Brightness * IESBrightnessScale;
        }

        result *= IESTexture->TextureMultiplier;
    }

    //if (UseInverseSquaredFalloff)
    //	result *= 16.0f;

    return result;
}

float SpotLight::GetScaledRadius() const
{
    return _radius * _transform.Scale.MaxValue();
}

void SpotLight::SetRadius(float value)
{
    value = Math::Max(0.0f, value);
    if (Math::NearEqual(value, _radius))
        return;

    _radius = value;
    UpdateBounds();
}

void SpotLight::SetOuterConeAngle(float value)
{
    // Clamp value
    value = Math::Clamp(value, 0.0f, 89.0f);

    // Check if value will change
    if (!Math::NearEqual(value, _outerConeAngle))
    {
        // Change values
        _innerConeAngle = Math::Min(_innerConeAngle, value - ZeroTolerance);
        _outerConeAngle = value;

        UpdateBounds();
    }
}

void SpotLight::SetInnerConeAngle(float value)
{
    // Clamp value
    value = Math::Clamp(value, 0.0f, 89.0f);

    // Check if value will change
    if (!Math::NearEqual(value, _innerConeAngle))
    {
        // Change values
        _innerConeAngle = value;
        _outerConeAngle = Math::Max(_outerConeAngle + ZeroTolerance, value);

        UpdateBounds();
    }
}

void SpotLight::UpdateBounds()
{
    // Cache light direction
    Vector3::Transform(Vector3::Forward, _transform.Orientation, _direction);
    _direction.Normalize();

    // Cache cone angles
    _cosOuterCone = Math::Cos(_outerConeAngle * DegreesToRadians);
    _cosInnerCone = Math::Cos(_innerConeAngle * DegreesToRadians);
    _invCosConeDifference = 1.0f / Math::Max(_cosInnerCone - _cosOuterCone, 0.0001f);

    // Cache bounds
    // Note: we use the law of cosines to find the distance to the furthest edge of the spotlight cone from a position that is halfway down the spotlight direction
    const float radius = GetScaledRadius();
    const float boundsRadius = Math::Sqrt(1.25f * radius * radius - radius * radius * _cosOuterCone);
    _sphere = BoundingSphere(GetPosition() + 0.5f * GetDirection() * radius, boundsRadius);
    BoundingBox::FromSphere(_sphere, _box);

    if (_sceneRenderingKey != -1)
        GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::Bounds);
}

void SpotLight::OnTransformChanged()
{
    // Base
    LightWithShadow::OnTransformChanged();

    UpdateBounds();
}

void SpotLight::Draw(RenderContext& renderContext)
{
    float brightness = ComputeBrightness();
    AdjustBrightness(renderContext.View, brightness);
    Float3 position;
    const float radius = GetScaledRadius();
    const float outerConeAngle = GetOuterConeAngle();
    if (EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::SpotLights)
        && EnumHasAnyFlags(renderContext.View.Pass, DrawPass::GBuffer)
        && brightness > ZeroTolerance
        && radius > ZeroTolerance
        && outerConeAngle > ZeroTolerance
        && CheckViewDistance(renderContext.View.Position, renderContext.View.Origin, position, brightness))
    {
        RenderSpotLightData data;
        data.Position = position;
        data.MinRoughness = MinRoughness;
        data.ShadowsDistance = ShadowsDistance;
        data.Color = Color.ToFloat3() * (Color.A * brightness);
        data.ShadowsStrength = ShadowsStrength;
        data.Direction = _direction;
        data.ShadowsFadeDistance = ShadowsFadeDistance;
        data.ShadowsNormalOffsetScale = ShadowsNormalOffsetScale;
        data.ShadowsDepthBias = ShadowsDepthBias;
        data.ShadowsSharpness = ShadowsSharpness;
        data.VolumetricScatteringIntensity = VolumetricScatteringIntensity;
        data.CastVolumetricShadow = CastVolumetricShadow;
        data.ShadowsUpdateRate = ShadowsUpdateRate;
        data.ShadowsUpdateRateAtDistance = ShadowsUpdateRateAtDistance;
        data.ShadowFrame = _invalidateShadowFrame;
        data.ShadowsResolution = (int32)ShadowsResolution;
        data.ShadowsMode = ShadowsMode;
        data.Radius = radius;
        data.FallOffExponent = FallOffExponent;
        data.UseInverseSquaredFalloff = UseInverseSquaredFalloff;
        data.SourceRadius = SourceRadius;
        data.CosOuterCone = _cosOuterCone;
        data.InvCosConeDifference = _invCosConeDifference;
        data.ContactShadowsLength = ContactShadowsLength;
        data.IndirectLightingIntensity = IndirectLightingIntensity;
        data.IESTexture = IESTexture ? IESTexture->GetTexture() : nullptr;
        Float3::Transform(Float3::Up, GetOrientation(), data.UpVector);
        data.OuterConeAngle = outerConeAngle;
        data.StaticFlags = GetStaticFlags();
        data.ID = GetID();
        data.ScreenSize = Math::Min(1.0f, Math::Sqrt(RenderTools::ComputeBoundsScreenRadiusSquared(position, (float)_sphere.Radius, renderContext.View)));
        renderContext.List->SpotLights.Add(data);
    }
}

#if USE_EDITOR

#include "Engine/Debug/DebugDraw.h"

void SpotLight::OnDebugDraw()
{
    if (SourceRadius > ZeroTolerance)
    {
        // Draw source tube
        DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(GetPosition(), SourceRadius), Color::Orange, 0, true);
    }

    // Base
    LightWithShadow::OnDebugDraw();
}

void SpotLight::OnDebugDrawSelected()
{
    const auto color = Color::Yellow;
    Vector3 right = _transform.GetRight();
    Vector3 up = _transform.GetUp();
    Vector3 forward = GetDirection();
    float radius = GetScaledRadius();
    float discRadius = radius * Math::Tan(_outerConeAngle * DegreesToRadians);
    float falloffDiscRadius = radius * Math::Tan(_innerConeAngle * DegreesToRadians);
    Vector3 position = GetPosition();

    DEBUG_DRAW_LINE(position, position + forward * radius + up * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - up * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius + right * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - right * discRadius, color, 0, true);

    DEBUG_DRAW_LINE(position, position + forward * radius + up * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - up * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius + right * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - right * falloffDiscRadius, color * 0.6f, 0, true);

    DEBUG_DRAW_CIRCLE(position + forward * radius, forward, discRadius, color, 0, true);
    DEBUG_DRAW_CIRCLE(position + forward * radius, forward, falloffDiscRadius, color * 0.6f, 0, true);

    // Base
    LightWithShadow::OnDebugDrawSelected();
}

void SpotLight::DrawLightsDebug(RenderView& view)
{
    const BoundingSphere sphere(_sphere.Center - view.Origin, _sphere.Radius);
    if (!view.CullingFrustum.Intersects(sphere) || !EnumHasAnyFlags(view.Flags, ViewFlags::SpotLights))
        return;
    
    const auto color = Color::Yellow;
    Vector3 right = _transform.GetRight();
    Vector3 up = _transform.GetUp();
    Vector3 forward = GetDirection();
    float radius = GetScaledRadius();
    float discRadius = radius * Math::Tan(_outerConeAngle * DegreesToRadians);
    float falloffDiscRadius = radius * Math::Tan(_innerConeAngle * DegreesToRadians);
    Vector3 position = GetPosition();

    DEBUG_DRAW_LINE(position, position + forward * radius + up * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - up * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius + right * discRadius, color, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - right * discRadius, color, 0, true);

    DEBUG_DRAW_LINE(position, position + forward * radius + up * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - up * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius + right * falloffDiscRadius, color * 0.6f, 0, true);
    DEBUG_DRAW_LINE(position, position + forward * radius - right * falloffDiscRadius, color * 0.6f, 0, true);

    DEBUG_DRAW_CIRCLE(position + forward * radius, forward, discRadius, color, 0, true);
    DEBUG_DRAW_CIRCLE(position + forward * radius, forward, falloffDiscRadius, color * 0.6f, 0, true);
}
#endif

void SpotLight::Serialize(SerializeStream& stream, const void* otherObj)
{
    // Base
    LightWithShadow::Serialize(stream, otherObj);

    SERIALIZE_GET_OTHER_OBJ(SpotLight);

    SERIALIZE_MEMBER(Radius, _radius);
    SERIALIZE_MEMBER(OuterAngle, _outerConeAngle);
    SERIALIZE_MEMBER(InnerAngle, _innerConeAngle);
    SERIALIZE(IESTexture);
    SERIALIZE(SourceRadius);
    SERIALIZE(FallOffExponent);
    SERIALIZE(UseInverseSquaredFalloff);
    SERIALIZE(UseIESBrightness);
    SERIALIZE(IESBrightnessScale);
}

void SpotLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
    // Base
    LightWithShadow::Deserialize(stream, modifier);

    DESERIALIZE_MEMBER(Radius, _radius);
    DESERIALIZE_MEMBER(OuterAngle, _outerConeAngle);
    DESERIALIZE_MEMBER(InnerAngle, _innerConeAngle);
    DESERIALIZE(IESTexture);
    DESERIALIZE(SourceRadius);
    DESERIALIZE(FallOffExponent);
    DESERIALIZE(UseInverseSquaredFalloff);
    DESERIALIZE(UseIESBrightness);
    DESERIALIZE(IESBrightnessScale);
}

bool SpotLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
{
    return CollisionsHelper::RayIntersectsSphere(ray, _sphere, distance, normal);
}
