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

namespace Flash;

/// <summary>Anything on the Flash display list.</summary>
public abstract class DisplayObject : AsObject
{
	public readonly Player Player;
	public MovieClip Parent;
	public CharDef Def;
	public int Depth;
	public string Name = "";
	public Matrix Matrix = Matrix.Identity;
	public ColorTransform Cx = ColorTransform.Identity;
	public List<Filter> Filters;
	public bool Visible = true;
	public int ClipDepth; // >0: this object masks depths (Depth, ClipDepth]
	public int Ratio;
	public int BlendMode;
	public bool Removed;
	/// <summary>Placed by a timeline (vs. created by script) and the frame it was placed on.</summary>
	public bool FromTimeline;
	public int PlacedFrame;
	/// <summary>Once script moves a timeline object, the timeline stops moving it.</summary>
	public bool ScriptTransformed;
	public DisplayObject MaskedBy; // setMask
	public DisplayObject MaskFor;
	/// <summary>For objects inside a button state (they have no Parent clip).</summary>
	public ButtonObj ButtonOwner;
	/// <summary>Original timeline depth (tag depth); -1 once script moved it with swapDepths.</summary>
	public int TimelineDepth = -1;

	public DisplayObject RenderParent => (DisplayObject)Parent ?? ButtonOwner;

	/// <summary>Layout tweak from the editor (F2 / FlashLayoutItem); applied on top of the SWF's own transform.</summary>
	public LayoutAdjust Adjust;

	/// <summary>The transform used for drawing and hit testing (SWF transform + layout tweak).</summary>
	public Matrix EffectiveMatrix
	{
		get
		{
			if ( Adjust == null || Adjust.IsIdentity ) return Matrix;
			var a = Adjust;
			double r = a.Rotation * Math.PI / 180.0;
			var local = new Matrix( Math.Cos( r ) * a.ScaleX, Math.Sin( r ) * a.ScaleX, -Math.Sin( r ) * a.ScaleY, Math.Cos( r ) * a.ScaleY, 0, 0 );
			var m = Matrix.Mul( local );
			m.Tx += a.X; m.Ty += a.Y;
			return m;
		}
	}

	/// <summary>A path that stays the same between runs (unnamed timeline objects are named by symbol and depth).</summary>
	public string StablePath
	{
		get
		{
			var parts = new List<string>();
			for ( DisplayObject d = this; d != null && d.RenderParent != null; d = d.RenderParent )
			{
				bool auto = string.IsNullOrEmpty( d.Name ) || (d.FromTimeline && d.Name.StartsWith( "instance", StringComparison.Ordinal ));
				parts.Add( auto ? $"#{d.Def?.Id ?? 0}@{d.TimelineDepth}" : d.Name );
			}
			parts.Reverse();
			return string.Join( "/", parts );
		}
	}

	protected DisplayObject( Player p, AsObject proto ) : base( proto ) { Player = p; }

	public MovieClip AsClip => this as MovieClip;
	public virtual bool HasOwnToString => false;

	public string TargetPath
	{
		get
		{
			if ( Parent == null ) return "_level0";
			var parts = new List<string>();
			DisplayObject d = this;
			while ( d != null && d.Parent != null ) { parts.Add( d.Name ); d = d.Parent; }
			parts.Reverse();
			return "_level0." + string.Join( ".", parts );
		}
	}

	public string SlashPath
	{
		get
		{
			if ( Parent == null ) return "/";
			var parts = new List<string>();
			DisplayObject d = this;
			while ( d != null && d.Parent != null ) { parts.Add( d.Name ); d = d.Parent; }
			parts.Reverse();
			return "/" + string.Join( "/", parts );
		}
	}

	public Matrix WorldMatrix
	{
		get
		{
			var m = EffectiveMatrix;
			var p = RenderParent;
			int g = 0;
			while ( p != null && g++ < 256 ) { m = p.EffectiveMatrix.Mul( m ); p = p.RenderParent; }
			return m;
		}
	}

	public bool IsVisibleInTree
	{
		get
		{
			for ( DisplayObject d = this; d != null; d = d.RenderParent ) if ( !d.Visible || d.Removed ) return false;
			return true;
		}
	}

	// Flash keeps (scaleX, scaleY, rotationX, rotationY) alongside the matrix so _xscale/_rotation round-trip exactly
	bool decomposed;
	double sx, sy, rx, ry;

