Editor/Cabinet/ArchCabinetGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Carcasses along a wall, emitted as named pieces so the counter can be stone over timber boxes without being a
// part of its own. Every solid goes through ArchMesh.Beam, which lofts its box along the line it is given - a run
// on a wall that does not lie on the grid is the same code as one that does.
//
// Nothing here works out where a front goes: the mounts on the resolved shape say, and a prefab hung on one lands
// exactly where the extruded front it replaced did.
public static class ArchCabinetGen {
	public static void Build(
		ArchMesh canvas,
		ArchBuiltPart built,
		ArchCabinetPart part,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style ) {
		var shape = ArchCabinetShape.Resolve( plan, kit, room, building, part );

		if ( !shape.Standing ) {
			return;
		}

		if ( shape.Type.Prefabbed ) {
			Stand( built, shape, part );

			return;
		}

		var chain = new[] { part.Palette, room?.Palette, building?.Palette }.Where( palette => palette is not null ).ToArray();
		var carcass = style.Brush( ArchSurface.CabinetCarcass, chain );
		var fronts = style.Brush( ArchSurface.CabinetFront, chain );
		var counter = style.Brush( ArchSurface.Worktop, chain );
		var ironmongery = style.Brush( ArchSurface.CabinetHandle, chain );
		var inside = part.Interior || shape.Type.Interior;

		foreach ( var cell in shape.Cells ) {
			using ( canvas.Part( ArchPieces.Carcass ) ) {
				Box( canvas, shape, cell, carcass, inside );
			}

			using ( canvas.Part( ArchPieces.Fronts ) ) {
				Fronts( canvas, built, shape, cell, part, fronts, ironmongery );
			}

			if ( inside && shape.Shelves > 0 ) {
				using ( canvas.Part( ArchPieces.Shelves ) ) {
					Shelves( canvas, shape, cell, carcass );
				}
			}

			if ( cell.Mount( ArchCabinetMounts.Plinth ) is { } plinth ) {
				using ( canvas.Part( ArchPieces.Plinth ) ) {
					Solid( canvas, plinth, carcass );
				}
			}

			if ( cell.Mount( ArchCabinetMounts.Worktop ) is { } top ) {
				using ( canvas.Part( ArchPieces.Worktop ) ) {
					Solid( canvas, top, counter );
				}
			}

			if ( cell.Mount( ArchCabinetMounts.Cornice ) is { } crown ) {
				using ( canvas.Part( ArchPieces.Cornice ) ) {
					Solid( canvas, crown, fronts );
				}
			}
		}

		foreach ( var joint in shape.Joints ) {
			Corner( canvas, built, joint, part, shape.Type, carcass, fronts, counter, ironmongery );
		}
	}

	// ART INSTEAD OF GEOMETRY. The run divided itself and found its corner exactly as a generated one does; what
	// stands in each module is the author's own model, asked for here and hung by the scene pass. Nothing is drawn -
	// a box under a prefab is a second cabinet inside the one the author made.
	static void Stand( ArchBuiltPart built, ArchCabinetShape shape, ArchCabinetPart part ) {
		var group = string.IsNullOrWhiteSpace( part.MaterialGroup ) ? shape.Type.MaterialGroup : part.MaterialGroup;

		foreach ( var cell in shape.Cells ) {
			if ( cell.Mount( ArchCabinetMounts.Carcass ) is { } mount ) {
				Asked( built, shape.Type.In( cell.Slot ), mount, ArchNames.Fitting( cell.Index, ArchCabinetMounts.Carcass ), group );
			}
		}

		foreach ( var joint in shape.Joints ) {
			if ( joint.Mount( ArchCabinetMounts.CornerCarcass ) is { } mount ) {
				Asked( built, shape.Type.In( CabinetSlot.Corner ), mount,
					ArchNames.Fitting( joint.Index, ArchCabinetMounts.CornerCarcass ), group );
			}
		}
	}

	static void Asked( ArchBuiltPart built, ArchCabinetCarcass carcass, ArchCabinetMount mount, string name, string group ) {
		if ( carcass is not { Standing: true } ) {
			return;
		}

		built?.Fittings.Add( new ArchFittingRequest {
			Name = name,
			Prefab = carcass.Prefab,
			Mount = mount.Local,
			Space = mount.Space,
			Fits = carcass.Fits,
			MaterialGroup = group,
			Anchor = new ArchCabinetAnchor( carcass.Anchor, carcass.AnchorAlong ),
			Turn = carcass.AnchorTurn
		} );
	}

