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

namespace Sunless.Architecture;

[Title( "Wall" ), Icon( "horizontal_rule" ), Group( "04" )]
public sealed class ArchWallSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Wall;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.WallBase, ArchSurface.Baseboard, ArchSurface.WallCap
	};

	ArchWallPreset draft;

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

	protected override string Advice() => "Drag to draw inside a room, or across a roof deck to stand a parapet on it.";

	// Picking the tool starts the sequence over, so the first step is open and asking which treatment this is.
	public override void OnEnabled() {
		base.OnEnabled();

		ArchWorkflow.Restart( Scope( "flow" ) );
	}

	// Seeded off the kit on first ask - the kit isn't loaded until the tool is up.
	ArchWallPreset Treatment() {
		return draft ??= ArchWallPreset.Copy( Owner.Kit.Walls.FirstOrDefault() );
	}

	// A post at the section and rise of the wall about to be stood, on the surface it will stand on, so what the
	// cursor came to rest on is a thing you can see rather than a thing you find out by releasing the mouse.
	protected override void DrawHover( Vector2 point ) {
		var treatment = Treatment();
		var deck = Pointer.OnDeck ?? ArchRoofWalls.Under( Owner.Plan, Owner.Level, point );
		var room = deck is null ? Owner.Contained( point ) : null;

		var seat = deck is not null ? ArchRoofWalls.Seat( deck, Owner.Kit ) : room?.BaseHeight ?? Owner.LevelHeight;
		var height = treatment.Height > 1f
			? treatment.Height
			: deck is not null ? ArchRoofWalls.ParapetHeight
			: room is { WallHeight: > 1f } ? room.WallHeight : Owner.Kit.WallHeight;

		ArchGhost.Marker( point, Thickness( treatment ), seat, seat + height, Pointer.Snap, Reach() );

		// Tag, not Note: WorldText lies in a plane, so the label reads back to front from half the angles a
		// building is drawn from - which is what "plain wall" mirrored over the deck was.
		ArchGhost.Tag( new Vector3( point.x, point.y, seat + height ), Landing( treatment, deck, room ),
			Pointer.Snap == ArchSnapKind.None ? ArchGhost.Line : ArchGhost.Accent );
	}

	// The marker ring shows how far the point will reach, so it draws the widest of the two bands it really has.
	float Reach() => Owner.SnapReach().Face;

	string Landing( ArchWallPreset treatment, ArchRoofPart deck, ArchRoom room ) {
		var on = ArchWallSnap.Describe( Pointer.Snap )
			?? (deck is not null ? $"parapet on {deck.Name}" : room is not null ? $"in {room.Name}" : null);

		return on is null ? treatment.Name : $"{treatment.Name} — {on}";
	}

	protected override void DrawPreview() {
		var treatment = Treatment();

		if ( Pointer.Snap != ArchSnapKind.None ) {
			ArchGhost.Marker( DragCurrent, Thickness( treatment ), DragHeight, DragHeight, Pointer.Snap, Reach() );
		}

		if ( DragDeck is { } deck ) {
			var seat = ArchRoofWalls.Seat( deck, Owner.Kit );
			var parapet = treatment.Height > 1f ? treatment.Height : ArchRoofWalls.ParapetHeight;

			ArchGhost.Ring( deck.Outline(), seat );
			ArchGhost.Wall( DragStart, DragCurrent, seat, parapet, Thickness( treatment ) );
			ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, seat + parapet ),
				$"parapet on {deck.Name} at deck {seat:0} — {(DragCurrent - DragStart).Length:0}" );

			return;
		}

		var room = Standing();
		var height = treatment.Height > 1f
			? treatment.Height
			: room is { WallHeight: > 1f } ? room.WallHeight : Owner.Kit.WallHeight;

		if ( room is null ) {
			ArchGhost.Wall( DragStart, DragCurrent, Owner.LevelHeight, height, Thickness( treatment ) );
			ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, Owner.LevelHeight + height ),
				$"no room on level {Owner.Level} here — draw a shell first, or drag on a deck for a parapet" );

			return;
		}

		ArchGhost.Wall( DragStart, DragCurrent, room.BaseHeight, height, Thickness( treatment ) );
		ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, room.BaseHeight + height ),
			$"{treatment.Name} — {(DragCurrent - DragStart).Length:0}" );
	}

	float Thickness( ArchWallPreset treatment ) {
		return treatment.Thickness > 0.1f ? treatment.Thickness : Owner.Kit.WallThickness;
	}

	// RoomAt falls back to the active room, which on any other storey is a wall filed where it cannot be seen. The
	// only honest host is one standing on the level being edited.
	ArchRoom Standing() {
		var room = Owner.RoomAt( DragStart, out _ );

		return room is not null && room.Floor == Owner.Level ? room : null;
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		if ( (to - from).Length < 1f ) {
			return;
		}

		// The deck the gesture BEGAN on, not one looked up from here: a drag pulled off the edge of the roof is
		// still that roof's parapet.
		if ( DragDeck is { } deck ) {
			var parapet = ArchRoofWalls.Add( Owner.Plan, deck, from, to, Treatment() );

			FinishDraft( parapet.Id );
			Owner.Commit( $"Stand parapet on {deck.Name}" );

			return;
		}

		if ( Standing() is not { } room ) {
			Log.Info( $"Architecture: nothing on level {Owner.Level} to hang that wall on. Draw a shell or a room first,"
				+ " or drag across a roof deck to stand a parapet on it." );

			return;
		}

		var wall = Treatment().Spawn( Owner.Plan.AllocateId(), from, to );

		room.Walls.Add( wall );
		FinishDraft( wall.Id );
		Owner.Commit();
	}

	// A drawn wall is a partition, a parapet on a deck or a boundary wall - never the fenestrated elevation of a
	// house, which the Building tool's shell stands and the Opening tools cut. So the sequence asks only what is
	// true of THIS wall: which treatment, how its head finishes, what its two faces wear, how big it is.
	//
	// Cladding, band courses, trim bands and pilasters are not here. They are what a TREATMENT is - the card you
	// picked already says "brick, 2 bands, pilasters" on it - so they are authored in the Wall Designer behind the
	// shelf's design chip, and edited on a standing wall through its own sheet in Select. A third copy down here
	// is how the panel came to say everything twice.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Treatment", Treatment().Name, TreatmentGlyph, step => Browse( panel, step ) );

		flow.Step( "Head", HeadName, HeadGlyph, BuildHead );

		flow.Step( "Faces", FacesName, "format_paint", BuildFaces );

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

	string TreatmentGlyph => ArchIcons.Get( ArchIcons.WallSlug( Treatment().Name ), "villa" );

	string HeadName {
		get {
			var treatment = Treatment();

			return ArchWallUi.Crowning( Owner.Kit, treatment ) is { } crown ? crown.Name : treatment.Cap ? "Coping" : "Bare";
		}
	}

	string HeadGlyph {
		get {
			var treatment = Treatment();

			return ArchWallUi.Crowning( Owner.Kit, treatment ) is not null ? "view_stream" : treatment.Cap ? "border_top" : "block";
		}
	}

	// Chose IS the refresh, and the head grid needs one either way - picking coping opens two numbers under it
	// and picking a cornice opens the style shelf.
	void BuildHead( ArchWorkflowStep step ) {
		ArchSidebarLayout.Form( step.Layout, form => ArchWallUi.Head( form, Owner.Kit, Treatment(), step.Chose, Restage ) );
	}

	string FacesName {
		get {
			var treatment = Treatment();
			var worn = new List<string> { treatment.Exterior ? "Exterior" : "Inside" };

			if ( treatment.SingleSided ) {
				worn.Add( "one face" );
			}

			if ( treatment.Baseboard ) {
				worn.Add( "skirting" );
			}

			if ( treatment.Wainscot ) {
				worn.Add( "wainscot" );
			}

			return string.Join( ", ", worn );
		}
	}

	void BuildFaces( ArchWorkflowStep step ) {
		ArchSidebarLayout.Form( step.Layout, form => ArchWallUi.Faces( form, Treatment(), Restage ) );
	}

	// What the two boxes below it read, kit placeholders and all, so the closed row and the open one agree. The
	// room's own wall height beats the kit's at drag time, which is what the hover tag on the marker says.
	string PlacementName {
		get {
			var treatment = Treatment();
			var rise = treatment.Height > 1f ? treatment.Height : Owner.Kit.WallHeight;

			return $"{rise:0} × {Thickness( treatment ):0}";
		}
	}

	void BuildPlacement( ArchWorkflowStep step ) {
		var treatment = Treatment();

		step.Layout.Add( ArchPartUi.Number( "Height", treatment.Height, Owner.Kit.WallHeight, value => treatment.Height = value ) );
		step.Layout.Add( ArchPartUi.Number( "Thickness", treatment.Thickness, Owner.Kit.WallThickness, value => treatment.Thickness = value ) );
	}

	// 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<ArchWallPreset>( panel, Owner.Kit, Scope( "presets" ), "Treatments" ) {
			Chosen = preset => string.Equals( Treatment().Name, preset.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = preset => Adopt( preset, step ),
			Design = () => new ArchWallDesigner( panel, Owner.Kit, Treatment(), designed => ApplyDesigned( designed, step ) ).Show()
		};

		shelf.DesignButton.Visible = true;
		shelf.DesignButton.ToolTip = "Design a wall treatment";
		shelf.Set( Offered() );

		step.Add( shelf );
	}

	List<ArchPresetItem<ArchWallPreset>> Offered() {
		return Owner.Kit.Walls
			.Select( preset => new ArchPresetItem<ArchWallPreset> {
				Value = preset,
				Name = preset.Name,
				Detail = Worn( preset ),
				Identity = ArchWallStage.Identity( preset ),
				Category = preset.Cladding != WallCladding.None ? preset.Cladding.ToString() : preset.Bands.Count > 0 ? "Banded" : "Plain",
				Badge = preset.Exterior ? null : "inside",
				Tags = $"{preset.Cladding} {preset.Bands.Count} bands {preset.Trims.Count} trims",
				View = ArchPresetPreview.Elevation,

				// A treatment IS the wall, so unlike an opening its card keeps the elevation it is worn on.
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchWallStage.Of( preset, Owner.Kit )
			} )
			.ToList();
	}

	static string Worn( ArchWallPreset preset ) {
		var worn = new List<string>();

		if ( preset.Bands.Count > 0 ) {
			worn.Add( $"{preset.Bands.Count} band{(preset.Bands.Count == 1 ? "" : "s")}" );
		}

		if ( preset.Cladding != WallCladding.None ) {
			worn.Add( preset.Cladding.ToString().ToLowerInvariant() );
		}

		if ( preset.Pilasters.Stands ) {
			worn.Add( "pilasters" );
		}

		return worn.Count == 0 ? "plain field" : string.Join( ", ", worn );
	}

	// Editing the draft changes what its own card should show, so the stale one is dropped.
	void Restage() {
		ArchPresetPreview.Invalidate( ArchWallStage.Identity( Treatment() ) );
	}

	void Adopt( ArchWallPreset preset, ArchWorkflowStep step ) {
		draft = ArchWallPreset.Copy( preset );

		step.Chose();
	}

	void ApplyDesigned( ArchWallPreset designed, ArchWorkflowStep step ) {
		draft = designed;

		Owner.RegisterWallPreset( ArchWallPreset.Copy( designed ) );
		ArchPresetPreview.Invalidate( ArchWallStage.Identity( designed ) );

		step.Chose();
	}
}