Editor/Tool/ArchBuildingWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;

namespace Sunless.Architecture;

// The building layout drawing: an orthographic PLAN of ONE building, storey by storey, opened from the button at the
// top of a building's sheet. It exists for the shapes the viewport is bad at - a chamfered corner, an L, a splay -
// where what you need is the numbers and a corner you can take hold of without a wall in front of it.
//
// Not an ArchDesignerDialog: those author a TYPE and hand a template back, where this holds a building that already
// stands in the plan and writes straight through to it, so every change is live in the viewport and on the undo stack
// the moment it is made. Modeless on purpose - you keep working in the scene with it open.
public sealed class ArchBuildingWindow : Window {
	static ArchBuildingWindow standing;

	readonly ArchTool tool;
	readonly int buildingId;

	const string LiveKey = "Arch.Layout.Live";
	const int PanelWidth = 264;

	ArchBuildingCanvas canvas;
	Layout storeys;
	Layout corner;
	ScrollArea panel;
	Label caption;
	Button build;

	// Off, an edit writes the plan and the drawing and the SCENE waits for Build. A whole building's rebuild measures
	// in seconds where the plan write measures in nothing, and the drawing is the thing you are looking at.
	bool live = EditorCookie.Get( LiveKey, false );

	float setback = 96f;

	// A press on the building already drawn puts the drawing away; a press on any other RE-POINTS it, or the button
	// would read as "close" on every building but the one the window happens to be holding.
	public static void Toggle( ArchTool tool, ArchBuilding building ) {
		if ( standing is not null && standing.buildingId == building?.Id ) {
			standing.Close();

			return;
		}

		Open( tool, building );
	}

	public static void Open( ArchTool tool, ArchBuilding building ) {
		if ( tool is null || building is null ) {
			return;
		}

		// One window, re-pointed. Two drawings of two buildings is two undo stacks racing over one plan.
		standing?.Close();

		standing = new ArchBuildingWindow( tool, building );
		standing.Show();
	}

	ArchBuildingWindow( ArchTool tool, ArchBuilding building ) {
		this.tool = tool;
		buildingId = building.Id;

		DeleteOnClose = true;
		WindowTitle = $"Building Layout — {building.Name}";
		Size = new Vector2( 1000, 660 );
		MinimumSize = new Vector2( 780, 500 );

		Canvas = new Widget( this ) { Layout = Layout.Row() };
		Canvas.Layout.Margin = 8;
		Canvas.Layout.Spacing = 8;

		BuildDrawing();
		BuildPanel();

		Rebuild();
	}

	ArchBuilding Subject() => tool?.Plan?.Buildings.FirstOrDefault( unit => unit.Id == buildingId );

	void BuildDrawing() {
		var column = Canvas.Layout.AddColumn( 1 );

		column.Spacing = 6;

		var bar = column.AddRow();

		bar.Spacing = 4;

		var others = new Checkbox( "Storeys under" ) { Value = true };

		others.Toggled += () => {
			canvas.Others = others.Value;
			canvas.Update();
		};

		bar.Add( others );

		var roofs = new Checkbox( "Roof" ) { Value = true };

		roofs.Toggled += () => {
			canvas.Roofs = roofs.Value;
			canvas.Update();
		};

		bar.Add( roofs );
		bar.AddStretchCell();

		var living = new Checkbox( "Live" ) { Value = live };

		living.Toggled += () => {
			live = living.Value;
			EditorCookie.Set( LiveKey, live );

			if ( live ) {
				Build();
			}
		};

		bar.Add( living );

		build = new Button.Primary( "Build", "refresh" ) { Clicked = Build };
		bar.Add( build );

		caption = new Label( "" );
		bar.Add( caption );

		canvas = new ArchBuildingCanvas( this, tool, Subject, Preview, Commit ) {
			Selected = RebuildCorner
		};

		column.Add( canvas, 1 );
		column.Add( new Label( "Drag a blue corner square to move it — every storey, wall, roof outline and parapet on "
			+ "that corner follows. Drag a green run square to push the whole elevation out. Drag the pink round grip "
			+ "inside a corner to cut it off at an angle: the facet becomes a real wall you can hang a door in." ) {
			WordWrap = true
		} );
	}

	void BuildPanel() {
		var column = Canvas.Layout.AddColumn();

		column.Spacing = 6;
		column.Add( new Label( "Storeys" ) );

		var scroll = new ScrollArea( this );

		scroll.Canvas = new Widget( scroll ) { Layout = Layout.Column() };
		scroll.Canvas.Layout.Spacing = 2;
		scroll.MinimumWidth = PanelWidth;
		scroll.MaximumWidth = PanelWidth;

		storeys = scroll.Canvas.Layout;
		column.Add( scroll, 1 );

		panel = new ScrollArea( this );
		panel.Canvas = new Widget( panel ) { Layout = Layout.Column() };
		panel.Canvas.Layout.Spacing = 4;
		panel.MinimumWidth = PanelWidth;
		panel.MaximumWidth = PanelWidth;

		corner = panel.Canvas.Layout;
		column.Add( panel, 1 );
	}

