Flash/Core/Value.cs
using System;
using System.Collections.Generic;
using System.Globalization;

namespace Flash;

public enum ValueKind : byte { Undefined, Null, Bool, Number, String, Object }

/// <summary>An ActionScript 1/2 value (ECMA-262 3rd edition rules, as Flash Player 7+ uses them).</summary>
public readonly struct Value
{
	public readonly ValueKind Kind;
	public readonly double Num;
	public readonly string Str;
	public readonly AsObject Obj;

	Value( ValueKind k, double n = 0, string s = null, AsObject o = null ) { Kind = k; Num = n; Str = s; Obj = o; }

	public static readonly Value Undefined = new( ValueKind.Undefined );
	public static readonly Value Null = new( ValueKind.Null );
	public static readonly Value True = new( ValueKind.Bool, 1 );
	public static readonly Value False = new( ValueKind.Bool, 0 );
	public static readonly Value NaN = new( ValueKind.Number, double.NaN );
	public static readonly Value Zero = new( ValueKind.Number, 0 );
	public static readonly Value EmptyString = new( ValueKind.String, 0, "" );

	public static Value From( double d ) => new( ValueKind.Number, d );
	public static Value From( int d ) => new( ValueKind.Number, d );
	public static Value From( bool b ) => b ? True : False;
	public static Value From( string s ) => s is null ? Undefined : new( ValueKind.String, 0, s );
	public static Value From( AsObject o ) => o is null ? Null : new( ValueKind.Object, 0, null, o );

	public static implicit operator Value( double d ) => From( d );
	public static implicit operator Value( int d ) => From( d );
	public static implicit operator Value( bool b ) => From( b );
	public static implicit operator Value( string s ) => From( s );
	public static implicit operator Value( AsObject o ) => From( o );

	public bool IsUndefined => Kind == ValueKind.Undefined;
	public bool IsNull => Kind == ValueKind.Null;
	public bool IsNullish => Kind == ValueKind.Undefined || Kind == ValueKind.Null;
	public bool IsObject => Kind == ValueKind.Object;
	public bool IsString => Kind == ValueKind.String;
	public bool IsNumber => Kind == ValueKind.Number;
	public bool IsBool => Kind == ValueKind.Bool;
	public bool BoolValue => Num != 0;

	public AsObject AsObj => Kind == ValueKind.Object ? Obj : null;

	// ---------------------------------------------------------------- conversions

	public bool ToBool( int swfVersion = 7 )
	{
		switch ( Kind )
		{
			case ValueKind.Bool: return Num != 0;
			case ValueKind.Number: return !double.IsNaN( Num ) && Num != 0;
			case ValueKind.String:
				if ( swfVersion >= 7 ) return Str.Length > 0;
				var n = StringToNumber( Str, swfVersion );
				return !double.IsNaN( n ) && n != 0;
			case ValueKind.Object: return true;
			default: return false;
		}
	}

	public double ToNumber( Avm1 vm )
	{
		switch ( Kind )
		{
			case ValueKind.Number: return Num;
			case ValueKind.Bool: return Num;
			case ValueKind.String: return StringToNumber( Str, vm?.SwfVersion ?? 7 );
			case ValueKind.Undefined: return (vm?.SwfVersion ?? 7) >= 7 ? double.NaN : 0;
			case ValueKind.Null: return (vm?.SwfVersion ?? 7) >= 7 ? double.NaN : 0;
			case ValueKind.Object:
				if ( Obj is DisplayObject ) return double.NaN;
				var p = Obj.ToPrimitive( vm, hintString: false );
				if ( p.Kind == ValueKind.Object ) return double.NaN;
				return p.ToNumber( vm );
		}
		return double.NaN;
	}

	public string ToStr( Avm1 vm )
	{
		switch ( Kind )
		{
			case ValueKind.String: return Str;
			case ValueKind.Number: return NumberToString( Num );
			case ValueKind.Bool: return Num != 0 ? "true" : "false";
			case ValueKind.Undefined: return (vm?.SwfVersion ?? 7) >= 7 ? "undefined" : "";
			case ValueKind.Null: return "null";
			case ValueKind.Object:
				if ( Obj is DisplayObject d && !d.HasOwnToString ) return d.TargetPath;
				var p = Obj.ToPrimitive( vm, hintString: true );
				if ( p.Kind == ValueKind.Object ) return p.Obj is AsFunction ? "[type Function]" : "[object Object]";
				return p.ToStr( vm );
		}
		return "";
	}

	public int ToInt32( Avm1 vm ) => ToInt32( ToNumber( vm ) );

	public static int ToInt32( double d )
	{
		if ( double.IsNaN( d ) || double.IsInfinity( d ) ) return 0;
		d = Math.Truncate( d );
		if ( d >= int.MinValue && d <= int.MaxValue ) return (int)d;
		double m = d % 4294967296.0;
		if ( m < 0 ) m += 4294967296.0;
		return (int)(uint)m;
	}

	public static uint ToUInt32( double d ) => (uint)ToInt32( d );

	public static double StringToNumber( string s, int swfVersion )
	{
		if ( s is null ) return double.NaN;
		var t = s.Trim( ' ', '\t', '\n', '\r', '\f', '\v' );
		if ( t.Length == 0 ) return swfVersion >= 5 ? double.NaN : 0;
		if ( t.Length > 2 && t[0] == '0' && (t[1] == 'x' || t[1] == 'X') )
		{
			long v = 0;
			for ( int i = 2; i < t.Length; i++ )
			{
				int h = HexDigit( t[i] );
				if ( h < 0 ) return double.NaN;
				v = v * 16 + h;
			}
			return v;
		}
		if ( t == "Infinity" || t == "+Infinity" ) return double.PositiveInfinity;
		if ( t == "-Infinity" ) return double.NegativeInfinity;
		// strict decimal literal
		int k = 0;
		if ( k < t.Length && (t[k] == '+' || t[k] == '-') ) k++;
		bool digits = false;
		while ( k < t.Length && char.IsDigit( t[k] ) ) { k++; digits = true; }
		if ( k < t.Length && t[k] == '.' ) { k++; while ( k < t.Length && char.IsDigit( t[k] ) ) { k++; digits = true; } }
		if ( !digits ) return double.NaN;
		if ( k < t.Length && (t[k] == 'e' || t[k] == 'E') )
		{
			int e = k + 1;
			if ( e < t.Length && (t[e] == '+' || t[e] == '-') ) e++;
			int ds = e;
			while ( e < t.Length && char.IsDigit( t[e] ) ) e++;
			if ( e == ds ) return double.NaN;
			k = e;
		}
		if ( k != t.Length ) return double.NaN;
		return double.TryParse( t, NumberStyles.Float, CultureInfo.InvariantCulture, out var r ) ? r : double.NaN;
	}

	static int HexDigit( char c )
	{
		if ( c >= '0' && c <= '9' ) return c - '0';
		if ( c >= 'a' && c <= 'f' ) return c - 'a' + 10;
		if ( c >= 'A' && c <= 'F' ) return c - 'A' + 10;
		return -1;
	}

	/// <summary>Flash's Number→String (15 significant digits, exponent form outside 1e-5..1e15).</summary>
	public static string NumberToString( double d )
	{
		if ( double.IsNaN( d ) ) return "NaN";
		if ( double.IsPositiveInfinity( d ) ) return "Infinity";
		if ( double.IsNegativeInfinity( d ) ) return "-Infinity";
		if ( d == 0 ) return "0";
		if ( Math.Abs( d ) < 1e15 && d == Math.Floor( d ) ) return ((long)d).ToString( CultureInfo.InvariantCulture );
		var s = d.ToString( "G15", CultureInfo.InvariantCulture );
		int e = s.IndexOf( 'E' );
		if ( e < 0 ) return s;
		var mant = s.Substring( 0, e );
		int exp = int.Parse( s.Substring( e + 1 ), CultureInfo.InvariantCulture );
		if ( exp >= -5 && exp < 15 )
			return d.ToString( "0.###############", CultureInfo.InvariantCulture );
		return mant + "e" + (exp >= 0 ? "+" : "-") + Math.Abs( exp ).ToString( CultureInfo.InvariantCulture );
	}

	public string TypeOf()
	{
		switch ( Kind )
		{
			case ValueKind.Undefined: return "undefined";
			case ValueKind.Null: return "null";
			case ValueKind.Bool: return "boolean";
			case ValueKind.Number: return "number";
			case ValueKind.String: return "string";
			default:
				if ( Obj is DisplayObject d ) return d is EditText ? "object" : "movieclip";
				if ( Obj is AsFunction ) return "function";
				return "object";
		}
	}

	public override string ToString() => ToStr( null );

	public bool SameRef( Value o ) => Kind == o.Kind && (Kind switch
	{
		ValueKind.Number => Num == o.Num,
		ValueKind.Bool => Num == o.Num,
		ValueKind.String => Str == o.Str,
		ValueKind.Object => ReferenceEquals( Obj, o.Obj ),
		_ => true
	});
}