PixelDoc.cs
namespace PixelPusher;

public enum PushTool
{
	Push,
	Flood,
	Sip,
	Block,
	Line,
	Oval
}

public enum StampRole
{
	Decor,
	Solid,
	OneWay,
	Hazard,
	Spring
}

public sealed class PixelDoc
{
	const int MaxUndo = 100;

	public string Name { get; set; } = "untitled";
	public int Width { get; private set; }
	public int Height { get; private set; }
	public string PaletteName { get; set; } = "CRT Night";
	public Color32[] Palette { get; private set; }
	public byte[] Indices { get; private set; }
	public int Version { get; private set; }
	public int Frame { get; private set; }
	public int FrameCount => _frames.Count;

	readonly List<byte[]> _frames = new();
	readonly List<byte[]> _undo = new();
	readonly List<byte[]> _redo = new();

	public PixelDoc( int width, int height, PalettePack pack = null )
	{
		pack ??= Palettes.Default;
		Width = Math.Clamp( width, 8, 256 );
		Height = Math.Clamp( height, 8, 256 );
		PaletteName = pack.Name;
		Palette = Palettes.Copy( pack );
		Indices = new byte[Width * Height];
		_frames.Add( Indices );
	}

	public int IndexAt( int x, int y )
	{
		if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
			return 0;
		return Indices[y * Width + x];
	}

	public Color32 ColorAt( int x, int y )
	{
		var i = IndexAt( x, y );
		if ( (uint)i >= (uint)Palette.Length )
			return Color32.Transparent;
		return Palette[i];
	}

	public void BeginStroke()
	{
		PushUndo();
		_redo.Clear();
	}

	bool _mirroring;

	public void SetIndex( int x, int y, byte index )
	{
		if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
			return;
		if ( (uint)index >= (uint)Palette.Length )
			index = 0;
		var i = y * Width + x;
		if ( Indices[i] != index )
		{
			Indices[i] = index;
			Version++;
		}
		if ( !_mirroring && PixelStudio.Mirror )
		{
			_mirroring = true;
			SetIndex( Width - 1 - x, y, index );
			_mirroring = false;
		}
	}

	public void FlipH()
	{
		BeginStroke();
		for ( var y = 0; y < Height; y++ )
		for ( var x = 0; x < Width / 2; x++ )
		{
			var a = y * Width + x;
			var b = y * Width + (Width - 1 - x);
			(Indices[a], Indices[b]) = (Indices[b], Indices[a]);
		}
		Version++;
	}

	public void FlipV()
	{
		BeginStroke();
		for ( var y = 0; y < Height / 2; y++ )
		for ( var x = 0; x < Width; x++ )
		{
			var a = y * Width + x;
			var b = (Height - 1 - y) * Width + x;
			(Indices[a], Indices[b]) = (Indices[b], Indices[a]);
		}
		Version++;
	}

	public void Rotate90()
	{
		if ( Width != Height )
			return;
		BeginStroke();
		var next = new byte[Indices.Length];
		for ( var y = 0; y < Height; y++ )
		for ( var x = 0; x < Width; x++ )
			next[x * Width + (Height - 1 - y)] = Indices[y * Width + x];
		Bind( next );
		Version++;
	}

	public void Replace( byte from, byte to )
	{
		BeginStroke();
		for ( var i = 0; i < Indices.Length; i++ )
		{
			if ( Indices[i] == from )
				Indices[i] = to;
		}
		Version++;
	}

	public void StampBrush( int x, int y, byte index, int size, bool circle )
	{
		var r = Math.Max( 1, size );
		var r2 = r * r;
		for ( var dy = -r + 1; dy < r; dy++ )
		for ( var dx = -r + 1; dx < r; dx++ )
		{
			if ( circle && dx * dx + dy * dy >= r2 )
				continue;
			SetIndex( x + dx, y + dy, index );
		}
	}

