OMRScopePhysicalLens.cs
using Sandbox;
using System;
using System.Linq;

/// <summary>
/// Bridges the existing OMR scope PiP render target to a physical model-space
/// image surface.
///
/// Phase 6C2 moves the procedural reticle onto that physical surface and
/// suppresses the legacy WorldPanel while the physical image is active.
///
/// Phase 6C3.1 reworks the lens into two deliberately separate layers:
/// ScopeGlass handles Facepunch-inspired refractive/PBR glass, while the
/// view-dependent eye-box/scope-shadow mask is applied to ScopeImagePlane.
/// This keeps the glass clear and dynamic instead of turning the whole lens
/// into an opaque grey overlay. Phase 6C3.2 adds scope-local lateral chromatic
/// aberration to the PiP image, strongest toward the lens edge and independently
/// tunable for hipfire and ADS. The reticle remains unsplit/gameplay-authoritative.
/// Phase 6C4 moves E19 lens-size measurement to the physical ScopeImagePlane
/// renderer. The legacy WorldPanel remains optional only as a compatibility
/// fallback for older optics and is no longer required by a fully migrated M700.
/// </summary>
[Title( "OMR Scope Physical Lens" )]
public sealed class OMRScopePhysicalLens : Component
{
	private const float DefaultReticleReferenceResolution = 512f;

	[Property, Group( "References" )]
	public OMRScopePiPCamera PiPCamera { get; set; }

	[Property, Group( "References" )]
	public ModelRenderer ImageRenderer { get; set; }

	[Property, Group( "References" )]
	[Description( "Optional physical convex glass renderer placed in front of ScopeImagePlane. If empty, ScopeGlass is resolved by name." )]
	public ModelRenderer GlassRenderer { get; set; }

	[Property, Group( "References" )]
	[Description( "Legacy model-space WorldPanel retained temporarily for E19 automatic lens-size measurement. It is hidden from render while the physical lens is active." )]
	public Sandbox.WorldPanel LegacyWorldPanel { get; set; }

	[Property, Group( "References" )]
	[Description( "Fallback child name used when ImageRenderer has not been assigned manually." )]
	public string ImageObjectName { get; set; } = "ScopeImagePlane";

	[Property, Group( "References" )]
	[Description( "Fallback child name used when GlassRenderer has not been assigned manually." )]
	public string GlassObjectName { get; set; } = "ScopeGlass";

	[Property, Group( "Physical Image" )]
	[Description( "Show the PiP render target on the physical ScopeImagePlane." )]
	public bool EnablePhysicalImage { get; set; } = false;

	[Property, Group( "Physical Image" )]
	public string ImageShaderPath { get; set; } = "shaders/omr_scope_image.shader";

	[Property, Group( "Physical Image" )]
	[Description( "Hide an optional legacy WorldPanel while the physical image is visible. Fully migrated Phase 6C4 optics can remove the WorldPanel entirely." )]
	public bool HideLegacyWorldPanelWhenPhysicalImage { get; set; } = true;

	[Property, Group( "Physical Reticle" )]
	[Description( "Render the optic profile's procedural reticle directly in the physical lens shader. ShowSimpleReticle and the authored reticle opacity still control final visibility." )]
	public bool EnablePhysicalReticle { get; set; } = true;

	[Property, Group( "Physical Reticle" ), Range( 128f, 2048f )]
	[Description( "Reference canvas used to convert the existing pixel-authored reticle dimensions to normalized lens UVs. 512 preserves the legacy WorldPanel tuning exactly." )]
	public float ReticleReferenceResolution { get; set; } = DefaultReticleReferenceResolution;

	[Property, Group( "Chromatic Aberration" )]
	[Description( "Enable scope-local lateral chromatic aberration on the PiP image. The effect grows toward the edge of the lens and is applied before the reticle, so aiming remains crisp." )]
	public bool EnableChromaticAberration { get; set; } = true;

	[Property, Group( "Chromatic Aberration" ), Range( 0f, 8f )]
	[Description( "Approximate red/blue channel separation in reference pixels while hipfiring. Keep this low so the off-eye preview does not become a rainbow smear." )]
	public float ChromaticAberrationHipPixels { get; set; } = 0.35f;

