Editor/Effigy/Multires.cs
using System;
using System.Collections.Generic;

namespace Effigy;

/// <summary>
/// Multiresolution sculpt levels over one cage.
///
/// THE ONE RULE THE WHOLE THING RESTS ON: level N+1's rest surface is the subdivision of level N
/// *displaced*, not of level N at rest. Everything else here is bookkeeping around that sentence.
///
/// It is what makes a low-level edit carry the high-level detail. Sculpt a pore at L3, then go
/// back to L1 and pull the jaw out: L1's deltas move the surface L2 and L3 are subdivided from,
/// so the pore rides the jaw. Subdivide the cage a fixed number of times instead and the jaw edit
/// either flattens the pore or leaves it floating off the surface — which is the failure this
/// design exists to avoid, and the reason a sculpt is a stack of levels rather than one dense mesh.
///
/// Level 0 is the cage itself and always exists; it carries deltas like any other level, so a
/// coarse edit is available without adding a level first. <see cref="ViewLevel"/> is display only —
/// dropping it shows fewer levels and DISCARDS NOTHING. Only <see cref="RemoveTopLevel"/> destroys
/// deltas, and it hands them back so the caller can undo it.
///
/// Deltas themselves are <see cref="SculptLayer"/>s in derived <see cref="SculptFrames"/>, so every
/// level rides a cage edit for the same reason a single level does — see <see cref="SetCage"/>.
/// </summary>
public sealed class MultiresSculpt
{
	PolyMesh _cage;
	int _viewLevel;

	readonly List<SculptLayer> _layers = new();

	// Rest mesh and frames per level. Cache ONLY: every entry is a pure function of the cage and
	// the layers below it, so throwing the whole thing away costs time and changes no answer.
	// Both are needed together — a layer is captured and applied through the frames of its own
	// rest mesh, and pairing a layer with anyone else's frames silently bends the sculpt.
	readonly List<PolyMesh> _rest = new();
	readonly List<SculptFrames> _frames = new();

	public MultiresSculpt( PolyMesh cage )
	{
		if ( cage is null )
			throw new ArgumentNullException( nameof( cage ) );

		_cage = cage.Clone();
		_layers.Add( Zero( _cage.VertexCount ) );
	}

	/// <summary>
	/// Bumped by every change that alters the model: adding or removing a level, recording deltas,
	/// setting a layer, re-basing the cage. A feature compares it against the revision it last built
	/// at to know its cached geometry is stale — see Feature.IsStale.
	///
	/// ViewLevel deliberately does NOT bump it. The view is not the model, and a preview level that
	/// forced a rebuild would be both wrong and slow.
	/// </summary>
	public int Revision { get; private set; }

	/// <summary>The highest level that exists. 0 means the cage alone.</summary>
	public int TopLevel => _layers.Count - 1;

	/// <summary>How many levels exist, cage included. Always at least 1.</summary>
	public int LevelCount => _layers.Count;

	/// <summary>Which level is displayed. Purely a view: the levels above it keep their deltas.</summary>
	public int ViewLevel
	{
		get => _viewLevel;
		set
		{
			if ( value < 0 || value > TopLevel )
				throw new ArgumentOutOfRangeException( nameof( value ),
					$"View level {value} does not exist; this sculpt has levels 0 to {TopLevel}." );

			_viewLevel = value;
		}
	}

	/// <summary>The cage, as given. A copy — edit it through <see cref="SetCage"/>.</summary>
	public PolyMesh Cage => _cage.Clone();

	/// <summary>The stored deltas at one level, for persistence and for undo.</summary>
	public SculptLayer LayerAt( int level )
	{
		Validate( level );
		return _layers[level];
	}

	/// <summary>Whether anything has actually been sculpted at this level.</summary>
	public bool HasDetail( int level )
	{
		Validate( level );

		foreach ( var d in _layers[level].Deltas )
		{
			if ( d.LengthSquared > 1e-20f )
				return true;
		}

		return false;
	}

	/// <summary>
	/// Vertex and face count at a level, without building it — what a level slider warns with
	/// before the user asks for something that costs 400k vertices.
	/// </summary>
	public (int Vertices, int Faces) Cost( int level ) => CatmullClark.PredictCost( _cage, level );

