Flash/Core/Avm1.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace Flash;

/// <summary>A value thrown by ActionScript (throw x;).</summary>
public sealed class AsThrow : Exception
{
	public readonly Value Thrown;
	public AsThrow( Value v ) : base( "ActionScript exception" ) { Thrown = v; }
}

public enum ScopeClass : byte { Global, Target, Local, With }

public sealed class Scope
{
	public readonly AsObject Values;
	public readonly ScopeClass Class;
	public readonly Scope Parent;
	public Scope( AsObject values, ScopeClass cls, Scope parent ) { Values = values; Class = cls; Parent = parent; }
}

/// <summary>The ActionScript 1/2 virtual machine (AVM1).</summary>
public sealed partial class Avm1
{
	public readonly Player Player;
	public int SwfVersion = 11;
	public AsObject Global;
	public AsObject ObjectProto, FunctionProto, ArrayProto, StringProto, NumberProto, BooleanProto, DateProto;
	public AsObject MovieClipProto, TextFieldProto, ButtonProto, SoundProto, ColorProto, SharedObjectProto;
	public NativeFunction ObjectCtor, FunctionCtor, ArrayCtor;
	public Random Rng = new();
	public readonly List<string> Log = new();
	int callDepth;
	const int MaxCallDepth = 256;
	const long MaxActionsPerCall = 2_000_000;
	long actionBudget;

	public Avm1( Player player )
	{
		Player = player;
		Builtins.Install( this );
	}

	public void Trace( string s )
	{
		Log.Add( s );
		if ( Log.Count > 500 ) Log.RemoveAt( 0 );
		Player.Host?.Trace( s );
	}

	// ======================================================================= object helpers

	public AsObject NewObject( AsObject proto = null ) => new AsObject( proto ?? ObjectProto );

	public AsArray NewArray( IEnumerable<Value> items = null )
	{
		var a = new AsArray( ArrayProto );
		if ( items != null ) a.Items.AddRange( items );
		return a;
	}

	public NativeFunction Native( string name, NativeCall call ) => new NativeFunction( FunctionProto, name, call );

	public void Method( AsObject target, string name, NativeCall call ) => target.DefineHidden( name, Value.From( Native( name, call ) ) );

	/// <summary>Turns a primitive into an object for property access / method calls.</summary>
	public AsObject ToObject( Value v )
	{
		switch ( v.Kind )
		{
			case ValueKind.Object: return v.Obj;
			case ValueKind.String: return new AsBoxed( StringProto, v );
			case ValueKind.Number: return new AsBoxed( NumberProto, v );
			case ValueKind.Bool: return new AsBoxed( BooleanProto, v );
			default: return null;
		}
	}

	public Value GetMember( Value objV, string name )
	{
		if ( objV.IsNullish ) return Value.Undefined;
		if ( objV.IsString && name == "length" ) return objV.Str.Length;
		var o = objV.IsObject ? objV.Obj : ToObject( objV );
		if ( o is null ) return Value.Undefined;
		return o.GetWithThis( name, this, objV.IsObject ? objV.Obj : o );
	}

	public void SetMember( Value objV, string name, Value v )
	{
		if ( !objV.IsObject ) return;
		objV.Obj.Set( name, v, this );
	}

	// ======================================================================= calling

	public Value CallFunction( AsFunction f, Value thisV, Value[] args )
	{
		if ( f is null ) return Value.Undefined;
		return f.Call( this, thisV, args ?? Array.Empty<Value>() );
	}

	public Value CallMethod( Value objV, string name, params Value[] args )
	{
		var f = GetMember( objV, name ).AsObj as AsFunction;
		if ( f is null ) return Value.Undefined;
		return CallFunction( f, objV, args );
	}

	public Value Construct( AsFunction ctor, Value[] args )
	{
		if ( ctor is null ) return Value.Undefined;
		return ctor.Construct( this, args );
	}

	/// <summary>Runs a frame script / clip event / button action on a timeline.</summary>
	public void RunScript( DisplayObject clip, byte[] code, int start = 0, int length = -1, Value? thisOverride = null )
	{
		if ( code is null || code.Length == 0 ) return;
		var scope = new Scope( clip, ScopeClass.Target, new Scope( Global, ScopeClass.Global, null ) );
		var ctx = new Ctx
		{
			Code = code, Scope = scope, This = thisOverride ?? Value.From( clip ), Target = clip, BaseClip = clip,
			Regs = new Value[4], Pool = Array.Empty<string>(), Version = SwfVersion,
		};
		actionBudget = MaxActionsPerCall;
		try
		{
			callDepth++;
			RunRange( ctx, start, length < 0 ? code.Length : start + length );
		}
		catch ( AsThrow t )
		{
			Trace( "Uncaught exception: " + t.Thrown.ToStr( this ) );
		}
		catch ( ScriptAbort a )
		{
			Trace( "Script aborted: " + a.Message );
		}
		finally { callDepth--; }
	}

