Editor/Bool/ArchBool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchBoolOp {
	Add,
	Subtract,
	Damage,
	Fixture,
	Detail,
	Extrude
}

public enum ArchBoolAdd {
	Slab,
	Ramp,
	Pillar,
	Beam,
	Slats
}

public sealed class ArchBoolPlacement {
	public ArchBoolOp Op { get; init; }
	public ArchBoolAdd Kind { get; init; }
	public ArchBoolReach Reach { get; init; }
	public ArchRoom Room { get; init; }
	public ArchPlatformPart Platform { get; init; }
	public ArchCutPart Cut { get; init; }
	public ArchPillarPart Pillar { get; init; }
	public ArchBeamPart Beam { get; init; }
}

public enum ArchBoolReach {
	Dragged,
	Through,
	IntoFloor,
	IntoCeiling,
	IntoWall
}

public sealed class ArchBoolDrag {
	public ArchBoolForm Form { get; init; }
	public int Segments { get; init; } = ArchFootprint.LeastSegments;
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }
	public float Bottom { get; init; }
	public float Top { get; init; }
	public float Least { get; init; } = ArchBool.MinSize;

	// Gesture direction — bounds lose the sign, ramp needs it
	public float Climb { get; init; }

	public bool Stands => Max.x - Min.x >= Least && Max.y - Min.y >= Least;

	public float Rise => MathF.Max( 1f, Top - Bottom );

	public List<Vector2> Loop() {
		return Form == ArchBoolForm.Circle
			? ArchFootprint.Ellipse( Min, Max, Segments )
			: ArchFootprint.Rect( Min, Max );
	}
}

public sealed class ArchBoolHost {
	public ArchBuilding Building { get; init; }
	public ArchRoadPart Road { get; init; }

	public int Id => Road?.Id ?? Building?.Id ?? 0;

	public bool Stands => Building is not null || Road is not null;

	public List<ArchCutPart> Cuts => Road?.Cuts ?? Building?.Cuts;

	// Roads have no storey
	public int Storey( int level ) => Road is null ? level : 0;
}

public static class ArchBool {
	public const float MinSize = ArchPlatform.MinSize;

	// Zones need a smaller minimum than solids (detail features like joints)
	public const float ZoneSize = 4f;

	public static float Least( ArchBoolOp op ) => op switch {
		ArchBoolOp.Damage or ArchBoolOp.Extrude => ZoneSize,
		_ => MinSize
	};

	public static ArchBoolHost Host( ArchPlan plan, ArchBuilding preferred, IReadOnlyList<Vector2> outline ) {
		if ( plan is null || outline is not { Count: >= 3 } ) {
			return new ArchBoolHost { Building = preferred };
		}

		return Host( plan, preferred, outline.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / outline.Count );
	}

	public static ArchBoolHost Host( ArchPlan plan, ArchBuilding preferred, Vector2 centre ) {
		if ( plan is null ) {
			return new ArchBoolHost { Building = preferred };
		}

		var standing = plan.Buildings
			.Where( building => building.Rooms.Any( room => room.HasFootprint && ArchFootprint.Contains( ArchFloorGen.Footprint( room ), centre ) )
				|| building.Platforms.Any( platform => ArchFootprint.Contains( platform.Outline(), centre ) ) )
			.ToList();

		if ( standing.Contains( preferred ) ) {
			return new ArchBoolHost { Building = preferred };
		}

		if ( standing.FirstOrDefault() is { } found ) {
			return new ArchBoolHost { Building = found };
		}

		if ( Carrying( plan, centre ) is { } road ) {
			return new ArchBoolHost { Road = road };
		}

		return new ArchBoolHost { Building = preferred ?? plan.Buildings.FirstOrDefault() };
	}

	static ArchRoadPart Carrying( ArchPlan plan, Vector2 point ) {
		ArchRoadPart carrying = null;
		var nearest = float.MaxValue;

		foreach ( var road in plan.Roads() ) {
			if ( !road.Curve().Nearest( point, out _, out var gap ) || gap >= nearest || gap > road.Reach() + Bored( road ) ) {
				continue;
			}

			nearest = gap;
			carrying = road;
		}

		return carrying;
	}

	static float Bored( ArchRoadPart road ) {
		var reach = 0f;

		foreach ( var tunnel in road.Tunnels ) {
			reach = MathF.Max( reach, MathF.Max( 0f, tunnel.Clearance ) + MathF.Max( 2f, tunnel.Lining ) );
		}

		return reach;
	}