	/// <summary>
	/// Add a level above the current top and make it the view. Its deltas start at zero, so the
	/// model does not change shape — the new level is a finer surface over the same shape, which is
	/// the only sane thing for "add a level" to mean.
	/// </summary>
	public int AddLevel()
	{
		var level = _layers.Count;
		EnsureBuilt( level );
		_layers.Add( Zero( _rest[level].VertexCount ) );
		_viewLevel = level;
		Revision++;
		return level;
	}

	/// <summary>
	/// Put a level back on top, with the deltas it had.
	///
	/// The other half of <see cref="RemoveTopLevel"/>, and the reason that one can be offered at all:
	/// dropping a level is the only call here that destroys anything, and a destructive button with
	/// no way back is one nobody should be given.
	///
	/// The layer has to be the right size for the level it is going onto - which it will be, if it
	/// came off the top and nothing below it has been sculpted since. If something HAS, the count no
	/// longer matches and this refuses rather than putting the old detail on new vertices.
	/// </summary>
	public void RestoreTopLevel( SculptLayer layer )
	{
		if ( layer is null )
			throw new ArgumentNullException( nameof( layer ) );

		var level = AddLevel();
		var expected = _layers[level].Count;

		if ( expected != layer.Count )
		{
			// Undo the AddLevel, so a refusal leaves the sculpt exactly as it found it.
			_layers.RemoveAt( level );
			Trim( _layers.Count );

			if ( _viewLevel > TopLevel )
				_viewLevel = TopLevel;

			throw new ArgumentException(
				$"That layer holds {layer.Count} deltas and level {level} now has {expected} vertices - "
				+ "the levels below it have been sculpted since it was removed, so its detail no longer "
				+ "belongs to those vertices." );
		}

		SetLayer( level, layer );
	}

	/// <summary>
	/// Drop the top level, returning its deltas so the caller can put it back with
	/// <see cref="RestoreTopLevel"/>. The only call here that destroys anything, which is why it
	/// hands the evidence over rather than swallowing it.
	/// </summary>
	public SculptLayer RemoveTopLevel()
	{
		if ( _layers.Count == 1 )
			throw new InvalidOperationException(
				"Level 0 is the cage, not a sculpt level, so it cannot be removed." );

		var dropped = _layers[^1];
		_layers.RemoveAt( _layers.Count - 1 );
		Trim( _layers.Count );

		if ( _viewLevel > TopLevel )
			_viewLevel = TopLevel;

		Revision++;
		return dropped;
	}

	/// <summary>
	/// The surface a level is sculpted against: every layer below it applied, then subdivided, but
	/// WITHOUT this level's own deltas. Deltas are measured from here, so this is also what their
	/// frames are built on.
	/// </summary>
	public PolyMesh Rest( int level ) => RestMesh( level ).Clone();

	/// <summary>The level as it looks: <see cref="Rest"/> plus this level's own deltas.</summary>
	public PolyMesh Evaluate( int level )
	{
		Validate( level );
		var mesh = RestMesh( level ).Clone();
		_layers[level].Apply( mesh, FramesFor( level ) );
		return mesh;
	}

	/// <summary>What the viewport shows.</summary>
	public PolyMesh Display() => Evaluate( _viewLevel );

	/// <summary>
	/// Frames of a level's REST surface — what <see cref="Record"/> measures deltas through. A brush
	/// wants the frames of the displaced mesh instead, which is what <see cref="Stroke"/> is for.
	/// </summary>
	public SculptFrames FramesFor( int level )
	{
		Validate( level );
		EnsureBuilt( level );
		return _frames[level];
	}

	/// <summary>
	/// Store a sculpted mesh as this level's deltas. The mesh must be <see cref="Evaluate"/> of that
	/// level after a brush has moved its vertices — same vertices, moved, never added or removed.
	///
	/// Levels above are left exactly as they are and simply re-evaluate against the new surface.
	/// That is the whole feature: this call is what a low-level edit is, and it does nothing to the
	/// detail above it.
	/// </summary>
	public void Record( int level, PolyMesh sculpted )
	{
		Validate( level );

		if ( sculpted is null )
			throw new ArgumentNullException( nameof( sculpted ) );

		var rest = RestMesh( level );

		if ( sculpted.VertexCount != rest.VertexCount )
			throw new ArgumentException(
				$"Level {level} has {rest.VertexCount} vertices and the sculpted mesh has "
				+ $"{sculpted.VertexCount}. A brush moves vertices; it must not add or remove them." );

		_layers[level] = SculptLayer.Capture( rest, sculpted, FramesFor( level ) );
		Trim( level + 1 );
		Revision++;
	}

