Editor/RigControlEditor/RigCamerasPanel.cs
using Editor;
using Marionette;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Marionette.Tools;

/// <summary>
/// The Cameras tab - the shots this clip was framed with, and the one place they are edited.
///
/// WHY CAMERAS ARE A FEATURE OF AN ANIMATION TOOL AT ALL. The framing you find by flying the
/// viewport is the framing the final shot should have, and the only way to get back to it - now,
/// after a reload, or in the game - is to write the numbers down. A camera is that write-down: a
/// name, a transform, a lens.
///
/// A CAMERA LIVES ON THE CLIP, NOT IN A PREFERENCE. The useful framing is per-shot: him at the
/// desk wants the shot over the desk's shoulder and the next clip wants something else. Kept as an
/// editor setting it would follow you between clips and be wrong for most of them.
///
/// AND IT IS WORKSPACE UNTIL TICKED OTHERWISE. Export With Clip is off by default, so a camera
/// dropped to look at an elbow stays in this window; ticked, RigAnimPlayerComponent spawns it when
/// the clip plays and the export bakes it into the shot file.
///
/// Modelled on RigLightsPanel: a list of small objects, each a property sheet with a row of buttons
/// under it. Same shape because it is the same job, and a second layout for it would be a second
/// thing to keep in step.
/// </summary>
internal sealed class RigCamerasPanel : Widget
{
	private readonly RigViewport _viewport;
	private readonly Widget _list;

	private RigAnimDocument _anim;

	/// <summary>Raised after this panel has changed the document, with the undo label. Same
	/// contract as every other panel in this window: the panel edits the list, the window
	/// rebuilds, marks dirty and records the undo step.</summary>
	public Action<string> Changed { get; set; }

	public RigCamerasPanel( Widget parent, RigViewport viewport ) : base( parent )
	{
		_viewport = viewport;

		Name = "Cameras";
		WindowTitle = "Cameras";
		SetWindowIcon( "videocam" );

		Layout = Layout.Column();

		Layout.Add( RigHelpBox.Create( this,
			"Shots for this clip, saved with it. A camera records where you looked from when a " +
			"moment read right, so you can get back to that framing - now, after a reload, or " +
			"in the game.",
			new[]
			{
				RigHelpBox.S( "Place At Camera",
					"Puts the selected camera where you are looking from, aimed the way you are " +
					"looking, with the viewport's own field of view. Fly to where the shot " +
					"should be, click the button. That is the whole workflow - find the framing, " +
					"then save it." ),

				RigHelpBox.S( "Export With Clip",
					"Off means the camera is workspace - this window only, never in a game. On " +
					"means RigAnimPlayerComponent spawns a camera at it when the clip plays, and " +
					"the export bakes it into the shot file. Either way the framing is saved on " +
					"the clip." ),
			} ) );

		var bar = Layout.AddRow();
		bar.Margin = 4;
		bar.Add( new Editor.Label( "Cameras" ), 1 );
		bar.Add( new Button( "Add Camera", "add" ) { Clicked = Add } );

		var scroll = Layout.Add( new ScrollArea( this ), 1 );
		scroll.VerticalScrollbarMode = ScrollbarMode.Auto;
		scroll.HorizontalScrollbarMode = ScrollbarMode.Off;

		_list = new Widget( this ) { Layout = Layout.Column() };
		_list.Layout.Margin = 4;
		_list.Layout.Spacing = 4;
		scroll.Canvas = _list;

		Rebuild();
	}

	public void SetAnim( RigAnimDocument anim )
	{
		_anim = anim;
		Rebuild();
	}

	private List<RigCamera> Cameras => _anim?.Cameras ??= new List<RigCamera>();

	private void Add()
	{
		if ( _anim is null )
			return;

		var camera = new RigCamera { Name = UniqueName() };

		// A new camera lands where you are looking from, with the viewport's own lens, rather than
		// at the origin inside the model - the first thing you do after adding a camera should not
		// have to be digging it out of somebody's chest.
		PlaceAtCamera( camera );

		Cameras.Add( camera );
		Rebuild();
		Changed?.Invoke( "Add Camera" );
	}

