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

namespace Sunless.Architecture;

// THE ONE CABINET DESIGN. A carcass is either extruded by the tool or supplied as art, and both are authored here as
// a sequence rather than as two windows - the first step is the fork and every step after it belongs to the half it
// answered. Save writes back to .cabinet.json, so what is tuned here becomes the shared type.
//
// The right-hand side swaps with the sequence: a generated type is judged as a short run through the production
// generators, and art is judged in the module it will actually stand in, aimed at the same way a front is.
public sealed class ArchCabinetDesigner : ArchDesignerDialog {
	readonly ArchTool tool;
	readonly Action<ArchCabinetType> applied;
	readonly ArchCabinetType draft;
	readonly ArchKit staging;

	const string Flow = "cabinet.designer";

	CabinetSlot slot = CabinetSlot.Module;
	ArchAimedPrefabStage aim;
	bool named;

	public ArchCabinetDesigner( Widget parent, ArchTool tool, ArchCabinetType initial, Action<ArchCabinetType> applied )
		: base( parent, "Cabinet Designer", "kitchen" ) {
		this.tool = tool;
		this.applied = applied;

		draft = ArchCabinetTypes.Copy( initial ?? ArchCabinetTypes.Find( tool?.Kit, "" ) );
		named = !string.IsNullOrWhiteSpace( draft.Name );
		staging = ArchCabinetStage.Staging( tool?.Kit, draft );

		// Opening the designer starts the sequence over, or a second design resumes on whichever step the last one
		// was left on - and the statics outlive a hotload.
		ArchWorkflow.Restart( Flow );

		Assemble(
			staging,
			"Live generated run",
			$"Drag the preview to orbit. {ArchCabinetStage.Modules} modules of the type you are designing, through the "
				+ "same carcass, front and worktop generators the plan builds with.",
			$"Cabinet types: Assets/{ArchCabinetCatalog.Directory}" );
	}

	protected override string SaveLabel => "Save Type";

	protected override ArchStaged Staged() => ArchCabinetStage.Of( draft );

	// The second viewport, standing in the same slot as the generated stage and shown only while a prefab is being
	// aimed. It reads whichever slot's art the Art step is holding, so switching slot re-aims without a rebuild.
	protected override Widget Aiming( Widget parent ) {
		aim = new ArchAimedPrefabStage( parent, () => Held(), Anchored, Room );

		return aim;
	}

	BBox Room() {
		return ArchAimedPrefabMount.Of( MathF.Max( 4f, draft.Depth ), MathF.Max( 6f, draft.Width ),
			MathF.Max( 8f, draft.Plinth + draft.Carcass + draft.Counter ) );
	}

	protected override void Populate() {
		Aim( false, "Live generated run", "Drag the preview to orbit." );

		var panel = new ToolSidebarWidget();

		Form.Layout.Add( panel );

		using var flow = ArchWorkflow.In( panel, Flow, Repopulate );

		flow.Step( "Source", draft.Prefabbed ? "Prefab" : "Procedural", draft.Prefabbed ? "category" : "grid_4x4", PickSource );

		flow.Step( "Cabinet", draft.Tier.ToString(), ArchCabinetUi.Glyph( draft.Tier ), PickTier );

		if ( draft.Prefabbed ) {
			flow.Step( "Art", ArtName, "widgets", PickArt );
			flow.Step( "Anchor", AnchorName, "my_location", BuildAnchor );
			flow.Step( "Finish", FinishName, "palette", BuildFinish );
			flow.Step( "Module", SizesName, "straighten", BuildModule );
		} else {
			flow.Step( "Front", ArchCabinetUi.Named( draft.On( ArchCabinetMounts.Front ) ), "border_all", PickFront );
			flow.Step( "Handles", draft.Handles ? draft.Grip.ToString() : "None", "drag_indicator", PickGrip );
			flow.Step( "Inside", InsideName, "inventory_2", BuildInside );
			flow.Step( "Sizes", SizesName, "straighten", BuildSizes );
			flow.Step( "Top", TopName, "countertops", BuildTop );
		}

		flow.Step( "Identity", string.IsNullOrWhiteSpace( draft.Name ) ? "Unnamed" : draft.Name, "badge", BuildIdentity );
	}

