Editor/Cabinet/ArchCabinetUi.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

sealed class ArchFollowRow : Widget {
	readonly IconButton badge;
	readonly Action clear;

	public ArchFollowRow( Widget control, bool own, string following, Action clear ) {
		this.clear = clear;

		Layout = Layout.Row();
		Layout.Spacing = 4;
		Layout.Add( control, 1 );

		badge = new IconButton( own ? "link_off" : "link" ) {
			FixedSize = 22f,
			IconSize = 14f,
			Background = Color.Transparent,
			BackgroundActive = Color.Transparent,
			Foreground = own ? Theme.Blue : Theme.TextControl.WithAlpha( 0.3f ),
			ToolTip = own ? $"Set here — press to go back to {following}" : following,
			OnClick = own ? Hand : null
		};

		Layout.Add( badge );
	}

	void Hand() {
		clear?.Invoke();

		badge.Icon = "link";
		badge.Foreground = Theme.TextControl.WithAlpha( 0.3f );
		badge.OnClick = null;
		badge.Update();
	}
}

public static class ArchCabinetUi {
	public static void Kinds( Layout group, IReadOnlyList<ArchCabinetType> offered, string chosen, Action<ArchCabinetType> adopt ) {
		using var grid = ArchIconGrid.In( group );

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

			grid.Pick( $"{Titled( standing )} — {standing.Detail}", $"cabinet_{standing.Name}", Glyph( standing.Tier ),
				string.Equals( standing.Name, chosen, StringComparison.OrdinalIgnoreCase ), () => adopt( standing ) );
		}
	}

	public static void Tier( Layout group, CabinetTier standing, Action<CabinetTier> assign ) {
		using var grid = ArchIconGrid.In( group );

		grid.Pick( "Follow the type — a larder stands tall, a wall unit hangs", "cabinet_tier_auto", "auto_awesome",
			standing == CabinetTier.Auto, () => assign( CabinetTier.Auto ) );

		grid.Pick( "Base — on the floor, counter over it", "cabinet_tier_base", "table_restaurant",
			standing == CabinetTier.Base, () => assign( CabinetTier.Base ) );

		grid.Pick( "Hanging — off the counter below, or the standard datum", "cabinet_tier_hanging", "shelves",
			standing == CabinetTier.Hanging, () => assign( CabinetTier.Hanging ) );

		grid.Pick( "Tall — floor to whatever covers it", "cabinet_tier_tall", "door_sliding",
			standing == CabinetTier.Tall, () => assign( CabinetTier.Tall ) );
	}

	public static void Front( Layout group, ArchCabinetFitting fitting, Action chose, Action changed ) {
		Faces( group, () => fitting, fitting, chose, changed );
	}

	// Writes the run's own fitting, not the type's — editing a shelf entry would silently re-cut every run on it
	public static void Front( Layout group, ArchCabinetPart part, ArchCabinetType type, string mount, Action chose, Action changed ) {
		using ( var grid = ArchIconGrid.In( group ) ) {
			grid.Pick( $"Following {Titled( type )} — {Named( type?.On( mount ) )}", "cabinet_front_following", "link",
				part.On( mount ) is null, () => {
					part.Fittings?.RemoveAll( standing => ArchCabinetMounts.Same( standing.Mount, mount ) );

					chose?.Invoke();
					changed?.Invoke();
				} );
		}

		Faces( group, () => Own( part, type, mount ), part.On( mount ) ?? type?.On( mount ), chose, changed );
	}

	static void Faces( Layout group, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, Action chose, Action changed ) {
		using ( var grid = ArchIconGrid.In( group ) ) {
			Face( grid, reach, showing, CabinetFront.Slab, "Slab — one flat door", "cabinet_front_slab", "crop_square", chose );
			Face( grid, reach, showing, CabinetFront.Shaker, "Shaker — stiles and rails round a flush field", "cabinet_front_shaker", "border_all", chose );
			Face( grid, reach, showing, CabinetFront.Panel, "Panel — a raised field inside the frame", "cabinet_front_panel", "dashboard", chose );
			Face( grid, reach, showing, CabinetFront.Glass, "Glass — frame with nothing in it", "cabinet_front_glass", "window", chose );
			Face( grid, reach, showing, CabinetFront.None, "Open — no front at all", "cabinet_front_open", "crop_free", chose );
		}

		Authored( group, reach, showing, chose, changed );
	}

	static void Authored( Layout group, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, Action chose, Action changed ) {
		var offered = ArchCabinetFronts.Authored();

		Layout row = null;

		for ( var index = 0; index < offered.Count; index++ ) {
			var preset = offered[index];

			if ( index % 2 == 0 ) {
				row = group.AddRow();
				row.Spacing = 4;
			}

			row.Add( Card( preset, reach, showing, chose, changed ) );
		}

		if ( offered.Count % 2 == 1 ) {
			row.AddStretchCell();
		}
	}

	static Widget Card( ArchCabinetFrontPreset preset, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing,
		Action chose, Action changed ) {
		var art = AssetSystem.FindByPath( preset.Prefab )?.GetAssetThumb();
		var tile = new ArchTile( null, art, preset.Name, preset.Detail, !preset.Anchored, new Vector2( 96f, 104f ) ) {
			LabelStrip = 26f,
			ToolTip = preset.Anchored
				? $"{preset.Name} — {preset.Prefab}"
				: $"{preset.Name} — not anchored yet, so it lands on its own origin",
			Current = () => string.Equals( showing?.Prefab, preset.Prefab, StringComparison.OrdinalIgnoreCase ),
			Clicked = () => Adopt( preset, reach(), chose, changed ),
			Menu = menu => menu.AddOption( $"Remove {preset.Name}", "delete", () => {
				ArchCabinetFronts.Remove( preset.Name );
				chose?.Invoke();
			} )
		};

		if ( art is null ) {
			tile.Glyph = rect => Paint.DrawIcon( rect, "widgets", rect.Height );
		}

		return tile;
	}

	static void Adopt( ArchCabinetFrontPreset preset, ArchCabinetFitting fitting, Action chose, Action changed ) {
		fitting.Prefab = preset.Prefab;
		fitting.Front = CabinetFront.None;
		fitting.Fits = preset.Scales;
		fitting.Anchor = preset.Anchor;
		fitting.AnchorAlong = preset.AnchorAlong;

		chose?.Invoke();
		changed?.Invoke();
	}

	public static void Inside( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed, Action refresh = null ) {
		var following = Titled( type );
		var settled = refresh ?? changed;

		group.Add( new ArchFollowRow(
			ArchPartUi.Check( "Interior — panels, shelves and the faces inside them", part.Interior || Inherited( type ).Interior,
				value => part.Interior = value, changed ),
			part.Interior,
			$"Following {following} — {(Inherited( type ).Interior ? "on" : "off")}",
			() => { part.Interior = false; settled?.Invoke(); } ) );

		group.Add( new ArchFollowRow(
			ArchPartUi.Check( "Handles — a bar on every front", part.Handles ?? Inherited( type ).Handles,
				value => part.Handles = value, changed ),
			part.Handles is not null,
			$"Following {following} — {(Inherited( type ).Handles ? "on" : "off")}",
			() => { part.Handles = null; settled?.Invoke(); } ) );

		Counted( group, "Shelves", part.Shelves, Inherited( type ).Shelves, following,
			value => part.Shelves = value, changed, settled );

		Counted( group, "Drawer rows", part.DrawerRows, Inherited( type ).DrawerRows, following,
			value => part.DrawerRows = value, changed, settled );
	}

	public static void Sizes( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed,
		ArchCabinetShape resolved = null, Action refresh = null ) {
		var following = Titled( type );
		var settled = refresh ?? changed;

		Measured( group, "Module width", part.ModuleWidth, resolved?.Cells.FirstOrDefault()?.Width ?? Inherited( type ).Width,
			following, value => part.ModuleWidth = value, changed, settled );

		Measured( group, "Depth", part.Depth, resolved?.Depth ?? Inherited( type ).Depth,
			following, value => part.Depth = value, changed, settled );

		Measured( group, "Height", part.Height, resolved is null ? Stood( type ) : resolved.Head - resolved.Seat,
			Reaches( type, resolved ), value => part.Height = value, changed, settled );

		Seat( group, part, resolved, changed, settled );
	}

	// Typing a seat pins it; the badge unpins
	public static void Seat( Layout group, ArchCabinetPart part, ArchCabinetShape resolved, Action changed, Action settled ) {
		group.Add( new ArchFollowRow(
			ArchPartUi.Number( "Seat height", part.Seated ? part.BaseHeight : 0f, resolved?.Seat ?? 0f, value => {
				part.BaseHeight = value;
				part.Seated = value > 0.001f;
			}, changed ),
			part.Seated,
			$"Following {Derives( resolved )} — {resolved?.Seat ?? 0f:0.##}",
			() => {
				part.Seated = false;
				part.BaseHeight = 0f;

				settled?.Invoke();
			} ) );
	}

	public static void Worktop( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed,
		ArchCabinetShape resolved = null, Action refresh = null ) {
		var following = Titled( type );
		var settled = refresh ?? changed;

		Measured( group, "Counter depth", part.TopDepth, resolved?.TopDepth ?? Counter( type ),
			following, value => part.TopDepth = value, changed, settled );

		Optional( group, "Thickness", part.TopThickness, resolved?.Thickness ?? Inherited( type ).TopThickness,
			following, value => part.TopThickness = value, changed, settled );

		Optional( group, "Past the ends", part.TopEndOversail, resolved?.EndOversail ?? Inherited( type ).TopEndOversail,
			following, value => part.TopEndOversail = value, changed, settled );

		Optional( group, "Past the back", part.TopBackOversail, resolved?.BackOversail ?? Inherited( type ).TopBackOversail,
			following, value => part.TopBackOversail = value, changed, settled );
	}

	public static void Finish( Layout group, ArchCabinetPart part, ArchCabinetType type, Action changed, Action refresh ) {
		var offered = ArchPrefabGroups.Offered( type?.In( CabinetSlot.Module )?.Prefab );

		if ( offered.Count == 0 ) {
			group.Add( ArchPartUi.Wrapped( "This type's art carries no material groups." ) );

			return;
		}

		using var grid = ArchIconGrid.In( group );

		grid.Pick( $"Following {Titled( type )} — {Finished( type?.MaterialGroup )}", "cabinet_finish_following", "link",
			string.IsNullOrWhiteSpace( part.MaterialGroup ), () => {
				part.MaterialGroup = "";

				refresh?.Invoke();
				changed?.Invoke();
			} );

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

			grid.Pick( $"{standing} — every carcass in this run wears it", $"cabinet_finish_{standing}".ToLowerInvariant(),
				"palette", string.Equals( part.MaterialGroup, standing, StringComparison.OrdinalIgnoreCase ), () => {
					part.MaterialGroup = standing;

					refresh?.Invoke();
					changed?.Invoke();
				} );
		}
	}

	static string Finished( string group ) => string.IsNullOrWhiteSpace( group ) ? "as modelled" : group;

	public static void Fittings( Layout group, ArchCabinetPart part, ArchCabinetType type, ArchCabinetShape resolved,
		Action changed, Action refresh ) {
		var module = resolved?.Cells.FirstOrDefault();

		if ( module is null ) {
			group.Add( ArchPartUi.Wrapped( "Nothing standing yet — a run has to resolve before its mounts can be dressed." ) );

			return;
		}

		foreach ( var mount in module.Mounts.Where( mount => Dressable( mount.Name ) ) ) {
			group.Add( Mounted( part, type, mount.Name, changed, refresh ) );
		}
	}

	static bool Dressable( string mount ) {
		return ArchCabinetMounts.Same( mount, ArchCabinetMounts.Front )
			|| ArchCabinetMounts.Same( mount, ArchCabinetMounts.FrontUpper )
			|| ArchCabinetMounts.Same( mount, ArchCabinetMounts.Drawer );
	}

	static Widget Mounted( ArchCabinetPart part, ArchCabinetType type, string mount, Action changed, Action refresh ) {
		var own = part.On( mount );
		var standing = own ?? type?.On( mount );
		var holder = new Widget( null ) { Layout = Layout.Row() };

		holder.Layout.Spacing = 4;
		holder.Layout.Add( new Label( mount ) { MinimumWidth = 88 } );
		holder.Layout.Add( new Label( Named( standing ) ) { MinimumWidth = 60 }, 1 );
		holder.Layout.Add( new IconButton( "edit" ) {
			FixedSize = 22f,
			IconSize = 14f,
			ToolTip = $"Dress the {mount.ToLowerInvariant()}",
			OnClick = () => Dress( part, type, mount, changed, refresh )
		} );

		return new ArchFollowRow( holder, own is not null,
			$"Following {Titled( type )} — {Named( type?.On( mount ) )}",
			() => {
				part.Fittings?.RemoveAll( fitting => ArchCabinetMounts.Same( fitting.Mount, mount ) );

				refresh?.Invoke();
				changed?.Invoke();
			} );
	}

	static void Dress( ArchCabinetPart part, ArchCabinetType type, string mount, Action changed, Action refresh ) {
		var panel = new ToolSidebarWidget();

		ArchSidebarSection.Show( panel, $"cabinet.mount.{mount}", mount, group =>
			Front( group, part, type, mount, refresh, changed ) );

		var popup = new PopupWidget( null ) { Layout = Layout.Column() };

		popup.Layout.Add( panel );
		popup.FixedWidth = ArchPartUi.PanelWidth + 24f;
		popup.OpenAtCursor( false );
	}

	static ArchCabinetFitting Own( ArchCabinetPart part, ArchCabinetType type, string mount ) {
		if ( part.On( mount ) is { } standing ) {
			return standing;
		}

		var following = type?.On( mount );
		var made = new ArchCabinetFitting {
			Mount = mount,
			Front = following?.Front ?? CabinetFront.Slab,
			Prefab = following?.Prefab ?? "",
			Fits = following?.Fits ?? false,
			Anchor = following?.Anchor ?? Vector3.Zero,
			AnchorAlong = following?.AnchorAlong ?? Vector3.Zero
		};

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

		return made;
	}

	static void Measured( Layout group, string label, float own, float inherited, string following,
		Action<float> assign, Action changed, Action settled ) {
		var control = ArchPartUi.Number( label, own, inherited, assign, changed );

		group.Add( new ArchFollowRow( control, own > 0.001f, $"Following {following} — {inherited:0.##}",
			() => Hand( control, () => assign( 0f ), settled ) ) );
	}

	static void Optional( Layout group, string label, float? own, float inherited, string following,
		Action<float?> assign, Action changed, Action settled ) {
		var control = ArchPartUi.OptionalNumber( label, own, inherited, assign, changed );

		group.Add( new ArchFollowRow( control, own is not null, $"Following {following} — {inherited:0.##}",
			() => Hand( control, () => assign( null ), settled ) ) );
	}

	static void Counted( Layout group, string label, int? own, int inherited, string following,
		Action<int?> assign, Action changed, Action settled ) {
		var control = ArchPartUi.Integer( label, own ?? 0, inherited, value => assign( value > 0 ? value : null ), changed );

		group.Add( new ArchFollowRow( control, own is not null, $"Following {following} — {inherited}",
			() => Hand( control, () => assign( null ), settled ) ) );
	}

	// Blanks the control immediately so the row reads as inherited before the next rebuild
	static void Hand( Widget control, Action assign, Action settled ) {
		assign();

		foreach ( var edit in control.Children.OfType<LineEdit>() ) {
			edit.Text = "";
		}

		settled?.Invoke();
	}

	public static string Titled( ArchCabinetType type ) {
		if ( type is null ) {
			return "None";
		}

		var name = type.Name.Replace( '_', ' ' );

		return name.Length == 0 ? "Unnamed" : char.ToUpperInvariant( name[0] ) + name[1..];
	}

	public static string Glyph( CabinetTier tier ) => tier switch {
		CabinetTier.Hanging => "shelves",
		CabinetTier.Tall => "door_sliding",
		_ => "kitchen"
	};

	public static string Named( CabinetFront front ) => front switch {
		CabinetFront.None => "Open",
		CabinetFront.Shaker => "Shaker",
		CabinetFront.Panel => "Panel",
		CabinetFront.Glass => "Glass",
		_ => "Slab"
	};

	public static string Named( ArchCabinetFitting fitting ) {
		if ( fitting is null ) {
			return "None";
		}

		return fitting.Prefabbed ? ArchCabinetFronts.Resolve( fitting.Prefab )?.Name ?? fitting.Prefab : Named( fitting.Front );
	}

	static string Reaches( ArchCabinetType type, ArchCabinetShape resolved ) {
		if ( resolved is null ) {
			return Titled( type );
		}

		return resolved.Tier == CabinetTier.Base ? $"{Titled( type )} standing free" : "whatever covers it";
	}

	static string Derives( ArchCabinetShape resolved ) {
		if ( resolved is null ) {
			return "the floor";
		}

		return resolved.Tier == CabinetTier.Hanging ? "the counter below" : "the floor";
	}

	static ArchCabinetType Inherited( ArchCabinetType type ) => type ?? new ArchCabinetType();

	static float Stood( ArchCabinetType type ) {
		return type is null ? 36f : MathF.Max( 0f, type.PlinthHeight ) + type.Carcass + MathF.Max( 0f, type.TopThickness );
	}

	static float Counter( ArchCabinetType type ) {
		return type is null ? 25f : type.TopDepth > 1f ? type.TopDepth : type.Depth + MathF.Max( 0f, type.TopOversail );
	}

	static void Face( ArchIconGrid grid, Func<ArchCabinetFitting> reach, ArchCabinetFitting showing, CabinetFront front,
		string tooltip, string slug, string fallback, Action chose ) {
		grid.Pick( tooltip, slug, fallback, showing is { Prefabbed: false } && showing.Front == front, () => {
			var fitting = reach();

			fitting.Front = front;
			fitting.Prefab = "";
			fitting.Anchor = Vector3.Zero;
			fitting.AnchorAlong = Vector3.Zero;

			chose?.Invoke();
		} );
	}
}