Editor/InteriorLayoutBuilder/RoomLayoutData.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Sandbox;

namespace ReusableRoomLayout;

public sealed class RoomLayoutDocument
{
	public const int MinimumFloor = -32;
	public const int MaximumFloor = 128;

	public int Version { get; set; } = 9;
	public int NextId { get; set; } = 1;
	public bool GeneratedGeometryEnabled { get; set; } = true;
	public RoomLayoutSettings Settings { get; set; } = new();
	public List<RoomLayoutRoom> Rooms { get; set; } = new();
	public List<RoomLayoutDoor> Doors { get; set; } = new();
	public List<RoomLayoutWindow> Windows { get; set; } = new();
	public List<RoomLayoutCorridor> Corridors { get; set; } = new();
	public List<RoomLayoutFloorCutout> FloorCutouts { get; set; } = new();

	public int AllocateId()
	{
		NextId = Math.Max( NextId, HighestId() + 1 );
		return NextId++;
	}

	public void NormalizeIds()
	{
		NextId = Math.Max( 1, HighestId() + 1 );
		var migrateOpeningDimensions = Version < 6;
		var migrateFloorSpacing = Version < 8;

		if ( migrateFloorSpacing && Math.Abs( Settings.FloorSpacing - 160.0f ) < 0.001f )
		{
			Settings.FloorSpacing = 0.0f;
		}

		foreach ( var room in Rooms )
		{
			room.Floor = ClampFloor( room.Floor );
		}

		foreach ( var corridor in Corridors )
		{
			corridor.Floor = ClampFloor( corridor.Floor );

			if ( corridor.BendPoints.Count > 0 )
			{
				corridor.ManualPath = true;
			}
		}

		foreach ( var cutout in FloorCutouts )
		{
			cutout.Floor = ClampFloor( cutout.Floor );
		}

		foreach ( var door in Doors )
		{
			if ( door.Width <= 0.0f )
			{
				door.Width = Settings.DoorWidth;
			}

			if ( migrateOpeningDimensions || door.Height <= 0.0f )
			{
				door.Height = Settings.DoorHeight;
			}
		}

		foreach ( var window in Windows )
		{
			if ( window.Width <= 0.0f )
			{
				window.Width = Settings.WindowWidth;
			}

			if ( migrateOpeningDimensions || window.Height <= 0.0f )
			{
				window.Height = Settings.WindowHeight;
			}

			if ( migrateOpeningDimensions || window.SillHeight < 0.0f )
			{
				window.SillHeight = Settings.WindowSillHeight;
			}
		}

		Version = 9;
	}

	public RoomLayoutRoom FindRoom( int id )
	{
		return Rooms.FirstOrDefault( room => room.Id == id );
	}

	public RoomLayoutDoor FindDoor( int id )
	{
		return Doors.FirstOrDefault( door => door.Id == id );
	}

	public RoomLayoutWindow FindWindow( int id )
	{
		return Windows.FirstOrDefault( window => window.Id == id );
	}

	public int FloorFor( RoomLayoutRoom room )
	{
		return room is null ? 0 : ClampFloor( room.Floor );
	}

	public int FloorFor( RoomLayoutCorridor corridor )
	{
		return corridor is null ? 0 : ClampFloor( corridor.Floor );
	}

	public int FloorFor( RoomLayoutFloorCutout cutout )
	{
		return cutout is null ? 0 : ClampFloor( cutout.Floor );
	}

	public int FloorFor( RoomLayoutDoor door )
	{
		if ( door is null )
		{
			return 0;
		}

		if ( door.RoomId != 0 )
		{
			return FloorFor( FindRoom( door.RoomId ) );
		}

		if ( door.CorridorId != 0 )
		{
			return FloorFor( Corridors.FirstOrDefault( corridor => corridor.Id == door.CorridorId ) );
		}

		return 0;
	}

	public int FloorFor( RoomLayoutWindow window )
	{
		if ( window is null )
		{
			return 0;
		}

		if ( window.RoomId != 0 )
		{
			return FloorFor( FindRoom( window.RoomId ) );
		}

		if ( window.CorridorId != 0 )
		{
			return FloorFor( Corridors.FirstOrDefault( corridor => corridor.Id == window.CorridorId ) );
		}

		return 0;
	}

	public bool CorridorDoorsAreOnFloor( RoomLayoutCorridor corridor )
	{
		var startDoor = FindDoor( corridor?.StartDoorId ?? 0 );
		var endDoor = FindDoor( corridor?.EndDoorId ?? 0 );
		return startDoor is not null &&
			endDoor is not null &&
			FloorFor( startDoor ) == FloorFor( corridor ) &&
			FloorFor( endDoor ) == FloorFor( corridor );
	}

	public IReadOnlyList<int> UsedFloors()
	{
		var floors = new HashSet<int>();

		foreach ( var room in Rooms )
		{
			floors.Add( FloorFor( room ) );
		}

		foreach ( var corridor in Corridors )
		{
			floors.Add( FloorFor( corridor ) );
		}

		foreach ( var cutout in FloorCutouts )
		{
			floors.Add( FloorFor( cutout ) );
		}

		if ( floors.Count == 0 )
		{
			floors.Add( 0 );
		}

		return floors.OrderBy( floor => floor ).ToArray();
	}