	void Decompose()
	{
		if ( decomposed ) return;
		sx = Math.Sqrt( Matrix.A * Matrix.A + Matrix.B * Matrix.B );
		sy = Math.Sqrt( Matrix.C * Matrix.C + Matrix.D * Matrix.D );
		rx = Math.Atan2( Matrix.B, Matrix.A );
		ry = Math.Atan2( -Matrix.C, Matrix.D );
		decomposed = true;
	}

	void Recompose()
	{
		Matrix.A = sx * Math.Cos( rx );
		Matrix.B = sx * Math.Sin( rx );
		Matrix.C = -sy * Math.Sin( ry );
		Matrix.D = sy * Math.Cos( ry );
		ScriptTransformed = true;
	}

	public double X { get => Matrix.Tx; set { Matrix.Tx = value; ScriptTransformed = true; } }
	public double Y { get => Matrix.Ty; set { Matrix.Ty = value; ScriptTransformed = true; } }

	public double XScale
	{
		get { Decompose(); return sx * 100.0; }
		set { Decompose(); sx = value / 100.0; Recompose(); }
	}

	public double YScale
	{
		get { Decompose(); return sy * 100.0; }
		set { Decompose(); sy = value / 100.0; Recompose(); }
	}

	public double Rotation
	{
		get { Decompose(); return rx * 180.0 / Math.PI; }
		set
		{
			Decompose();
			var v = value % 360.0;
			if ( v > 180 ) v -= 360; else if ( v < -180 ) v += 360;
			double nr = v * Math.PI / 180.0;
			ry += nr - rx;
			rx = nr;
			Recompose();
		}
	}

	public void SetMatrixFromTimeline( Matrix m )
	{
		Matrix = m;
		decomposed = false;
	}

	public void InvalidateTransformCache() { decomposed = false; }

	public double Alpha
	{
		get => Cx.Am * 100.0;
		set { Cx.Am = value / 100.0; ScriptTransformed = true; }
	}

	/// <summary>Local bounds (own coordinate space) of this object's content.</summary>
	public abstract (double x0, double y0, double x1, double y1)? LocalBounds();

	public (double x0, double y0, double x1, double y1)? BoundsIn( Matrix m )
	{
		var b = LocalBounds();
		if ( b is null ) return null;
		var (x0, y0, x1, y1) = b.Value;
		double mx0 = double.MaxValue, my0 = double.MaxValue, mx1 = double.MinValue, my1 = double.MinValue;
		foreach ( var (px, py) in new[] { (x0, y0), (x1, y0), (x0, y1), (x1, y1) } )
		{
			var (tx, ty) = m.Apply( px, py );
			mx0 = Math.Min( mx0, tx ); my0 = Math.Min( my0, ty ); mx1 = Math.Max( mx1, tx ); my1 = Math.Max( my1, ty );
		}
		return (mx0, my0, mx1, my1);
	}

	public double Width
	{
		get { var b = BoundsIn( Matrix ); return b is null ? 0 : b.Value.x1 - b.Value.x0; }
		set
		{
			var b = LocalBounds();
			if ( b is null ) return;
			double w = b.Value.x1 - b.Value.x0;
			if ( w <= 0 ) return;
			// approximation that matches Flash for unrotated objects
			XScale = value / w * 100.0 * Math.Sign( XScale == 0 ? 1 : XScale );
		}
	}

	public double Height
	{
		get { var b = BoundsIn( Matrix ); return b is null ? 0 : b.Value.y1 - b.Value.y0; }
		set
		{
			var b = LocalBounds();
			if ( b is null ) return;
			double h = b.Value.y1 - b.Value.y0;
			if ( h <= 0 ) return;
			YScale = value / h * 100.0 * Math.Sign( YScale == 0 ? 1 : YScale );
		}
	}

	/// <summary>Point hit test in stage coordinates against actual content.</summary>
	public bool HitTestPoint( double sx, double sy, bool shapeOnly )
	{
		var (lx, ly) = WorldMatrix.Inverse().Apply( sx, sy );
		return HitLocal( lx, ly, shapeOnly );
	}

	/// <summary>Hit test in this object's own coordinate space.</summary>
	public abstract bool HitLocal( double lx, double ly, bool shapeOnly );

	public bool HitChild( double plx, double ply, bool shapeOnly )
	{
		if ( !Visible || (Adjust != null && Adjust.Hidden) ) return false;
		var (lx, ly) = EffectiveMatrix.Inverse().Apply( plx, ply );
		return HitLocal( lx, ly, shapeOnly );
	}

