OMRViewModelCamera.cs
using Sandbox;
using System;
using System.Linq;

/// <summary>
/// Local-only first-person camera stack for One More Round.
///
/// Phase 8F2A keeps the proven low-near-plane projection but captures ordinary
/// arms/weapon into a transparent render target. A later screen camera preserves
/// the world color, receives OMR's world-look + viewmodel composite command
/// lists, renders physical scope surfaces directly, then owns the HUD.
/// </summary>
[Title( "OMR ViewModel Camera" )]
public sealed class OMRViewModelCamera : Component, ICameraModifier
{
    private const string DefaultViewModelTag = "viewmodel";
    private const string DefaultScopeSurfaceTag = "viewmodel_scope_surface";
    private const string DefaultLightTag = "light";

    [Property, Group( "References" )] public CameraComponent SourceCamera { get; set; }
    [Property, Group( "Projection" ), Range( 0.25f, 5f )] public float ViewModelZNear { get; set; } = 1f;
    [Property, Group( "Projection" ), Range( 1, 10 )] public int PriorityOffset { get; set; } = 1;
    [Property, Group( "Projection" )] public bool MatchSourceFieldOfView { get; set; } = true;
    [Property, Group( "Projection" ), Range( 30f, 120f )] public float ViewModelFieldOfView { get; set; } = 80f;
    public float CurrentViewModelFieldOfView { get; private set; }
    public float CurrentViewModelAimFovScale { get; private set; } = 1f;

    [Property, Group( "Filtering" )] public string ViewModelTag { get; set; } = DefaultViewModelTag;
    [Property, Group( "Filtering" )] public string ScopeSurfaceTag { get; set; } = DefaultScopeSurfaceTag;
    [Property, Group( "Filtering" )] public string LightTag { get; set; } = DefaultLightTag;
    [Property, Group( "Filtering" )] public bool PropagateTagToViewModelChildren { get; set; } = true;

    [Property, Group( "Presentation" )]
    [Description( "Legacy compatibility toggle. The 8F2A offscreen capture intentionally keeps native camera post-processing disabled; OMR applies its own viewmodel treatment during composite." )]
    public bool EnableViewModelPostProcessing { get; set; } = false;

    [Property, Group( "Diagnostics" )] public bool LogSetup { get; set; } = false;
    [Property, Group( "Diagnostics" )] public bool LogViewSync { get; set; } = false;

    private GameObject _viewModelCameraObject;
    private CameraComponent _viewModelCamera;
    private GameObject _compositeCameraObject;
    private CameraComponent _compositeCamera;
    private Texture _viewModelRenderTarget;
    private int _viewModelRenderWidth;
    private int _viewModelRenderHeight;

    private CameraView _sourceView;
    private bool _hasSourceView;
    private bool _addedWorldCameraExclude;
    private float _nextDiagnosticAt;
    private PlayerState _localPlayer;
    private BaseInventoryComponent _localInventory;
    private OMRVisualFoundation _visualFoundation;

    int ICameraModifier.CameraOrder => 1_000_000;

    protected override void OnStart()
    {
        ResolveSourceCamera();
        if ( SourceCamera is null || !SourceCamera.IsValid )
        {
            Log.Warning( "[OMR VM CAM] No valid source CameraComponent was found." );
            Enabled = false;
            return;
        }

        CreatePresentationCameras();
        ConfigureWorldCameraFilter();
        if ( PropagateTagToViewModelChildren ) TagLocalViewModelHierarchies();
        EnsureViewModelRenderTarget();
        BindVisualFoundationRuntimePass();

        if ( LogSetup )
        {
            Log.Info( $"[OMR VM CAM] Ready | Independent viewmodel buffer | World ZNear:{SourceCamera.ZNear:0.##} | Viewmodel ZNear:{ViewModelZNear:0.##} | World Priority:{SourceCamera.Priority} | Capture Priority:{_viewModelCamera?.Priority} | Composite Priority:{_compositeCamera?.Priority} | RT:{_viewModelRenderWidth}x{_viewModelRenderHeight}" );
        }
    }

