Error LoadRotatedGraphic added

Error LoadRotatedGraphic added

Strict

Import mojo

Import flxextern
Import flxpoint
Import flxobject
Import flxcamera
Import flxg

Import system.flxanim
Import system.flxcolor

Import "data/flx_default.png"

Class FlxSprite Extends FlxObject

	Global ClassObject:FlxClass = new FlxSpriteClass()

	Field origin:FlxPoint

	Field offset:FlxPoint

	Field scale:FlxPoint

	Field blend:Int

	Field finished:Bool

	Field frameWidth:Int

	Field frameHeight:Int

	Field frames:Int

	Field dirty:Bool

	Field _color:FlxColor

	Field _mixedColor:FlxColor

	Field _camera:FlxCamera

Private
	Global _GraphicLoader:FlxGraphicLoader = New FlxGraphicLoader()

	Field _animations:StringMap<FlxAnim>

	Field _flipped:Bool

	Field _flipNeeded:Bool

	Field _curAnim:FlxAnim

	Field _curFrame:Int

	Field _curIndex:Int

	Field _frameTimer:Float

	Field _callback:FlxAnimationListener

	Field _facing:Int

	Field _alpha:Float

	Field _bakedRotation:Float

	Field _pixels:Image

	Field _matrix:Float[6]

	Field _surfaceColor:FlxColor

	Field _halfWidth:Float

	Field _halfHeight:Float

Public
	Method New(x:Float = 0, y:Float = 0, simpleGraphic:String = "")
		Super.New(x, y)

		offset = New FlxPoint()
		origin = New FlxPoint()

		scale = New FlxPoint(1.0, 1.0)
		_alpha = 1
		_color = New FlxColor(FlxG.WHITE)
		blend = AlphaBlend

		finished = False
		_facing = RIGHT
		_animations = New StringMap<FlxAnim>
		_flipped = False
		_curAnim = Null
		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0

		_callback = Null

		_surfaceColor = New FlxColor(FlxG.WHITE)
		_mixedColor = New FlxColor(FlxG.WHITE)

		If (simpleGraphic.Length() = 0)
			simpleGraphic = FlxG.DATA_PREFIX + "default"
		End If

		LoadGraphic(simpleGraphic)
	End Method

	Method Destroy:Void()
		If (_animations <> Null) Then
			_animations.Clear()
			_animations = Null
		End If

		offset = Null
		origin = Null
		scale = Null
		_curAnim = Null
		_callback = Null
		_color = Null
		_surfaceColor = Null
		_mixedColor = Null
		_camera = Null
	End Method

	Method LoadGraphic:FlxSprite(graphic:String, animated:Bool = False, reverse:Bool = False, width:Int = 0, height:Int = 0, unique:Bool = False)
		_bakedRotation = 0

		_GraphicLoader.name = graphic
		_GraphicLoader.animated = animated
		_GraphicLoader.width = width
		_GraphicLoader.height = height

		_pixels = FlxG.AddBitmap(graphic, _GraphicLoader, unique)

		If (reverse) Then
			_flipped = True
		Else
			_flipped = True
		End if

		Self.width = _pixels.Width()
		frameWidth = Self.width

		Self.height = _pixels.Height()
		frameHeight = Self.height

		_ResetHelpers()

		Return Self
	End Method

	Method LoadRotatedGraphic:FlxSprite(graphic:String, rotations:Int = 16, frame:Int = -1)
		Error "LoadRotatedGraphic not currenlty support in " + FlxG.LIBRARY_NAME
		'TODO
		Return Null
	End Method

	Method MakeGraphic:FlxSprite(width:Int, height:Int, color:Int = FlxG.WHITE)
		_pixels = Null
		_bakedRotation = 0
		_surfaceColor.SetARGB(color)

		Self.width = width
		frameWidth = width
		Self.height = height
		frameHeight = height

		_ResetHelpers()

		Return Self
	End Method

	Method PostUpdate:Void()
		Super.PostUpdate()
		_UpdateAnimation()
	End Method

	Method Draw:Void()
		If (_flickerTimer <> 0) Then
			_flicker = Not _flicker
			If (_flicker) Return
		End If

		If (Not OnScreen(FlxG._CurrentCamera)) Return

		If (dirty) _CalcFrame()

		_camera = FlxG._CurrentCamera

		If (FlxG._LastDrawingBlend <> blend) Then
			SetBlend(blend)
			FlxG._LastDrawingBlend = blend
		End If

		_point.x = x - int(_camera.scroll.x * scrollFactor.x) - offset.x
		_point.y = y - int(_camera.scroll.y * scrollFactor.y) - offset.y

		If (_point.x > 0) Then
			_point.x += 0.0000001
		Else
			_point.x -= 0.0000001
		End If

		If (_point.y > 0) Then
			_point.y += 0.0000001
		Else
			_point.y -= 0.0000001
		End If

		If (_pixels <> Null) Then
			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_color, _camera._color)

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
					FlxG._LastDrawingColor = _mixedColor.argb
				End If
			Else
				If (FlxG._LastDrawingColor <> _color.argb) Then
					SetColor(_color.r, _color.g, _color.b)
					FlxG._LastDrawingColor = _color.argb
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _alpha

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
					FlxG._LastDrawingAlpha = _mixedAlpha
				End If
			Else
				If (FlxG._LastDrawingAlpha <> _alpha) Then
					SetAlpha(_alpha)
					FlxG._LastDrawingAlpha = _alpha
				End If
			End If

			If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
				If (Not _flipNeeded) Then
					DrawImage(_pixels, _point.x, _point.y, _curIndex)
				Else
					PushMatrix()
						Transform(-1, 0, 0, 1, _point.x + _halfWidth, _point.y + _halfHeight)
						DrawImage(_pixels, -_halfWidth, -_halfHeight, _curIndex)
					PopMatrix()
				End If
			Else
				PushMatrix()
					'Translate
					_matrix[4] = _point.x + origin.x
					_matrix[5] = _point.y + origin.y

					'Scale
					_matrix[0] = scale.x
					_matrix[3] = scale.y

					'Rotate
					If (angle <> 0 And _bakedRotation = 0) Then
						Local sin:Float = -Sin(angle)
						Local cos:Float = Cos(angle)

						_matrix[1] = -sin * _matrix[3]
						_matrix[2] = _matrix[0] * sin
						_matrix[0] *= cos
						_matrix[3] = cos * _matrix[3]
					End If

					Transform(_matrix[0], _matrix[1], _matrix[2], _matrix[3], _matrix[4], _matrix[5])

					If (_flipNeeded) Then
						Transform(-1, 0, 0, 1, _halfWidth - origin.x, _halfHeight - origin.y)
					End If

					DrawImage(_pixels, -origin.x, -origin.y, _curIndex)
				PopMatrix()
			End If
		Else
			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_surfaceColor, _camera._color)

				If (_color.argb <> FlxG.WHITE) Then
					_mixedColor.MixRGB(_color)
				End If

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
					FlxG._LastDrawingColor = _mixedColor.argb
				End If
			Else
				If (_color.argb <> FlxG.WHITE) Then
					_mixedColor.MixRGB(_surfaceColor, _color)

					If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
						SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
						FlxG._LastDrawingColor = _mixedColor.argb
					End If
				Else
					If (FlxG._LastDrawingColor <> _surfaceColor.argb) Then
						SetColor(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b)
						FlxG._LastDrawingColor = _surfaceColor.argb
					End If
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _alpha

				If (_surfaceColor.a < 1) Then
					_mixedAlpha *= _surfaceColor.a
				End If

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
					FlxG._LastDrawingAlpha = _mixedAlpha
				End If
			Else
				If (_surfaceColor.a < 1) Then
					Local _mixedAlpha:Float = _surfaceColor.a * _alpha

					If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
						SetAlpha(_mixedAlpha)
						FlxG._LastDrawingAlpha = _mixedAlpha
					End If
				Else
					If (FlxG._LastDrawingAlpha <> _alpha) Then
						SetAlpha(_alpha)
						FlxG._LastDrawingAlpha = _alpha
					End If
				End If
			End If

			If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
				_DrawSurface(_point.x, _point.y)
			Else
				PushMatrix()
					'Translate
					_matrix[4] = _point.x + origin.x
					_matrix[5] = _point.y + origin.y

					'Scale
					_matrix[0] = scale.x
					_matrix[3] = scale.y

					'Rotate
					If (angle <> 0 And _bakedRotation = 0) Then
						Local sin:Float = -Sin(angle)
						Local cos:Float = Cos(angle)

						_matrix[1] = -sin * _matrix[3]
						_matrix[2] = _matrix[0] * sin
						_matrix[0] *= cos
						_matrix[3] = cos * _matrix[3]
					End If

					Transform(_matrix[0], _matrix[1], _matrix[2], _matrix[3], _matrix[4], _matrix[5])
					_DrawSurface(-origin.x, -origin.y)
				PopMatrix()
			End If
		End If

		_VisibleCount += 1;
		If(FlxG.VisualDebug And Not ignoreDrawDebug) DrawDebug(_camera);
	End Method

	Method DrawFrame:Void(force:Bool = False)
		If (force Or dirty) _CalcFrame()
	End Method

	Method AddAnimation:Void(name:String, frames:Int[], frameRate:Float = 0, looped:Bool = True)
		_animations.Set(name, New FlxAnim(name, frames, frameRate, looped))
	End Method

	Method AddAnimationCallback:Void(animationCallback:FlxAnimationCallback)
		_callback = animationCallback
	End Method

	Method Play:Void(animName:String, force:Bool = False)
		If (Not force And _curAnim <> Null And animName = _curAnim.name And( _curAnim.looped Or Not finished )) Return

		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0

		Local anim:FlxAnim = _animations.Get(animName)

		If (anim <> Null) Then
			_curAnim = anim

			If (_curAnim.delay <= 0) Then
				finished = True
			Else
				finished = False
			End If

			_curIndex = _curAnim.frames[_curFrame]
			dirty = True
			Return
		End If

		FlxG.Log("WARNING: No animation called ~q" + animName + "~q")
	End Method

	Method RandomFrame:Void()
		_curAnim = Null

		If (_pixels <> Null) Then
			_curIndex = Int(FlxG.Random() * (_pixels.Frames() - 1))
		Else
			_curIndex = 0
		End If

		dirty = True
	End Method

	Method SetOriginToCorner:Void()
		origin.x = 0
		origin.y = 0
	End Method

	Method CenterOffsets:Void(adjustPosition:Bool = False)
		offset.x = (frameWidth - width) * .5
		offset.y = (frameHeight - height) * .5

		If (adjustPosition) Then
			x += offset.x
			y += offset.y
		End If
	End Method

	Method Pixels:Image() Property
		Return _pixels
	End Method

	Method Pixels:Void(pixels:Image) Property
		_pixels = pixels

		If (_pixels <> Null) Then
			width = pixels.Width()
			height = pixels.Height()
		Else
			width = 0
			height = 0
		End If

		frameWidth = width
		frameHeight = height
		_ResetHelpers()
	End Method

	Method Facing:Int() Property
		Return _facing
	End Method

	Method Facing:Void(direction:Int) Property
		If (_facing <> direction) dirty = True
		_facing = direction

		_flipNeeded = (_flipped And _facing = LEFT)
	End Method

	Method Alpha:Float() Property
		Return _alpha
	End Method

	Method Alpha:Void(alpha:Float) Property
		If (alpha < 0) alpha = 0
		_alpha = alpha
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetRGB(color)
	End Method

	Method Frame:Int() Property
		Return _curIndex
	End Method

	Method Frame:Void(frame:Int) Property
		_curAnim = Null
		_curIndex = frame
		dirty = True
	End Method

	Method OnScreen:Bool(camera:FlxCamera)
		If (camera = Null) camera = FlxG.Camera
		GetScreenXY(_point, camera)

		_point.x = _point.x - offset.x
		_point.y = _point.y - offset.y

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			Return _point.x + frameWidth > 0 And _point.x < camera.Width And _point.y + frameHeight > 0 And _point.y < camera.Height
		End If

		Local radius:Float = Sqrt(_halfWidth * _halfWidth + _halfHeight * _halfHeight) * Max(Abs(scale.x), Abs(scale.y))
		_point.x += _halfWidth
		_point.y += _halfHeight

		Return _point.x + radius > 0 And _point.x - radius < camera.Width And _point.y + radius > 0 And _point.y - radius < camera.Height
	End Method

	Method _DrawSurface:Void(x:Float, y:Float)
		DrawRect(x, y, frameWidth, frameHeight)
	End Method

	Method _ResetHelpers:Void()
		_halfWidth = frameWidth * .5
		_halfHeight = frameHeight * .5
		origin.Make(_halfWidth, _halfHeight)

		If (_pixels <> Null) Then
			frames = _pixels.Frames()
		Else
			frames = 1
		End If

		_curIndex = 0
		_mixedColor.SetARGB(FlxG.WHITE)
	End Method

	Method ToString:String()
		Return "FlxSprite"
	End Method