	// One row per storey the building actually holds, with what stands on it - the list is the navigation, exactly as
	// the climb is in the stair drawing.
	void RebuildStoreys() {
		storeys.Clear( true );

		if ( Subject() is not { } building ) {
			return;
		}

		foreach ( var floor in building.Rooms.Select( room => room.Floor ).Distinct().OrderByDescending( floor => floor ) ) {
			storeys.Add( StoreyRow( building, floor ) );
		}

		storeys.AddStretchCell();
	}

	Widget StoreyRow( ArchBuilding building, int floor ) {
		var rooms = building.Rooms.Where( room => room.Floor == floor ).ToList();
		var picked = floor == canvas.Storey;

		var row = new Widget( this ) { Layout = Layout.Column() };

		row.Layout.Margin = 6;
		row.Layout.Spacing = 2;

		var head = row.Layout.AddRow();

		head.Spacing = 4;
		head.Add( new Label( ArchStoreyEdit.Name( floor ) ) );
		head.AddStretchCell();
		head.Add( new Label( $"{rooms.Count} room{(rooms.Count == 1 ? "" : "s")}" ) );

		var walls = rooms.Sum( room => room.Walls.Count );
		var holes = rooms.SelectMany( room => room.Walls ).Sum( wall => wall.Openings.Count );

		row.Layout.Add( new Label( $"{walls} walls · {holes} openings"
			+ (rooms.Any( room => room.Facade ) ? " · facade" : "") ) );

		row.MouseLeftPress += () => {
			canvas.Storey = floor;
			canvas.Picked = -1;
			canvas.PickedEdge = -1;
			canvas.Forget();
			Rebuild();
		};

		row.SetStyles( picked
			? "background-color: #3b6ea5; border-radius: 3px;"
			: "background-color: #2a2a2e; border-radius: 3px;" );

		return row;
	}

	// What can be done to the corner or the run held. It is the same set of edits the viewport's own widgets make, put
	// where the numbers can be typed instead of dragged.
	void RebuildCorner() {
		corner.Clear( true );

		if ( Subject() is not { } building ) {
			return;
		}

		var ring = canvas.Ring();

		if ( ring is not { Count: >= 3 } ) {
			corner.Add( new Label( "This storey makes no single ring of corners — a detached wing is two shells, and "
				+ "neither one describes the other." ) { WordWrap = true } );
			corner.AddStretchCell();

			return;
		}

		if ( canvas.Picked >= 0 && canvas.Picked < ring.Count ) {
			CornerPage( building, ring, canvas.Picked );
		} else if ( canvas.PickedEdge >= 0 && canvas.PickedEdge < ring.Count ) {
			RunPage( building, ring, canvas.PickedEdge );
		} else {
			corner.Add( new Label( "Click a corner or a run in the drawing." ) { WordWrap = true } );
		}

		corner.AddStretchCell();
	}

	// The corner's own two numbers, and the SHAPES it can be turned into. Named operations rather than free corner
	// surgery: a chamfer moves eight lists in step across every storey, and a shell edited by adding and merging bare
	// points has no invariant left to protect it - two removes past a triangle folded a whole building flat.
	void CornerPage( ArchBuilding building, List<Vector2> ring, int index ) {
		var at = ring[index];

		corner.Add( new Label( $"Corner {index + 1}" ) );
		corner.Add( ArchPartUi.Number( "East", at.x, 0f, value => Move( building, ring, index, new Vector2( value, at.y ) ) ) );
		corner.Add( ArchPartUi.Number( "North", at.y, 0f, value => Move( building, ring, index, new Vector2( at.x, value ) ) ) );

		corner.Add( new Label( "Chamfer" ) );
		corner.Add( new Label( "Cut the corner off at an angle and stand a real wall across the gap. The facet is dressed "
			+ "off the elevation beside it, so the bands, pilasters and window rhythm carry round — and it takes a door "
			+ "like any other wall." ) { WordWrap = true } );
		corner.Add( ArchPartUi.Number( "Setback along each run", setback, 96f, value => setback = value ) );
		corner.Add( new Button.Primary( "Chamfer this corner", "content_cut" ) {
			Clicked = () => canvas.Chamfer( index, setback ),
			ToolTip = "Or drag the pink round grip inside the corner in the drawing"
		} );
	}

