Editor/Data/ArchPlan.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchPlan {
	public int Version { get; set; } = 3;
	public int NextId { get; set; } = 1;
	public string KitName { get; set; } = "default";

	public List<ArchUnit> Units { get; set; } = new();

	// Migration — folded into Units by Normalize.
	[JsonPropertyName( "Buildings" )]
	public List<ArchBuilding> LegacyBuildings { get; set; }

	[JsonPropertyName( "Roads" )]
	public List<ArchRoadPart> LegacyRoads { get; set; }

	[JsonIgnore]
	public IReadOnlyList<ArchBuilding> Buildings => Units.OfType<ArchBuilding>().ToList();

	public List<ArchSiteAssembly> Assemblies { get; set; } = new();
	public List<ArchAssetInstance> Instances { get; set; } = new();
	public List<ArchLayerRecord> Layers { get; set; } = new();
	public List<ArchPartRecord> Parts { get; set; } = new();
	public List<ArchLayerLink> Links { get; set; } = new();

	public int AllocateId() => NextId++;

	public IEnumerable<ArchRoom> AllRooms() => Buildings.SelectMany( building => building.Rooms );

	public ArchBuilding FindBuilding( int id ) => Buildings.FirstOrDefault( building => building.Id == id );

	public ArchRoom FindRoom( int id ) => AllRooms().FirstOrDefault( room => room.Id == id );

	public ArchBuilding OwnerOf( ArchRoom room ) => Buildings.FirstOrDefault( building => building.Rooms.Contains( room ) );

	void Adopt() {
		if ( LegacyBuildings is { Count: > 0 } ) {
			Units.AddRange( LegacyBuildings );
		}

		if ( LegacyRoads is { Count: > 0 } ) {
			Units.AddRange( LegacyRoads );
		}

		LegacyBuildings = null;
		LegacyRoads = null;
	}

	[JsonIgnore]
	public bool HasContent => this.Roads().Count > 0 || Units.Any( unit => unit.HasContent );

	public int DiscardEmptyTargets() {
		var dropped = 0;

		foreach ( var building in Buildings ) {
			dropped += building.Rooms.RemoveAll( room => !room.HasContent );
		}

		return dropped + Units.RemoveAll( unit => !unit.HasContent );
	}

	public void Normalize() {
		Adopt();

		var kinds = ArchKinds.Load();

		Reserve();

		Renumber( kinds );

		foreach ( var building in Buildings ) {
			foreach ( var fence in building.Fences.Where( entry => entry.Path.Count >= 2 && entry.Nodes.Count == 0 ) ) {
				fence.Nodes = fence.Path.Select( ArchCurveNode.At ).ToList();
				fence.Path = new List<Vector3>();
			}

			foreach ( var room in building.Rooms ) {
				foreach ( var stair in room.Stairs ) {
					Reshaft( stair );
				}
			}

			foreach ( var platform in building.Platforms ) {
				foreach ( var stair in platform.Stairs ) {
					Reshaft( stair );
				}
			}

			foreach ( var porch in building.Rooms.SelectMany( room => room.Porches ) ) {
				foreach ( var stair in porch.Stairs ) {
					Reshaft( stair );
				}
			}
		}

		foreach ( var building in Buildings ) {
			foreach ( var room in building.Rooms ) {
				room.Walls.RemoveAll( wall => wall.Length < 1f );
			}

			foreach ( var roof in building.Roofs ) {
				roof.Walls.RemoveAll( wall => wall.Length < 1f );
			}

			foreach ( var roof in building.Roofs.Where( roof => roof.HasFootprint ) ) {
				roof.Reshape( roof.Footprint );
			}

			SeparateSingleSpanRoofs( building );
		}

		foreach ( var fixup in kinds.All.OfType<IArchNormalizes>() ) {
			fixup.Normalize( this );
		}
	}

	void Reshaft( ArchStairPart stair ) {
		if ( stair.Legs is { Count: > 0 } legs ) {
			var (core, lanes) = ArchStairLanes.FromLegs( legs );

			core.Rise = MathF.Max( 4f, stair.StoredRise > 1f ? stair.StoredRise : stair.Core?.Rise ?? 0f );

			stair.Core = core;
			stair.Lanes = lanes;
			stair.Legs = null;
		}

		stair.StoredRise = 0f;

		ArchStairLanes.DeriveLandings( this, stair );
		ArchStairLanes.Number( this, stair );
	}

	void SeparateSingleSpanRoofs( ArchBuilding building ) {
		for ( var roofIndex = building.Roofs.Count - 1; roofIndex >= 0; roofIndex-- ) {
			var roof = building.Roofs[roofIndex];
			var outline = roof.Outline();

			if ( roof.Style is not (RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth) ||
				!roof.HasFootprint ||
				outline.Count == 4 ) {
				continue;
			}

			var rooms = building.Rooms
				.Where( room => room.Floor == roof.Level )
				.Select( room => (Room: room, Footprint: ArchFloorGen.Footprint( room )) )
				.Where( candidate => candidate.Footprint.Count == 4 )
				.Where( candidate => ArchRegion.Covers( new[] { outline }, candidate.Footprint ) )
				.ToList();

			rooms.RemoveAll( candidate => rooms.Any( other =>
				!ReferenceEquals( candidate.Room, other.Room ) &&
				MathF.Abs( ArchFootprint.SignedArea( other.Footprint ) ) > MathF.Abs( ArchFootprint.SignedArea( candidate.Footprint ) ) &&
				ArchRegion.Covers( new[] { other.Footprint }, candidate.Footprint ) ) );

			var occupied = ArchFootprint.Union( rooms.Select( candidate => candidate.Footprint ).ToList() );

			if ( rooms.Count < 2 ||
				!ArchRegion.Covers( occupied, outline ) ||
				occupied.Any( loop => !ArchRegion.Covers( new[] { outline }, loop ) ) ) {
				continue;
			}

			building.Roofs.RemoveAt( roofIndex );

			for ( var roomIndex = rooms.Count - 1; roomIndex >= 0; roomIndex-- ) {
				var section = roof.Duplicate( roomIndex == 0 ? roof.Id : AllocateId(), rooms[roomIndex].Footprint );
				building.Roofs.Insert( roofIndex, section );
			}
		}
	}

	// Never lowered — deleted ids must not be reused
	void Reserve() {
		NextId = System.Math.Max( System.Math.Max( 1, NextId ), HighestId() + 1 );
	}

	void Renumber( ArchKinds kinds ) {
		var seen = new HashSet<int>();
		var moved = new List<string>();

		foreach ( var part in this.Everything( kinds ) ) {
			var held = ArchPlanStore.IdOf( part );

			if ( held == 0 || seen.Add( held ) ) {
				continue;
			}

			var fresh = AllocateId();

			if ( !ArchPlanStore.SetId( part, fresh ) ) {
				continue;
			}

			seen.Add( fresh );
			moved.Add( $"{part.GetType().Name} {held} to {fresh}" );
		}

		if ( moved.Count > 0 ) {
			Log.Warning( $"Architecture moved {moved.Count} part(s) off an id another part already held: {string.Join( ", ", moved )}." );
		}
	}

	// Reads the serialized JSON so all id-bearing parts are found regardless of type
	int HighestId() {
		using var document = JsonDocument.Parse( ArchStorage.Keyed( this ) );

		return ArchPlanStore.HighestIdIn( document.RootElement );
	}
}

