Code/OceanWaves.cs
using System;
using System.Threading.Tasks;

namespace OceanLib;

/// <summary>
/// The sea an <see cref="Ocean"/> draws, replayed on the CPU for physics. It takes the swell cascade - every wave
/// longer than a few metres, the ones that lift and roll a hull - and rebuilds it exactly with an FFT of its own,
/// small enough for the CPU because the swell's waves all sit near the middle of the spectrum. The chop of the finer
/// cascades isn't replayed: a hull rides over it. No GPU readback and no latency, and it runs the same on a server
/// with no GPU, so every machine floats its boats on the same water.
///
/// It matches the GPU wave for wave: same wave vectors, frequencies snapped to the loop period in the same float
/// arithmetic, and the same half-texel shift the GPU's sampler applies to the cascade's map. The grid is rebuilt
/// <see cref="Rate"/> times a second of sea time and blended in between - the swell's shortest waves take over two
/// seconds to pass - and each one is built ahead of time on a worker thread (<see cref="Prefetch"/>), so the physics
/// never waits for one. The sea is linear in its amplitudes, so each grid holds both sea states apart and the blend
/// between them is applied as it's read: exact, whatever the blend has moved to since the grid was built.
/// </summary>
public sealed class OceanWaves
{
	/// <summary>Grid rebuilds per second of sea time.</summary>
	public const float Rate = 20.0f;

	/// <summary>Points per side of the CPU's grid.</summary>
	public int GridSize { get; }

	/// <summary>How many waves it replays.</summary>
	public int Count { get; }

	/// <summary>Metres the grid covers: the swell cascade's tile.</summary>
	public float TileLength { get; }

	/// <summary>How far from the first sea state to the second, as on the GPU.</summary>
	public float Blend { get; set; }

	/// <summary>Grids built on the main thread because none was ready - ideally none after the first.</summary>
	public int SynchronousBuilds { get; private set; }

	/// <summary>Grids built on worker threads.</summary>
	public int BackgroundBuilds { get; private set; }

	/// <summary>The sea at one moment, on the grid, for each sea state: height, x-shift and y-shift in metres.</summary>
	private sealed class Grid
	{
		// The moment held, as a count of steps: whole numbers, so a moment found and a moment asked for always match.
		public int Step = int.MinValue;

		// Built on the main thread (Pending null and Filled), or by a worker whose task has finished - which is also
		// what makes its writes visible here.
		public bool Filled;
		public Task Pending;
		public bool Ready => Filled && (Pending is null || Pending.IsCompleted);

		public float[][] Height, ShiftX, ShiftY;
		public float[] WorkRe, WorkIm, PairRe, PairIm, LineRe, LineIm;
	}

	// The two in use, and the next three: a velocity reads the sea a step ahead, and the physics can run a little ahead
	// of the frame that asked for the prefetch.
	private const int GridCount = 5;
	private const int Lookahead = 3;

	private readonly int m_Log;
	private readonly float m_Shift;
	private readonly float m_Loop;
	private readonly bool m_Same;
	private readonly int[] m_Index;
	private readonly float[] m_Kx, m_Ky, m_Omega;

	// Each wave's h0 and conj(h0(-k)), per sea state.
	private readonly float[][] m_Are, m_Aim, m_Cre, m_Cim;

	private readonly float[] m_TwiddleRe, m_TwiddleIm;
	private readonly int[] m_Reverse;
	private readonly List<Grid> m_Grids = new();