Private
	Method _UpdateAnimation:Void()
		If (_bakedRotation > 0) Then
			Local oldIndex:Int = _curIndex
			Local angleHelper:Int = angle Mod 360

			If (angleHelper < 0) angleHelper += 360
			_curIndex = angleHelper / _bakedRotation + 0.5

			If (oldIndex <> _curIndex) dirty = True

		ElseIf (_curAnim <> Null And _curAnim.delay > 0 And (_curAnim.looped Or Not finished))
			_frameTimer += FlxG.Elapsed
			While (_frameTimer > _curAnim.delay)
				_frameTimer -= _curAnim.delay

				If (_curFrame = _curAnim.frames.Length() - 1) Then
					If (_curAnim.looped) _curFrame = 0
					finished = True
				Else
					_curFrame += 1
				End If

				_curIndex = _curAnim.frames[_curFrame]
				dirty = True
			Wend
		End If

		If (dirty) _CalcFrame()
	End Method

	Method _CalcFrame:Void()
		If (_callback <> Null) Then
			If (_curAnim <> Null) Then
				_callback.OnAnimationFrame(_curAnim.name, _curFrame, _curIndex)
			Else
				_callback.OnAnimationFrame("", _curFrame, _curIndex)
			End If
		End If
		dirty = False
	End Method

End Class

Private
Class FlxSpriteClass Implements FlxClass

	Method CreateInstance:Object()
		Return New FlxSprite()
	End Method

	Method InstanceOf:Bool(object:Object)
		Return (FlxSprite(object) <> Null)
	End Method

End Class

Class FlxGraphicLoader Extends FlxResourceLoader<Image>

	Field name:String
	Field animated:Bool
	Field width:Float
	Field height:Float

	Method Load:Image(name:String)
		Local image:Image = LoadImage(FlxAssetsManager.GetImagePath(Self.name))

		If (Not animated) Then
			Return image
		Else
			Local frames:Int

			If (width = 0 And height = 0) Then
				width = image.Height()
			ElseIf (width = 0) Then
				width = height
			ElseIf (height = 0) Then
				height = width
			End If

			If (height = 0) Then
				frames = Ceil(image.Width() / width)
				height = width
			Else
				frames = Ceil((image.Width() * image.Height()) / (width * height))
			End If

			Return image.GrabImage(0, 0, width, height, frames)
		End If
	End Method

End Class

Strict

Import mojo

Import flxextern
Import flxpoint
Import flxobject
Import flxcamera
Import flxg

Import system.flxanim
Import system.flxcolor

Import "data/flx_default.png"

Class FlxSprite Extends FlxObject

	Global ClassObject:FlxClass = new FlxSpriteClass()

	Field origin:FlxPoint

	Field offset:FlxPoint

	Field scale:FlxPoint

	Field blend:Int

	Field finished:Bool

	Field frameWidth:Int

	Field frameHeight:Int

	Field frames:Int

	Field dirty:Bool

	Field _color:FlxColor

	Field _mixedColor:FlxColor

	Field _camera:FlxCamera

Private
	Global _GraphicLoader:FlxGraphicLoader = New FlxGraphicLoader()

	Field _animations:StringMap<FlxAnim>

	Field _flipped:Bool

	Field _flipNeeded:Bool

	Field _curAnim:FlxAnim

	Field _curFrame:Int

	Field _curIndex:Int

	Field _frameTimer:Float

	Field _callback:FlxAnimationListener

	Field _facing:Int

	Field _alpha:Float

	Field _bakedRotation:Float

	Field _pixels:Image

	Field _matrix:Float[6]

	Field _surfaceColor:FlxColor

	Field _halfWidth:Float

	Field _halfHeight:Float

Public
	Method New(x:Float = 0, y:Float = 0, simpleGraphic:String = "")
		Super.New(x, y)

		offset = New FlxPoint()
		origin = New FlxPoint()

		scale = New FlxPoint(1.0, 1.0)
		_alpha = 1
		_color = New FlxColor(FlxG.WHITE)
		blend = AlphaBlend

		finished = False
		_facing = RIGHT
		_animations = New StringMap<FlxAnim>
		_flipped = False
		_curAnim = Null
		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0

		_callback = Null

		_surfaceColor = New FlxColor(FlxG.WHITE)
		_mixedColor = New FlxColor(FlxG.WHITE)

		If (simpleGraphic.Length() = 0)
			simpleGraphic = FlxG.DATA_PREFIX + "default"
		End If

		LoadGraphic(simpleGraphic)
	End Method

	Method Destroy:Void()
		If (_animations <> Null) Then
			_animations.Clear()
			_animations = Null
		End If

		offset = Null
		origin = Null
		scale = Null
		_curAnim = Null
		_callback = Null
		_color = Null
		_surfaceColor = Null
		_mixedColor = Null
		_camera = Null
	End Method

	Method LoadGraphic:FlxSprite(graphic:String, animated:Bool = False, reverse:Bool = False, width:Int = 0, height:Int = 0, unique:Bool = False)
		_bakedRotation = 0

		_GraphicLoader.name = graphic
		_GraphicLoader.animated = animated
		_GraphicLoader.width = width
		_GraphicLoader.height = height

		_pixels = FlxG.AddBitmap(graphic, _GraphicLoader, unique)

		If (reverse) Then
			_flipped = True
		Else
			_flipped = True
		End if

		Self.width = _pixels.Width()
		frameWidth = Self.width

		Self.height = _pixels.Height()
		frameHeight = Self.height

		_ResetHelpers()

		Return Self
	End Method

	Method LoadRotatedGraphic:FlxSprite(graphic:String, rotations:Int = 16, frame:Int = -1)
		'TODO
		Return Null
	End Method

	Method MakeGraphic:FlxSprite(width:Int, height:Int, color:Int = FlxG.WHITE)
		_pixels = Null
		_bakedRotation = 0
		_surfaceColor.SetARGB(color)

		Self.width = width
		frameWidth = width
		Self.height = height
		frameHeight = height

		_ResetHelpers()

		Return Self
	End Method

	Method PostUpdate:Void()
		Super.PostUpdate()
		_UpdateAnimation()
	End Method

	Method Draw:Void()
		If (_flickerTimer <> 0) Then
			_flicker = Not _flicker
			If (_flicker) Return
		End If

		If (Not OnScreen(FlxG._CurrentCamera)) Return

		If (dirty) _CalcFrame()

		_camera = FlxG._CurrentCamera

		If (FlxG._LastDrawingBlend <> blend) Then
			SetBlend(blend)
			FlxG._LastDrawingBlend = blend
		End If

		_point.x = x - int(_camera.scroll.x * scrollFactor.x) - offset.x
		_point.y = y - int(_camera.scroll.y * scrollFactor.y) - offset.y

		If (_point.x > 0) Then
			_point.x += 0.0000001
		Else
			_point.x -= 0.0000001
		End If

		If (_point.y > 0) Then
			_point.y += 0.0000001
		Else
			_point.y -= 0.0000001
		End If

		If (_pixels <> Null) Then
			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_color, _camera._color)

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
					FlxG._LastDrawingColor = _mixedColor.argb
				End If
			Else
				If (FlxG._LastDrawingColor <> _color.argb) Then
					SetColor(_color.r, _color.g, _color.b)
					FlxG._LastDrawingColor = _color.argb
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _alpha

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
					FlxG._LastDrawingAlpha = _mixedAlpha
				End If
			Else
				If (FlxG._LastDrawingAlpha <> _alpha) Then
					SetAlpha(_alpha)
					FlxG._LastDrawingAlpha = _alpha
				End If
			End If

			If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
				If (Not _flipNeeded) Then
					DrawImage(_pixels, _point.x, _point.y, _curIndex)
				Else
					PushMatrix()
						Transform(-1, 0, 0, 1, _point.x + _halfWidth, _point.y + _halfHeight)
						DrawImage(_pixels, -_halfWidth, -_halfHeight, _curIndex)
					PopMatrix()
				End If
			Else
				PushMatrix()
					'Translate
					_matrix[4] = _point.x + origin.x
					_matrix[5] = _point.y + origin.y

					'Scale
					_matrix[0] = scale.x
					_matrix[3] = scale.y

					'Rotate
					If (angle <> 0 And _bakedRotation = 0) Then
						Local sin:Float = -Sin(angle)
						Local cos:Float = Cos(angle)

						_matrix[1] = -sin * _matrix[3]
						_matrix[2] = _matrix[0] * sin
						_matrix[0] *= cos
						_matrix[3] = cos * _matrix[3]
					End If

					Transform(_matrix[0], _matrix[1], _matrix[2], _matrix[3], _matrix[4], _matrix[5])

					If (_flipNeeded) Then
						Transform(-1, 0, 0, 1, _halfWidth - origin.x, _halfHeight - origin.y)
					End If

					DrawImage(_pixels, -origin.x, -origin.y, _curIndex)
				PopMatrix()
			End If
		Else
			If (_camera.Color <> FlxG.WHITE) Then
				_mixedColor.MixRGB(_surfaceColor, _camera._color)

				If (_color.argb <> FlxG.WHITE) Then
					_mixedColor.MixRGB(_color)
				End If

				If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
					SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
					FlxG._LastDrawingColor = _mixedColor.argb
				End If
			Else
				If (_color.argb <> FlxG.WHITE) Then
					_mixedColor.MixRGB(_surfaceColor, _color)

					If (FlxG._LastDrawingColor <> _mixedColor.argb) Then
						SetColor(_mixedColor.r, _mixedColor.g, _mixedColor.b)
						FlxG._LastDrawingColor = _mixedColor.argb
					End If
				Else
					If (FlxG._LastDrawingColor <> _surfaceColor.argb) Then
						SetColor(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b)
						FlxG._LastDrawingColor = _surfaceColor.argb
					End If
				End If
			End If

			If (_camera.Alpha < 1) Then
				Local _mixedAlpha:Float = _camera.Alpha * _alpha

				If (_surfaceColor.a < 1) Then
					_mixedAlpha *= _surfaceColor.a
				End If

				If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
					SetAlpha(_mixedAlpha)
					FlxG._LastDrawingAlpha = _mixedAlpha
				End If
			Else
				If (_surfaceColor.a < 1) Then
					Local _mixedAlpha:Float = _surfaceColor.a * _alpha

					If (FlxG._LastDrawingAlpha <> _mixedAlpha) Then
						SetAlpha(_mixedAlpha)
						FlxG._LastDrawingAlpha = _mixedAlpha
					End If
				Else
					If (FlxG._LastDrawingAlpha <> _alpha) Then
						SetAlpha(_alpha)
						FlxG._LastDrawingAlpha = _alpha
					End If
				End If
			End If

			If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
				_DrawSurface(_point.x, _point.y)
			Else
				PushMatrix()
					'Translate
					_matrix[4] = _point.x + origin.x
					_matrix[5] = _point.y + origin.y

					'Scale
					_matrix[0] = scale.x
					_matrix[3] = scale.y

					'Rotate
					If (angle <> 0 And _bakedRotation = 0) Then
						Local sin:Float = -Sin(angle)
						Local cos:Float = Cos(angle)

						_matrix[1] = -sin * _matrix[3]
						_matrix[2] = _matrix[0] * sin
						_matrix[0] *= cos
						_matrix[3] = cos * _matrix[3]
					End If

					Transform(_matrix[0], _matrix[1], _matrix[2], _matrix[3], _matrix[4], _matrix[5])
					_DrawSurface(-origin.x, -origin.y)
				PopMatrix()
			End If
		End If

		_VisibleCount += 1;
		If(FlxG.VisualDebug And Not ignoreDrawDebug) DrawDebug(_camera);
	End Method

	Method DrawFrame:Void(force:Bool = False)
		If (force Or dirty) _CalcFrame()
	End Method

	Method AddAnimation:Void(name:String, frames:Int[], frameRate:Float = 0, looped:Bool = True)
		_animations.Set(name, New FlxAnim(name, frames, frameRate, looped))
	End Method

	Method AddAnimationCallback:Void(animationCallback:FlxAnimationCallback)
		_callback = animationCallback
	End Method

	Method Play:Void(animName:String, force:Bool = False)
		If (Not force And _curAnim <> Null And animName = _curAnim.name And( _curAnim.looped Or Not finished )) Return

		_curFrame = 0
		_curIndex = 0
		_frameTimer = 0

		Local anim:FlxAnim = _animations.Get(animName)

		If (anim <> Null) Then
			_curAnim = anim

			If (_curAnim.delay <= 0) Then
				finished = True
			Else
				finished = False
			End If

			_curIndex = _curAnim.frames[_curFrame]
			dirty = True
			Return
		End If

		FlxG.Log("WARNING: No animation called ~q" + animName + "~q")
	End Method

	Method RandomFrame:Void()
		_curAnim = Null

		If (_pixels <> Null) Then
			_curIndex = Int(FlxG.Random() * (_pixels.Frames() - 1))
		Else
			_curIndex = 0
		End If

		dirty = True
	End Method

	Method SetOriginToCorner:Void()
		origin.x = 0
		origin.y = 0
	End Method

	Method CenterOffsets:Void(adjustPosition:Bool = False)
		offset.x = (frameWidth - width) * .5
		offset.y = (frameHeight - height) * .5

		If (adjustPosition) Then
			x += offset.x
			y += offset.y
		End If
	End Method

	Method Pixels:Image() Property
		Return _pixels
	End Method

	Method Pixels:Void(pixels:Image) Property
		_pixels = pixels

		If (_pixels <> Null) Then
			width = pixels.Width()
			height = pixels.Height()
		Else
			width = 0
			height = 0
		End If

		frameWidth = width
		frameHeight = height
		_ResetHelpers()
	End Method

	Method Facing:Int() Property
		Return _facing
	End Method

	Method Facing:Void(direction:Int) Property
		If (_facing <> direction) dirty = True
		_facing = direction

		_flipNeeded = (_flipped And _facing = LEFT)
	End Method

	Method Alpha:Float() Property
		Return _alpha
	End Method

	Method Alpha:Void(alpha:Float) Property
		If (alpha < 0) alpha = 0
		_alpha = alpha
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetRGB(color)
	End Method

	Method Frame:Int() Property
		Return _curIndex
	End Method

	Method Frame:Void(frame:Int) Property
		_curAnim = Null
		_curIndex = frame
		dirty = True
	End Method

	Method OnScreen:Bool(camera:FlxCamera)
		If (camera = Null) camera = FlxG.Camera
		GetScreenXY(_point, camera)

		_point.x = _point.x - offset.x
		_point.y = _point.y - offset.y

		If ((angle = 0 Or _bakedRotation > 0) And scale.x = 1 And scale.y = 1) Then
			Return _point.x + frameWidth > 0 And _point.x < camera.Width And _point.y + frameHeight > 0 And _point.y < camera.Height
		End If

		Local radius:Float = Sqrt(_halfWidth * _halfWidth + _halfHeight * _halfHeight) * Max(Abs(scale.x), Abs(scale.y))
		_point.x += _halfWidth
		_point.y += _halfHeight

		Return _point.x + radius > 0 And _point.x - radius < camera.Width And _point.y + radius > 0 And _point.y - radius < camera.Height
	End Method

	Method _DrawSurface:Void(x:Float, y:Float)
		DrawRect(x, y, frameWidth, frameHeight)
	End Method

	Method _ResetHelpers:Void()
		_halfWidth = frameWidth * .5
		_halfHeight = frameHeight * .5
		origin.Make(_halfWidth, _halfHeight)

		If (_pixels <> Null) Then
			frames = _pixels.Frames()
		Else
			frames = 1
		End If

		_curIndex = 0
		_mixedColor.SetARGB(FlxG.WHITE)
	End Method

	Method ToString:String()
		Return "FlxSprite"
	End Method