	public static ArchBoolDrag Sketch(
		ArchGridService grid,
		ArchKit kit,
		ArchBoolOp op,
		Vector2 from,
		Vector2 to,
		float plane,
		float reach,
		ArchBoolForm form,
		int segments,
		ArchBoolReach into = ArchBoolReach.Through,
		float depth = 0f,
		float surface = 0f ) {
		var start = grid.Base( from );
		var finish = grid.Base( to );
		var (bottom, top) = Band( kit, op, into, plane, reach, depth, surface );

		return new ArchBoolDrag {
			Form = form,
			Segments = Math.Max( ArchFootprint.LeastSegments, segments ),
			Min = new Vector2( MathF.Min( start.x, finish.x ), MathF.Min( start.y, finish.y ) ),
			Max = new Vector2( MathF.Max( start.x, finish.x ), MathF.Max( start.y, finish.y ) ),
			Bottom = bottom,
			Top = top,
			Least = Least( op ),
			Climb = ArchRamp.Climbed( from, to )
		};
	}

	static (float Bottom, float Top) Band( ArchKit kit, ArchBoolOp op, ArchBoolReach into, float plane, float reach, float depth, float surface ) {
		var bite = ArchLap.Bite( kit );
		var bitten = MathF.Max( 1f, depth );

		if ( op is not (ArchBoolOp.Subtract or ArchBoolOp.Damage or ArchBoolOp.Extrude) ) {
			return (plane, plane + MathF.Max( 4f, reach ));
		}

		return into switch {
			ArchBoolReach.IntoCeiling => (surface - bite, surface + bitten),
			ArchBoolReach.IntoFloor => (surface - bitten, surface + bite),
			ArchBoolReach.IntoWall => (surface - bite, surface + bitten),
			// Through starts below the plane — the plane is the top of the slab being cut
			_ => (plane - kit.FloorThickness, plane + MathF.Max( 4f, reach ))
		};
	}

	public static ArchBoolReach Aimed( ArchBoolReach picked, ArchCursorFace face, bool wall = false ) {
		if ( picked != ArchBoolReach.Dragged ) {
			return picked;
		}

		if ( wall ) {
			return ArchBoolReach.IntoWall;
		}

		return face == ArchCursorFace.Ceiling ? ArchBoolReach.IntoCeiling : ArchBoolReach.Through;
	}

	public static ArchCutAffects Aimed( ArchBoolReach into, ArchCutAffects authored ) => into switch {
		ArchBoolReach.IntoCeiling => ArchCutAffects.Ceilings | ArchCutAffects.Roofs,
		ArchBoolReach.IntoFloor => ArchCutAffects.Floors,
		ArchBoolReach.IntoWall => ArchCutAffects.Walls | ArchCutAffects.Trims | ArchCutAffects.Windows,
		_ => authored
	};

	public static float Through( ArchPlan plan, ArchKit kit, int level, Vector2 min, Vector2 max, float plane ) {
		var head = plane + kit.WallHeight;

		if ( plan is null ) {
			return head - plane;
		}

		var footprint = ArchFootprint.Rect( min, max );
		var bite = ArchLap.Bite( kit );

		foreach ( var building in plan.Buildings ) {
			foreach ( var room in building.Rooms.Where( room => room.Floor >= level && room.HasFootprint ) ) {
				if ( ArchFootprint.Overlaps( ArchFloorGen.Footprint( room ), footprint ) ) {
					head = MathF.Max( head, room.BaseHeight + ArchFloorGen.WallHeight( room, kit ) + bite );
				}
			}
		}

		return ArchAsks.Headroom( plan, kit, level, footprint, head ) - plane;
	}

	public static object Place( ArchPlan plan, ArchBoolHost host, int level, ArchBoolDrag drag, ArchBoolPlacement placing ) {
		if ( !drag.Stands || host is not { Stands: true } ) {
			return null;
		}

		if ( placing.Op != ArchBoolOp.Add ) {
			return Sink( plan, host, level, drag, placing );
		}

		if ( host.Building is null ) {
			return null;
		}

		return placing.Kind switch {
			ArchBoolAdd.Slab or ArchBoolAdd.Ramp
				=> ArchPlatform.Add( plan, host.Building, level, drag.Bottom, drag.Top, drag.Loop(), Raked( placing, drag ) ),
			ArchBoolAdd.Pillar => Column( plan, drag, placing ),
			_ => Timber( plan, drag, placing )
		};
	}

	// Works on a copy — the subtool holds the draft for the whole session
	static ArchPlatformPart Raked( ArchBoolPlacement placing, ArchBoolDrag drag ) {
		var draft = placing.Platform.Dressing();

		draft.Ramp = placing.Kind == ArchBoolAdd.Ramp;

		if ( draft.Ramp ) {
			draft.RampYaw = drag.Climb;
		}

		return draft;
	}

