OMRVisualFoundation.ColorGrading.cs
using Sandbox;
using Sandbox.Rendering;
using System;

/// <summary>
/// Phase 8F3F5 / Hotfix 1: color grading for OMR's two-camera presentation stack.
///
/// Editor preview uses the native Sandbox.ColorGrading component on the authored
/// gameplay camera. Runtime cannot rely on that late native pass because OMR's
/// higher-priority composite camera owns the final image. Instead we reuse the
/// engine ColorGrading shader in a command list on the final overlay camera,
/// after the world look but before the independent viewmodel composite.
///
/// Result:
/// - world color grading is visible in the final gameplay image;
/// - viewmodel/arms remain outside the grading pass;
/// - no need to enable generic post-processing on the composite camera;
/// - LUT / RGB / HSV curve controls stay compatible with S&box's native shader.
/// </summary>
public sealed partial class OMRVisualFoundation
{
    private static readonly ProfilingSampler ColorGradingGpuScope = new( "OMR World Color Grading" );
    private bool _runtimeGradingCommandsBuilt;
    private int _runtimeGradingCommandsHash;

    private const string RuntimeColorGradingShaderPath = "ColorGrading.shader";
    private const int RuntimeColorGradingCommandOrder = 1500;

    [Property, Group( "World Look/Color Grading" ), Title( "Enable Color Grading" )]
    [Description( "World-only grading. In Play this is applied on OMR's final overlay camera before the viewmodel composite." )]
    public bool EnableColorGrading { get; set; } = false;

    [Property, Group( "World Look/Color Grading" ), Title( "Grading Method" )]
    public Sandbox.ColorGrading.GradingType ColorGradingMethod { get; set; } =
        Sandbox.ColorGrading.GradingType.TemperatureControl;

    [Property, Group( "World Look/Color Grading" ), Range( 0f, 1f ), Title( "Blend Factor" )]
    [Description( "Overall grading weight. World Look blend weight is applied on top of this." )]
    public float ColorGradingBlendFactor { get; set; } = 0.15f;

    [Property, Group( "World Look/Color Grading" ), Range( 0f, 4f ), Title( "Color Temperature" )]
    [ShowIf( nameof( ColorGradingMethod ), Sandbox.ColorGrading.GradingType.TemperatureControl )]
    [Description( "Artist-friendly temperature scale: 0 = very warm, 1 ≈ 3000 K, 2 = neutral 6500 K, 3 ≈ 9800 K, 4 = very cool. Internally converted to Kelvin for S&box." )]
    public float ColorGradingTemperature { get; set; } = 2f;

    // 8F3F5 compatibility: older scene revisions serialized the raw Kelvin
    // value under this name. Keep it hidden long enough to migrate those scenes
    // automatically, then clear it so the new 0-4 control becomes authoritative.
    [Hide]
    public float ColorGradingTemperatureK { get; set; } = 0f;