	// ------------------------------------------------------------------ AS property access

	static readonly string[] IndexedNames = { "_x", "_y", "_xscale", "_yscale", "_currentframe", "_totalframes", "_alpha", "_visible", "_width", "_height",
		"_rotation", "_target", "_framesloaded", "_name", "_droptarget", "_url", "_highquality", "_focusrect", "_soundbuftime", "_quality", "_xmouse", "_ymouse" };

	public Value GetIndexedProperty( int idx, Avm1 vm ) => idx >= 0 && idx < IndexedNames.Length ? Get( IndexedNames[idx], vm ) : Value.Undefined;
	public void SetIndexedProperty( int idx, Value v, Avm1 vm ) { if ( idx >= 0 && idx < IndexedNames.Length ) Set( IndexedNames[idx], v, vm ); }

	protected bool GetDisplayProperty( string name, Avm1 vm, out Value v )
	{
		switch ( name )
		{
			case "_x": v = Math.Round( X * 20 ) / 20; return true;
			case "_y": v = Math.Round( Y * 20 ) / 20; return true;
			case "_xscale": v = XScale; return true;
			case "_yscale": v = YScale; return true;
			case "_alpha": v = Alpha; return true;
			case "_visible": v = Visible; return true;
			case "_width": v = Math.Round( Width * 20 ) / 20; return true;
			case "_height": v = Math.Round( Height * 20 ) / 20; return true;
			case "_rotation": v = Rotation; return true;
			case "_name": v = Name; return true;
			case "_target": v = SlashPath; return true;
			case "_parent": v = Parent != null ? Value.From( Parent ) : Value.Undefined; return true;
			case "_root": v = Value.From( Player.Root ); return true;
			case "_global": v = Value.From( vm.Global ); return true;
			case "_url": v = "ss2.swf"; return true;
			case "_quality": v = "HIGH"; return true;
			case "_highquality": v = 1; return true;
			case "_focusrect": v = Value.Null; return true;
			case "_soundbuftime": v = 5; return true;
			case "_droptarget": v = Player.DropTarget?.SlashPath ?? ""; return true;
			case "_xmouse":
			case "_ymouse":
				{
					var inv = WorldMatrix.Inverse();
					var (mx, my) = inv.Apply( Player.MouseX, Player.MouseY );
					v = Math.Round( (name == "_xmouse" ? mx : my) * 20 ) / 20;
					return true;
				}
		}
		v = default;
		return false;
	}

	protected bool SetDisplayProperty( string name, Value v, Avm1 vm )
	{
		switch ( name )
		{
			case "_x": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) X = n; return true; }
			case "_y": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) Y = n; return true; }
			case "_xscale": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) XScale = n; return true; }
			case "_yscale": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) YScale = n; return true; }
			case "_alpha": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) Alpha = n; return true; }
			case "_visible": Visible = v.IsNumber ? v.Num != 0 : v.ToBool( vm.SwfVersion ); return true;
			case "_width": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) Width = n; return true; }
			case "_height": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) Height = n; return true; }
			case "_rotation": { var n = v.ToNumber( vm ); if ( !double.IsNaN( n ) ) Rotation = n; return true; }
			case "_name": Name = v.ToStr( vm ); return true;
			case "_target": case "_url": case "_droptarget": case "_xmouse": case "_ymouse": case "_parent": case "_root": case "_currentframe": case "_totalframes": case "_framesloaded":
				return true;
			case "_quality": case "_highquality": case "_focusrect": case "_soundbuftime": return true;
		}
		return false;
	}

	static readonly HashSet<string> displayProps = new( IndexedNames ) { "_parent", "_root", "_global" };

	protected bool IsDisplayProperty( string name ) => displayProps.Contains( name );

	/// <summary>Called when removed from the display list.</summary>
	public virtual void OnRemoved()
	{
		Removed = true;
		if ( MaskFor != null ) { MaskFor.MaskedBy = null; MaskFor = null; }
		if ( MaskedBy != null ) { MaskedBy.MaskFor = null; MaskedBy = null; }
	}

	public virtual void RemoveFromScript()
	{
		// removeMovieClip only works on script depths (>= 0)
		if ( Parent == null || Depth < 0 ) return;
		Parent.RemoveChild( this );
	}

	public virtual void Duplicate( string name, int depth ) { }

	/// <summary>Called by the player each frame for edit text variable sync etc.</summary>
	public virtual void Sync() { }
}

// ================================================================================== leaf objects