	[Property, Group( "Chromatic Aberration" ), Range( 0f, 8f )]
	[Description( "Approximate red/blue channel separation in reference pixels at full ADS. 2-3 px is clearly visible without overwhelming the sight picture." )]
	public float ChromaticAberrationAdsPixels { get; set; } = 2.25f;

	[Property, Group( "Chromatic Aberration" ), Range( 128f, 2048f )]
	[Description( "Reference resolution used to convert the authored pixel separation into normalized lens UVs. 512 keeps the effect stable across PiP render-target quality changes." )]
	public float ChromaticAberrationReferenceResolution { get; set; } = 512f;

	[Property, Group( "Chromatic Aberration" ), Range( 0f, 0.95f )]
	[Description( "Normalized radial position where lateral color separation begins. 0 is the lens center and 1 is the edge." )]
	public float ChromaticAberrationStartRadius { get; set; } = 0.18f;

	[Property, Group( "Chromatic Aberration" ), Range( 0.25f, 4f )]
	[Description( "Controls how quickly chromatic separation ramps from the protected center toward the lens edge. Higher values keep the center cleaner." )]
	public float ChromaticAberrationEdgePower { get; set; } = 1.35f;

	[Property, Group( "Physical Glass" )]
	[Description( "Enable the separate convex ScopeGlass renderer. The glass is presentation-only and never changes the PiP camera, reticle or ballistic aim." )]
	public bool EnablePhysicalGlass { get; set; } = false;

	[Property, Group( "Physical Glass" )]
	public string GlassShaderPath { get; set; } = "shaders/omr_scope_glass.shader";

	[Property, Group( "Physical Glass" ), Range( 1f, 1.05f )]
	[Description( "Index-of-refraction style strength used by the framebuffer refraction path. Facepunch sight glass commonly stays very close to 1.0; start subtle." )]
	public float GlassRefractionStrength { get; set; } = 1.003f;

	[Property, Group( "Physical Glass" ), Range( 0f, 1f )]
	[Description( "PBR roughness of the physical glass. Lower values produce a cleaner, sharper coating/reflection." )]
	public float GlassRoughness { get; set; } = 0.10f;

	[Property, Group( "Physical Glass" ), Range( 0f, 1f )]
	[Description( "Additional framebuffer blur used by the refracted view. It is multiplied by roughness and grazing angle in the shader." )]
	public float GlassBlurAmount { get; set; } = 0.18f;

	[Property, Group( "Physical Glass" ), Range( 0f, 1000f )]
	[Description( "Thin-film interference wavelength/thickness control, following Facepunch's glass_scope shader convention." )]
	public float GlassIridescence { get; set; } = 600f;

	[Property, Group( "Physical Glass" ), Range( 0f, 2f )]
	[Description( "Strength of the thin-film coating coloration. OMR defaults much lower than Facepunch's RMR material because the M700 scope should stay visually clean." )]
	public float GlassIridescenceScale { get; set; } = 0.08f;

	[Property, Group( "Physical Glass" ), Range( 0f, 2f )]
	[Description( "Multiplier applied to Lens Hip/ADS Reflection Opacity for the physical glass coating." )]
	public float GlassReflectionScale { get; set; } = 1f;

	[Property, Group( "Physical Glass" ), Range( 0f, 2f )]
	[Description( "Multiplier applied to Lens Hip/ADS Tint Opacity on the physical glass." )]
	public float GlassTintScale { get; set; } = 1f;

	[Property, Group( "Scope Shadow" ), Range( 0f, 2f )]
	[Description( "Multiplier applied to the profile's normal lens edge darkening. The mask is now applied to ScopeImagePlane rather than painted onto the glass." )]
	public float GlassEdgeDarkeningScale { get; set; } = 0.75f;

	[Property, Group( "Scope Shadow" ), Range( 0f, 2f )]
	[Description( "Multiplier for E19 eye-box shadow strength. The shadow darkens the PiP itself instead of making ScopeGlass opaque." )]
	public float GlassEyeBoxShadowScale { get; set; } = 1f;