	public void Line( int x0, int y0, int x1, int y1, byte index, int size, bool circle )
	{
		var dx = Math.Abs( x1 - x0 );
		var dy = Math.Abs( y1 - y0 );
		var sx = x0 < x1 ? 1 : -1;
		var sy = y0 < y1 ? 1 : -1;
		var err = dx - dy;
		while ( true )
		{
			StampBrush( x0, y0, index, size, circle );
			if ( x0 == x1 && y0 == y1 )
				break;
			var e2 = 2 * err;
			if ( e2 > -dy )
			{
				err -= dy;
				x0 += sx;
			}
			if ( e2 < dx )
			{
				err += dx;
				y0 += sy;
			}
		}
	}

	public void Rect( int x0, int y0, int x1, int y1, byte index, bool filled, int size )
	{
		if ( x0 > x1 ) (x0, x1) = (x1, x0);
		if ( y0 > y1 ) (y0, y1) = (y1, y0);
		if ( filled )
		{
			for ( var y = y0; y <= y1; y++ )
			for ( var x = x0; x <= x1; x++ )
				SetIndex( x, y, index );
			return;
		}

		Line( x0, y0, x1, y0, index, size, false );
		Line( x1, y0, x1, y1, index, size, false );
		Line( x1, y1, x0, y1, index, size, false );
		Line( x0, y1, x0, y0, index, size, false );
	}

	public void Ellipse( int x0, int y0, int x1, int y1, byte index, bool filled, int size )
	{
		if ( x0 > x1 ) (x0, x1) = (x1, x0);
		if ( y0 > y1 ) (y0, y1) = (y1, y0);
		var rx = (x1 - x0) * 0.5f;
		var ry = (y1 - y0) * 0.5f;
		var cx = (x0 + x1) * 0.5f;
		var cy = (y0 + y1) * 0.5f;
		if ( rx < 0.5f ) rx = 0.5f;
		if ( ry < 0.5f ) ry = 0.5f;

		if ( filled )
		{
			for ( var y = y0; y <= y1; y++ )
			for ( var x = x0; x <= x1; x++ )
			{
				var nx = (x - cx) / rx;
				var ny = (y - cy) / ry;
				if ( nx * nx + ny * ny <= 1.05f )
					SetIndex( x, y, index );
			}
			return;
		}

		for ( var y = y0; y <= y1; y++ )
		for ( var x = x0; x <= x1; x++ )
		{
			var nx = (x - cx) / rx;
			var ny = (y - cy) / ry;
			var d = nx * nx + ny * ny;
			if ( d <= 1.08f && d >= 0.72f )
				StampBrush( x, y, index, size, false );
		}
	}

	public void Flood( int x, int y, byte index )
	{
		if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
			return;
		var target = Indices[y * Width + x];
		if ( target == index )
			return;

		var stack = new Stack<(int x, int y)>();
		stack.Push( (x, y) );
		var seen = new bool[Width * Height];
		while ( stack.Count > 0 )
		{
			var (cx, cy) = stack.Pop();
			if ( (uint)cx >= (uint)Width || (uint)cy >= (uint)Height )
				continue;
			var i = cy * Width + cx;
			if ( seen[i] || Indices[i] != target )
				continue;
			seen[i] = true;
			Indices[i] = index;
			stack.Push( (cx + 1, cy) );
			stack.Push( (cx - 1, cy) );
			stack.Push( (cx, cy + 1) );
			stack.Push( (cx, cy - 1) );
		}
		Version++;
	}

	public void Clear()
	{
		BeginStroke();
		Array.Clear( Indices );
		Version++;
	}

	public void ApplyPalette( PalettePack pack )
	{
		if ( pack is null )
			return;
		PaletteName = pack.Name;
		Palette = Palettes.Copy( pack );
		Version++;
	}

	public bool CanUndo => _undo.Count > 0;
	public bool CanRedo => _redo.Count > 0;

	public void Undo()
	{
		if ( _undo.Count == 0 )
			return;
		_redo.Add( CopyBytes( Indices ) );
		Bind( _undo[^1] );
		_undo.RemoveAt( _undo.Count - 1 );
		Version++;
	}

	public void Redo()
	{
		if ( _redo.Count == 0 )
			return;
		_undo.Add( CopyBytes( Indices ) );
		Bind( _redo[^1] );
		_redo.RemoveAt( _redo.Count - 1 );
		Version++;
	}

