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

namespace Sunless.Architecture;

public enum CabinetStanding {
	Backed,
	Free
}

public static class ArchCabinets {
	public static ArchCabinetPart Place( ArchPlan plan, ArchKit kit, ArchFixtureStation? at, ArchCabinetPart draft, out ArchRoom host ) {
		host = null;

		if ( plan is null || draft is null || at is not { } station ) {
			return null;
		}

		host = station.Room;

		var thickness = station.Wall.Thickness > 0f ? station.Wall.Thickness : kit.WallThickness;
		var facing = Inward( station );

		return Drafted( plan, host, station.Room.Floor, station.Centre + facing * (thickness * 0.5f), facing,
			station.Width, draft );
	}

	public static ArchCabinetPart Free( ArchPlan plan, ArchRoom room, ArchGridService grid, Vector2 from, Vector2 to,
		int quarters, float module, ArchCabinetPart draft ) {
		if ( plan is null || room is null || grid is null || draft is null ) {
			return null;
		}

		var facing = Turned( Inward( plan, room, from ), quarters );
		var along = new Vector2( -facing.y, facing.x );
		var travelled = Vector2.Dot( to - from, along );
		var least = MathF.Max( ArchFixtureAnchor.Least, grid.Subgrid( module, 4 ) );

		if ( MathF.Abs( travelled ) < grid.SubgridSize() ) {
			return Drafted( plan, room, room.Floor, from, facing, least, draft );
		}

		return Drafted( plan, room, room.Floor, from + along * (travelled * 0.5f), facing,
			MathF.Max( least, MathF.Abs( travelled ) ), draft );
	}

	public static Vector2 Turned( Vector2 facing, int quarters ) {
		return ((quarters % 4 + 4) % 4) switch {
			1 => new Vector2( -facing.y, facing.x ),
			2 => -facing,
			3 => new Vector2( facing.y, -facing.x ),
			_ => facing
		};
	}

	public static ArchCabinetPart Continues( ArchPlan plan, ArchRoom room, ArchCabinetPart placed, float module ) {
		if ( plan is null || room is null || placed is null ) {
			return null;
		}

		var facing = placed.Facing;
		var along = placed.Along;
		var slack = MathF.Max( 1f, module * 0.5f );

		foreach ( var standing in plan.Filed<ArchCabinetPart>( room ) ) {
			if ( ReferenceEquals( standing, placed ) || standing.Level != placed.Level ) {
				continue;
			}

			if ( Vector2.Dot( standing.Facing, facing ) < 0.999f || !Alike( standing, placed ) ) {
				continue;
			}

			// On the same back line as well as looking the same way, or two runs on opposite sides of a partition
			// - or one standing off the wall as an island - are read as continuing each other.
			if ( MathF.Abs( Vector2.Dot( standing.Anchor - placed.Anchor, facing ) ) > 1f ) {
				continue;
			}

			if ( MathF.Abs( Vector2.Dot( standing.Anchor - placed.Anchor, along ) )
				> (standing.Run + placed.Run) * 0.5f + slack ) {
				continue;
			}

			return standing;
		}

		return null;
	}

	public static void Absorb( ArchCabinetPart standing, ArchCabinetPart placed ) {
		var along = standing.Along;
		var offset = Vector2.Dot( placed.Anchor - standing.Anchor, along );
		var from = MathF.Min( -standing.Run * 0.5f, offset - placed.Run * 0.5f );
		var to = MathF.Max( standing.Run * 0.5f, offset + placed.Run * 0.5f );

		standing.Anchor += along * ((from + to) * 0.5f);
		standing.Width = to - from;
	}

	public static ArchCabinetPart Joined( ArchCabinetPart standing, ArchCabinetPart placed ) {
		var grown = Copy( standing );

		Absorb( grown, placed );

		return grown;
	}

	static bool Alike( ArchCabinetPart standing, ArchCabinetPart placed ) {
		if ( !string.Equals( standing.Type, placed.Type, StringComparison.OrdinalIgnoreCase ) || standing.Tier != placed.Tier ) {
			return false;
		}

		return standing.Seated == placed.Seated
			&& (!standing.Seated || MathF.Abs( standing.BaseHeight - placed.BaseHeight ) < 0.01f);
	}

	public static ArchCabinetShape Shaped( ArchPlan plan, ArchKit kit, ArchCabinetPart sketch, ArchRoom room ) {
		if ( sketch is null || room is null ) {
			return null;
		}

		var shape = ArchCabinetShape.Resolve( plan, kit, room, plan?.OwnerOf( room ), sketch );

		return shape.Standing ? shape : null;
	}