	public OceanWaves( OceanSpectrum from, OceanSpectrum to, float loopPeriod )
	{
		int n = from.Size;
		TileLength = from.TileLength;
		m_Loop = loopPeriod;
		m_Same = ReferenceEquals( from, to );

		// Every wave either sea state has, and how far from the middle of the spectrum they reach: the grid just has
		// to hold them.
		int reach = 0, count = 0;

		for ( int i = 0; i < from.Texels.Length; i++ )
		{
			if ( from.Texels[i] == Vector4.Zero && to.Texels[i] == Vector4.Zero )
				continue;

			count++;
			reach = Math.Max( reach, Math.Max( Math.Abs( i % n - n / 2 ), Math.Abs( i / n - n / 2 ) ) );
		}

		GridSize = 16;
		while ( GridSize / 2 <= reach )
			GridSize *= 2;

		GridSize = Math.Min( GridSize, n );
		m_Log = (int)Math.Round( Math.Log2( GridSize ) );

		// The GPU samples its map with texel centres half a texel in, so the map it shows at a spot is the sea half a
		// texel back.
		m_Shift = 0.5f * TileLength / n;

		Count = count;
		m_Index = new int[count];
		m_Kx = new float[count]; m_Ky = new float[count]; m_Omega = new float[count];
		m_Are = new[] { new float[count], new float[count] };
		m_Aim = new[] { new float[count], new float[count] };
		m_Cre = new[] { new float[count], new float[count] };
		m_Cim = new[] { new float[count], new float[count] };

		float omega0 = 2.0f * MathF.PI / loopPeriod;
		int w = 0;

		for ( int i = 0; i < from.Texels.Length; i++ )
		{
			var a = from.Texels[i];
			var b = to.Texels[i];

			if ( a == Vector4.Zero && b == Vector4.Zero )
				continue;

			int ix = i % n - n / 2, iy = i / n - n / 2;
			var k = from.WaveVector( i % n, i / n );

			m_Index[w] = ((iy + GridSize) % GridSize) * GridSize + (ix + GridSize) % GridSize;
			m_Kx[w] = k.x;
			m_Ky[w] = k.y;

			// As the GPU works it out, in floats, so a frequency on the edge of a step snaps the same way.
			m_Omega[w] = MathF.Floor( MathF.Sqrt( 9.81f * k.Length ) / omega0 ) * omega0;
			m_Are[0][w] = a.x; m_Aim[0][w] = a.y; m_Cre[0][w] = a.z; m_Cim[0][w] = a.w;
			m_Are[1][w] = b.x; m_Aim[1][w] = b.y; m_Cre[1][w] = b.z; m_Cim[1][w] = b.w;
			w++;
		}

		m_TwiddleRe = new float[GridSize / 2];
		m_TwiddleIm = new float[GridSize / 2];

		// Inverse transform: e^{+i 2 pi j / N}.
		for ( int j = 0; j < GridSize / 2; j++ )
			(m_TwiddleIm[j], m_TwiddleRe[j]) = MathF.SinCos( 2.0f * MathF.PI * j / GridSize );

		m_Reverse = new int[GridSize];
		for ( int j = 0; j < GridSize; j++ )
		{
			int r = 0;
			for ( int bit = 0; bit < m_Log; bit++ )
				r |= ((j >> bit) & 1) << (m_Log - 1 - bit);
			m_Reverse[j] = r;
		}

		for ( int g = 0; g < GridCount; g++ )
			m_Grids.Add( NewGrid() );
	}

	private Grid NewGrid()
	{
		int cells = GridSize * GridSize;
		int states = m_Same ? 1 : 2;

		var grid = new Grid
		{
			Height = new float[states][], ShiftX = new float[states][], ShiftY = new float[states][],
			WorkRe = new float[cells], WorkIm = new float[cells], PairRe = new float[cells], PairIm = new float[cells],
			LineRe = new float[GridSize], LineIm = new float[GridSize],
		};

		for ( int s = 0; s < states; s++ )
		{
			grid.Height[s] = new float[cells];
			grid.ShiftX[s] = new float[cells];
			grid.ShiftY[s] = new float[cells];
		}

		return grid;
	}

	private static int StepOf( float time ) => (int)MathF.Floor( time * Rate );

	private Grid Find( int step )
	{
		foreach ( var g in m_Grids )
			if ( g.Ready && g.Step == step )
				return g;

		return null;
	}

	private bool Pending( int step )
	{
		foreach ( var g in m_Grids )
			if ( g.Pending is { IsCompleted: false } && g.Step == step )
				return true;

		return false;
	}

	/// <summary>A grid not being built and not one of the moments in use: the oldest.</summary>
	private Grid FreeGrid( int keepA, int keepB )
	{
		Grid best = null;

		foreach ( var g in m_Grids )
		{
			if ( g.Pending is { IsCompleted: false } )
				continue;

			if ( g.Ready && (g.Step == keepA || g.Step == keepB) )
				continue;

			if ( best is null || !g.Ready || (best.Ready && g.Step < best.Step) )
				best = g;
		}

		return best;
	}

