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

namespace Flash;

/// <summary>What the engine needs from whoever hosts it (s&amp;box, or the test harness).</summary>
public interface IFlashHost
{
	void Trace( string message );
	/// <summary>Plays a sound; returns a handle (or null).</summary>
	object PlaySound( SoundDef sound, double volume, double pan, int loops, double offsetSeconds );
	void StopSound( object handle );
	bool IsSoundPlaying( object handle );
	double SoundPosition( object handle );
	void SetSoundVolume( object handle, double volume, double pan );
	string LoadShared( string name );
	void SaveShared( string name, string data );
	void GetUrl( string url, string window );
	void SetFullScreen( bool on );
}

/// <summary>One thing to draw, in stage coordinates.</summary>
public sealed class RenderItem
{
	public DisplayObject Obj;
	public Matrix World;
	public ColorTransform Cx;
	public List<Filter> Filters; // own + inherited
	public double[] Clip; // stage-space rect x0,y0,x1,y1 or null (bounding box of the masks)
	public List<RenderItem> Masks; // mask shapes (stage space) that clip this item, or null
	public int Index;
}

/// <summary>Runs a SWF: timelines, ActionScript, input, sound.</summary>
public sealed class Player
{
	public readonly Library Lib;
	public readonly Avm1 Vm;
	public MovieClip Root;
	public IFlashHost Host;
	public double TimeMs;
	public long FrameNumber;
	double accum;

	MovieClip execHead;
	readonly List<(DisplayObject target, byte[] code, AsFunction fn, Value thisV, Value[] args)> queue = new();
	int instanceCounter;
	public readonly Dictionary<string, AsFunction> RegisteredClasses = new();

	public Player( Library lib, IFlashHost host )
	{
		Lib = lib;
		Host = host;
		Vm = new Avm1( this ) { SwfVersion = lib.Version };
	}

	public string NextInstanceName() => "instance" + (++instanceCounter);

	/// <summary>Layout tweaks by StablePath (from the editor).</summary>
	public readonly Dictionary<string, LayoutAdjust> Layout = new();

	public void ApplyLayout( DisplayObject d )
	{
		if ( Layout.Count == 0 ) { d.Adjust = null; return; }
		d.Adjust = Layout.TryGetValue( d.StablePath, out var a ) ? a : null;
	}

	/// <summary>Re-match every object against the layout table (after loading or editing).</summary>
	public void ReapplyLayout()
	{
		if ( Root == null ) return;
		void Walk( DisplayObject d )
		{
			if ( d != Root ) ApplyLayout( d );
			if ( d is MovieClip mc ) foreach ( var c in mc.Children ) Walk( c );
			if ( d is ButtonObj b ) foreach ( var l in b.StateChildren.Values ) foreach ( var c in l ) Walk( c );
		}
		Walk( Root );
	}

	// ======================================================================= lifecycle

	public void Start()
	{
		Root = new MovieClip( this, Lib.Main ) { Name = "" };
		Root.Def = null;
		// DoInitAction blocks define classes/prototypes before anything else runs
		foreach ( var ia in Lib.Init ) Vm.RunScript( Root, ia.Code );
		Construct( Root );
		RunQueue();
		SyncTexts( Root );
	}

	/// <summary>Advance by real time; runs as many SWF frames as are due.</summary>
	public void Tick( double dtSeconds )
	{
		TimeMs += dtSeconds * 1000.0;
		RunIntervals();
		accum += dtSeconds;
		double frameTime = 1.0 / Lib.FrameRate;
		int n = 0;
		while ( accum >= frameTime && n < 4 )
		{
			accum -= frameTime;
			Frame();
			n++;
		}
		if ( accum > frameTime * 4 ) accum = 0;
	}

	/// <summary>One SWF frame.</summary>
	public void Frame()
	{
		FrameNumber++;
		// newest clips first (Flash/Ruffle AVM1 execution order)
		var list = new List<MovieClip>();
		MovieClip prev = null;
		for ( var c = execHead; c != null; c = c.NextExec )
		{
			if ( c.Removed )
			{
				if ( prev == null ) execHead = c.NextExec; else prev.NextExec = c.NextExec;
				c.InExecList = false;
				continue;
			}
			list.Add( c );
			prev = c;
		}
		foreach ( var c in list )
		{
			if ( c.Removed ) continue;
			if ( !c.Initialized ) { DispatchEvent( c, "Load" ); c.Initialized = true; }
			else DispatchEvent( c, "EnterFrame" );
			if ( c.Playing && !c.Removed ) c.AdvanceFrame();
		}
		Builtins.UpdateSounds( this );
		RunQueue();
		UpdateDrag();
		SyncTexts( Root );
		RecheckHover();
	}