public sealed class ShapeObj : DisplayObject
{
	public ShapeDef Shape => (ShapeDef)Def;
	public ShapeObj( Player p, ShapeDef d ) : base( p, p.Vm.ObjectProto ) { Def = d; }
	public override (double x0, double y0, double x1, double y1)? LocalBounds() => (Shape.X0, Shape.Y0, Shape.X1, Shape.Y1);

	public override bool HitLocal( double lx, double ly, bool shapeOnly )
	{
		var s = Shape;
		if ( lx < s.X0 || ly < s.Y0 || lx > s.X1 || ly > s.Y1 ) return false;
		if ( !shapeOnly ) return true;
		return s.Mask == null || s.Mask.Test( lx, ly );
	}
}

public sealed class MorphObj : DisplayObject
{
	public MorphDef Morph => (MorphDef)Def;
	public MorphObj( Player p, MorphDef d ) : base( p, p.Vm.ObjectProto ) { Def = d; }
	public double T => Ratio / 65535.0;

	public override (double x0, double y0, double x1, double y1)? LocalBounds()
	{
		var a = Morph.B0; var b = Morph.B1; double t = T;
		return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t);
	}

	public override bool HitLocal( double lx, double ly, bool shapeOnly )
	{
		var b = LocalBounds().Value;
		return lx >= b.x0 && ly >= b.y0 && lx <= b.x1 && ly <= b.y1;
	}
}

public sealed class StaticTextObj : DisplayObject
{
	public StaticTextDef Text => (StaticTextDef)Def;
	public StaticTextObj( Player p, StaticTextDef d ) : base( p, p.Vm.ObjectProto ) { Def = d; }
	public override (double x0, double y0, double x1, double y1)? LocalBounds() => (Text.X0, Text.Y0, Text.X1, Text.Y1);

	public override bool HitLocal( double lx, double ly, bool shapeOnly )
	{
		return lx >= Text.X0 && ly >= Text.Y0 && lx <= Text.X1 && ly <= Text.Y1;
	}
}

/// <summary>A dynamic or input text field.</summary>
public sealed class EditText : DisplayObject
{
	public EditTextDef Edit => (EditTextDef)Def;
	public string Text = "";
	public string HtmlSource; // when html
	public bool IsHtml;
	public double[] TextColor;
	public string VariableName;
	string lastVarText;
	public bool Selectable = true;
	public string Restrict;
	public int MaxChars;
	public int Version; // bumps when the text changes (renderers can cache)

	public EditText( Player p, EditTextDef d ) : base( p, p.Vm.TextFieldProto )
	{
		Def = d;
		IsHtml = d.Html;
		TextColor = d.Color;
		VariableName = d.Variable;
		MaxChars = d.MaxLength;
		var init = d.InitialText ?? "";
		if ( IsHtml ) SetHtml( init ); else Text = init.Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
	}

	public override (double x0, double y0, double x1, double y1)? LocalBounds() => (Edit.X0, Edit.Y0, Edit.X1, Edit.Y1);

	public override bool HitLocal( double lx, double ly, bool shapeOnly )
	{
		return lx >= Edit.X0 && ly >= Edit.Y0 && lx <= Edit.X1 && ly <= Edit.Y1;
	}

	public void SetText( string t )
	{
		t = (t ?? "").Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
		if ( IsHtml ) { HtmlSource = t; Text = HtmlText.Strip( t ); }
		else Text = t;
		Version++;
	}

	public void SetHtml( string h )
	{
		HtmlSource = h ?? "";
		Text = HtmlText.Strip( HtmlSource ).Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
		Version++;
	}

