UI/OMRScopeLensPanel.razor
@using System;
@using System.Globalization;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>
	@if ( ShouldRenderLens )
	{
		<div class="lens-window">
			@if ( UsesOrientationTransition )
			{
				<OMRScopeFeedPanel class="lens-feed lens-feed-hip" style="@HipFeedStyle" SourceTexture=@ScopeTexture />
				<OMRScopeFeedPanel class="lens-feed lens-feed-ads" style="@AdsFeedStyle" SourceTexture=@ScopeTexture />
			}
			else
			{
				<OMRScopeFeedPanel class="lens-feed" style="@SingleFeedStyle" SourceTexture=@ScopeTexture />
			}

			<div class="lens-erect-transition" style="@LensErectTransitionStyle"></div>
			<div class="lens-transmission" style="@LensTransmissionStyle"></div>
			<div class="lens-tint" style="@LensTintStyle"></div>
			<div class="lens-vignette" style="@LensVignetteStyle"></div>
			<div class="lens-reflection" style="@LensReflectionStyle"></div>

			@if ( ShowReticle )
			{
				<OMRScopeReticlePanel class="scope-reticle"
					ReticleStyle=@(Profile?.SimpleReticleStyle ?? OMRScopeReticleStyle.FineCross)
					ReticleColor=@(Profile?.SimpleReticleColor ?? Color.Black)
					Thickness=@(Profile?.SimpleReticleThickness ?? 1.5f)
					Gap=@(Profile?.SimpleReticleGap ?? 8f)
					LineLength=@(Profile?.SimpleReticleLineLength ?? 72f)
					DotSize=@(Profile?.SimpleReticleDotSize ?? 3f)
					ReticleOpacity=@ReticleOpacity
					OutlineWidth=@(Profile?.SimpleReticleOutlineWidth ?? 0f)
					OutlineOpacity=@(Profile?.SimpleReticleOutlineOpacity ?? 0f) />
			}
		</div>
	}
</root>

