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

namespace Sunless.Architecture;

// Tabbable dock beside the Hierarchy; view controls live on the toolbar.
[Dock( "Editor", "Architecture", "domain" )]
public sealed class ArchDockPanel : Widget {
	public const string DockName = "Architecture";

	Layout targets;
	Layout context;
	Layout actions;

	object trackedItem;
	int trackedLevel = int.MinValue;
	int trackedRoom = int.MinValue;
	int trackedBuilding = int.MinValue;
	int trackedParts = int.MinValue;
	bool trackedActive;

	public ArchDockPanel( Widget parent ) : base( parent ) {
		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 4;

		targets = Layout.AddColumn();
		Layout.AddSeparator();
		context = Layout.AddColumn();
		Layout.AddSeparator();
		actions = Layout.AddColumn();
		Layout.AddStretchCell();

		Rebuild();
	}

	public static void Open() {
		EditorWindow.DockManager.SetDockState( DockName, true );
		EditorWindow.DockManager.RaiseDock( DockName );
	}

	// The dock outlives the tool, so it reads whichever Architecture tab is up and says so when none is.
	static ArchTool Tool => ArchTool.Active;

	[EditorEvent.Frame]
	void OnFrame() {
		var tool = Tool;
		var item = tool?.Picked?.Item;

		// NextId doubles as a stamp for "something was added" - the panel has to notice additions.
		var parts = tool?.Plan.NextId ?? 0;

		if ( ReferenceEquals( item, trackedItem )
			&& (tool is not null) == trackedActive
			&& (tool?.Level ?? int.MinValue) == trackedLevel
			&& (tool?.ActiveRoomId ?? int.MinValue) == trackedRoom
			&& (tool?.ActiveBuildingId ?? int.MinValue) == trackedBuilding
			&& parts == trackedParts ) {
			return;
		}

		trackedActive = tool is not null;
		trackedItem = item;
		trackedLevel = tool?.Level ?? int.MinValue;
		trackedRoom = tool?.ActiveRoomId ?? int.MinValue;
		trackedBuilding = tool?.ActiveBuildingId ?? int.MinValue;
		trackedParts = parts;

		Rebuild();
	}

	void Rebuild() {
		targets.Clear( true );
		context.Clear( true );
		actions.Clear( true );

		if ( Tool is not { } tool ) {
			targets.Add( ArchPartUi.Wrapped( "Pick the Architecture or Roads tool in the scene view to edit this scene's plan." ) );

			return;
		}

		tool.BuildTargets( targets, Rebuild );

		BuildStatus( tool );
		BuildActions( tool );
	}

	// The status line: what the document is, not what is selected - that is the Layer Inspector's.
	void BuildStatus( ArchTool tool ) {
		var layers = tool.Plan.Layers.Count;
		var groups = tool.Plan.Assemblies.Count;

		var line = new Label( $"{tool.Plan.Buildings.Count} buildings · {tool.Plan.Roads().Count} roads · {groups} groups · {layers} layer records" );
		line.SetStyles( "color: rgba(255,255,255,0.45);" );
		line.WordWrap = true;

		context.Add( line );
	}