	[Property, Group( "Scope Shadow" ), Range( 0f, 2f )]
	[Description( "View-dependent parallax amount used to move the exit-pupil/scope-shadow mask from the actual camera-to-lens angle. This is based on the same projection idea used by Facepunch for sight parallax." )]
	public float ScopeShadowParallaxScale { get; set; } = 0.65f;

	[Property, Group( "Scope Shadow" ), Range( 0f, 1.5f )]
	[Description( "Normalized radius where the dynamic eye-box shadow begins." )]
	public float ScopeShadowInnerRadius { get; set; } = 0.58f;

	[Property, Group( "Scope Shadow" ), Range( 0.05f, 1.5f )]
	[Description( "Normalized radius where the dynamic eye-box shadow becomes fully opaque before its authored strength multiplier." )]
	public float ScopeShadowOuterRadius { get; set; } = 0.98f;

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

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

	private Material _imageMaterial;
	private string _loadedShaderPath;
	private Material _glassMaterial;
	private string _loadedGlassShaderPath;
	private bool _lastVisible;
	private bool _lastGlassVisible;
	private bool _loggedMissingRenderer;
	private bool _loggedMissingGlassRenderer;
	private bool _loggedMissingPiP;
	private OMRVisualFoundation _visualFoundation;

	private bool _legacyRenderStateCaptured;
	private bool _legacyEnabled;
	private bool _legacyWasSuppressed;

	protected override void OnStart()
	{
		ResolveReferences();
		EnsureImageMaterial();
		EnsureGlassMaterial();
		ApplyPhysicalImageState();

		if ( LogSetup )
		{
			Log.Info(
				$"[OMR SCOPE PHYSICAL] Ready | Image:{ImageRenderer?.GameObject?.Name ?? "NONE"} | " +
				$"Glass:{GlassRenderer?.GameObject?.Name ?? "NONE"} | PiP:{PiPCamera?.GameObject?.Name ?? "NONE"} | " +
				$"LegacyPanel:{LegacyWorldPanel?.GameObject?.Name ?? "NONE"} | " +
				$"ImageShader:{ImageShaderPath} | GlassShader:{GlassShaderPath}"
			);
		}
	}

	protected override void OnUpdate()
	{
		ResolveReferences();
		EnsureImageMaterial();
		EnsureGlassMaterial();
		ApplyPhysicalImageState();
	}

	protected override void OnDisabled()
	{
		DisablePhysicalPresentation();
		base.OnDisabled();
	}

	protected override void OnDestroy()
	{
		DisablePhysicalPresentation();
		base.OnDestroy();
	}