	private string UniqueName()
	{
		var existing = Cameras.Select( c => c?.Name ).ToHashSet();
		var index = 0;

		while ( existing.Contains( $"camera {index}" ) )
			index++;

		return $"camera {index}";
	}

	public void Rebuild()
	{
		_list.Layout.Clear( true );

		if ( _anim is null )
		{
			_list.Layout.Add( new Editor.Label( "Open a Rig Animation asset to add cameras." ) { Enabled = false } );
			return;
		}

		foreach ( var camera in Cameras.Where( c => c is not null ).ToList() )
			_list.Layout.Add( Build( camera ) );

		if ( Cameras.Count == 0 )
		{
			_list.Layout.Add( new Editor.Label(
				"No cameras. Add one and place it where the shot should be." )
			{ Enabled = false } );
		}
	}

	private Widget Build( RigCamera camera )
	{
		var panel = new Widget( _list ) { Layout = Layout.Column() };
		panel.Layout.Margin = 6;
		panel.Layout.Spacing = 4;

		var header = panel.Layout.AddRow();
		header.Spacing = 6;

		var chip = new Editor.Label( "[cam]" ) { Color = Theme.Blue };
		chip.SetStyles( "font-weight: bold;" );
		header.Add( chip );
		header.Add( new Editor.Label( camera.Name ) { Enabled = false }, 1 );

		// The one badge worth having on the header: whether this camera leaves the window. It is
		// the difference between a working shot and something that will show up in a game.
		if ( camera.Export )
		{
			var exported = new Editor.Label( "exports" ) { Color = Theme.Green };
			exported.SetStyles( "font-weight: bold;" );
			header.Add( exported );
		}

		var serialized = EditorTypeLibrary.GetSerializedObject( camera );

		serialized.OnPropertyChanged += _ =>
		{
			Changed?.Invoke( "Edit Camera" );
			Rebuild();
		};

		var sheet = new ControlSheet();
		sheet.AddObject( serialized );
		panel.Layout.Add( sheet );

		panel.Layout.Add( Buttons( camera ) );

		return panel;
	}

	private Widget Buttons( RigCamera camera )
	{
		var row = new Widget { Layout = Layout.Row() };
		row.Layout.Spacing = 4;

		var toggle = new Button( camera.Enabled ? "On" : "Off", camera.Enabled ? "toggle_on" : "toggle_off" )
		{
			ToolTip = "Turn this camera off without losing it - the way to see what one shot is doing"
		};

		toggle.Clicked = () =>
		{
			camera.Enabled = !camera.Enabled;
			toggle.Text = camera.Enabled ? "On" : "Off";
			toggle.Icon = camera.Enabled ? "toggle_on" : "toggle_off";
			Changed?.Invoke( "Toggle Camera" );
		};

		row.Layout.Add( toggle );

		row.Layout.Add( new Button( "Place At Camera", "photo_camera" )
		{
			ToolTip = "Move this camera to the viewport camera, aimed where you are looking",
			Clicked = () =>
			{
				PlaceAtCamera( camera );
				Rebuild();
				Changed?.Invoke( "Place Camera" );
			}
		} );

		row.Layout.AddStretchCell();

		row.Layout.Add( new Button( "Delete", "delete" )
		{
			Clicked = () =>
			{
				Cameras.Remove( camera );
				Rebuild();
				Changed?.Invoke( "Delete Camera" );
			}
		} );

		return row;
	}

	/// <summary>Where you are looking from, aimed where you are looking, with the viewport's own
	/// field of view - so the shot reads back exactly as it looked.</summary>
	private void PlaceAtCamera( RigCamera camera )
	{
		var view = _viewport?.ViewCamera ?? Transform.Zero;

		camera.Position = view.Position;
		camera.Rotation = view.Rotation.Angles();
		camera.FieldOfView = _viewport?.ViewFov ?? 80f;
	}
}