Editor/Cabinet/ArchCabinetFittingRequest.cs
using Sandbox;

namespace Sunless.Architecture;

// Where on the author's own prefab the mount takes hold, in the PREFAB's local space. A picked vertex gives a
// point and nothing else; a picked edge gives its midpoint and the direction it runs in.
public readonly record struct ArchCabinetAnchor( Vector3 Point, Vector3 Along ) {
	public static ArchCabinetAnchor None => default;

	public bool Directed => Along.Length > 0.001f;
}

// ANYTHING AN AUTHOR AIMS A PREFAB WITH. A front, a whole carcass and a run's own override all answer the same five
// questions, so the aiming viewport and the landing solve are written once and none of them knows which it is
// holding.
public interface IArchAimedPrefab {
	string Prefab { get; }
	Vector3 Anchor { get; }
	Vector3 AnchorAlong { get; }
	Angles AnchorTurn { get; }
	bool Scales { get; }

	bool Standing => !string.IsNullOrWhiteSpace( Prefab );

	bool Anchored => Anchor != Vector3.Zero || AnchorAlong != Vector3.Zero;

	bool OnAnEdge => AnchorAlong.Length > 0.01f;
}

// The five answers as a VALUE, for a preset that keeps them as flat fields rather than as an object of its own -
// an opening's leaf art. Folded into a build key, so value semantics are the point.
public readonly record struct ArchAimedPrefab(
	string Prefab,
	Vector3 Anchor,
	Vector3 AnchorAlong,
	Angles AnchorTurn,
	bool Scales ) : IArchAimedPrefab {
	public bool Standing => !string.IsNullOrWhiteSpace( Prefab );

	public bool Anchored => Anchor != Vector3.Zero || AnchorAlong != Vector3.Zero;

	public bool OnAnEdge => AnchorAlong.Length > 0.01f;

	public ulong Fold( ulong hash ) {
		hash = ArchHash.Fold( hash, Prefab );
		hash = ArchHash.Fold( hash, Anchor );
		hash = ArchHash.Fold( hash, AnchorAlong );
		hash = ArchHash.Fold( hash, new Vector3( AnchorTurn.pitch, AnchorTurn.yaw, AnchorTurn.roll ) );

		return ArchHash.Fold( hash, Scales );
	}
}

// A prefab to hang on one mount of one module. The tool draws nothing for it - art the author made beats anything
// extruded - so this is the whole instruction, and the scene pass is the only thing that reads it.
public sealed class ArchFittingRequest {
	public string Name { get; init; }
	public string Prefab { get; init; }

	// The mount's own frame: origin at the top of the opening, at the carcass face, at the run-start end, with
	// Forward out of the face, Left along the run and Up up.
	public Transform Mount { get; init; }
	public Vector3 Space { get; init; }

	// Off is the honest default: a front modelled at a real size stands at that size, and squashing one into a
	// module it does not fit is a thing the author has to ask for.
	public bool Fits { get; init; }

	// Written onto every renderer the instance carries, so one row of art re-finishes without a second prefab.
	// Empty leaves whatever the prefab was authored with.
	public string MaterialGroup { get; init; }

	public ArchCabinetAnchor Anchor { get; init; }

	// What the author turned the art by in the designer, which is the only answer a VERTEX pick can be given: a
	// point carries no direction, so nothing else can tell a front modelled face-on from one modelled edge-on.
	public Angles Turn { get; init; }

	// The anchor point lands on the mount origin and the prefab stands in the mount's frame - its own x out of the
	// cabinet face, y along the run, z up. Solving the position AFTER the scale is what keeps the picked point
	// still whether the prefab was fitted or left alone.
	public Transform Standing( BBox bounds ) {
		var rotation = Turned();
		var scale = Fits ? Filled( bounds ) : Vector3.One;

		return new Transform( Mount.Position - rotation * (Anchor.Point * scale), rotation, scale );
	}

	public ulong Fold( ulong hash ) {
		hash = ArchHash.Fold( hash, Name );
		hash = ArchHash.Fold( hash, Prefab );
		hash = ArchHash.Fold( hash, Mount.Position );
		hash = ArchHash.Fold( hash, Mount.Rotation.Forward );
		hash = ArchHash.Fold( hash, Mount.Rotation.Up );
		hash = ArchHash.Fold( hash, Space );
		hash = ArchHash.Fold( hash, Fits );
		hash = ArchHash.Fold( hash, MaterialGroup );
		hash = ArchHash.Fold( hash, Anchor.Point );
		hash = ArchHash.Fold( hash, Anchor.Along );

		return ArchHash.Fold( hash, new Vector3( Turn.pitch, Turn.yaw, Turn.roll ) );
	}

	// A picked edge squares the prefab up by the SHORTEST turn onto the mount axis it already points nearest, so
	// art modelled straight is left exactly as it is and art a few degrees out is corrected rather than spun. A
	// vertex carries no direction at all, so it leaves the prefab standing as the mount's frame puts it.
	Rotation Turned() {
		var nudged = Mount.Rotation * Turn.ToRotation();

		if ( !Anchor.Directed ) {
			return nudged;
		}

		var pointing = nudged * Anchor.Along.Normal;
		var nearest = Nearest( pointing );

		return pointing.Dot( nearest ) > 0.9999f
			? nudged
			: Rotation.FromToRotation( pointing, nearest ) * nudged;
	}

	Vector3 Nearest( Vector3 pointing ) {
		var axes = new[]
		{
			Mount.Rotation.Forward, -Mount.Rotation.Forward,
			Mount.Rotation.Left, -Mount.Rotation.Left,
			Mount.Rotation.Up, -Mount.Rotation.Up
		};

		var best = axes[0];

		foreach ( var axis in axes ) {
			if ( pointing.Dot( axis ) > pointing.Dot( best ) ) {
				best = axis;
			}
		}

		return best;
	}

	Vector3 Filled( BBox bounds ) {
		var size = bounds.Size;

		return new Vector3( Fitted( size.x, Space.x ), Fitted( size.y, Space.y ), Fitted( size.z, Space.z ) );
	}

	static float Fitted( float modelled, float room ) {
		return modelled > 0.01f && room > 0.01f ? room / modelled : 1f;
	}
}