public sealed class ArchPalette {
	public Dictionary<string, string> Materials { get; set; } = new();
	public Dictionary<string, float> TexelScales { get; set; } = new();
	public Dictionary<string, Vector2> TextureOffsets { get; set; } = new();
	public Dictionary<string, ArchFaceUvSet> FaceMappings { get; set; } = new();

	public bool TryGet( ArchSurface surface, out string path ) {
		return Materials.TryGetValue( surface.ToString(), out path ) && !string.IsNullOrWhiteSpace( path );
	}

	public void Set( ArchSurface surface, string path ) {
		TexelScales.Remove( surface.ToString() );

		if ( string.IsNullOrWhiteSpace( path ) ) {
			Materials.Remove( surface.ToString() );
			TextureOffsets?.Remove( surface.ToString() );
			return;
		}

		Materials[surface.ToString()] = path;
	}

	public void Set( ArchSurface surface, string path, float texelScale ) {
		Set( surface, path );

		if ( !string.IsNullOrWhiteSpace( path ) ) {
			SetScale( surface, texelScale );
		}
	}

	public void SetScale( ArchSurface surface, float texelScale ) {
		if ( texelScale <= 0f ) {
			TexelScales.Remove( surface.ToString() );
			return;
		}

		TexelScales[surface.ToString()] = texelScale;
	}

	public float ScaleFor( ArchSurface surface ) {
		return TexelScales.TryGetValue( surface.ToString(), out var scale ) ? scale : 0f;
	}

	public void SetOffset( ArchSurface surface, Vector2 offset ) {
		if ( offset.IsNearZeroLength ) {
			TextureOffsets?.Remove( surface.ToString() );
			return;
		}

		TextureOffsets ??= new();
		TextureOffsets[surface.ToString()] = offset;
	}

	public Vector2 OffsetFor( ArchSurface surface ) {
		return TextureOffsets is not null && TextureOffsets.TryGetValue( surface.ToString(), out var offset ) ? offset : Vector2.Zero;
	}
}