	// The unit standing where two runs meet: a pentagon carcass with two backs on the walls, a return onto each run
	// and the 45° front across the inside corner. Its top is the piece that carries the counter round the L, so it
	// is emitted whether or not the front is the tool's - a prefabbed corner door replaces the slab and nothing else.
	static void Corner( ArchMesh canvas, ArchBuiltPart built, ArchCabinetJoint joint, ArchCabinetPart part,
		ArchCabinetType type, ArchBrush carcass, ArchBrush fronts, ArchBrush counter, ArchBrush ironmongery ) {
		using ( canvas.Part( ArchPieces.Carcass ) ) {
			Prism( canvas, joint.Loop, joint.Foot, joint.Head, carcass );
		}

		if ( joint.Plinth > 0.01f ) {
			using ( canvas.Part( ArchPieces.Plinth ) ) {
				Prism( canvas, joint.Kick, joint.Seat, joint.Seat + joint.Plinth, carcass );
			}
		}

		if ( joint.Cornice > 0.01f ) {
			using ( canvas.Part( ArchPieces.Cornice ) ) {
				Prism( canvas, joint.Crown, joint.Head, joint.Head + joint.Cornice, fronts );
			}
		}

		if ( joint.Counter > 0.01f ) {
			using ( canvas.Part( ArchPieces.Worktop ) ) {
				Prism( canvas, joint.Top, joint.Head, joint.Head + joint.Counter, counter );
			}
		}

		foreach ( var name in new[]
		{
			ArchCabinetMounts.CornerFront, ArchCabinetMounts.CornerFrontUpper, ArchCabinetMounts.CornerDrawer
		} ) {
			if ( joint.Mount( name ) is not { } front ) {
				continue;
			}

			var own = Fitted( part, type, name );
			var following = Fitted( part, type, ArchCabinetMounts.Flat( name ) );

			// ART ONLY EVER HANGS WHERE IT WAS NAMED. A door modelled for a module is the wrong width on a 45° face,
			// so the corner follows the run's extruded STYLE and never its prefab.
			if ( own is { Prefabbed: true } ) {
				built?.Fittings.Add( new ArchFittingRequest {
					Name = ArchNames.Fitting( joint.Index, name ),
					Prefab = own.Prefab,
					Mount = front.Local,
					Space = front.Space,
					Fits = own.Fits,
					Anchor = new ArchCabinetAnchor( own.Anchor, own.AnchorAlong ),
					Turn = own.AnchorTurn
				} );

				continue;
			}

			var style = own?.Front ?? (following is { Prefabbed: false } ? following.Front : CabinetFront.Slab);

			if ( style == CabinetFront.None ) {
				continue;
			}

			using ( canvas.Part( ArchPieces.Fronts ) ) {
				Front( canvas, front, style, type, fronts );
				CornerGrip( canvas, joint, name, type, ironmongery );
			}
		}
	}

	static void CornerGrip( ArchMesh canvas, ArchCabinetJoint joint, string front, ArchCabinetType type, ArchBrush brush ) {
		var name = ArchCabinetMounts.Same( front, ArchCabinetMounts.CornerFrontUpper )
			? ArchCabinetMounts.CornerHandleUpper
			: ArchCabinetMounts.CornerHandle;

		if ( ArchCabinetMounts.Same( front, ArchCabinetMounts.CornerDrawer ) || joint.Mount( name ) is not { } grip ) {
			return;
		}

		Ironmongery( canvas, grip, type.Grip, brush );
	}

	static void Prism( ArchMesh canvas, IReadOnlyList<Vector2> loop, float foot, float head, ArchBrush brush ) {
		if ( loop.Count < 3 || head - foot < 0.05f ) {
			return;
		}

		var bottom = loop.Select( point => new Vector3( point.x, point.y, foot ) ).ToList();
		var top = loop.Select( point => new Vector3( point.x, point.y, head ) ).ToList();

		canvas.Prism( bottom, top, brush );
	}