	public Value InvokeBytecode( BytecodeFunction f, Value thisV, Value[] args, AsObject superOwner )
	{
		if ( callDepth >= MaxCallDepth ) throw new ScriptAbort( "stack overflow in " + f.Name );
		var activation = new AsObject( null );
		var scope = new Scope( activation, ScopeClass.Local, f.Scope );
		var ctx = new Ctx
		{
			Code = f.Code, Scope = scope, This = thisV, BaseClip = f.BaseClip, Target = f.BaseClip, Pool = f.ConstantPool,
			Version = f.SwfVersion, Func = f, Args = args,
			Regs = new Value[f.IsFunction2 ? Math.Max( f.RegisterCount, 1 ) : 4],
		};
		if ( ctx.Target != null && ctx.Target.Removed && thisV.Obj is DisplayObject td && !td.Removed ) { ctx.Target = td; ctx.BaseClip = td; }

		var argsObj = NewArray( args );
		argsObj.DefineHidden( "callee", Value.From( f ) );
		argsObj.DefineHidden( "caller", Value.Null );

		AsObject superOwnerProto = null;
		if ( thisV.IsObject ) superOwnerProto = superOwner ?? thisV.Obj.Proto;
		SuperObject superObj = superOwnerProto != null ? new SuperObject( this, thisV, superOwnerProto ) : null;

		if ( f.IsFunction2 )
		{
			ushort fl = f.Flags2;
			int r = 1;
			// flag bits: 0 preloadThis,1 suppressThis,2 preloadArgs,3 suppressArgs,4 preloadSuper,5 suppressSuper,6 preloadRoot,7 preloadParent,8 preloadGlobal
			if ( (fl & 0x01) != 0 ) ctx.Regs[r++] = thisV;
			if ( (fl & 0x04) != 0 ) ctx.Regs[r++] = Value.From( argsObj );
			if ( (fl & 0x10) != 0 ) ctx.Regs[r++] = superObj != null ? Value.From( superObj ) : Value.Undefined;
			if ( (fl & 0x40) != 0 ) ctx.Regs[r++] = Value.From( Player.Root );
			if ( (fl & 0x80) != 0 ) ctx.Regs[r++] = f.BaseClip?.Parent != null ? Value.From( f.BaseClip.Parent ) : Value.Undefined;
			if ( (fl & 0x100) != 0 ) ctx.Regs[r++] = Value.From( Global );
			if ( (fl & 0x02) == 0 ) activation.SetOwn( "this", thisV );
			if ( (fl & 0x08) == 0 ) activation.SetOwn( "arguments", Value.From( argsObj ) );
			if ( (fl & 0x20) == 0 && superObj != null ) activation.SetOwn( "super", Value.From( superObj ) );
			for ( int i = 0; i < f.ParamNames.Length; i++ )
			{
				var v = i < args.Length ? args[i] : Value.Undefined;
				int reg = f.ParamRegisters[i];
				if ( reg != 0 && reg < ctx.Regs.Length ) ctx.Regs[reg] = v;
				else activation.SetOwn( f.ParamNames[i], v );
			}
		}
		else
		{
			activation.SetOwn( "this", thisV );
			activation.SetOwn( "arguments", Value.From( argsObj ) );
			if ( superObj != null ) activation.SetOwn( "super", Value.From( superObj ) );
			for ( int i = 0; i < f.ParamNames.Length; i++ )
				activation.SetOwn( f.ParamNames[i], i < args.Length ? args[i] : Value.Undefined );
		}

		callDepth++;
		try
		{
			var c = RunRange( ctx, f.Start, f.Start + f.Length );
			return c.Kind == Completion.Return ? c.Value : Value.Undefined;
		}
		finally { callDepth--; }
	}

	// ======================================================================= interpreter

	sealed class Ctx
	{
		public byte[] Code;
		public Scope Scope;
		public Value This;
		public DisplayObject Target, BaseClip;
		public string[] Pool;
		public Value[] Regs;
		public int Version;
		public BytecodeFunction Func;
		public Value[] Args;
		public readonly List<Value> Stack = new( 16 );
	}

	enum Completion { Normal, Return, Jump }

	readonly struct Result
	{
		public readonly Completion Kind; public readonly Value Value; public readonly int JumpTo;
		public Result( Completion k, Value v = default, int jump = 0 ) { Kind = k; Value = v; JumpTo = jump; }
	}

	static Value Pop( Ctx c )
	{
		var s = c.Stack;
		if ( s.Count == 0 ) return Value.Undefined;
		var v = s[s.Count - 1];
		s.RemoveAt( s.Count - 1 );
		return v;
	}

	static Value Peek( Ctx c ) => c.Stack.Count == 0 ? Value.Undefined : c.Stack[c.Stack.Count - 1];

	static void Push( Ctx c, Value v ) => c.Stack.Add( v );