Private
	Method _UpdateAnimation:Void()
		If (_bakedRotation > 0) Then
			Local oldIndex:Int = _curIndex
			Local angleHelper:Int = angle Mod 360

			If (angleHelper < 0) angleHelper += 360
			_curIndex = angleHelper / _bakedRotation + 0.5

			If (oldIndex <> _curIndex) dirty = True

		ElseIf (_curAnim <> Null And _curAnim.delay > 0 And (_curAnim.looped Or Not finished))
			_frameTimer += FlxG.Elapsed
			While (_frameTimer > _curAnim.delay)
				_frameTimer -= _curAnim.delay

				If (_curFrame = _curAnim.frames.Length() - 1) Then
					If (_curAnim.looped) _curFrame = 0
					finished = True
				Else
					_curFrame += 1
				End If

				_curIndex = _curAnim.frames[_curFrame]
				dirty = True
			Wend
		End If

		If (dirty) _CalcFrame()
	End Method

	Method _CalcFrame:Void()
		If (_callback <> Null) Then
			If (_curAnim <> Null) Then
				_callback.OnAnimationFrame(_curAnim.name, _curFrame, _curIndex)
			Else
				_callback.OnAnimationFrame("", _curFrame, _curIndex)
			End If
		End If
		dirty = False
	End Method

End Class

Private
Class FlxSpriteClass Implements FlxClass

	Method CreateInstance:Object()
		Return New FlxSprite()
	End Method

	Method InstanceOf:Bool(object:Object)
		Return (FlxSprite(object) <> Null)
	End Method

End Class

Class FlxGraphicLoader Extends FlxResourceLoader<Image>

	Field name:String
	Field animated:Bool
	Field width:Float
	Field height:Float

	Method Load:Image(name:String)
		Local image:Image = LoadImage(FlxAssetsManager.GetImagePath(Self.name))

		If (Not animated) Then
			Return image
		Else
			Local frames:Int

			If (width = 0 And height = 0) Then
				width = image.Height()
			ElseIf (width = 0) Then
				width = height
			ElseIf (height = 0) Then
				height = width
			End If

			If (height = 0) Then
				frames = Ceil(image.Width() / width)
				height = width
			Else
				frames = Ceil((image.Width() * image.Height()) / (width * height))
			End If

			Return image.GrabImage(0, 0, width, height, frames)
		End If
	End Method

End Class


Some fixes

Some fixes

Strict

Import flxg
Import flxpoint
Import flxrect
Import flxbasic
Import flxobject

Import plugin.monkey.flxcolor

Class FlxCamera Extends FlxBasic

	Const STYLE_LOCKON:Int = 0

	Const STYLE_PLATFORMER:Int = 1

	Const STYLE_TOPDOWN:Int = 2

	Const STYLE_TOPDOWN_TIGHT:Int = 3

	Const SHAKE_BOTH_AXES:Int = 0

	Const SHAKE_HORIZONTAL_ONLY:Int = 1

	Const SHAKE_VERTICAL_ONLY:Int  = 2

	Global defaultZoom:Float

	Field target:FlxObject

	Field deadzone:FlxRect

	Field bounds:FlxRect

	Field scroll:FlxPoint

	Field point:FlxPoint

Private
	Const _MIN_FLOAT_VALUE:Float = .0000001

	Field _x:Float

	Field _y:Float

	Field _realX:Float

	Field _realY:Float

	Field _width:Float

	Field _height:Float

	Field _realWidth:Float

	Field _realHeight:Float

	Field _zoom:Float

	Field _point:FlxPoint

	Field _color:FlxColor

	Field _scaleX:Float

	Field _scaleY:Float

	Field _clipped:Bool

	Field _bgColor:FlxColor

	Field _fxFlashColor:Int

	Field _fxFlashDuration:Float

	Field _fxFlashComplete:FlxFunction

	Field _fxFlashAlpha:Float

	Field _fxFadeColor:Int

	Field _fxFadeDuration:Float

	Field _fxFadeComplete:FlxFunction

	Field _fxFadeAlpha:Float

	Field _fxShakeIntensity:Float

	Field _fxShakeDuration:Float

	Field _fxShakeComplete:FlxFunction

	Field _fxShakeOffset:FlxPoint

	Field _fxShakeDirection:Int

	Field _fill:FlxColor