	private void ResolveReferences()
	{
		if ( ImageRenderer is null || !ImageRenderer.IsValid )
		{
			GameObject imageObject = GameObject
				.GetAllObjects( true )
				.FirstOrDefault(
					x =>
						x is not null &&
						x.IsValid &&
						string.Equals( x.Name, ImageObjectName, StringComparison.OrdinalIgnoreCase )
				);

			ImageRenderer = imageObject?.Components.Get<ModelRenderer>();
		}

		if ( GlassRenderer is null || !GlassRenderer.IsValid )
		{
			GameObject glassObject = GameObject
				.GetAllObjects( true )
				.FirstOrDefault(
					x =>
						x is not null &&
						x.IsValid &&
						string.Equals( x.Name, GlassObjectName, StringComparison.OrdinalIgnoreCase )
				);

			GlassRenderer = glassObject?.Components.Get<ModelRenderer>();
		}

		if ( PiPCamera is null || !PiPCamera.IsValid )
		{
			PiPCamera = Scene?
				.GetAllComponents<OMRScopePiPCamera>()
				.FirstOrDefault( x => x is not null && x.IsValid );
		}

		if ( _visualFoundation is null || !_visualFoundation.IsValid )
		{
			_visualFoundation =
				PiPCamera?.SourceCamera?.GameObject?
					.Components
					.Get<OMRVisualFoundation>();

			_visualFoundation ??= Scene?
				.GetAllComponents<OMRVisualFoundation>()
				.FirstOrDefault( x => x is not null && x.IsValid );
		}

		if ( LegacyWorldPanel is null || !LegacyWorldPanel.IsValid )
		{
			LegacyWorldPanel = GameObject.GetComponent<Sandbox.WorldPanel>( true );
			_legacyRenderStateCaptured = false;
			_legacyWasSuppressed = false;
		}

		CaptureLegacyWorldPanelRenderState();

		if ( ImageRenderer is null || !ImageRenderer.IsValid )
		{
			if ( LogSetup && !_loggedMissingRenderer )
			{
				_loggedMissingRenderer = true;
				Log.Warning(
					$"[OMR SCOPE PHYSICAL] No ModelRenderer found for '{ImageObjectName}'."
				);
			}
		}
		else
		{
			_loggedMissingRenderer = false;
		}

		if ( GlassRenderer is null || !GlassRenderer.IsValid )
		{
			if ( LogSetup && EnablePhysicalGlass && !_loggedMissingGlassRenderer )
			{
				_loggedMissingGlassRenderer = true;
				Log.Warning(
					$"[OMR SCOPE PHYSICAL] No ModelRenderer found for '{GlassObjectName}'."
				);
			}
		}
		else
		{
			_loggedMissingGlassRenderer = false;
		}

		if ( PiPCamera is null || !PiPCamera.IsValid )
		{
			if ( LogSetup && !_loggedMissingPiP )
			{
				_loggedMissingPiP = true;
				Log.Warning( "[OMR SCOPE PHYSICAL] No OMRScopePiPCamera found in the scene." );
			}
		}
		else
		{
			_loggedMissingPiP = false;
		}
	}

	private void EnsureImageMaterial()
	{
		if ( string.IsNullOrWhiteSpace( ImageShaderPath ) )
			return;

		if (
			_imageMaterial is not null &&
			_imageMaterial.IsValid &&
			string.Equals( _loadedShaderPath, ImageShaderPath, StringComparison.OrdinalIgnoreCase )
		)
		{
			return;
		}

		_imageMaterial = Material.FromShader( ImageShaderPath );
		_loadedShaderPath = ImageShaderPath;
	}

	private void EnsureGlassMaterial()
	{
		if ( string.IsNullOrWhiteSpace( GlassShaderPath ) )
			return;

		if (
			_glassMaterial is not null &&
			_glassMaterial.IsValid &&
			string.Equals( _loadedGlassShaderPath, GlassShaderPath, StringComparison.OrdinalIgnoreCase )
		)
		{
			return;
		}

		_glassMaterial = Material.FromShader( GlassShaderPath );
		_loadedGlassShaderPath = GlassShaderPath;
	}

