Editor/Tool/Subtools/ArchRoofSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Roof" ), Icon( "roofing" ), Group( "08" )]
public sealed class ArchRoofSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Roof;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.Roof, ArchSurface.RoofEdge, ArchSurface.Soffit, ArchSurface.Gutter, ArchSurface.WallCap, ArchSurface.Plant };

	readonly ArchRoofPart draft = new() { Style = RoofStyle.Hip };
	readonly ArchRoofLightPart lightDraft = new();

	RidgeRun ridge = RidgeRun.Auto;
	float eaveDrop;

	// A fitting goes on a deck already there rather than standing a new section - explicit so a miss says so.
	bool fitting;

	protected override string Title() => fitting ? lightDraft.Form.Label() : "Roof";

	protected override string Advice() {
		return fitting
			? lightDraft.Form.Advice()
			: "Drag the roof footprint. Drop the eave to step a section down under a taller one.";
	}

	public override void OnEnabled() {
		base.OnEnabled();

		ArchWorkflow.Restart( Scope( "flow" ) );
	}

	protected override void DrawPreview() {
		if ( fitting ) {
			DrawLightPreview();
			return;
		}

		var min = Min( DragStart, DragCurrent );
		var max = Max( DragStart, DragCurrent );
		var eave = Owner.LevelHeight + MathF.Max( 32f, Owner.Kit.WallHeight - MathF.Max( 0f, eaveDrop ) );

		ArchGhost.Pitch( min, max, eave, Owner.Kit.RoofPitch, draft.Style, ArchBuild.Ridged( ridge, min, max ) );
		ArchGhost.Note( new Vector3( max.x, max.y, eave ), $"{draft.Style} eave {eave:0}" );
	}

	void DrawLightPreview() {
		var min = Min( DragStart, DragCurrent );
		var max = Max( DragStart, DragCurrent );
		var roof = ArchRoofLight.Under( Owner.Plan, Owner.Level, DragStart, lightDraft.Form );

		if ( roof is null ) {
			var over = ArchRoofLight.Over( Owner.Plan, Owner.Level, DragStart );

			ArchGhost.Plate( min, max, Owner.LevelHeight, 8 );
			ArchGhost.Note( new Vector3( max.x, max.y, Owner.LevelHeight ),
				over is null
					? $"no deck here for a {lightDraft.Form.Label().ToLowerInvariant()}"
					: $"{over.Name} is a {over.Style} — {Carried()}" );

			return;
		}

		if ( Levelled( roof ) ) {
			ArchGhost.Plate( min, max, Owner.LevelHeight, 8 );
			ArchGhost.Note( new Vector3( max.x, max.y, Owner.LevelHeight ),
				$"{roof.Name} is flat — a dormer needs a slope to break out of" );

			return;
		}

		// Off the deck plane under the drag, never the eave: a plant item on a pitch reads where it will stand.
		var deck = ArchRoofGen.Seat( roof, Owner.Kit, min, max, lightDraft.CurbWidth );
		var top = deck + ArchRoofGen.Stack( lightDraft, Owner.Kit ).Top;

		ArchGhost.Ring( roof.Outline(), deck );
		ArchGhost.Volume( min, max, deck, top );

		var note = $"{lightDraft.Form.Label()} {max.x - min.x:0} x {max.y - min.y:0} on {roof.Name} at deck {deck:0}";

		if ( roof.Style == RoofStyle.Gable ) {
			note += " — across the ridge gives one per slope";
		}

		ArchGhost.Note( new Vector3( max.x, max.y, top ), note );
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		if ( fitting ) {
			CarveLight( from, to );
			return;
		}

		var min = Min( from, to );
		var max = Max( from, to );

		if ( (max - min).Length < 8f ) {
			return;
		}

		var building = Owner.ActiveBuilding();
		var height = Owner.LevelHeight + MathF.Max( 32f, Owner.Kit.WallHeight - MathF.Max( 0f, eaveDrop ) );

		var roof = ArchBuild.Roof( Owner.Plan, Owner.Kit, Owner.Level, draft.Style, min, max, height, draft.Gutters, draft.Parapet, ridge );
		roof.Name = $"Roof{building.Roofs.Count + 1}";

		building.Roofs.Add( roof );
		FinishDraft( roof.Id );
		Owner.Commit();
	}

	void CarveLight( Vector2 from, Vector2 to ) {
		var roof = ArchRoofLight.Under( Owner.Plan, Owner.Level, from, lightDraft.Form );

		if ( roof is null ) {
			var over = ArchRoofLight.Over( Owner.Plan, Owner.Level, from );

			Log.Info( over is null
				? "Architecture: no roof section under that drag at all. A light is cut out of a deck already there, and plant stands on one."
				: $"Architecture: {over.Name} is a {over.Style}, and a {lightDraft.Form.Label().ToLowerInvariant()} is not carried by one. {Carried()}." );

			return;
		}

		if ( Levelled( roof ) ) {
			Log.Info( $"Architecture: {roof.Name} is flat, and a dormer breaks OUT of a slope — a level deck has no"
				+ " downhill for it to face. Put one on a Shed or a Gable, or use a Monitor for a raised glazed box on the flat." );

			return;
		}

		if ( ArchRoofLight.Add( Owner.Plan, roof, from, to, lightDraft ) is not { } light ) {
			Log.Info( $"Architecture: that drag is smaller than {ArchRoofLight.MinSize} inches on a side, so there is no light in it." );
			return;
		}

		FinishDraft( light.Id );
		Owner.Commit( $"Add {light.Form.Label()}" );
	}

	// A dormer is the one form with a direction, and it takes it from the fall of the deck it stands on.
	bool Levelled( ArchRoofPart roof ) => lightDraft.Form.Roofed() && roof.Style == RoofStyle.Flat;

	// What the draft's own form is carried by, so a refusal names the sections that would take it rather than
	// reciting the same list of three whatever was picked.
	string Carried() {
		return lightDraft.Form.Roofed()
			? "a dormer wants a slope, so Shed, Gable and Hip carry one and a Flat does not"
			: "Flat, Shed and Gable carry one; a Hip is a skeleton of triangles and trapezoids, which the carve grid cannot hold";
	}

	// Every form comes off its own stock - a kerb is a flashing upstand, a housing is a low wall, a flue and a stack
	// come off the kit's plant sizes - so switching form restocks it. An unchanged pick keeps whatever was typed.
	void Restock( RoofLightForm form ) {
		if ( lightDraft.Form == form ) {
			return;
		}

		lightDraft.Form = form;
		lightDraft.Curb = form.RiseStock( Owner.Kit );
		lightDraft.CurbWidth = form.ReachStock( Owner.Kit );
		lightDraft.PaneSpan = form.PaneStock();
	}

	// The tool draws two different things, so the first step asks WHICH - and the rest of the sequence is the one
	// that thing has. A section is styled, run and dressed; a fitting is glazed or not and sized, and everything
	// about the deck it lands on is answered by the drag rather than by a control.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		if ( fitting ) {
			panel.Layout.Add( ArchSidebarLayout.Advice( "Stands on the deck plane under it, never a typed height, so it follows the pitch — and clips to the deck it lands on." ) );
		}

		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Kind", KindName, KindGlyph, PickKind );

		if ( fitting ) {
			if ( lightDraft.Form.Glazes() ) {
				flow.Step( "Glazing", GlazingName, GlazingGlyph, PickGlazing );
			}

			flow.Step( "Size", SizeName, "straighten", BuildSize );

			return;
		}

		flow.Step( "Style", draft.Style.ToString(), ArchIcons.RoofStyleGlyph( draft.Style ), PickStyle );

		if ( ArchRoofPlane.Ridged( draft.Style ) ) {
			flow.Step( "Ridge", RidgeName, ArchIcons.RidgeGlyph( ridge ), PickRidge );
		}

		flow.Step( "Cornice", CorniceName, "view_stream", step => PickCornice( panel, step ) );

		flow.Step( "Edges", EdgesName, "crop_din", BuildEdges );

		flow.Step( "Eave", $"{eaveDrop:0} down", "vertical_align_bottom", BuildEave );
	}

	string KindName => fitting ? lightDraft.Form.Label() : "Section";

	string KindGlyph => fitting ? lightDraft.Form.Glyph() : "roofing";

	void PickKind( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Section — a roof over the footprint you drag", "roof_hip", "roofing", !fitting, () => Draw( step ) );

		foreach ( var value in Enum.GetValues<RoofLightForm>() ) {
			var form = value;

			grid.Pick( $"{form.Label()} — {form.Advice()}", form.Slug(), form.Glyph(), fitting && lightDraft.Form == form,
				() => Fit( form, step ) );
		}
	}

	void Draw( ArchWorkflowStep step ) {
		fitting = false;

		step.Chose();
	}

	void Fit( RoofLightForm form, ArchWorkflowStep step ) {
		fitting = true;

		Restock( form );

		step.Chose();
	}

	void PickStyle( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		foreach ( var value in Enum.GetValues<RoofStyle>() ) {
			var captured = value;

			grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ),
				draft.Style == captured, () => Shape( captured, step ) );
		}
	}

	void Shape( RoofStyle style, ArchWorkflowStep step ) {
		draft.Style = style;

		step.Chose();
	}

	string RidgeName => ridge.ToString();

	void PickRidge( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		foreach ( var value in Enum.GetValues<RidgeRun>() ) {
			var captured = value;

			grid.Pick( ArchIcons.RidgeAdvice( captured ), ArchIcons.RidgeSlug( captured ),
				ArchIcons.RidgeGlyph( captured ), ridge == captured, () => Run( captured, step ) );
		}
	}

	void Run( RidgeRun run, ArchWorkflowStep step ) {
		ridge = run;

		step.Chose();
	}

	string CorniceName => Owner.Kit.FindCornice( draft.Cornice ) is { } style ? style.Name : "None";

	// The same shelf a wall head crowns itself from, hung off the SIDEBAR so its designer survives the refresh
	// that picking a style triggers.
	void PickCornice( ToolSidebarWidget panel, ArchWorkflowStep step ) {
		step.Add( ArchCorniceUi.Picker( panel, Owner.Kit, Scope( "cornices" ), () => draft.Cornice,
			name => Crown( name, step ) ) );
	}

	void Crown( string cornice, ArchWorkflowStep step ) {
		draft.Cornice = cornice;

		step.Chose();
	}

	string EdgesName {
		get {
			var worn = new List<string> { draft.Parapet ? "Parapet" : draft.Gutters ? "Gutters" : "Bare" };

			if ( draft.Guardrail ) {
				worn.Add( "guardrail" );
			}

			return string.Join( ", ", worn );
		}
	}

	void BuildEdges( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Parapet wall cap", "opt_parapet", "crop_din", draft.Parapet,
			value => { draft.Parapet = value; Refresh(); } );

		// A parapet stands where the gutter would hang, so the deck drains inside it and ArchBuild.Roof refuses
		// gutters outright - offering the toggle would be offering nothing.
		if ( !draft.Parapet ) {
			grid.Toggle( "Gutters", "opt_gutters", "water_damage", draft.Gutters, value => draft.Gutters = value );
		}

		grid.Toggle( "Guardrail round the roof edge", "opt_guardrail", "fence", draft.Guardrail,
			value => draft.Guardrail = value );
	}

	// Dropping the eave is how a section steps down under a taller one, so it reads as a fall rather than a height.
	void BuildEave( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Number( "Eave drop", eaveDrop, 0f, value => eaveDrop = value ) );
	}

	string GlazingName => lightDraft.Glazed ? "Glazed" : "Open";

	string GlazingGlyph => lightDraft.Glazed ? "window" : "crop_free";

	void PickGlazing( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Glazed — a pane in its head", "opt_glazed", "window", lightDraft.Glazed, () => Glaze( true, step ) );
		grid.Pick( "Open — the hole left as it is", "opt_unglazed", "crop_free", !lightDraft.Glazed, () => Glaze( false, step ) );
	}

	void Glaze( bool glazed, ArchWorkflowStep step ) {
		lightDraft.Glazed = glazed;

		step.Chose();
	}

	string SizeName {
		get {
			var form = lightDraft.Form;
			var rise = lightDraft.Curb > 0f ? lightDraft.Curb : form.RiseStock( Owner.Kit );
			var reach = lightDraft.CurbWidth > 0f ? lightDraft.CurbWidth : form.ReachStock( Owner.Kit );

			return $"{rise:0} up, {reach:0} thick";
		}
	}

	// The footprint is never here - it is dragged on the deck. What the two numbers MEAN on this form is
	// ArchRoofLightKinds' answer, so a chimney is never offered a pane width it would ignore.
	void BuildSize( ArchWorkflowStep step ) {
		var form = lightDraft.Form;

		step.Layout.Add( ArchPartUi.Number( form.RiseLabel(), lightDraft.Curb, form.RiseStock( Owner.Kit ),
			value => lightDraft.Curb = value ) );

		step.Layout.Add( ArchPartUi.Number( form.ReachLabel(), lightDraft.CurbWidth, form.ReachStock( Owner.Kit ),
			value => lightDraft.CurbWidth = value ) );

		if ( form.Glazes() && lightDraft.Glazed ) {
			step.Layout.Add( ArchPartUi.Number( "Widest pane", lightDraft.PaneSpan, form.PaneStock(),
				value => lightDraft.PaneSpan = value ) );
		}
	}
}