	/// <summary>Pull the bound variable into the field (Flash does this when the variable changes).</summary>
	public override void Sync()
	{
		if ( string.IsNullOrEmpty( VariableName ) || Parent == null ) return;
		var vm = Player.Vm;
		Value v;
		var name = VariableName;
		AsObject holder = Parent;
		int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( ':' ) );
		if ( dot >= 0 )
		{
			var path = name.Substring( 0, dot );
			var d = vm.ResolveTargetPath( Parent, path );
			holder = d;
			name = name.Substring( dot + 1 );
		}
		if ( holder == null ) return;
		v = holder.Get( name, vm );
		if ( v.IsUndefined )
		{
			if ( !holder.Has( name ) )
			{
				// the field's initial text initialises the variable (Flash behaviour)
				holder.Set( name, IsHtml ? (Value)(HtmlSource ?? "") : Text, vm );
				lastVarText = IsHtml ? HtmlSource : Text;
			}
			return;
		}
		var s = v.ToStr( vm );
		if ( s == lastVarText ) return;
		lastVarText = s;
		if ( IsHtml ) SetHtml( s ); else { Text = s.Replace( "\r\n", "\n" ).Replace( '\r', '\n' ); Version++; }
	}

	/// <summary>User typed into an input field.</summary>
	public void UserInput( string t )
	{
		if ( MaxChars > 0 && t.Length > MaxChars ) t = t.Substring( 0, MaxChars );
		Text = t; Version++;
		if ( !string.IsNullOrEmpty( VariableName ) && Parent != null )
		{
			var name = VariableName;
			AsObject holder = Parent;
			int dot = Math.Max( name.LastIndexOf( '.' ), name.LastIndexOf( ':' ) );
			if ( dot >= 0 ) { holder = Player.Vm.ResolveTargetPath( Parent, name.Substring( 0, dot ) ); name = name.Substring( dot + 1 ); }
			holder?.Set( name, t, Player.Vm );
			lastVarText = t;
		}
		Player.Vm.CallMethod( Value.From( this ), "onChanged", Value.From( this ) );
	}

	protected override bool TryGetNative( string name, Avm1 vm, out Value v )
	{
		if ( GetDisplayProperty( name, vm, out v ) ) return true;
		switch ( name )
		{
			case "text": v = Text; return true;
			case "htmlText": v = IsHtml ? (HtmlSource ?? Text) : Text; return true;
			case "html": v = IsHtml; return true;
			case "textColor": v = (double)(((int)TextColor[0] << 16) | ((int)TextColor[1] << 8) | (int)TextColor[2]); return true;
			case "length": v = Text.Length; return true;
			case "variable": v = VariableName is null ? Value.Null : (Value)VariableName; return true;
			case "selectable": v = Selectable; return true;
			case "restrict": v = Restrict is null ? Value.Null : (Value)Restrict; return true;
			case "maxChars": v = MaxChars == 0 ? Value.Null : (Value)MaxChars; return true;
			case "multiline": v = Edit.Multiline; return true;
			case "wordWrap": v = Edit.WordWrap; return true;
			case "textWidth": v = Text.Length * Edit.Size * 0.5; return true;
			case "textHeight": v = Edit.Size * 1.2; return true;
		}
		return false;
	}

	protected override bool TrySetNative( string name, Value v, Avm1 vm )
	{
		if ( SetDisplayProperty( name, v, vm ) ) return true;
		switch ( name )
		{
			case "text": SetText( v.ToStr( vm ) ); if ( IsHtml ) { Text = v.ToStr( vm ); HtmlSource = null; } return true;
			case "htmlText": if ( IsHtml ) SetHtml( v.ToStr( vm ) ); else SetText( v.ToStr( vm ) ); return true;
			case "html": IsHtml = v.ToBool( vm.SwfVersion ); Version++; return true;
			case "textColor":
				{
					int c = v.ToInt32( vm );
					TextColor = new double[] { (c >> 16) & 255, (c >> 8) & 255, c & 255, 255 };
					Version++;
					return true;
				}
			case "variable": VariableName = v.IsNullish ? null : v.ToStr( vm ); lastVarText = null; return true;
			case "selectable": Selectable = v.ToBool( vm.SwfVersion ); return true;
			case "restrict": Restrict = v.IsNullish ? null : v.ToStr( vm ); return true;
			case "maxChars": MaxChars = v.IsNullish ? 0 : v.ToInt32( vm ); return true;
		}
		return false;
	}

	protected override bool HasNativeProperty( string name ) => IsDisplayProperty( name ) || name is "text" or "htmlText" or "textColor" or "length" or "variable";
}

/// <summary>A SWF button (DefineButton2): up/over/down states and on(...) actions.</summary>
public sealed class ButtonObj : DisplayObject
{
	public ButtonDef Button => (ButtonDef)Def;
	public string State = "u"; // u, o, d
	public bool Enabled = true;
	public bool UseHandCursor = true;
	public readonly Dictionary<string, List<DisplayObject>> StateChildren = new();