	private void ApplyPhysicalImageState()
	{
		if ( ImageRenderer is null || !ImageRenderer.IsValid )
		{
			SetPhysicalGlassVisible( false );
			RestoreLegacyWorldPanelRenderState();
			return;
		}

		Texture texture = PiPCamera?.OutputTexture;
		OMROpticProfile profile = PiPCamera?.ActiveProfile;
		IWeaponOpticState optic = PiPCamera?.ActiveOpticState;

		bool shouldShow =
			EnablePhysicalImage &&
			PiPCamera is not null &&
			PiPCamera.IsValid &&
			PiPCamera.IsRendering &&
			texture is not null &&
			texture.IsValid &&
			profile is not null &&
			profile.IsValid &&
			profile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture &&
			_imageMaterial is not null &&
			_imageMaterial.IsValid;

		ImageRenderer.Enabled = shouldShow;
		ApplyLegacyWorldPanelVisibility( shouldShow );
		ApplyPhysicalGlassState( shouldShow, profile, optic );

		if ( shouldShow != _lastVisible )
		{
			_lastVisible = shouldShow;

			if ( LogStateChanges )
			{
				Log.Info(
					$"[OMR SCOPE PHYSICAL] Visible:{shouldShow} | " +
					$"Texture:{(texture is not null && texture.IsValid ? texture.Width : 0)} | " +
					$"Aim:{optic?.AimFraction ?? 0f:0.00}"
				);
			}
		}

		if ( !shouldShow )
			return;

		// Re-apply the override after enable-state changes. Renderer attributes are
		// runtime-only, so they are intentionally refreshed every frame below.
		if ( ImageRenderer.MaterialOverride != _imageMaterial )
			ImageRenderer.MaterialOverride = _imageMaterial;

		float aimFraction = optic?.AimFraction ?? 0f;
		float erectBlend = ResolveBlend(
			aimFraction,
			profile.LensErectStart,
			profile.LensErectFull,
			profile.HipPreviewOrientation != profile.AdsPreviewOrientation
		);

		Vector2 hipFlip = OrientationToFlip( profile.HipPreviewOrientation );
		Vector2 adsFlip = OrientationToFlip( profile.AdsPreviewOrientation );

		float midpoint = 4f * erectBlend * (1f - erectBlend);
		float transitionDarkening = midpoint * MathX.Clamp(
			profile.LensErectTransitionDarkening,
			0f,
			0.75f
		);

		ImageRenderer.Attributes.Set( "ScopeTexture", texture );
		ImageRenderer.Attributes.Set( "ScopeHipFlip", hipFlip );
		ImageRenderer.Attributes.Set( "ScopeAdsFlip", adsFlip );
		ImageRenderer.Attributes.Set( "ScopeErectBlend", erectBlend );
		ImageRenderer.Attributes.Set( "ScopeTransitionDarkening", transitionDarkening );

		ApplyWorldLookAttributes( texture );
		ApplyChromaticAberrationAttributes( profile, aimFraction );
		ApplyPhysicalReticleAttributes( profile, aimFraction );
		ApplyPhysicalOpticalMaskAttributes( profile, aimFraction );
	}