	public void RemoveRoom( int roomId )
	{
		var removedDoorIds = Doors.Where( door => door.RoomId == roomId )
			.Select( door => door.Id )
			.ToHashSet();

		Rooms.RemoveAll( room => room.Id == roomId );
		Doors.RemoveAll( door => door.RoomId == roomId );
		Windows.RemoveAll( window => window.RoomId == roomId );
		Corridors.RemoveAll( corridor =>
			removedDoorIds.Contains( corridor.StartDoorId ) ||
			removedDoorIds.Contains( corridor.EndDoorId ) );
	}

	private int HighestId()
	{
		var highestRoom = Rooms.Count == 0 ? 0 : Rooms.Max( room => room.Id );
		var highestDoor = Doors.Count == 0 ? 0 : Doors.Max( door => door.Id );
		var highestWindow = Windows.Count == 0 ? 0 : Windows.Max( window => window.Id );
		var highestCorridor = Corridors.Count == 0 ? 0 : Corridors.Max( corridor => corridor.Id );
		var highestCutout = FloorCutouts.Count == 0 ? 0 : FloorCutouts.Max( cutout => cutout.Id );
		return Math.Max( Math.Max( highestRoom, highestWindow ), Math.Max( Math.Max( highestDoor, highestCorridor ), highestCutout ) );
	}

	public static int ClampFloor( int floor )
	{
		return Math.Clamp( floor, MinimumFloor, MaximumFloor );
	}
}

public sealed class RoomLayoutSettings
{
	public float GridSize { get; set; } = 64.0f;
	public float WallHeight { get; set; } = 128.0f;
	public float WallThickness { get; set; } = 12.0f;
	public float WallModuleLength { get; set; } = 64.0f;
	public float WallCapHeight { get; set; } = 6.0f;
	public float FloorSpacing { get; set; }
	public float FloorThickness { get; set; } = 4.0f;
	public float FloorModuleSize { get; set; } = 64.0f;
	public float TextureWorldSize { get; set; } = 128.0f;
	public float DefaultCorridorWidth { get; set; } = 64.0f;
	public float DoorWidth { get; set; } = 64.0f;
	public float DoorHeight { get; set; } = 96.0f;
	public float DoorFrameThickness { get; set; } = 8.0f;
	public float WindowWidth { get; set; } = 64.0f;
	public float WindowHeight { get; set; } = 64.0f;
	public float WindowSillHeight { get; set; } = 48.0f;
	public float WindowFrameThickness { get; set; } = 2.0f;
	public bool BaseboardsEnabled { get; set; } = true;
	public bool ThresholdsEnabled { get; set; } = true;
	public bool RoofEnabled { get; set; }
	public float BaseboardHeight { get; set; } = 4.0f;
	public float BaseboardDepth { get; set; } = 2.0f;
	public float ThresholdDepth { get; set; } = 12.0f;
	public float RoofThickness { get; set; } = 4.0f;
	public string FloorMaterialPath { get; set; } = "";
	public string WallMaterialPath { get; set; } = "";
	public string OuterWallMaterialPath { get; set; } = "";
	public string InnerWallMaterialPath { get; set; } = "";
	public string WallCapMaterialPath { get; set; } = "";
	public string DoorFrameMaterialPath { get; set; } = "";
	public string WindowFrameMaterialPath { get; set; } = "";
	public string BaseboardMaterialPath { get; set; } = "";
	public string CorridorFloorMaterialPath { get; set; } = "";
	public string ThresholdMaterialPath { get; set; } = "";
	public string RoofMaterialPath { get; set; } = "";
	public float FloorMaterialScale { get; set; }
	public float WallMaterialScale { get; set; }
	public float OuterWallMaterialScale { get; set; }
	public float InnerWallMaterialScale { get; set; }
	public float WallCapMaterialScale { get; set; }
	public float DoorFrameMaterialScale { get; set; }
	public float WindowFrameMaterialScale { get; set; }
	public float BaseboardMaterialScale { get; set; }
	public float CorridorFloorMaterialScale { get; set; }
	public float ThresholdMaterialScale { get; set; }
	public float RoofMaterialScale { get; set; }

	[JsonIgnore]
	public float DefaultFloorSpacing => MathF.Max( 1.0f, WallHeight ) +
		MathF.Max( 0.0f, WallCapHeight ) +
		MathF.Max( 0.5f, FloorThickness );

	[JsonIgnore]
	public float EffectiveFloorSpacing => FloorSpacing > 0.0f
		? Math.Clamp( FloorSpacing, 1.0f, 8192.0f )
		: Math.Clamp( DefaultFloorSpacing, 1.0f, 8192.0f );
}

