DiamondPanel.Options.cs
using System;
using Sandbox;
using Sandbox.UI;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	public enum OptionsSlider { None, RestartDelay, Rumble, Master, Music }

	public bool OptionsOpen { get; private set; }
	/// <summary>Any overlay owns input and pauses gameplay and title animation behind it.</summary>
	public bool ModalOpen => TutorialOpen || OptionsOpen || LeaderboardOpen;
	public OptionsSlider DraggingSlider { get; private set; }
	bool optionsWasPaused;
	DiamondBoard optionsBoard;
	float optionsColorPhase;
	float disableFluidKnob;
	SoundHandle optionsPreview;

	const float OptionsWidth = 700, OptionsHeight = 604;
	const float OptionsLabelX = 48, DisableFluidY = 150, RestartDelayY = 226, RumbleY = 302, VolumeDividerY = 351, VolumeHeadingY = 398, MasterY = 468, MusicY = 544;
	// Restart Delay and Controller Rumble have longer labels than the volume sliders, so their shared track start is farther right.
	const float SliderRight = 568, SliderValueX = 618, VolumeTrackLeft = 196, LongLabelTrackLeft = 404;
	const int DisableFluidColor = 0, RestartDelayColor = 2, RumbleColor = 3, MasterColor = 1, MusicColor = 4;
	static Rect OptionsButton( Vector2 size ) => new( LetterboxCenter( size, true ) - 136, Top + 796, 272, 64 );
	// Share the tutorial's screen-pixel fitting, so the close button and scale match it.
	static Rect OptionsFrame( Vector2 size )
	{
		float scale = MathF.Min( size.x * 0.5f / OptionsWidth, size.y * 0.62f / OptionsHeight );
		var extent = new Vector2( OptionsWidth, OptionsHeight ) * scale;
		return new Rect( (size - extent) * 0.5f, extent );
	}
	static Rect DisableFluidRow => new( 32, DisableFluidY - 34, OptionsWidth - 64, 68 );
	static Rect DisableFluidTrack => new( SliderRight - 28, DisableFluidY - 7, 112, 14 );
	static float SliderY( OptionsSlider id ) => id switch
	{
		OptionsSlider.RestartDelay => RestartDelayY, OptionsSlider.Rumble => RumbleY, OptionsSlider.Master => MasterY, _ => MusicY
	};
	static Rect SliderTrack( OptionsSlider id )
	{
		float left = id is OptionsSlider.RestartDelay or OptionsSlider.Rumble ? LongLabelTrackLeft : VolumeTrackLeft;
		return new( left, SliderY( id ) - 4, SliderRight - left, 8 );
	}
	static Rect SliderHit( OptionsSlider id )
	{
		var track = SliderTrack( id );
		return new( track.Left - 24, track.Center.y - 34, track.Width + 48, 68 );
	}

	public void OpenOptions()
	{
		if ( ModalOpen || Board is null ) return;
		optionsBoard = Board;
		optionsWasPaused = Board.Paused;
		if ( !Board.Paused ) Board.TogglePause();
		OptionsOpen = true;
		DraggingSlider = OptionsSlider.None;
		disableFluidKnob = DiamondSettings.Current.DisableFluid ? 1 : 0;
		StopTitleAudio();
	}

	public void CloseOptions()
	{
		if ( !OptionsOpen ) return;
		EndOptionsDrag();
		if ( optionsBoard is not null && optionsBoard.Paused != optionsWasPaused ) optionsBoard.TogglePause();
		OptionsOpen = false;
		optionsBoard = null;
	}

	public void UpdateOptions( float delta, bool backspace )
	{
		if ( !OptionsOpen ) return;
		if ( backspace ) { CloseOptions(); return; }
		delta = MathF.Max( 0, delta );
		optionsColorPhase = (optionsColorPhase + delta * 2.4f) % Palette.Length;
		float target = DiamondSettings.Current.DisableFluid ? 1 : 0;
		disableFluidKnob = target > disableFluidKnob ? MathF.Min( target, disableFluidKnob + delta / 0.14f ) : MathF.Max( target, disableFluidKnob - delta / 0.14f );
		if ( DraggingSlider == OptionsSlider.None ) return;
		// Mouse-up normally ends the drag; losing the active state covers releases we never saw.
		if ( !HasActive ) { EndOptionsDrag(); return; }
		DragOptionsSlider( MousePosition, Box.Rect.Size );
	}

	/// <summary>Opens Options from the letterbox button, or handles a press inside the open overlay.</summary>
	public void HandleOptionsClick( Vector2 mouse, Vector2 size )
	{
		if ( !OptionsOpen )
		{
			if ( TitleScreen || TutorialOpen ) return;
			float scale = MathF.Min( size.x / CanvasWidth, size.y / CanvasHeight );
			if ( scale <= 0 ) return;
			var point = (mouse - (size - new Vector2( CanvasWidth, CanvasHeight ) * scale) * 0.5f) / scale;
			if ( OptionsButton( size ).IsInside( point ) ) { PlayClickSound(); OpenOptions(); }
			return;
		}
		var frame = OptionsFrame( size );
		if ( TutorialClose( frame, OptionsWidth ).IsInside( mouse ) ) { PlayClickSound(); CloseOptions(); return; }
		if ( !frame.IsInside( mouse ) ) { CloseOptions(); return; }
		var local = OptionsLocal( mouse, frame );
		if ( DisableFluidRow.IsInside( local ) )
		{
			PlayClickSound();
			DiamondSettings.Current.DisableFluid = !DiamondSettings.Current.DisableFluid;
			DiamondSettings.Save();
			return;
		}
		DraggingSlider = SliderHit( OptionsSlider.RestartDelay ).IsInside( local ) ? OptionsSlider.RestartDelay
			: SliderHit( OptionsSlider.Rumble ).IsInside( local ) ? OptionsSlider.Rumble
			: SliderHit( OptionsSlider.Master ).IsInside( local ) ? OptionsSlider.Master
			: SliderHit( OptionsSlider.Music ).IsInside( local ) ? OptionsSlider.Music : OptionsSlider.None;
		DragOptionsSlider( mouse, size );
	}

	public void DragOptionsSlider( Vector2 mouse, Vector2 size )
	{
		if ( DraggingSlider == OptionsSlider.None ) return;
		var track = SliderTrack( DraggingSlider );
		float value = Math.Clamp( (OptionsLocal( mouse, OptionsFrame( size ) ).x - track.Left) / track.Width, 0, 1 );
		// The delay setter snaps to its 0.05-second steps.
		if ( DraggingSlider == OptionsSlider.RestartDelay ) DiamondSettings.Current.RestartDelay = value;
		else if ( DraggingSlider == OptionsSlider.Rumble ) DiamondSettings.Current.ControllerRumble = MathF.Round( value * 100 ) / 100;
		else if ( DraggingSlider == OptionsSlider.Master ) DiamondSettings.Current.MasterVolume = MathF.Round( value * 100 ) / 100;
		else DiamondSettings.Current.MusicVolume = MathF.Round( value * 100 ) / 100;
	}

	/// <summary>Finish a slider drag, saving it and previewing the new effects or rumble level.</summary>
	public void EndOptionsDrag()
	{
		if ( DraggingSlider == OptionsSlider.None ) return;
		bool master = DraggingSlider == OptionsSlider.Master;
		bool rumble = DraggingSlider == OptionsSlider.Rumble;
		DraggingSlider = OptionsSlider.None;
		DiamondSettings.Save();
		// Preview on a real cue: the hit of a slam that cracks a diamond.
		if ( rumble ) DiamondHaptics.DamageHit( 0.7f );
		if ( !master || IsMuted ) return;
		optionsPreview?.Stop();
		optionsPreview = Sound.Play( "sfx/diamond_impact.sound" );
		if ( optionsPreview is null ) return;
		optionsPreview.SpacialBlend = 0;
		optionsPreview.Volume *= 0.7f * DiamondSettings.EffectsGain;
	}

	protected override void OnMouseUp( MousePanelEvent e )
	{
		if ( e.Button != "mouseleft" ) return;
		EndOptionsDrag();
		leaderboardDragging = false;
	}

	static Vector2 OptionsLocal( Vector2 mouse, Rect frame ) => (mouse - frame.Position) / (frame.Width / OptionsWidth);

	void DrawOptionsButton( Painter p, float scale )
	{
		var size = Box.Rect.Size;
		var mouse = (MousePosition - (size - new Vector2( CanvasWidth, CanvasHeight ) * scale) * 0.5f) / scale;
		DrawTitleButton( p, OptionsButton( size ), "OPTIONS", mouse, scale, fontSize: 26, brightness: 0.7f );
	}

	void DrawOptions( Painter p )
	{
		if ( !OptionsOpen ) return;
		using var scope = p.Scope();
		p.Fill = Color.Black.WithAlpha( 0.32f );
		p.Stroke = Stroke.None;
		p.Rect( p.Bounds );
		var frame = OptionsFrame( p.Bounds.Size );
		p.Fill = Color.Parse( "#0d151e" ).Value;
		p.Rect( frame );
		p.Clip( frame );
		float scale = frame.Width / OptionsWidth;
		if ( scale <= 0 ) return;
		DrawModalClose( p, frame, OptionsWidth );
		var mouse = OptionsLocal( MousePosition, frame );
		p.Translate( frame.Position );
		p.Scale( scale );
		var settings = DiamondSettings.Current;
		DrawScoreNumber( p, "OPTIONS", new Vector2( OptionsWidth * 0.5f, 60 ), 52, ScoreColor( optionsColorPhase ) );
		DrawDisableFluidToggle( p, mouse );
		DrawOptionsSlider( p, "RESTART DELAY", settings.RestartDelay, DiamondSettings.FormatSeconds( settings.RestartDelay ),
			RestartDelayColor, OptionsSlider.RestartDelay, mouse );
		DrawOptionsSlider( p, "CONTROLLER RUMBLE", settings.ControllerRumble, $"{MathF.Round( settings.ControllerRumble * 100 )}",
			RumbleColor, OptionsSlider.Rumble, mouse );
		p.Fill = Ink.WithAlpha( 0.07f );
		p.Stroke = Stroke.None;
		p.Rect( new Rect( 32, VolumeDividerY - 1, OptionsWidth - 64, 2 ) );
		DrawScoreNumber( p, "VOLUME", new Vector2( OptionsLabelX, VolumeHeadingY ), 34, Muted * 0.8f, 0 );
		DrawOptionsSlider( p, "MASTER", settings.MasterVolume, $"{MathF.Round( settings.MasterVolume * 100 )}", MasterColor, OptionsSlider.Master, mouse );
		DrawOptionsSlider( p, "MUSIC", settings.MusicVolume, $"{MathF.Round( settings.MusicVolume * 100 )}", MusicColor, OptionsSlider.Music, mouse );
	}

	void DrawDisableFluidToggle( Painter p, Vector2 mouse )
	{
		bool hover = DraggingSlider == OptionsSlider.None && DisableFluidRow.IsInside( mouse );
		bool pressed = hover && HasActive;
		p.Stroke = Stroke.None;
		if ( hover )
		{
			p.Fill = Ink.WithAlpha( pressed ? 0.07f : 0.04f );
			p.Rect( DisableFluidRow );
		}
		DrawScoreNumber( p, "DISABLE FLUID", new Vector2( OptionsLabelX, DisableFluidY ), 30, hover ? Ink : Ink * 0.8f, 0 );
		var track = DisableFluidTrack;
		float t = disableFluidKnob * disableFluidKnob * (3 - 2 * disableFluidKnob);
		float x = track.Left + 17 + (track.Width - 34) * t;
		p.Fill = Ink.WithAlpha( hover ? 0.1f : 0.06f );
		p.Rect( track );
		DrawTrackFill( p, track, x, Palette[DisableFluidColor], t );
		DrawOptionsHandle( p, new Vector2( x, DisableFluidY ), DisableFluidColor, hover ? 0.68f : 0.62f, t );
	}

	void DrawOptionsSlider( Painter p, string label, float value, string valueText, int color, OptionsSlider id, Vector2 mouse, float labelSize = 26 )
	{
		bool active = DraggingSlider == id;
		bool hover = active || DraggingSlider == OptionsSlider.None && SliderHit( id ).IsInside( mouse );
		float y = SliderY( id );
		DrawScoreNumber( p, label, new Vector2( OptionsLabelX, y ), labelSize, hover ? Ink : Ink * 0.8f, 0 );
		var track = SliderTrack( id );
		p.Stroke = Stroke.None;
		p.Fill = Ink.WithAlpha( hover ? 0.1f : 0.06f );
		p.Rect( track );
		// Faint quarter notches, cut as small diamonds into the empty track.
		for ( int i = 1; i < 4; i++ )
		{
			var notch = new Vector2( track.Left + track.Width * i / 4, y );
			p.Fill = Ink.WithAlpha( 0.14f );
			p.Polygon( [notch + new Vector2( 0, -7 ), notch + new Vector2( 4, 0 ), notch + new Vector2( 0, 7 ), notch + new Vector2( -4, 0 )] );
		}
		float x = track.Left + track.Width * value;
		DrawTrackFill( p, track, x, Palette[color], 1 );
		DrawOptionsHandle( p, new Vector2( x, y ), color, active ? 0.72f : hover ? 0.68f : 0.62f, 1 );
		var valueCenter = new Vector2( SliderValueX, y );
		if ( valueText.EndsWith( 's' ) ) DrawSecondsValue( p, valueText[..^1], valueCenter, 26, hover ? Ink : Muted );
		else DrawScoreNumber( p, valueText, valueCenter, 26, hover ? Ink : Muted );
	}

	/// <summary>Score-font digits followed by a smaller score-font S, sharing their baseline so it reads as a unit, not a 5.</summary>
	static void DrawSecondsValue( Painter p, string number, Vector2 center, float fontSize, Color color )
	{
		const float UnitScale = 0.68f, CapHeight = 0.52f;
		float unitSize = fontSize * UnitScale, gap = fontSize * 0.1f;
		float numberWidth = DiamondScoreEffects.MeasureNumber( number ).x * fontSize / DiamondScoreEffects.PickupFontSize;
		float unitWidth = DiamondScoreEffects.MeasureNumber( "S" ).x * unitSize / DiamondScoreEffects.PickupFontSize;
		float left = center.x - (numberWidth + gap + unitWidth) * 0.5f;
		DrawScoreNumber( p, number, new Vector2( left, center.y ), fontSize, color, 0 );
		// Caps-only glyphs sit centered on the line, so drop the smaller S by half the cap-height difference.
		DrawScoreNumber( p, "S", new Vector2( left + numberWidth + gap, center.y + (fontSize - unitSize) * CapHeight * 0.5f ), unitSize, color, 0 );
	}

	static void DrawTrackFill( Painter p, Rect track, float x, Color color, float opacity )
	{
		if ( x <= track.Left || opacity <= 0 ) return;
		p.Stroke = Stroke.None;
		p.Fill = color.WithAlpha( 0.12f * opacity );
		p.Rect( new Rect( track.Left, track.Center.y - 8, x - track.Left, 16 ) );
		p.Fill = color.WithAlpha( 0.8f * opacity );
		p.Rect( new Rect( track.Left, track.Top, x - track.Left, track.Height ) );
	}

	/// <summary>A small in-game gem with its bonded outline; <paramref name="lit"/> fades it in from dull steel.</summary>
	void DrawOptionsHandle( Painter p, Vector2 center, int color, float scale, float lit )
	{
		if ( lit < 1 ) DrawSteelHandle( p, center, scale, 1 - lit );
		if ( lit <= 0 ) return;
		if ( lit >= 1 ) DrawDiamond( p, center, color, false, scale );
		else DrawDiamondVector( p, center, color, false, scale, lit );
		var cells = new DiamondBoard.Diamond[] { new( 0, 0, color ) };
		DrawGroupOutlines( p, cells, DiamondMatching.FindGroups( cells ),
			center - new Vector2( DiamondBoard.LaneX( 0 ) * scale, 0 ), scale, lit );
	}

	static void DrawSteelHandle( Painter p, Vector2 center, float scale, float opacity )
	{
		using var scope = p.Scope();
		p.Translate( center );
		p.Scale( scale );
		float w = DiamondBoard.Width * 0.5f, h = DiamondBoard.Height * 0.5f;
		Vector2 top = new( 0, -h ), right = new( w, 0 ), bottom = new( 0, h ), left = new( -w, 0 ), middle = Vector2.Zero;
		var steel = FloorInk * 0.75f;
		Facet( p, steel, 0.32f, opacity, false, [top, middle, left] );
		Facet( p, steel, 0.02f, opacity, false, [top, right, middle] );
		Facet( p, steel, -0.42f, opacity, false, [middle, right, bottom] );
		Facet( p, steel, -0.12f, opacity, false, [left, middle, bottom] );
		DrawGemStroke( p, [top, right, bottom, left], FloorInk.WithAlpha( opacity ), 2, true );
	}
}