Flash/Core/Library.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace Flash;

/// <summary>2D affine matrix in Flash order: x' = A*x + C*y + Tx, y' = B*x + D*y + Ty.</summary>
public struct Matrix
{
	public double A, B, C, D, Tx, Ty;
	public static readonly Matrix Identity = new() { A = 1, D = 1 };

	public Matrix( double a, double b, double c, double d, double tx, double ty ) { A = a; B = b; C = c; D = d; Tx = tx; Ty = ty; }

	/// <summary>this * other : apply other first, then this.</summary>
	public Matrix Mul( Matrix o ) => new(
		A * o.A + C * o.B, B * o.A + D * o.B,
		A * o.C + C * o.D, B * o.C + D * o.D,
		A * o.Tx + C * o.Ty + Tx, B * o.Tx + D * o.Ty + Ty );

	public (double x, double y) Apply( double x, double y ) => (A * x + C * y + Tx, B * x + D * y + Ty);

	public Matrix Inverse()
	{
		double det = A * D - B * C;
		if ( Math.Abs( det ) < 1e-12 ) return Identity;
		double ia = D / det, ib = -B / det, ic = -C / det, id = A / det;
		return new Matrix( ia, ib, ic, id, -(ia * Tx + ic * Ty), -(ib * Tx + id * Ty) );
	}

	public static Matrix FromArray( List<object> a )
	{
		if ( a is null ) return Identity;
		return new Matrix( Json.D( a[0] ), Json.D( a[1] ), Json.D( a[2] ), Json.D( a[3] ), Json.D( a[4] ), Json.D( a[5] ) );
	}

	public bool IsIdentity => A == 1 && B == 0 && C == 0 && D == 1 && Tx == 0 && Ty == 0;
}

/// <summary>Color transform: out = in * Mult + Add (add terms in 0..255).</summary>
public struct ColorTransform
{
	public double Rm, Gm, Bm, Am, Ra, Ga, Ba, Aa;
	public static readonly ColorTransform Identity = new() { Rm = 1, Gm = 1, Bm = 1, Am = 1 };

	public ColorTransform Mul( ColorTransform inner ) => new()
	{
		// apply inner first, then this
		Rm = Rm * inner.Rm, Gm = Gm * inner.Gm, Bm = Bm * inner.Bm, Am = Am * inner.Am,
		Ra = Rm * inner.Ra + Ra, Ga = Gm * inner.Ga + Ga, Ba = Bm * inner.Ba + Ba, Aa = Am * inner.Aa + Aa,
	};

	public static ColorTransform FromArray( List<object> a )
	{
		if ( a is null ) return Identity;
		return new ColorTransform
		{
			Rm = Json.D( a[0] ), Gm = Json.D( a[1] ), Bm = Json.D( a[2] ), Am = Json.D( a[3] ),
			Ra = Json.D( a[4] ), Ga = Json.D( a[5] ), Ba = Json.D( a[6] ), Aa = Json.D( a[7] ),
		};
	}

	public bool IsIdentity => Rm == 1 && Gm == 1 && Bm == 1 && Am == 1 && Ra == 0 && Ga == 0 && Ba == 0 && Aa == 0;
}

public sealed class Filter
{
	public string Kind; // glow, shadow, blur, cmatrix, bevel
	public double[] Color; // rgba 0..255
	public double BlurX, BlurY, Strength, Angle, Distance;
	public bool Inner, Knockout;
	public double[] ColorMatrix;