Public
	Method New(x:Int, y:Int, width:Int, height:Int, zoom:Float = 0)
		Zoom = zoom 'must be first for init
		X = x
		Y = y
		Width = width
		Height = height
		target = Null
		deadzone = Null
		scroll = New FlxPoint()
		_point = New FlxPoint()
		bounds = Null
		_bgColor = New FlxColor(FlxG.BgColor())
		_color = New FlxColor()

		_fxFlashColor = 0
		_fxFlashDuration = 0
		_fxFlashComplete = null
		_fxFlashAlpha = 0

		_fxFadeColor = 0
		_fxFadeDuration = 0
		_fxFadeComplete = Null
		_fxFadeAlpha = 0

		_fxShakeIntensity = 0
		_fxShakeDuration = 0
		_fxShakeComplete = Null
		_fxShakeOffset = New FlxPoint()
		_fxShakeDirection = 0

		_fill = New FlxColor(0)
	End Method

	Method Destroy:Void()
		target = Null
		scroll = Null
		deadzone = Null
		bounds = Null
		_color = Null
		_bgColor = Null
		_fxFlashComplete = Null
		_fxFadeComplete = Null
		_fxShakeComplete = Null
		_fxShakeOffset = Null
		_fill = Null
	End Method

	Method Update:Void()
		If (_fxFlashAlpha > 0) Then
			_fxFlashAlpha -= FlxG.elapsed / _fxFlashDuration

			If (_fxFlashAlpha <= 0 And _fxFlashComplete <> Null) Then
				_fxFlashComplete.Call()
			End If
		End If

		If (_fxFadeAlpha > 0 And _fxFadeAlpha < 1) Then
			_fxFadeAlpha += FlxG.elapsed / _fxFadeDuration

			If (_fxFadeAlpha >= 1) Then
				_fxFadeAlpha = 1
				If (_fxFadeComplete <> Null) _fxFadeComplete.Call()
			End If
		End If

		If (_fxShakeDuration > 0) Then
			_fxShakeDuration -= FlxG.elapsed
			If (_fxShakeDuration <= 0) Then
				_fxShakeOffset.x = 0
				_fxShakeOffset.y = 0

				If (_fxShakeComplete <> Null) _fxShakeComplete.Call()
			Else
				If (_fxShakeDirection = SHAKE_BOTH_AXES Or _fxShakeDirection = SHAKE_HORIZONTAL_ONLY) Then
					_fxShakeOffset.x = (Rnd() * _fxShakeIntensity * _width * 2 - _fxShakeIntensity * _width) * _zoom
				End If

				If (_fxShakeDirection = SHAKE_BOTH_AXES Or _fxShakeDirection = SHAKE_VERTICAL_ONLY) Then
					_fxShakeOffset.y = Rnd() * _fxShakeIntensity * _height * 2 - _fxShakeIntensity * _height * _zoom
				End If
			End If
		End If
	End Method

	Method Lock:Void()
		If (_clipped) Then
			SetScissor(_realX + _fxShakeOffset.x * FlxG._deviceScaleFactorX, _realY + _fxShakeOffset.y * FlxG._deviceScaleFactorY, _realWidth, _realHeight)
		End If

		PushMatrix()

		Translate(_x + _fxShakeOffset.x, _y + _fxShakeOffset.y)
		Scale(_scaleX, _scaleY)

		SetAlpha(_color.a)

		If (_clipped Or _bgColor.argb <> FlxG._bgColor.argb)
			SetColor(_bgColor.r, _bgColor.g, _bgColor.b)
			DrawRect(0, 0, _width, _height)
			FlxG._lastDrawingColor = _bgColor.argb
		End If

		If (_color.argb <> FlxG._lastDrawingColor) Then
			SetColor(_color.r, _color.g, _color.b)
			FlxG._lastDrawingColor = _color.argb
		End if
	End Method

	Method Unlock:Void()
		If (_fill.argb <> 0) Then
			If (_fill.argb <> FlxG._lastDrawingColor) Then
				SetAlpha(_fill.a)
				SetColor(_fill.r, _fill.g, _fill.b)
			End if

			DrawRect(0, 0, _width, _height)
			FlxG._lastDrawingColor = _fill.argb
		End If

		PopMatrix()
		_fill.SetARGB(0)
	End Method

	Method Flash:Void(color:Int = FlxG.WHITE, duration:Float = 1, onComplete:FlxFunction = Null, force:Bool = False)
		If (Not force And _fxFlashAlpha > 0) Return

		_fxFlashColor = color
		If (duration <= 0) duration = _MIN_FLOAT_VALUE
		_fxFlashDuration = duration
		_fxFlashComplete = onComplete
		_fxFlashAlpha = 1
	End Method

	Method Fade:Void(color:Int = FlxG.BLACK, duration:Float = 1, onComplete:FlxFunction = Null, force:Bool = False)
		If (Not force And _fxFadeAlpha > 0)	Return

		_fxFadeColor = color
		If (duration <= 0) duration = _MIN_FLOAT_VALUE
		_fxFadeDuration = duration
		_fxFadeComplete = onComplete
		_fxFadeAlpha = _MIN_FLOAT_VALUE
	End Method

	Method Shake:Void(intensity:Float = 0.05, duration:Float = 0.5, onComplete:FlxFunction = Null, force:Bool = True, direction:Int = SHAKE_BOTH_AXES)
		If (Not force And (_fxShakeOffset.x <> 0 Or _fxShakeOffset.y <> 0)) Return

		_fxShakeIntensity = intensity
		_fxShakeDuration = duration
		_fxShakeComplete = onComplete
		_fxShakeDirection = direction
		_fxShakeOffset.x = 0
		_fxShakeOffset.y = 0
	End Method

	Method StopFX:Void()
		_fxFlashAlpha = 0
		_fxFadeAlpha = 0
		_fxShakeDuration = 0
		_fxShakeOffset.x = 0
		_fxShakeOffset.y = 0
	End Method

	Method X:Float() Property
		Return _x
	End Method

	Method X:Void(x:Float) Property
		_x = x
		_realX = _x * FlxG._deviceScaleFactorX

		If (x <> 0) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Y:Float() Property
		Return _y
	End Method

	Method Y:Void(y:Float) Property
		_y = y
		_realY = _y * FlxG._deviceScaleFactorY

		If (y <> 0) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Width:Float() Property
		Return _width
	End Method

	Method Width:Void(width:Float) Property
		_width = width
		_realWidth = Min(Float(FlxG.DEVICE_WIDTH), Floor(_width * _scaleX * FlxG._deviceScaleFactorX))

		If (_realWidth <> FlxG.DEVICE_WIDTH) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Height:Float() Property
		Return _height
	End Method

	Method Height:Void(height:Float) Property
		_height = height
		_realHeight = Min(Float(FlxG.DEVICE_HEIGHT), Floor(_height * _scaleY * FlxG._deviceScaleFactorY))

		If (_realHeight <> FlxG.DEVICE_HEIGHT) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Zoom:Float() Property
		Return _zoom
	End Method

	Method Zoom:Void(zoom:Float) Property
		If (zoom = 0) Then
			_zoom = FlxCamera.defaultZoom
		Else
			_zoom = zoom
		End If

		SetScale(_zoom, _zoom)
	End Method

	Method GetScale:FlxPoint()
		Return FlxPoint.Make(_scaleX, _scaleY)
	End Method

	Method SetScale:Void(x:Float, y:Float)
		_scaleX = x
		_scaleY = y
		_realWidth = Min(Float(FlxG.DEVICE_WIDTH), Floor(_width * _scaleX * FlxG._deviceScaleFactorX))
		_realHeight = Min(Float(FlxG.DEVICE_HEIGHT), Floor(_height * _scaleY * FlxG._deviceScaleFactorY))
	End Method

	Method Fill:Void(color:Int, blendAlpha:Bool = True)
		If (blendAlpha) Then
			_fill.SetARGB(color)
		Else
			_fill.SetRGB(color)
		End If
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetARGB(color)
	End Method

	Method BgColor:Int() Property
		Return _bgColor.argb
	End Method

	Method BgColor:Void(color:Int) Property
		_bgColor.SetARGB(color)
	End Method

	Method DrawFX:Void()
		If (_fxFlashAlpha > 0) Then
			Local alphaComponent:Float

			alphaComponent = _fxFlashColor Shr 24
			If (alphaComponent <= 0) alphaComponent = $FF
			Fill(((alphaComponent * _fxFlashAlpha) Shl 24) + (_fxFlashColor & $00FFFFFF))
		End If

		If (_fxFadeAlpha > 0) Then
			Local alphaComponent:Float

			alphaComponent = _fxFadeColor Shr 24
			If (alphaComponent <= 0) alphaComponent = $FF
			Fill(((alphaComponent * _fxFadeAlpha) Shl 24) + (_fxFadeColor & $00FFFFFF))
		End If
	End Method

	Method ToString:String()
		Return "FlxCamera"
	End Method

End Class

Strict

Import flxg
Import flxpoint
Import flxrect
Import flxbasic
Import flxobject

Import plugin.monkey.flxcolor

Class FlxCamera Extends FlxBasic

	Const STYLE_LOCKON:Int = 0

	Const STYLE_PLATFORMER:Int = 1

	Const STYLE_TOPDOWN:Int = 2

	Const STYLE_TOPDOWN_TIGHT:Int = 3

	Const SHAKE_BOTH_AXES:Int = 0

	Const SHAKE_HORIZONTAL_ONLY:Int = 1

	Const SHAKE_VERTICAL_ONLY:Int  = 2

	Global defaultZoom:Float

	Field target:FlxObject

	Field deadzone:FlxRect

	Field bounds:FlxRect

	Field scroll:FlxPoint

	Field point:FlxPoint

Private
	Const _MIN_FLOAT_VALUE:Float = .0000001

	Field _x:Float

	Field _y:Float

	Field _realX:Float

	Field _realY:Float

	Field _width:Float

	Field _height:Float

	Field _realWidth:Float

	Field _realHeight:Float

	Field _zoom:Float

	Field _point:FlxPoint

	Field _color:FlxColor

	Field _scaleX:Float

	Field _scaleY:Float

	Field _clipped:Bool

	Field _bgColor:FlxColor

	Field _fxFlashColor:Int

	Field _fxFlashDuration:Float

	Field _fxFlashComplete:FlxFunction

	Field _fxFlashAlpha:Float

	Field _fxFadeColor:Int

	Field _fxFadeDuration:Float

	Field _fxFadeComplete:FlxFunction

	Field _fxFadeAlpha:Float

	Field _fxShakeIntensity:Float

	Field _fxShakeDuration:Float

	Field _fxShakeComplete:FlxFunction

	Field _fxShakeOffset:FlxPoint

	Field _fxShakeDirection:Int

	Field _fill:FlxColor