	void PickSource( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Procedural — the tool extrudes the box, the plinth, the doors and the top", "cabinet_made",
			"grid_4x4", !draft.Prefabbed, () => Answer( () => draft.Prefabbed = false, step ) );

		grid.Pick( "Prefab — your own art, placed by the same rules a generated run is", "cabinet_art",
			"category", draft.Prefabbed, () => Answer( Adopt, step ) );
	}

	// The art SURVIVES a trip back through Procedural, so an author comparing the two halves does not lose what they
	// dropped. A module slot is opened on the way in, or the Art step has nothing to hang the first prefab on.
	void Adopt() {
		draft.Prefabbed = true;
		draft.Carcasses ??= new List<ArchCabinetCarcass>();

		if ( !draft.Carcasses.Any( carcass => carcass.Slot == CabinetSlot.Module ) ) {
			draft.Carcasses.Add( new ArchCabinetCarcass { Slot = CabinetSlot.Module } );
		}

		slot = CabinetSlot.Module;
	}

	void PickTier( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Base — on the floor, counter over it", "designed_tier_base", "table_restaurant",
			draft.Tier == CabinetTier.Base, () => Answer( () => draft.Tier = CabinetTier.Base, step ) );

		grid.Pick( "Hanging — off the counter below, or the standard datum", "designed_tier_hanging", "shelves",
			draft.Tier == CabinetTier.Hanging, () => Answer( () => draft.Tier = CabinetTier.Hanging, step ) );

		grid.Pick( "Tall — floor to whatever covers it", "designed_tier_tall", "door_sliding",
			draft.Tier == CabinetTier.Tall, () => Answer( () => draft.Tier = CabinetTier.Tall, step ) );
	}

	string ArtName {
		get {
			var standing = draft.Carcasses.Count( carcass => carcass.Standing );

			return standing == 0 ? "Nothing dropped" : $"{standing} of {Enum.GetValues<CabinetSlot>().Length} slots";
		}
	}

	// One slot at a time, because a run stands a different model in its corner from the one it stands in its body
	// and both are the same question asked twice.
	void PickArt( ArchWorkflowStep step ) {
		using ( var grid = ArchIconGrid.In( step.Layout ) ) {
			Slot( grid, CabinetSlot.Module, "Module — the carcass in the body of the run", "view_module" );
			Slot( grid, CabinetSlot.Corner, "Corner — the unit filling the square where two runs meet", "join_inner" );
			Slot( grid, CabinetSlot.End, "End — the unit on a run that met no other run", "last_page" );
			Slot( grid, CabinetSlot.Filler, "Filler — the make-up carcass a run that would not divide evenly takes", "width_normal" );
		}

		step.Layout.Add( new ArchPrefabWell( Form, () => Held()?.Prefab ?? "", Took ) );
		step.Layout.Add( Note( Standing() ) );
	}

	void Slot( ArchIconGrid grid, CabinetSlot which, string tooltip, string glyph ) {
		var dressed = draft.In( which ) is { Standing: true } ? " ✓" : "";

		grid.Pick( tooltip + dressed, $"cabinet_slot_{which}".ToLowerInvariant(), glyph, slot == which, () => {
			slot = which;

			Repopulate();
		} );
	}

	string Standing() {
		if ( Held() is not { Standing: true } carcass ) {
			return $"Nothing in the {slot} slot — it falls back to the module's own art.";
		}

		return $"{slot}: {carcass.Prefab}";
	}

	// The art for the slot being edited, made on the WRITE rather than the read, or merely opening the step would
	// file an empty slot the run then has to fall back out of.
	ArchCabinetCarcass Held() {
		return draft.Carcasses?.FirstOrDefault( carcass => carcass.Slot == slot );
	}

	ArchCabinetCarcass Own() {
		if ( Held() is { } standing ) {
			return standing;
		}

		var made = new ArchCabinetCarcass { Slot = slot };

		draft.Carcasses ??= new List<ArchCabinetCarcass>();
		draft.Carcasses.Add( made );

		return made;
	}

	void Took( string path ) {
		var carcass = Own();

		carcass.Prefab = path ?? "";
		carcass.Anchor = Vector3.Zero;
		carcass.AnchorAlong = Vector3.Zero;

		if ( !named ) {
			draft.Name = ArchCabinetFronts.Titled( carcass.Prefab );
		}

		Repopulate();
	}

	string AnchorName {
		get {
			if ( Held() is not { Standing: true } carcass ) {
				return "No art";
			}

			return carcass.Anchor == Vector3.Zero && carcass.AnchorAlong == Vector3.Zero ? "Own origin" : "Anchored";
		}
	}

	// THE STEP THAT SWAPS THE VIEWPORT. A closed step never builds, so the generated stage comes back by itself the
	// moment the author moves on - nothing has to remember to put it back.
	void BuildAnchor( ArchWorkflowStep step ) {
		if ( Held() is not { Standing: true } carcass ) {
			step.Layout.Add( Note( "Drop a prefab into a slot before anchoring it." ) );

			return;
		}

		Aim( true, "Your prefab, standing in its module",
			"Left-click an edge or a corner to anchor it — the aim snaps to real geometry. Right-drag to orbit." );

		step.Layout.Add( Note( Aimed( carcass ) ) );
		step.Layout.Add( new Button( "Clear the anchor", "backspace" ) { Clicked = () => Anchored( Vector3.Zero, Vector3.Zero ) } );

		using ( var grid = ArchIconGrid.In( step.Layout ) ) {
			Turn( grid, carcass, "Left — a quarter turn anticlockwise", "cabinet_turn_left", "rotate_left", new Angles( 0f, 90f, 0f ) );
			Turn( grid, carcass, "Right — a quarter turn clockwise", "cabinet_turn_right", "rotate_right", new Angles( 0f, -90f, 0f ) );
			Turn( grid, carcass, "Flip — face the other way", "cabinet_flip", "flip", new Angles( 0f, 180f, 0f ) );
			Turn( grid, carcass, "Tip — a quarter turn onto its face", "cabinet_tip", "screen_rotation", new Angles( 90f, 0f, 0f ) );
			Turn( grid, carcass, "Roll — a quarter turn on its own axis", "cabinet_roll", "rotate_90_degrees_ccw", new Angles( 0f, 0f, 90f ) );

			grid.Pick( "Square — back to the tool's own guess", "cabinet_untwist", "restart_alt",
				carcass.AnchorTurn == default, () => {
					carcass.AnchorTurn = default;

					Repopulate();
				} );
		}

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

		sizing.Pick( "Modelled — stands at the size it was drawn", "cabinet_modelled", "straighten", !carcass.Fits, () => {
			carcass.Fits = false;

			Repopulate();
		} );

		sizing.Pick( "Stretched — scaled into the module it fills", "cabinet_stretched", "open_in_full", carcass.Fits, () => {
			carcass.Fits = true;

			Repopulate();
		} );
	}

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

			Repopulate();
		} );
	}

	static string Aimed( ArchCabinetCarcass carcass ) {
		if ( carcass.Anchor == Vector3.Zero && carcass.AnchorAlong == Vector3.Zero ) {
			return "Not anchored — the prefab's own origin lands on the mount. Click an edge or a corner to change that.";
		}

		return carcass.AnchorAlong.Length > 0.01f
			? $"Anchored on an edge at {carcass.Anchor.x:0.##}, {carcass.Anchor.y:0.##}, {carcass.Anchor.z:0.##}."
			: $"Anchored on a corner at {carcass.Anchor.x:0.##}, {carcass.Anchor.y:0.##}, {carcass.Anchor.z:0.##}.";
	}

	void Anchored( Vector3 anchor, Vector3 along ) {
		var carcass = Own();

		carcass.Anchor = anchor;
		carcass.AnchorAlong = along;

		Repopulate();
	}

	string FinishName => string.IsNullOrWhiteSpace( draft.MaterialGroup ) ? "As modelled" : draft.MaterialGroup;

	// The groups the ART itself offers, read off the model rather than typed - a name that is not on the model is a
	// group that silently does nothing.
	void BuildFinish( ArchWorkflowStep step ) {
		var offered = Groups();

		if ( offered.Count == 0 ) {
			step.Layout.Add( Note( "This prefab's model carries no material groups." ) );

			return;
		}

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

		grid.Pick( "As modelled — whatever the prefab was authored with", "cabinet_group_none", "block",
			string.IsNullOrWhiteSpace( draft.MaterialGroup ), () => Answer( () => draft.MaterialGroup = "", step ) );

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

			grid.Pick( $"{standing} — every carcass in the run wears it", $"cabinet_group_{standing}".ToLowerInvariant(),
				"palette", string.Equals( draft.MaterialGroup, standing, StringComparison.OrdinalIgnoreCase ),
				() => Answer( () => draft.MaterialGroup = standing, step ) );
		}
	}

	List<string> Groups() => ArchPrefabGroups.Offered( draft.In( CabinetSlot.Module )?.Prefab );

	void PickFront( ArchWorkflowStep step ) {
		ArchCabinetUi.Front( step.Layout, Fronted(), step.Chose, Stage.Restage );
	}

	ArchCabinetFitting Fronted() {
		if ( draft.On( ArchCabinetMounts.Front ) is { } standing ) {
			return standing;
		}

		var made = new ArchCabinetFitting { Mount = ArchCabinetMounts.Front, Front = CabinetFront.Slab };

		draft.Fittings ??= new List<ArchCabinetFitting>();
		draft.Fittings.Add( made );

		return made;
	}

	void PickGrip( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "None — no ironmongery at all", "cabinet_grip_none", "block", !draft.Handles,
			() => Answer( () => draft.Handles = false, step ) );

		Grip( grid, step, CabinetGrip.Bar, "Bar — a rail on two stems", "drag_indicator" );
		Grip( grid, step, CabinetGrip.Knob, "Knob — a single stub on the stile", "radio_button_checked" );
		Grip( grid, step, CabinetGrip.Cup, "Cup — a shallow pull under a lip", "download" );
	}

	void Grip( ArchIconGrid grid, ArchWorkflowStep step, CabinetGrip style, string tooltip, string glyph ) {
		grid.Pick( tooltip, $"cabinet_grip_{style}".ToLowerInvariant(), glyph, draft.Handles && draft.Grip == style,
			() => Answer( () => {
				draft.Handles = true;
				draft.Grip = style;
			}, step ) );
	}

	string InsideName => ArchPartUi.Listed( (draft.Interior, "interior"), (draft.Shelves > 0, "shelves"),
		(draft.DrawerRows > 0, "drawers") );

	void BuildInside( ArchWorkflowStep step ) {
		step.Layout.Add( Check( "Interior — real panels and shelves behind the doors", draft.Interior,
			value => draft.Interior = value ) );

		step.Layout.Add( Integer( "Shelves", draft.Shelves, value => draft.Shelves = Math.Max( 0, value ) ) );
		step.Layout.Add( Integer( "Drawer rows", draft.DrawerRows, value => draft.DrawerRows = Math.Max( 0, value ) ) );
	}

	string SizesName => $"{draft.Width:0} × {draft.Depth:0}";

	void BuildSizes( ArchWorkflowStep step ) {
		BuildModule( step );

		step.Layout.Add( Number( "Plinth", draft.PlinthHeight, value => draft.PlinthHeight = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Plinth recess", draft.PlinthRecess, value => draft.PlinthRecess = MathF.Max( 0f, value ) ) );
	}

	void BuildModule( ArchWorkflowStep step ) {
		step.Layout.Add( Number( "Module width", draft.Width, value => draft.Width = MathF.Max( 6f, value ) ) );
		step.Layout.Add( Number( "Depth", draft.Depth, value => draft.Depth = MathF.Max( 4f, value ) ) );
		step.Layout.Add( Number( "Carcass height", draft.Height, value => draft.Height = MathF.Max( 4f, value ) ) );

		if ( draft.Tier == CabinetTier.Hanging ) {
			step.Layout.Add( Number( "Hangs at", draft.Datum, value => draft.Datum = MathF.Max( 0f, value ) ) );
		}
	}

	string TopName {
		get {
			if ( draft.CorniceHeight > 0.01f && draft.Counter <= 0.01f ) {
				return $"Crown {draft.CorniceHeight:0.##}";
			}

			return draft.Counter > 0.01f ? $"{draft.Counter:0.##} thick" : "Bare";
		}
	}

	void BuildTop( ArchWorkflowStep step ) {
		step.Layout.Add( Number( "Worktop", draft.TopThickness, value => draft.TopThickness = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Past the front", draft.TopOversail, value => draft.TopOversail = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Past the ends", draft.TopEndOversail, value => draft.TopEndOversail = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Past the back", draft.TopBackOversail, value => draft.TopBackOversail = MathF.Max( 0f, value ) ) );

		step.Layout.Add( Heading( "Cornice" ) );
		step.Layout.Add( Note( "A run carries a crown or a counter, never both — a worktop over 0 takes it over." ) );
		step.Layout.Add( Number( "Crown", draft.CorniceHeight, value => draft.CorniceHeight = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Projection", draft.CorniceProjection, value => draft.CorniceProjection = MathF.Max( 0f, value ) ) );
		step.Layout.Add( Number( "Past the ends", draft.CorniceOversail, value => draft.CorniceOversail = MathF.Max( 0f, value ) ) );
	}

	void BuildIdentity( ArchWorkflowStep step ) {
		step.Layout.Add( Text( "Name", draft.Name, value => {
			draft.Name = value;
			named = !string.IsNullOrWhiteSpace( value );
		} ) );

		step.Layout.Add( Text( "Detail", draft.Detail, value => draft.Detail = value ) );
	}

	// Mutate, then advance: advancing IS the refresh, so calling one as well tears the panel down twice.
	void Answer( Action change, ArchWorkflowStep step ) {
		change();

		Stage.Restage();

		step.Chose();
	}

	protected override string Diagnose() {
		return string.Join( "\n",
			$"type: {draft.Name} ({draft.Tier}), {(draft.Prefabbed ? "prefab" : "procedural")}",
			$"module: {draft.Width:0.##} x {draft.Depth:0.##}, carcass {draft.Carcass:0.##}",
			$"plinth: {draft.Plinth:0.##} recessed {draft.PlinthRecess:0.##}",
			$"top: {draft.Counter:0.##} thick, oversail {draft.TopOversail:0.##} / {draft.TopEndOversail:0.##} / {draft.TopBackOversail:0.##}",
			$"cornice: {draft.CorniceHeight:0.##} projecting {draft.CorniceProjection:0.##} past {draft.CorniceOversail:0.##}",
			$"fronts: {string.Join( ", ", draft.Fittings.Select( fitting => $"{fitting.Mount}={ArchCabinetUi.Named( fitting )}" ) )}",
			$"carcasses: {string.Join( ", ", draft.Carcasses.Select( carcass => $"{carcass.Slot}={carcass.Prefab}" ) )}",
			$"finish: {(string.IsNullOrWhiteSpace( draft.MaterialGroup ) ? "as modelled" : draft.MaterialGroup)}" );
	}

	protected override void Apply() {
		applied?.Invoke( ArchCabinetTypes.Copy( draft ) );
		Close();
	}

	protected override void SaveTemplate() {
		if ( string.IsNullOrWhiteSpace( draft.Name ) ) {
			Log.Warning( "Architecture: the cabinet type needs a name before it can be saved." );

			return;
		}

		if ( !ArchShelved.Save( ArchShelf.Cabinets, draft ) ) {
			Log.Warning( $"Architecture: could not write the cabinet type '{draft.Name}'." );

			return;
		}

		tool?.RegisterCabinetType( ArchCabinetTypes.Copy( draft ) );
		applied?.Invoke( ArchCabinetTypes.Copy( draft ) );
		Close();
	}
}