	// ======================================================================= creation

	public DisplayObject Instantiate( int charId, MovieClip parent )
	{
		var def = Lib.Get( charId );
		switch ( def )
		{
			case SpriteDef sd:
				{
					var mc = new MovieClip( this, sd );
					if ( sd.ExportName != null && RegisteredClasses.TryGetValue( sd.ExportName, out var cls ) )
					{
						mc.RegisteredClass = cls;
						var proto = cls.Get( "prototype", Vm ).AsObj;
						if ( proto != null ) mc.Proto = proto;
					}
					return mc;
				}
			case ShapeDef s: return new ShapeObj( this, s );
			case MorphDef m: return new MorphObj( this, m );
			case StaticTextDef t: return new StaticTextObj( this, t );
			case EditTextDef e: return new EditText( this, e );
			case ButtonDef b: return new ButtonObj( this, b );
		}
		return null;
	}

	/// <summary>A newly placed/attached object starts running (load event + first frame).</summary>
	public void Construct( DisplayObject d, AsObject initObject = null, bool byScript = false )
	{
		if ( d is not MovieClip mc ) return;
		if ( !mc.InExecList )
		{
			mc.NextExec = execHead;
			execHead = mc;
			mc.InExecList = true;
		}
		// a registered AS2 class's constructor runs first for timeline-placed clips
		if ( mc.RegisteredClass != null && !byScript ) QueueCall( mc.RegisteredClass, Value.From( mc ), Array.Empty<Value>() );
		// Load event fires before the first frame's scripts
		DispatchEvent( mc, "Load" );
		mc.Initialized = true;
		if ( mc.Playing ) mc.Seek( 1, true );
		else if ( mc.CurrentFrame == 0 ) mc.Seek( 1, true );

		if ( initObject != null )
		{
			var keys = initObject.EnumerableKeys();
			for ( int i = keys.Count - 1; i >= 0; i-- ) mc.Set( keys[i], initObject.Get( keys[i], Vm ), Vm );
		}
		if ( mc.RegisteredClass != null && byScript ) SafeCall( mc.RegisteredClass, Value.From( mc ), Array.Empty<Value>() );
	}

	public void Unload( DisplayObject d, bool recursiveOnly = false )
	{
		if ( d.Removed ) return;
		if ( d is MovieClip mc ) DispatchEvent( mc, "Unload" );
		d.OnRemoved();
		if ( ReferenceEquals( hover, d ) ) hover = null;
		if ( ReferenceEquals( pressed, d ) ) pressed = null;
		if ( ReferenceEquals( dragTarget, d ) ) dragTarget = null;
		focusedText = focusedText != null && focusedText.Removed ? null : focusedText;
	}

	// ======================================================================= actions

	public void QueueAction( DisplayObject target, byte[] code ) => queue.Add( (target, code, null, default, null) );

	public void QueueCall( AsFunction fn, Value thisV, Value[] args ) => queue.Add( (null, null, fn, thisV, args) );

	public void RunQueue()
	{
		int guard = 0;
		while ( queue.Count > 0 && guard++ < 100000 )
		{
			var item = queue[0];
			queue.RemoveAt( 0 );
			if ( item.code != null )
			{
				if ( item.target is { Removed: true } ) continue;
				Vm.RunScript( item.target, item.code );
			}
			else if ( item.fn != null )
			{
				SafeCall( item.fn, item.thisV, item.args );
			}
		}
	}

	public Value SafeCall( AsFunction fn, Value thisV, Value[] args )
	{
		try { return Vm.CallFunction( fn, thisV, args ); }
		catch ( AsThrow t ) { Vm.Trace( "Uncaught exception: " + t.Thrown.ToStr( Vm ) ); }
		catch ( ScriptAbort a ) { Vm.Trace( "Script aborted: " + a.Message ); }
		return Value.Undefined;
	}