	private void ApplyWorldLookAttributes( Texture texture )
	{
		if ( ImageRenderer is null || !ImageRenderer.IsValid )
			return;

		// OMRVisualFoundation is now the single source of truth for scope
		// inheritance and independent scope treatment. Do not gate it again
		// here; the foundation's ScopeInheritWorldLook setting decides whether
		// the PiP mirrors the world or uses its own profile.
		OMRVisualFoundation visual =
			_visualFoundation is not null && _visualFoundation.IsValid
				? _visualFoundation
				: null;

		float blend =
			visual?.EffectiveScopeLookBlend ?? 0f;

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookBlend",
			blend
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookPixelateScale",
			visual?.EffectiveScopePixelateScale ?? 0f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookColorEnabled",
			visual?.EffectiveScopeColorAdjustmentsEnabled == true ? 1f : 0f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookBrightness",
			visual?.EffectiveScopeBrightness ?? 1f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookContrast",
			visual?.EffectiveScopeContrast ?? 1f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookSaturation",
			visual?.EffectiveScopeSaturation ?? 1f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookHueRotate",
			visual?.EffectiveScopeHueRotate ?? 0f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookGrainIntensity",
			visual?.EffectiveScopeFilmGrainIntensity ?? 0f
		);

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookGrainResponse",
			visual?.EffectiveScopeFilmGrainResponse ?? 0.5f
		);

		float resolution =
			PiPCamera is not null && PiPCamera.IsValid && PiPCamera.OutputResolution > 0
				? PiPCamera.OutputResolution
				: texture?.Width ?? 512;

		ImageRenderer.Attributes.Set(
			"ScopeWorldLookResolution",
			MathF.Max( resolution, 1f )
		);

	}

	private void ApplyChromaticAberrationAttributes(
		OMROpticProfile profile,
		float aimFraction
	)
	{
		if ( ImageRenderer is null || !ImageRenderer.IsValid )
			return;

		float strengthUv = 0f;
		if ( EnableChromaticAberration )
		{
			float adsBlend = ResolveBlend(
				aimFraction,
				profile.LensClarityStart,
				profile.LensClarityFull,
				true
			);

			float separationPixels = MathX.Lerp(
				MathF.Max( ChromaticAberrationHipPixels, 0f ),
				MathF.Max( ChromaticAberrationAdsPixels, 0f ),
				adsBlend
			);

			float reference = MathF.Max( ChromaticAberrationReferenceResolution, 1f );
			strengthUv = separationPixels / reference;
		}

		ImageRenderer.Attributes.Set(
			"ScopeChromaticAberrationStrength",
			MathF.Max( strengthUv, 0f )
		);
		ImageRenderer.Attributes.Set(
			"ScopeChromaticAberrationStartRadius",
			MathX.Clamp( ChromaticAberrationStartRadius, 0f, 0.95f )
		);
		ImageRenderer.Attributes.Set(
			"ScopeChromaticAberrationEdgePower",
			MathX.Clamp( ChromaticAberrationEdgePower, 0.25f, 4f )
		);
	}

	private void ApplyPhysicalGlassState(
		bool physicalImageVisible,
		OMROpticProfile profile,
		IWeaponOpticState optic
	)
	{
		bool shouldShow =
			EnablePhysicalGlass &&
			physicalImageVisible &&
			GlassRenderer is not null &&
			GlassRenderer.IsValid &&
			_glassMaterial is not null &&
			_glassMaterial.IsValid;

		SetPhysicalGlassVisible( shouldShow );
		if ( !shouldShow )
			return;

		if ( GlassRenderer.MaterialOverride != _glassMaterial )
			GlassRenderer.MaterialOverride = _glassMaterial;

		float aimFraction = optic?.AimFraction ?? 0f;
		float clarityBlend = ResolveBlend(
			aimFraction,
			profile.LensClarityStart,
			profile.LensClarityFull,
			profile.EnableLensSimulation
		);

		float tintOpacity = 0f;
		float reflectionOpacity = 0f;

		if ( profile.EnableLensSimulation )
		{
			tintOpacity = MathX.Lerp(
				MathX.Clamp( profile.LensHipTintOpacity, 0f, 1f ),
				MathX.Clamp( profile.LensAdsTintOpacity, 0f, 1f ),
				clarityBlend
			) * MathF.Max( GlassTintScale, 0f );

			reflectionOpacity = MathX.Lerp(
				MathX.Clamp( profile.LensHipReflectionOpacity, 0f, 1f ),
				MathX.Clamp( profile.LensAdsReflectionOpacity, 0f, 1f ),
				clarityBlend
			) * MathF.Max( GlassReflectionScale, 0f );
		}

		Color tint = profile.LensTintColor;
		GlassRenderer.Attributes.Set(
			"ScopeGlassTint",
			new Vector4( tint.r, tint.g, tint.b, tint.a )
		);
		GlassRenderer.Attributes.Set( "ScopeGlassTintOpacity", MathX.Clamp( tintOpacity, 0f, 1f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassReflectionOpacity", MathX.Clamp( reflectionOpacity, 0f, 1f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassRefractionStrength", MathX.Clamp( GlassRefractionStrength, 1f, 1.05f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassRoughness", MathX.Clamp( GlassRoughness, 0f, 1f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassBlurAmount", MathX.Clamp( GlassBlurAmount, 0f, 1f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassIridescence", MathX.Clamp( GlassIridescence, 0f, 1000f ) );
		GlassRenderer.Attributes.Set( "ScopeGlassIridescenceScale", MathF.Max( GlassIridescenceScale, 0f ) );
	}

	private void ApplyPhysicalOpticalMaskAttributes(
		OMROpticProfile profile,
		float aimFraction
	)
	{
		if ( ImageRenderer is null || !ImageRenderer.IsValid )
			return;

		float clarityBlend = ResolveBlend(
			aimFraction,
			profile.LensClarityStart,
			profile.LensClarityFull,
			profile.EnableLensSimulation
		);

		float edgeDarkening = 0f;
		if ( profile.EnableLensSimulation )
		{
			edgeDarkening = MathX.Lerp(
				MathX.Clamp( profile.LensHipEdgeDarkening, 0f, 1f ),
				MathX.Clamp( profile.LensAdsEdgeDarkening, 0f, 1f ),
				clarityBlend
			) * MathF.Max( GlassEdgeDarkeningScale, 0f );
		}

		float eyeBoxShadow = 0f;
		Vector2 eyeBoxOffset = Vector2.Zero;

		if ( profile.EnableEyeBoxSimulation && PiPCamera is not null && PiPCamera.IsValid )
		{
			float misalignment =
				1f - MathX.Clamp( PiPCamera.CurrentEyeBoxAlignment, 0f, 1f );

			eyeBoxShadow =
				MathX.Clamp( profile.EyeBoxShadowStrength, 0f, 1f ) *
				misalignment *
				MathF.Max( GlassEyeBoxShadowScale, 0f );

			float shift =
				MathX.Clamp( profile.EyeBoxGradientShiftPercent, 0f, 35f ) / 100f;

			eyeBoxOffset = PiPCamera.CurrentEyeBoxOffset * shift;
		}

		float innerRadius = MathX.Clamp( ScopeShadowInnerRadius, 0f, 1.5f );
		float outerRadius = MathX.Clamp(
			ScopeShadowOuterRadius,
			innerRadius + 0.001f,
			1.5f
		);

		ImageRenderer.Attributes.Set(
			"ScopeLensEdgeDarkening",
			MathX.Clamp( edgeDarkening, 0f, 1f )
		);
		ImageRenderer.Attributes.Set(
			"ScopeEyeBoxShadow",
			MathX.Clamp( eyeBoxShadow, 0f, 1f )
		);
		ImageRenderer.Attributes.Set( "ScopeEyeBoxOffset", eyeBoxOffset );
		ImageRenderer.Attributes.Set(
			"ScopeEyeBoxParallaxScale",
			MathF.Max( ScopeShadowParallaxScale, 0f )
		);
		ImageRenderer.Attributes.Set( "ScopeEyeBoxInnerRadius", innerRadius );
		ImageRenderer.Attributes.Set( "ScopeEyeBoxOuterRadius", outerRadius );
	}

	private void SetPhysicalGlassVisible( bool visible )
	{
		if ( GlassRenderer is null || !GlassRenderer.IsValid )
		{
			_lastGlassVisible = false;
			return;
		}

		GlassRenderer.Enabled = visible;

		if ( visible == _lastGlassVisible )
			return;

		_lastGlassVisible = visible;
		if ( LogStateChanges )
		{
			Log.Info( $"[OMR SCOPE PHYSICAL] GlassVisible:{visible}" );
		}
	}

	private void ApplyPhysicalReticleAttributes( OMROpticProfile profile, float aimFraction )
	{
		float opacity = 0f;
		float outlineOpacity = 0f;
		float thickness = MathF.Max( profile.SimpleReticleThickness, 0.5f );
		float gap = MathF.Max( profile.SimpleReticleGap, 0f );
		float lineLength = MathF.Max( profile.SimpleReticleLineLength, 1f );
		float dotSize = MathF.Max( profile.SimpleReticleDotSize, 0f );
		float outlineWidth = MathF.Max( profile.SimpleReticleOutlineWidth, 0f );
		float dotEnabled = profile.SimpleReticleStyle == OMRScopeReticleStyle.DotCross ? 1f : 0f;

		if ( profile.SimpleReticleStyle == OMRScopeReticleStyle.Duplex )
		{
			// Preserve the exact visual convention used by the legacy
			// OMRScopeReticlePanel so migration does not retune the optic.
			thickness *= 1.75f;
			gap *= 1.5f;
		}

		if ( EnablePhysicalReticle && profile.ShowSimpleReticle )
		{
			float clarityBlend = ResolveBlend(
				aimFraction,
				profile.LensClarityStart,
				profile.LensClarityFull,
				profile.EnableLensSimulation
			);

			opacity = MathX.Lerp(
				MathX.Clamp( profile.ReticleHipOpacity, 0f, 1f ),
				MathX.Clamp( profile.ReticleAdsOpacity, 0f, 1f ),
				clarityBlend
			);

			outlineOpacity = MathX.Clamp(
				profile.SimpleReticleOutlineOpacity,
				0f,
				1f
			) * opacity;
		}

		float reference = MathF.Max( ReticleReferenceResolution, 1f );
		Color reticleColor = profile.SimpleReticleColor;

		ImageRenderer.Attributes.Set(
			"ScopeReticleColor",
			new Vector4( reticleColor.r, reticleColor.g, reticleColor.b, reticleColor.a )
		);
		ImageRenderer.Attributes.Set( "ScopeReticleOpacity", opacity );
		ImageRenderer.Attributes.Set( "ScopeReticleThickness", thickness / reference );
		ImageRenderer.Attributes.Set( "ScopeReticleGap", gap / reference );
		ImageRenderer.Attributes.Set( "ScopeReticleLineLength", lineLength / reference );
		ImageRenderer.Attributes.Set( "ScopeReticleDotSize", dotSize / reference );
		ImageRenderer.Attributes.Set( "ScopeReticleDotEnabled", dotEnabled );
		ImageRenderer.Attributes.Set( "ScopeReticleOutlineWidth", outlineWidth / reference );
		ImageRenderer.Attributes.Set( "ScopeReticleOutlineOpacity", outlineOpacity );
	}

	private void CaptureLegacyWorldPanelRenderState()
	{
		if (
			_legacyRenderStateCaptured ||
			LegacyWorldPanel is null ||
			!LegacyWorldPanel.IsValid
		)
		{
			return;
		}

		// WorldPanel inherits Component. E19 only reads the component's authored
		// PanelSize and transform, so it can remain a valid measurement proxy while
		// disabled. Disabling the renderer is also the most reliable way to prevent
		// coplanar z-fighting with ScopeImagePlane across current S&box builds.
		_legacyEnabled = LegacyWorldPanel.Enabled;
		_legacyRenderStateCaptured = true;
	}

	private void ApplyLegacyWorldPanelVisibility( bool physicalImageVisible )
	{
		if (
			LegacyWorldPanel is null ||
			!LegacyWorldPanel.IsValid ||
			!_legacyRenderStateCaptured
		)
		{
			return;
		}

		bool shouldSuppress = HideLegacyWorldPanelWhenPhysicalImage && physicalImageVisible;
		if ( shouldSuppress == _legacyWasSuppressed )
			return;

		_legacyWasSuppressed = shouldSuppress;
		LegacyWorldPanel.Enabled = shouldSuppress ? false : _legacyEnabled;
	}

	private void RestoreLegacyWorldPanelRenderState()
	{
		if (
			LegacyWorldPanel is null ||
			!LegacyWorldPanel.IsValid ||
			!_legacyRenderStateCaptured ||
			!_legacyWasSuppressed
		)
		{
			return;
		}

		_legacyWasSuppressed = false;
		LegacyWorldPanel.Enabled = _legacyEnabled;
	}

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

		SetPhysicalGlassVisible( false );
		_lastVisible = false;
		RestoreLegacyWorldPanelRenderState();
	}

	private static float ResolveBlend(
		float value,
		float startFraction,
		float fullFraction,
		bool usesTransition
	)
	{
		if ( !usesTransition )
			return 1f;

		float start = MathX.Clamp( startFraction, 0f, 1f );
		float full = MathX.Clamp( fullFraction, start, 1f );
		float clampedValue = MathX.Clamp( value, 0f, 1f );

		if ( full <= start + 0.0001f )
			return clampedValue >= full ? 1f : 0f;

		return MathX.Clamp( (clampedValue - start) / (full - start), 0f, 1f );
	}

	private static Vector2 OrientationToFlip( OMRScopePreviewOrientation orientation )
	{
		return orientation switch
		{
			OMRScopePreviewOrientation.Rotate180 => new Vector2( 1f, 1f ),
			OMRScopePreviewOrientation.FlipHorizontal => new Vector2( 1f, 0f ),
			OMRScopePreviewOrientation.FlipVertical => new Vector2( 0f, 1f ),
			_ => Vector2.Zero
		};
	}
}