	public ButtonObj( Player p, ButtonDef d ) : base( p, p.Vm.ButtonProto )
	{
		Def = d;
		foreach ( var st in new[] { "u", "o", "d", "h" } )
		{
			var list = new List<DisplayObject>();
			foreach ( var r in d.Records )
			{
				if ( r.States.IndexOf( st, StringComparison.Ordinal ) < 0 ) continue;
				var child = p.Instantiate( r.CharId, null );
				if ( child == null ) continue;
				child.Parent = null;
				child.SetMatrixFromTimeline( r.M );
				if ( r.Cx.HasValue ) child.Cx = r.Cx.Value;
				child.Filters = r.Filters;
				child.Depth = r.Depth;
				child.ButtonOwner = this;
				child.FromTimeline = true;
				list.Add( child );
				if ( child is MovieClip mc ) p.Construct( mc );
			}
			StateChildren[st] = list;
		}
	}

	public List<DisplayObject> Current => StateChildren.TryGetValue( State, out var l ) ? l : null;

	public override void OnRemoved()
	{
		base.OnRemoved();
		foreach ( var l in StateChildren.Values ) foreach ( var c in l ) Player.Unload( c );
	}

	public override (double x0, double y0, double x1, double y1)? LocalBounds()
	{
		(double, double, double, double)? r = null;
		foreach ( var c in StateChildren["u"] )
		{
			var b = c.BoundsIn( c.Matrix );
			if ( b is null ) continue;
			r = r is null ? b : (Math.Min( r.Value.Item1, b.Value.x0 ), Math.Min( r.Value.Item2, b.Value.y0 ), Math.Max( r.Value.Item3, b.Value.x1 ), Math.Max( r.Value.Item4, b.Value.y1 ));
		}
		return r;
	}

	public override bool HitLocal( double lx, double ly, bool shapeOnly )
	{
		var hit = StateChildren["h"];
		if ( hit.Count == 0 ) hit = StateChildren["u"];
		foreach ( var c in hit )
			if ( c.HitChild( lx, ly, true ) ) return true;
		return false;
	}

	protected override bool TryGetNative( string name, Avm1 vm, out Value v )
	{
		if ( GetDisplayProperty( name, vm, out v ) ) return true;
		if ( name == "enabled" ) { v = Enabled; return true; }
		if ( name == "useHandCursor" ) { v = UseHandCursor; return true; }
		return false;
	}

	protected override bool TrySetNative( string name, Value v, Avm1 vm )
	{
		if ( SetDisplayProperty( name, v, vm ) ) return true;
		if ( name == "enabled" ) { Enabled = v.ToBool( vm.SwfVersion ); return true; }
		if ( name == "useHandCursor" ) { UseHandCursor = v.ToBool( vm.SwfVersion ); return true; }
		return false;
	}

	protected override bool HasNativeProperty( string name ) => IsDisplayProperty( name );
}

/// <summary>A layout tweak: offset (in the parent's space), scale/rotation about the object's origin, or hidden.</summary>
public sealed class LayoutAdjust
{
	public double X, Y, ScaleX = 1, ScaleY = 1, Rotation;
	public bool Hidden;
	public bool IsIdentity => X == 0 && Y == 0 && ScaleX == 1 && ScaleY == 1 && Rotation == 0 && !Hidden;
}

public static class HtmlText
{
	/// <summary>Plain text of a Flash HTML string (tags removed, entities decoded, &lt;br&gt;/&lt;p&gt; as newlines).</summary>
	public static string Strip( string html )
	{
		if ( string.IsNullOrEmpty( html ) ) return "";
		var sb = new System.Text.StringBuilder();
		int i = 0;
		bool firstP = true;
		while ( i < html.Length )
		{
			char c = html[i];
			if ( c == '<' )
			{
				int e = html.IndexOf( '>', i );
				if ( e < 0 ) break;
				var tag = html.Substring( i + 1, e - i - 1 ).Trim().ToLowerInvariant();
				if ( tag.StartsWith( "br" ) ) sb.Append( '\n' );
				else if ( tag.StartsWith( "p" ) && !tag.StartsWith( "/" ) ) { if ( !firstP ) sb.Append( '\n' ); firstP = false; }
				i = e + 1;
				continue;
			}
			if ( c == '&' )
			{
				int e = html.IndexOf( ';', i );
				if ( e > i && e - i < 8 )
				{
					var ent = html.Substring( i + 1, e - i - 1 );
					string rep = ent switch { "lt" => "<", "gt" => ">", "amp" => "&", "quot" => "\"", "apos" => "'", "nbsp" => " ", _ => null };
					if ( rep != null ) { sb.Append( rep ); i = e + 1; continue; }
				}
			}
			sb.Append( c );
			i++;
		}
		return sb.ToString();
	}
}