    protected override void OnUpdate()
    {
        ResolveSourceCamera();
        if ( SourceCamera is null || !SourceCamera.IsValid ) return;

        if ( _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid )
        {
            CreatePresentationCameras();
            ConfigureWorldCameraFilter();
        }

        if ( _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;

        if ( PropagateTagToViewModelChildren ) TagLocalViewModelHierarchies();
        EnsureViewModelRenderTarget();
        ConfigurePresentationCameras();
        BindVisualFoundationRuntimePass();
    }

    protected override void OnDisabled()
    {
        RestoreWorldCameraFilter();
        UnbindVisualFoundationRuntimePass();
        DestroyPresentationCameras();
        ReleaseViewModelRenderTarget();
        _hasSourceView = false;
        CurrentViewModelFieldOfView = 0f;
        CurrentViewModelAimFovScale = 1f;
        base.OnDisabled();
    }

    protected override void OnDestroy()
    {
        RestoreWorldCameraFilter();
        UnbindVisualFoundationRuntimePass();
        DestroyPresentationCameras();
        ReleaseViewModelRenderTarget();
        _hasSourceView = false;
        CurrentViewModelFieldOfView = 0f;
        CurrentViewModelAimFovScale = 1f;
        base.OnDestroy();
    }

    void ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )
    {
        if ( SourceCamera is null || !SourceCamera.IsValid || _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;

        if ( camera == SourceCamera )
        {
            _sourceView = view;
            _hasSourceView = true;
            return;
        }

        if ( camera != _viewModelCamera && camera != _compositeCamera ) return;

        CameraView source = _hasSourceView ? _sourceView : SourceCamera.View;
        view.Position = source.Position;
        view.Rotation = source.Rotation;
        IWeaponOpticState opticState = ResolveLocalOpticState();
        view.FieldOfView = ResolveViewModelFieldOfView( source.FieldOfView, opticState );
        CurrentViewModelFieldOfView = view.FieldOfView;
        view.ZNear = ViewModelZNear;
        view.ZFar = source.ZFar;

        if ( LogViewSync && camera == _compositeCamera && RealTime.Now >= _nextDiagnosticAt )
        {
            _nextDiagnosticAt = RealTime.Now + 1f;
            Log.Info( $"[OMR VM CAM] Sync | FOV:{view.FieldOfView:0.##} | AimFovScale:{CurrentViewModelAimFovScale:0.###} | ZNear:{view.ZNear:0.##} | RT:{_viewModelRenderWidth}x{_viewModelRenderHeight} | Pos:{view.Position}" );
        }
    }

    public float ResolveViewModelFieldOfView( float composedSourceFieldOfView, IWeaponOpticState opticState = null )
    {
        float baseFieldOfView = MatchSourceFieldOfView ? composedSourceFieldOfView : ViewModelFieldOfView;
        float aimScale = MathX.Clamp( opticState?.CurrentViewModelAimFovScale ?? 1f, 0.5f, 1.5f );
        CurrentViewModelAimFovScale = aimScale;
        return MathX.Clamp( baseFieldOfView * aimScale, 1f, 179f );
    }

    private IWeaponOpticState ResolveLocalOpticState() => ResolveLocalInventory()?.ActiveItem as IWeaponOpticState;

    private BaseInventoryComponent ResolveLocalInventory()
    {
        if ( _localPlayer is not null && _localPlayer.IsValid && !_localPlayer.IsProxy && !_localPlayer.IsBot && _localInventory is not null && _localInventory.IsValid ) return _localInventory;
        _localPlayer = Scene?.GetAllComponents<PlayerState>().FirstOrDefault( x => x is not null && x.IsValid && !x.IsProxy && !x.IsBot );
        _localInventory = _localPlayer?.Components.Get<BaseInventoryComponent>();
        return _localInventory;
    }

    private void ResolveSourceCamera()
    {
        if ( SourceCamera is not null && SourceCamera.IsValid ) return;
        SourceCamera = Components.Get<CameraComponent>();
        SourceCamera ??= Scene?.Camera;
    }

    private void CreatePresentationCameras()
    {
        if ( Scene is null || SourceCamera is null || !SourceCamera.IsValid ) return;

        if ( _viewModelCamera is null || !_viewModelCamera.IsValid )
        {
            _viewModelCameraObject = Scene.CreateObject( true );
            _viewModelCameraObject.Name = "OMR ViewModel Capture Camera";
            _viewModelCameraObject.NetworkMode = NetworkMode.Never;
            _viewModelCameraObject.Flags |= GameObjectFlags.NotSaved;
            _viewModelCamera = _viewModelCameraObject.Components.Create<CameraComponent>();
            _viewModelCamera.IsMainCamera = false;
            // The editor/game viewport can report 0x0 for the first frame after
            // entering Play. Keep the capture camera disabled until a real
            // render target exists so it never falls back to drawing on-screen
            // or asks the renderer to interpolate a zero-sized viewport.
            _viewModelCamera.Enabled = false;
        }

        if ( _compositeCamera is null || !_compositeCamera.IsValid )
        {
            _compositeCameraObject = Scene.CreateObject( true );
            _compositeCameraObject.Name = "OMR ViewModel Composite Camera";
            _compositeCameraObject.NetworkMode = NetworkMode.Never;
            _compositeCameraObject.Flags |= GameObjectFlags.NotSaved;
            _compositeCamera = _compositeCameraObject.Components.Create<CameraComponent>();
            _compositeCamera.IsMainCamera = true;
        }

        ConfigurePresentationCameras();
    }

    private void ConfigurePresentationCameras()
    {
        if ( SourceCamera is null || !SourceCamera.IsValid ) return;

        if ( _viewModelCamera is not null && _viewModelCamera.IsValid )
        {
            _viewModelCamera.ClearFlags = ClearFlags.All;
            _viewModelCamera.BackgroundColor = Color.Transparent;
            _viewModelCamera.EnablePostProcessing = false;
            _viewModelCamera.IsMainCamera = false;
            _viewModelCamera.FovAxis = SourceCamera.FovAxis;
            _viewModelCamera.Viewport = SourceCamera.Viewport;
            _viewModelCamera.TargetEye = SourceCamera.TargetEye;
            _viewModelCamera.ZNear = ViewModelZNear;
            _viewModelCamera.ZFar = SourceCamera.ZFar;
            _viewModelCamera.Priority = SourceCamera.Priority + PriorityOffset;

            if ( !string.IsNullOrWhiteSpace( ViewModelTag ) && !_viewModelCamera.RenderTags.Has( ViewModelTag ) ) _viewModelCamera.RenderTags.Add( ViewModelTag );
            if ( !string.IsNullOrWhiteSpace( LightTag ) && !_viewModelCamera.RenderTags.Has( LightTag ) ) _viewModelCamera.RenderTags.Add( LightTag );
            if ( !string.IsNullOrWhiteSpace( ScopeSurfaceTag ) && !_viewModelCamera.RenderExcludeTags.Has( ScopeSurfaceTag ) ) _viewModelCamera.RenderExcludeTags.Add( ScopeSurfaceTag );
        }

        if ( _compositeCamera is not null && _compositeCamera.IsValid )
        {
            _compositeCamera.ClearFlags = ClearFlags.Depth | ClearFlags.Stencil;
            _compositeCamera.EnablePostProcessing = false;
            _compositeCamera.IsMainCamera = true;
            _compositeCamera.FovAxis = SourceCamera.FovAxis;
            _compositeCamera.Viewport = SourceCamera.Viewport;
            _compositeCamera.TargetEye = SourceCamera.TargetEye;
            _compositeCamera.ZNear = ViewModelZNear;
            _compositeCamera.ZFar = SourceCamera.ZFar;
            _compositeCamera.Priority = SourceCamera.Priority + PriorityOffset + 1;

            if ( !string.IsNullOrWhiteSpace( ScopeSurfaceTag ) && !_compositeCamera.RenderTags.Has( ScopeSurfaceTag ) ) _compositeCamera.RenderTags.Add( ScopeSurfaceTag );
            if ( !string.IsNullOrWhiteSpace( LightTag ) && !_compositeCamera.RenderTags.Has( LightTag ) ) _compositeCamera.RenderTags.Add( LightTag );
        }
    }

    private void EnsureViewModelRenderTarget()
    {
        if ( SourceCamera is null || !SourceCamera.IsValid || _viewModelCamera is null || !_viewModelCamera.IsValid ) return;

        Vector2 size = SourceCamera.ScreenRect.Size;

        // ScreenRect can legitimately be 0x0 during the first editor Play
        // frame while the game viewport is being constructed. Do not fabricate
        // a 2x2 target: simply wait until the renderer publishes a valid size.
        // This avoids the transient "Interop width should be higher than 0"
        // warning and prevents the capture camera from ever rendering to the
        // screen while its offscreen target is unavailable.
        if ( size.x < 2f || size.y < 2f )
        {
            _viewModelCamera.Enabled = false;
            _viewModelCamera.RenderTarget = null;
            _viewModelCamera.CustomSize = null;
            return;
        }

        float scale = OMRClientPerformance.ViewModelResolutionScale;
        int width = Math.Max( 2, (int)MathF.Round( size.x * scale ) );
        int height = Math.Max( 2, (int)MathF.Round( size.y * scale ) );

        if ( _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid && _viewModelRenderWidth == width && _viewModelRenderHeight == height )
        {
            _viewModelCamera.RenderTarget = _viewModelRenderTarget;
            _viewModelCamera.CustomSize = new Vector2( width, height );
            _viewModelCamera.Enabled = true;
            return;
        }

        Texture oldTarget = _viewModelRenderTarget;
        _visualFoundation?.UnbindRuntimeViewModelTexture( oldTarget );
        _viewModelCamera.RenderTarget = null;

        _viewModelRenderTarget = Texture.CreateRenderTarget()
            .WithSize( width, height )
            .WithFormat( ImageFormat.RGBA8888 )
            .WithInitialColor( Color.Transparent )
            .Create( "OMR ViewModel Capture", true );

        _viewModelRenderWidth = width;
        _viewModelRenderHeight = height;
        _viewModelCamera.RenderTarget = _viewModelRenderTarget;
        _viewModelCamera.CustomSize = new Vector2( width, height );
        _viewModelCamera.Enabled = true;

        if ( oldTarget is not null && oldTarget.IsValid ) oldTarget.Dispose();
        if ( LogSetup && _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid ) Log.Info( $"[OMR VM CAM] Capture target ready | {width}x{height} | RGBA8888" );
    }

    private void ReleaseViewModelRenderTarget()
    {
        Texture target = _viewModelRenderTarget;
        _visualFoundation?.UnbindRuntimeViewModelTexture( target );
        if ( _viewModelCamera is not null && _viewModelCamera.IsValid )
        {
            _viewModelCamera.Enabled = false;
            _viewModelCamera.RenderTarget = null;
            _viewModelCamera.CustomSize = null;
        }
        _viewModelRenderTarget = null;
        _viewModelRenderWidth = 0;
        _viewModelRenderHeight = 0;
        if ( target is not null && target.IsValid ) target.Dispose();
    }

    private void ConfigureWorldCameraFilter()
    {
        if ( SourceCamera is null || !SourceCamera.IsValid || string.IsNullOrWhiteSpace( ViewModelTag ) ) return;
        if ( !SourceCamera.RenderExcludeTags.Has( ViewModelTag ) )
        {
            SourceCamera.RenderExcludeTags.Add( ViewModelTag );
            _addedWorldCameraExclude = true;
        }
    }

    private void RestoreWorldCameraFilter()
    {
        if ( _addedWorldCameraExclude && SourceCamera is not null && SourceCamera.IsValid && !string.IsNullOrWhiteSpace( ViewModelTag ) ) SourceCamera.RenderExcludeTags.Remove( ViewModelTag );
        _addedWorldCameraExclude = false;
    }

    private void BindVisualFoundationRuntimePass()
    {
        if ( SourceCamera is null || !SourceCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;
        OMRVisualFoundation foundation = SourceCamera.GameObject.Components.Get<OMRVisualFoundation>();

        if ( foundation != _visualFoundation )
        {
            if ( _visualFoundation is not null )
            {
                _visualFoundation.UnbindRuntimeViewModelTexture( _viewModelRenderTarget );
                _visualFoundation.UnbindRuntimeOverlayCamera( _compositeCamera );
            }
            _visualFoundation = foundation;
        }

        _visualFoundation?.BindRuntimeOverlayCamera( _compositeCamera );
        if ( _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid ) _visualFoundation?.BindRuntimeViewModelTexture( _viewModelRenderTarget );
    }

    private void UnbindVisualFoundationRuntimePass()
    {
        if ( _visualFoundation is not null )
        {
            _visualFoundation.UnbindRuntimeViewModelTexture( _viewModelRenderTarget );
            _visualFoundation.UnbindRuntimeOverlayCamera( _compositeCamera );
        }
        _visualFoundation = null;
    }

    private void DestroyPresentationCameras()
    {
        if ( _viewModelCameraObject is not null && _viewModelCameraObject.IsValid ) _viewModelCameraObject.Destroy();
        if ( _compositeCameraObject is not null && _compositeCameraObject.IsValid ) _compositeCameraObject.Destroy();
        _viewModelCameraObject = null;
        _viewModelCamera = null;
        _compositeCameraObject = null;
        _compositeCamera = null;
    }

	private GameObject _taggedViewModel;
	private string _taggedViewModelTag;
	private string _taggedScopeTag;
	private float _nextHierarchyTagAt;
	private void TagLocalViewModelHierarchies()
	{
		if ( Scene is null || string.IsNullOrWhiteSpace( ViewModelTag ) ) return;
		OneMoreRoundWeapon weapon = ResolveLocalInventory()?.ActiveItem as OneMoreRoundWeapon;
		GameObject viewModel = weapon?.IsValid == true && !weapon.IsBotControlled ? weapon.ViewModel : null;
		if ( viewModel?.IsValid != true ) { _taggedViewModel = null; return; }
		if ( viewModel == _taggedViewModel && ViewModelTag == _taggedViewModelTag &&
			ScopeSurfaceTag == _taggedScopeTag && RealTime.Now < _nextHierarchyTagAt ) return;
		_taggedViewModel = viewModel;
		_taggedViewModelTag = ViewModelTag;
		_taggedScopeTag = ScopeSurfaceTag;
		_nextHierarchyTagAt = RealTime.Now + 0.50f;
		TagHierarchy( viewModel );
		TagScopeSurfaces( viewModel );
	}

    private void TagHierarchy( GameObject gameObject )
    {
        if ( gameObject is null || !gameObject.IsValid ) return;
        if ( !gameObject.Tags.Has( ViewModelTag ) ) gameObject.Tags.Add( ViewModelTag );
        foreach ( GameObject child in gameObject.Children ) TagHierarchy( child );
    }

    private void TagScopeSurfaces( GameObject viewModel )
    {
        if ( viewModel is null || !viewModel.IsValid || string.IsNullOrWhiteSpace( ScopeSurfaceTag ) ) return;

        foreach ( OMRScopePhysicalLens lens in viewModel.Components.GetAll<OMRScopePhysicalLens>( FindMode.InSelf | FindMode.InDescendants ) )
        {
            if ( lens is null || !lens.IsValid ) continue;
            AddScopeSurfaceTag( lens.ImageRenderer?.GameObject );
            AddScopeSurfaceTag( lens.GlassRenderer?.GameObject );
        }

        TagScopeSurfaceNamesRecursive( viewModel );
    }

    private void TagScopeSurfaceNamesRecursive( GameObject gameObject )
    {
        if ( gameObject is null || !gameObject.IsValid ) return;
        if ( string.Equals( gameObject.Name, "ScopeImagePlane", StringComparison.OrdinalIgnoreCase ) || string.Equals( gameObject.Name, "ScopeGlass", StringComparison.OrdinalIgnoreCase ) ) AddScopeSurfaceTag( gameObject );
        foreach ( GameObject child in gameObject.Children ) TagScopeSurfaceNamesRecursive( child );
    }

    private void AddScopeSurfaceTag( GameObject gameObject )
    {
        if ( gameObject is null || !gameObject.IsValid || string.IsNullOrWhiteSpace( ScopeSurfaceTag ) ) return;
        if ( !gameObject.Tags.Has( ScopeSurfaceTag ) ) gameObject.Tags.Add( ScopeSurfaceTag );
    }
}