	/// <summary>Clip event (onClipEvent handlers on the placement + the onX method).</summary>
	public void DispatchEvent( MovieClip mc, string ev )
	{
		if ( mc.ClipActions != null )
			foreach ( var ca in mc.ClipActions )
				if ( ca.Events.Contains( ev ) ) QueueAction( mc, ca.Code );
		string method = ev switch
		{
			"Load" => "onLoad", "EnterFrame" => "onEnterFrame", "Unload" => "onUnload", "MouseDown" => "onMouseDown",
			"MouseUp" => "onMouseUp", "MouseMove" => "onMouseMove", "KeyDown" => "onKeyDown", "KeyUp" => "onKeyUp", "Data" => "onData",
			_ => null
		};
		if ( method == null ) return;
		var f = mc.Get( method, Vm ).AsObj as AsFunction;
		if ( f != null ) QueueCall( f, Value.From( mc ), Array.Empty<Value>() );
	}

	void BroadcastClipEvent( MovieClip root, string ev )
	{
		for ( var c = execHead; c != null; c = c.NextExec )
			if ( !c.Removed ) DispatchEvent( c, ev );
	}

	// ======================================================================= text fields

	void SyncTexts( MovieClip mc )
	{
		foreach ( var c in mc.Children )
		{
			if ( c is EditText et ) et.Sync();
			else if ( c is MovieClip m ) SyncTexts( m );
			else if ( c is ButtonObj b )
				foreach ( var l in b.StateChildren.Values ) foreach ( var x in l ) if ( x is EditText bt ) { bt.Parent = null; }
		}
	}

	public EditText focusedText;

	// ======================================================================= mouse

	public double MouseX, MouseY;
	public bool MouseIsDown;
	DisplayObject hover, pressed;
	public DisplayObject Hovered => hover;
	public DisplayObject DropTarget;
	public bool HandCursor => hover is ButtonObj b ? b.UseHandCursor : hover is MovieClip m && m.UseHandCursor && hover != null;

	public void MouseMove( double x, double y )
	{
		MouseX = x; MouseY = y;
		BroadcastClipEvent( Root, "MouseMove" );
		BroadcastListeners( "Mouse", "onMouseMove" );
		UpdateDrag();
		RecheckHover();
		RunQueue();
	}

	public void MouseDown()
	{
		MouseIsDown = true;
		BroadcastClipEvent( Root, "MouseDown" );
		BroadcastListeners( "Mouse", "onMouseDown" );
		RecheckHover();
		var target = hover;
		pressed = target;
		if ( target is ButtonObj b ) { b.State = "d"; RunButtonActions( b, "press" ); }
		else if ( target is MovieClip mc ) CallHandler( mc, "onPress" );
		// clicking an input field focuses it
		var t = FindTextField( Root, MouseX, MouseY );
		focusedText = t != null && t.Edit.Input ? t : null;
		RunQueue();
	}

	public void MouseUp()
	{
		MouseIsDown = false;
		BroadcastClipEvent( Root, "MouseUp" );
		BroadcastListeners( "Mouse", "onMouseUp" );
		RecheckHover();
		var p = pressed;
		pressed = null;
		if ( p != null && !p.Removed )
		{
			if ( ReferenceEquals( p, hover ) )
			{
				if ( p is ButtonObj b ) { b.State = "o"; RunButtonActions( b, "release" ); }
				else if ( p is MovieClip mc ) CallHandler( mc, "onRelease" );
			}
			else
			{
				if ( p is ButtonObj b ) { b.State = "u"; RunButtonActions( b, "releaseOutside" ); }
				else if ( p is MovieClip mc ) CallHandler( mc, "onReleaseOutside" );
			}
		}
		RunQueue();
	}

	void RecheckHover()
	{
		if ( Root == null ) return;
		var now = FindInteractive( Root, MouseX, MouseY );
		if ( ReferenceEquals( now, hover ) ) return;
		var old = hover;
		hover = now;
		if ( old != null && !old.Removed )
		{
			if ( old is ButtonObj ob ) { ob.State = "u"; RunButtonActions( ob, MouseIsDown && ReferenceEquals( pressed, old ) ? "dragOut" : "rollOut" ); }
			else if ( old is MovieClip om ) CallHandler( om, MouseIsDown && ReferenceEquals( pressed, old ) ? "onDragOut" : "onRollOut" );
		}
		if ( now != null )
		{
			if ( now is ButtonObj nb ) { nb.State = MouseIsDown && ReferenceEquals( pressed, now ) ? "d" : "o"; RunButtonActions( nb, MouseIsDown && ReferenceEquals( pressed, now ) ? "dragOver" : "rollOver" ); }
			else if ( now is MovieClip nm ) CallHandler( nm, MouseIsDown && ReferenceEquals( pressed, now ) ? "onDragOver" : "onRollOver" );
		}
	}