	static ushort U16( byte[] b, int i ) => (ushort)(b[i] | (b[i + 1] << 8));
	static short S16( byte[] b, int i ) => (short)(b[i] | (b[i + 1] << 8));
	static int I32( byte[] b, int i ) => b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24);

	static string CStr( byte[] b, ref int i, int end )
	{
		int s = i;
		while ( i < end && b[i] != 0 ) i++;
		var str = Encoding.UTF8.GetString( b, s, i - s );
		i++; // skip NUL
		return str;
	}

	Value[] PopArgs( Ctx c, int n )
	{
		n = Math.Clamp( n, 0, c.Stack.Count );
		var a = new Value[n];
		for ( int i = 0; i < n; i++ ) a[i] = Pop( c );
		return a;
	}

	Result RunRange( Ctx c, int start, int end )
	{
		var code = c.Code;
		int pc = start;
		while ( pc < end && pc < code.Length )
		{
			if ( --actionBudget < 0 ) throw new ScriptAbort( "too many actions (infinite loop?)" );
			int op = code[pc];
			int len = 0;
			int body = pc + 1;
			if ( op >= 0x80 )
			{
				len = U16( code, pc + 1 );
				body = pc + 3;
			}
			int next = body + len;
			if ( op == 0 ) return new Result( Completion.Normal );

			switch ( op )
			{
				// -------------------------------------------------- timeline control (SWF 3)
				case 0x04: c.Target?.AsClip?.NextFrame(); break;
				case 0x05: c.Target?.AsClip?.PrevFrame(); break;
				case 0x06: c.Target?.AsClip?.Play(); break;
				case 0x07: c.Target?.AsClip?.Stop(); break;
				case 0x08: break; // toggle quality
				case 0x09: Player.StopAllSounds(); break;
				case 0x81: c.Target?.AsClip?.GotoFrame( U16( code, body ) + 1, stop: true ); break;
				case 0x8C: { int i = body; var label = CStr( code, ref i, next ); c.Target?.AsClip?.GotoLabel( label, stop: true ); break; }
				case 0x8A: break; // WaitForFrame: everything is loaded
				case 0x8D: Pop( c ); break; // WaitForFrame2
				case 0x8B:
					{
						int i = body; var t = CStr( code, ref i, next );
						c.Target = t.Length == 0 ? c.BaseClip : ResolveTargetPath( c, t ) ?? c.Target;
						break;
					}
				case 0x20:
					{
						var tv = Pop( c );
						if ( tv.IsObject && tv.Obj is DisplayObject d ) c.Target = d;
						else
						{
							var t = tv.ToStr( this );
							c.Target = t.Length == 0 ? c.BaseClip : ResolveTargetPath( c, t ) ?? c.BaseClip;
						}
						break;
					}
				case 0x83:
					{
						int i = body; var url = CStr( code, ref i, next ); var win = CStr( code, ref i, next );
						Player.GetUrl( url, win );
						break;
					}
				case 0x9A:
					{
						var win = Pop( c ); var url = Pop( c );
						int flags = code[body];
						if ( (flags & 0x40) != 0 )
						{
							// loadMovie / loadVariables into a target
							Player.GetUrl( url.ToStr( this ), win.ToStr( this ) );
						}
						else Player.GetUrl( url.ToStr( this ), win.ToStr( this ) );
						break;
					}
				case 0x9F:
					{
						int flags = code[body];
						int bias = (flags & 0x02) != 0 ? U16( code, body + 1 ) : 0;
						bool play = (flags & 0x01) != 0;
						var fv = Pop( c );
						GotoFrameValue( c, fv, bias, !play );
						break;
					}
				case 0x9E: // Call (frame actions)
					{
						var fv = Pop( c ).ToStr( this );
						CallFrame( c, fv );
						break;
					}

				// -------------------------------------------------- stack / arithmetic
				case 0x96: ActionPush( c, body, next ); break;
				case 0x17: Pop( c ); break;
				case 0x4C: Push( c, Peek( c ) ); break;
				case 0x4D: { var a = Pop( c ); var b = Pop( c ); Push( c, a ); Push( c, b ); break; }
				case 0x0A: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a + b ); break; }
				case 0x0B: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a - b ); break; }
				case 0x0C: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a * b ); break; }
				case 0x0D:
					{
						var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this );
						if ( b == 0 && c.Version < 5 ) Push( c, "#ERROR#" ); else Push( c, a / b );
						break;
					}
				case 0x3F: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a % b ); break; }
				case 0x0E: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a == b ); break; }
				case 0x0F: { var b = Pop( c ).ToNumber( this ); var a = Pop( c ).ToNumber( this ); Push( c, a < b ); break; }
				case 0x10: { var b = Pop( c ).ToBool( c.Version ); var a = Pop( c ).ToBool( c.Version ); Push( c, a && b ); break; }
				case 0x11: { var b = Pop( c ).ToBool( c.Version ); var a = Pop( c ).ToBool( c.Version ); Push( c, a || b ); break; }
				case 0x12: { var a = Pop( c ).ToBool( c.Version ); Push( c, !a ); break; }
				case 0x13: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, a == b ); break; }
				case 0x14: Push( c, Pop( c ).ToStr( this ).Length ); break;
				case 0x31: Push( c, Pop( c ).ToStr( this ).Length ); break;
				case 0x15:
				case 0x35:
					{
						int count = Pop( c ).ToInt32( this ); int index = Pop( c ).ToInt32( this ) - 1; var s = Pop( c ).ToStr( this );
						if ( index < 0 ) index = 0;
						if ( index >= s.Length ) { Push( c, "" ); break; }
						if ( count < 0 || index + count > s.Length ) count = s.Length - index;
						Push( c, s.Substring( index, count ) );
						break;
					}
				case 0x21: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, a + b ); break; }
				case 0x29: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, string.CompareOrdinal( a, b ) < 0 ); break; }
				case 0x68: { var b = Pop( c ).ToStr( this ); var a = Pop( c ).ToStr( this ); Push( c, string.CompareOrdinal( a, b ) > 0 ); break; }
				case 0x18: Push( c, (double)Value.ToInt32( Math.Truncate( Pop( c ).ToNumber( this ) ) ) ); break;
				case 0x32: case 0x36: { var s = Pop( c ).ToStr( this ); Push( c, s.Length > 0 ? s[0] : 0 ); break; }
				case 0x33: case 0x37: { int n = Pop( c ).ToInt32( this ); Push( c, n == 0 ? "" : ((char)n).ToString() ); break; }
				case 0x30: { int max = Pop( c ).ToInt32( this ); Push( c, max <= 0 ? 0 : Rng.Next( max ) ); break; }
				case 0x34: Push( c, Player.TimeMs ); break;
				case 0x26: Trace( Pop( c ).ToStr( this ) ); break;
				case 0x47:
					{
						var b = Pop( c ); var a = Pop( c );
						var pa = a.IsObject ? a.Obj.ToPrimitive( this, false ) : a;
						var pb = b.IsObject ? b.Obj.ToPrimitive( this, false ) : b;
						if ( a.IsObject && a.Obj is DisplayObject ) pa = a.ToStr( this );
						if ( b.IsObject && b.Obj is DisplayObject ) pb = b.ToStr( this );
						if ( pa.IsString || pb.IsString ) Push( c, pa.ToStr( this ) + pb.ToStr( this ) );
						else Push( c, pa.ToNumber( this ) + pb.ToNumber( this ) );
						break;
					}
				case 0x48: { var b = Pop( c ); var a = Pop( c ); Push( c, LessThan( a, b ) ); break; }
				case 0x67: { var b = Pop( c ); var a = Pop( c ); Push( c, LessThan( b, a ) ); break; }
				case 0x49: { var b = Pop( c ); var a = Pop( c ); Push( c, AbstractEquals( a, b ) ); break; }
				case 0x66: { var b = Pop( c ); var a = Pop( c ); Push( c, StrictEquals( a, b ) ); break; }
				case 0x4A: Push( c, Pop( c ).ToNumber( this ) ); break;
				case 0x4B: Push( c, Pop( c ).ToStr( this ) ); break;
				case 0x50: Push( c, Pop( c ).ToNumber( this ) + 1 ); break;
				case 0x51: Push( c, Pop( c ).ToNumber( this ) - 1 ); break;
				case 0x60: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a & b ); break; }
				case 0x61: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a | b ); break; }
				case 0x62: { var b = Pop( c ).ToInt32( this ); var a = Pop( c ).ToInt32( this ); Push( c, a ^ b ); break; }
				case 0x63: { var b = Pop( c ).ToInt32( this ) & 31; var a = Pop( c ).ToInt32( this ); Push( c, a << b ); break; }
				case 0x64: { var b = Pop( c ).ToInt32( this ) & 31; var a = Pop( c ).ToInt32( this ); Push( c, a >> b ); break; }
				case 0x65: { var b = Pop( c ).ToInt32( this ) & 31; var a = (uint)Pop( c ).ToInt32( this ); Push( c, (double)(a >> b) ); break; }
				case 0x44: Push( c, Pop( c ).TypeOf() ); break;
				case 0x45:
					{
						var v = Pop( c );
						Push( c, v.IsObject && v.Obj is DisplayObject d ? (Value)d.TargetPath : Value.Undefined );
						break;
					}

				// -------------------------------------------------- variables
				case 0x1C:
					{
						var name = Pop( c ).ToStr( this );
						Push( c, GetVariable( c, name ) );
						break;
					}
				case 0x1D:
					{
						var v = Pop( c ); var name = Pop( c ).ToStr( this );
						SetVariable( c, name, v );
						break;
					}
				case 0x3C:
					{
						var v = Pop( c ); var name = Pop( c ).ToStr( this );
						DefineLocal( c, name, v );
						break;
					}
				case 0x41:
					{
						var name = Pop( c ).ToStr( this );
						var local = LocalScope( c );
						if ( !local.Values.HasOwnProperty( name ) ) local.Values.SetOwn( name, Value.Undefined );
						break;
					}
				case 0x3A:
					{
						var name = Pop( c ).ToStr( this ); var obj = Pop( c );
						Push( c, obj.IsObject && obj.Obj.Delete( name ) );
						break;
					}
				case 0x3B:
					{
						var name = Pop( c ).ToStr( this );
						bool ok = false;
						for ( var s = c.Scope; s != null; s = s.Parent )
							if ( s.Values.HasOwnProperty( name ) ) { ok = s.Values.Delete( name ); break; }
						Push( c, ok );
						break;
					}
				case 0x4E:
					{
						var nameV = Pop( c ); var obj = Pop( c );
						Push( c, GetMember( obj, nameV.ToStr( this ) ) );
						break;
					}
				case 0x4F:
					{
						var v = Pop( c ); var nameV = Pop( c ); var obj = Pop( c );
						SetMember( obj, nameV.ToStr( this ), v );
						break;
					}
				case 0x22:
					{
						int idx = Pop( c ).ToInt32( this ); var tv = Pop( c );
						var d = TargetFromValue( c, tv );
						Push( c, d != null ? d.GetIndexedProperty( idx, this ) : Value.Undefined );
						break;
					}
				case 0x23:
					{
						var v = Pop( c ); int idx = Pop( c ).ToInt32( this ); var tv = Pop( c );
						var d = TargetFromValue( c, tv );
						d?.SetIndexedProperty( idx, v, this );
						break;
					}
				case 0x87:
					{
						int r = code[body];
						if ( r < c.Regs.Length ) c.Regs[r] = Peek( c );
						break;
					}
				case 0x88:
					{
						int n = U16( code, body ); int i = body + 2;
						var pool = new string[n];
						for ( int k = 0; k < n; k++ ) pool[k] = CStr( code, ref i, next );
						c.Pool = pool;
						break;
					}

				// -------------------------------------------------- objects
				case 0x40:
					{
						var name = Pop( c ).ToStr( this ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
						var ctor = GetVariable( c, name ).AsObj as AsFunction;
						Push( c, ctor != null ? Construct( ctor, args ) : Value.Undefined );
						break;
					}
				case 0x53:
					{
						var nameV = Pop( c ); var obj = Pop( c ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
						AsFunction ctor;
						if ( nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0) ) ctor = obj.AsObj as AsFunction;
						else ctor = GetMember( obj, nameV.ToStr( this ) ).AsObj as AsFunction;
						Push( c, ctor != null ? Construct( ctor, args ) : Value.Undefined );
						break;
					}
				case 0x42:
					{
						int n = Pop( c ).ToInt32( this );
						var arr = NewArray();
						for ( int i = 0; i < n; i++ ) arr.Items.Add( Pop( c ) );
						Push( c, arr );
						break;
					}
				case 0x43:
					{
						int n = Pop( c ).ToInt32( this );
						var obj = NewObject();
						for ( int i = 0; i < n; i++ ) { var v = Pop( c ); var k = Pop( c ).ToStr( this ); obj.Set( k, v, this ); }
						Push( c, obj );
						break;
					}
				case 0x3D:
					{
						var name = Pop( c ).ToStr( this ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
						var (holder, fv) = ResolveCallable( c, name );
						if ( fv.AsObj is AsFunction f )
						{
							var thisV = holder != null ? Value.From( holder ) : (c.Target != null ? Value.From( c.Target ) : Value.Undefined);
							if ( holder != null && LocalScopeHolds( c, holder ) ) thisV = c.Target != null ? Value.From( c.Target ) : Value.Undefined;
							Push( c, CallFunction( f, thisV, args ) );
						}
						else Push( c, Value.Undefined );
						if ( c.BaseClip != null && c.BaseClip.Removed && c.Func == null ) return new Result( Completion.Normal );
						break;
					}
				case 0x52:
					{
						var nameV = Pop( c ); var obj = Pop( c ); int n = Pop( c ).ToInt32( this ); var args = PopArgs( c, n );
						if ( obj.IsNullish ) { Push( c, Value.Undefined ); break; }
						if ( (nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0)) && obj.Obj is SuperObject sc )
						{
							Push( c, sc.CallConstructor( args ) );
							break;
						}
						if ( nameV.IsUndefined || (nameV.IsString && nameV.Str.Length == 0) )
						{
							// obj itself is the function
							if ( obj.AsObj is AsFunction f0 ) Push( c, CallFunction( f0, Value.Undefined, args ) );
							else Push( c, Value.Undefined );
							break;
						}
						var name = nameV.ToStr( this );
						if ( obj.IsObject && obj.Obj is SuperObject sup )
						{
							Push( c, sup.CallMethod( name, args ) );
							break;
						}
						var fv = GetMember( obj, name );
						if ( fv.AsObj is AsFunction f )
						{
							var thisV = obj.IsObject ? obj : Value.From( ToObject( obj ) );
							// for prototype methods, super inside should start above the prototype that owns the method
							Push( c, f.Call( this, thisV, args, FindOwner( obj, name ) ) );
						}
						else Push( c, Value.Undefined );
						if ( c.BaseClip != null && c.BaseClip.Removed && c.Func == null ) return new Result( Completion.Normal );
						break;
					}
				case 0x3E:
					return new Result( Completion.Return, Pop( c ) );
				case 0x54:
					{
						var ctor = Pop( c ); var obj = Pop( c );
						Push( c, obj.IsObject && ctor.AsObj is AsFunction && InstanceOf( obj.Obj, ctor.Obj ) );
						break;
					}
				case 0x2B:
					{
						var obj = Pop( c ); var ctor = Pop( c );
						Push( c, obj.IsObject && ctor.AsObj is AsFunction && InstanceOf( obj.Obj, ctor.Obj ) ? obj : Value.Null );
						break;
					}
				case 0x2C:
					{
						var ctor = Pop( c ); int n = Pop( c ).ToInt32( this ); var ifaces = PopArgs( c, n );
						var proto = ctor.AsObj?.Get( "prototype", this ).AsObj;
						if ( proto != null ) proto.Native = ifaces;
						break;
					}
				case 0x69:
					{
						var superCtor = Pop( c ).AsObj; var sub = Pop( c ).AsObj;
						if ( sub != null && superCtor != null )
						{
							var proto = NewObject( superCtor.Get( "prototype", this ).AsObj );
							proto.DefineHidden( "__constructor__", Value.From( superCtor ) );
							if ( SwfVersion >= 7 ) proto.DefineHidden( "constructor", Value.From( sub ) );
							sub.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum );
						}
						break;
					}
				case 0x46:
					{
						var name = Pop( c ).ToStr( this );
						var obj = GetVariable( c, name );
						Push( c, Value.Undefined );
						if ( obj.IsObject ) foreach ( var k in obj.Obj.EnumerableKeys() ) Push( c, k );
						break;
					}
				case 0x55:
					{
						var obj = Pop( c );
						Push( c, Value.Undefined );
						if ( obj.IsObject ) foreach ( var k in obj.Obj.EnumerableKeys() ) Push( c, k );
						break;
					}
				case 0x2A:
					throw new AsThrow( Pop( c ) );

				// -------------------------------------------------- functions & blocks
				case 0x9B:
					{
						int i = body;
						var name = CStr( code, ref i, next );
						int np = U16( code, i ); i += 2;
						var ps = new string[np];
						for ( int k = 0; k < np; k++ ) ps[k] = CStr( code, ref i, next );
						int size = U16( code, i );
						var f = MakeFunction( c, name, ps, null, false, 0, 0, next, size );
						next += size;
						if ( name.Length > 0 ) SetVariable( c, name, f );
						else Push( c, f );
						break;
					}
				case 0x8E:
					{
						int i = body;
						var name = CStr( code, ref i, next );
						int np = U16( code, i ); i += 2;
						int regCount = code[i++];
						ushort flags = (ushort)(code[i] | (code[i + 1] << 8)); i += 2;
						var ps = new string[np]; var pr = new byte[np];
						for ( int k = 0; k < np; k++ ) { pr[k] = code[i++]; ps[k] = CStr( code, ref i, next ); }
						int size = U16( code, i );
						var f = MakeFunction( c, name, ps, pr, true, regCount, flags, next, size );
						next += size;
						if ( name.Length > 0 ) SetVariable( c, name, f );
						else Push( c, f );
						break;
					}
				case 0x94:
					{
						int size = U16( code, body );
						var obj = Pop( c );
						int blockStart = next;
						int blockEnd = next + size;
						var withObj = obj.IsObject ? obj.Obj : ToObject( obj );
						var saved = c.Scope;
						if ( withObj != null ) c.Scope = new Scope( withObj, ScopeClass.With, c.Scope );
						Result r;
						try { r = RunRange( c, blockStart, blockEnd ); }
						finally { c.Scope = saved; }
						if ( r.Kind == Completion.Return ) return r;
						if ( r.Kind == Completion.Jump )
						{
							if ( r.JumpTo >= start && r.JumpTo < end ) { pc = r.JumpTo; continue; }
							if ( r.JumpTo == end ) return new Result( Completion.Normal );
							return r;
						}
						next = blockEnd;
						break;
					}
				case 0x8F:
					{
						int i = body;
						int flags = code[i++];
						int trySize = U16( code, i ); i += 2;
						int catchSize = U16( code, i ); i += 2;
						int finallySize = U16( code, i ); i += 2;
						bool catchInReg = (flags & 4) != 0;
						string catchName = null; int catchReg = 0;
						if ( catchInReg ) catchReg = code[i++]; else catchName = CStr( code, ref i, next );
						int tryStart = next, catchStart = tryStart + trySize, finStart = catchStart + catchSize, after = finStart + finallySize;
						Result r = new( Completion.Normal );
						try
						{
							r = RunRange( c, tryStart, catchStart );
						}
						catch ( AsThrow t ) when ( (flags & 1) != 0 )
						{
							if ( catchInReg ) { if ( catchReg < c.Regs.Length ) c.Regs[catchReg] = t.Thrown; }
							else LocalScope( c ).Values.SetOwn( catchName, t.Thrown );
							r = RunRange( c, catchStart, finStart );
						}
						finally
						{
							if ( (flags & 2) != 0 && finallySize > 0 )
							{
								var fr = RunRange( c, finStart, after );
								if ( fr.Kind == Completion.Return ) r = fr;
							}
						}
						if ( r.Kind == Completion.Return ) return r;
						if ( r.Kind == Completion.Jump )
						{
							if ( r.JumpTo >= start && r.JumpTo < end ) { pc = r.JumpTo; continue; }
							if ( r.JumpTo == end ) return new Result( Completion.Normal );
							return r;
						}
						next = after;
						break;
					}
				case 0x99:
					{
						int target = next + S16( code, body );
						if ( target == end ) return new Result( Completion.Normal );
						if ( target < start || target > end ) return new Result( Completion.Jump, default, target );
						pc = target;
						continue;
					}
				case 0x9D:
					{
						int target = next + S16( code, body );
						if ( Pop( c ).ToBool( c.Version ) )
						{
							if ( target == end ) return new Result( Completion.Normal );
							if ( target < start || target > end ) return new Result( Completion.Jump, default, target );
							pc = target;
							continue;
						}
						break;
					}

				// -------------------------------------------------- movie clips
				case 0x24:
					{
						int depth = Pop( c ).ToInt32( this ); var tname = Pop( c ).ToStr( this ); var src = Pop( c );
						var srcClip = TargetFromValue( c, src );
						srcClip?.Duplicate( tname, depth + 0 );
						break;
					}
				case 0x25:
					{
						var tv = Pop( c );
						var d = TargetFromValue( c, tv );
						d?.RemoveFromScript();
						break;
					}
				case 0x27:
					{
						var target = Pop( c ); bool lockCenter = Pop( c ).ToBool( c.Version ); bool constrain = Pop( c ).ToBool( c.Version );
						double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
						if ( constrain ) { y2 = Pop( c ).ToNumber( this ); x2 = Pop( c ).ToNumber( this ); y1 = Pop( c ).ToNumber( this ); x1 = Pop( c ).ToNumber( this ); }
						var d = TargetFromValue( c, target );
						if ( d != null ) Player.StartDrag( d, lockCenter, constrain ? new double[] { x1, y1, x2, y2 } : null );
						break;
					}
				case 0x28: Player.StopDrag(); break;

				default:
					// unknown / unsupported action: skip
					break;
			}
			pc = next;
		}
		return new Result( Completion.Normal );
	}

	void ActionPush( Ctx c, int i, int end )
	{
		var code = c.Code;
		while ( i < end )
		{
			int type = code[i++];
			switch ( type )
			{
				case 0: Push( c, CStr( code, ref i, end ) ); break;
				case 1: Push( c, (double)BitConverter.Int32BitsToSingle( I32( code, i ) ) ); i += 4; break;
				case 2: Push( c, Value.Null ); break;
				case 3: Push( c, Value.Undefined ); break;
				case 4: { int r = code[i++]; Push( c, r < c.Regs.Length ? c.Regs[r] : Value.Undefined ); break; }
				case 5: Push( c, code[i++] != 0 ); break;
				case 6:
					{
						long hi = (uint)I32( code, i ); long lo = (uint)I32( code, i + 4 );
						Push( c, BitConverter.Int64BitsToDouble( (hi << 32) | lo ) );
						i += 8;
						break;
					}
				case 7: Push( c, (double)I32( code, i ) ); i += 4; break;
				case 8: { int k = code[i++]; Push( c, k < c.Pool.Length ? (Value)c.Pool[k] : Value.Undefined ); break; }
				case 9: { int k = U16( code, i ); i += 2; Push( c, k < c.Pool.Length ? (Value)c.Pool[k] : Value.Undefined ); break; }
				default: return;
			}
		}
	}

	BytecodeFunction MakeFunction( Ctx c, string name, string[] ps, byte[] regs, bool f2, int regCount, ushort flags, int start, int size )
	{
		var f = new BytecodeFunction( FunctionProto )
		{
			Name = name, Code = c.Code, Start = start, Length = size, ParamNames = ps, ParamRegisters = regs ?? new byte[ps.Length],
			IsFunction2 = f2, RegisterCount = regCount, Flags2 = flags, ConstantPool = c.Pool, Scope = c.Scope,
			BaseClip = c.BaseClip ?? Player.Root, SwfVersion = c.Version,
		};
		var proto = NewObject();
		proto.DefineHidden( "constructor", Value.From( f ) );
		f.SetOwn( "prototype", Value.From( proto ), PropFlags.DontEnum );
		return f;
	}

	// ======================================================================= variable resolution

	Scope LocalScope( Ctx c )
	{
		for ( var s = c.Scope; s != null; s = s.Parent )
			if ( s.Class == ScopeClass.Local || s.Class == ScopeClass.Target ) return s;
		return c.Scope;
	}

	bool LocalScopeHolds( Ctx c, AsObject holder )
	{
		for ( var s = c.Scope; s != null; s = s.Parent )
			if ( s.Class == ScopeClass.Local && ReferenceEquals( s.Values, holder ) ) return true;
		return false;
	}

	static bool IsPath( string name )
	{
		foreach ( var ch in name ) if ( ch == '/' || ch == ':' || ch == '.' ) return true;
		return false;
	}

	Value GetVariable( Ctx c, string name )
	{
		if ( name.Length > 0 && IsPath( name ) && !char.IsDigit( name[0] ) )
		{
			if ( TryResolvePathVariable( c, name, out var pv ) ) return pv;
		}
		var (holder, v) = ResolveCallable( c, name );
		return v;
	}

	(AsObject holder, Value value) ResolveCallable( Ctx c, string name )
	{
		switch ( name )
		{
			case "this": return (null, c.This);
			case "_root": return (null, Value.From( Player.Root ));
			case "_global": return (null, Value.From( Global ));
			case "_parent":
				{
					// _parent resolves on the current target unless shadowed by a local
					break;
				}
		}
		for ( var s = c.Scope; s != null; s = s.Parent )
		{
			var o = s.Values;
			if ( o is DisplayObject d && d.Removed ) o = c.Target ?? Player.Root;
			if ( o.Has( name ) ) return (o, o.Get( name, this ));
		}
		if ( name == "_parent" ) return (null, c.Target?.Parent != null ? Value.From( c.Target.Parent ) : Value.Undefined);
		if ( name.StartsWith( "_level" ) ) return (null, Value.From( Player.Root ));
		return (null, Value.Undefined);
	}

	void SetVariable( Ctx c, string name, Value v )
	{
		if ( name.Length > 0 && IsPath( name ) && !char.IsDigit( name[0] ) )
		{
			if ( TrySetPathVariable( c, name, v ) ) return;
		}
		for ( var s = c.Scope; s != null; s = s.Parent )
		{
			var o = s.Values;
			if ( o is DisplayObject d && d.Removed ) continue;
			if ( s.Class == ScopeClass.Target || o.Has( name ) || s.Parent == null )
			{
				o.Set( name, v, this );
				return;
			}
		}
	}

	void DefineLocal( Ctx c, string name, Value v )
	{
		// in a with block, an existing property on the with target wins
		for ( var s = c.Scope; s != null; s = s.Parent )
		{
			if ( s.Class == ScopeClass.With )
			{
				if ( s.Values.Has( name ) ) { s.Values.Set( name, v, this ); return; }
				continue;
			}
			s.Values.SetOwn( name, v );
			return;
		}
	}

	AsObject FindOwner( Value obj, string name )
	{
		if ( !obj.IsObject ) return null;
		var o = obj.Obj; int g = 0;
		while ( o != null && g++ < 64 )
		{
			if ( o.GetOwnProperty( name ) != null ) return ReferenceEquals( o, obj.Obj ) ? null : o;
			o = o.Proto;
		}
		return null;
	}

	public bool InstanceOf( AsObject obj, AsObject ctor )
	{
		var proto = ctor.Get( "prototype", this ).AsObj;
		if ( proto is null ) return false;
		var o = obj.Proto; int g = 0;
		while ( o != null && g++ < 128 )
		{
			if ( ReferenceEquals( o, proto ) ) return true;
			if ( o.Native is Value[] ifaces )
				foreach ( var i in ifaces ) if ( i.AsObj != null && ReferenceEquals( i.Obj, ctor ) ) return true;
			o = o.Proto;
		}
		return false;
	}

	// ======================================================================= target paths

	DisplayObject TargetFromValue( Ctx c, Value v )
	{
		if ( v.IsObject && v.Obj is DisplayObject d ) return d;
		var s = v.ToStr( this );
		if ( s.Length == 0 ) return c.Target;
		return ResolveTargetPath( c, s );
	}

	public DisplayObject ResolveTargetPath( DisplayObject start, string path ) => ResolveTargetPath( new Ctx { Target = start, BaseClip = start, This = Value.From( start ) }, path );

	DisplayObject ResolveTargetPath( Ctx c, string path )
	{
		if ( string.IsNullOrEmpty( path ) ) return c.Target;
		DisplayObject cur = c.Target ?? Player.Root;
		int i = 0;
		if ( path[0] == '/' ) { cur = Player.Root; i = 1; }
		var parts = path.Substring( i ).Split( '/', '.' );
		bool first = true;
		foreach ( var raw in parts )
		{
			var p = raw;
			if ( p.Length == 0 ) continue;
			if ( p == ".." || p == "_parent" ) { cur = cur?.Parent; }
			else if ( p == "_root" || p.StartsWith( "_level" ) ) cur = Player.Root;
			else if ( p == "this" ) { cur = c.This.Obj as DisplayObject ?? cur; }
			else if ( first && p != null && c.Scope != null && !(cur is MovieClip mc0 && mc0.ChildByName( p ) != null) )
			{
				// first segment can be a variable holding a clip
				var v = GetVariableNoPath( c, p );
				if ( v.IsObject && v.Obj is DisplayObject vd ) cur = vd;
				else cur = (cur as MovieClip)?.ChildByName( p );
			}
			else
			{
				var mc = cur as MovieClip;
				if ( mc is null ) return null;
				var child = mc.ChildByName( p );
				if ( child == null )
				{
					var v = mc.Get( p, this );
					child = v.Obj as DisplayObject;
				}
				cur = child;
			}
			first = false;
			if ( cur is null ) return null;
		}
		return cur;
	}

	Value GetVariableNoPath( Ctx c, string name ) => ResolveCallable( c, name ).value;

	bool TryResolvePathVariable( Ctx c, string name, out Value v )
	{
		v = Value.Undefined;
		int colon = name.LastIndexOf( ':' );
		string targetPath, varName;
		if ( colon >= 0 ) { targetPath = name.Substring( 0, colon ); varName = name.Substring( colon + 1 ); }
		else
		{
			int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( '/' ) );
			if ( dot < 0 ) return false;
			targetPath = name.Substring( 0, dot ); varName = name.Substring( dot + 1 );
			if ( name.IndexOf( '/' ) >= 0 && name.IndexOf( '.' ) < 0 && name.IndexOf( ':' ) < 0 )
			{
				// "/a/b" is a clip path
				var d = ResolveTargetPath( c, name );
				if ( d != null ) { v = Value.From( d ); return true; }
			}
		}
		var start = targetPath.Length == 0 ? c.Target : ResolveTargetPath( c, targetPath );
		if ( start == null )
		{
			// maybe an object path like a.b.c on plain objects
			var parts = name.Split( '.' );
			var cur = GetVariableNoPath( c, parts[0] );
			for ( int k = 1; k < parts.Length; k++ ) cur = GetMember( cur, parts[k] );
			v = cur;
			return true;
		}
		v = start.Get( varName, this );
		return true;
	}

	bool TrySetPathVariable( Ctx c, string name, Value v )
	{
		int colon = name.LastIndexOf( ':' );
		string targetPath, varName;
		if ( colon >= 0 ) { targetPath = name.Substring( 0, colon ); varName = name.Substring( colon + 1 ); }
		else
		{
			int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( '/' ) );
			if ( dot < 0 ) return false;
			targetPath = name.Substring( 0, dot ); varName = name.Substring( dot + 1 );
		}
		var start = targetPath.Length == 0 ? c.Target : ResolveTargetPath( c, targetPath );
		if ( start == null )
		{
			var parts = name.Split( '.' );
			var cur = GetVariableNoPath( c, parts[0] );
			for ( int k = 1; k < parts.Length - 1; k++ ) cur = GetMember( cur, parts[k] );
			if ( cur.IsObject ) { cur.Obj.Set( parts[^1], v, this ); return true; }
			return false;
		}
		start.Set( varName, v, this );
		return true;
	}

	// ======================================================================= frames

	void GotoFrameValue( Ctx c, Value fv, int bias, bool stop )
	{
		var target = c.Target?.AsClip;
		if ( fv.IsString )
		{
			var s = fv.Str;
			int colon = s.LastIndexOf( ':' );
			if ( colon >= 0 )
			{
				target = ResolveTargetPath( c, s.Substring( 0, colon ) )?.AsClip;
				s = s.Substring( colon + 1 );
			}
			if ( target == null ) return;
			var n = Value.StringToNumber( s, SwfVersion );
			if ( !double.IsNaN( n ) ) target.GotoFrame( (int)n + bias, stop );
			else target.GotoLabel( s, stop );
			return;
		}
		if ( target == null ) return;
		target.GotoFrame( Value.ToInt32( fv.ToNumber( this ) ) + bias, stop );
	}

	void CallFrame( Ctx c, string fv )
	{
		var target = c.Target?.AsClip;
		var s = fv;
		int colon = s.LastIndexOf( ':' );
		if ( colon >= 0 ) { target = ResolveTargetPath( c, s.Substring( 0, colon ) )?.AsClip; s = s.Substring( colon + 1 ); }
		if ( target == null ) return;
		var n = Value.StringToNumber( s, SwfVersion );
		int frame = !double.IsNaN( n ) ? (int)n : target.FrameOfLabel( s );
		if ( frame <= 0 ) return;
		foreach ( var code in target.FrameScripts( frame ) ) RunScript( target, code );
	}

	// ======================================================================= comparison

	public Value LessThan( Value a, Value b )
	{
		var pa = a.IsObject ? a.Obj.ToPrimitive( this, false ) : a;
		var pb = b.IsObject ? b.Obj.ToPrimitive( this, false ) : b;
		if ( pa.IsString && pb.IsString ) return string.CompareOrdinal( pa.Str, pb.Str ) < 0;
		double x = pa.ToNumber( this ), y = pb.ToNumber( this );
		if ( double.IsNaN( x ) || double.IsNaN( y ) ) return Value.Undefined;
		return x < y;
	}

	public bool StrictEquals( Value a, Value b )
	{
		if ( a.Kind != b.Kind ) return false;
		switch ( a.Kind )
		{
			case ValueKind.Undefined: case ValueKind.Null: return true;
			case ValueKind.Number: return a.Num == b.Num;
			case ValueKind.Bool: return a.Num == b.Num;
			case ValueKind.String: return a.Str == b.Str;
			default: return ReferenceEquals( a.Obj, b.Obj );
		}
	}

	public bool AbstractEquals( Value a, Value b )
	{
		if ( a.Kind == b.Kind ) return StrictEquals( a, b );
		if ( a.IsNullish && b.IsNullish ) return true;
		if ( a.IsNullish || b.IsNullish ) return false;
		if ( a.IsNumber && b.IsString ) return a.Num == b.ToNumber( this );
		if ( a.IsString && b.IsNumber ) return a.ToNumber( this ) == b.Num;
		if ( a.IsBool ) return AbstractEquals( a.Num, b );
		if ( b.IsBool ) return AbstractEquals( a, b.Num );
		if ( (a.IsNumber || a.IsString) && b.IsObject ) return AbstractEquals( a, b.Obj.ToPrimitive( this, false ) );
		if ( a.IsObject && (b.IsNumber || b.IsString) ) return AbstractEquals( a.Obj.ToPrimitive( this, false ), b );
		return false;
	}
}