	public static List<Filter> FromJson( List<object> list )
	{
		if ( list is null ) return null;
		var r = new List<Filter>();
		foreach ( List<object> f in list )
		{
			var k = (string)f[0];
			var x = new Filter { Kind = k };
			switch ( k )
			{
				case "glow":
					x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[2] ); x.BlurY = Json.D( f[3] ); x.Strength = Json.D( f[4] );
					x.Inner = Json.B( f[5] ); x.Knockout = Json.B( f[6] );
					break;
				case "shadow":
					x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[2] ); x.BlurY = Json.D( f[3] ); x.Angle = Json.D( f[4] );
					x.Distance = Json.D( f[5] ); x.Strength = Json.D( f[6] ); x.Inner = Json.B( f[7] ); x.Knockout = Json.B( f[8] );
					break;
				case "blur": x.BlurX = Json.D( f[1] ); x.BlurY = Json.D( f[2] ); break;
				case "cmatrix": x.ColorMatrix = Json.Doubles( f[1] ); break;
				case "bevel":
					x.Color = Json.Doubles( f[1] ); x.BlurX = Json.D( f[3] ); x.BlurY = Json.D( f[4] ); x.Angle = Json.D( f[5] );
					x.Distance = Json.D( f[6] ); x.Strength = Json.D( f[7] ); x.Inner = Json.B( f[8] );
					break;
			}
			r.Add( x );
		}
		return r;
	}
}

// ============================================================================ character definitions

public abstract class CharDef
{
	public int Id;
	public string ExportName;
}

public sealed class ShapeDef : CharDef
{
	public double X0, Y0, X1, Y1; // bounds in px
	public string Image; // path relative to the art root
	public int Zoom; // image px per Flash px
	public HitMask Mask;
}

public sealed class MorphDef : CharDef
{
	public double[] B0, B1;
	public string ImageStart, ImageEnd;
	public int Zoom;
}

public sealed class StaticTextDef : CharDef
{
	public double X0, Y0, X1, Y1;
	public string Image;
	public int Zoom;
}

public sealed class EditTextDef : CharDef
{
	public double X0, Y0, X1, Y1;
	public int FontId;
	public double Size;
	public double[] Color = { 0, 0, 0, 255 };
	public int Align; // 0 left 1 right 2 center 3 justify
	public double LeftMargin, RightMargin, Indent, Leading;
	public int MaxLength;
	public string InitialText, Variable;
	public bool Html, Multiline, WordWrap, Password, Border, NoSelect, AutoSize, Input, Outlines;
}

public sealed class FrameOp
{
	public char Kind; // P place, M modify, R remove, A action, S sound
	public int Depth, CharId;
	public bool Move;
	public Matrix? M;
	public ColorTransform? Cx;
	public string Name;
	public int? Ratio;
	public int ClipDepth;
	public List<Filter> Filters;
	public bool HasFilters;
	public int BlendMode;
	public List<ClipActionDef> ClipActions;
	public byte[] Code;
	public int SoundId;
	public bool SoundStop;
	public bool SoundNoMultiple;
	public int SoundLoops;
}

public sealed class ClipActionDef
{
	public List<string> Events;
	public int Key;
	public byte[] Code;
}

public sealed class SpriteDef : CharDef
{
	public int FrameCount;
	public List<FrameOp>[] Frames;
	public Dictionary<string, int> Labels = new();
}

public sealed class ButtonRecordDef
{
	public string States; // u o d h
	public int CharId, Depth;
	public Matrix M;
	public ColorTransform? Cx;
	public List<Filter> Filters;
}

public sealed class ButtonActionDef
{
	public List<string> On;
	public int Key;
	public byte[] Code;
}

public sealed class ButtonDef : CharDef
{
	public List<ButtonRecordDef> Records = new();
	public List<ButtonActionDef> Actions = new();
	public bool TrackAsMenu;
}

public sealed class SoundDef : CharDef
{
	public string File;
	public int Samples, Rate;
	public bool Stereo;
	public double Seconds => Rate switch { 0 => Samples / 5512.5, 1 => Samples / 11025.0, 2 => Samples / 22050.0, _ => Samples / 44100.0 };
}

public sealed class FontDef
{
	public int Id;
	public string Name, Family;
	public bool Bold, Italic;
	public double Ascent, Descent, Leading;
}

public sealed class InitAction
{
	public int SpriteId;
	public byte[] Code;
}

/// <summary>1 bit per Flash pixel alpha mask for hit testing a shape.</summary>
public sealed class HitMask
{
	public int X, Y, W, H; // in Flash px relative to the shape origin
	public byte[] Bits;

