Editor/Services/ArchOutput.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchDoorRequest {
	public ArchOpening Opening { get; init; }
	public ArchWall Wall { get; init; }
	public Vector3 HingeLocal { get; init; }
	// Wall leaves inherit yaw from the wall's node; anything else sets its own.
	public Rotation Rotation { get; init; } = Rotation.Identity;
	public float LeafWidth { get; init; }
	public float LeafHeight { get; init; }
	public float LeafThickness { get; init; }
	public bool Mirrored { get; init; }
	public ArchBrush Brush { get; init; }

	public ArchAimedPrefab Art { get; init; }
	public string MaterialGroup { get; init; }

	public bool Hung => Art.Standing;
}

public sealed class ArchGlassRequest {
	public ArchOpening Opening { get; init; }
	public ArchMesh Canvas { get; init; }
}

public sealed class ArchBuiltPart {
	public string Path { get; init; }
	public Transform NodeTransform { get; init; }
	public ArchMesh Canvas { get; init; }
	public List<ArchDoorRequest> Doors { get; } = new();
	public List<ArchGlassRequest> Glass { get; } = new();
	public List<ArchFittingRequest> Fittings { get; } = new();

	public int Owner { get; init; }
	public string Piece { get; init; } = "";

	public ArchCollisionMode? Asked { get; init; }
	public ArchCollisionMode? Scope { get; init; }

	public bool? AskedShadows { get; init; }

	public bool? ScopeShadows { get; init; }

	public object Source { get; init; }
	public ArchCollisionMode Resolved { get; set; } = ArchCollisionMode.Complex;

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

	// Geometry only — author settings ride Settings, not Key
	public ulong Key {
		get {
			var hash = ArchHash.Fold( Canvas.Content, NodeTransform.Position );

			hash = ArchHash.Fold( hash, NodeTransform.Rotation.Forward );

			foreach ( var door in Doors ) {
				hash = ArchHash.Fold( hash, door.Opening?.Id ?? 0 );
				hash = ArchHash.Fold( hash, door.Opening?.StartOpen ?? false );
				hash = ArchHash.Fold( hash, (int)(door.Opening?.Kind ?? default) );
				hash = ArchHash.Fold( hash, door.HingeLocal );
				hash = ArchHash.Fold( hash, door.Rotation.Forward );
				hash = ArchHash.Fold( hash, door.LeafWidth );
				hash = ArchHash.Fold( hash, door.LeafHeight );
				hash = ArchHash.Fold( hash, door.LeafThickness );
				hash = ArchHash.Fold( hash, door.Mirrored );
				hash = ArchHash.Fold( hash, door.Brush.Material?.Name );
				hash = ArchHash.Fold( hash, door.MaterialGroup );
				hash = door.Art.Fold( hash );
			}

			foreach ( var pane in Glass ) {
				hash = ArchHash.Fold( hash, pane.Opening?.Id ?? 0 );
				hash = ArchHash.Fold( hash, pane.Canvas?.Content ?? 0ul );
			}

			foreach ( var fitting in Fittings ) {
				hash = fitting.Fold( hash );
			}

			return hash;
		}
	}

	// Moves no vertex — a toggle writes flags without re-cooking the mesh
	public ulong Settings => ArchHash.Fold( ArchHash.Fold( ArchHash.Seed, (int)Resolved ), ResolvedShadows );
}

public sealed class ArchOutput {
	readonly List<ArchBuiltPart> parts = new();

	readonly Dictionary<string, Transform> frames = new();

	readonly ArchPlan plan;

	ArchCollisionMode? scope;

	bool? shadowScope;

	public ArchOutput( ArchPlan plan = null ) {
		this.plan = plan;
	}

	public IReadOnlyDictionary<string, Transform> Frames => frames;

	public IDisposable Within( ArchCollisionMode? asked ) {
		var held = scope;

		scope = asked ?? held;

		return new ScopeReset( this, held );
	}

	public IDisposable WithinShadows( int itemId ) {
		var held = shadowScope;

		shadowScope = ArchParts.AskedShadows( plan, itemId, "" ) ?? held;

		return new ShadowScopeReset( this, held );
	}