	/// <summary>
	/// Put deltas straight into a level, without a mesh to measure them from. Persistence reads them
	/// back this way, and so does an undo that kept a whole layer rather than a stroke's diff.
	///
	/// The count has to match what the cage produces at that level, which is why it is checked here
	/// rather than trusted: deltas from the wrong level land on real vertex indices and produce a
	/// mesh that is merely wrong rather than obviously broken.
	/// </summary>
	public void SetLayer( int level, SculptLayer layer )
	{
		Validate( level );

		if ( layer is null )
			throw new ArgumentNullException( nameof( layer ) );

		var expected = RestMesh( level ).VertexCount;

		if ( layer.Count != expected )
			throw new ArgumentException(
				$"Level {level} has {expected} vertices and this layer has {layer.Count}." );

		_layers[level] = layer;
		Trim( level + 1 );
		Revision++;
	}

	/// <summary>
	/// Run a stroke at one level and store the result there. Returns the sparse undo from
	/// <see cref="Brush"/>, which <see cref="Undo"/> takes back.
	///
	/// THIS EXISTS TO GET THE TWO SETS OF FRAMES THE RIGHT WAY ROUND. A brush works on the surface
	/// the user can see, so it takes the frames of the DISPLACED mesh; a delta is measured from the
	/// rest surface, so <see cref="Record"/> captures through the frames of the REST mesh. Both are
	/// correct in their own place, and swapping them is invisible on a fresh level — the two are the
	/// same mesh until something has been sculpted there — and wrong on every level after that.
	///
	/// Of the six brushes only Inflate reads the frames at all; Draw, Grab, Flatten and Pinch take
	/// their direction from the stroke sample, and Smooth from the neighbours. So the bug this
	/// prevents is narrow, and correspondingly easy to leave in.
	/// </summary>
	public BrushUndo Stroke( int level, BrushStroke stroke, float[] mask = null )
	{
		Validate( level );

		if ( stroke is null )
			throw new ArgumentNullException( nameof( stroke ) );

		var displaced = Evaluate( level );
		var undo = Brush.Apply( displaced, stroke, SculptFrames.Build( displaced ), mask );
		Record( level, displaced );

		return undo;
	}

	/// <summary>
	/// Put back what one stroke moved. Sparse, like the undo itself — the alternative is a copy of
	/// every delta per stroke, which at L4 is most of a megabyte for a flick of the wrist.
	/// </summary>
	public void Undo( int level, BrushUndo undo )
	{
		Validate( level );

		if ( undo is null )
			throw new ArgumentNullException( nameof( undo ) );

		var displaced = Evaluate( level );
		undo.Restore( displaced );
		Record( level, displaced );
	}

	/// <summary>
	/// Swap in a rebuilt cage. Every level re-derives its frames from the new surface and every
	/// delta rides the edit — the parametric half of the tool staying editable is the entire point
	/// of storing deltas in a derived frame.
	///
	/// Refuses a cage of different topology rather than misapplying the deltas or silently dropping
	/// them. Reprojection (step 10) is what will eventually make that case work; until it exists a
	/// clear refusal is the honest answer, and the deltas are untouched by it.
	/// </summary>
	public void SetCage( PolyMesh cage )
	{
		if ( cage is null )
			throw new ArgumentNullException( nameof( cage ) );

		if ( !CanRebase( cage, out var why ) )
			throw new InvalidOperationException( why );

		_cage = cage.Clone();
		Trim( 0 );
		Revision++;
	}