	DisplayObject FindInteractive( MovieClip mc, double x, double y )
	{
		if ( !mc.Visible || mc.Removed ) return null;
		for ( int i = mc.Children.Count - 1; i >= 0; i-- )
		{
			var c = mc.Children[i];
			if ( c.Removed || !c.Visible || c.ClipDepth > 0 || c.MaskFor != null ) continue;
			if ( c is ButtonObj b )
			{
				if ( b.Enabled && b.HitTestPoint( x, y, true ) ) return b;
			}
			else if ( c is MovieClip m )
			{
				if ( m.Enabled && m.HasButtonHandlers )
				{
					if ( m.HitTestPoint( x, y, true ) ) return m;
					continue;
				}
				var r = FindInteractive( m, x, y );
				if ( r != null ) return r;
			}
		}
		return null;
	}

	EditText FindTextField( MovieClip mc, double x, double y )
	{
		for ( int i = mc.Children.Count - 1; i >= 0; i-- )
		{
			var c = mc.Children[i];
			if ( c.Removed || !c.Visible ) continue;
			if ( c is EditText e && e.HitTestPoint( x, y, false ) ) return e;
			if ( c is MovieClip m ) { var r = FindTextField( m, x, y ); if ( r != null ) return r; }
		}
		return null;
	}

	void RunButtonActions( ButtonObj b, string cond )
	{
		var target = b.Parent ?? Root;
		foreach ( var a in b.Button.Actions )
			if ( a.On.Contains( cond ) ) QueueAction( target, a.Code );
		// AS2 handlers assigned by script: myButton.onRelease = function() {...}
		string handler = cond switch
		{
			"press" => "onPress", "release" => "onRelease", "releaseOutside" => "onReleaseOutside", "rollOver" => "onRollOver",
			"rollOut" => "onRollOut", "dragOut" => "onDragOut", "dragOver" => "onDragOver", _ => null
		};
		if ( handler == null ) return;
		var f = b.Get( handler, Vm ).AsObj as AsFunction;
		if ( f != null ) QueueCall( f, Value.From( b ), Array.Empty<Value>() );
	}

	void CallHandler( MovieClip mc, string name )
	{
		var f = mc.Get( name, Vm ).AsObj as AsFunction;
		if ( f != null ) QueueCall( f, Value.From( mc ), Array.Empty<Value>() );
	}

	// ======================================================================= keyboard

	public readonly HashSet<int> KeysDown = new();
	public int LastKey, LastAscii;

	public void KeyDown( int keyCode, int ascii )
	{
		KeysDown.Add( keyCode );
		LastKey = keyCode; LastAscii = ascii;
		BroadcastClipEvent( Root, "KeyDown" );
		BroadcastListeners( "Key", "onKeyDown" );
		RunQueue();
	}

	public void KeyUp( int keyCode, int ascii )
	{
		KeysDown.Remove( keyCode );
		LastKey = keyCode; LastAscii = ascii;
		BroadcastClipEvent( Root, "KeyUp" );
		BroadcastListeners( "Key", "onKeyUp" );
		RunQueue();
	}

	void BroadcastListeners( string globalName, string method )
	{
		var obj = Vm.Global.Get( globalName, Vm );
		if ( !obj.IsObject ) return;
		var listeners = obj.Obj.Get( "_listeners", Vm ).AsObj as AsArray;
		if ( listeners == null ) return;
		foreach ( var l in listeners.Items.ToArray() )
		{
			var f = Vm.GetMember( l, method ).AsObj as AsFunction;
			if ( f != null ) QueueCall( f, l, Array.Empty<Value>() );
		}
	}

	// ======================================================================= drag

	DisplayObject dragTarget;
	bool dragLock;
	double[] dragRect;
	double dragOffX, dragOffY;

	public void StartDrag( DisplayObject d, bool lockCenter, double[] rect )
	{
		dragTarget = d;
		dragLock = lockCenter;
		dragRect = rect;
		var (px, py) = ParentMouse( d );
		dragOffX = lockCenter ? 0 : d.X - px;
		dragOffY = lockCenter ? 0 : d.Y - py;
		UpdateDrag();
	}

	public void StopDrag()
	{
		dragTarget = null;
	}