	readonly struct ScopeReset( ArchOutput output, ArchCollisionMode? restore ) : IDisposable {
		public void Dispose() => output.scope = restore;
	}

	readonly struct ShadowScopeReset( ArchOutput output, bool? restore ) : IDisposable {
		public void Dispose() => output.shadowScope = restore;
	}

	public ArchOutput Draw<TPart>(
		string path,
		Transform nodeTransform,
		Transform projection,
		TPart source,
		bool welded,
		Action<ArchMesh, TPart, ArchBuiltPart> create ) {
		var canvas = new ArchMesh( projection ).Splitting();

		if ( welded ) {
			canvas.Welded();
		}

		ArchParts.Split( path, out var owner, out var piece );

		var part = new ArchBuiltPart {
			Path = path,
			NodeTransform = nodeTransform,
			Canvas = canvas,
			Source = source,
			Owner = owner,
			Piece = piece,
			Asked = ArchParts.Asked( plan, owner, piece ) ?? (source as IArchCollides)?.Collision,
			Scope = scope,
			AskedShadows = ArchParts.AskedShadows( plan, owner, piece ),
			ScopeShadows = shadowScope
		};

		create( canvas, source, part );

		File( part, canvas, source );

		return this;
	}

	public ArchOutput Add( string path, Transform nodeTransform, ArchMesh canvas ) {
		if ( canvas is null || !canvas.Emitted ) {
			return this;
		}

		ArchParts.Split( path, out var owner, out var piece );

		File( new ArchBuiltPart {
			Path = path,
			NodeTransform = nodeTransform,
			Canvas = canvas,
			Owner = owner,
			Piece = piece,
			Asked = ArchParts.Asked( plan, owner, piece ),
			Scope = scope,
			AskedShadows = ArchParts.AskedShadows( plan, owner, piece ),
			ScopeShadows = shadowScope
		}, canvas, null );

		return this;
	}

	public ArchOutput Add( string path, Transform nodeTransform, Action<ArchMesh> create ) {
		var canvas = new ArchMesh( Transform.Zero ).Welded().Splitting();

		create( canvas );

		return Add( path, nodeTransform, canvas );
	}

	// An empty host is not filed — the cache would read it as a vanished node
	void File( ArchBuiltPart part, ArchMesh canvas, object source ) {
		if ( !canvas.IsEmpty ) {
			parts.Add( part );
		}

		if ( canvas.Pieces.Count == 0 ) {
			return;
		}

		frames[part.Path] = part.NodeTransform;

		var orphaned = canvas.IsEmpty && (part.Doors.Count > 0 || part.Glass.Count > 0 || part.Fittings.Count > 0)
			? part
			: null;

		foreach ( var piece in canvas.Pieces ) {
			Filed( part, source, piece, "", ref orphaned );
		}
	}

	void Filed( ArchBuiltPart host, object source, ArchMeshPiece piece, string under, ref ArchBuiltPart orphaned ) {
		var below = ArchParts.Joined( under, piece.Name );
		var named = ArchParts.Joined( host.Piece, below );

		var part = new ArchBuiltPart {
			Path = $"{host.Path}/{below}",
			NodeTransform = Transform.Zero,
			Canvas = piece.Canvas,
			Source = source,
			Owner = host.Owner,
			Piece = named,
			Asked = ArchParts.Asked( plan, host.Owner, named ) ?? host.Asked,
			Scope = host.Scope,
			AskedShadows = ArchParts.AskedShadows( plan, host.Owner, named ) ?? host.AskedShadows,
			ScopeShadows = host.ScopeShadows
		};

		if ( !piece.Canvas.IsEmpty ) {
			if ( orphaned is not null ) {
				part.Doors.AddRange( orphaned.Doors );
				part.Glass.AddRange( orphaned.Glass );
				part.Fittings.AddRange( orphaned.Fittings );
				orphaned = null;
			}

			parts.Add( part );
		}

		foreach ( var nested in piece.Canvas.Pieces ) {
			Filed( host, source, nested, below, ref orphaned );
		}
	}

	public IReadOnlyList<ArchBuiltPart> Create() {
		return parts.ToList();
	}
}