public sealed class RoomLayoutRoom
{
	public int Id { get; set; }
	public int Floor { get; set; }
	public string Name { get; set; } = "";
	public RoomLayoutRect Bounds { get; set; }
	public string FloorMaterialPath { get; set; } = "";
	public string WallMaterialPath { get; set; } = "";
	public string OuterWallMaterialPath { get; set; } = "";
	public string InnerWallMaterialPath { get; set; } = "";
	public string WallCapMaterialPath { get; set; } = "";
	public string BaseboardMaterialPath { get; set; } = "";
	public string ThresholdMaterialPath { get; set; } = "";
	public string RoofMaterialPath { get; set; } = "";
	public float FloorMaterialScale { get; set; }
	public float WallMaterialScale { get; set; }
	public float OuterWallMaterialScale { get; set; }
	public float InnerWallMaterialScale { get; set; }
	public float WallCapMaterialScale { get; set; }
	public float BaseboardMaterialScale { get; set; }
	public float ThresholdMaterialScale { get; set; }
	public float RoofMaterialScale { get; set; }
}

public sealed class RoomLayoutDoor
{
	public int Id { get; set; }
	public int RoomId { get; set; }
	public int CorridorId { get; set; }
	public int CorridorSegmentIndex { get; set; }
	public int CorridorSide { get; set; }
	public RoomLayoutWallSide Side { get; set; }
	public float Offset { get; set; }
	public float Width { get; set; }
	public float Height { get; set; } = -1.0f;
	public string DoorFrameMaterialPath { get; set; } = "";
	public float DoorFrameMaterialScale { get; set; }
}

public sealed class RoomLayoutWindow
{
	public int Id { get; set; }
	public int RoomId { get; set; }
	public int CorridorId { get; set; }
	public int CorridorSegmentIndex { get; set; }
	public int CorridorSide { get; set; }
	public RoomLayoutWallSide Side { get; set; }
	public float Offset { get; set; }
	public float Width { get; set; }
	public float Height { get; set; } = -1.0f;
	public float SillHeight { get; set; } = -1.0f;
	public string WindowFrameMaterialPath { get; set; } = "";
	public float WindowFrameMaterialScale { get; set; }
}

public sealed class RoomLayoutCorridor
{
	public int Id { get; set; }
	public int Floor { get; set; }
	public int StartDoorId { get; set; }
	public int EndDoorId { get; set; }
	public float Width { get; set; }
	public bool ManualPath { get; set; }
	public string FloorMaterialPath { get; set; } = "";
	public string WallMaterialPath { get; set; } = "";
	public string OuterWallMaterialPath { get; set; } = "";
	public string InnerWallMaterialPath { get; set; } = "";
	public string WallCapMaterialPath { get; set; } = "";
	public string BaseboardMaterialPath { get; set; } = "";
	public string ThresholdMaterialPath { get; set; } = "";
	public string RoofMaterialPath { get; set; } = "";
	public float FloorMaterialScale { get; set; }
	public float WallMaterialScale { get; set; }
	public float OuterWallMaterialScale { get; set; }
	public float InnerWallMaterialScale { get; set; }
	public float WallCapMaterialScale { get; set; }
	public float BaseboardMaterialScale { get; set; }
	public float ThresholdMaterialScale { get; set; }
	public float RoofMaterialScale { get; set; }
	public List<RoomLayoutPoint> BendPoints { get; set; } = new();
}

public sealed class RoomLayoutFloorCutout
{
	public int Id { get; set; }
	public int Floor { get; set; }
	public string Name { get; set; } = "";
	public RoomLayoutRect Bounds { get; set; }
}

public enum RoomLayoutWallSide
{
	North,
	East,
	South,
	West
}

public struct RoomLayoutPoint
{
	public float X { get; set; }
	public float Y { get; set; }

	public RoomLayoutPoint( float x, float y )
	{
		X = x;
		Y = y;
	}

	[JsonIgnore]
	public Vector2 Vector => new( X, Y );

	public static RoomLayoutPoint FromVector( Vector2 point ) => new( point.x, point.y );
}

public struct RoomLayoutRect
{
	public float X { get; set; }
	public float Y { get; set; }
	public float Width { get; set; }
	public float Height { get; set; }

	public RoomLayoutRect( float x, float y, float width, float height )
	{
		X = x;
		Y = y;
		Width = width;
		Height = height;
	}

	[JsonIgnore]
	public Vector2 Min => new( X, Y );

	[JsonIgnore]
	public Vector2 Max => new( X + Width, Y + Height );

	[JsonIgnore]
	public Vector2 Center => new( X + Width * 0.5f, Y + Height * 0.5f );

	public bool Contains( Vector2 point )
	{
		return point.x >= X && point.x <= X + Width &&
			point.y >= Y && point.y <= Y + Height;
	}

	public RoomLayoutRect MovedBy( Vector2 delta )
	{
		return new RoomLayoutRect( X + delta.x, Y + delta.y, Width, Height );
	}

	public static RoomLayoutRect FromPoints( Vector2 a, Vector2 b )
	{
		var minX = MathF.Min( a.x, b.x );
		var minY = MathF.Min( a.y, b.y );
		var maxX = MathF.Max( a.x, b.x );
		var maxY = MathF.Max( a.y, b.y );
		return new RoomLayoutRect( minX, minY, maxX - minX, maxY - minY );
	}
}