public interface IArchPainted {
	ArchPalette Palette { get; set; }
}

public sealed class ArchFaceUvSet {
	public List<ArchFaceUvVariation> Variations { get; set; } = new();
}

public sealed class ArchFaceUvVariation {
	public string Signature { get; set; }
	public List<ArchFaceUvFace> Faces { get; set; } = new();
}

public sealed class ArchFaceUvFace {
	public List<Vector2> Coordinates { get; set; } = new();
}

[JsonConverter( typeof( ArchUnitConverter ) )]
public abstract class ArchUnit : IArchCollides, IArchNamed {
	public ArchCollisionMode? Collision { get; set; }
	public ArchKind Kind { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Unit";
	public ArchPalette Palette { get; set; } = new();
	public List<ArchCutPart> Cuts { get; set; } = new();

	// Round-trips unknown module data so plans don't lose fields from uninstalled libraries
	[JsonExtensionData]
	public Dictionary<string, JsonElement> Payloads { get; set; } = new();

	[JsonIgnore]
	public abstract bool HasContent { get; }
}

public sealed class ArchOpaqueUnit : ArchUnit {
	public override bool HasContent => true;
}

public sealed class ArchBuilding : ArchUnit, IArchPainted {
	public ArchBuilding() {
		Name = "Building";
		Kind = ArchKind.Building;
	}

	public string Archetype { get; set; } = "";
	public List<ArchRoom> Rooms { get; set; } = new();
	public List<ArchRoofPart> Roofs { get; set; } = new();
	public List<ArchDownpipePart> Downpipes { get; set; } = new();
	public List<ArchPipePart> Pipes { get; set; } = new();
	public List<ArchPipeBracketPart> Brackets { get; set; } = new();
	public List<ArchFencePart> Fences { get; set; } = new();
	public List<ArchPlatformPart> Platforms { get; set; } = new();
	public List<ArchLadderPart> Ladders { get; set; } = new();
	public List<ArchBalconyPart> Balconies { get; set; } = new();
	public List<ArchExteriorStairPart> ExteriorStairs { get; set; } = new();
	public bool GuttersEnabled { get; set; } = true;
	public float Facing { get; set; }
	public float StoreyHeight { get; set; }
	public List<ArchFloorCutout> Cutouts { get; set; } = new();

	[JsonIgnore]
	public override bool HasContent => Roofs.Count > 0 || Downpipes.Count > 0 || Fences.Count > 0 || Platforms.Count > 0
		|| Ladders.Count > 0 || Cuts.Count > 0 || Balconies.Count > 0 || ExteriorStairs.Count > 0
		|| Pipes.Count > 0 || Brackets.Count > 0
		|| Rooms.Any( room => room.HasContent );
}

public sealed class ArchRoom : IArchCollides, IArchPainted, IArchNamed {
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Room";
	public int Floor { get; set; }
	public float BaseHeight { get; set; }
	public float WallHeight { get; set; }
	public bool HasFloor { get; set; } = true;
	public bool HasCeiling { get; set; } = true;
	public bool Facade { get; set; }
	public float CeilingDepth { get; set; }
	public bool FloorBoards { get; set; }
	public float FloorBoardYaw { get; set; }
	public bool RaisedFoundation { get; set; } = true;
	public bool Spans { get; set; }
	public bool OverhangPosts { get; set; }
	public ArchPillarPart OverhangPost { get; set; } = new();
	public float WalkwayTop { get; set; }
	public WalkwayInterior Interior { get; set; }
	public bool WalkwaySkirting { get; set; } = true;
	public ArchPalette Palette { get; set; } = new();
	public List<ArchWall> Walls { get; set; } = new();
	public List<ArchStairPart> Stairs { get; set; } = new();
	public List<ArchTrimPart> Trims { get; set; } = new();
	public List<ArchPillarPart> Pillars { get; set; } = new();
	public List<ArchSpanPart> PierSpans { get; set; } = new();
	public List<ArchBeamPart> Beams { get; set; } = new();
	public List<ArchPorchPart> Porches { get; set; } = new();
	public List<ArchApproachPart> Approaches { get; set; } = new();
	public List<ArchCabinetPart> Cabinets { get; set; } = new();
	public List<Vector2> Footprint { get; set; } = new();

	[JsonExtensionData]
	public Dictionary<string, JsonElement> Payloads { get; set; } = new();

	[JsonIgnore]
	public bool HasFootprint => Footprint.Count >= 3;

	[JsonIgnore]
	public bool HasContent => HasFootprint || Walls.Count > 0 || Stairs.Count > 0 || Pillars.Count > 0
		|| Trims.Count > 0 || Porches.Count > 0 || Approaches.Count > 0 || Beams.Count > 0 || PierSpans.Count > 0
		|| Cabinets.Count > 0;
}