Public
	Method New(x:Int, y:Int, width:Int, height:Int, zoom:Float = 0)
		Zoom = zoom 'must be first for init
		X = x
		Y = y
		Width = width
		Height = height
		target = Null
		deadzone = Null
		scroll = New FlxPoint()
		_point = New FlxPoint()
		bounds = Null
		_bgColor = New FlxColor(FlxG.BgColor())
		_color = New FlxColor()

		_fxFlashColor = 0
		_fxFlashDuration = 0
		_fxFlashComplete = null
		_fxFlashAlpha = 0

		_fxFadeColor = 0
		_fxFadeDuration = 0
		_fxFadeComplete = Null
		_fxFadeAlpha = 0

		_fxShakeIntensity = 0
		_fxShakeDuration = 0
		_fxShakeComplete = Null
		_fxShakeOffset = New FlxPoint()
		_fxShakeDirection = 0

		_fill = New FlxColor(0)
	End Method

	Method Destroy:Void()
		target = Null
		scroll = Null
		deadzone = Null
		bounds = Null
		_color = Null
		_bgColor = Null
		_fxFlashComplete = Null
		_fxFadeComplete = Null
		_fxShakeComplete = Null
		_fxShakeOffset = Null
		_fill = Null
	End Method

	Method Update:Void()
		If (_fxFlashAlpha > 0) Then
			_fxFlashAlpha -= FlxG.elapsed / _fxFlashDuration

			If (_fxFlashAlpha <= 0 And _fxFlashComplete <> Null) Then
				_fxFlashComplete.Call()
			End If
		End If

		If (_fxFadeAlpha > 0 And _fxFadeAlpha < 1) Then
			_fxFadeAlpha += FlxG.elapsed / _fxFadeDuration

			If (_fxFadeAlpha >= 1) Then
				_fxFadeAlpha = 1
				If (_fxFadeComplete <> Null) _fxFadeComplete.Call()
			End If
		End If

		If (_fxShakeDuration > 0) Then
			_fxShakeDuration -= FlxG.elapsed
			If (_fxShakeDuration <= 0) Then
				_fxShakeOffset.x = 0
				_fxShakeOffset.y = 0

				If (_fxShakeComplete <> Null) _fxShakeComplete.Call()
			Else
				If (_fxShakeDirection = SHAKE_BOTH_AXES Or _fxShakeDirection = SHAKE_HORIZONTAL_ONLY) Then
					_fxShakeOffset.x = (Rnd() * _fxShakeIntensity * _width * 2 - _fxShakeIntensity * _width) * _zoom
				End If

				If (_fxShakeDirection = SHAKE_BOTH_AXES Or _fxShakeDirection = SHAKE_VERTICAL_ONLY) Then
					_fxShakeOffset.y = Rnd() * _fxShakeIntensity * _height * 2 - _fxShakeIntensity * _height * _zoom
				End If
			End If
		End If
	End Method

	Method Lock:Void()
		If (_clipped) Then
			If (_fxShakeOffset.x <> 0 Or _fxShakeOffset.y <> 0) Then
				SetScissor(_realX + _fxShakeOffset.x * FlxG._deviceScaleFactorX, _realY + _fxShakeOffset.y * FlxG._deviceScaleFactorY, _realWidth, _realHeight)
			Else
				SetScissor(_realX, _realY, _realWidth, _realHeight)
			End If
		End If

		PushMatrix()

		Translate(_x + _fxShakeOffset.x, _y + _fxShakeOffset.y)
		Scale(_scaleX, _scaleY)

		SetAlpha(_color.a)

		If (_clipped Or _bgColor.argb <> FlxG._bgColor.argb)
			SetColor(_bgColor.r, _bgColor.g, _bgColor.b)
			DrawRect(0, 0, _width, _height)
			FlxG._lastDrawingColor = _bgColor.argb
		End If

		If (_color.argb <> FlxG._lastDrawingColor) Then
			SetColor(_color.r, _color.g, _color.b)
			FlxG._lastDrawingColor = _color.argb
		End if
	End Method

	Method Unlock:Void()
		If (_fill.argb <> 0) Then
			If (_fill.argb <> FlxG._lastDrawingColor) Then
				SetAlpha(_fill.a)
				SetColor(_fill.r, _fill.g, _fill.b)
			End if

			DrawRect(0, 0, _width, _height)
			FlxG._lastDrawingColor = _fill.argb
		End If

		PopMatrix()
		_fill.SetARGB(0)
	End Method

	Method Flash:Void(color:Int = FlxG.WHITE, duration:Float = 1, onComplete:FlxFunction = Null, force:Bool = False)
		If (Not force And _fxFlashAlpha > 0) Return

		_fxFlashColor = color
		If (duration <= 0) duration = _MIN_FLOAT_VALUE
		_fxFlashDuration = duration
		_fxFlashComplete = onComplete
		_fxFlashAlpha = 1
	End Method

	Method Fade:Void(color:Int = FlxG.BLACK, duration:Float = 1, onComplete:FlxFunction = Null, force:Bool = False)
		If (Not force And _fxFadeAlpha > 0)	Return

		_fxFadeColor = color
		If (duration <= 0) duration = _MIN_FLOAT_VALUE
		_fxFadeDuration = duration
		_fxFadeComplete = onComplete
		_fxFadeAlpha = _MIN_FLOAT_VALUE
	End Method

	Method Shake:Void(intensity:Float = 0.05, duration:Float = 0.5, onComplete:FlxFunction = Null, force:Bool = True, direction:Int = SHAKE_BOTH_AXES)
		If (Not force And (_fxShakeOffset.x <> 0 Or _fxShakeOffset.y <> 0)) Return

		_fxShakeIntensity = intensity
		_fxShakeDuration = duration
		_fxShakeComplete = onComplete
		_fxShakeDirection = direction
		_fxShakeOffset.x = 0
		_fxShakeOffset.y = 0
	End Method

	Method StopFX:Void()
		_fxFlashAlpha = 0
		_fxFadeAlpha = 0
		_fxShakeDuration = 0
		_fxShakeOffset.x = 0
		_fxShakeOffset.y = 0
	End Method

	Method X:Float() Property
		Return _x
	End Method

	Method X:Void(x:Float) Property
		_x = x
		_realX = _x * FlxG._deviceScaleFactorX

		If (x <> 0) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Y:Float() Property
		Return _y
	End Method

	Method Y:Void(y:Float) Property
		_y = y
		_realY = _y * FlxG._deviceScaleFactorY

		If (y <> 0) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Width:Float() Property
		Return _width
	End Method

	Method Width:Void(width:Float) Property
		_width = width
		_realWidth = Min(Float(FlxG.DEVICE_WIDTH), Floor(_width * _scaleX * FlxG._deviceScaleFactorX))

		If (_realWidth <> FlxG.DEVICE_WIDTH) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Height:Float() Property
		Return _height
	End Method

	Method Height:Void(height:Float) Property
		_height = height
		_realHeight = Min(Float(FlxG.DEVICE_HEIGHT), Floor(_height * _scaleY * FlxG._deviceScaleFactorY))

		If (_realHeight <> FlxG.DEVICE_HEIGHT) Then
			_clipped = True
		Else
			_clipped = False
		End If
	End Method

	Method Zoom:Float() Property
		Return _zoom
	End Method

	Method Zoom:Void(zoom:Float) Property
		If (zoom = 0) Then
			_zoom = FlxCamera.defaultZoom
		Else
			_zoom = zoom
		End If

		SetScale(_zoom, _zoom)
	End Method

	Method GetScale:FlxPoint()
		Return FlxPoint.Make(_scaleX, _scaleY)
	End Method

	Method SetScale:Void(x:Float, y:Float)
		_scaleX = x
		_scaleY = y
		_realWidth = Min(Float(FlxG.DEVICE_WIDTH), Floor(_width * _scaleX * FlxG._deviceScaleFactorX))
		_realHeight = Min(Float(FlxG.DEVICE_HEIGHT), Floor(_height * _scaleY * FlxG._deviceScaleFactorY))
	End Method

	Method Fill:Void(color:Int, blendAlpha:Bool = True)
		If (blendAlpha) Then
			_fill.SetARGB(color)
		Else
			_fill.SetRGB(color)
		End If
	End Method

	Method Color:Int() Property
		Return _color.argb
	End Method

	Method Color:Void(color:Int) Property
		_color.SetARGB(color)
	End Method

	Method BgColor:Int() Property
		Return _bgColor.argb
	End Method

	Method BgColor:Void(color:Int) Property
		_bgColor.SetARGB(color)
	End Method

	Method DrawFX:Void()
		If (_fxFlashAlpha > 0) Then
			Local alphaComponent:Float

			alphaComponent = _fxFlashColor Shr 24
			If (alphaComponent <= 0) alphaComponent = $FF
			Fill(((alphaComponent * _fxFlashAlpha) Shl 24) + (_fxFlashColor & $00FFFFFF))
		End If

		If (_fxFadeAlpha > 0) Then
			Local alphaComponent:Float

			alphaComponent = _fxFadeColor Shr 24
			If (alphaComponent <= 0) alphaComponent = $FF
			Fill(((alphaComponent * _fxFadeAlpha) Shl 24) + (_fxFadeColor & $00FFFFFF))
		End If
	End Method

	Method ToString:String()
		Return "FlxCamera"
	End Method

End Class


Added file checker for XMLParser in ParseFile

--HG--
extra : convert_revision : svn%3Ae934d3cd-0d57-f34a-94d0-9e8d31ae1c29/trunk%40469

' Diddy XML parser
' This is a very simple XML parser, and expects a document to be as close to well-formed as possible.
' The only real leniency it has is that attributes may be defined without surrounding quotes/double quotes.
' All XML characters should be escaped in strings. > < etc.
' Currently it supports regular tag and attribute definitions, processing instructions, and skipping comments.
' For now, exported indentation is hardcoded to 2 spaces.
' Written from scratch, using as little string manipulation as possible.

Import assert
Import functions
Import collections
Import stringbuilder

