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

namespace Sunless.Architecture;

enum ArchLayoutGrip {
	Corner,
	Edge,
	Bevel
}

static class ArchLayoutInk {
	public static readonly Color Storey = new( 0.30f, 0.58f, 0.82f );
	public static readonly Color Other = new( 0.58f, 0.62f, 0.70f );
	public static readonly Color Walls = new( 0.92f, 0.55f, 0.18f );
	public static readonly Color Holes = new( 0.96f, 0.92f, 0.74f );
	public static readonly Color Roof = new( 0.86f, 0.66f, 0.26f );
	public static readonly Color Corner = new( 0.42f, 0.72f, 0.92f );
	public static readonly Color Edge = new( 0.46f, 0.86f, 0.46f );
	public static readonly Color Bevel = new( 0.88f, 0.46f, 0.72f );
	public static readonly Color Dimension = new( 0.80f, 0.88f, 0.96f );
}

public sealed class ArchBuildingCanvas : Widget {
	const float Padding = 40f;
	const float GripSize = 9f;

	const float Elbow = 22f;

	readonly ArchTool tool;
	readonly Func<ArchBuilding> subject;
	readonly Action changed;
	readonly Action committed;

	readonly List<(ArchLayoutGrip Kind, int Index, Vector2 At)> grips = new();

	float scale = 1f;
	Vector2 origin;

	// Held for whole gesture — re-deriving mid-drag rebinds indices
	List<Vector2> ring;

	int held = -1;
	ArchLayoutGrip heldKind;
	bool dragging;
	Vector2 hover;

	float setback;

	ArchBuilding holding;

	public int Storey { get; set; }

	public bool Others { get; set; } = true;

	public bool Roofs { get; set; } = true;

	public int Picked { get; set; } = -1;
	public int PickedEdge { get; set; } = -1;

	public Action Selected { get; set; }

	public ArchBuildingCanvas( Widget parent, ArchTool tool, Func<ArchBuilding> subject, Action changed, Action committed ) : base( parent ) {
		this.tool = tool;
		this.subject = subject;
		this.changed = changed;
		this.committed = committed;

		MinimumSize = new Vector2( 460, 360 );
		MouseTracking = true;
		FocusMode = FocusMode.Click;
	}

	public List<Vector2> Ring() {
		if ( dragging && ring is { Count: >= 3 } ) {
			return ring;
		}

		if ( subject() is not { } building ) {
			return ring = null;
		}

		return ring = ArchFootprint.Outer( ArchFootprint.Union( Standing( building, Storey )
				.Select( ArchFloorGen.Footprint )
				.Where( loop => loop.Count >= 3 )
				.ToList() ) )
			.FirstOrDefault();
	}

	public void Forget() => ring = null;

	static IEnumerable<ArchRoom> Standing( ArchBuilding building, int floor ) {
		return building.Rooms.Where( room => room.Floor == floor );
	}

	Vector2 Screen( Vector2 flat ) => origin + new Vector2( flat.x, -flat.y ) * scale;

	Vector2 Local( Vector2 screen ) {
		var flat = (screen - origin) / MathF.Max( 0.0001f, scale );

		return new Vector2( flat.x, -flat.y );
	}

	// Framed on whole building so switching storey doesn't re-zoom
	void Frame( ArchBuilding building ) {
		var points = building.Rooms
			.SelectMany( ArchFloorGen.Footprint )
			.Concat( building.Roofs.SelectMany( roof => roof.Outline() ) )
			.ToList();

		if ( points.Count == 0 ) {
			scale = 1f;
			origin = new Vector2( Width * 0.5f, Height * 0.5f );

			return;
		}

		ArchFootprint.Bounds( points, out var min, out var max );

		var span = max - min;
		var page = new Vector2( MathF.Max( 1f, Width - Padding * 2f ), MathF.Max( 1f, Height - Padding * 2f ) );

		scale = MathF.Min( page.x / MathF.Max( 1f, span.x ), page.y / MathF.Max( 1f, span.y ) );
		origin = new Vector2(
			Padding + (page.x - span.x * scale) * 0.5f - min.x * scale,
			Padding + (page.y - span.y * scale) * 0.5f + max.y * scale );
	}

	protected override void OnPaint() {
		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground.Darken( 0.25f ) );
		Paint.DrawRect( LocalRect );

		if ( subject() is not { } building || building.Rooms.Count == 0 ) {
			Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
			Paint.DrawText( LocalRect, "no building", TextFlag.Center );

			return;
		}