	void RunPage( ArchBuilding building, List<Vector2> ring, int index ) {
		var from = ring[index];
		var to = ring[(index + 1) % ring.Count];
		var span = to - from;

		corner.Add( new Label( $"Run {index + 1} — {span.Length:0.#} long" ) );
		corner.Add( new Label( $"{from.x:0.#}, {from.y:0.#} to {to.x:0.#}, {to.y:0.#}" ) { WordWrap = true } );

		var walls = On( building, from, to ).ToList();

		corner.Add( new Label( walls.Count == 0
			? "No wall stands on this run."
			: string.Join( ", ", walls.Select( wall => $"wall {wall.Id} · {wall.Length:0.#}" ) ) ) { WordWrap = true } );

		corner.Add( ArchPartUi.Number( "Push the elevation out by", 0f, 0f, value => Push( building, ring, index, value ) ) );
	}

	// Every wall on this storey whose whole span lies along the run, so the panel names the walls the push will carry.
	IEnumerable<ArchWall> On( ArchBuilding building, Vector2 from, Vector2 to ) {
		var span = to - from;

		if ( span.IsNearZeroLength ) {
			yield break;
		}

		var along = span.Normal;
		var normal = new Vector2( -along.y, along.x );

		foreach ( var wall in building.Rooms.Where( room => room.Floor == canvas.Storey ).SelectMany( room => room.Walls ) ) {
			if ( MathF.Abs( Vector2.Dot( wall.Start - from, normal ) ) < ArchCarry.CornerReach
				&& MathF.Abs( Vector2.Dot( wall.End - from, normal ) ) < ArchCarry.CornerReach ) {
				yield return wall;
			}
		}
	}

	void Move( ArchBuilding building, List<Vector2> ring, int index, Vector2 to ) {
		if ( !ArchCarry.Corner( building, ring[index], ArchGridService.Fine( to ) ) ) {
			return;
		}

		canvas.Forget();
		Commit();
	}

	void Push( ArchBuilding building, List<Vector2> ring, int index, float distance ) {
		var from = ring[index];
		var to = ring[(index + 1) % ring.Count];
		var span = to - from;

		if ( span.IsNearZeroLength || MathF.Abs( distance ) < 0.05f ) {
			return;
		}

		var normal = new Vector2( -span.Normal.y, span.Normal.x );

		if ( !ArchCarry.Along( building, from, to, normal * ArchGridService.Fine( distance ) ) ) {
			return;
		}

		canvas.Forget();
		Commit();
	}

	// The whole window: for a change of STRUCTURE - a corner cut, a storey picked, a shape re-traced.
	void Rebuild() {
		if ( Subject() is null ) {
			Close();

			return;
		}

		Retitle();
		RebuildStoreys();
		RebuildCorner();

		canvas.Update();
	}

	void Retitle() {
		if ( Subject() is not { } building ) {
			return;
		}

		var floors = building.Rooms.Select( room => room.Floor ).Distinct().Count();
		var waiting = tool is { Unbuilt: true };

		caption.Text = $"{floors} storey{(floors == 1 ? "" : "s")}"
			+ $" · {building.Roofs.Count} roof{(building.Roofs.Count == 1 ? "" : "s")}"
			+ (waiting ? " · unbuilt" : "");

		build.Enabled = waiting;
	}

	void Build() {
		tool?.Build();
		Retitle();
	}

	// A drag mid-gesture: the plan is already written, but generated geometry and the undo entry wait for the release.
	// It repaints and retitles and NOTHING else - rebuilding the storey list between two mouse-moves
	// destroys and recreates a widget per storey, which is what makes a drag crawl.
	void Preview() {
		Retitle();
		canvas.Update();
	}

	void Commit() {
		if ( tool?.Plan is not null ) {
			if ( live ) {
				tool.Commit( "Edit Building Layout" );
			} else {
				tool.Bank( "Edit Building Layout" );
			}
		}

		Rebuild();
	}

	// The drawing writes straight through to the plan, so it has to be as undoable as the viewport is - every edit here
	// already lands on the scene session's stack, and this is the key that reaches it without leaving the window.
	protected override void OnKeyPress( KeyEvent e ) {
		if ( !e.HasCtrl || SceneEditorSession.Active is not { } session ) {
			base.OnKeyPress( e );

			return;
		}

		if ( e.Key == KeyCode.Z && !e.HasShift ) {
			session.UndoSystem.Undo();
		} else if ( e.Key == KeyCode.Y || (e.Key == KeyCode.Z && e.HasShift) ) {
			session.UndoSystem.Redo();
		} else {
			base.OnKeyPress( e );

			return;
		}

		e.Accepted = true;

		// The plan is a fresh document after a rewind, and the corner held may not be there any more.
		canvas.Picked = -1;
		canvas.PickedEdge = -1;
		canvas.Forget();

		Rebuild();
	}

	protected override void OnClosed() {
		base.OnClosed();

		if ( standing == this ) {
			standing = null;
		}
	}
}