	public Color32 OnionAt( int x, int y )
	{
		if ( FrameCount < 2 )
			return Color32.Transparent;
		var prev = Frame == 0 ? FrameCount - 1 : Frame - 1;
		var buf = _frames[prev];
		if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
			return Color32.Transparent;
		var i = buf[y * Width + x];
		if ( i == 0 || (uint)i >= (uint)Palette.Length )
			return Color32.Transparent;
		Color c = Palette[i];
		return new Color32(
			(byte)MathF.Round( c.r * 255f ),
			(byte)MathF.Round( c.g * 255f ),
			(byte)MathF.Round( c.b * 255f ),
			90 );
	}

	public void SetFrame( int index )
	{
		if ( _frames.Count == 0 )
			return;
		Frame = Math.Clamp( index, 0, _frames.Count - 1 );
		Indices = _frames[Frame];
		Version++;
	}

	public void AddFrame()
	{
		if ( _frames.Count >= 16 )
			return;
		BeginStroke();
		_frames.Insert( Frame + 1, CopyBytes( Indices ) );
		SetFrame( Frame + 1 );
	}

	public void DeleteFrame()
	{
		if ( _frames.Count <= 1 )
		{
			Clear();
			return;
		}
		BeginStroke();
		_frames.RemoveAt( Frame );
		SetFrame( Math.Min( Frame, _frames.Count - 1 ) );
	}

	void Bind( byte[] buf )
	{
		Indices = buf;
		if ( Frame >= 0 && Frame < _frames.Count )
			_frames[Frame] = buf;
	}

	public PixelSave ToSave()
	{
		var pal = new string[Palette.Length];
		for ( var i = 0; i < Palette.Length; i++ )
			pal[i] = Palettes.Hex( Palette[i] ) == "transparent" ? "#00000000" : Palettes.Hex( Palette[i] );

		return new PixelSave
		{
			Name = Name,
			Width = Width,
			Height = Height,
			PaletteName = PaletteName,
			Palette = pal,
			Indices = CopyBytes( Indices ),
			Frames = _frames.Count > 1 ? _frames.Select( CopyBytes ).ToList() : null,
			Frame = Frame
		};
	}

	public static PixelDoc FromSave( PixelSave save )
	{
		if ( save is null )
			return new PixelDoc( 16, 16 );
		var pack = Palettes.Named( save.PaletteName );
		var doc = new PixelDoc( save.Width, save.Height, pack )
		{
			Name = save.Name ?? "untitled",
			PaletteName = save.PaletteName ?? pack.Name
		};
		if ( save.Palette is { Length: > 0 } )
		{
			doc.Palette = new Color32[save.Palette.Length];
			for ( var i = 0; i < save.Palette.Length; i++ )
				doc.Palette[i] = Palettes.Parse( save.Palette[i] );
		}
		if ( save.Frames is { Count: > 1 } )
		{
			doc._frames.Clear();
			foreach ( var frame in save.Frames )
			{
				var buf = new byte[doc.Width * doc.Height];
				if ( frame is { Length: > 0 } )
					Array.Copy( frame, buf, Math.Min( buf.Length, frame.Length ) );
				doc._frames.Add( buf );
			}
			doc.SetFrame( save.Frame );
		}
		else if ( save.Indices is { Length: > 0 } )
		{
			var n = Math.Min( doc.Indices.Length, save.Indices.Length );
			Array.Copy( save.Indices, doc.Indices, n );
		}
		return doc;
	}

	void PushUndo()
	{
		_undo.Add( CopyBytes( Indices ) );
		if ( _undo.Count > MaxUndo )
			_undo.RemoveAt( 0 );
	}

	public static byte[] CopyBytes( byte[] src )
	{
		if ( src is null )
			return new byte[0];
		var dst = new byte[src.Length];
		Array.Copy( src, dst, src.Length );
		return dst;
	}
}

public sealed class PixelSave
{
	public string Name { get; set; }
	public int Width { get; set; }
	public int Height { get; set; }
	public string PaletteName { get; set; }
	public string[] Palette { get; set; }
	public byte[] Indices { get; set; }
	public List<byte[]> Frames { get; set; }
	public int Frame { get; set; }
}