		grips.Clear();

		Frame( building );

		if ( Others ) {
			PaintOtherStoreys( building );
		}

		if ( Roofs ) {
			PaintRoofs( building );
		}

		PaintRooms( building );
		PaintWalls( building );
		PaintRing();
		PaintFacetPreview();
		PaintGrips();
		PaintLegend( building );
	}

	void PaintOtherStoreys( ArchBuilding building ) {
		foreach ( var room in building.Rooms.Where( room => room.Floor != Storey ) ) {
			Outline( ArchFloorGen.Footprint( room ), ArchLayoutInk.Other.WithAlpha( 0.28f ), 1f );
		}
	}

	void PaintRoofs( ArchBuilding building ) {
		foreach ( var roof in building.Roofs ) {
			Outline( roof.Outline(), ArchLayoutInk.Roof.WithAlpha( 0.45f ), 1f );
		}
	}

	void PaintRooms( ArchBuilding building ) {
		foreach ( var room in Standing( building, Storey ) ) {
			var loop = ArchFloorGen.Footprint( room );

			if ( loop.Count < 3 ) {
				continue;
			}

			Paint.ClearPen();
			Paint.SetBrush( ArchLayoutInk.Storey.WithAlpha( 0.10f ) );
			Paint.DrawPolygon( loop.Select( Screen ).ToArray() );
		}
	}

	void PaintWalls( ArchBuilding building ) {
		var stock = MathF.Max( 1f, tool?.Kit?.WallThickness ?? 8f );

		foreach ( var wall in Standing( building, Storey ).SelectMany( room => room.Walls ) ) {
			var span = wall.End - wall.Start;

			if ( span.IsNearZeroLength ) {
				continue;
			}

			var half = wall.Normal * ((wall.Thickness > 0f ? wall.Thickness : stock) * 0.5f);

			Paint.ClearPen();
			Paint.SetBrush( ArchLayoutInk.Walls.WithAlpha( 0.55f ) );
			Paint.DrawPolygon(
				Screen( wall.Start - half ),
				Screen( wall.End - half ),
				Screen( wall.End + half ),
				Screen( wall.Start + half ) );

			foreach ( var opening in wall.Openings ) {
				var from = wall.PointAt( Math.Clamp( opening.Left, 0f, wall.Length ) );
				var to = wall.PointAt( Math.Clamp( opening.Right, 0f, wall.Length ) );

				Paint.ClearPen();
				Paint.SetBrush( ArchLayoutInk.Holes.WithAlpha( 0.9f ) );
				Paint.DrawPolygon(
					Screen( from - half ),
					Screen( to - half ),
					Screen( to + half ),
					Screen( from + half ) );
			}
		}
	}

	void PaintRing() {
		if ( Ring() is not { Count: >= 3 } loop ) {
			Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
			Paint.DrawText( LocalRect, $"storey {Storey} makes no single outline — a detached wing has no one ring of corners", TextFlag.Center );

			return;
		}

		Paint.SetDefaultFont( 7 );

		for ( var index = 0; index < loop.Count; index++ ) {
			var next = (index + 1) % loop.Count;
			var from = loop[index];
			var to = loop[next];
			var picked = index == PickedEdge;

			Paint.ClearBrush();
			Paint.SetPen( picked ? Theme.Primary : ArchLayoutInk.Storey, picked ? 3f : 2f );
			Paint.DrawLine( Screen( from ), Screen( to ) );

			Dimension( from, to, index );

			grips.Add( (ArchLayoutGrip.Edge, index, (from + to) * 0.5f) );
			grips.Add( (ArchLayoutGrip.Corner, index, from) );

			if ( Bisector( loop, index ) is { } inward ) {
				grips.Add( (ArchLayoutGrip.Bevel, index, from + inward * Elbow) );
			}
		}
	}

	void Dimension( Vector2 from, Vector2 to, int index ) {
		var middle = Screen( (from + to) * 0.5f );
		var length = (to - from).Length;

		Paint.SetPen( ArchLayoutInk.Dimension.WithAlpha( index == PickedEdge ? 0.95f : 0.55f ) );
		Paint.DrawText( new Rect( middle.x - 40f, middle.y - 18f, 80f, 14f ), $"{length:0.#}", TextFlag.Center );

		var corner = Screen( from );

		Paint.SetPen( (index == Picked ? Theme.Primary : ArchLayoutInk.Corner).WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( corner.x + 8f, corner.y + 6f, 130f, 13f ),
			$"{index + 1}  {from.x:0.#}, {from.y:0.#}", TextFlag.LeftTop );
	}

	static Vector2? Bisector( List<Vector2> loop, int index ) {
		var at = loop[index];
		var back = loop[(index - 1 + loop.Count) % loop.Count] - at;
		var forward = loop[(index + 1) % loop.Count] - at;

		if ( back.IsNearZeroLength || forward.IsNearZeroLength ) {
			return null;
		}

		var inward = back.Normal + forward.Normal;

		return inward.IsNearZeroLength ? null : inward.Normal;
	}

	void PaintFacetPreview() {
		if ( !dragging || heldKind != ArchLayoutGrip.Bevel || Ring() is not { Count: >= 3 } loop || held < 0 || held >= loop.Count ) {
			return;
		}

		if ( ArchChamfer.Facing( loop, loop[held], setback ) is not { Stands: true } facet ) {
			return;
		}

		Paint.ClearBrush();
		Paint.SetPen( ArchLayoutInk.Bevel, 3f );
		Paint.DrawLine( Screen( facet.From ), Screen( facet.To ) );

		var middle = Screen( (facet.From + facet.To) * 0.5f );

		Paint.SetDefaultFont( 8 );
		Paint.SetPen( ArchLayoutInk.Bevel );
		Paint.DrawText( new Rect( middle.x + 10f, middle.y - 7f, 200f, 14f ),
			$"{facet.Reach:0.#} back · {(facet.To - facet.From).Length:0.#} facet", TextFlag.LeftCenter );
	}

	void PaintGrips() {
		foreach ( var grip in grips ) {
			var at = Screen( grip.At );
			var hot = grip.Kind switch {
				ArchLayoutGrip.Edge => grip.Index == PickedEdge,
				_ => grip.Index == Picked
			};

			var hue = grip.Kind switch {
				ArchLayoutGrip.Edge => ArchLayoutInk.Edge,
				ArchLayoutGrip.Bevel => ArchLayoutInk.Bevel,
				_ => ArchLayoutInk.Corner
			};

			Paint.ClearPen();
			Paint.SetBrush( hue.WithAlpha( hot ? 1f : 0.6f ) );

			if ( grip.Kind == ArchLayoutGrip.Bevel ) {
				Paint.DrawRect( Box( at ).Grow( -1.5f ), GripSize );

				continue;
			}

			Paint.DrawRect( Box( at ), 2 );
		}
	}

	static Rect Box( Vector2 at ) => new( at.x - GripSize * 0.5f, at.y - GripSize * 0.5f, GripSize, GripSize );

	void Outline( IReadOnlyList<Vector2> loop, Color hue, float width ) {
		if ( loop.Count < 3 ) {
			return;
		}

		Paint.ClearBrush();
		Paint.SetPen( hue, width );

		for ( var index = 0; index < loop.Count; index++ ) {
			Paint.DrawLine( Screen( loop[index] ), Screen( loop[(index + 1) % loop.Count] ) );
		}
	}

	static readonly (Color Hue, string Name)[] Keys =
	{
		(ArchLayoutInk.Storey, "this storey"),
		(ArchLayoutInk.Other, "other storeys"),
		(ArchLayoutInk.Walls, "walls"),
		(ArchLayoutInk.Roof, "roof"),
		(ArchLayoutInk.Corner, "corner"),
		(ArchLayoutInk.Edge, "run"),
		(ArchLayoutInk.Bevel, "chamfer")
	};

	void PaintLegend( ArchBuilding building ) {
		var top = 8f;

		Paint.SetDefaultFont( 7 );

		foreach ( var key in Keys ) {
			Paint.ClearPen();
			Paint.SetBrush( key.Hue.WithAlpha( 0.85f ) );
			Paint.DrawRect( new Rect( Width - 92f, top + 3f, 8f, 8f ), 2 );

			Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
			Paint.DrawText( new Rect( Width - 80f, top, 74f, 14f ), key.Name, TextFlag.LeftCenter );

			top += 13f;
		}

		Paint.SetPen( Theme.TextControl.WithAlpha( 0.55f ) );
		Paint.DrawText( new Rect( 8f, Height - 18f, Width - 16f, 14f ),
			$"{building.Name} · storey {Storey} · {Ring()?.Count ?? 0} corners · {Standing( building, Storey ).Sum( room => room.Walls.Count )} walls",
			TextFlag.LeftCenter );
	}

	protected override void OnMousePress( MouseEvent e ) {
		base.OnMousePress( e );

		held = -1;
		dragging = false;

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

		foreach ( var grip in grips ) {
			if ( !Box( Screen( grip.At ) ).Grow( 3f ).IsInside( e.LocalPosition ) ) {
				continue;
			}

			held = grip.Index;
			heldKind = grip.Kind;
			holding = building;
			dragging = true;
			setback = 0f;

			if ( grip.Kind == ArchLayoutGrip.Edge ) {
				PickedEdge = grip.Index;
			} else {
				Picked = grip.Index;
			}

			Selected?.Invoke();
			Update();

			return;
		}

		if ( Nearest( e.LocalPosition ) is var corner && corner >= 0 ) {
			Picked = corner;
			Selected?.Invoke();
			Update();
		}
	}

	int Nearest( Vector2 screen ) {
		if ( Ring() is not { Count: >= 3 } loop ) {
			return -1;
		}

		var found = -1;
		var best = 40f;

		for ( var index = 0; index < loop.Count; index++ ) {
			var away = (Screen( loop[index] ) - screen).Length;

			if ( away >= best ) {
				continue;
			}

			best = away;
			found = index;
		}

		return found;
	}

	protected override void OnMouseMove( MouseEvent e ) {
		base.OnMouseMove( e );

		hover = e.LocalPosition;

		if ( !dragging || held < 0 || holding is null || Ring() is not { Count: >= 3 } loop || held >= loop.Count ) {
			return;
		}

		var landed = Rested( Local( e.LocalPosition ) );

		if ( heldKind == ArchLayoutGrip.Bevel ) {
			setback = Reached( loop, held, landed );
			Update();

			return;
		}

		if ( heldKind == ArchLayoutGrip.Corner ) {
			if ( !ArchCarry.Corner( holding, loop[held], landed ) ) {
				return;
			}

			loop[held] = landed;
		} else {
			var next = (held + 1) % loop.Count;
			var span = loop[next] - loop[held];

			if ( span.IsNearZeroLength ) {
				return;
			}

			var normal = new Vector2( -span.Normal.y, span.Normal.x );
			var reach = normal * ArchGridService.Fine( Vector2.Dot( landed - (loop[held] + loop[next]) * 0.5f, normal ) );

			if ( reach.IsNearZeroLength || !ArchCarry.Along( holding, loop[held], loop[next], reach ) ) {
				return;
			}

			loop[held] += reach;
			loop[next] += reach;
		}

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

	// Projected onto bisector then divided back by half-angle
	static float Reached( List<Vector2> loop, int index, Vector2 landed ) {
		if ( Bisector( loop, index ) is not { } inward ) {
			return 0f;
		}

		var back = (loop[(index - 1 + loop.Count) % loop.Count] - loop[index]).Normal;
		var travel = MathF.Max( 0f, Vector2.Dot( landed - loop[index], inward ) );

		return ArchGridService.Fine( travel / MathF.Max( 0.2f, Vector2.Dot( inward, back ) ) );
	}

	// Snaps to walls first, then grid
	Vector2 Rested( Vector2 local ) {
		if ( tool is not null && tool.Walled( local, out var wall, Storey ) ) {
			return wall;
		}

		return ArchGridService.Fine( local );
	}

	protected override void OnMouseReleased( MouseEvent e ) {
		base.OnMouseReleased( e );

		var bevelled = dragging && heldKind == ArchLayoutGrip.Bevel;
		var cut = setback;

		dragging = false;
		held = -1;
		holding = null;
		setback = 0f;

		if ( bevelled ) {
			Chamfer( Picked, cut );

			return;
		}

		committed?.Invoke();
	}

	public void Chamfer( int index, float back ) {
		if ( tool?.Plan is null || subject() is not { } building || Ring() is not { Count: >= 3 } loop
			|| index < 0 || index >= loop.Count ) {
			return;
		}

		var report = ArchChamfer.Corner( tool.Plan, building, loop[index], back );

		if ( !report.Chamfered ) {
			Log.Info( "Architecture: nothing was cut — the drag left no facet to stand." );

			return;
		}

		Stranded( report.Stranded );
		Settle();
	}

	static void Stranded( int holes ) {
		if ( holes > 0 ) {
			Log.Info( $"Architecture: {holes} opening{(holes == 1 ? "" : "s")} stood in the stretch the edit took away and went with it." );
		}
	}

	void Settle() {
		ring = null;
		Picked = -1;
		PickedEdge = -1;

		committed?.Invoke();
	}
}