Flash/Core/AsObject.cs
using System;
using System.Collections.Generic;

namespace Flash;

[Flags]
public enum PropFlags : byte { None = 0, DontEnum = 1, DontDelete = 2, ReadOnly = 4 }

public sealed class Property
{
	public Value Value;
	public PropFlags Flags;
	public AsFunction Getter, Setter;
	public bool IsVirtual => Getter != null || Setter != null;
}

/// <summary>A plain ActionScript object (with prototype chain).</summary>
public class AsObject
{
	readonly Dictionary<string, Property> props = new();
	readonly List<string> order = new();
	public AsObject Proto;
	/// <summary>Native payload for built-in classes (Date ticks, Sound, Color target...).</summary>
	public object Native;

	public AsObject() { }
	public AsObject( AsObject proto ) { Proto = proto; }

	public virtual string ClassName => "Object";

	// --------------------------------------------------------------- own property storage

	public Property GetOwnProperty( string name ) => props.TryGetValue( name, out var p ) ? p : null;

	public bool HasOwnProperty( string name ) => props.ContainsKey( name ) || HasNativeProperty( name );

	public void SetOwn( string name, Value v, PropFlags flags = PropFlags.None )
	{
		if ( props.TryGetValue( name, out var p ) ) { p.Value = v; p.Getter = null; p.Setter = null; p.Flags = flags; return; }
		props[name] = new Property { Value = v, Flags = flags };
		order.Add( name );
	}

	public void DefineHidden( string name, Value v ) => SetOwn( name, v, PropFlags.DontEnum );

	public void AddVirtual( string name, AsFunction getter, AsFunction setter, PropFlags flags = PropFlags.None )
	{
		if ( !props.TryGetValue( name, out var p ) ) { p = new Property(); props[name] = p; order.Add( name ); }
		p.Getter = getter; p.Setter = setter; p.Flags = flags; p.Value = Value.Undefined;
	}

	public virtual bool Delete( string name )
	{
		if ( !props.TryGetValue( name, out var p ) ) return false;
		if ( (p.Flags & PropFlags.DontDelete) != 0 ) return false;
		props.Remove( name );
		order.Remove( name );
		return true;
	}

	public void SetFlags( string name, PropFlags set, PropFlags clear )
	{
		if ( props.TryGetValue( name, out var p ) ) p.Flags = (p.Flags | set) & ~clear;
	}

	public IEnumerable<string> OwnKeys => order;

	public int OwnCount => order.Count;

	// --------------------------------------------------------------- hooks for special objects

	/// <summary>Built-in (non-dictionary) properties, e.g. a MovieClip's _x or an array's indices.</summary>
	protected virtual bool TryGetNative( string name, Avm1 vm, out Value v ) { v = default; return false; }
	protected virtual bool TrySetNative( string name, Value v, Avm1 vm ) => false;
	protected virtual bool HasNativeProperty( string name ) => false;
	/// <summary>Names to enumerate besides the dictionary (array indices, child clips...).</summary>
	public virtual IEnumerable<string> NativeKeys => Array.Empty<string>();

	// --------------------------------------------------------------- lookup

	public Value Get( string name, Avm1 vm ) => GetWithThis( name, vm, this );

	public Value GetWithThis( string name, Avm1 vm, AsObject thisObj )
	{
		if ( name == "__proto__" ) return Proto is null ? Value.Undefined : Value.From( Proto );
		var o = this;
		int guard = 0;
		while ( o != null && guard++ < 256 )
		{
			if ( o.props.TryGetValue( name, out var p ) )
			{
				if ( p.Getter != null ) return vm.CallFunction( p.Getter, Value.From( thisObj ), Array.Empty<Value>() );
				if ( p.Setter != null ) return Value.Undefined;
				return p.Value;
			}
			if ( o.TryGetNative( name, vm, out var nv ) ) return nv;
			o = o.Proto;
		}
		return Value.Undefined;
	}

	public bool Has( string name )
	{
		if ( name == "__proto__" ) return Proto != null;
		var o = this; int guard = 0;
		while ( o != null && guard++ < 256 )
		{
			if ( o.props.ContainsKey( name ) || o.HasNativeProperty( name ) ) return true;
			o = o.Proto;
		}
		return false;
	}