	(double, double) ParentMouse( DisplayObject d )
	{
		var pm = d.Parent?.WorldMatrix ?? Matrix.Identity;
		return pm.Inverse().Apply( MouseX, MouseY );
	}

	void UpdateDrag()
	{
		if ( dragTarget == null || dragTarget.Removed ) return;
		var (px, py) = ParentMouse( dragTarget );
		double x = px + dragOffX, y = py + dragOffY;
		if ( dragRect != null )
		{
			x = Math.Clamp( x, Math.Min( dragRect[0], dragRect[2] ), Math.Max( dragRect[0], dragRect[2] ) );
			y = Math.Clamp( y, Math.Min( dragRect[1], dragRect[3] ), Math.Max( dragRect[1], dragRect[3] ) );
		}
		dragTarget.X = x; dragTarget.Y = y;
	}

	// ======================================================================= intervals

	sealed class Interval { public int Id; public AsFunction Fn; public Value This; public Value[] Args; public double Every, Next; public bool Once; }
	readonly List<Interval> intervals = new();
	int intervalId;

	public int SetInterval( AsFunction fn, Value thisV, double ms, Value[] args, bool once = false )
	{
		var iv = new Interval { Id = ++intervalId, Fn = fn, This = thisV, Args = args, Every = Math.Max( 1, ms ), Next = TimeMs + Math.Max( 1, ms ), Once = once };
		intervals.Add( iv );
		return iv.Id;
	}

	public void ClearInterval( int id ) => intervals.RemoveAll( i => i.Id == id );

	void RunIntervals()
	{
		if ( intervals.Count == 0 ) return;
		foreach ( var iv in intervals.ToArray() )
		{
			if ( TimeMs < iv.Next ) continue;
			iv.Next = TimeMs + iv.Every;
			if ( iv.Once ) intervals.Remove( iv );
			SafeCall( iv.Fn, iv.This, iv.Args );
			RunQueue();
		}
	}

	// ======================================================================= sound / url

	public readonly List<(SoundDef def, object handle, DisplayObject owner)> TimelineSounds = new();

	/// <summary>Volume set by a Sound object with no target (or _root): scales every sound, 0..1.</summary>
	public double GlobalVolume = 1;

	public void SetGlobalVolume( double v )
	{
		GlobalVolume = Math.Clamp( v, 0, 1 );
		foreach ( var s in TimelineSounds ) Host?.SetSoundVolume( s.handle, GlobalVolume, 0 );
		Builtins.RefreshSoundVolumes( this );
	}

	public void StartSound( int soundId, bool stop, int loops, DisplayObject owner, bool noMultiple = false )
	{
		if ( Lib.Get( soundId ) is not SoundDef sd ) return;
		// forget finished ones
		for ( int i = TimelineSounds.Count - 1; i >= 0; i-- )
			if ( Host == null || !Host.IsSoundPlaying( TimelineSounds[i].handle ) ) TimelineSounds.RemoveAt( i );
		if ( stop )
		{
			for ( int i = TimelineSounds.Count - 1; i >= 0; i-- )
				if ( TimelineSounds[i].def == sd ) { Host?.StopSound( TimelineSounds[i].handle ); TimelineSounds.RemoveAt( i ); }
			return;
		}
		if ( noMultiple )
			foreach ( var t in TimelineSounds ) if ( t.def == sd ) return;
		var h = Host?.PlaySound( sd, GlobalVolume, 0, Math.Max( 1, loops ), 0 );
		if ( h != null ) TimelineSounds.Add( (sd, h, owner) );
	}

	public void StopAllSounds()
	{
		foreach ( var s in TimelineSounds ) Host?.StopSound( s.handle );
		TimelineSounds.Clear();
		Builtins.StopAllSoundObjects( this );
	}

	public void GetUrl( string url, string window )
	{
		if ( url.StartsWith( "FSCommand:", StringComparison.OrdinalIgnoreCase ) )
		{
			var cmd = url.Substring( 10 );
			if ( cmd == "fullscreen" ) Host?.SetFullScreen( window == "true" );
			return;
		}
		Host?.GetUrl( url, window );
	}

	// ======================================================================= rendering

	/// <summary>Flattens the display list into draw items, back to front.</summary>
	public void CollectRender( List<RenderItem> output )
	{
		output.Clear();
		if ( Root == null ) return;
		Collect( Root, Matrix.Identity, ColorTransform.Identity, null, null, output );
		for ( int i = 0; i < output.Count; i++ ) output[i].Index = i;
	}