	public static bool Preview( ArchPlan plan, ArchKit kit, ArchCabinetPart sketch, ArchRoom room ) {
		if ( Shaped( plan, kit, sketch, room ) is not { } shape ) {
			return false;
		}

		var building = plan?.OwnerOf( room );

		var lift = ArchAnswers.Load().Lift( plan, building, kit );
		var seat = shape.Seat + lift;
		var head = shape.Head + lift;

		if ( shape.Cells.Count > 0 ) {
			ArchGhost.Prism( shape.Outline(), seat, head );
		}

		foreach ( var joint in shape.Joints ) {
			ArchGhost.Prism( joint.Loop, joint.Foot + lift, joint.Head + lift );
		}

		foreach ( var cell in shape.Cells.Skip( 1 ) ) {
			ArchGhost.Path( new List<Vector3>
			{
				new( cell.Start.x, cell.Start.y, seat ),
				new( cell.Start.x, cell.Start.y, head )
			}, false );
		}

		ArchGhost.Note( new Vector3( shape.End.x, shape.End.y, head ), shape.Cells.Count > 0
			? $"{shape.Type.Name} — {shape.Cells.Count} × {shape.Cells[0].Width:0} at {seat:0}"
			: $"{shape.Type.Name} — corner only at {seat:0}" );

		return true;
	}

	static ArchCabinetPart Drafted( ArchPlan plan, ArchRoom room, int level, Vector2 anchor, Vector2 facing,
		float width, ArchCabinetPart draft ) {
		return new ArchCabinetPart {
			Id = plan.AllocateId(),
			Name = $"Cabinets{plan.Filed<ArchCabinetPart>( room ).Count() + 1}",
			Level = level,
			Anchor = anchor,
			Outward = facing,
			Width = width,
			Tier = draft.Tier,
			Type = draft.Type,
			ModuleWidth = draft.ModuleWidth,
			Depth = draft.Depth,
			TopDepth = draft.TopDepth,
			Height = draft.Height,
			Interior = draft.Interior,
			Shelves = draft.Shelves,
			DrawerRows = draft.DrawerRows,
			Handles = draft.Handles,
			TopThickness = draft.TopThickness,
			TopEndOversail = draft.TopEndOversail,
			TopBackOversail = draft.TopBackOversail,
			MaterialGroup = draft.MaterialGroup,
			Fittings = draft.Fittings?.Select( Copy ).ToList() ?? new List<ArchCabinetFitting>()
		};
	}

	// Inverted — cabinets face INTO the room
	static Vector2 Inward( ArchFixtureStation station ) {
		var outward = station.Outward;

		return outward.Length < 0.01f ? new Vector2( 1f, 0f ) : -outward.Normal;
	}

	// Free-standing: face out of nearest wall, toward the aim
	static Vector2 Inward( ArchPlan plan, ArchRoom room, Vector2 point ) {
		var facing = new Vector2( 1f, 0f );
		var nearest = float.MaxValue;

		foreach ( var wall in plan.Filed<ArchWall>( room ) ) {
			var length = wall.Length;

			if ( length < 0.5f ) {
				continue;
			}

			var along = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );
			var offset = point - wall.PointAt( along );

			if ( offset.Length >= nearest ) {
				continue;
			}

			nearest = offset.Length;
			facing = Vector2.Dot( wall.Normal, offset ) < 0f ? -wall.Normal : wall.Normal;
		}

		return facing;
	}

	static ArchCabinetPart Copy( ArchCabinetPart part ) {
		return new ArchCabinetPart {
			Id = part.Id,
			Name = part.Name,
			Level = part.Level,
			Anchor = part.Anchor,
			Outward = part.Outward,
			Width = part.Width,
			Tier = part.Tier,
			Type = part.Type,
			ModuleWidth = part.ModuleWidth,
			Depth = part.Depth,
			TopDepth = part.TopDepth,
			Height = part.Height,
			BaseHeight = part.BaseHeight,
			Seated = part.Seated,
			Interior = part.Interior,
			Shelves = part.Shelves,
			DrawerRows = part.DrawerRows,
			Handles = part.Handles,
			TopThickness = part.TopThickness,
			TopEndOversail = part.TopEndOversail,
			TopBackOversail = part.TopBackOversail,
			MaterialGroup = part.MaterialGroup,
			Palette = part.Palette,
			Fittings = part.Fittings?.Select( Copy ).ToList() ?? new List<ArchCabinetFitting>()
		};
	}

	static ArchCabinetFitting Copy( ArchCabinetFitting fitting ) {
		return new ArchCabinetFitting {
			Mount = fitting.Mount,
			Front = fitting.Front,
			Prefab = fitting.Prefab,
			Fits = fitting.Fits,
			Anchor = fitting.Anchor,
			AnchorAlong = fitting.AnchorAlong,
			AnchorTurn = fitting.AnchorTurn
		};
	}
}