Class XMLParser
	Const TAG_DEFAULT:Int = 0
	Const TAG_COMMENT:Int = 1
	Const TAG_CDATA:Int = 2
	Const TAG_DOCTYPE:Int = 3

	Field str:String
	Field tags:Int[] ' tag character indexes, alternating < and >
	Field tagType:Int[]
	Field tagCount:Int = 0
	Field tagsLength:Int
	Field quotes:Int[] ' quote character indexes, alternating opening and closing
	Field quoteCount:Int = 0
	Field quotesLength:Int
	Field pis:Int[] ' pi character indexes, alternating <? And ?> (index is on <>)
	Field piCount:Int = 0
	Field pisLength:Int

	Method CacheControlCharacters:Void()
		tagsLength = 128
		quotesLength = 128
		pisLength = 128
		tags = New Int[tagsLength]
		tagType = New Int[tagsLength]
		quotes = New Int[quotesLength]
		pis = New Int[quotesLength]
		tagCount = 0
		quoteCount = 0
		piCount = 0
		Local inTag:Bool = False
		Local inQuote:Bool = False
		Local inComment:Bool = False
		Local inCdata:Bool = False
		Local inDoctype:Bool = False
		Local inPi:Bool = False
		Local strlen:Int = str.Length
		For Local i:Int = 0 Until strlen
			' if we're in a comment, we're only looking for -->
			If inComment Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_HYPHEN And str[i-2] = ASC_HYPHEN Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_COMMENT
					tagCount += 1
					inComment = False
				End
			' if we're in a cdata, we're only looking for ]]>
			ElseIf inCdata Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_CLOSE_BRACKET And str[i-2] = ASC_CLOSE_BRACKET Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_CDATA
					tagCount += 1
					inCdata = False
				End
			' if we're in a quoted string, we're only looking for "
			ElseIf inQuote Then
				If str[i] = ASC_DOUBLE_QUOTE Then
					If quoteCount = quotesLength Then
						quotesLength *= 2
						quotes = quotes.Resize(quotesLength)
					End
					quotes[quoteCount] = i
					quoteCount += 1
					inQuote = False
				End
			' check if we should start a new quoted string
			ElseIf str[i] = ASC_DOUBLE_QUOTE Then
				If quoteCount = quotesLength Then
					quotesLength *= 2
					quotes = quotes.Resize(quotesLength)
				End
				quotes[quoteCount] = i
				quoteCount += 1
				inQuote = True
			' if we're in a processing instruction, we're only looking for ?>
			ElseIf inPi Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_QUESTION Then
					If piCount = pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = False
				End
			' if we're in a doctype, we're only looking for >
			ElseIf inDoctype Then
				If str[i] = ASC_GREATER_THAN Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DOCTYPE
					tagCount += 1
					inDoctype = False
				End
			' less than
			ElseIf str[i] = ASC_LESS_THAN Then
				' if we're in a tag, die
				If inTag Then AssertError "Invalid less than!"
				' check for prolog
				If str[i+1] = ASC_EXCLAMATION Then
					' comment?
					If str[i+2] = ASC_HYPHEN And str[i+3] = ASC_HYPHEN Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_COMMENT
						tagCount += 1
						inComment = True
					' cdata?
					ElseIf str[i+2] = ASC_OPEN_BRACKET And
							(str[i+3] = ASC_UPPER_C Or str[i+3] = ASC_LOWER_C) And
							(str[i+4] = ASC_UPPER_D Or str[i+4] = ASC_LOWER_D) And
							(str[i+5] = ASC_UPPER_A Or str[i+5] = ASC_LOWER_A) And
							(str[i+6] = ASC_UPPER_T Or str[i+6] = ASC_LOWER_T) And
							(str[i+7] = ASC_UPPER_A Or str[i+7] = ASC_LOWER_A) And
							str[i+8] = ASC_OPEN_BRACKET Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_CDATA
						tagCount += 1
						inCdata = True
					' doctype?
					ElseIf (str[i+2] = ASC_UPPER_D Or str[i+2] = ASC_LOWER_D) And
							(str[i+3] = ASC_UPPER_O Or str[i+3] = ASC_LOWER_O) And
							(str[i+4] = ASC_UPPER_C Or str[i+4] = ASC_LOWER_C) And
							(str[i+5] = ASC_UPPER_T Or str[i+5] = ASC_LOWER_T) And
							(str[i+6] = ASC_UPPER_Y Or str[i+6] = ASC_LOWER_Y) And
							(str[i+7] = ASC_UPPER_P Or str[i+7] = ASC_LOWER_P) And
							(str[i+8] = ASC_UPPER_E Or str[i+8] = ASC_LOWER_E) Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_DOCTYPE
						tagCount += 1
						inDoctype = True
					Else
						AssertError "Invalid prolog."
					End
				' check for processing instruction
				ElseIf str[i+1] = ASC_QUESTION Then
					If piCount = pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = True
				' finally, it must just be opening a tag
				Else
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DEFAULT
					tagCount += 1
					inTag = True
				End
			' greater than
			ElseIf str[i] = ASC_GREATER_THAN Then
				If Not inTag Then AssertError "Invalid greater than!"
				If tagCount = tagsLength Then
					tagsLength *= 2
					tags = tags.Resize(tagsLength)
					tagType = tagType.Resize(tagsLength)
				End
				tags[tagCount] = i
				tagType[tagCount] = TAG_DEFAULT
				tagCount += 1
				inTag = False
			End
		Next
		If inQuote Then AssertError "Unclosed quote!"
		If inTag Then AssertError "Unclosed tag!"
		If inComment Then AssertError "Unclosed comment!"
		If inCdata Then AssertError "Unclosed cdata!"
		If inPi Then AssertError "Unclosed processing instruction!"
	End

	' get the contents of a tag (given start and end)
	Method GetTagContents:XMLElement(startIndex:Int, endIndex:Int)
		' die if empty tag
		If startIndex = endIndex Then AssertError("Empty tag detected.")
		' our element
		Local e:XMLElement = New XMLElement
		Local a:Int, singleQuoted:Bool, doubleQuoted:Bool, key:String, value:String

		' get the name
		a = startIndex
		While a < endIndex
			If str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
				If a = endIndex-1 Then
					e.name = str[startIndex..endIndex]
				Else
					e.name = str[startIndex..a]
				End
				a += 1
				Exit
			End
			a += 1
		End
		startIndex = a

		' TODO: validate tag name is alphanumeric
		' if no name, die
		If e.name = "" Then AssertError("Error reading tag name.")

		' loop on all tokens
		While startIndex < endIndex
			' trim leading whitespace
			While startIndex < endIndex And (str[startIndex]=ASC_SPACE Or str[startIndex]=ASC_TAB Or str[startIndex]=ASC_LF Or str[startIndex]=ASC_CR)
				startIndex += 1
			End

			' clear check variables
			singleQuoted = False
			doubleQuoted = False
			key = ""
			value = ""

			' find the key
			a = startIndex
			While a < endIndex
				If str[a] = ASC_EQUALS Or str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
					If a=endIndex-1 Then
						key = str[startIndex..endIndex]
					Else
						key = str[startIndex..a]
					End
					a += 1
					Exit
				End
				a += 1
			End
			startIndex = a

			' if the key is empty, there was an error (unless we've hit the end of the string)
			If key = "" Then
				If a < endIndex Then
					AssertError("Error reading attribute key.")
				Else
					Exit
				End
			End

			' if it stopped on an equals, get the value
			If str[a-1] = ASC_EQUALS Then
				singleQuoted = False
				doubleQuoted = False
				While a < endIndex
					' check if it's a single quote
					If str[a] = ASC_SINGLE_QUOTE And Not doubleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							singleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected single quote detected in attribute value.")
						Else
							' we must be ending the quote here, so grab it and break out
							singleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' check if it's a double quote
					ElseIf str[a] = ASC_DOUBLE_QUOTE And Not singleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							doubleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected double quote detected in attribute value.")
						Else
							' we must be ending the quote here, so break out
							doubleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' should we be ending the attribute?
					ElseIf a = endIndex-1 Or (Not singleQuoted And Not doubleQuoted And (str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR))
						If a=endIndex-1 Then
							value = str[startIndex..endIndex]
						Else
							value = str[startIndex..a]
						End
						a += 1
						Exit
					End
					a += 1
				End
				startIndex = a
				value = UnescapeXMLString(value)

				If singleQuoted Or doubleQuoted Then AssertError("Unclosed quote detected.")
			End

			' set the attribute
			e.SetAttribute(key, value)

			If a >= endIndex Then Exit
		End
		Return e
	End

	Method ParseFile:XMLDocument(filename:String)
		Local xmlString:String = LoadString(filename)
		If Not xmlString Then
			AssertError("Error: Cannot load " + filename)
		End
		Return ParseString(xmlString)
	End

	Method TrimString:Void(startIdx:Int, endIdx:Int, trimmed:Int[])
		Local trimStart:Int = startIdx, trimEnd:Int = endIdx
		While trimEnd > trimStart
			Local ch:Int = str[trimEnd-1]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimEnd -= 1
			Else
				Exit
			End
		End
		While trimStart < trimEnd
			Local ch:Int = str[trimStart]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimStart += 1
			Else
				Exit
			End
		End
		trimmed[0] = trimStart
		trimmed[1] = trimEnd
	End

	' parses an xml doc, currently doesn't support nested PI or prolog
	Method ParseString:XMLDocument(str:String)
		Self.str = str

		Local doc:XMLDocument = New XMLDocument
		Local elements:ArrayList<XMLElement> = New ArrayList<XMLElement>
		Local thisE:XMLElement = Null, newE:XMLElement = Null
		Local index:Int = 0, a:Int, b:Int, c:Int, nextIndex:Int
		Local trimmed:Int[] = New Int[2]

		' cache all the control characters
		CacheControlCharacters()

		' find first opening tag
		If tagCount = 0 Then AssertError "Something seriously wrong... no tags!"

		' parse processing instructions
		index = 0
		a = pis[index]+2
		b = pis[index+1]-1
		While index < piCount
			TrimString(a, b, trimmed)
			If trimmed[0] <> trimmed[1] Then
				newE = GetTagContents(trimmed[0], trimmed[1])
				newE.pi = True
				doc.pi.Add(newE)
				newE = Null
			Else
				AssertError "Empty processing instruction."
			End
			index += 2
		End

		' loop on tags
		index = 0
		While index+1 < tagCount
			' we skip comments
			If tagType[index] = TAG_COMMENT Then
				' skip comments

			' if it's cdata
			ElseIf tagType[index] = TAG_CDATA Then
				' get the text between < and >
				a = tags[index]+9 ' "![CDATA[".Length
				b = tags[index+1]-2 ' "]]".Length

				' add a cdata element
				newE = New XMLElement
				newE.cdata = True
				newE.value = str[a..b]
				newE.parent = thisE
				thisE.AddChild(newE)
				newE = Null

			' otherwise we do normal tag stuff
			Else
				' get the text between < and >
				a = tags[index]+1
				b = tags[index+1]

				' trim the string
				TrimString(a, b, trimmed)

				' if it's a completely empty tag name, die
				If trimmed[0] = trimmed[1] Then AssertError "Empty tag."

				' check if the first character is a slash (end tag)
				If str[trimmed[0]] = ASC_SLASH Then
					' if no current element, die
					If thisE = Null Then AssertError("Closing tag found outside main document tag.")

					' strip the slash
					trimmed[0] += 1

					' check that the tag name length matches
					If trimmed[1] - trimmed[0] <> thisE.name.Length Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")

					' check that the tag name matches (manually so that we don't create an entire string slice when the first character could be wrong!)
					For Local nameIdx:Int = 0 Until thisE.name.Length
						If str[trimmed[0]+nameIdx] <> thisE.name[nameIdx] Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")
					Next

					' pop the element from the stack, or set the document root
					If Not elements.IsEmpty() Then
						thisE = elements.RemoveLast()
					Else
						'doc.root = thisE
						Exit
					End

				' check if the last character is a slash (self closing tag)
				ElseIf str[trimmed[1]-1] = ASC_SLASH Then
					' strip the slash
					trimmed[1] -= 1

					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					' add as a child or set as the root
					If doc.root = Null Then doc.root = newE
					If thisE <> Null Then
						thisE.AddChild(newE)
					Else
						'doc.root = newE
						Exit
					End
					newE = Null

				' otherwise it's an opening tag
				Else
					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					If doc.root = Null Then doc.root = newE

					' add as a child if we already have an element
					If thisE <> Null Then
						thisE.AddChild(newE)
					End

					' push this element
					elements.AddLast(thisE)

					' and set as the current
					thisE = newE
					newE = Null
				End
			End

			' get any text between tags
			index += 1
			If index < tagCount Then
				a = tags[index]+1
				b = tags[index+1]
				TrimString(a, b, trimmed)
				If trimmed[0] <> trimmed[1] Then
					If thisE <> Null Then
						thisE.value += UnescapeXMLString(str[trimmed[0]..trimmed[1]])
					Else
						'AssertError("Loose text outside of any tag!") - Getting this for some reason, I'll fix it later :/
					End
				End
			End

			' next tag
			index += 1
		End
		If doc.root = Null Then AssertError("Error parsing XML: no document tag found.")
		Return doc
	End
End

Class XMLDocument
Private
	Field xmlVersion:String = "1.0"
	Field xmlEncoding:String = "UTF-8"
	Field root:XMLElement
	Field pi:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field prologs:ArrayList<XMLElement> = New ArrayList<XMLElement>

Public

' Constructors
	Method New(rootName:String="")
		If rootName <> "" Then root = New XMLElement(rootName)
	End

	Method New(root:XMLElement)
		Self.root = root
	End

' Methods
	Method ExportString:String(formatXML:Bool = True)
		' we start with the xml instruction
		Local output:String = "<?xml version=~q"+xmlVersion+"~q encoding=~q"+xmlEncoding+"~q?>"
		If formatXML Then output += "~n"
		' root node
		output += root.ToString(formatXML) + "~n"
		' done!
		Return output
	End

	Method SaveFile:Void(filename:String)
		' TODO when file IO is ready!
	End

' Properties
	Method Root:XMLElement() Property
		Return root
	End

	Method Prologs:ArrayList<XMLElement>() Property
		Return prologs
	End

	Method ProcessingInstructions:ArrayList<XMLElement>() Property
		Return pi
	End

	Method Version:String() Property
		Return xmlVersion
	End

	Method Encoding:String() Property
		Return xmlEncoding
	End
End

Class XMLAttribute
Private
	Field name:String
	Field value:String

' Constructors
	Method New(name:String, value:String)
		Self.name = name
		Self.value = value
	End
End

Class XMLElement
Private
	Field openTagStart:String = "<"
	Field openTagEnd:String = ">"
	Field selfCloseEnd:String = " />"
	Field closeTagStart:String = "</"
	Field closeTagEnd:String = ">"

	Field name:String
	Field attributes:ArrayList<XMLAttribute> = New ArrayList<XMLAttribute>
	Field children:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field value:String
	Field parent:XMLElement

	Field pi:Bool
	Field prolog:Bool
	Field cdata:Bool

Public
' Constructors
	Method New()
	End

	Method New(name:String, parent:XMLElement = Null)
		Self.parent = parent
		Self.name = name
		If parent <> Null Then parent.children.Add(Self)
	End