	public bool Test( double x, double y )
	{
		int ix = (int)Math.Floor( x ) - X, iy = (int)Math.Floor( y ) - Y;
		if ( ix < 0 || iy < 0 || ix >= W || iy >= H ) return false;
		int bit = iy * W + ix;
		return (Bits[bit >> 3] & (1 << (bit & 7))) != 0;
	}
}

/// <summary>Everything decoded from the SWF (by tools/extract.py).</summary>
public sealed class Library
{
	public double StageWidth, StageHeight, FrameRate;
	public int Version;
	public double[] Background;
	public readonly Dictionary<int, CharDef> Chars = new();
	public readonly Dictionary<string, int> Exports = new();
	public readonly Dictionary<int, FontDef> Fonts = new();
	public readonly List<InitAction> Init = new();
	public SpriteDef Main;
	public Dictionary<string, int> ShapeZoom = new();

	public CharDef Get( int id ) => Chars.TryGetValue( id, out var c ) ? c : null;

	public CharDef GetExport( string name )
	{
		if ( name != null && Exports.TryGetValue( name, out var id ) ) return Get( id );
		return null;
	}

	/// <param name="json">ss2.json</param>
	/// <param name="zoomsJson">zooms.json (shape/text id → image zoom)</param>
	/// <param name="hitmasks">hitmasks.bin, or null</param>
	public static Library Load( string json, string zoomsJson, byte[] hitmasks )
	{
		var lib = new Library();
		var root = (Dictionary<string, object>)Json.Parse( json );
		var st = (Dictionary<string, object>)root["stage"];
		lib.StageWidth = Json.D( st["w"] ); lib.StageHeight = Json.D( st["h"] ); lib.FrameRate = Json.D( st["fps"] );
		lib.Version = (int)Json.D( st["version"] );
		lib.Background = Json.Doubles( st["bg"] );
		var zooms = zoomsJson != null ? (Dictionary<string, object>)Json.Parse( zoomsJson ) : new Dictionary<string, object>();

		foreach ( var kv in (Dictionary<string, object>)root["exports"] ) lib.Exports[kv.Key] = (int)Json.D( kv.Value );
		foreach ( var kv in (Dictionary<string, object>)root["fonts"] )
		{
			var f = (Dictionary<string, object>)kv.Value;
			var fd = new FontDef
			{
				Id = int.Parse( kv.Key, CultureInfo.InvariantCulture ), Name = (string)f["name"], Bold = Json.B( f["bold"] ), Italic = Json.B( f["italic"] ),
				Ascent = Json.D( f["ascent"] ), Descent = Json.D( f["descent"] ), Leading = Json.D( f["leading"] ),
			};
			fd.Family = "SS2 F" + fd.Id;
			lib.Fonts[fd.Id] = fd;
		}
		foreach ( Dictionary<string, object> ia in (List<object>)root["init"] )
			lib.Init.Add( new InitAction { SpriteId = (int)Json.D( ia["sprite"] ), Code = Base64.Decode( (string)ia["b"] ) } );

		foreach ( var kv in (Dictionary<string, object>)root["chars"] )
		{
			int id = int.Parse( kv.Key, CultureInfo.InvariantCulture );
			var c = (Dictionary<string, object>)kv.Value;
			var t = (string)c["t"];
			int zoom = zooms.TryGetValue( kv.Key, out var zv ) ? (int)Json.D( zv ) : 2;
			CharDef def = null;
			switch ( t )
			{
				case "s":
					{
						var b = Json.Doubles( c["b"] );
						def = new ShapeDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3], Image = $"s/{id}.png", Zoom = zoom };
						break;
					}
				case "x":
					{
						var b = Json.Doubles( c["b"] );
						def = new StaticTextDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3], Image = $"x/{id}.png", Zoom = zoom };
						break;
					}
				case "m":
					def = new MorphDef { B0 = Json.Doubles( c["b0"] ), B1 = Json.Doubles( c["b1"] ), ImageStart = $"m/{id}.start.png", ImageEnd = $"m/{id}.end.png", Zoom = 4 };
					break;
				case "e":
					{
						var b = Json.Doubles( c["b"] );
						var e = new EditTextDef { X0 = b[0], Y0 = b[1], X1 = b[2], Y1 = b[3] };
						if ( c.TryGetValue( "font", out var fo ) ) e.FontId = (int)Json.D( fo );
						if ( c.TryGetValue( "size", out var so ) ) e.Size = Json.D( so );
						if ( c.TryGetValue( "color", out var co ) ) e.Color = Json.Doubles( co );
						if ( c.TryGetValue( "align", out var al ) ) e.Align = (int)Json.D( al );
						if ( c.TryGetValue( "lm", out var lm ) ) e.LeftMargin = Json.D( lm );
						if ( c.TryGetValue( "rm", out var rm ) ) e.RightMargin = Json.D( rm );
						if ( c.TryGetValue( "indent", out var ind ) ) e.Indent = Json.D( ind );
						if ( c.TryGetValue( "leading", out var ld ) ) e.Leading = Json.D( ld );
						if ( c.TryGetValue( "max", out var mx ) ) e.MaxLength = (int)Json.D( mx );
						if ( c.TryGetValue( "text", out var tx ) ) e.InitialText = (string)tx;
						if ( c.TryGetValue( "var", out var va ) ) e.Variable = (string)va;
						e.Html = c.ContainsKey( "html" ); e.Multiline = c.ContainsKey( "multi" ); e.WordWrap = c.ContainsKey( "wrap" );
						e.Password = c.ContainsKey( "pass" ); e.Border = c.ContainsKey( "border" ); e.NoSelect = c.ContainsKey( "nosel" );
						e.AutoSize = c.ContainsKey( "auto" ); e.Input = c.ContainsKey( "input" ); e.Outlines = c.ContainsKey( "outl" );
						def = e;
						break;
					}
				case "p":
					def = ParseSprite( c );
					break;
				case "b":
					{
						var bd = new ButtonDef { TrackAsMenu = Json.D( c["menu"] ) != 0 };
						foreach ( Dictionary<string, object> r in (List<object>)c["recs"] )
						{
							bd.Records.Add( new ButtonRecordDef
							{
								States = (string)r["st"], CharId = (int)Json.D( r["c"] ), Depth = (int)Json.D( r["d"] ),
								M = Matrix.FromArray( r["m"] as List<object> ),
								Cx = r.TryGetValue( "cx", out var cx ) ? ColorTransform.FromArray( (List<object>)cx ) : null,
								Filters = r.TryGetValue( "f", out var ff ) ? Filter.FromJson( ff as List<object> ) : null,
							} );
						}
						foreach ( Dictionary<string, object> a in (List<object>)c["acts"] )
						{
							var on = new List<string>();
							foreach ( var o in (List<object>)a["on"] ) on.Add( (string)o );
							bd.Actions.Add( new ButtonActionDef { On = on, Key = (int)Json.D( a["key"] ), Code = Base64.Decode( (string)a["b"] ) } );
						}
						bd.Records.Sort( ( x, y ) => x.Depth.CompareTo( y.Depth ) );
						def = bd;
						break;
					}
				case "a":
					def = new SoundDef { File = $"a/{id}", Samples = (int)Json.D( c["samples"] ), Rate = (int)Json.D( c["rate"] ), Stereo = Json.B( c["stereo"] ) };
					break;
				default:
					continue;
			}
			def.Id = id;
			lib.Chars[id] = def;
		}
		foreach ( var kv in lib.Exports )
			if ( lib.Chars.TryGetValue( kv.Value, out var d ) && d.ExportName == null ) d.ExportName = kv.Key;
		lib.Main = lib.Chars[0] as SpriteDef;

		if ( hitmasks != null ) LoadHitMasks( lib, hitmasks );
		return lib;
	}

	static SpriteDef ParseSprite( Dictionary<string, object> c )
	{
		var sd = new SpriteDef { FrameCount = Math.Max( 1, (int)Json.D( c["fc"] ) ) };
		foreach ( var kv in (Dictionary<string, object>)c["labels"] ) sd.Labels[kv.Key] = (int)Json.D( kv.Value );
		var frames = (List<object>)c["frames"];
		sd.Frames = new List<FrameOp>[Math.Max( sd.FrameCount, frames.Count )];
		for ( int f = 0; f < sd.Frames.Length; f++ )
		{
			var list = new List<FrameOp>();
			sd.Frames[f] = list;
			if ( f >= frames.Count ) continue;
			foreach ( List<object> op in (List<object>)frames[f] )
			{
				var k = ((string)op[0])[0];
				var fo = new FrameOp { Kind = k };
				switch ( k )
				{
					case 'P':
						fo.Depth = (int)Json.D( op[1] ); fo.CharId = (int)Json.D( op[2] );
						ReadPlace( fo, (Dictionary<string, object>)op[3] );
						fo.Move = Json.D( op[4] ) != 0;
						break;
					case 'M':
						fo.Depth = (int)Json.D( op[1] );
						ReadPlace( fo, (Dictionary<string, object>)op[2] );
						break;
					case 'R': fo.Depth = (int)Json.D( op[1] ); break;
					case 'A': fo.Code = Base64.Decode( (string)op[1] ); break;
					case 'S':
						fo.SoundId = (int)Json.D( op[1] );
						var so = (Dictionary<string, object>)op[2];
						fo.SoundStop = so.ContainsKey( "stop" );
						fo.SoundNoMultiple = so.ContainsKey( "nomulti" );
						fo.SoundLoops = so.TryGetValue( "loops", out var lo ) ? (int)Json.D( lo ) : 1;
						break;
				}
				list.Add( fo );
			}
		}
		if ( sd.Frames.Length > sd.FrameCount ) sd.FrameCount = sd.Frames.Length;
		return sd;
	}

	static void ReadPlace( FrameOp fo, Dictionary<string, object> o )
	{
		if ( o.TryGetValue( "m", out var m ) ) fo.M = Matrix.FromArray( (List<object>)m );
		if ( o.TryGetValue( "cx", out var cx ) ) fo.Cx = ColorTransform.FromArray( (List<object>)cx );
		if ( o.TryGetValue( "n", out var n ) ) fo.Name = (string)n;
		if ( o.TryGetValue( "r", out var r ) ) fo.Ratio = (int)Json.D( r );
		if ( o.TryGetValue( "cd", out var cd ) ) fo.ClipDepth = (int)Json.D( cd );
		if ( o.TryGetValue( "f", out var f ) ) { fo.Filters = Filter.FromJson( (List<object>)f ); fo.HasFilters = true; }
		if ( o.TryGetValue( "bm", out var bm ) ) fo.BlendMode = (int)Json.D( bm );
		if ( o.TryGetValue( "ca", out var ca ) )
		{
			fo.ClipActions = new List<ClipActionDef>();
			foreach ( Dictionary<string, object> a in (List<object>)ca )
			{
				var evs = new List<string>();
				foreach ( var e in (List<object>)a["ev"] ) evs.Add( (string)e );
				fo.ClipActions.Add( new ClipActionDef { Events = evs, Key = (int)Json.D( a["key"] ), Code = Base64.Decode( (string)a["b"] ) } );
			}
		}
	}

	/// <summary>hitmasks.bin: repeated [int id, int x, int y, int w, int h, bytes...]</summary>
	static void LoadHitMasks( Library lib, byte[] data )
	{
		int i = 0;
		int Rd() { int v = data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24); i += 4; return v; }
		while ( i + 20 <= data.Length )
		{
			int id = Rd(), x = Rd(), y = Rd(), w = Rd(), h = Rd();
			int n = (w * h + 7) / 8;
			var bits = new byte[n];
			Array.Copy( data, i, bits, 0, n );
			i += n;
			if ( lib.Chars.TryGetValue( id, out var d ) && d is ShapeDef s ) s.Mask = new HitMask { X = x, Y = y, W = w, H = h, Bits = bits };
		}
	}
}

