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

namespace Flash;

/// <summary>The ActionScript 2 standard library (the parts SS2 uses, plus the obvious neighbours).</summary>
public static class Builtins
{
	static Value Arg( Value[] a, int i ) => i < a.Length ? a[i] : Value.Undefined;

	public static void Install( Avm1 vm )
	{
		var objectProto = new AsObject( null );
		vm.ObjectProto = objectProto;
		var functionProto = new AsObject( objectProto );
		vm.FunctionProto = functionProto;
		var global = new AsObject( objectProto );
		vm.Global = global;

		InstallObject( vm );
		InstallFunction( vm );
		InstallArray( vm );
		InstallString( vm );
		InstallNumberBoolean( vm );
		InstallMath( vm );
		InstallDate( vm );
		InstallGlobals( vm );
		InstallMovieClip( vm );
		InstallTextFieldButton( vm );
		InstallSound( vm );
		InstallColor( vm );
		InstallKeyMouseStage( vm );
		InstallSharedObject( vm );
		InstallBroadcaster( vm );
	}

	static NativeFunction Ctor( Avm1 vm, string name, AsObject proto, NativeCall call, NativeCall construct = null )
	{
		var f = vm.Native( name, call );
		f.Constructor = construct;
		f.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum | PropFlags.DontDelete );
		proto.DefineHidden( "constructor", Value.From( f ) );
		vm.Global.DefineHidden( name, Value.From( f ) );
		return f;
	}

	// ========================================================================= Object

	static void InstallObject( Avm1 vm )
	{
		var p = vm.ObjectProto;
		vm.ObjectCtor = Ctor( vm, "Object", p, ( vm, t, a ) =>
		{
			var v = Arg( a, 0 );
			if ( v.IsObject ) return v;
			if ( !v.IsNullish ) return Value.From( vm.ToObject( v ) );
			return t.IsObject && !(t.Obj is DisplayObject) ? t : Value.From( vm.NewObject() );
		}, ( vm, t, a ) =>
		{
			var v = Arg( a, 0 );
			if ( v.IsObject ) return v;
			if ( !v.IsNullish ) return Value.From( vm.ToObject( v ) );
			return Value.From( vm.NewObject() );
		} );
		vm.Method( p, "hasOwnProperty", ( vm, t, a ) => t.IsObject && t.Obj.HasOwnProperty( Arg( a, 0 ).ToStr( vm ) ) );
		vm.Method( p, "toString", ( vm, t, a ) => t.IsObject && t.Obj is AsFunction ? "[type Function]" : "[object Object]" );
		vm.Method( p, "toLocaleString", ( vm, t, a ) => "[object Object]" );
		vm.Method( p, "valueOf", ( vm, t, a ) => t );
		vm.Method( p, "isPropertyEnumerable", ( vm, t, a ) =>
		{
			if ( !t.IsObject ) return false;
			var pr = t.Obj.GetOwnProperty( Arg( a, 0 ).ToStr( vm ) );
			return pr != null && (pr.Flags & PropFlags.DontEnum) == 0;
		} );
		vm.Method( p, "isPrototypeOf", ( vm, t, a ) =>
		{
			var o = Arg( a, 0 ).AsObj;
			if ( !t.IsObject || o == null ) return false;
			for ( var x = o.Proto; x != null; x = x.Proto ) if ( ReferenceEquals( x, t.Obj ) ) return true;
			return false;
		} );
		vm.Method( p, "addProperty", ( vm, t, a ) =>
		{
			if ( !t.IsObject ) return false;
			var name = Arg( a, 0 ).ToStr( vm );
			var g = Arg( a, 1 ).AsObj as AsFunction;
			var s = Arg( a, 2 ).AsObj as AsFunction;
			if ( name.Length == 0 || g == null ) return false;
			t.Obj.AddVirtual( name, g, s );
			return true;
		} );
		vm.Method( p, "watch", ( vm, t, a ) => false );
		vm.Method( p, "unwatch", ( vm, t, a ) => false );
		var ctor = vm.ObjectCtor;
		vm.Method( ctor, "registerClass", ( vm, t, a ) =>
		{
			var name = Arg( a, 0 ).ToStr( vm );
			if ( Arg( a, 1 ).AsObj is AsFunction f ) vm.Player.RegisteredClasses[name] = f;
			else vm.Player.RegisteredClasses.Remove( name );
			return true;
		} );
	}

	// ========================================================================= Function

	static void InstallFunction( Avm1 vm )
	{
		var p = vm.FunctionProto;
		vm.FunctionCtor = Ctor( vm, "Function", p, ( vm, t, a ) => Arg( a, 0 ), ( vm, t, a ) => Arg( a, 0 ).IsObject ? Arg( a, 0 ) : Value.From( vm.NewObject() ) );
		vm.Method( p, "call", ( vm, t, a ) =>
		{
			if ( t.AsObj is not AsFunction f ) return Value.Undefined;
			var thisArg = Arg( a, 0 );
			var rest = a.Length > 1 ? a[1..] : Array.Empty<Value>();
			return f.Call( vm, thisArg.IsNullish ? Value.Undefined : thisArg, rest );
		} );
		vm.Method( p, "apply", ( vm, t, a ) =>
		{
			if ( t.AsObj is not AsFunction f ) return Value.Undefined;
			var thisArg = Arg( a, 0 );
			var args = Array.Empty<Value>();
			if ( Arg( a, 1 ).AsObj is AsArray arr ) args = arr.Items.ToArray();
			else if ( Arg( a, 1 ).IsObject )
			{
				int n = Arg( a, 1 ).Obj.Get( "length", vm ).ToInt32( vm );
				args = new Value[Math.Max( 0, n )];
				for ( int i = 0; i < args.Length; i++ ) args[i] = Arg( a, 1 ).Obj.Get( i.ToString(), vm );
			}
			return f.Call( vm, thisArg.IsNullish ? Value.Undefined : thisArg, args );
		} );
		vm.Method( p, "toString", ( vm, t, a ) => "[type Function]" );
	}

	// ========================================================================= Array

	static void InstallArray( Avm1 vm )
	{
		var p = new AsArray( vm.ObjectProto );
		vm.ArrayProto = p;
		NativeCall make = ( vm, t, a ) =>
		{
			var arr = vm.NewArray();
			if ( a.Length == 1 && a[0].IsNumber )
			{
				int n = Value.ToInt32( a[0].Num );
				for ( int i = 0; i < n; i++ ) arr.Items.Add( Value.Undefined );
			}
			else arr.Items.AddRange( a );
			return arr;
		};
		vm.ArrayCtor = Ctor( vm, "Array", p, make, make );
		var ctor = vm.ArrayCtor;
		ctor.SetOwn( "CASEINSENSITIVE", 1 ); ctor.SetOwn( "DESCENDING", 2 ); ctor.SetOwn( "UNIQUESORT", 4 );
		ctor.SetOwn( "RETURNINDEXEDARRAY", 8 ); ctor.SetOwn( "NUMERIC", 16 );

		AsArray A( Value t ) => t.AsObj as AsArray;

		vm.Method( p, "push", ( vm, t, a ) => { var arr = A( t ); if ( arr == null ) return Value.Undefined; arr.Items.AddRange( a ); return arr.Items.Count; } );
		vm.Method( p, "pop", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null || arr.Items.Count == 0 ) return Value.Undefined;
			var v = arr.Items[^1]; arr.Items.RemoveAt( arr.Items.Count - 1 ); return v;
		} );
		vm.Method( p, "shift", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null || arr.Items.Count == 0 ) return Value.Undefined;
			var v = arr.Items[0]; arr.Items.RemoveAt( 0 ); return v;
		} );
		vm.Method( p, "unshift", ( vm, t, a ) => { var arr = A( t ); if ( arr == null ) return Value.Undefined; arr.Items.InsertRange( 0, a ); return arr.Items.Count; } );
		vm.Method( p, "reverse", ( vm, t, a ) => { var arr = A( t ); arr?.Items.Reverse(); return t; } );
		vm.Method( p, "join", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null ) return "";
			var sep = Arg( a, 0 ).IsUndefined ? "," : Arg( a, 0 ).ToStr( vm );
			return Join( vm, arr, sep );
		} );
		vm.Method( p, "toString", ( vm, t, a ) => { var arr = A( t ); return arr == null ? "" : Join( vm, arr, "," ); } );
		vm.Method( p, "concat", ( vm, t, a ) =>
		{
			var arr = A( t );
			var r = vm.NewArray( arr?.Items );
			foreach ( var x in a ) { if ( x.AsObj is AsArray xa ) r.Items.AddRange( xa.Items ); else r.Items.Add( x ); }
			return r;
		} );
		vm.Method( p, "slice", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null ) return vm.NewArray();
			int n = arr.Items.Count;
			int s = Clamp( Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToInt32( vm ), n );
			int e = Clamp( Arg( a, 1 ).IsUndefined ? n : Arg( a, 1 ).ToInt32( vm ), n );
			var r = vm.NewArray();
			for ( int i = s; i < e; i++ ) r.Items.Add( arr.Items[i] );
			return r;
		} );
		vm.Method( p, "splice", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null || a.Length == 0 ) return Value.Undefined;
			int n = arr.Items.Count;
			int s = Clamp( Arg( a, 0 ).ToInt32( vm ), n );
			int del = a.Length > 1 ? Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n - s ) : n - s;
			var removed = vm.NewArray( arr.Items.GetRange( s, del ) );
			arr.Items.RemoveRange( s, del );
			if ( a.Length > 2 ) arr.Items.InsertRange( s, a[2..] );
			return removed;
		} );
		vm.Method( p, "sort", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null ) return Value.Undefined;
			AsFunction cmp = Arg( a, 0 ).AsObj as AsFunction;
			int flags = cmp != null ? Arg( a, 1 ).ToInt32( vm ) : (Arg( a, 0 ).IsNumber ? Arg( a, 0 ).ToInt32( vm ) : 0);
			return SortArray( vm, arr, flags, ( x, y ) =>
			{
				if ( cmp != null ) return Math.Sign( vm.CallFunction( cmp, Value.Undefined, new[] { x, y } ).ToNumber( vm ) );
				return CompareDefault( vm, x, y, flags );
			} );
		} );
		vm.Method( p, "sortOn", ( vm, t, a ) =>
		{
			var arr = A( t ); if ( arr == null ) return Value.Undefined;
			var fields = new List<string>();
			if ( Arg( a, 0 ).AsObj is AsArray fa ) foreach ( var f in fa.Items ) fields.Add( f.ToStr( vm ) );
			else fields.Add( Arg( a, 0 ).ToStr( vm ) );
			int flags = Arg( a, 1 ).IsNumber ? Arg( a, 1 ).ToInt32( vm ) : 0;
			return SortArray( vm, arr, flags, ( x, y ) =>
			{
				foreach ( var f in fields )
				{
					int c = CompareDefault( vm, vm.GetMember( x, f ), vm.GetMember( y, f ), flags );
					if ( c != 0 ) return c;
				}
				return 0;
			} );
		} );
	}

	static int Clamp( int i, int n ) => i < 0 ? Math.Max( 0, n + i ) : Math.Min( i, n );

	static string Join( Avm1 vm, AsArray arr, string sep )
	{
		var sb = new StringBuilder();
		for ( int i = 0; i < arr.Items.Count; i++ )
		{
			if ( i > 0 ) sb.Append( sep );
			var v = arr.Items[i];
			if ( !v.IsNullish ) sb.Append( v.ToStr( vm ) );
			else if ( v.IsNull ) sb.Append( "null" );
			else sb.Append( "undefined" );
		}
		return sb.ToString();
	}

	static int CompareDefault( Avm1 vm, Value x, Value y, int flags )
	{
		int c;
		if ( (flags & 16) != 0 )
		{
			double a = x.ToNumber( vm ), b = y.ToNumber( vm );
			c = a < b ? -1 : a > b ? 1 : 0;
		}
		else
		{
			var a = x.ToStr( vm ); var b = y.ToStr( vm );
			c = (flags & 1) != 0 ? string.Compare( a, b, StringComparison.OrdinalIgnoreCase ) : string.CompareOrdinal( a, b );
			c = Math.Sign( c );
		}
		return (flags & 2) != 0 ? -c : c;
	}

	static Value SortArray( Avm1 vm, AsArray arr, int flags, Comparison<Value> cmp )
	{
		var idx = new List<int>();
		for ( int i = 0; i < arr.Items.Count; i++ ) idx.Add( i );
		var items = arr.Items;
		// stable merge sort via index comparison
		idx.Sort( ( i, j ) => { int c = cmp( items[i], items[j] ); return c != 0 ? c : i.CompareTo( j ); } );
		if ( (flags & 4) != 0 )
			for ( int k = 1; k < idx.Count; k++ ) if ( cmp( items[idx[k - 1]], items[idx[k]] ) == 0 ) return 0;
		if ( (flags & 8) != 0 )
		{
			var r = vm.NewArray();
			foreach ( var i in idx ) r.Items.Add( i );
			return r;
		}
		var sorted = new List<Value>();
		foreach ( var i in idx ) sorted.Add( items[i] );
		arr.Items.Clear();
		arr.Items.AddRange( sorted );
		return Value.From( arr );
	}

	// ========================================================================= String

	static void InstallString( Avm1 vm )
	{
		var p = new AsBoxed( vm.ObjectProto, "" );
		vm.StringProto = p;
		var ctor = Ctor( vm, "String", p, ( vm, t, a ) => a.Length == 0 ? "" : (Value)Arg( a, 0 ).ToStr( vm ),
			( vm, t, a ) => Value.From( new AsBoxed( vm.StringProto, a.Length == 0 ? "" : Arg( a, 0 ).ToStr( vm ) ) ) );
		vm.Method( ctor, "fromCharCode", ( vm, t, a ) =>
		{
			var sb = new StringBuilder();
			foreach ( var c in a ) sb.Append( (char)c.ToInt32( vm ) );
			return sb.ToString();
		} );
		string S( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToStr( vm ) : t.ToStr( vm );

		vm.Method( p, "toString", ( vm, t, a ) => S( vm, t ) );
		vm.Method( p, "valueOf", ( vm, t, a ) => S( vm, t ) );
		vm.Method( p, "charAt", ( vm, t, a ) => { var s = S( vm, t ); int i = Arg( a, 0 ).ToInt32( vm ); return i >= 0 && i < s.Length ? s[i].ToString() : ""; } );
		vm.Method( p, "charCodeAt", ( vm, t, a ) => { var s = S( vm, t ); int i = Arg( a, 0 ).ToInt32( vm ); return i >= 0 && i < s.Length ? s[i] : Value.NaN; } );
		vm.Method( p, "concat", ( vm, t, a ) => { var sb = new StringBuilder( S( vm, t ) ); foreach ( var x in a ) sb.Append( x.ToStr( vm ) ); return sb.ToString(); } );
		vm.Method( p, "indexOf", ( vm, t, a ) =>
		{
			var s = S( vm, t ); var f = Arg( a, 0 ).ToStr( vm );
			int start = Arg( a, 1 ).IsUndefined ? 0 : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, s.Length );
			return s.IndexOf( f, start, StringComparison.Ordinal );
		} );
		vm.Method( p, "lastIndexOf", ( vm, t, a ) =>
		{
			var s = S( vm, t ); var f = Arg( a, 0 ).ToStr( vm );
			int start = Arg( a, 1 ).IsUndefined ? s.Length : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, s.Length );
			if ( f.Length == 0 ) return Math.Min( start, s.Length );
			int from = Math.Min( start + f.Length - 1, s.Length - 1 );
			return from < 0 ? -1 : s.LastIndexOf( f, from, StringComparison.Ordinal );
		} );
		vm.Method( p, "slice", ( vm, t, a ) =>
		{
			var s = S( vm, t ); int n = s.Length;
			int st = Clamp( Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToInt32( vm ), n );
			int e = Clamp( Arg( a, 1 ).IsUndefined ? n : Arg( a, 1 ).ToInt32( vm ), n );
			return e > st ? s.Substring( st, e - st ) : "";
		} );
		vm.Method( p, "substring", ( vm, t, a ) =>
		{
			var s = S( vm, t ); int n = s.Length;
			int st = Math.Clamp( Arg( a, 0 ).ToInt32( vm ), 0, n );
			int e = Arg( a, 1 ).IsUndefined ? n : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n );
			if ( st > e ) (st, e) = (e, st);
			return s.Substring( st, e - st );
		} );
		vm.Method( p, "substr", ( vm, t, a ) =>
		{
			var s = S( vm, t ); int n = s.Length;
			int st = Clamp( Arg( a, 0 ).ToInt32( vm ), n );
			int len = Arg( a, 1 ).IsUndefined ? n - st : Math.Clamp( Arg( a, 1 ).ToInt32( vm ), 0, n - st );
			return s.Substring( st, len );
		} );
		vm.Method( p, "split", ( vm, t, a ) =>
		{
			var s = S( vm, t );
			var r = vm.NewArray();
			if ( Arg( a, 0 ).IsUndefined ) { r.Items.Add( s ); return r; }
			var sep = Arg( a, 0 ).ToStr( vm );
			int limit = Arg( a, 1 ).IsUndefined ? int.MaxValue : Arg( a, 1 ).ToInt32( vm );
			if ( sep.Length == 0 ) { foreach ( var ch in s ) { if ( r.Items.Count >= limit ) break; r.Items.Add( ch.ToString() ); } return r; }
			foreach ( var part in s.Split( sep ) ) { if ( r.Items.Count >= limit ) break; r.Items.Add( part ); }
			return r;
		} );
		vm.Method( p, "toLowerCase", ( vm, t, a ) => S( vm, t ).ToLowerInvariant() );
		vm.Method( p, "toUpperCase", ( vm, t, a ) => S( vm, t ).ToUpperInvariant() );
	}

	// ========================================================================= Number / Boolean

	static void InstallNumberBoolean( Avm1 vm )
	{
		var np = new AsBoxed( vm.ObjectProto, 0 );
		vm.NumberProto = np;
		var nc = Ctor( vm, "Number", np, ( vm, t, a ) => a.Length == 0 ? 0 : Arg( a, 0 ).ToNumber( vm ),
			( vm, t, a ) => Value.From( new AsBoxed( vm.NumberProto, a.Length == 0 ? 0 : Arg( a, 0 ).ToNumber( vm ) ) ) );
		nc.SetOwn( "MAX_VALUE", double.MaxValue ); nc.SetOwn( "MIN_VALUE", double.Epsilon ); nc.SetOwn( "NaN", double.NaN );
		nc.SetOwn( "POSITIVE_INFINITY", double.PositiveInfinity ); nc.SetOwn( "NEGATIVE_INFINITY", double.NegativeInfinity );
		double N( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToNumber( vm ) : t.ToNumber( vm );
		vm.Method( np, "valueOf", ( vm, t, a ) => N( vm, t ) );
		vm.Method( np, "toString", ( vm, t, a ) =>
		{
			double d = N( vm, t );
			int radix = Arg( a, 0 ).IsUndefined ? 10 : Arg( a, 0 ).ToInt32( vm );
			if ( radix == 10 || radix < 2 || radix > 36 || double.IsNaN( d ) || double.IsInfinity( d ) ) return Value.NumberToString( d );
			long n = (long)d;
			bool neg = n < 0; if ( neg ) n = -n;
			var sb = new StringBuilder();
			do { int dg = (int)(n % radix); sb.Insert( 0, (char)(dg < 10 ? '0' + dg : 'a' + dg - 10) ); n /= radix; } while ( n > 0 );
			if ( neg ) sb.Insert( 0, '-' );
			return sb.ToString();
		} );

		var bp = new AsBoxed( vm.ObjectProto, false );
		vm.BooleanProto = bp;
		Ctor( vm, "Boolean", bp, ( vm, t, a ) => Arg( a, 0 ).ToBool( vm.SwfVersion ),
			( vm, t, a ) => Value.From( new AsBoxed( vm.BooleanProto, Arg( a, 0 ).ToBool( vm.SwfVersion ) ) ) );
		bool B( Avm1 vm, Value t ) => t.IsObject && t.Obj is AsBoxed b ? b.Primitive.ToBool( vm.SwfVersion ) : t.ToBool( vm.SwfVersion );
		vm.Method( bp, "valueOf", ( vm, t, a ) => B( vm, t ) );
		vm.Method( bp, "toString", ( vm, t, a ) => B( vm, t ) ? "true" : "false" );
	}

	// ========================================================================= Math

	static void InstallMath( Avm1 vm )
	{
		var m = vm.NewObject();
		vm.Global.DefineHidden( "Math", Value.From( m ) );
		double D( Avm1 vm, Value[] a, int i ) => Arg( a, i ).ToNumber( vm );
		vm.Method( m, "abs", ( vm, t, a ) => Math.Abs( D( vm, a, 0 ) ) );
		vm.Method( m, "acos", ( vm, t, a ) => Math.Acos( D( vm, a, 0 ) ) );
		vm.Method( m, "asin", ( vm, t, a ) => Math.Asin( D( vm, a, 0 ) ) );
		vm.Method( m, "atan", ( vm, t, a ) => Math.Atan( D( vm, a, 0 ) ) );
		vm.Method( m, "atan2", ( vm, t, a ) => Math.Atan2( D( vm, a, 0 ), D( vm, a, 1 ) ) );
		vm.Method( m, "ceil", ( vm, t, a ) => Math.Ceiling( D( vm, a, 0 ) ) );
		vm.Method( m, "cos", ( vm, t, a ) => Math.Cos( D( vm, a, 0 ) ) );
		vm.Method( m, "exp", ( vm, t, a ) => Math.Exp( D( vm, a, 0 ) ) );
		vm.Method( m, "floor", ( vm, t, a ) => Math.Floor( D( vm, a, 0 ) ) );
		vm.Method( m, "log", ( vm, t, a ) => Math.Log( D( vm, a, 0 ) ) );
		vm.Method( m, "max", ( vm, t, a ) =>
		{
			if ( a.Length == 0 ) return double.NegativeInfinity;
			double r = double.NegativeInfinity;
			foreach ( var x in a ) { var d = x.ToNumber( vm ); if ( double.IsNaN( d ) ) return double.NaN; r = Math.Max( r, d ); }
			return r;
		} );
		vm.Method( m, "min", ( vm, t, a ) =>
		{
			if ( a.Length == 0 ) return double.PositiveInfinity;
			double r = double.PositiveInfinity;
			foreach ( var x in a ) { var d = x.ToNumber( vm ); if ( double.IsNaN( d ) ) return double.NaN; r = Math.Min( r, d ); }
			return r;
		} );
		vm.Method( m, "pow", ( vm, t, a ) => Math.Pow( D( vm, a, 0 ), D( vm, a, 1 ) ) );
		vm.Method( m, "random", ( vm, t, a ) => vm.Rng.NextDouble() );
		vm.Method( m, "round", ( vm, t, a ) => Math.Floor( D( vm, a, 0 ) + 0.5 ) );
		vm.Method( m, "sin", ( vm, t, a ) => Math.Sin( D( vm, a, 0 ) ) );
		vm.Method( m, "sqrt", ( vm, t, a ) => Math.Sqrt( D( vm, a, 0 ) ) );
		vm.Method( m, "tan", ( vm, t, a ) => Math.Tan( D( vm, a, 0 ) ) );
		m.DefineHidden( "PI", Math.PI ); m.DefineHidden( "E", Math.E ); m.DefineHidden( "LN2", Math.Log( 2 ) ); m.DefineHidden( "LN10", Math.Log( 10 ) );
		m.DefineHidden( "LOG2E", 1 / Math.Log( 2 ) ); m.DefineHidden( "LOG10E", 1 / Math.Log( 10 ) ); m.DefineHidden( "SQRT1_2", Math.Sqrt( 0.5 ) ); m.DefineHidden( "SQRT2", Math.Sqrt( 2 ) );
	}

	// ========================================================================= Date

	static void InstallDate( Avm1 vm )
	{
		var p = vm.NewObject();
		vm.DateProto = p;
		NativeCall make = ( vm, t, a ) =>
		{
			var o = new AsObject( vm.DateProto );
			DateTime dt;
			if ( a.Length == 0 ) dt = DateTime.Now;
			else if ( a.Length == 1 ) dt = new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ).AddMilliseconds( Arg( a, 0 ).ToNumber( vm ) ).ToLocalTime();
			else
			{
				int y = Arg( a, 0 ).ToInt32( vm ); if ( y < 100 ) y += 1900;
				dt = new DateTime( y, 1, 1 ).AddMonths( Arg( a, 1 ).ToInt32( vm ) ).AddDays( (a.Length > 2 ? Arg( a, 2 ).ToInt32( vm ) : 1) - 1 )
					.AddHours( Arg( a, 3 ).IsUndefined ? 0 : Arg( a, 3 ).ToInt32( vm ) ).AddMinutes( Arg( a, 4 ).IsUndefined ? 0 : Arg( a, 4 ).ToInt32( vm ) )
					.AddSeconds( Arg( a, 5 ).IsUndefined ? 0 : Arg( a, 5 ).ToInt32( vm ) );
			}
			o.Native = dt;
			return Value.From( o );
		};
		Ctor( vm, "Date", p, ( vm, t, a ) => DateTime.Now.ToString( "ddd MMM d HH:mm:ss yyyy", CultureInfo.InvariantCulture ), make );
		DateTime Dt( Value t ) => t.AsObj?.Native is DateTime d ? d : DateTime.Now;
		vm.Method( p, "getFullYear", ( vm, t, a ) => Dt( t ).Year );
		vm.Method( p, "getYear", ( vm, t, a ) => Dt( t ).Year - 1900 );
		vm.Method( p, "getMonth", ( vm, t, a ) => Dt( t ).Month - 1 );
		vm.Method( p, "getDate", ( vm, t, a ) => Dt( t ).Day );
		vm.Method( p, "getDay", ( vm, t, a ) => (int)Dt( t ).DayOfWeek );
		vm.Method( p, "getHours", ( vm, t, a ) => Dt( t ).Hour );
		vm.Method( p, "getMinutes", ( vm, t, a ) => Dt( t ).Minute );
		vm.Method( p, "getSeconds", ( vm, t, a ) => Dt( t ).Second );
		vm.Method( p, "getMilliseconds", ( vm, t, a ) => Dt( t ).Millisecond );
		vm.Method( p, "getTime", ( vm, t, a ) => (Dt( t ).ToUniversalTime() - new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc )).TotalMilliseconds );
		vm.Method( p, "valueOf", ( vm, t, a ) => (Dt( t ).ToUniversalTime() - new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc )).TotalMilliseconds );
		vm.Method( p, "getTimezoneOffset", ( vm, t, a ) => -(Dt( t ) - Dt( t ).ToUniversalTime()).TotalMinutes );
		vm.Method( p, "toString", ( vm, t, a ) => Dt( t ).ToString( "ddd MMM d HH:mm:ss yyyy", CultureInfo.InvariantCulture ) );
	}

	// ========================================================================= global functions

	static void InstallGlobals( Avm1 vm )
	{
		var g = vm.Global;
		vm.Method( g, "trace", ( vm, t, a ) => { vm.Trace( Arg( a, 0 ).ToStr( vm ) ); return Value.Undefined; } );
		vm.Method( g, "isNaN", ( vm, t, a ) => double.IsNaN( Arg( a, 0 ).ToNumber( vm ) ) );
		vm.Method( g, "isFinite", ( vm, t, a ) => { var d = Arg( a, 0 ).ToNumber( vm ); return !double.IsNaN( d ) && !double.IsInfinity( d ); } );
		vm.Method( g, "parseInt", ( vm, t, a ) =>
		{
			var s = Arg( a, 0 ).ToStr( vm ).Trim();
			int radix = Arg( a, 1 ).IsUndefined ? 0 : Arg( a, 1 ).ToInt32( vm );
			bool neg = false; int i = 0;
			if ( i < s.Length && (s[i] == '-' || s[i] == '+') ) { neg = s[i] == '-'; i++; }
			if ( radix == 0 )
			{
				if ( i + 1 < s.Length && s[i] == '0' && (s[i + 1] == 'x' || s[i + 1] == 'X') ) { radix = 16; i += 2; }
				else if ( i + 1 < s.Length && s[i] == '0' ) radix = 8;
				else radix = 10;
			}
			else if ( radix == 16 && i + 1 < s.Length && s[i] == '0' && (s[i + 1] == 'x' || s[i + 1] == 'X') ) i += 2;
			double r = 0; bool any = false;
			for ( ; i < s.Length; i++ )
			{
				char c = char.ToLowerInvariant( s[i] );
				int d = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'z' ? c - 'a' + 10 : 99;
				if ( d >= radix ) break;
				r = r * radix + d; any = true;
			}
			return any ? (neg ? -r : r) : double.NaN;
		} );
		vm.Method( g, "parseFloat", ( vm, t, a ) =>
		{
			var s = Arg( a, 0 ).ToStr( vm ).TrimStart();
			int i = 0;
			if ( i < s.Length && (s[i] == '-' || s[i] == '+') ) i++;
			while ( i < s.Length && char.IsDigit( s[i] ) ) i++;
			if ( i < s.Length && s[i] == '.' ) { i++; while ( i < s.Length && char.IsDigit( s[i] ) ) i++; }
			if ( i < s.Length && (s[i] == 'e' || s[i] == 'E') )
			{
				int j = i + 1; if ( j < s.Length && (s[j] == '-' || s[j] == '+') ) j++;
				if ( j < s.Length && char.IsDigit( s[j] ) ) { i = j; while ( i < s.Length && char.IsDigit( s[i] ) ) i++; }
			}
			return double.TryParse( s.Substring( 0, i ), NumberStyles.Float, CultureInfo.InvariantCulture, out var r ) ? r : double.NaN;
		} );
		vm.Method( g, "escape", ( vm, t, a ) =>
		{
			var s = Arg( a, 0 ).ToStr( vm ); var sb = new StringBuilder();
			foreach ( var b in Encoding.UTF8.GetBytes( s ) )
			{
				char c = (char)b;
				if ( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "@*_+-./".IndexOf( c ) >= 0 ) sb.Append( c );
				else sb.Append( '%' ).Append( b.ToString( "X2" ) );
			}
			return sb.ToString();
		} );
		vm.Method( g, "unescape", ( vm, t, a ) =>
		{
			var s = Arg( a, 0 ).ToStr( vm ); var bytes = new List<byte>();
			for ( int i = 0; i < s.Length; i++ )
			{
				if ( s[i] == '%' && i + 2 < s.Length && int.TryParse( s.Substring( i + 1, 2 ), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h ) ) { bytes.Add( (byte)h ); i += 2; }
				else if ( s[i] == '+' ) bytes.Add( (byte)' ' );
				else bytes.AddRange( Encoding.UTF8.GetBytes( s[i].ToString() ) );
			}
			return Encoding.UTF8.GetString( bytes.ToArray() );
		} );
		vm.Method( g, "getTimer", ( vm, t, a ) => Math.Floor( vm.Player.TimeMs ) );
		vm.Method( g, "getVersion", ( vm, t, a ) => "WIN 11,0,1,152" );
		vm.Method( g, "updateAfterEvent", ( vm, t, a ) => Value.Undefined );
		vm.Method( g, "random", ( vm, t, a ) => { int n = Arg( a, 0 ).ToInt32( vm ); return n <= 0 ? 0 : vm.Rng.Next( n ); } );
		vm.Method( g, "int", ( vm, t, a ) => (double)Arg( a, 0 ).ToInt32( vm ) );
		vm.Method( g, "targetPath", ( vm, t, a ) => Arg( a, 0 ).AsObj is DisplayObject d ? (Value)d.TargetPath : Value.Undefined );
		vm.Method( g, "setInterval", ( vm, t, a ) =>
		{
			if ( Arg( a, 0 ).AsObj is AsFunction f ) return vm.Player.SetInterval( f, Value.Undefined, Arg( a, 1 ).ToNumber( vm ), a.Length > 2 ? a[2..] : Array.Empty<Value>() );
			if ( Arg( a, 0 ).IsObject )
			{
				var obj = Arg( a, 0 );
				var fm = vm.GetMember( obj, Arg( a, 1 ).ToStr( vm ) ).AsObj as AsFunction;
				if ( fm != null ) return vm.Player.SetInterval( fm, obj, Arg( a, 2 ).ToNumber( vm ), a.Length > 3 ? a[3..] : Array.Empty<Value>() );
			}
			return Value.Undefined;
		} );
		vm.Method( g, "clearInterval", ( vm, t, a ) => { vm.Player.ClearInterval( Arg( a, 0 ).ToInt32( vm ) ); return Value.Undefined; } );
		vm.Method( g, "setTimeout", ( vm, t, a ) =>
		{
			if ( Arg( a, 0 ).AsObj is AsFunction f ) return vm.Player.SetInterval( f, Value.Undefined, Arg( a, 1 ).ToNumber( vm ), a.Length > 2 ? a[2..] : Array.Empty<Value>(), true );
			return Value.Undefined;
		} );
		vm.Method( g, "clearTimeout", ( vm, t, a ) => { vm.Player.ClearInterval( Arg( a, 0 ).ToInt32( vm ) ); return Value.Undefined; } );
		vm.Method( g, "ASSetPropFlags", ( vm, t, a ) =>
		{
			var o = Arg( a, 0 ).AsObj; if ( o == null ) return Value.Undefined;
			int set = Arg( a, 2 ).ToInt32( vm ), clear = Arg( a, 3 ).ToInt32( vm );
			var names = new List<string>();
			if ( Arg( a, 1 ).IsNull ) names.AddRange( o.OwnKeys );
			else if ( Arg( a, 1 ).AsObj is AsArray arr ) foreach ( var x in arr.Items ) names.Add( x.ToStr( vm ) );
			else names.AddRange( Arg( a, 1 ).ToStr( vm ).Split( ',' ) );
			foreach ( var n in names ) o.SetFlags( n, (PropFlags)(set & 7), (PropFlags)(clear & 7) );
			return Value.Undefined;
		} );
		g.DefineHidden( "NaN", double.NaN );
		g.DefineHidden( "Infinity", double.PositiveInfinity );

		var system = vm.NewObject();
		var security = vm.NewObject();
		vm.Method( security, "allowDomain", ( vm, t, a ) => Value.Undefined );
		vm.Method( security, "allowInsecureDomain", ( vm, t, a ) => Value.Undefined );
		system.SetOwn( "security", Value.From( security ) );
		var caps = vm.NewObject();
		caps.SetOwn( "version", "WIN 11,0,1,152" ); caps.SetOwn( "os", "Windows" ); caps.SetOwn( "playerType", "StandAlone" );
		caps.SetOwn( "screenResolutionX", 1920 ); caps.SetOwn( "screenResolutionY", 1080 ); caps.SetOwn( "language", "en" );
		system.SetOwn( "capabilities", Value.From( caps ) );
		system.SetOwn( "useCodepage", false );
		g.DefineHidden( "System", Value.From( system ) );

		// TextFormat (plain property bag)
		var tfp = vm.NewObject();
		Ctor( vm, "TextFormat", tfp, ( vm, t, a ) =>
		{
			if ( !t.IsObject ) return Value.Undefined;
			string[] names = { "font", "size", "color", "bold", "italic", "underline", "url", "target", "align", "leftMargin", "rightMargin", "indent", "leading" };
			for ( int i = 0; i < names.Length; i++ ) t.Obj.Set( names[i], i < a.Length ? a[i] : Value.Null, vm );
			return Value.Undefined;
		} );
	}

	// ========================================================================= MovieClip

	static MovieClip Clip( Value t ) => t.AsObj as MovieClip;
	static DisplayObject Dobj( Value t ) => t.AsObj as DisplayObject;

	static void InstallMovieClip( Avm1 vm )
	{
		var p = vm.NewObject();
		vm.MovieClipProto = p;
		Ctor( vm, "MovieClip", p, ( vm, t, a ) => Value.Undefined );

		vm.Method( p, "play", ( vm, t, a ) => { Clip( t )?.Play(); return Value.Undefined; } );
		vm.Method( p, "stop", ( vm, t, a ) => { Clip( t )?.Stop(); return Value.Undefined; } );
		vm.Method( p, "nextFrame", ( vm, t, a ) => { Clip( t )?.NextFrame(); return Value.Undefined; } );
		vm.Method( p, "prevFrame", ( vm, t, a ) => { Clip( t )?.PrevFrame(); return Value.Undefined; } );
		NativeCall gotoFn( bool stop ) => ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
			var f = Arg( a, 0 );
			if ( f.IsString )
			{
				var n = Value.StringToNumber( f.Str, vm.SwfVersion );
				if ( !double.IsNaN( n ) ) mc.GotoFrame( (int)n, stop ); else mc.GotoLabel( f.Str, stop );
			}
			else mc.GotoFrame( f.ToInt32( vm ), stop );
			return Value.Undefined;
		};
		vm.Method( p, "gotoAndPlay", gotoFn( false ) );
		vm.Method( p, "gotoAndStop", gotoFn( true ) );

		vm.Method( p, "attachMovie", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
			var id = Arg( a, 0 ).ToStr( vm ); var name = Arg( a, 1 ).ToStr( vm ); int depth = Arg( a, 2 ).ToInt32( vm );
			var def = vm.Player.Lib.GetExport( id );
			if ( def == null ) return Value.Undefined;
			var obj = vm.Player.Instantiate( def.Id, mc );
			if ( obj == null ) return Value.Undefined;
			obj.Name = name;
			mc.AddChild( obj, depth );
			vm.Player.Construct( obj, Arg( a, 3 ).AsObj, byScript: true );
			return Value.From( obj );
		} );
		vm.Method( p, "createEmptyMovieClip", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
			var c = new MovieClip( vm.Player, null ) { Name = Arg( a, 0 ).ToStr( vm ) };
			mc.AddChild( c, Arg( a, 1 ).ToInt32( vm ) );
			vm.Player.Construct( c, null, true );
			return Value.From( c );
		} );
		vm.Method( p, "duplicateMovieClip", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null || mc.Parent == null ) return Value.Undefined;
			var name = Arg( a, 0 ).ToStr( vm ); int depth = Arg( a, 1 ).ToInt32( vm );
			mc.Duplicate( name, depth );
			var copy = mc.Parent.ChildAtDepth( depth );
			if ( copy != null && Arg( a, 2 ).AsObj is AsObject init )
				foreach ( var k in init.EnumerableKeys() ) copy.Set( k, init.Get( k, vm ), vm );
			return copy != null ? Value.From( copy ) : Value.Undefined;
		} );
		vm.Method( p, "removeMovieClip", ( vm, t, a ) => { Dobj( t )?.RemoveFromScript(); return Value.Undefined; } );
		vm.Method( p, "unloadMovie", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
			foreach ( var c in mc.Children.ToArray() ) mc.RemoveChild( c );
			return Value.Undefined;
		} );
		vm.Method( p, "loadMovie", ( vm, t, a ) => Value.Undefined );
		vm.Method( p, "swapDepths", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null || mc.Parent == null ) return Value.Undefined;
			var x = Arg( a, 0 );
			if ( x.AsObj is DisplayObject other ) { if ( other.Parent == mc.Parent ) mc.SwapDepths( other.Depth ); }
			else mc.SwapDepths( x.ToInt32( vm ) );
			return Value.Undefined;
		} );
		vm.Method( p, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
		vm.Method( p, "getNextHighestDepth", ( vm, t, a ) => Clip( t ) is { } mc ? mc.NextHighestDepth() : Value.Undefined );
		vm.Method( p, "getInstanceAtDepth", ( vm, t, a ) => Clip( t )?.ChildAtDepth( Arg( a, 0 ).ToInt32( vm ) ) is { } c ? Value.From( c ) : Value.Undefined );
		vm.Method( p, "getBytesLoaded", ( vm, t, a ) => 7586504 );
		vm.Method( p, "getBytesTotal", ( vm, t, a ) => 7586504 );
		vm.Method( p, "getSWFVersion", ( vm, t, a ) => vm.SwfVersion );
		vm.Method( p, "getURL", ( vm, t, a ) => { vm.Player.GetUrl( Arg( a, 0 ).ToStr( vm ), Arg( a, 1 ).ToStr( vm ) ); return Value.Undefined; } );
		vm.Method( p, "getBounds", ( vm, t, a ) =>
		{
			var d = Dobj( t ); if ( d == null ) return Value.Undefined;
			var space = Arg( a, 0 ).AsObj as DisplayObject ?? d;
			var m = space.WorldMatrix.Inverse().Mul( d.WorldMatrix );
			var b = d.BoundsIn( m );
			var r = vm.NewObject();
			if ( b == null ) { r.Set( "xMin", 6710886.35, vm ); r.Set( "yMin", 6710886.35, vm ); r.Set( "xMax", 6710886.35, vm ); r.Set( "yMax", 6710886.35, vm ); return r; }
			r.Set( "xMin", b.Value.x0, vm ); r.Set( "yMin", b.Value.y0, vm ); r.Set( "xMax", b.Value.x1, vm ); r.Set( "yMax", b.Value.y1, vm );
			return r;
		} );
		vm.Method( p, "localToGlobal", ( vm, t, a ) =>
		{
			var d = Dobj( t ); var pt = Arg( a, 0 ).AsObj; if ( d == null || pt == null ) return Value.Undefined;
			var (x, y) = d.WorldMatrix.Apply( pt.Get( "x", vm ).ToNumber( vm ), pt.Get( "y", vm ).ToNumber( vm ) );
			pt.Set( "x", x, vm ); pt.Set( "y", y, vm );
			return Value.Undefined;
		} );
		vm.Method( p, "globalToLocal", ( vm, t, a ) =>
		{
			var d = Dobj( t ); var pt = Arg( a, 0 ).AsObj; if ( d == null || pt == null ) return Value.Undefined;
			var (x, y) = d.WorldMatrix.Inverse().Apply( pt.Get( "x", vm ).ToNumber( vm ), pt.Get( "y", vm ).ToNumber( vm ) );
			pt.Set( "x", x, vm ); pt.Set( "y", y, vm );
			return Value.Undefined;
		} );
		vm.Method( p, "hitTest", ( vm, t, a ) =>
		{
			var d = Dobj( t ); if ( d == null ) return false;
			if ( a.Length >= 2 )
			{
				double x = Arg( a, 0 ).ToNumber( vm ), y = Arg( a, 1 ).ToNumber( vm );
				bool shape = Arg( a, 2 ).ToBool( vm.SwfVersion );
				if ( shape ) return d.HitTestPoint( x, y, true );
				var b = d.BoundsIn( d.WorldMatrix );
				return b != null && x >= b.Value.x0 && x <= b.Value.x1 && y >= b.Value.y0 && y <= b.Value.y1;
			}
			var other = Arg( a, 0 ).AsObj as DisplayObject;
			if ( other == null ) return false;
			var b1 = d.BoundsIn( d.WorldMatrix ); var b2 = other.BoundsIn( other.WorldMatrix );
			if ( b1 == null || b2 == null ) return false;
			return b1.Value.x0 <= b2.Value.x1 && b2.Value.x0 <= b1.Value.x1 && b1.Value.y0 <= b2.Value.y1 && b2.Value.y0 <= b1.Value.y1;
		} );
		vm.Method( p, "setMask", ( vm, t, a ) =>
		{
			var d = Dobj( t ); if ( d == null ) return Value.Undefined;
			if ( d.MaskedBy != null ) { d.MaskedBy.MaskFor = null; d.MaskedBy = null; }
			if ( Arg( a, 0 ).AsObj is DisplayObject m ) { d.MaskedBy = m; m.MaskFor = d; }
			return Value.Undefined;
		} );
		vm.Method( p, "startDrag", ( vm, t, a ) =>
		{
			var d = Dobj( t ); if ( d == null ) return Value.Undefined;
			double[] rect = a.Length >= 5 ? new[] { Arg( a, 1 ).ToNumber( vm ), Arg( a, 2 ).ToNumber( vm ), Arg( a, 3 ).ToNumber( vm ), Arg( a, 4 ).ToNumber( vm ) } : null;
			vm.Player.StartDrag( d, Arg( a, 0 ).ToBool( vm.SwfVersion ), rect );
			return Value.Undefined;
		} );
		vm.Method( p, "stopDrag", ( vm, t, a ) => { vm.Player.StopDrag(); return Value.Undefined; } );
		vm.Method( p, "createTextField", ( vm, t, a ) =>
		{
			var mc = Clip( t ); if ( mc == null ) return Value.Undefined;
			var def = new EditTextDef { X0 = Arg( a, 2 ).ToNumber( vm ), Y0 = Arg( a, 3 ).ToNumber( vm ), Size = 12, FontId = 0, Color = new double[] { 0, 0, 0, 255 } };
			def.X1 = def.X0 + Arg( a, 4 ).ToNumber( vm ); def.Y1 = def.Y0 + Arg( a, 5 ).ToNumber( vm );
			var tf = new EditText( vm.Player, def ) { Name = Arg( a, 0 ).ToStr( vm ) };
			mc.AddChild( tf, Arg( a, 1 ).ToInt32( vm ) );
			return Value.From( tf );
		} );
	}

	static void InstallTextFieldButton( Avm1 vm )
	{
		var tp = vm.NewObject();
		vm.TextFieldProto = tp;
		Ctor( vm, "TextField", tp, ( vm, t, a ) => Value.Undefined );
		vm.Method( tp, "setTextFormat", ( vm, t, a ) => Value.Undefined );
		vm.Method( tp, "setNewTextFormat", ( vm, t, a ) => Value.Undefined );
		vm.Method( tp, "getTextFormat", ( vm, t, a ) => Value.From( vm.NewObject() ) );
		vm.Method( tp, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
		vm.Method( tp, "removeTextField", ( vm, t, a ) => { Dobj( t )?.RemoveFromScript(); return Value.Undefined; } );
		vm.Method( tp, "replaceSel", ( vm, t, a ) => Value.Undefined );

		var bp = vm.NewObject();
		vm.ButtonProto = bp;
		Ctor( vm, "Button", bp, ( vm, t, a ) => Value.Undefined );
		vm.Method( bp, "getDepth", ( vm, t, a ) => Dobj( t ) is { } d ? d.Depth : Value.Undefined );
	}

	// ========================================================================= Sound

	sealed class SoundState
	{
		public DisplayObject Target;
		public SoundDef Def;
		public object Handle;
		public double Volume = 100, Pan = 0;
		public double StartedAt;
		public bool Completed;
	}

	static readonly List<(AsObject obj, SoundState st)> liveSounds = new();

	public static void StopAllSoundObjects( Player p )
	{
		foreach ( var (o, st) in liveSounds ) if ( st.Handle != null ) { p.Host?.StopSound( st.Handle ); st.Handle = null; }
	}

	/// <summary>Volume is per target clip in Flash: all Sound objects on the same target share it.</summary>
	static readonly Dictionary<DisplayObject, double> targetVolume = new();

	static void InstallSound( Avm1 vm )
	{
		var p = vm.NewObject();
		vm.SoundProto = p;
		Ctor( vm, "Sound", p, ( vm, t, a ) =>
		{
			if ( !t.IsObject ) return Value.Undefined;
			t.Obj.Native = new SoundState { Target = Arg( a, 0 ).AsObj as DisplayObject };
			return Value.Undefined;
		} );
		SoundState St( Value t ) => t.AsObj?.Native as SoundState;
		double Vol( SoundState s ) => IsGlobal( vm.Player, s ) ? vm.Player.GlobalVolume * 100 : (targetVolume.TryGetValue( s.Target, out var v ) ? v : 100);

		vm.Method( p, "attachSound", ( vm, t, a ) =>
		{
			var s = St( t ); if ( s == null ) return Value.Undefined;
			s.Def = vm.Player.Lib.GetExport( Arg( a, 0 ).ToStr( vm ) ) as SoundDef;
			return Value.Undefined;
		} );
		vm.Method( p, "start", ( vm, t, a ) =>
		{
			var s = St( t ); if ( s?.Def == null ) return Value.Undefined;
			double offset = Arg( a, 0 ).IsUndefined ? 0 : Arg( a, 0 ).ToNumber( vm );
			int loops = Arg( a, 1 ).IsUndefined ? 1 : Math.Max( 1, Arg( a, 1 ).ToInt32( vm ) );
			if ( s.Handle != null ) { vm.Player.Host?.StopSound( s.Handle ); s.Handle = null; }
			s.Handle = vm.Player.Host?.PlaySound( s.Def, Effective( vm.Player, s ), s.Pan / 100.0, loops, double.IsNaN( offset ) ? 0 : offset );
			s.StartedAt = vm.Player.TimeMs;
			s.Completed = false;
			liveSounds.RemoveAll( x => ReferenceEquals( x.obj, t.Obj ) );
			liveSounds.Add( (t.Obj, s) );
			return Value.Undefined;
		} );
		vm.Method( p, "stop", ( vm, t, a ) =>
		{
			var s = St( t ); if ( s == null ) return Value.Undefined;
			if ( !Arg( a, 0 ).IsUndefined )
			{
				var def = vm.Player.Lib.GetExport( Arg( a, 0 ).ToStr( vm ) );
				foreach ( var (o, st) in liveSounds ) if ( st.Def == def && st.Handle != null ) { vm.Player.Host?.StopSound( st.Handle ); st.Handle = null; }
				return Value.Undefined;
			}
			if ( s.Target == null || s.Target == vm.Player.Root ) vm.Player.StopAllSounds();
			else
			{
				// Flash: stop() on a targeted Sound stops every sound attached to that clip
				foreach ( var (o, st) in liveSounds )
					if ( st.Target == s.Target && st.Handle != null ) { vm.Player.Host?.StopSound( st.Handle ); st.Handle = null; st.Completed = true; }
				if ( s.Handle != null ) { vm.Player.Host?.StopSound( s.Handle ); s.Handle = null; }
			}
			return Value.Undefined;
		} );
		vm.Method( p, "setVolume", ( vm, t, a ) =>
		{
			var s = St( t ); if ( s == null ) return Value.Undefined;
			var v = Arg( a, 0 ).ToNumber( vm );
			if ( double.IsNaN( v ) ) return Value.Undefined;
			s.Volume = v;
			if ( IsGlobal( vm.Player, s ) ) vm.Player.SetGlobalVolume( v / 100.0 );
			else { targetVolume[s.Target] = v; RefreshSoundVolumes( vm.Player ); }
			return Value.Undefined;
		} );
		vm.Method( p, "getVolume", ( vm, t, a ) => St( t ) is { } s ? Vol( s ) : 100 );
		vm.Method( p, "setPan", ( vm, t, a ) => { if ( St( t ) is { } s ) s.Pan = Arg( a, 0 ).ToNumber( vm ); return Value.Undefined; } );
		vm.Method( p, "getPan", ( vm, t, a ) => St( t )?.Pan ?? 0 );
		vm.Method( p, "loadSound", ( vm, t, a ) => Value.Undefined );
		vm.Method( p, "getBytesLoaded", ( vm, t, a ) => 1 );
		vm.Method( p, "getBytesTotal", ( vm, t, a ) => 1 );
		p.AddVirtual( "position", vm.Native( "position", ( vm, t, a ) =>
		{
			var s = St( t ); if ( s?.Handle == null ) return 0;
			return Math.Floor( (vm.Player.Host?.SoundPosition( s.Handle ) ?? 0) * 1000 );
		} ), null );
		p.AddVirtual( "duration", vm.Native( "duration", ( vm, t, a ) => St( t )?.Def is { } d ? Math.Floor( d.Seconds * 1000 ) : 0 ), null );
	}

	static bool IsGlobal( Player p, SoundState s ) => s.Target == null || s.Target == p.Root;

	/// <summary>Flash: a sound's volume is its target clip's volume times the global (no-target) volume.</summary>
	static double Effective( Player p, SoundState s )
	{
		double t = !IsGlobal( p, s ) && targetVolume.TryGetValue( s.Target, out var v ) ? v / 100.0 : 1;
		return Math.Max( 0, t * p.GlobalVolume );
	}

	public static void RefreshSoundVolumes( Player p )
	{
		foreach ( var (o, st) in liveSounds )
			if ( st.Handle != null ) p.Host?.SetSoundVolume( st.Handle, Effective( p, st ), st.Pan / 100.0 );
	}

	/// <summary>Fire onSoundComplete for finished sounds (call once per frame).</summary>
	public static void UpdateSounds( Player p )
	{
		foreach ( var (o, st) in liveSounds.ToArray() )
		{
			if ( st.Handle == null || st.Completed ) continue;
			if ( p.Host != null && !p.Host.IsSoundPlaying( st.Handle ) )
			{
				st.Completed = true;
				st.Handle = null;
				var f = o.Get( "onSoundComplete", p.Vm ).AsObj as AsFunction;
				if ( f != null ) p.QueueCall( f, Value.From( o ), Array.Empty<Value>() );
			}
		}
		liveSounds.RemoveAll( x => x.st.Completed && x.st.Handle == null );
	}

	// ========================================================================= Color

	static void InstallColor( Avm1 vm )
	{
		var p = vm.NewObject();
		vm.ColorProto = p;
		Ctor( vm, "Color", p, ( vm, t, a ) =>
		{
			if ( !t.IsObject ) return Value.Undefined;
			var target = Arg( a, 0 ).AsObj as DisplayObject;
			if ( target == null && Arg( a, 0 ).IsString ) target = vm.ResolveTargetPath( vm.Player.Root, Arg( a, 0 ).Str );
			t.Obj.Native = target;
			return Value.Undefined;
		} );
		DisplayObject T( Value t ) => t.AsObj?.Native as DisplayObject;
		vm.Method( p, "setRGB", ( vm, t, a ) =>
		{
			var d = T( t ); if ( d == null ) return Value.Undefined;
			int c = Arg( a, 0 ).ToInt32( vm );
			d.Cx = new ColorTransform { Rm = 0, Gm = 0, Bm = 0, Am = d.Cx.Am, Ra = (c >> 16) & 255, Ga = (c >> 8) & 255, Ba = c & 255, Aa = d.Cx.Aa };
			d.ScriptTransformed = true;
			return Value.Undefined;
		} );
		vm.Method( p, "getRGB", ( vm, t, a ) =>
		{
			var d = T( t ); if ( d == null ) return 0;
			return ((int)d.Cx.Ra << 16) | ((int)d.Cx.Ga << 8) | (int)d.Cx.Ba;
		} );
		vm.Method( p, "setTransform", ( vm, t, a ) =>
		{
			var d = T( t ); var o = Arg( a, 0 ).AsObj; if ( d == null || o == null ) return Value.Undefined;
			var cx = d.Cx;
			double G( string n, double def ) { var v = o.Get( n, vm ); return v.IsUndefined ? def : v.ToNumber( vm ); }
			cx.Rm = G( "ra", cx.Rm * 100 ) / 100; cx.Gm = G( "ga", cx.Gm * 100 ) / 100; cx.Bm = G( "ba", cx.Bm * 100 ) / 100; cx.Am = G( "aa", cx.Am * 100 ) / 100;
			cx.Ra = G( "rb", cx.Ra ); cx.Ga = G( "gb", cx.Ga ); cx.Ba = G( "bb", cx.Ba ); cx.Aa = G( "ab", cx.Aa );
			d.Cx = cx;
			d.ScriptTransformed = true;
			return Value.Undefined;
		} );
		vm.Method( p, "getTransform", ( vm, t, a ) =>
		{
			var d = T( t ); var r = vm.NewObject(); if ( d == null ) return r;
			r.Set( "ra", d.Cx.Rm * 100, vm ); r.Set( "ga", d.Cx.Gm * 100, vm ); r.Set( "ba", d.Cx.Bm * 100, vm ); r.Set( "aa", d.Cx.Am * 100, vm );
			r.Set( "rb", d.Cx.Ra, vm ); r.Set( "gb", d.Cx.Ga, vm ); r.Set( "bb", d.Cx.Ba, vm ); r.Set( "ab", d.Cx.Aa, vm );
			return r;
		} );
	}

	// ========================================================================= Key / Mouse / Stage

	static void InstallKeyMouseStage( Avm1 vm )
	{
		var key = vm.NewObject();
		vm.Global.DefineHidden( "Key", Value.From( key ) );
		MakeBroadcaster( vm, key );
		vm.Method( key, "isDown", ( vm, t, a ) => vm.Player.KeysDown.Contains( Arg( a, 0 ).ToInt32( vm ) ) );
		vm.Method( key, "getCode", ( vm, t, a ) => vm.Player.LastKey );
		vm.Method( key, "getAscii", ( vm, t, a ) => vm.Player.LastAscii );
		vm.Method( key, "isToggled", ( vm, t, a ) => false );
		var codes = new (string, int)[] { ("BACKSPACE", 8), ("TAB", 9), ("ENTER", 13), ("SHIFT", 16), ("CONTROL", 17), ("CAPSLOCK", 20), ("ESCAPE", 27), ("SPACE", 32),
			("PGUP", 33), ("PGDN", 34), ("END", 35), ("HOME", 36), ("LEFT", 37), ("UP", 38), ("RIGHT", 39), ("DOWN", 40), ("INSERT", 45), ("DELETEKEY", 46) };
		foreach ( var (n, c) in codes ) key.DefineHidden( n, c );

		var mouse = vm.NewObject();
		vm.Global.DefineHidden( "Mouse", Value.From( mouse ) );
		MakeBroadcaster( vm, mouse );
		vm.Method( mouse, "show", ( vm, t, a ) => 1 );
		vm.Method( mouse, "hide", ( vm, t, a ) => 1 );

		var stage = vm.NewObject();
		vm.Global.DefineHidden( "Stage", Value.From( stage ) );
		MakeBroadcaster( vm, stage );
		stage.AddVirtual( "width", vm.Native( "width", ( vm, t, a ) => vm.Player.Lib.StageWidth ), null );
		stage.AddVirtual( "height", vm.Native( "height", ( vm, t, a ) => vm.Player.Lib.StageHeight ), null );
		stage.SetOwn( "scaleMode", "showAll" );
		stage.SetOwn( "align", "" );
		stage.SetOwn( "showMenu", true );
		bool fullscreen = false;
		stage.AddVirtual( "displayState",
			vm.Native( "displayState", ( vm, t, a ) => fullscreen ? "fullScreen" : "normal" ),
			vm.Native( "displayState", ( vm, t, a ) => { fullscreen = Arg( a, 0 ).ToStr( vm ) == "fullScreen"; vm.Player.Host?.SetFullScreen( fullscreen ); return Value.Undefined; } ) );
	}

	// ========================================================================= SharedObject

	static void InstallSharedObject( Avm1 vm )
	{
		var p = vm.NewObject();
		vm.SharedObjectProto = p;
		var ctor = Ctor( vm, "SharedObject", p, ( vm, t, a ) => Value.Undefined );
		var cache = new Dictionary<string, AsObject>();
		vm.Method( ctor, "getLocal", ( vm, t, a ) =>
		{
			var name = Arg( a, 0 ).ToStr( vm );
			if ( cache.TryGetValue( name, out var so ) ) return so;
			so = new AsObject( p ) { Native = name };
			AsObject data = null;
			var saved = vm.Player.Host?.LoadShared( name );
			if ( !string.IsNullOrEmpty( saved ) )
			{
				try { data = FromJson( vm, Json.Parse( saved ) ).AsObj; } catch ( Exception ) { data = null; }
			}
			so.SetOwn( "data", Value.From( data ?? vm.NewObject() ) );
			cache[name] = so;
			return so;
		} );
		vm.Method( p, "flush", ( vm, t, a ) =>
		{
			if ( t.AsObj?.Native is not string name ) return false;
			var data = t.Obj.Get( "data", vm );
			var sb = new StringBuilder();
			ToJson( vm, data, sb, new HashSet<AsObject>() );
			vm.Player.Host?.SaveShared( name, sb.ToString() );
			return true;
		} );
		vm.Method( p, "clear", ( vm, t, a ) =>
		{
			if ( t.AsObj?.Native is not string name ) return Value.Undefined;
			t.Obj.SetOwn( "data", Value.From( vm.NewObject() ) );
			vm.Player.Host?.SaveShared( name, "" );
			return Value.Undefined;
		} );
		vm.Method( p, "getSize", ( vm, t, a ) => 1024 );
	}

	static void ToJson( Avm1 vm, Value v, StringBuilder sb, HashSet<AsObject> seen )
	{
		switch ( v.Kind )
		{
			case ValueKind.Undefined: sb.Append( "{\"$u\":1}" ); return;
			case ValueKind.Null: sb.Append( "null" ); return;
			case ValueKind.Bool: sb.Append( v.BoolValue ? "true" : "false" ); return;
			case ValueKind.Number:
				if ( double.IsNaN( v.Num ) || double.IsInfinity( v.Num ) ) { sb.Append( "{\"$n\":\"" ).Append( Value.NumberToString( v.Num ) ).Append( "\"}" ); return; }
				sb.Append( v.Num.ToString( "R", CultureInfo.InvariantCulture ) ); return;
			case ValueKind.String: Str( v.Str, sb ); return;
		}
		var o = v.Obj;
		if ( o is AsFunction || o is DisplayObject || !seen.Add( o ) ) { sb.Append( "{\"$u\":1}" ); return; }
		if ( o is AsArray arr )
		{
			sb.Append( '[' );
			for ( int i = 0; i < arr.Items.Count; i++ ) { if ( i > 0 ) sb.Append( ',' ); ToJson( vm, arr.Items[i], sb, seen ); }
			sb.Append( ']' );
		}
		else
		{
			sb.Append( '{' );
			bool first = true;
			foreach ( var k in o.OwnKeys )
			{
				if ( !first ) sb.Append( ',' );
				first = false;
				Str( k, sb ); sb.Append( ':' );
				ToJson( vm, o.Get( k, vm ), sb, seen );
			}
			sb.Append( '}' );
		}
		seen.Remove( o );
	}

	static void Str( string s, StringBuilder sb )
	{
		sb.Append( '"' );
		foreach ( var c in s )
		{
			switch ( c )
			{
				case '"': sb.Append( "\\\"" ); break;
				case '\\': sb.Append( "\\\\" ); break;
				case '\n': sb.Append( "\\n" ); break;
				case '\r': sb.Append( "\\r" ); break;
				case '\t': sb.Append( "\\t" ); break;
				default:
					if ( c < 32 ) sb.Append( "\\u" ).Append( ((int)c).ToString( "x4" ) );
					else sb.Append( c );
					break;
			}
		}
		sb.Append( '"' );
	}

	static Value FromJson( Avm1 vm, object j )
	{
		switch ( j )
		{
			case null: return Value.Null;
			case bool b: return b;
			case double d: return d;
			case string s: return s;
			case List<object> l:
				{
					var arr = vm.NewArray();
					foreach ( var x in l ) arr.Items.Add( FromJson( vm, x ) );
					return arr;
				}
			case Dictionary<string, object> dict:
				{
					if ( dict.Count == 1 && dict.ContainsKey( "$u" ) ) return Value.Undefined;
					if ( dict.Count == 1 && dict.TryGetValue( "$n", out var n ) ) return Value.StringToNumber( (string)n, 7 );
					var o = vm.NewObject();
					foreach ( var kv in dict ) o.Set( kv.Key, FromJson( vm, kv.Value ), vm );
					return o;
				}
		}
		return Value.Undefined;
	}

	// ========================================================================= AsBroadcaster

	static void InstallBroadcaster( Avm1 vm )
	{
		var b = vm.NewObject();
		vm.Global.DefineHidden( "AsBroadcaster", Value.From( b ) );
		vm.Method( b, "initialize", ( vm, t, a ) => { if ( Arg( a, 0 ).AsObj is AsObject o ) MakeBroadcaster( vm, o ); return Value.Undefined; } );
	}

	static void MakeBroadcaster( Avm1 vm, AsObject o )
	{
		o.DefineHidden( "_listeners", Value.From( vm.NewArray() ) );
		vm.Method( o, "addListener", ( vm, t, a ) =>
		{
			var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
			if ( l == null ) return false;
			var x = Arg( a, 0 );
			l.Items.RemoveAll( v => vm.StrictEquals( v, x ) );
			l.Items.Add( x );
			return true;
		} );
		vm.Method( o, "removeListener", ( vm, t, a ) =>
		{
			var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
			if ( l == null ) return false;
			var x = Arg( a, 0 );
			return l.Items.RemoveAll( v => vm.StrictEquals( v, x ) ) > 0;
		} );
		vm.Method( o, "broadcastMessage", ( vm, t, a ) =>
		{
			var l = vm.GetMember( t, "_listeners" ).AsObj as AsArray;
			if ( l == null || a.Length == 0 ) return Value.Undefined;
			var name = Arg( a, 0 ).ToStr( vm );
			var rest = a[1..];
			foreach ( var x in l.Items.ToArray() )
			{
				var f = vm.GetMember( x, name ).AsObj as AsFunction;
				if ( f != null ) vm.CallFunction( f, x, rest );
			}
			return l.Items.Count > 0;
		} );
	}
}