Editor/Tool/Subtools/ArchOpeningSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Opening" ), Icon( "sensor_door" ), Group( "05" )]
public sealed class ArchOpeningSubtool( ArchTool owner ) : ArchOpeningPlacingSubtool( owner ) {
	public override ArchSurface[] Surfaces => new[] { ArchSurface.Reveal, ArchSurface.Trim, ArchSurface.Threshold, ArchSurface.DoorLeaf };

	bool withLeaf = true;
	bool slidOpen;
	float? startHeight;
	float? endHeight;
	OpeningFurniture? furniture;
	ArchOpeningPreset custom;

	protected override string Title() => "Opening";

	protected override string Advice() => "Click a wall, or drag a frame.";

	// Only a kind that hangs is ever asked, so anything else keeps the hole exactly as the preset cut it.
	protected override void Adorn( ArchOpening opening ) {
		if ( !opening.Kind.Hangs() ) {
			return;
		}

		opening.Leaf = withLeaf;
		opening.StartOpen = withLeaf && slidOpen;
	}

	protected override ArchOpeningPreset Shape( ArchOpeningPreset preset ) {
		var firstHeight = startHeight ?? preset.SillHeight;
		var secondHeight = endHeight ?? preset.SillHeight + preset.Height;
		var bottom = MathF.Min( firstHeight, secondHeight );
		var top = MathF.Max( firstHeight, secondHeight );

		var shaped = preset.Copy();

		shaped.Height = MathF.Max( ArchGridService.FinestSize, top - bottom );
		shaped.SillHeight = bottom;
		shaped.Furniture = furniture ?? preset.Furniture;

		return shaped;
	}

	protected override ArchOpeningPreset Preset() {
		return custom
			?? Owner.Kit.FindOpening( Owner.ActivePreset )
			?? Owner.Kit.Openings.FirstOrDefault( preset => preset.Kind != OpeningKind.Window );
	}

	// Which unit, what hangs in it, what is bolted over it, then where it sits - picked steps first so the
	// sequence carries itself as far as the two typed heights, which can never say they have been answered.
	// An archway asks neither of the middle two: it hangs nothing and carries nothing, and the kind table says so.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		var chosen = Preset();

		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Opening", chosen?.Name ?? "None", Glyph( chosen ), step => Browse( panel, step ) );

		if ( chosen?.Kind.Hangs() == true ) {
			flow.Step( "Leaf", LeafName, LeafGlyph, PickLeaf );
		}

		if ( chosen?.Kind.Furnishes() == true ) {
			flow.Step( "Fitted", ArchOpeningUi.Name( Fitted( chosen ) ), ArchOpeningUi.Glyph( Fitted( chosen ) ), PickFitted );
		}