@code
{
	private OMRScopePiPCamera _pipCamera;

	private OMRScopePiPCamera PiPCamera
	{
		get
		{
			if ( _pipCamera is not null && _pipCamera.IsValid )
				return _pipCamera;

			_pipCamera = Scene?
				.GetAllComponents<OMRScopePiPCamera>()
				.FirstOrDefault( x => x is not null && x.IsValid );

			return _pipCamera;
		}
	}

	private IWeaponOpticState OpticState => PiPCamera?.ActiveOpticState;
	private OMROpticProfile Profile => PiPCamera?.ActiveProfile;
	private Texture ScopeTexture => PiPCamera?.OutputTexture;

	private bool ShouldRenderLens =>
		PiPCamera is not null &&
		PiPCamera.IsRendering &&
		OpticState is not null &&
		Profile is not null &&
		Profile.IsValid &&
		Profile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture &&
		ScopeTexture is not null &&
		ScopeTexture.IsValid;

	private bool ShowReticle =>
		Profile?.ShowSimpleReticle ?? true;

	private bool UsesOrientationTransition =>
		Profile is not null &&
		Profile.IsValid &&
		Profile.HipPreviewOrientation != Profile.AdsPreviewOrientation;

	private float LensErectBlend
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !UsesOrientationTransition )
				return 1f;

			float start = MathX.Clamp( Profile.LensErectStart, 0f, 1f );
			float full = MathX.Clamp( Profile.LensErectFull, start, 1f );
			float aim = OpticState?.AimFraction ?? 0f;

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

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

	private float FeedZoomScale =>
		MathX.Clamp( PiPCamera?.PresentationZoomScale ?? 1f, 1f, 20f );

	private static string OrientationTransform(
		OMRScopePreviewOrientation orientation,
		float zoom
	)
	{
		float z = MathX.Clamp( zoom, 1f, 20f );
		string sx = Css( z );
		string sy = sx;

		return orientation switch
		{
			OMRScopePreviewOrientation.Rotate180 => $"scale(-{sx},-{sy})",
			OMRScopePreviewOrientation.FlipHorizontal => $"scale(-{sx},{sy})",
			OMRScopePreviewOrientation.FlipVertical => $"scale({sx},-{sy})",
			_ => $"scale({sx},{sy})"
		};
	}

	private string SingleFeedStyle =>
		$"transform:{OrientationTransform( Profile?.AdsPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";

	private string HipFeedStyle =>
		$"opacity:{Css( 1f - LensErectBlend )};transform:{OrientationTransform( Profile?.HipPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";

	private string AdsFeedStyle =>
		$"opacity:{Css( LensErectBlend )};transform:{OrientationTransform( Profile?.AdsPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";

	private string LensErectTransitionStyle
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !UsesOrientationTransition )
				return "opacity:0;";

			float midpoint = 4f * LensErectBlend * (1f - LensErectBlend);
			float opacity = midpoint * MathX.Clamp( Profile.LensErectTransitionDarkening, 0f, 0.75f );
			return $"background-color:rgba(0,0,0,1);opacity:{Css( opacity )};";
		}
	}

	private float ReticleOpacity =>
		Profile is null || !Profile.IsValid
			? 1f
			: MathX.Lerp(
				MathX.Clamp( Profile.ReticleHipOpacity, 0f, 1f ),
				MathX.Clamp( Profile.ReticleAdsOpacity, 0f, 1f ),
				LensClarityBlend
			);

	private float LensClarityBlend
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
				return 1f;

			float start = MathX.Clamp( Profile.LensClarityStart, 0f, 1f );
			float full = MathX.Clamp( Profile.LensClarityFull, start, 1f );

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

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

	private float OpticalLerp( float hip, float ads ) =>
		MathX.Lerp( hip, ads, LensClarityBlend );

	private string LensTransmissionStyle
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
				return "opacity:0;";

			// Do not put a CSS filter directly on OMRScopeFeedPanel. The panel draws a
			// live GPU render target in OnDraw; forcing that through an off-screen UI
			// filter pass proved unreliable and could collapse the lens to a flat gray
			// surface. Simulate light transmission with a simple black veil here, while
			// blur/saturation/contrast remain authored for the eventual lens shader.
			float brightness = MathX.Clamp(
				OpticalLerp( Profile.LensHipBrightness, Profile.LensAdsBrightness ),
				0f,
				1f
			);

			float opacity = 1f - brightness;
			return $"background-color:rgba(0,0,0,1);opacity:{Css( opacity )};";
		}
	}

	private string LensTintStyle
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
				return "opacity:0;";

			float opacity = OpticalLerp( Profile.LensHipTintOpacity, Profile.LensAdsTintOpacity );
			return $"background-color:{Profile.LensTintColor.Rgba};opacity:{Css( opacity )};";
		}
	}

	private string LensVignetteStyle
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
				return "opacity:0;";

			float baseDarkness = MathX.Clamp(
				OpticalLerp( Profile.LensHipEdgeDarkening, Profile.LensAdsEdgeDarkening ),
				0f,
				1f
			);

			float eyeBoxShadow = 0f;
			float centerX = 50f;
			float centerY = 50f;

			if ( Profile.EnableEyeBoxSimulation && PiPCamera is not null )
			{
				float misalignment = 1f - MathX.Clamp( PiPCamera.CurrentEyeBoxAlignment, 0f, 1f );
				eyeBoxShadow = MathX.Clamp( Profile.EyeBoxShadowStrength, 0f, 1f ) * misalignment;

				Vector2 offset = PiPCamera.CurrentEyeBoxOffset;
				float shift = MathX.Clamp( Profile.EyeBoxGradientShiftPercent, 0f, 35f );
				centerX = MathX.Clamp( 50f + offset.x * shift, 15f, 85f );
				centerY = MathX.Clamp( 50f - offset.y * shift, 15f, 85f );
			}

			// Combine the authored edge darkening with physical off-axis scope shadow
			// without simply adding two opacities past 1. Positioned radial gradients are
			// GPU-rendered by current S&box UI, so the clear exit-pupil region can drift
			// toward the physical optic as the eye moves off-axis.
			float darkness = 1f - (1f - baseDarkness) * (1f - eyeBoxShadow);

			return
				$"background-image:radial-gradient(circle at {Css( centerX )}% {Css( centerY )}%," +
				$"rgba(0,0,0,0),rgba(0,0,0,{Css( darkness )}));";
		}
	}

	private string LensReflectionStyle
	{
		get
		{
			if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
				return "opacity:0;";

			float opacity = OpticalLerp( Profile.LensHipReflectionOpacity, Profile.LensAdsReflectionOpacity );
			return $"opacity:{Css( opacity )};";
		}
	}

	private static string Css( float value ) =>
		value.ToString( "0.###", CultureInfo.InvariantCulture );

	protected override int BuildHash()
	{
		HashCode hash = new();
		Texture texture = ScopeTexture;

		hash.Add( ShouldRenderLens );
		hash.Add( texture?.Index ?? -1 );
		hash.Add( PiPCamera?.PresentationZoomScale ?? 1f );
		hash.Add( PiPCamera?.CurrentEyeBoxAlignment ?? 1f );
		hash.Add( PiPCamera?.CurrentEyeBoxOffset ?? Vector2.Zero );
		hash.Add( Profile?.ResourceName );
		hash.Add( OpticState?.AimFraction ?? 0f );
		hash.Add( Profile?.EnableLensSimulation );
		hash.Add( Profile?.LensClarityStart );
		hash.Add( Profile?.LensClarityFull );
		hash.Add( Profile?.LensHipBlur );
		hash.Add( Profile?.LensAdsBlur );
		hash.Add( Profile?.LensHipBrightness );
		hash.Add( Profile?.LensAdsBrightness );
		hash.Add( Profile?.LensHipSaturation );
		hash.Add( Profile?.LensAdsSaturation );
		hash.Add( Profile?.LensHipContrast );
		hash.Add( Profile?.LensAdsContrast );
		hash.Add( Profile?.LensTintColor );
		hash.Add( Profile?.LensHipTintOpacity );
		hash.Add( Profile?.LensAdsTintOpacity );
		hash.Add( Profile?.LensHipEdgeDarkening );
		hash.Add( Profile?.LensAdsEdgeDarkening );
		hash.Add( Profile?.LensHipReflectionOpacity );
		hash.Add( Profile?.LensAdsReflectionOpacity );
		hash.Add( Profile?.ShowSimpleReticle );
		hash.Add( Profile?.HipPreviewOrientation );
		hash.Add( Profile?.AdsPreviewOrientation );
		hash.Add( Profile?.LensErectStart );
		hash.Add( Profile?.LensErectFull );
		hash.Add( Profile?.LensErectTransitionDarkening );
		hash.Add( Profile?.EnableEyeBoxSimulation );
		hash.Add( Profile?.EyeBoxPerfectAngleDegrees );
		hash.Add( Profile?.EyeBoxFullShadowAngleDegrees );
		hash.Add( Profile?.EyeBoxShadowStrength );
		hash.Add( Profile?.EyeBoxGradientShiftPercent );
		hash.Add( Profile?.ReticleHipOpacity );
		hash.Add( Profile?.ReticleAdsOpacity );
		hash.Add( Profile?.SimpleReticleOutlineWidth );
		hash.Add( Profile?.SimpleReticleOutlineOpacity );
		hash.Add( Profile?.SimpleReticleStyle );
		hash.Add( Profile?.SimpleReticleColor );
		hash.Add( Profile?.SimpleReticleThickness );
		hash.Add( Profile?.SimpleReticleGap );
		hash.Add( Profile?.SimpleReticleLineLength );
		hash.Add( Profile?.SimpleReticleDotSize );

		return hash.ToHashCode();
	}
}