Editor/Designers/ArchOpeningDesigner.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchOpeningDesigner : ArchDesignerDialog {
	readonly ArchKit kit;
	readonly Action<ArchOpeningPreset> applied;
	readonly bool windowsOnly;
	ArchOpeningPreset draft;
	float startHeight;
	float endHeight;
	bool aiming;

	public ArchOpeningDesigner(
		Widget parent,
		ArchKit kit,
		ArchOpeningPreset initial,
		bool windowsOnly,
		Action<ArchOpeningPreset> applied )
		: base( parent, windowsOnly ? "Window Designer" : "Opening Designer", windowsOnly ? "window" : "sensor_door" ) {
		this.kit = kit;
		this.applied = applied;
		this.windowsOnly = windowsOnly;
		draft = Copy( initial );
		startHeight = draft.SillHeight;
		endHeight = draft.SillHeight + draft.Height;

		if ( windowsOnly ) {
			draft.Kind = OpeningKind.Window;
			draft.Glazed = true;
			draft.Leaf = false;
		}

		Assemble(
			kit,
			"Live generated unit",
			"Drag the preview to orbit. The wall cut, trim, sill, leaf, sashes and glass use the production generators.",
			$"Preset assets: Assets/{ArchOpeningCatalog.Directory}" );
	}

	protected override string SaveLabel => "Save Preset";

	// The second viewport, standing in the same slot as the generated unit and shown only while the leaf's art is
	// being aimed. Reading the draft means switching the prefab re-aims without rebuilding anything.
	protected override Widget Aiming( Widget parent ) {
		return new ArchAimedPrefabStage( parent, () => draft.LeafArt, Anchored, LeafRoom );
	}

	// The leaf's own frame, exactly as the scene pass lays it out: the hinge at the foot, x across the opening,
	// y through the wall, z up.
	BBox LeafRoom() {
		var width = draft.Kind == OpeningKind.DoubleDoor ? draft.Width * 0.5f : draft.Width;
		var half = MathF.Max( 0.25f, kit.DoorLeafThickness ) * 0.5f;
		var height = MathF.Abs( endHeight - startHeight ) - kit.DoorUndercut;

		return new BBox( new Vector3( 0f, -half, 0f ),
			new Vector3( MathF.Max( 1f, width ), half, MathF.Max( 1f, height ) ) );
	}

	bool Aimed => aiming && draft.IsHung && draft.LeafArt.Standing;

	// The kind decides which rows exist: a control the generator never reads is left out.
	protected override void Populate() {
		Frame();

		var form = Form.Layout;

		form.Add( Heading( "Identity" ) );
		form.Add( Text( "Name", draft.Name, value => draft.Name = value ) );

		if ( !windowsOnly ) {
			form.Add( Row( "Type", Kinds() ) );
		}

		form.Add( Heading( "Frame" ) );
		form.Add( Number( "Width", draft.Width, value => draft.Width = MathF.Max( ArchGridService.FinestSize, value ) ) );
		form.Add( Number( "Start height", startHeight, value => startHeight = MathF.Max( 0f, value ) ) );
		form.Add( Number( "End height", endHeight, value => endHeight = MathF.Max( 0f, value ) ) );
		form.Add( Gate( "Casing", draft.Cased, value => draft.Cased = value ) );

		if ( draft.Cased ) {
			form.Add( Number( "Casing width", draft.CasingWidth > 0f ? draft.CasingWidth : kit.CasingWidth,
				value => draft.CasingWidth = MathF.Max( 0f, value ) ) );
		}

		if ( draft.Kind.Sills() ) {
			form.Add( Check( "Sill board under it", draft.HasSill, value => draft.HasSill = value ) );
		}

		if ( draft.Kind.Furnishes() || draft.Kind.Hoods().Any() ) {
			using var fittings = ArchIconGrid.In( form );

			ArchOpeningUi.Furniture( fittings, draft.Kind, draft.Furniture, value => {
				draft.Furniture = value;
				Stage.Restage();
			} );

			ArchOpeningUi.Hood( fittings, draft.Kind, draft.Hood, value => {
				draft.Hood = value;
				Stage.Restage();
			} );
		}

		if ( draft.Kind.Hangs() ) {
			form.Add( Heading( "Leaf" ) );
			form.Add( Gate( "Hang a leaf in it", draft.Leaf, value => draft.Leaf = value ) );
		}

		if ( draft.IsHung ) {
			form.Add( Check( "Starts open", draft.StartOpen, value => draft.StartOpen = value ) );
			form.Add( new ArchPrefabWell( Form, () => draft.LeafPrefab, Took ) );

			BuildArt( form );
		}

		if ( draft.Kind.Glazes() ) {
			form.Add( Heading( "Glazing" ) );
			form.Add( Gate( "Glazed", draft.Glazed, value => draft.Glazed = value ) );
		}

		if ( !draft.IsGlazed ) {
			return;
		}

		form.Add( Check( "Automatic layout", draft.AutoLayout, value => draft.AutoLayout = value ) );
		form.Add( Integer( "Lights", draft.Lights, value => draft.Lights = value ) );
		form.Add( Integer( "Sashes", draft.Sashes, value => draft.Sashes = value ) );
		form.Add( Integer( "Pane columns", draft.PanesAcross, value => draft.PanesAcross = value ) );
		form.Add( Integer( "Pane rows", draft.PanesDown, value => draft.PanesDown = value ) );
		form.Add( Check( "Breakable", draft.Breakable, value => draft.Breakable = value ) );
		form.Add( Check( "Already broken", draft.Glass == GlassState.Broken, value => {
			draft.Glass = value ? GlassState.Broken : GlassState.Intact;
		} ) );
	}

	// Art the author drew, standing where the extruded leaf would have. The whole leaf is either one or the other,
	// so dropping a prefab in is what turns the tool's own box off.
	void BuildArt( Layout form ) {
		form.Add( Note( Standing() ) );

		if ( !draft.LeafArt.Standing ) {
			return;
		}

		form.Add( new Button( aiming ? "Back to the unit" : "Aim the art", aiming ? "view_in_ar" : "my_location" ) {
			Clicked = () => {
				aiming = !aiming;

				Repopulate();
			}
		} );

		form.Add( Note( AnchorNote() ) );
		form.Add( new Button( "Clear the anchor", "backspace" ) { Clicked = () => Anchored( Vector3.Zero, Vector3.Zero ) } );
		form.Add( new Button( "Back to the extruded leaf", "delete" ) { Clicked = () => Took( "" ) } );

		using ( var grid = ArchIconGrid.In( form ) ) {
			Turn( grid, "Left — a quarter turn anticlockwise", "leaf_turn_left", "rotate_left", new Angles( 0f, 90f, 0f ) );
			Turn( grid, "Right — a quarter turn clockwise", "leaf_turn_right", "rotate_right", new Angles( 0f, -90f, 0f ) );
			Turn( grid, "Flip — face the other way", "leaf_flip", "flip", new Angles( 0f, 180f, 0f ) );
			Turn( grid, "Tip — a quarter turn onto its face", "leaf_tip", "screen_rotation", new Angles( 90f, 0f, 0f ) );
			Turn( grid, "Roll — a quarter turn on its own axis", "leaf_roll", "rotate_90_degrees_ccw", new Angles( 0f, 0f, 90f ) );

			grid.Pick( "Square — back to the tool's own guess", "leaf_untwist", "restart_alt",
				draft.LeafAnchorTurn == default, () => {
					draft.LeafAnchorTurn = default;

					Repopulate();
				} );
		}

		using ( var sizing = ArchIconGrid.In( form ) ) {
			sizing.Pick( "Modelled — hangs at the size it was drawn", "leaf_modelled", "straighten", !draft.LeafFits,
				() => Sized( false ) );

			sizing.Pick( "Stretched — scaled into the opening it fills", "leaf_stretched", "open_in_full", draft.LeafFits,
				() => Sized( true ) );
		}

		BuildFinish( form );
	}

	void BuildFinish( Layout form ) {
		var offered = ArchPrefabGroups.Offered( draft.LeafPrefab );

		if ( offered.Count == 0 ) {
			return;
		}

		using var grid = ArchIconGrid.In( form );

		grid.Pick( "As modelled — whatever the prefab was authored with", "leaf_group_none", "block",
			string.IsNullOrWhiteSpace( draft.LeafMaterialGroup ), () => Finished( "" ) );

		foreach ( var group in offered ) {
			var standing = group;

			grid.Pick( $"{standing} — every leaf of this preset wears it", $"leaf_group_{standing}".ToLowerInvariant(),
				"palette", string.Equals( draft.LeafMaterialGroup, standing, StringComparison.OrdinalIgnoreCase ),
				() => Finished( standing ) );
		}
	}

	void Turn( ArchIconGrid grid, string tooltip, string slug, string glyph, Angles step ) {
		grid.Pick( tooltip, slug, glyph, false, () => {
			draft.LeafAnchorTurn = (draft.LeafAnchorTurn.ToRotation() * step.ToRotation()).Angles();

			Repopulate();
		} );
	}

	void Sized( bool fits ) {
		draft.LeafFits = fits;

		Repopulate();
	}

	void Finished( string group ) {
		draft.LeafMaterialGroup = group;

		Repopulate();
	}

	// A dropped prefab is a fresh aim: an anchor picked on the last one names a vertex this one does not have.
	void Took( string path ) {
		draft.LeafPrefab = path ?? "";
		draft.LeafAnchor = Vector3.Zero;
		draft.LeafAnchorAlong = Vector3.Zero;
		aiming = draft.LeafArt.Standing;

		Repopulate();
	}

	void Anchored( Vector3 anchor, Vector3 along ) {
		draft.LeafAnchor = anchor;
		draft.LeafAnchorAlong = along;

		Repopulate();
	}

	string Standing() {
		return draft.LeafArt.Standing
			? draft.LeafPrefab
			: "No art — the tool extrudes the leaf. Drop a prefab in to hang your own instead.";
	}

	string AnchorNote() {
		if ( !draft.LeafArt.Anchored ) {
			return "Not anchored — the prefab's own origin lands on the hinge. Click an edge or a corner to change that.";
		}

		var at = $"{draft.LeafAnchor.x:0.##}, {draft.LeafAnchor.y:0.##}, {draft.LeafAnchor.z:0.##}";

		return draft.LeafArt.OnAnEdge ? $"Anchored on an edge at {at}." : $"Anchored on a corner at {at}.";
	}

	// Which viewport the right-hand side shows. The generated unit is the resting state, so leaving the aim behind
	// needs nothing remembered.
	void Frame() {
		if ( Aimed ) {
			Aim( true, "Your prefab, hanging in the opening",
				"Left-click an edge or a corner to anchor it — the aim snaps to real geometry. Right-drag to orbit." );

			return;
		}

		Aim( false, "Live generated unit",
			"Drag the preview to orbit. The wall cut, trim, sill, leaf, sashes and glass use the production generators." );
	}

	protected override ArchStaged Staged() => ArchOpeningStage.Of( Snapshot(), kit );

	protected override string Diagnose() {
		var unit = Snapshot();

		return string.Join( "\n",
			$"preset: {unit.Name}, kind {unit.Kind}",
			$"hole: {unit.Width:0.##} wide, {unit.SillHeight:0.##} to {unit.SillHeight + unit.Height:0.##} ({unit.Height:0.##} tall)",
			$"kind offers: glazing {unit.Kind.Glazes()}, sill {unit.Kind.Sills()}, leaf {unit.Kind.Hangs()}, "
				+ $"furniture {string.Join( "/", unit.Kind.Furnishings() )}",
			$"resolved: glazed {unit.IsGlazed}, silled {unit.IsSilled}, hung {unit.IsHung}, fitted {unit.Fitted}",
			$"casing: {unit.Cased} width {(unit.CasingWidth > 0f ? unit.CasingWidth : kit.CasingWidth):0.##}",
			$"leaf: starts open {unit.StartOpen}",
			$"leaf art: {(unit.LeafArt.Standing ? unit.LeafPrefab : "extruded")}, anchored {unit.LeafArt.Anchored}, "
				+ $"turn {unit.LeafAnchorTurn}, stretched {unit.LeafFits}, "
				+ $"group {(string.IsNullOrWhiteSpace( unit.LeafMaterialGroup ) ? "as modelled" : unit.LeafMaterialGroup)}",
			$"glazing: auto {unit.AutoLayout}, {unit.Lights} lights, {unit.Sashes} sashes, "
				+ $"{unit.PanesAcross} x {unit.PanesDown} panes, {unit.Glass}, breakable {unit.Breakable}" );
	}

	Widget Kinds() {
		var kinds = new ComboBox( Form );

		foreach ( var option in Enum.GetValues<OpeningKind>().Where( value => !value.Glazes() ) ) {
			var captured = option;

			kinds.AddItem( option.ToString(), ArchIcons.OpeningGlyph( new ArchOpeningPreset { Kind = option } ), () => {
				draft.Kind = captured;
				Repopulate();
			}, selected: option == draft.Kind );
		}

		return kinds;
	}

	protected override void Apply() {
		applied?.Invoke( Snapshot() );
		Close();
	}

	protected override void SaveTemplate() {
		var saved = Snapshot();

		if ( !new ArchOpeningCatalog().Save( saved ) ) {
			Log.Warning( "Architecture: the opening preset needs a name before it can be saved." );
			return;
		}

		var index = kit.Openings.FindIndex( preset => string.Equals( preset.Name, saved.Name, StringComparison.OrdinalIgnoreCase ) );

		if ( index >= 0 ) {
			kit.Openings[index] = saved;
		} else {
			kit.Openings.Add( saved );
		}

		applied?.Invoke( Copy( saved ) );
		Close();
	}

	ArchOpeningPreset Snapshot() {
		var result = Copy( draft );
		result.Width = ArchGridService.Least( result.Width, ArchGridService.FinestSize );
		var bottom = MathF.Min( startHeight, endHeight );
		var top = MathF.Max( startHeight, endHeight );
		result.SillHeight = ArchGridService.Least( bottom, 0f );
		result.Height = ArchGridService.Least( top - bottom, ArchGridService.FinestSize );
		result.Lights = Math.Max( 1, result.Lights );
		result.Sashes = Math.Max( 1, result.Sashes );
		result.PanesAcross = Math.Max( 1, result.PanesAcross );
		result.PanesDown = Math.Max( 1, result.PanesDown );

		if ( windowsOnly ) {
			result.Kind = OpeningKind.Window;
		}

		Settle( result );

		return result;
	}

	// Retyped kinds keep stale fields that would read as live in saves and reports.
	static void Settle( ArchOpeningPreset preset ) {
		if ( !preset.Kind.Carries( preset.Furniture ) ) {
			preset.Furniture = OpeningFurniture.None;
		}

		if ( !preset.Kind.Wears( preset.Hood ) ) {
			preset.Hood = OpeningHood.None;
		}

		if ( !preset.Kind.Glazes() ) {
			preset.Glazed = false;
			preset.Breakable = false;
			preset.Glass = GlassState.Intact;
		}

		if ( !preset.Kind.Sills() ) {
			preset.HasSill = false;
		}

		if ( preset.Kind.Hangs() ) {
			return;
		}

		preset.Leaf = false;
		preset.StartOpen = false;
		preset.LeafPrefab = "";
		preset.LeafAnchor = Vector3.Zero;
		preset.LeafAnchorAlong = Vector3.Zero;
		preset.LeafAnchorTurn = default;
		preset.LeafFits = false;
		preset.LeafMaterialGroup = "";
	}

	public static ArchOpeningPreset Copy( ArchOpeningPreset preset ) => preset?.Copy() ?? new ArchOpeningPreset();
}