    [Property, Group( "World Look/Color Grading" ), Title( "Lookup Texture" )]
    [ShowIf( nameof( ColorGradingMethod ), Sandbox.ColorGrading.GradingType.LUT )]
    [Description( "Optional LUT path for later experimentation." )]
    public Texture ColorGradingLookupTexture { get; set; } = Texture.White;

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Curve Color Space" )]
    public Sandbox.ColorGrading.ColorSpaceEnum ColorGradingColorSpace { get; set; } =
        Sandbox.ColorGrading.ColorSpaceEnum.None;

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Red Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.RGB )]
    public Curve ColorGradingRedCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Green Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.RGB )]
    public Curve ColorGradingGreenCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Blue Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.RGB )]
    public Curve ColorGradingBlueCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Hue Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.HSV )]
    public Curve ColorGradingHueCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Saturation Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.HSV )]
    public Curve ColorGradingSaturationCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Property, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Value Curve" )]
    [ShowIf( nameof( ColorGradingColorSpace ), Sandbox.ColorGrading.ColorSpaceEnum.HSV )]
    public Curve ColorGradingValueCurve { get; set; } = CreateDefaultColorGradingCurve();

    [Button, Group( "World Look/Color Grading/A-B" ), Title( "Neutral Temperature" )]
    public void ApplyNeutralColorGradingPreset()
    {
        EnableColorGrading = true;
        ColorGradingMethod = Sandbox.ColorGrading.GradingType.TemperatureControl;
        ColorGradingBlendFactor = 1f;
        ColorGradingTemperature = 2f;
        ColorGradingColorSpace = Sandbox.ColorGrading.ColorSpaceEnum.None;
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading/A-B" ), Title( "Diagnostic Warm" )]
    [Description( "Deliberately obvious runtime check: full blend at 3500 K. If this does not visibly warm the world, the pass is not reaching the final image." )]
    public void ApplyDiagnosticWarmColorGradingPreset()
    {
        EnableColorGrading = true;
        ColorGradingMethod = Sandbox.ColorGrading.GradingType.TemperatureControl;
        ColorGradingBlendFactor = 1f;
        ColorGradingTemperature = KelvinToTemperatureControl( 3500f );
        ColorGradingColorSpace = Sandbox.ColorGrading.ColorSpaceEnum.None;
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading/A-B" ), Title( "Subtle Warm Standard" )]
    public void ApplyWarmColorGradingPreset()
    {
        EnableColorGrading = true;
        ColorGradingMethod = Sandbox.ColorGrading.GradingType.TemperatureControl;
        ColorGradingBlendFactor = 0.16f;
        ColorGradingTemperature = KelvinToTemperatureControl( 6000f );
        ColorGradingColorSpace = Sandbox.ColorGrading.ColorSpaceEnum.None;
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading/A-B" ), Title( "Subtle Cool Standard" )]
    public void ApplyCoolColorGradingPreset()
    {
        EnableColorGrading = true;
        ColorGradingMethod = Sandbox.ColorGrading.GradingType.TemperatureControl;
        ColorGradingBlendFactor = 0.14f;
        ColorGradingTemperature = KelvinToTemperatureControl( 7200f );
        ColorGradingColorSpace = Sandbox.ColorGrading.ColorSpaceEnum.None;
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading/A-B" ), Title( "Disable Color Grading" )]
    public void DisableColorGrading()
    {
        EnableColorGrading = false;
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading/Advanced Curves" ), Title( "Reset Curves To Default" )]
    public void ResetColorGradingCurves()
    {
        ColorGradingRedCurve = CreateDefaultColorGradingCurve();
        ColorGradingGreenCurve = CreateDefaultColorGradingCurve();
        ColorGradingBlueCurve = CreateDefaultColorGradingCurve();
        ColorGradingHueCurve = CreateDefaultColorGradingCurve();
        ColorGradingSaturationCurve = CreateDefaultColorGradingCurve();
        ColorGradingValueCurve = CreateDefaultColorGradingCurve();
        RebuildColorGrading();
    }

    [Button, Group( "World Look/Color Grading" ), Title( "Setup Color Grading Preview" )]
    [Description( "Adds/reuses Sandbox.ColorGrading on the authored gameplay camera for editor preview. Runtime uses OMR's final-camera bridge instead." )]
    public void SetupColorGradingPreview()
    {
        CameraComponent camera = GetComponent<CameraComponent>( true );
        if ( camera is null || !camera.IsValid )
        {
            Log.Warning(
                "[OMR VISUAL] Color Grading preview setup failed: OMRVisualFoundation must share a GameObject with the gameplay CameraComponent."
            );
            return;
        }

        ResolveColorGradingComponent();

        if ( _colorGrading is null || !_colorGrading.IsValid )
        {
            _colorGrading = GameObject.AddComponent<Sandbox.ColorGrading>( false );
            _colorGradingCreatedByFoundation = false;
        }

        ApplyNativeColorGradingSettings();

        Log.Info(
            $"[OMR VISUAL] Native Color Grading preview ready on '{GameObject.Name}' | " +
            $"Enabled:{_colorGrading.Enabled} | Method:{_colorGrading.GradingMethod} | " +
            $"Blend:{_colorGrading.BlendFactor:0.###} | ColorSpace:{_colorGrading.ColorSpace}"
        );
    }

    private Sandbox.ColorGrading _colorGrading;
    private bool _colorGradingCreatedByFoundation;
    private bool _colorGradingReadyLogged;
    private bool _colorGradingTemperatureMigrated;

    private CommandList _runtimeColorGradingCommands;
    private Material _runtimeColorGradingMaterial;
    private RenderAttributes _runtimeColorGradingAttributes;
    private bool _runtimeColorGradingCommandAttached;
    private bool _runtimeColorGradingMaterialFailureLogged;

    private float EffectiveColorGradingBlend =>
        EnableWorldLook && EnableColorGrading
            ? MathX.Clamp( ColorGradingBlendFactor, 0f, 1f ) * MathX.Clamp( BlendWeight, 0f, 1f )
            : 0f;

    // Temperature UI is deliberately not linear in Kelvin. White balance is
    // much more intuitive in reciprocal color temperature (mireds), so the
    // 0-4 control gets useful resolution on both the warm and cool sides.
    // Chosen anchors:
    //   0 = 1950 K, 1 = 3000 K, 2 = 6500 K, 3 ≈ 9811 K, 4 = 20000 K.
    private const float ColorTemperatureWarmKelvin = 1950f;
    private const float ColorTemperatureNeutralKelvin = 6500f;
    private const float ColorTemperatureCoolKelvin = 20000f;

    private float EffectiveColorTemperatureKelvin
    {
        get
        {
            EnsureColorTemperatureMigration();
            return TemperatureControlToKelvin( ColorGradingTemperature );
        }
    }

    private void EnsureColorTemperatureMigration()
    {
        if ( _colorGradingTemperatureMigrated )
            return;

        _colorGradingTemperatureMigrated = true;

        // Old builds stored Kelvin directly. Anything above the new 0-4 range
        // is therefore unambiguously a legacy value. Preserve the authored look
        // automatically instead of forcing the user to retune it.
        if ( ColorGradingTemperatureK > 4.001f )
            ColorGradingTemperature = KelvinToTemperatureControl( ColorGradingTemperatureK );

        ColorGradingTemperatureK = 0f;
    }

    private static float TemperatureControlToKelvin( float control )
    {
        float value = MathX.Clamp( control, 0f, 4f );

        float warmMired = 1000000f / ColorTemperatureWarmKelvin;
        float neutralMired = 1000000f / ColorTemperatureNeutralKelvin;
        float coolMired = 1000000f / ColorTemperatureCoolKelvin;

        float mired;
        if ( value <= 2f )
        {
            float t = value / 2f;
            mired = MathX.Lerp( warmMired, neutralMired, t );
        }
        else
        {
            float t = (value - 2f) / 2f;
            mired = MathX.Lerp( neutralMired, coolMired, t );
        }

        return 1000000f / MathF.Max( mired, 0.0001f );
    }

    private static float KelvinToTemperatureControl( float kelvin )
    {
        float k = MathX.Clamp( kelvin, ColorTemperatureWarmKelvin, ColorTemperatureCoolKelvin );
        float mired = 1000000f / k;
        float warmMired = 1000000f / ColorTemperatureWarmKelvin;
        float neutralMired = 1000000f / ColorTemperatureNeutralKelvin;
        float coolMired = 1000000f / ColorTemperatureCoolKelvin;

        if ( k <= ColorTemperatureNeutralKelvin )
        {
            float denom = MathF.Max( warmMired - neutralMired, 0.0001f );
            float t = MathX.Clamp( (warmMired - mired) / denom, 0f, 1f );
            return t * 2f;
        }

        float coolDenom = MathF.Max( neutralMired - coolMired, 0.0001f );
        float coolT = MathX.Clamp( (neutralMired - mired) / coolDenom, 0f, 1f );
        return 2f + coolT * 2f;
    }

    private static Curve CreateDefaultColorGradingCurve() =>
        new Curve( new Curve.Frame( 0f, 0.5f ), new Curve.Frame( 1f, 1f ) );

    private void ResetColorGradingSettings()
    {
        EnableColorGrading = false;
        ColorGradingMethod = Sandbox.ColorGrading.GradingType.TemperatureControl;
        ColorGradingBlendFactor = 0.15f;
        ColorGradingTemperature = 2f;
        ColorGradingTemperatureK = 0f;
        _colorGradingTemperatureMigrated = true;
        ColorGradingLookupTexture = Texture.White;
        ColorGradingColorSpace = Sandbox.ColorGrading.ColorSpaceEnum.None;

        ColorGradingRedCurve = CreateDefaultColorGradingCurve();
        ColorGradingGreenCurve = CreateDefaultColorGradingCurve();
        ColorGradingBlueCurve = CreateDefaultColorGradingCurve();
        ColorGradingHueCurve = CreateDefaultColorGradingCurve();
        ColorGradingSaturationCurve = CreateDefaultColorGradingCurve();
        ColorGradingValueCurve = CreateDefaultColorGradingCurve();
    }

    private void ResolveColorGradingComponent()
    {
        if ( _colorGrading is not null && _colorGrading.IsValid )
            return;

        _colorGrading = GetComponent<Sandbox.ColorGrading>( true );
        _colorGradingCreatedByFoundation = false;
    }

    private void RebuildColorGrading()
    {
        ResolveColorGradingComponent();

        // Editor preview: use S&box's native component exactly as authored.
        if ( !Game.IsPlaying )
        {
            if ( _colorGrading is not null && _colorGrading.IsValid )
                ApplyNativeColorGradingSettings();

            return;
        }

        // Runtime: the higher-priority OMR composite camera owns the final
        // framebuffer, so disable the source-camera native pass and apply the
        // same engine shader through our final-camera command bridge instead.
        if ( _colorGrading is not null && _colorGrading.IsValid )
            _colorGrading.Enabled = false;

        EnsureRuntimeColorGradingPass();
        RebuildRuntimeColorGradingCommands();
    }

    private void ApplyNativeColorGradingSettings()
    {
        if ( _colorGrading is null || !_colorGrading.IsValid )
            return;

        _colorGrading.GradingMethod = ColorGradingMethod;
        _colorGrading.BlendFactor = EffectiveColorGradingBlend;
        _colorGrading.ColorTempK = EffectiveColorTemperatureKelvin;
        _colorGrading.LookupTexture = ColorGradingLookupTexture ?? Texture.White;
        _colorGrading.ColorSpace = ColorGradingColorSpace;
        _colorGrading.RedCurve = ColorGradingRedCurve;
        _colorGrading.GreenCurve = ColorGradingGreenCurve;
        _colorGrading.BlueCurve = ColorGradingBlueCurve;
        _colorGrading.HueCurve = ColorGradingHueCurve;
        _colorGrading.SaturationCurve = ColorGradingSaturationCurve;
        _colorGrading.ValueCurve = ColorGradingValueCurve;
        _colorGrading.Enabled = EffectiveColorGradingBlend > 0.0001f;
    }

    private void EnsureRuntimeColorGradingPass()
    {
        if ( !Game.IsPlaying )
            return;

        if ( _runtimeOverlayCamera is null || !_runtimeOverlayCamera.IsValid )
            return;

        if ( _runtimeColorGradingMaterial is null || !_runtimeColorGradingMaterial.IsValid )
        {
            _runtimeColorGradingMaterial = Material.FromShader( RuntimeColorGradingShaderPath );

            if ( _runtimeColorGradingMaterial is null || !_runtimeColorGradingMaterial.IsValid )
            {
                if ( !_runtimeColorGradingMaterialFailureLogged )
                {
                    _runtimeColorGradingMaterialFailureLogged = true;
                    Log.Warning(
                        $"[OMR VISUAL] Could not create runtime Color Grading material from '{RuntimeColorGradingShaderPath}'."
                    );
                }

                return;
            }

            _runtimeColorGradingMaterialFailureLogged = false;
        }

        _runtimeColorGradingAttributes ??= new RenderAttributes();
        _runtimeColorGradingCommands ??= new CommandList( "OMR Runtime World Color Grading" );

        if ( !_runtimeColorGradingCommandAttached )
        {
            _runtimeOverlayCamera.AddCommandList(
                _runtimeColorGradingCommands,
                Stage.AfterDepthPrepass,
                RuntimeColorGradingCommandOrder
            );

            _runtimeColorGradingCommandAttached = true;
        }
    }

    private void RebuildRuntimeColorGradingCommands()
    {
        if ( _runtimeColorGradingCommands is null ||
             _runtimeColorGradingMaterial is null ||
             !_runtimeColorGradingMaterial.IsValid ||
             _runtimeColorGradingAttributes is null )
            return;

        int signature = HashCode.Combine( _runtimeColorGradingMaterial, EffectiveColorGradingBlend,
            EffectiveColorTemperatureKelvin, ColorGradingMethod, ColorGradingColorSpace, ColorGradingLookupTexture );
        if ( ColorGradingColorSpace == Sandbox.ColorGrading.ColorSpaceEnum.None &&
            _runtimeGradingCommandsBuilt && signature == _runtimeGradingCommandsHash ) return;
        _runtimeGradingCommandsBuilt = true;
        _runtimeGradingCommandsHash = signature;

        _runtimeColorGradingCommands.Reset();

        float blend = EffectiveColorGradingBlend;
        _runtimeColorGradingCommands.Enabled = blend > 0.0001f;

        if ( !_runtimeColorGradingCommands.Enabled )
        {
            _colorGradingReadyLogged = false;
            return;
        }

        _runtimeColorGradingAttributes.Set( "BlendFactor", blend );
        _runtimeColorGradingAttributes.Set( "ColorTempK", EffectiveColorTemperatureKelvin );
        _runtimeColorGradingAttributes.SetComboEnum( "D_CGRAD_PASS", ColorGradingMethod );
        _runtimeColorGradingAttributes.SetComboEnum( "D_COLORSPACE", ColorGradingColorSpace );

        if ( ColorGradingMethod == Sandbox.ColorGrading.GradingType.LUT )
            _runtimeColorGradingAttributes.Set( "LookupTexture", ColorGradingLookupTexture ?? Texture.White );

        if ( ColorGradingColorSpace == Sandbox.ColorGrading.ColorSpaceEnum.RGB )
        {
            SetRuntimeColorGradingCurve( ColorGradingRedCurve, "R" );
            SetRuntimeColorGradingCurve( ColorGradingGreenCurve, "G" );
            SetRuntimeColorGradingCurve( ColorGradingBlueCurve, "B" );
        }
        else if ( ColorGradingColorSpace == Sandbox.ColorGrading.ColorSpaceEnum.HSV )
        {
            SetRuntimeColorGradingCurve( ColorGradingHueCurve, "H" );
            SetRuntimeColorGradingCurve( ColorGradingSaturationCurve, "S" );
            SetRuntimeColorGradingCurve( ColorGradingValueCurve, "V" );
        }

        using ( _runtimeColorGradingCommands.ProfileScope( ColorGradingGpuScope ) )
        {
            _runtimeColorGradingCommands.Attributes.GrabFrameTexture( "ColorBuffer" );
            _runtimeColorGradingCommands.Blit( _runtimeColorGradingMaterial, _runtimeColorGradingAttributes );
        }

        if ( LogSetup && !_colorGradingReadyLogged )
        {
            _colorGradingReadyLogged = true;
            Log.Info(
                $"[OMR VISUAL] Runtime world Color Grading active | " +
                $"OverlayCamera:{_runtimeOverlayCamera.GameObject.Name} | " +
                $"Method:{ColorGradingMethod} | Blend:{blend:0.###} | " +
                $"Temperature:{EffectiveColorTemperatureKelvin:0}K | ColorSpace:{ColorGradingColorSpace}"
            );
        }
    }

    private void SetRuntimeColorGradingCurve( Curve curve, string channelLetter )
    {
        if ( _runtimeColorGradingAttributes is null )
            return;

        int totalFrames = Math.Min( curve.Length, 4 );
        _runtimeColorGradingAttributes.Set( "CurveFrames" + channelLetter, totalFrames );

        if ( totalFrames <= 0 )
            return;

        float[] xArray = new float[totalFrames];

        for ( int i = 0; i < totalFrames; ++i )
        {
            Curve.Frame frame = curve.Frames[i];
            xArray[i] = frame.Time;

            Vector4 localFrame = new Vector4(
                frame.Time,
                frame.Value,
                frame.In,
                frame.Out
            );

            _runtimeColorGradingAttributes.Set(
                "CurveFrame" + channelLetter + i,
                localFrame
            );
        }

        Vector4 divisorVector = new Vector4( 0f );

        if ( totalFrames >= 2 )
        {
            divisorVector.x = 1f / MathF.Max( xArray[1] - xArray[0], 0.00001f );

            if ( totalFrames >= 3 )
            {
                divisorVector.y = 1f / MathF.Max( xArray[2] - xArray[1], 0.00001f );

                if ( totalFrames >= 4 )
                    divisorVector.z = 1f / MathF.Max( xArray[3] - xArray[2], 0.00001f );
            }
        }

        _runtimeColorGradingAttributes.Set(
            "CurveDivisors" + channelLetter,
            divisorVector
        );
    }

    private void ReleaseRuntimeColorGradingPass()
    {
        _runtimeGradingCommandsBuilt = false;
        if ( _runtimeColorGradingCommandAttached &&
             _runtimeOverlayCamera is not null &&
             _runtimeOverlayCamera.IsValid &&
             _runtimeColorGradingCommands is not null )
        {
            _runtimeOverlayCamera.RemoveCommandList( _runtimeColorGradingCommands );
        }

        _runtimeColorGradingCommandAttached = false;

        if ( _runtimeColorGradingCommands is not null )
        {
            _runtimeColorGradingCommands.Enabled = false;
            _runtimeColorGradingCommands.Reset();
        }

        _runtimeColorGradingCommands = null;
        _runtimeColorGradingAttributes = null;
        _runtimeColorGradingMaterial = null;
        _runtimeColorGradingMaterialFailureLogged = false;
        _colorGradingReadyLogged = false;
    }

    private void ReleaseColorGrading()
    {
        if ( _colorGrading is not null && _colorGrading.IsValid )
        {
            _colorGrading.Enabled = false;

            if ( _colorGradingCreatedByFoundation )
                _colorGrading.Destroy();
        }

        _colorGrading = null;
        _colorGradingCreatedByFoundation = false;
        ReleaseRuntimeColorGradingPass();
    }
}