/// <summary>Stops a runaway script.</summary>
public sealed class ScriptAbort : Exception
{
	public ScriptAbort( string m ) : base( m ) { }
}

/// <summary>The value of `super` inside a method or constructor.</summary>
public sealed class SuperObject : AsObject
{
	readonly Avm1 vm;
	readonly Value thisV;
	readonly AsObject owner; // prototype that owns the running method (its __constructor__ is the parent class)

	public SuperObject( Avm1 vm, Value thisV, AsObject owner ) : base( owner?.Proto ) { this.vm = vm; this.thisV = thisV; this.owner = owner; }

	public Value CallMethod( string name, Value[] args )
	{
		var start = owner?.Proto;
		if ( start is null ) return Value.Undefined;
		var f = start.Get( name, vm ).AsObj as AsFunction;
		if ( f is null ) return Value.Undefined;
		AsObject found = start; int g = 0;
		while ( found != null && found.GetOwnProperty( name ) == null && g++ < 64 ) found = found.Proto;
		return f.Call( vm, thisV, args, found );
	}

	/// <summary>super(...) — runs the parent class constructor on this.</summary>
	public Value CallConstructor( Value[] args )
	{
		var ctor = owner?.Get( "__constructor__", vm ).AsObj as AsFunction;
		if ( ctor is null ) return Value.Undefined;
		return ctor.Call( vm, thisV, args, owner.Proto );
	}
}