		flow.Step( "Placement", PlacementName( chosen ), "straighten", BuildPlacement );
	}

	static string Glyph( ArchOpeningPreset preset ) => preset is null ? "sensor_door" : ArchIcons.OpeningGlyph( preset );

	OpeningFurniture Fitted( ArchOpeningPreset preset ) => furniture ?? preset?.Furniture ?? OpeningFurniture.None;

	string LeafName => withLeaf ? slidOpen ? "Standing open" : "Shut" : "Bare";

	string LeafGlyph => withLeaf ? slidOpen ? "door_open" : "door_front" : "block";

	// Hung, and how it starts, is one three-state answer rather than a toggle governing a second toggle - the
	// generator has only ever read it as one, since a leaf standing open is the only way StartOpen means anything.
	void PickLeaf( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Bare — the hole is left empty", "opt_leaf_none", "block",
			!withLeaf, () => Hang( false, false, step ) );

		grid.Pick( "Shut — a leaf hung in it, closed when the level starts", "opt_leaf", "door_front",
			withLeaf && !slidOpen, () => Hang( true, false, step ) );

		grid.Pick( "Standing open — a leaf hung in it, open when the level starts", "opt_start_open", "door_open",
			withLeaf && slidOpen, () => Hang( true, true, step ) );
	}

	void Hang( bool leaf, bool open, ArchWorkflowStep step ) {
		withLeaf = leaf;
		slidOpen = open;

		step.Chose();
	}

	void PickFitted( ArchWorkflowStep step ) {
		var chosen = Preset();

		using var grid = ArchIconGrid.In( step.Layout );

		ArchOpeningUi.Furniture( grid, chosen.Kind, Fitted( chosen ), value => Fit( value, step ) );
	}

	void Fit( OpeningFurniture fitted, ArchWorkflowStep step ) {
		furniture = fitted;

		step.Chose();
	}

	// What the ghost will read, so the closed row and the released drag agree.
	string PlacementName( ArchOpeningPreset preset ) {
		if ( preset is null ) {
			return "";
		}

		var unit = Shape( preset );

		return $"{unit.Height:0} high at {unit.SillHeight:0}";
	}

	void BuildPlacement( ArchWorkflowStep step ) {
		var chosen = Preset();

		step.Layout.Add( ArchPartUi.OptionalNumber( "Start height", startHeight, chosen?.SillHeight ?? 0f, value => startHeight = value ) );
		step.Layout.Add( ArchPartUi.OptionalNumber( "End height", endHeight, chosen?.SillHeight + chosen?.Height ?? 0f, value => endHeight = value ) );

		ArchOpeningDragUi.DragOptions( step.Layout, placement.Drag );
	}

	// The shelf and the designer hang off the SIDEBAR rather than off the step, because a step's widget is torn
	// down on the next refresh and a modal parented to one would go with it.
	void Browse( ToolSidebarWidget panel, ArchWorkflowStep step ) {
		var shelf = new ArchPresetBrowser<ArchOpeningPreset>( panel, Owner.Kit, Scope( "presets" ), "Openings" ) {
			Chosen = preset => string.Equals( Owner.ActivePreset, preset.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = preset => Use( preset, step ),
			Design = () => Author( panel, step ),
			Exports = "door"
		};

		shelf.DesignButton.Visible = true;
		shelf.DesignButton.ToolTip = "Design an opening preset";
		shelf.Set( Offered() );

		step.Add( shelf );
	}

	List<ArchPresetItem<ArchOpeningPreset>> Offered() {
		return Owner.Offered( preset => preset.Kind != OpeningKind.Window )
			.Select( preset => new ArchPresetItem<ArchOpeningPreset> {
				Value = preset,
				Name = preset.Name,
				Detail = ArchOpeningStage.Describe( preset ),
				Identity = ArchOpeningStage.Identity( preset ),
				Category = preset.Kind.ToString(),
				Badge = preset.Leaf ? null : "no leaf",
				Glyph = ArchIcons.OpeningGlyph( preset ),
				Tags = $"{preset.Kind} {(preset.Leaf ? "leaf" : "open")}",
				Recommended = Owner.Recommends( preset ),
				View = ArchPresetPreview.Elevation,
				Interest = ArchOpeningStage.Interest( preset, Owner.Kit ),
				Recipe = () => ArchOpeningStage.Of( preset, Owner.Kit )
			} )
			.ToList();
	}

	// A retyped preset can no longer honour the last one's leaf state, and custom sizing described a hole
	// this one does not have.
	void Use( ArchOpeningPreset preset, ArchWorkflowStep step ) {
		custom = null;
		Owner.ActivePreset = preset.Name;
		startHeight = null;
		endHeight = null;
		furniture = null;
		withLeaf = preset.Leaf;
		slidOpen = preset.StartOpen;

		step.Chose();
	}

	void Author( Widget panel, ArchWorkflowStep step ) {
		var initial = Preset() is { } preset ? Shape( preset ) : new ArchOpeningPreset();

		new ArchOpeningDesigner( panel, Owner.Kit, initial, false, designed => ApplyDesigned( designed, step ) ).Show();
	}

	void ApplyDesigned( ArchOpeningPreset preset, ArchWorkflowStep step ) {
		custom = ArchOpeningDesigner.Copy( preset );
		Owner.RegisterOpeningPreset( ArchOpeningDesigner.Copy( preset ) );
		Owner.ActivePreset = preset.Name;
		startHeight = null;
		endHeight = null;
		furniture = preset.Furniture;
		withLeaf = preset.Leaf;
		slidOpen = preset.StartOpen;

		ArchPresetPreview.Invalidate( ArchOpeningStage.Identity( preset ) );

		step.Chose();
	}
}