	/// <summary>
	/// Whether <see cref="SetCage"/> would accept this cage, and if not, why — in the shape a
	/// diagnostic wants: what stopped it, with both models' numbers, and what would work.
	/// </summary>
	public bool CanRebase( PolyMesh cage, out string why )
	{
		if ( cage is null )
			throw new ArgumentNullException( nameof( cage ) );

		if ( cage.VertexCount != _cage.VertexCount || cage.FaceCount != _cage.FaceCount )
		{
			why = $"The rebuilt cage has {cage.VertexCount} vertices and {cage.FaceCount} faces; this "
				+ $"sculpt was made on {_cage.VertexCount} vertices and {_cage.FaceCount} faces. Deltas "
				+ "are stored per vertex, so they cannot be placed on it. Undo the feature edit that "
				+ "changed the cage's topology, or re-sculpt on the new cage.";
			return false;
		}

		for ( var f = 0; f < cage.FaceCount; f++ )
		{
			var a = _cage.Faces[f].Indices;
			var b = cage.Faces[f].Indices;

			if ( a.Length != b.Length || !SameIndices( a, b ) )
			{
				why = $"The rebuilt cage has the same vertex and face counts but face {f} joins "
					+ "different vertices, so it is a different surface wearing the same numbers. "
					+ "Applying the deltas would put every detail somewhere arbitrary. Undo the feature "
					+ "edit that changed the cage's topology, or re-sculpt on the new cage.";
				return false;
			}
		}

		why = null;
		return true;
	}

	/// <summary>
	/// A stable id for the cage's topology — counts and face indices, deliberately NOT positions,
	/// because positions are exactly what a parametric edit is expected to change. Persistence
	/// (step 6) stores this beside the deltas so a reopened document can tell a moved cage from a
	/// rebuilt one without keeping a copy of the old mesh.
	/// </summary>
	public static long TopologyId( PolyMesh mesh )
	{
		if ( mesh is null )
			throw new ArgumentNullException( nameof( mesh ) );

		// FNV-1a, written out rather than leaning on GetHashCode: this value goes in a file and has
		// to mean the same thing in the next process and on the next runtime.
		const long prime = 0x100000001b3;
		var hash = unchecked((long)0xcbf29ce484222325);

		void Mix( int value )
		{
			for ( var b = 0; b < 4; b++ )
			{
				hash ^= (value >> (b * 8)) & 0xff;
				hash = unchecked(hash * prime);
			}
		}

		Mix( mesh.VertexCount );
		Mix( mesh.FaceCount );

		foreach ( var face in mesh.Faces )
		{
			Mix( face.Count );

			foreach ( var i in face.Indices )
				Mix( i );
		}

		return hash;
	}

	static bool SameIndices( int[] a, int[] b )
	{
		for ( var i = 0; i < a.Length; i++ )
		{
			if ( a[i] != b[i] )
				return false;
		}

		return true;
	}

	static SculptLayer Zero( int vertexCount ) => new( new Vec3[vertexCount] );

	void Validate( int level )
	{
		if ( level < 0 || level > TopLevel )
			throw new ArgumentOutOfRangeException( nameof( level ),
				$"Level {level} does not exist; this sculpt has levels 0 to {TopLevel}." );
	}

	PolyMesh RestMesh( int level )
	{
		Validate( level );
		EnsureBuilt( level );
		return _rest[level];
	}

	/// <summary>Build rest surfaces and frames up to `level`, reusing whatever is still valid.</summary>
	void EnsureBuilt( int level )
	{
		if ( _rest.Count == 0 )
		{
			var cage = _cage.Clone();
			_rest.Add( cage );
			_frames.Add( SculptFrames.Build( cage ) );
		}

		for ( var l = _rest.Count; l <= level; l++ )
		{
			// The one rule, in code: displace the level below, THEN subdivide.
			var below = _rest[l - 1].Clone();
			_layers[l - 1].Apply( below, _frames[l - 1] );

			var rest = CatmullClark.Subdivide( below, 1 );
			_rest.Add( rest );
			_frames.Add( SculptFrames.Build( rest ) );
		}
	}

	/// <summary>Drop the cached surfaces from `level` up; they sit downstream of something that moved.</summary>
	void Trim( int level )
	{
		if ( _rest.Count > level )
			_rest.RemoveRange( level, _rest.Count - level );

		if ( _frames.Count > level )
			_frames.RemoveRange( level, _frames.Count - level );
	}
}