	/// <summary>
	/// Starts building, on worker threads, the grids the physics will need next after this time - call once a frame.
	/// </summary>
	public void Prefetch( float time )
	{
		int before = StepOf( time );

		for ( int ahead = 1; ahead <= Lookahead; ahead++ )
		{
			int step = before + ahead;

			if ( Find( step ) is not null || Pending( step ) )
				continue;

			var grid = FreeGrid( before, before + 1 );

			if ( grid is null )
				return;

			grid.Step = step;
			grid.Filled = true;
			BackgroundBuilds++;
			grid.Pending = GameTask.RunInThreadAsync( () => Build( grid, step / Rate ) );
		}
	}

	/// <summary>The grids either side of a time - built here only if no worker got to them - and how far between.</summary>
	private (Grid, Grid, float) Prepare( float time )
	{
		int before = StepOf( time );

		var a = Find( before ) ?? BuildNow( before, before + 1 );
		var b = Find( before + 1 ) ?? BuildNow( before + 1, before );
		return (a, b, time * Rate - before);
	}

	private Grid BuildNow( int step, int keep )
	{
		// Never waits on a worker: its task completes through the main thread, so waiting for it here never ends. A step
		// a worker is still on is built again here, and a spare grid is added if every one is busy - rare, and kept.
		var grid = FreeGrid( keep, keep );

		if ( grid is null )
		{
			grid = NewGrid();
			m_Grids.Add( grid );
		}

		grid.Pending = null;
		grid.Step = step;
		Build( grid, step / Rate );
		grid.Filled = true;
		SynchronousBuilds++;
		return grid;
	}

	private void Build( Grid grid, float time )
	{
		for ( int state = 0; state < grid.Height.Length; state++ )
			BuildState( grid, time, state );
	}

	private void BuildState( Grid grid, float time, int state )
	{
		Array.Clear( grid.WorkRe ); Array.Clear( grid.WorkIm ); Array.Clear( grid.PairRe ); Array.Clear( grid.PairIm );

		// The waves repeat over the loop period, so any time past it is the same sea as the time it wraps to.
		float t = time % m_Loop;
		var are = m_Are[state]; var aim = m_Aim[state]; var cre = m_Cre[state]; var cim = m_Cim[state];

		for ( int w = 0; w < Count; w++ )
		{
			var (s, c) = MathF.SinCos( m_Omega[w] * t );

			// h(k, t) = h0(k) e^{i w t} + conj(h0(-k)) e^{-i w t}
			float re = are[w] * c - aim[w] * s + cre[w] * c + cim[w] * s;
			float im = are[w] * s + aim[w] * c - cre[w] * s + cim[w] * c;

			float kl = MathF.Sqrt( m_Kx[w] * m_Kx[w] + m_Ky[w] * m_Ky[w] );
			float nx = m_Kx[w] / kl, ny = m_Ky[w] / kl;

			// Two real fields share one complex FFT as a + i b: height plus i times the x-shift, whose spectrum is
			// i kx/|k| h - so i times it is -kx/|k| h. The y-shift goes alone: i ky/|k| h.
			int at = m_Index[w];
			grid.WorkRe[at] += re * (1.0f - nx);
			grid.WorkIm[at] += im * (1.0f - nx);
			grid.PairRe[at] += -im * ny;
			grid.PairIm[at] += re * ny;
		}

		Transform( grid, grid.WorkRe, grid.WorkIm );
		Transform( grid, grid.PairRe, grid.PairIm );

		int cells = GridSize * GridSize;
		Array.Copy( grid.WorkRe, grid.Height[state], cells );
		Array.Copy( grid.WorkIm, grid.ShiftX[state], cells );
		Array.Copy( grid.PairRe, grid.ShiftY[state], cells );
	}

	/// <summary>Inverse 2D FFT in place: every row, then every column, radix 2.</summary>
	private void Transform( Grid grid, float[] re, float[] im )
	{
		int n = GridSize;

		for ( int row = 0; row < n; row++ )
			Line( grid, re, im, row * n, 1 );

		for ( int column = 0; column < n; column++ )
			Line( grid, re, im, column, n );
	}