// ============================================================================ tiny JSON + base64

public static class Json
{
	public static double D( object o ) => o switch { double d => d, long l => l, int i => i, bool b => b ? 1 : 0, _ => 0 };
	public static bool B( object o ) => o switch { bool b => b, double d => d != 0, _ => false };

	public static double[] Doubles( object o )
	{
		if ( o is not List<object> l ) return null;
		var r = new double[l.Count];
		for ( int i = 0; i < l.Count; i++ ) r[i] = D( l[i] );
		return r;
	}

	public static object Parse( string s )
	{
		int i = 0;
		var v = Read( s, ref i );
		return v;
	}

	static void Ws( string s, ref int i ) { while ( i < s.Length && (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t') ) i++; }

	static object Read( string s, ref int i )
	{
		Ws( s, ref i );
		char c = s[i];
		if ( c == '{' )
		{
			var d = new Dictionary<string, object>();
			i++; Ws( s, ref i );
			if ( s[i] == '}' ) { i++; return d; }
			while ( true )
			{
				Ws( s, ref i );
				var k = ReadString( s, ref i );
				Ws( s, ref i ); i++; // :
				d[k] = Read( s, ref i );
				Ws( s, ref i );
				if ( s[i] == ',' ) { i++; continue; }
				i++; return d;
			}
		}
		if ( c == '[' )
		{
			var l = new List<object>();
			i++; Ws( s, ref i );
			if ( s[i] == ']' ) { i++; return l; }
			while ( true )
			{
				l.Add( Read( s, ref i ) );
				Ws( s, ref i );
				if ( s[i] == ',' ) { i++; continue; }
				i++; return l;
			}
		}
		if ( c == '"' ) return ReadString( s, ref i );
		if ( c == 't' ) { i += 4; return true; }
		if ( c == 'f' ) { i += 5; return false; }
		if ( c == 'n' ) { i += 4; return null; }
		int st = i;
		while ( i < s.Length && "+-0123456789.eE".IndexOf( s[i] ) >= 0 ) i++;
		return double.Parse( s.Substring( st, i - st ), NumberStyles.Float, CultureInfo.InvariantCulture );
	}

	static string ReadString( string s, ref int i )
	{
		i++; // "
		var sb = new StringBuilder();
		while ( s[i] != '"' )
		{
			char c = s[i++];
			if ( c != '\\' ) { sb.Append( c ); continue; }
			char e = s[i++];
			switch ( e )
			{
				case 'n': sb.Append( '\n' ); break;
				case 'r': sb.Append( '\r' ); break;
				case 't': sb.Append( '\t' ); break;
				case 'b': sb.Append( '\b' ); break;
				case 'f': sb.Append( '\f' ); break;
				case 'u': sb.Append( (char)int.Parse( s.Substring( i, 4 ), NumberStyles.HexNumber, CultureInfo.InvariantCulture ) ); i += 4; break;
				default: sb.Append( e ); break;
			}
		}
		i++;
		return sb.ToString();
	}
}

public static class Base64
{
	static readonly int[] map = Build();

	static int[] Build()
	{
		var m = new int[128];
		for ( int i = 0; i < 128; i++ ) m[i] = -1;
		const string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
		for ( int i = 0; i < a.Length; i++ ) m[a[i]] = i;
		return m;
	}

	public static byte[] Decode( string s )
	{
		var outp = new List<byte>( s.Length * 3 / 4 );
		int buf = 0, bits = 0;
		foreach ( var ch in s )
		{
			if ( ch >= 128 ) continue;
			int v = map[ch];
			if ( v < 0 ) continue;
			buf = (buf << 6) | v; bits += 6;
			if ( bits >= 8 ) { bits -= 8; outp.Add( (byte)((buf >> bits) & 0xFF) ); }
		}
		return outp.ToArray();
	}
}