	void Collect( DisplayObject d, Matrix parentM, ColorTransform parentCx, List<Filter> parentFilters, List<RenderItem> masks, List<RenderItem> output )
	{
		if ( d.Removed || !d.Visible || (d.Adjust != null && d.Adjust.Hidden) ) return;
		var m = parentM.Mul( d.EffectiveMatrix );
		var cx = parentCx.Mul( d.Cx );
		if ( cx.Am <= 0 && cx.Aa <= 0 ) return;
		var filters = parentFilters;
		if ( d.Filters != null && d.Filters.Count > 0 )
		{
			filters = parentFilters == null ? new List<Filter>( d.Filters ) : new List<Filter>( parentFilters );
			if ( parentFilters != null ) filters.AddRange( d.Filters );
		}
		if ( d.MaskedBy != null && !d.MaskedBy.Removed )
		{
			var mi = new List<RenderItem>();
			CollectMask( d.MaskedBy, d.MaskedBy.RenderParent?.WorldMatrix ?? Matrix.Identity, mi );
			masks = masks == null ? mi : Concat( masks, mi );
		}

		switch ( d )
		{
			case MovieClip mc:
				{
					List<RenderItem> layerMask = null;
					int maskUntil = int.MinValue;
					foreach ( var c in mc.Children )
					{
						if ( c.Depth > maskUntil ) { layerMask = null; maskUntil = int.MinValue; }
						if ( c.ClipDepth > 0 )
						{
							layerMask = new List<RenderItem>();
							CollectMask( c, m, layerMask );
							maskUntil = c.TimelineDepth >= 0 ? c.ClipDepth + MovieClip.TimelineDepthOffset : c.ClipDepth;
							continue;
						}
						if ( c.MaskFor != null ) continue; // used as a setMask mask
						Collect( c, m, cx, filters, layerMask != null ? (masks == null ? layerMask : Concat( masks, layerMask )) : masks, output );
					}
					break;
				}
			case ButtonObj b:
				{
					var list = b.Current;
					if ( list != null ) foreach ( var c in list ) Collect( c, m, cx, filters, masks, output );
					break;
				}
			default:
				{
					var it = new RenderItem { Obj = d, World = m, Cx = cx, Filters = filters, Masks = masks };
					if ( masks != null ) it.Clip = MaskBounds( masks );
					output.Add( it );
					break;
				}
		}
	}

	static List<RenderItem> Concat( List<RenderItem> a, List<RenderItem> b ) { var r = new List<RenderItem>( a ); r.AddRange( b ); return r; }

	/// <summary>The shapes of a mask (visibility and color are ignored, as in Flash).</summary>
	void CollectMask( DisplayObject d, Matrix parentM, List<RenderItem> output )
	{
		if ( d.Removed ) return;
		var m = parentM.Mul( d.EffectiveMatrix );
		switch ( d )
		{
			case MovieClip mc: foreach ( var c in mc.Children ) if ( c.ClipDepth == 0 ) CollectMask( c, m, output ); break;
			case ButtonObj b: if ( b.Current != null ) foreach ( var c in b.Current ) CollectMask( c, m, output ); break;
			default: output.Add( new RenderItem { Obj = d, World = m, Cx = ColorTransform.Identity } ); break;
		}
	}

	static double[] MaskBounds( List<RenderItem> masks )
	{
		double x0 = double.MaxValue, y0 = double.MaxValue, x1 = double.MinValue, y1 = double.MinValue;
		foreach ( var mi in masks )
		{
			var b = mi.Obj.BoundsIn( mi.World );
			if ( b == null ) continue;
			x0 = Math.Min( x0, b.Value.x0 ); y0 = Math.Min( y0, b.Value.y0 ); x1 = Math.Max( x1, b.Value.x1 ); y1 = Math.Max( y1, b.Value.y1 );
		}
		return x0 == double.MaxValue ? new double[] { 0, 0, 0, 0 } : new[] { x0, y0, x1, y1 };
	}

	static double[] Intersect( double[] a, double[] b )
	{
		if ( a == null ) return b;
		if ( b == null ) return a;
		return new[] { Math.Max( a[0], b[0] ), Math.Max( a[1], b[1] ), Math.Min( a[2], b[2] ), Math.Min( a[3], b[3] ) };
	}
}