	public virtual void Set( string name, Value v, Avm1 vm )
	{
		if ( name == "__proto__" ) { Proto = v.AsObj; return; }
		if ( props.TryGetValue( name, out var p ) )
		{
			if ( p.Setter != null ) { vm.CallFunction( p.Setter, Value.From( this ), new[] { v } ); return; }
			if ( p.Getter != null ) return;
			if ( (p.Flags & PropFlags.ReadOnly) != 0 ) return;
			p.Value = v;
			return;
		}
		if ( TrySetNative( name, v, vm ) ) return;
		// inherited virtual property?
		var o = Proto; int guard = 0;
		while ( o != null && guard++ < 256 )
		{
			if ( o.props.TryGetValue( name, out var ip ) )
			{
				if ( ip.Setter != null ) { vm.CallFunction( ip.Setter, Value.From( this ), new[] { v } ); return; }
				if ( ip.Getter != null ) return;
				break;
			}
			o = o.Proto;
		}
		props[name] = new Property { Value = v };
		order.Add( name );
	}

	/// <summary>All enumerable names, own first then inherited (for..in).</summary>
	public List<string> EnumerableKeys()
	{
		var seen = new HashSet<string>();
		var list = new List<string>();
		var o = this; int guard = 0;
		while ( o != null && guard++ < 256 )
		{
			foreach ( var k in o.NativeKeys )
				if ( seen.Add( k ) ) list.Add( k );
			for ( int i = o.order.Count - 1; i >= 0; i-- )
			{
				var k = o.order[i];
				if ( !seen.Add( k ) ) continue;
				if ( (o.props[k].Flags & PropFlags.DontEnum) == 0 ) list.Add( k );
			}
			o = o.Proto;
		}
		return list;
	}

	public virtual Value ToPrimitive( Avm1 vm, bool hintString )
	{
		var first = hintString ? "toString" : "valueOf";
		var second = hintString ? "valueOf" : "toString";
		var f = Get( first, vm ).AsObj as AsFunction;
		if ( f != null )
		{
			var r = vm.CallFunction( f, Value.From( this ), Array.Empty<Value>() );
			if ( r.Kind != ValueKind.Object ) return r;
		}
		f = Get( second, vm ).AsObj as AsFunction;
		if ( f != null )
		{
			var r = vm.CallFunction( f, Value.From( this ), Array.Empty<Value>() );
			if ( r.Kind != ValueKind.Object ) return r;
		}
		return Value.From( this );
	}

	public bool IsInstanceOf( AsObject ctor, Avm1 vm )
	{
		var proto = ctor.Get( "prototype", vm ).AsObj;
		if ( proto is null ) return false;
		var o = Proto; int guard = 0;
		while ( o != null && guard++ < 256 )
		{
			if ( ReferenceEquals( o, proto ) ) return true;
			// AS2 "implements": interfaces listed in __implements__
			o = o.Proto;
		}
		return false;
	}
}

/// <summary>An ActionScript Array.</summary>
public sealed class AsArray : AsObject
{
	public readonly List<Value> Items = new();
	public AsArray( AsObject proto ) : base( proto ) { }
	public override string ClassName => "Array";

	public static bool TryIndex( string name, out int index )
	{
		index = 0;
		if ( string.IsNullOrEmpty( name ) || name.Length > 9 ) return false;
		for ( int i = 0; i < name.Length; i++ )
		{
			char c = name[i];
			if ( c < '0' || c > '9' ) return false;
			if ( i == 0 && c == '0' && name.Length > 1 ) return false;
			index = index * 10 + (c - '0');
		}
		return true;
	}

	protected override bool TryGetNative( string name, Avm1 vm, out Value v )
	{
		if ( name == "length" ) { v = Items.Count; return true; }
		if ( TryIndex( name, out int i ) ) { v = i < Items.Count ? Items[i] : Value.Undefined; return true; }
		v = default; return false;
	}