	void BuildActions( ArchTool tool ) {
		BuildBakeSwitch( tool );

		var row = actions.AddRow();
		row.Spacing = 4;

		row.Add( new Button.Primary( "Rebuild", "refresh" ) { Clicked = () => tool.RebuildCold() } );
		// Disown first: both edit the built meshes, so what is standing stops being what the tool emitted and the
		// next rebuild has to emit all of it rather than leave the polish in place.
		row.Add( new Button( "Bake", "merge_type" ) {
			ToolTip = "Merge each room's meshes into one per collision-and-shadow pair. Doors and Solids parts stay as they are. The parts it merged stay in the scene switched off, so you can swap back to them.",
			Clicked = () => {
				tool.Disown();
				Log.Info( $"Architecture: merged {ArchBake.MergeScene( tool.Scene )} meshes." );

				tool.ShowBaked = true;
				tool.ApplyVisibility();
				Rebuild();
			}
		} );

		row.Add( new Button( "", "cleaning_services" ) {
			ToolTip = "Delete hidden interior faces",
			Clicked = () => { tool.Disown(); Log.Info( $"Architecture: deleted {ArchCull.Clean( tool.Scene, tool.Plan, tool.Kit )} hidden faces." ); }
		} );
		row.Add( new Button( "", "vertical_align_top" ) {
			ToolTip = "Open room under the ground so a stamp has somewhere to carve to. The surface does not move. Not undoable.",
			Clicked = () => ArchTerrain.Headroom( tool.Scene )
		} );
		row.Add( new Button( "", "landscape" ) {
			ToolTip = "Stamp the heightmap under every building, road and drive. Not undoable.",
			Clicked = () => ArchTerrain.Flatten( tool.Scene, tool.Plan, tool.Kit )
		} );
		row.Add( new Button( "", "content_copy" ) {
			ToolTip = "Copy wall debug - names, endpoints, grid status and resolved joins - to the clipboard",
			Clicked = () => CopyWallDebug( tool )
		} );
		row.Add( new Button( "", "grid_on" ) {
			ToolTip = "Copy mesh debug for the selected objects - face counts, materials, texel scales and every face whose texture axes came out NaN, collapsed or off its own plane",
			Clicked = () => CopyMeshDebug( tool )
		} );
		row.Add( new Button( "", "palette" ) { ToolTip = "Load map palette", Clicked = () => LoadMapPalette( tool ) } );
		row.Add( new Button( "", "tune" ) {
			ToolTip = "Which subtools stand on the shelves",
			Clicked = () => new ArchShelfSettingsDialog( this ).Show()
		} );
		row.Add( new Button.Danger( "", "delete_forever" ) { ToolTip = "Clear plan", Clicked = () => { tool.ClearPlan(); Rebuild(); } } );
		row.AddStretchCell();
	}

	// Only there once a bake is standing, because a switch between two things when only one of them exists is a
	// control that reads as broken. It carries the Unbake because that is the other half of the same question.
	void BuildBakeSwitch( ArchTool tool ) {
		if ( !ArchBake.Standing( ArchScene.FindRoot( tool.Scene ) ) ) {
			return;
		}

		var row = actions.AddRow();
		row.Spacing = 4;

		var switched = new Button.Primary( tool.ShowBaked ? "Showing Baked" : "Showing Unbaked",
			tool.ShowBaked ? "merge_type" : "call_split" ) {
			ToolTip = "Swap between the merged copy and the parts it merged. Both are in the scene - one is switched off.",
			Clicked = () => { tool.ShowBaked = !tool.ShowBaked; tool.ApplyVisibility(); Rebuild(); }
		};

		row.Add( switched );

		row.Add( new Button( "", "layers_clear" ) {
			ToolTip = "Drop the merged copies and hand every source back. No rebuild needed.",
			Clicked = () => {
				Log.Info( $"Architecture: dropped {ArchBake.Unbake( ArchScene.FindRoot( tool.Scene ) )} baked meshes." );
				Rebuild();
			}
		} );

		row.AddStretchCell();
	}

	void CopyWallDebug( ArchTool tool ) {
		var report = ArchReport.WallDebug( tool.Plan, tool.Kit );

		EditorUtility.Clipboard.Copy( report );
		Log.Info( $"Architecture: wall debug copied to the clipboard ({report.Length} characters)." );
	}

	// The whole selection and everything under it: one building's geometry is a tree of meshes, and the
	// face that reads wrong is never the one the object list has highlighted.
	void CopyMeshDebug( ArchTool tool ) {
		var picked = SceneEditorSession.Active?.Selection.OfType<GameObject>().ToList() ?? new List<GameObject>();
		var meshes = picked.Count > 0
			? picked.SelectMany( target => target.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) )
			: tool.Scene.GetAllObjects( false ).SelectMany( target => target.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) );

		var report = ArchReport.MeshDebug( meshes.Distinct() );

		EditorUtility.Clipboard.Copy( report );
		Log.Info( $"Architecture: mesh debug copied to the clipboard ({report.Length} characters)." );
	}

	void LoadMapPalette( ArchTool tool ) {
		ArchMapPalette.ShowPicker( this, tool.Kit.Palette, () => {
			ArchStyle.InvalidateCache();
			ArchStorage.SaveKit( tool.Kit );
			tool.Commit();
			Rebuild();
		} );
	}
}