	private void Line( Grid grid, float[] re, float[] im, int start, int stride )
	{
		int n = GridSize;
		var lr = grid.LineRe;
		var li = grid.LineIm;

		for ( int j = 0; j < n; j++ )
		{
			int from = start + m_Reverse[j] * stride;
			lr[j] = re[from];
			li[j] = im[from];
		}

		for ( int size = 2; size <= n; size <<= 1 )
		{
			int half = size >> 1;
			int step = n / size;

			for ( int block = 0; block < n; block += size )
			{
				for ( int j = 0; j < half; j++ )
				{
					float wr = m_TwiddleRe[j * step], wi = m_TwiddleIm[j * step];
					int a = block + j, b = a + half;
					float tr = lr[b] * wr - li[b] * wi;
					float ti = lr[b] * wi + li[b] * wr;
					lr[b] = lr[a] - tr; li[b] = li[a] - ti;
					lr[a] += tr; li[a] += ti;
				}
			}
		}

		for ( int j = 0; j < n; j++ )
		{
			re[start + j * stride] = lr[j];
			im[start + j * stride] = li[j];
		}
	}

	/// <summary>
	/// The swell's displacement at a point of the undisplaced grid, in metres: x and y the choppy shift, z the height.
	/// Read off the grids with a Catmull-Rom curve, smooth between their points, blended between the two sea states
	/// and between the two moments either side.
	/// </summary>
	public Vector3 Displacement( Vector2 metres, float time, float choppiness )
	{
		var (a, b, moment) = Prepare( time );

		int n = GridSize;
		float u = (metres.x - m_Shift) / TileLength * n;
		float v = (metres.y - m_Shift) / TileLength * n;
		int x0 = (int)MathF.Floor( u ), y0 = (int)MathF.Floor( v );

		Span<float> wx = stackalloc float[4];
		Span<float> wy = stackalloc float[4];
		CatmullRom( u - x0, wx );
		CatmullRom( v - y0, wy );

		// The two moments, each blended between the sea states, then between each other.
		float state = m_Same ? 0.0f : Blend;
		int last = m_Same ? 0 : 1;
		float wa0 = (1.0f - moment) * (1.0f - state), wa1 = (1.0f - moment) * state;
		float wb0 = moment * (1.0f - state), wb1 = moment * state;
		var ah0 = a.Height[0]; var ah1 = a.Height[last]; var bh0 = b.Height[0]; var bh1 = b.Height[last];
		var ax0 = a.ShiftX[0]; var ax1 = a.ShiftX[last]; var bx0 = b.ShiftX[0]; var bx1 = b.ShiftX[last];
		var ay0 = a.ShiftY[0]; var ay1 = a.ShiftY[last]; var by0 = b.ShiftY[0]; var by1 = b.ShiftY[last];

		float h = 0.0f, dx = 0.0f, dy = 0.0f;

		for ( int j = 0; j < 4; j++ )
		{
			int row = ((y0 - 1 + j) % n + n) % n * n;

			for ( int i = 0; i < 4; i++ )
			{
				int at = row + ((x0 - 1 + i) % n + n) % n;
				float weight = wx[i] * wy[j];
				h += (ah0[at] * wa0 + ah1[at] * wa1 + bh0[at] * wb0 + bh1[at] * wb1) * weight;
				dx += (ax0[at] * wa0 + ax1[at] * wa1 + bx0[at] * wb0 + bx1[at] * wb1) * weight;
				dy += (ay0[at] * wa0 + ay1[at] * wa1 + by0[at] * wb0 + by1[at] * wb1) * weight;
			}
		}

		return new Vector3( dx * choppiness, dy * choppiness, h );
	}

	private static void CatmullRom( float t, Span<float> weights )
	{
		float t2 = t * t, t3 = t2 * t;
		weights[0] = -0.5f * t3 + t2 - 0.5f * t;
		weights[1] = 1.5f * t3 - 2.5f * t2 + 1.0f;
		weights[2] = -1.5f * t3 + 2.0f * t2 + 0.5f * t;
		weights[3] = 0.5f * t3 - 0.5f * t2;
	}

	/// <summary>
	/// The height of the swell's surface above a point in the world, in metres. The waves carry the water sideways as
	/// well as up, so the water over a spot started out elsewhere: a few rounds of "where would it have come from"
	/// find it.
	/// </summary>
	public float HeightAt( Vector2 metres, float time, float choppiness, int iterations = 3 )
	{
		var start = metres;

		for ( int i = 0; i < iterations; i++ )
		{
			var d = Displacement( start, time, choppiness );
			start = metres - new Vector2( d.x, d.y );
		}

		return Displacement( start, time, choppiness ).z;
	}
}