	protected override bool TrySetNative( string name, Value v, Avm1 vm )
	{
		if ( name == "length" )
		{
			int n = Math.Max( 0, Value.ToInt32( v.ToNumber( vm ) ) );
			if ( n < Items.Count ) Items.RemoveRange( n, Items.Count - n );
			else while ( Items.Count < n ) Items.Add( Value.Undefined );
			return true;
		}
		if ( TryIndex( name, out int i ) )
		{
			while ( Items.Count <= i ) Items.Add( Value.Undefined );
			Items[i] = v;
			return true;
		}
		return false;
	}

	protected override bool HasNativeProperty( string name ) => name == "length" || (TryIndex( name, out int i ) && i < Items.Count);

	public override bool Delete( string name )
	{
		if ( TryIndex( name, out int i ) && i < Items.Count ) { Items[i] = Value.Undefined; return true; }
		return base.Delete( name );
	}

	public override IEnumerable<string> NativeKeys
	{
		get
		{
			for ( int i = Items.Count - 1; i >= 0; i-- )
				if ( !Items[i].IsUndefined ) yield return i.ToString();
		}
	}
}

/// <summary>Anything callable.</summary>
public abstract class AsFunction : AsObject
{
	public string Name = "";
	protected AsFunction( AsObject proto ) : base( proto ) { }
	public override string ClassName => "Function";
	public abstract Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null );

	/// <summary>new F(args)</summary>
	public virtual Value Construct( Avm1 vm, Value[] args )
	{
		var protoV = Get( "prototype", vm );
		var obj = vm.NewObject( protoV.AsObj ?? vm.ObjectProto );
		obj.DefineHidden( "__constructor__", Value.From( this ) );
		if ( vm.SwfVersion < 7 ) obj.DefineHidden( "constructor", Value.From( this ) );
		var r = Call( vm, Value.From( obj ), args, null );
		if ( r.IsObject ) return r;
		return Value.From( obj );
	}
}

public delegate Value NativeCall( Avm1 vm, Value thisV, Value[] args );

public sealed class NativeFunction : AsFunction
{
	readonly NativeCall call;
	/// <summary>Optional: when called with new, builds the instance itself (Array, Date...).</summary>
	public NativeCall Constructor;

	public NativeFunction( AsObject proto, string name, NativeCall call ) : base( proto ) { Name = name; this.call = call; }

	public override Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null ) => call( vm, thisV, args );

	public override Value Construct( Avm1 vm, Value[] args )
	{
		if ( Constructor != null )
		{
			var r = Constructor( vm, Value.Undefined, args );
			if ( r.IsObject && !r.Obj.HasOwnProperty( "__constructor__" ) ) r.Obj.DefineHidden( "__constructor__", Value.From( this ) );
			return r;
		}
		return base.Construct( vm, args );
	}
}

/// <summary>A function defined in bytecode (DefineFunction / DefineFunction2).</summary>
public sealed class BytecodeFunction : AsFunction
{
	public byte[] Code;
	public int Start, Length;
	public string[] ParamNames;
	public byte[] ParamRegisters; // DefineFunction2 only (0 = no register)
	public bool IsFunction2;
	public int RegisterCount;
	public ushort Flags2;
	public string[] ConstantPool;
	public Scope Scope; // captured scope chain
	public DisplayObject BaseClip; // timeline the function was defined on
	public int SwfVersion;

	public BytecodeFunction( AsObject proto ) : base( proto ) { }

	public override Value Call( Avm1 vm, Value thisV, Value[] args, AsObject superCtorOwner = null )
		=> vm.InvokeBytecode( this, thisV, args, superCtorOwner );
}

/// <summary>Primitive wrapper objects (new String("x"), (5).toString(), ...).</summary>
public sealed class AsBoxed : AsObject
{
	public Value Primitive;
	public AsBoxed( AsObject proto, Value v ) : base( proto ) { Primitive = v; }
	public override string ClassName => Primitive.Kind switch { ValueKind.String => "String", ValueKind.Number => "Number", _ => "Boolean" };

	protected override bool TryGetNative( string name, Avm1 vm, out Value v )
	{
		if ( Primitive.IsString && name == "length" ) { v = Primitive.Str.Length; return true; }
		v = default; return false;
	}

	public override Value ToPrimitive( Avm1 vm, bool hintString ) => Primitive;
}