' Methods
	Method IsProcessingInstruction:Bool()
		Return pi
	End

	Method IsProlog:Bool()
		Return prolog
	End

	Method IsCharacterData:Bool()
		Return cdata
	End

	' avoid using this method if you can, because you should try not to have "floating" elements
	Method AddChild:Void(child:XMLElement)
		If children.Contains(child) Return
		children.Add(child)
		child.parent = Self
	End

	Method HasAttribute:Bool(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return True
		Next
		Return False
	End

	Method GetAttribute:String(name:String, defaultValue:String = "")
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return att.value
		Next
		Return defaultValue
	End

	Method SetAttribute:String(name:String, value:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				Local old:String = att.value
				att.value = value
				Return old
			End
		Next
		attributes.Add(New XMLAttribute(name, value))
		Return ""
	End

	Method ClearAttribute:String(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				attributes.Remove(att)
				Return att.value
			End
		Next
		Return ""
	End

	Method Dispose:Void(removeSelf:Bool = True)
		' dispose children
		Local en:IEnumerator<XMLElement> = children.Enumerator()
		While en.HasNext()
			Local element:XMLElement = en.NextObject()
			element.Dispose(False)
			en.Remove()
		End
		' remove self from parent if this is not recursing
		If removeSelf And parent <> Null Then parent.children.Remove(Self)
		' clear out the parent
		parent = Null
	End

	Method ToString:String(formatXML:Bool=True, indentation:Int=0)
		Local rv:String = ""
		If formatXML Then
			For Local i% = 0 Until indentation
				rv += "  "
			Next
		End
		rv += openTagStart + name
		If Not attributes.IsEmpty() Then
			For Local i% = 0 Until attributes.Size
				Local att:XMLAttribute = attributes.Get(i)
				rv += " " + att.name + "=~q" + EscapeXMLString(att.value) + "~q"
			Next
		End
		If children.IsEmpty() Then
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() = 0
				rv += selfCloseEnd
			Else
				rv += openTagEnd + value.Trim() + closeTagStart + name + closeTagEnd
			End
		Else
			rv += openTagEnd
			If formatXML Then
				rv += "~n"
			End
			For Local i% = 0 Until children.Size
				rv += children.Get(i).ToString(formatXML, indentation + 1)
			End
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() > 0 Then
				If Not formatXML Then
					rv += esc
				Else
					rv += "~n" + esc + "~n"
				End
			End
			If formatXML Then
				For Local i% = 0 Until indentation
					rv += "  "
				Next
			End
			rv += closeTagStart + name + closeTagEnd
		End
		If formatXML Then
			rv += "~n"
		End
		Return rv
	End

	Method GetChildrenByName:ArrayList<XMLElement>(findName:String)
		Local rv:ArrayList<XMLElement> = New ArrayList<XMLElement>
		For Local element:XMLElement = Eachin children
			If element.name = findName Then rv.Add(element)
		Next
		Return rv
	End

	Method GetFirstChildByName:XMLElement(findName:String)
		For Local element:XMLElement = Eachin children
			If element.name = findName Then Return element
		Next
		Return Null
	End

' Properties
	Method Children:ArrayList<XMLElement>() Property
		Return children
	End

	Method Parent:XMLElement() Property
		Return parent
	End

	Method Name:String() Property
		Return name
	End

	Method Name:Void(name:String) Property
		Self.name = name
	End

	Method Value:String() Property
		Return value
	End

	Method Value:Void(value:String) Property
		Self.value = value
	End
End

' hopefully this stringbuilder-based replace should be faster since it doesn't create intermediate string objects!
Function EscapeXMLString:String(str:String)
	xmlsb.Length = 0
	For Local i:Int = 0 Until str.Length
		Select str[i]
			Case ASC_AMPERSAND
				xmlsb.Append("&")
			Case ASC_LESS_THAN
				xmlsb.Append("<")
			Case ASC_GREATER_THAN
				xmlsb.Append(">")
			Case ASC_SINGLE_QUOTE
				xmlsb.Append("'")
			Case ASC_DOUBLE_QUOTE
				xmlsb.Append(""")
			Default
				xmlsb.AppendByte(str[i])
		End
	End
	Return xmlsb.ToString()
End

' unescaping is rare so we won't bother using a stringbuilder
Function UnescapeXMLString:String(str:String)
	str = str.Replace(""", "~q")
	str = str.Replace("'", "'")
	str = str.Replace(">", ">")
	str = str.Replace("<", "<")
	str = str.Replace("&", "&")
	Return str
End

Private
Global xmlsb:StringBuilder = New StringBuilder

' Diddy XML parser
' This is a very simple XML parser, and expects a document to be as close to well-formed as possible.
' The only real leniency it has is that attributes may be defined without surrounding quotes/double quotes.
' All XML characters should be escaped in strings. > < etc.
' Currently it supports regular tag and attribute definitions, processing instructions, and skipping comments.
' For now, exported indentation is hardcoded to 2 spaces.
' Written from scratch, using as little string manipulation as possible.

Import assert
Import functions
Import collections
Import stringbuilder

Class XMLParser
	Const TAG_DEFAULT:Int = 0
	Const TAG_COMMENT:Int = 1
	Const TAG_CDATA:Int = 2
	Const TAG_DOCTYPE:Int = 3

	Field str:String
	Field tags:Int[] ' tag character indexes, alternating < and >
	Field tagType:Int[]
	Field tagCount:Int = 0
	Field tagsLength:Int
	Field quotes:Int[] ' quote character indexes, alternating opening and closing
	Field quoteCount:Int = 0
	Field quotesLength:Int
	Field pis:Int[] ' pi character indexes, alternating <? And ?> (index is on <>)
	Field piCount:Int = 0
	Field pisLength:Int

	Method CacheControlCharacters:Void()
		tagsLength = 128
		quotesLength = 128
		pisLength = 128
		tags = New Int[tagsLength]
		tagType = New Int[tagsLength]
		quotes = New Int[quotesLength]
		pis = New Int[quotesLength]
		tagCount = 0
		quoteCount = 0
		piCount = 0
		Local inTag:Bool = False
		Local inQuote:Bool = False
		Local inComment:Bool = False
		Local inCdata:Bool = False
		Local inDoctype:Bool = False
		Local inPi:Bool = False
		Local strlen:Int = str.Length
		For Local i:Int = 0 Until strlen
			' if we're in a comment, we're only looking for -->
			If inComment Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_HYPHEN And str[i-2] = ASC_HYPHEN Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_COMMENT
					tagCount += 1
					inComment = False
				End
			' if we're in a cdata, we're only looking for ]]>
			ElseIf inCdata Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_CLOSE_BRACKET And str[i-2] = ASC_CLOSE_BRACKET Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_CDATA
					tagCount += 1
					inCdata = False
				End
			' if we're in a quoted string, we're only looking for "
			ElseIf inQuote Then
				If str[i] = ASC_DOUBLE_QUOTE Then
					If quoteCount = quotesLength Then
						quotesLength *= 2
						quotes = quotes.Resize(quotesLength)
					End
					quotes[quoteCount] = i
					quoteCount += 1
					inQuote = False
				End
			' check if we should start a new quoted string
			ElseIf str[i] = ASC_DOUBLE_QUOTE Then
				If quoteCount = quotesLength Then
					quotesLength *= 2
					quotes = quotes.Resize(quotesLength)
				End
				quotes[quoteCount] = i
				quoteCount += 1
				inQuote = True
			' if we're in a processing instruction, we're only looking for ?>
			ElseIf inPi Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_QUESTION Then
					If piCount = pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = False
				End
			' if we're in a doctype, we're only looking for >
			ElseIf inDoctype Then
				If str[i] = ASC_GREATER_THAN Then
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DOCTYPE
					tagCount += 1
					inDoctype = False
				End
			' less than
			ElseIf str[i] = ASC_LESS_THAN Then
				' if we're in a tag, die
				If inTag Then AssertError "Invalid less than!"
				' check for prolog
				If str[i+1] = ASC_EXCLAMATION Then
					' comment?
					If str[i+2] = ASC_HYPHEN And str[i+3] = ASC_HYPHEN Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_COMMENT
						tagCount += 1
						inComment = True
					' cdata?
					ElseIf str[i+2] = ASC_OPEN_BRACKET And
							(str[i+3] = ASC_UPPER_C Or str[i+3] = ASC_LOWER_C) And
							(str[i+4] = ASC_UPPER_D Or str[i+4] = ASC_LOWER_D) And
							(str[i+5] = ASC_UPPER_A Or str[i+5] = ASC_LOWER_A) And
							(str[i+6] = ASC_UPPER_T Or str[i+6] = ASC_LOWER_T) And
							(str[i+7] = ASC_UPPER_A Or str[i+7] = ASC_LOWER_A) And
							str[i+8] = ASC_OPEN_BRACKET Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_CDATA
						tagCount += 1
						inCdata = True
					' doctype?
					ElseIf (str[i+2] = ASC_UPPER_D Or str[i+2] = ASC_LOWER_D) And
							(str[i+3] = ASC_UPPER_O Or str[i+3] = ASC_LOWER_O) And
							(str[i+4] = ASC_UPPER_C Or str[i+4] = ASC_LOWER_C) And
							(str[i+5] = ASC_UPPER_T Or str[i+5] = ASC_LOWER_T) And
							(str[i+6] = ASC_UPPER_Y Or str[i+6] = ASC_LOWER_Y) And
							(str[i+7] = ASC_UPPER_P Or str[i+7] = ASC_LOWER_P) And
							(str[i+8] = ASC_UPPER_E Or str[i+8] = ASC_LOWER_E) Then
						If tagCount = tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_DOCTYPE
						tagCount += 1
						inDoctype = True
					Else
						AssertError "Invalid prolog."
					End
				' check for processing instruction
				ElseIf str[i+1] = ASC_QUESTION Then
					If piCount = pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = True
				' finally, it must just be opening a tag
				Else
					If tagCount = tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DEFAULT
					tagCount += 1
					inTag = True
				End
			' greater than
			ElseIf str[i] = ASC_GREATER_THAN Then
				If Not inTag Then AssertError "Invalid greater than!"
				If tagCount = tagsLength Then
					tagsLength *= 2
					tags = tags.Resize(tagsLength)
					tagType = tagType.Resize(tagsLength)
				End
				tags[tagCount] = i
				tagType[tagCount] = TAG_DEFAULT
				tagCount += 1
				inTag = False
			End
		Next
		If inQuote Then AssertError "Unclosed quote!"
		If inTag Then AssertError "Unclosed tag!"
		If inComment Then AssertError "Unclosed comment!"
		If inCdata Then AssertError "Unclosed cdata!"
		If inPi Then AssertError "Unclosed processing instruction!"
	End

	' get the contents of a tag (given start and end)
	Method GetTagContents:XMLElement(startIndex:Int, endIndex:Int)
		' die if empty tag
		If startIndex = endIndex Then AssertError("Empty tag detected.")
		' our element
		Local e:XMLElement = New XMLElement
		Local a:Int, singleQuoted:Bool, doubleQuoted:Bool, key:String, value:String

		' get the name
		a = startIndex
		While a < endIndex
			If str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
				If a = endIndex-1 Then
					e.name = str[startIndex..endIndex]
				Else
					e.name = str[startIndex..a]
				End
				a += 1
				Exit
			End
			a += 1
		End
		startIndex = a

		' TODO: validate tag name is alphanumeric
		' if no name, die
		If e.name = "" Then AssertError("Error reading tag name.")

		' loop on all tokens
		While startIndex < endIndex
			' trim leading whitespace
			While startIndex < endIndex And (str[startIndex]=ASC_SPACE Or str[startIndex]=ASC_TAB Or str[startIndex]=ASC_LF Or str[startIndex]=ASC_CR)
				startIndex += 1
			End

			' clear check variables
			singleQuoted = False
			doubleQuoted = False
			key = ""
			value = ""

			' find the key
			a = startIndex
			While a < endIndex
				If str[a] = ASC_EQUALS Or str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
					If a=endIndex-1 Then
						key = str[startIndex..endIndex]
					Else
						key = str[startIndex..a]
					End
					a += 1
					Exit
				End
				a += 1
			End
			startIndex = a

			' if the key is empty, there was an error (unless we've hit the end of the string)
			If key = "" Then
				If a < endIndex Then
					AssertError("Error reading attribute key.")
				Else
					Exit
				End
			End

			' if it stopped on an equals, get the value
			If str[a-1] = ASC_EQUALS Then
				singleQuoted = False
				doubleQuoted = False
				While a < endIndex
					' check if it's a single quote
					If str[a] = ASC_SINGLE_QUOTE And Not doubleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							singleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected single quote detected in attribute value.")
						Else
							' we must be ending the quote here, so grab it and break out
							singleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' check if it's a double quote
					ElseIf str[a] = ASC_DOUBLE_QUOTE And Not singleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							doubleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected double quote detected in attribute value.")
						Else
							' we must be ending the quote here, so break out
							doubleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' should we be ending the attribute?
					ElseIf a = endIndex-1 Or (Not singleQuoted And Not doubleQuoted And (str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR))
						If a=endIndex-1 Then
							value = str[startIndex..endIndex]
						Else
							value = str[startIndex..a]
						End
						a += 1
						Exit
					End
					a += 1
				End
				startIndex = a
				value = UnescapeXMLString(value)

				If singleQuoted Or doubleQuoted Then AssertError("Unclosed quote detected.")
			End

			' set the attribute
			e.SetAttribute(key, value)

			If a >= endIndex Then Exit
		End
		Return e
	End

	Method ParseFile:XMLDocument(filename:String)
		Return ParseString(LoadString(filename))
	End

	Method TrimString:Void(startIdx:Int, endIdx:Int, trimmed:Int[])
		Local trimStart:Int = startIdx, trimEnd:Int = endIdx
		While trimEnd > trimStart
			Local ch:Int = str[trimEnd-1]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimEnd -= 1
			Else
				Exit
			End
		End
		While trimStart < trimEnd
			Local ch:Int = str[trimStart]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimStart += 1
			Else
				Exit
			End
		End
		trimmed[0] = trimStart
		trimmed[1] = trimEnd
	End

	' parses an xml doc, currently doesn't support nested PI or prolog
	Method ParseString:XMLDocument(str:String)
		Self.str = str

		Local doc:XMLDocument = New XMLDocument
		Local elements:ArrayList<XMLElement> = New ArrayList<XMLElement>
		Local thisE:XMLElement = Null, newE:XMLElement = Null
		Local index:Int = 0, a:Int, b:Int, c:Int, nextIndex:Int
		Local trimmed:Int[] = New Int[2]

		' cache all the control characters
		CacheControlCharacters()

		' find first opening tag
		If tagCount = 0 Then AssertError "Something seriously wrong... no tags!"

		' parse processing instructions
		index = 0
		a = pis[index]+2
		b = pis[index+1]-1
		While index < piCount
			TrimString(a, b, trimmed)
			If trimmed[0] <> trimmed[1] Then
				newE = GetTagContents(trimmed[0], trimmed[1])
				newE.pi = True
				doc.pi.Add(newE)
				newE = Null
			Else
				AssertError "Empty processing instruction."
			End
			index += 2
		End

		' loop on tags
		index = 0
		While index+1 < tagCount
			' we skip comments
			If tagType[index] = TAG_COMMENT Then
				' skip comments

			' if it's cdata
			ElseIf tagType[index] = TAG_CDATA Then
				' get the text between < and >
				a = tags[index]+9 ' "![CDATA[".Length
				b = tags[index+1]-2 ' "]]".Length

				' add a cdata element
				newE = New XMLElement
				newE.cdata = True
				newE.value = str[a..b]
				newE.parent = thisE
				thisE.AddChild(newE)
				newE = Null

			' otherwise we do normal tag stuff
			Else
				' get the text between < and >
				a = tags[index]+1
				b = tags[index+1]

				' trim the string
				TrimString(a, b, trimmed)

				' if it's a completely empty tag name, die
				If trimmed[0] = trimmed[1] Then AssertError "Empty tag."

				' check if the first character is a slash (end tag)
				If str[trimmed[0]] = ASC_SLASH Then
					' if no current element, die
					If thisE = Null Then AssertError("Closing tag found outside main document tag.")

					' strip the slash
					trimmed[0] += 1

					' check that the tag name length matches
					If trimmed[1] - trimmed[0] <> thisE.name.Length Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")

					' check that the tag name matches (manually so that we don't create an entire string slice when the first character could be wrong!)
					For Local nameIdx:Int = 0 Until thisE.name.Length
						If str[trimmed[0]+nameIdx] <> thisE.name[nameIdx] Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")
					Next

					' pop the element from the stack, or set the document root
					If Not elements.IsEmpty() Then
						thisE = elements.RemoveLast()
					Else
						'doc.root = thisE
						Exit
					End

				' check if the last character is a slash (self closing tag)
				ElseIf str[trimmed[1]-1] = ASC_SLASH Then
					' strip the slash
					trimmed[1] -= 1

					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					' add as a child or set as the root
					If doc.root = Null Then doc.root = newE
					If thisE <> Null Then
						thisE.AddChild(newE)
					Else
						'doc.root = newE
						Exit
					End
					newE = Null

				' otherwise it's an opening tag
				Else
					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					If doc.root = Null Then doc.root = newE

					' add as a child if we already have an element
					If thisE <> Null Then
						thisE.AddChild(newE)
					End

					' push this element
					elements.AddLast(thisE)

					' and set as the current
					thisE = newE
					newE = Null
				End
			End

			' get any text between tags
			index += 1
			If index < tagCount Then
				a = tags[index]+1
				b = tags[index+1]
				TrimString(a, b, trimmed)
				If trimmed[0] <> trimmed[1] Then
					If thisE <> Null Then
						thisE.value += UnescapeXMLString(str[trimmed[0]..trimmed[1]])
					Else
						'AssertError("Loose text outside of any tag!") - Getting this for some reason, I'll fix it later :/
					End
				End
			End

			' next tag
			index += 1
		End
		If doc.root = Null Then AssertError("Error parsing XML: no document tag found.")
		Return doc
	End
End

Class XMLDocument
Private
	Field xmlVersion:String = "1.0"
	Field xmlEncoding:String = "UTF-8"
	Field root:XMLElement
	Field pi:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field prologs:ArrayList<XMLElement> = New ArrayList<XMLElement>

Public

' Constructors
	Method New(rootName:String="")
		If rootName <> "" Then root = New XMLElement(rootName)
	End

	Method New(root:XMLElement)
		Self.root = root
	End

' Methods
	Method ExportString:String(formatXML:Bool = True)
		' we start with the xml instruction
		Local output:String = "<?xml version=~q"+xmlVersion+"~q encoding=~q"+xmlEncoding+"~q?>"
		If formatXML Then output += "~n"
		' root node
		output += root.ToString(formatXML) + "~n"
		' done!
		Return output
	End

	Method SaveFile:Void(filename:String)
		' TODO when file IO is ready!
	End

' Properties
	Method Root:XMLElement() Property
		Return root
	End

	Method Prologs:ArrayList<XMLElement>() Property
		Return prologs
	End

	Method ProcessingInstructions:ArrayList<XMLElement>() Property
		Return pi
	End

	Method Version:String() Property
		Return xmlVersion
	End

	Method Encoding:String() Property
		Return xmlEncoding
	End
End

Class XMLAttribute
Private
	Field name:String
	Field value:String

' Constructors
	Method New(name:String, value:String)
		Self.name = name
		Self.value = value
	End
End

Class XMLElement
Private
	Field openTagStart:String = "<"
	Field openTagEnd:String = ">"
	Field selfCloseEnd:String = " />"
	Field closeTagStart:String = "</"
	Field closeTagEnd:String = ">"

	Field name:String
	Field attributes:ArrayList<XMLAttribute> = New ArrayList<XMLAttribute>
	Field children:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field value:String
	Field parent:XMLElement

	Field pi:Bool
	Field prolog:Bool
	Field cdata:Bool

Public
' Constructors
	Method New()
	End

	Method New(name:String, parent:XMLElement = Null)
		Self.parent = parent
		Self.name = name
		If parent <> Null Then parent.children.Add(Self)
	End

' Methods
	Method IsProcessingInstruction:Bool()
		Return pi
	End

	Method IsProlog:Bool()
		Return prolog
	End

	Method IsCharacterData:Bool()
		Return cdata
	End

	' avoid using this method if you can, because you should try not to have "floating" elements
	Method AddChild:Void(child:XMLElement)
		If children.Contains(child) Return
		children.Add(child)
		child.parent = Self
	End

	Method HasAttribute:Bool(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return True
		Next
		Return False
	End

	Method GetAttribute:String(name:String, defaultValue:String = "")
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return att.value
		Next
		Return defaultValue
	End

	Method SetAttribute:String(name:String, value:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				Local old:String = att.value
				att.value = value
				Return old
			End
		Next
		attributes.Add(New XMLAttribute(name, value))
		Return ""
	End

	Method ClearAttribute:String(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				attributes.Remove(att)
				Return att.value
			End
		Next
		Return ""
	End

	Method Dispose:Void(removeSelf:Bool = True)
		' dispose children
		Local en:IEnumerator<XMLElement> = children.Enumerator()
		While en.HasNext()
			Local element:XMLElement = en.NextObject()
			element.Dispose(False)
			en.Remove()
		End
		' remove self from parent if this is not recursing
		If removeSelf And parent <> Null Then parent.children.Remove(Self)
		' clear out the parent
		parent = Null
	End

	Method ToString:String(formatXML:Bool=True, indentation:Int=0)
		Local rv:String = ""
		If formatXML Then
			For Local i% = 0 Until indentation
				rv += "  "
			Next
		End
		rv += openTagStart + name
		If Not attributes.IsEmpty() Then
			For Local i% = 0 Until attributes.Size
				Local att:XMLAttribute = attributes.Get(i)
				rv += " " + att.name + "=~q" + EscapeXMLString(att.value) + "~q"
			Next
		End
		If children.IsEmpty() Then
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() = 0
				rv += selfCloseEnd
			Else
				rv += openTagEnd + value.Trim() + closeTagStart + name + closeTagEnd
			End
		Else
			rv += openTagEnd
			If formatXML Then
				rv += "~n"
			End
			For Local i% = 0 Until children.Size
				rv += children.Get(i).ToString(formatXML, indentation + 1)
			End
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() > 0 Then
				If Not formatXML Then
					rv += esc
				Else
					rv += "~n" + esc + "~n"
				End
			End
			If formatXML Then
				For Local i% = 0 Until indentation
					rv += "  "
				Next
			End
			rv += closeTagStart + name + closeTagEnd
		End
		If formatXML Then
			rv += "~n"
		End
		Return rv
	End

	Method GetChildrenByName:ArrayList<XMLElement>(findName:String)
		Local rv:ArrayList<XMLElement> = New ArrayList<XMLElement>
		For Local element:XMLElement = Eachin children
			If element.name = findName Then rv.Add(element)
		Next
		Return rv
	End

	Method GetFirstChildByName:XMLElement(findName:String)
		For Local element:XMLElement = Eachin children
			If element.name = findName Then Return element
		Next
		Return Null
	End

' Properties
	Method Children:ArrayList<XMLElement>() Property
		Return children
	End

	Method Parent:XMLElement() Property
		Return parent
	End

	Method Name:String() Property
		Return name
	End

	Method Name:Void(name:String) Property
		Self.name = name
	End

	Method Value:String() Property
		Return value
	End

	Method Value:Void(value:String) Property
		Self.value = value
	End
End

' hopefully this stringbuilder-based replace should be faster since it doesn't create intermediate string objects!
Function EscapeXMLString:String(str:String)
	xmlsb.Length = 0
	For Local i:Int = 0 Until str.Length
		Select str[i]
			Case ASC_AMPERSAND
				xmlsb.Append("&")
			Case ASC_LESS_THAN
				xmlsb.Append("<")
			Case ASC_GREATER_THAN
				xmlsb.Append(">")
			Case ASC_SINGLE_QUOTE
				xmlsb.Append("'")
			Case ASC_DOUBLE_QUOTE
				xmlsb.Append(""")
			Default
				xmlsb.AppendByte(str[i])
		End
	End
	Return xmlsb.ToString()
End

' unescaping is rare so we won't bother using a stringbuilder
Function UnescapeXMLString:String(str:String)
	str = str.Replace(""", "~q")
	str = str.Replace("'", "'")
	str = str.Replace(">", ">")
	str = str.Replace("<", "<")
	str = str.Replace("&", "&")
	Return str
End

Private
Global xmlsb:StringBuilder = New StringBuilder