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

namespace Sunless.Architecture;

[Title( "Select" ), Icon( "ads_click" ), Group( "01" )]
public sealed class ArchSelectSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	bool editing;

	bool geometry;

	IReadOnlyList<ArchFaceDetail> faces = Array.Empty<ArchFaceDetail>();
	int faceIndex;

	// Latched — scene view claims ctrl+click, so modifier alone isn't reliable
	bool adding;

	IReadOnlyList<ArchHit> stack = Array.Empty<ArchHit>();
	int stackIndex;
	bool stackOpen;

	protected override bool UsesDrag => false;

	protected override bool Keeps => true;


	public override ArchViewAxis[] Works => new[] { ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side };

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

	protected override string Advice() {
		if ( Carrying ) {
			return $"Carrying {carried.Name} — click where it should stand. R turns it a quarter; Cancel puts it back.";
		}

		if ( geometry ) {
			return Owner.DebugFaces.Count == 1
				? $"{Owner.DebugFaces[0].Label} — the stack has opened to it; Copy Debug Info hands it over."
				: "Click a surface to pick the FACE under the cursor. The Plan Layers stack opens to it, and Copy Debug Info says what it collides with.";
		}

		if ( Owner.Picked is { Item: ArchBuilding } picked ) {
			return $"{picked.Describe()} — R turns it a quarter; the turn ring swings it to any angle.";
		}

		return Owner.Picked?.Describe() ?? "Click an authored part, or Alt+click to list every part under the cursor. Repeat clicks cycle.";
	}

	ArchBuilding carried;
	ArchAssetGhost carriedGhost;
	int carriedQuarters;

	public bool Carrying => carried is not null && carriedGhost is { Shapes.Count: > 0 };

	public override bool Placing => false;

	public void Carry( ArchBuilding building ) {
		carried = building;
		carriedGhost = ArchBuildingStamp.Ghost( building );
		carriedQuarters = 0;
	}

	public override void OnUpdate() {
		if ( Owner.CarryRequest is { } asked ) {
			Owner.CarryRequest = null;

			Carry( asked );
		}

		base.OnUpdate();
	}

	protected override ArchRunState Run() {
		return Carrying ? new ArchRunState( true, $"Carrying {carried.Name}" ) : default;
	}

	protected override void FinishRun() => Carry( null );

	protected override void CancelRun() => Carry( null );

	public override bool Turns() {
		if ( !Carrying ) {
			return false;
		}

		carriedQuarters = (carriedQuarters + 1) % 4;

		return true;
	}

	protected override void DrawHover( Vector2 point ) {
		base.DrawHover( point );

		if ( !Carrying ) {
			return;
		}

		var map = Landing( point );

		foreach ( var shape in carriedGhost.Shapes ) {
			ArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );
		}

		var span = map.Swapped( carriedGhost.Max - carriedGhost.Min );

		ArchGhost.Note( new Vector3( point.x + span.x * 0.5f, point.y + span.y * 0.5f, Owner.LevelHeight ),
			$"{carried.Name} — {span.x:0} x {span.y:0}" );
	}

	ArchRemap Landing( Vector2 point ) => ArchBuildingStamp.Carried( carriedGhost, point, carriedQuarters * 90f );

	void PutDown( Vector2 point ) {
		var moved = carried;
		var turn = carriedQuarters * 90f;

		ArchCarry.Unit( moved, Landing( point ), Owner.Plan );

		moved.Facing = (moved.Facing + turn) % 360f;

		Carry( null );
		Owner.Commit( $"Move {moved.Name}" );
	}

	protected override bool Adjusting() {
		if ( Carrying ) {
			return false;
		}

		if ( Owner.Picked is not { } picked ) {
			editing = false;

			return false;
		}

		using ( ArchGhost.Begin() ) {
			if ( ArchHandles.Draw( Owner, picked ) ) {
				editing = true;
			}

			if ( editing ) {
				ArchEditGhost.Draw( Owner, picked );
			}
		}

		if ( !editing ) {
			return ArchShapeHandles.HandlePressed;
		}

		if ( !Gizmo.WasLeftMouseReleased ) {
			return true;
		}

		editing = false;

		if ( picked.Item is ArchStairPart stair ) {
			ArchPiercers.Pierce( Owner.Plan, picked.Building, picked.Room, stair, Owner.Kit );
		}

		Owner.Commit( $"Edit {picked.Describe()}" );

		return true;
	}

	protected override void OnClick( Vector2 point ) {
		if ( Carrying ) {
			PutDown( point );

			return;
		}

		if ( Gizmo.Pressed.Any || editing ) {
			return;
		}

		if ( Owner.InElevation ) {
			stack = Array.Empty<ArchHit>();
			stackOpen = false;

			Owner.Select( PickInSection() );

			return;
		}

		// Free pointer — no snap, pick what's actually under the cursor
		var found = ArchPick.HitsAt( Owner.LayerTree, Owner.Kit, Owner.Level, Pointer.Free );

		var repeat = stack.Count > 0 && found.Count == stack.Count
			&& found.Select( hit => hit.Layer ).SequenceEqual( stack.Select( hit => hit.Layer ) );

		stack = found;
		stackOpen = global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );
		stackIndex = repeat ? (stackIndex + 1) % stack.Count : 0;

		PickFromStack();
	}

	protected override bool TakesFreeClick => geometry;

	protected override void OnFreeClick() {
		var hits = Owner.FacesUnder( Gizmo.CurrentRay );
		var adds = Adding;

		if ( hits.Count == 0 ) {
			faces = Array.Empty<ArchFaceDetail>();

			if ( !adds ) {
				Owner.SelectFaces( null );
			}

			Refresh();

			return;
		}

		var repeat = faces.Count == hits.Count && faces.All( hits.Contains );

		faces = hits;

		// Adding picks the next unpicked face in the stack
		if ( adds ) {
			var next = hits.FirstOrDefault( face => !Owner.DebugFaces.Contains( face ) );

			faceIndex = hits.IndexOf( next ?? hits[0] );

			Take( next ?? hits[0], true );

			return;
		}

		faceIndex = repeat ? (faceIndex + 1) % hits.Count : 0;

		Take( hits[faceIndex], false );
	}

	// Application not Gizmo — Gizmo.Active is null outside a gizmo scope
	static bool Adds => global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl )
		|| global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift );

	bool Adding => adding || Adds;

	void Take( ArchFaceDetail face, bool adding ) {
		var picked = Owner.DebugFaces.ToList();

		if ( !adding ) {
			picked.Clear();
			picked.Add( face );
		} else if ( !picked.Remove( face ) ) {
			picked.Add( face );
		}

		Owner.SelectFaces( picked );

		ArchTool.ShowGeometry = true;

		ArchLayersDockPanel.Open();
		Refresh();
	}

	void PickFromStack() {
		if ( stack.Count == 0 ) {
			Owner.Select( null );

			return;
		}

		var hit = stack[Math.Min( stackIndex, stack.Count - 1 )];
		var node = Owner.LayerTree.Find( hit.Layer.ItemId );

		if ( node is null ) {
			Owner.Select( null );

			return;
		}

		Owner.Select( new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building } );
	}

	ArchSelection PickInSection() {
		if ( Owner.EditedBuilding() is not { } building || !Owner.Cursor( out var cursor ) ) {
			return null;
		}

		var cut = Owner.Section( building );

		if ( cut.At( cut.Plane.Across( cursor.Plan ), cursor.Height ) is not { } piece ) {
			return null;
		}

		var room = building.Rooms.FirstOrDefault( candidate => candidate.Floor == piece.Floor
			&& (ReferenceEquals( candidate, piece.Item )
				|| candidate.Walls.Any( wall => ReferenceEquals( wall, piece.Item ) || wall.Openings.Any( opening => ReferenceEquals( opening, piece.Item ) ) )) );

		return new ArchSelection { Item = piece.Item, Room = room, Building = building };
	}

	protected override void BuildOptions( ToolSidebarWidget panel ) {
		ArchSidebarSection.Show( panel, Scope( "pick" ), "Pick", group => {
			using ( var grid = ArchIconGrid.In( group ) ) {
				grid.Pick( "Elements — the authored parts: buildings, rooms, walls, the things you drew", "select_element", "ads_click",
					!geometry, () => { geometry = false; Owner.SelectFaces( null ); Refresh(); } );

				grid.Pick( "Geometry — the built FACE under the cursor, for debugging a z-fight or a buried fitting", "select_face", "crop_square",
					geometry, () => { geometry = true; Refresh(); } );
			}

			if ( geometry ) {
				group.Add( ArchPartUi.Check( "Add each click to the pick (or hold Ctrl / Shift)", adding,
					value => adding = value ) );
			}
		} );

		if ( geometry ) {
			BuildFaceOptions( panel );

			return;
		}

		if ( stackOpen && stack.Count > 0 ) {
			var group = panel.AddGroup( "Objects under cursor" );

			for ( var index = 0; index < stack.Count; index++ ) {
				var captured = index;
				var hit = stack[index];
				var node = Owner.LayerTree.Find( hit.Layer.ItemId );
				var name = node is null ? hit.Layer.Kind.ToString() : Owner.LayerTree.Breadcrumb( node );

				group.Add( new Button( $"{name} · {hit.Channel}" ) {
					Clicked = () => {
						stackIndex = captured;
						PickFromStack();
						Refresh();
					}
				} );
			}
		}

		if ( Owner.Picked is not { Item: not null } picked ) {
			return;
		}

		if ( Owner.LayerTree.Find( picked.Item ) is { } layer && Authoring( layer.Kind ) is { Adopts: true } authoring && authoring != this ) {
			authoring.BuildAdopted( panel, picked, Refresh );

			return;
		}

		ArchPartUi.Sheet( panel, Owner, picked, Refresh );
	}

	void BuildFaceOptions( ToolSidebarWidget panel ) {
		if ( faces.Count > 0 ) {
			var under = panel.AddGroup( "Under the cursor" );

			for ( var index = 0; index < faces.Count; index++ ) {
				var captured = index;
				var face = faces[index];
				var picked = Owner.DebugFaces.Contains( face );

				under.Add( new Button( $"{(picked ? "✓ " : "")}{face.Piece.Split( '/' )[^1]} · {face.Role} · {face.Plane}" ) {
					Clicked = () => {
						faceIndex = captured;
						Take( face, Adding );
					}
				} );
			}
		}

		if ( Owner.DebugFaces.Count == 0 ) {
			panel.AddGroup( "Face" ).Add( Wrapped( "Nothing picked. Click any built surface — ceilings and soffits included. Ctrl+click adds, clicking again drops." ) );

			return;
		}

		var group = panel.AddGroup( $"{Owner.DebugFaces.Count} picked" );

		foreach ( var face in Owner.DebugFaces.Take( 6 ) ) {
			group.Add( Wrapped( $"{face.Piece.Split( '/' )[^1]} · {face.Label}" ) );
		}

		var fought = Owner.DebugFaces
			.SelectMany( Owner.Fighting )
			.Distinct()
			.Where( face => !Owner.DebugFaces.Contains( face ) )
			.ToList();

		if ( fought.Count > 0 ) {
			group.Add( new Button( $"Add the {fought.Count} face(s) it z-fights", "layers" ) {
				Clicked = () => {
					Owner.SelectFaces( Owner.DebugFaces.Concat( fought ).ToList() );
					Refresh();
				}
			} );
		}

		group.Add( new Button.Primary( "Copy Briefing", "smart_toy" ) { Clicked = () => Log.Info( Owner.CopyFaceDebug( true ) ) } );
		group.Add( new Button( "Copy Debug Info Plain", "content_copy" ) { Clicked = () => Log.Info( Owner.CopyFaceDebug() ) } );
	}
}