	static ArchPillarPart Column( ArchPlan plan, ArchBoolDrag drag, ArchBoolPlacement placing ) {
		if ( placing.Room is not { } room ) {
			return null;
		}

		var part = placing.Pillar.Section();

		part.Id = plan.AllocateId();
		part.Name = $"Pillar{room.Pillars.Count + 1}";
		part.Placement = PillarPlacement.Freestanding;
		part.Origin = (drag.Min + drag.Max) * 0.5f;
		part.BaseHeight = drag.Bottom;
		part.Height = drag.Rise;
		part.Width = drag.Max.x - drag.Min.x;
		part.Depth = drag.Max.y - drag.Min.y;
		part.Sides = drag.Form == ArchBoolForm.Circle ? drag.Segments : 0;

		room.Pillars.Add( part );

		return part;
	}

	static ArchBeamPart Timber( ArchPlan plan, ArchBoolDrag drag, ArchBoolPlacement placing ) {
		if ( placing.Room is not { } room ) {
			return null;
		}

		var beam = Hung( drag, placing );

		beam.Id = plan.AllocateId();
		beam.Name = $"{(beam.Members == BeamMembers.Field ? "Slats" : "Beam")}{room.Beams.Count + 1}";
		beam.Level = room.Floor;

		room.Beams.Add( beam );

		return beam;
	}

	public static ArchBeamPart Hung( ArchBoolDrag drag, ArchBoolPlacement placing ) {
		var draft = placing.Beam;
		var beam = new ArchBeamPart {
			Members = placing.Kind == ArchBoolAdd.Slats ? BeamMembers.Field : BeamMembers.One,
			TopHeight = drag.Top,
			Drop = drag.Rise,
			Type = draft.Type,
			Sides = draft.Sides,
			MemberWidth = draft.MemberWidth,
			MemberGap = draft.MemberGap,
			Yaw = draft.Yaw,
			Level = placing.Room?.Floor ?? 0
		};

		beam.Reshape( drag.Loop() );

		return beam;
	}

	static ArchCutPart Sink( ArchPlan plan, ArchBoolHost host, int level, ArchBoolDrag drag, ArchBoolPlacement placing ) {
		var draft = placing.Cut;
		var filed = host.Cuts;
		var cut = new ArchCutPart {
			Id = plan.AllocateId(),
			Name = placing.Op switch {
				ArchBoolOp.Damage => $"Damage{filed.Count + 1}",
				ArchBoolOp.Extrude => $"Extrude{filed.Count + 1}",
				_ => $"Cut{filed.Count + 1}"
			},
			Level = host.Storey( level ),
			Profile = CutProfile.Poly,
			Affects = placing.Op == ArchBoolOp.Extrude
				? ArchExtrude.Targets( draft.Extrude )
				: Aimed( placing.Reach, draft.Affects ),
			Ramp = draft.Ramp,
			RampFall = draft.RampFall,
			RampYaw = draft.Ramp ? drag.Climb : draft.RampYaw,
			Enclosure = draft.Enclosure,
			WallThickness = draft.WallThickness,
			Edge = draft.Edge,
			EdgeWidth = draft.EdgeWidth,
			Head = draft.Head,
			HeadHeight = draft.HeadHeight,
			HeadRoof = draft.HeadRoof,
			Damage = placing.Op == ArchBoolOp.Damage ? draft.Damage : ArchDamageKind.None,
			Extrude = placing.Op == ArchBoolOp.Extrude ? draft.Extrude : ArchExtrudeKind.None,
			ExtrudeDepth = draft.ExtrudeDepth,
			ExtrudeCourse = draft.ExtrudeCourse,
			ExtrudeJoint = draft.ExtrudeJoint,
			ExtrudeBothFaces = draft.ExtrudeBothFaces,
			ExtrudePillarType = draft.ExtrudePillarType,
			ExtrudeFooting = draft.ExtrudeFooting,
			DamageAmount = draft.DamageAmount,
			DamageCellWidth = draft.DamageCellWidth,
			DamageCellLength = draft.DamageCellLength,
			DamageDepth = draft.DamageDepth,
			MasonryPattern = draft.MasonryPattern,
			MasonryCarves = draft.MasonryCarves,
			DamageDrop = draft.DamageDrop,
			DamageTilt = draft.DamageTilt
		};

		cut.Segments.Add( new ArchCutSegment {
			Start = drag.Min,
			End = drag.Max,
			BaseHeight = drag.Bottom,
			TopHeight = drag.Top,
			Loop = drag.Loop()
		} );

		filed.Add( cut );

		return cut;
	}

}