	// Interior off is ONE solid: five outward faces and nothing a player who never opens it can see. Interior on
	// is the real panels - two ends, a back, a bottom and a top - which is what a door that swings costs.
	static void Box( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, ArchBrush brush, bool inside ) {
		var face = shape.Depth - shape.Type.FrontThickness;

		if ( !inside ) {
			canvas.Beam( cell.Start, cell.End, 0f, face, cell.Foot, cell.Head, brush );

			return;
		}

		var panel = MathF.Max( 0.25f, shape.Type.PanelThickness );
		var inner = InnerRun( shape, cell, panel, out var from, out var to );

		canvas.Beam( cell.Start, cell.Start + shape.Along * panel, 0f, face, cell.Foot, cell.Head, brush );
		canvas.Beam( cell.End - shape.Along * panel, cell.End, 0f, face, cell.Foot, cell.Head, brush );

		if ( !inner ) {
			return;
		}

		canvas.Beam( from, to, 0f, panel, cell.Foot, cell.Head, brush );
		canvas.Beam( from, to, panel, face, cell.Foot, cell.Foot + panel, brush );
		canvas.Beam( from, to, panel, face, cell.Head - panel, cell.Head, brush );
	}

	// Divided by ArchDivide over the clear height, so shelves land evenly whatever the carcass turned out to be -
	// the count is the type's, the spacing is never authored.
	static void Shelves( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, ArchBrush brush ) {
		var panel = MathF.Max( 0.25f, shape.Type.PanelThickness );

		if ( !InnerRun( shape, cell, panel, out var from, out var to ) ) {
			return;
		}

		var foot = cell.Foot + panel;
		var head = cell.Head - panel;
		var clear = head - foot;

		if ( clear < panel * 4f ) {
			return;
		}

		var division = ArchDivide.Into( clear, shape.Shelves + 1 );

		foreach ( var station in division.Inner ) {
			var seat = foot + station;

			canvas.Beam( from, to, panel, shape.Depth - shape.Type.FrontThickness - panel, seat, seat + panel, brush );
		}
	}

	static bool InnerRun( ArchCabinetShape shape, ArchCabinetCell cell, float panel, out Vector2 from, out Vector2 to ) {
		from = cell.Start + shape.Along * panel;
		to = cell.End - shape.Along * panel;

		return (to - from).Length > panel;
	}

	static void Fronts( ArchMesh canvas, ArchBuiltPart built, ArchCabinetShape shape, ArchCabinetCell cell,
		ArchCabinetPart part, ArchBrush brush, ArchBrush ironmongery ) {
		foreach ( var name in new[] { ArchCabinetMounts.Front, ArchCabinetMounts.FrontUpper, ArchCabinetMounts.Drawer } ) {
			if ( cell.Mount( name ) is not { } mount ) {
				continue;
			}

			var fitting = Fitted( part, shape.Type, name );

			if ( fitting is null ) {
				continue;
			}

			// A prefab is not the tool's to draw: it is asked for here and hung by the scene pass, and drawing a
			// slab under it would leave a second door inside the one the author modelled.
			if ( fitting.Prefabbed ) {
				built?.Fittings.Add( new ArchFittingRequest {
					Name = ArchNames.Fitting( cell.Index, name ),
					Prefab = fitting.Prefab,
					Mount = mount.Local,
					Space = mount.Space,
					Fits = fitting.Fits,
					Anchor = new ArchCabinetAnchor( fitting.Anchor, fitting.AnchorAlong ),
					Turn = fitting.AnchorTurn
				} );

				continue;
			}

			if ( fitting.Front == CabinetFront.None ) {
				continue;
			}

			Front( canvas, mount, fitting.Front, shape.Type, brush );
			Grip( canvas, shape, cell, name, ironmongery );
		}
	}

	static void Front( ArchMesh canvas, ArchCabinetMount mount, CabinetFront style, ArchCabinetType type, ArchBrush brush ) {
		if ( style == CabinetFront.Slab ) {
			Solid( canvas, mount, brush );

			return;
		}

		// A framed front is stiles, rails and a field between them - the same four-member walk a grille and a
		// wainscot panel take, held to one thickness so the joint reads at the corner.
		var stile = MathF.Max( 1.5f, MathF.Min( mount.Space.y * 0.3f, 2.5f ) );
		var rail = MathF.Max( 1.5f, MathF.Min( mount.Space.z * 0.3f, 2.5f ) );

		Slice( canvas, mount, 0f, stile, 0f, mount.Space.z, mount.Space.x, brush );
		Slice( canvas, mount, mount.Space.y - stile, mount.Space.y, 0f, mount.Space.z, mount.Space.x, brush );
		Slice( canvas, mount, stile, mount.Space.y - stile, 0f, rail, mount.Space.x, brush );
		Slice( canvas, mount, stile, mount.Space.y - stile, mount.Space.z - rail, mount.Space.z, mount.Space.x, brush );

		if ( style == CabinetFront.Glass ) {
			return;
		}

		if ( style == CabinetFront.Panel ) {
			Fielded( canvas, mount, type, stile, rail, brush );

			return;
		}

		Slice( canvas, mount, stile, mount.Space.y - stile, rail, mount.Space.z - rail, mount.Space.x * 0.4f, brush );
	}

	// A raised panel is TWO steps, not one: the field sinks into the frame and a fielded centre comes back out of
	// it, so the door throws a shadow at its frame and a second one round its own middle.
	static void Fielded( ArchMesh canvas, ArchCabinetMount mount, ArchCabinetType type, float stile, float rail,
		ArchBrush brush ) {
		var thickness = MathF.Max( 0.2f, mount.Space.x );
		var sunk = MathF.Min( MathF.Max( 0.05f, type.PanelInset ), thickness * 0.6f );
		var field = thickness - sunk;
		var raised = MathF.Min( field + MathF.Max( 0f, type.PanelRaise ), thickness - 0.05f );

		Slice( canvas, mount, stile, mount.Space.y - stile, rail, mount.Space.z - rail, field, brush );

		var across = mount.Space.y - stile * 2f;
		var down = mount.Space.z - rail * 2f;
		var margin = MathF.Min( MathF.Max( 0f, type.PanelMargin ), MathF.Min( across, down ) * 0.3f );

		if ( margin < 0.05f || raised - field < 0.05f ) {
			return;
		}

		Slice( canvas, mount, stile + margin, mount.Space.y - stile - margin, rail + margin,
			mount.Space.z - rail - margin, raised, brush );
	}

	// What the door is opened by, in the grip mount's own frame - along it by Space.y, down it by Space.z and out
	// of the face by Space.x.
	static void Ironmongery( ArchMesh canvas, ArchCabinetMount grip, CabinetGrip style, ArchBrush brush ) {
		var stem = MathF.Max( 0.2f, grip.Space.z );

		switch ( style ) {
			case CabinetGrip.Knob:
				var knob = MathF.Min( grip.Space.y, MathF.Max( 0.6f, grip.Space.z * 2f ) );

				Slice( canvas, grip, grip.Space.y - knob, grip.Space.y, 0f, knob, grip.Space.x * 1.4f, brush );

				break;

			case CabinetGrip.Cup:
				Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z, grip.Space.x * 0.35f, brush );
				Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z * 0.45f, grip.Space.x, brush );

				break;

			default:
				Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z, grip.Space.x, brush );
				Slice( canvas, grip, 0f, stem, 0f, grip.Space.z, -grip.Space.x, brush );
				Slice( canvas, grip, grip.Space.y - stem, grip.Space.y, 0f, grip.Space.z, -grip.Space.x, brush );

				break;
		}
	}

	// A grip hangs off the front it belongs to, in whatever the type's ironmongery is - the smallest thing that
	// reads as a handle at the range a kitchen is seen from.
	static void Grip( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, string front, ArchBrush brush ) {
		var name = ArchCabinetMounts.Same( front, ArchCabinetMounts.FrontUpper )
			? ArchCabinetMounts.HandleUpper
			: ArchCabinetMounts.Handle;

		if ( ArchCabinetMounts.Same( front, ArchCabinetMounts.Drawer ) || cell.Mount( name ) is not { } grip ) {
			return;
		}

		Ironmongery( canvas, grip, shape.Type.Grip, brush );
	}

	static void Solid( ArchMesh canvas, ArchCabinetMount mount, ArchBrush brush ) {
		Slice( canvas, mount, 0f, mount.Space.y, 0f, mount.Space.z, mount.Space.x, brush );
	}

	// One box inside a mount's own frame: along it from-to, down from its top, and out of its -x edge. A negative
	// reach goes the other way, which is how a handle's stems stand back into the door.
	static void Slice( ArchMesh canvas, ArchCabinetMount mount, float from, float to, float down, float under,
		float reach, ArchBrush brush ) {
		var along = mount.Local.Rotation.Left;
		var origin = mount.Local.Position + along * from;
		var end = mount.Local.Position + along * to;
		var start = new Vector2( origin.x, origin.y );
		var finish = new Vector2( end.x, end.y );
		var near = MathF.Min( 0f, reach );
		var far = MathF.Max( 0f, reach );

		canvas.Beam( start, finish, near, far, mount.Local.Position.z - under, mount.Local.Position.z - down, brush );
	}

	// The run's own answer first, then the type's. A run says only what it disagrees with, so a retyped run picks
	// every other front up from the type it was pointed at.
	static ArchCabinetFitting Fitted( ArchCabinetPart part, ArchCabinetType type, string mount ) {
		return part.On( mount ) ?? type.On( mount );
	}
}