Replaced AssertError with XMLParseException in xml.monkey.

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

' 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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 Throw New XMLParseException("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+1 >= 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+1 >= 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+1 >= 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
						Throw New XMLParseException("Invalid prolog.")
					End
				' check for processing instruction
				ElseIf str[i+1] = ASC_QUESTION Then
					If piCount+1 >= 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+1 >= 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 Throw New XMLParseException("Invalid greater than!")
				If tagCount+1 = 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 Throw New XMLParseException("Unclosed quote!")
		If inTag Then Throw New XMLParseException("Unclosed tag!")
		If inComment Then Throw New XMLParseException("Unclosed comment!")
		If inCdata Then Throw New XMLParseException("Unclosed cdata!")
		If inPi Then Throw New XMLParseException("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 Throw New XMLParseException("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 Throw New XMLParseException("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
					Throw New XMLParseException("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
							Throw New XMLParseException("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
							Throw New XMLParseException("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 Throw New XMLParseException("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
			Throw New XMLParseException("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 Throw New XMLParseException("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
				Throw New XMLParseException("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 Throw New XMLParseException("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 Throw New XMLParseException("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 Throw New XMLParseException("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 Throw New XMLParseException("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 Throw New XMLParseException("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

Class XMLParseException Extends DiddyException
	Method New(message:String="", cause:Throwable=Null)
		Super.New(message, cause)
	End
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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 >= 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+1 = 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


Added 'VIRTUALOS_REAL_FILEPATH'; imports 'brl.filepath'.

Strict

Public

' Preprocessor related:
#If TARGET = "html5"
	#VIRTUALOS_IMPLEMENTED = True
	#VIRTUALOS_MAP_ENV = True
	'#VIRTUALOS_REAL_FILEPATH = True
#Elseif LANG = "cpp" And TARGET <> "win8"
	#VIRTUALOS_REAL = True
#End

' Imports (Public):
#If Not VIRTUALOS_IMPLEMENTED
	#If VIRTUALOS_REAL
		Import os
	#End
#Else
	Import "native/os.${LANG}"
#End

#If VIRTUALOS_REAL_FILEPATH
	Import brl.filepath
#End

' Imports (Private):
Private

#If VIRTUALOS_IMPLEMENTED
	Import regal.stringutil
#End

Public

#If VIRTUALOS_IMPLEMENTED
	' Constant variable(s):
	Const FILETYPE_NONE:=		0
	Const FILETYPE_FILE:=		1
	Const FILETYPE_DIR:=		2

	' Functions (External):
	Extern

	Function HostOS:String()
	Function AppPath:String()
	Function AppArgs:String[]()
	Function RealPath:String(Path:String)
	Function FileType:Int(Path:String)
	Function FileSize:Int(Path:String)
	Function FileTime:Int(Path:String)
	Function CopyFile:Bool(Src:String, Dst:String)
	Function DeleteFile:Bool(Path:String)
	Function LoadString:String(Path:String)
	Function SaveString:Int(Str:String, Path:String)
	Function LoadDir:String[](Path:String)
	Function CreateDir:Bool(Path:String)
	Function DeleteDir:Bool(Path:String)
	Function ChangeDir:Int(Path:String)
	Function CurrentDir:String()

	#If Not VIRTUALOS_MAP_ENV
		Function SetEnv:Int(Name:String, Value:String)
		Function GetEnv:String(Name:String)
	#End

	Function Execute:Int(CMD:String)
	Function ExitApp:Int(RetCode:Int)

	Public

	' Global variable(s) (Private):
	Private

	Global __OS_Env:= New StringMap<String>()

	Public

	' Functions (Monkey):
	#If VIRTUALOS_MAP_ENV
		Function SetEnv:Void(name:String, value:String)
			__OS_Env.Set(name, value)

			Return
		End

		Function GetEnv:String(name:String)
			Return __OS_Env.Get(name)
		End
	#End

	Function LoadDir:String[](Path:String, Recursive:Bool, Hidden:Bool=False)
		Local Dirs:= New StringDeque()
		Local Files:= New StringDeque()

		' Add an initial value for safety.
		Dirs.PushFirst("")

		While (Not Dirs.IsEmpty)
			Local Dir:= Dirs.PopFirst()

			For Local F:= Eachin LoadDir(Path + "/" + Dir)
				If ((Not Hidden) And F.StartsWith(".")) Then
					Continue
				Endif

				If (Dir.Length > 0) Then
					F = Dir + "/" + F
				Endif

				Select (FileType(Path + "/" + F))
					Case FILETYPE_FILE
						Files.PushLast(F)
					Case FILETYPE_DIR
						If (Recursive) Then
							Dirs.PushLast(F)
						Else
							Files.PushLast(F)
						Endif
				End Select
			Next
		Wend

		Return Files.ToArray()
	End

	Function CopyDir:Bool(SourcePath:String, DestinationPath:String, Recursive:Bool=True, Hidden:Bool=False)
		If (FileType(SourcePath) <> FILETYPE_DIR) Then
			Return False
		Endif

		Local Files:= LoadDir(SourcePath)

		Select (FileType(DestinationPath))
			Case FILETYPE_NONE
				If (Not CreateDir(DestinationPath)) Then
					Return False
				Endif
			Case FILETYPE_FILE
				Return False
		End Select

		For Local F:= Eachin Files
			If ((Not Hidden) And F.StartsWith(".")) Then
				Continue
			Endif

			Local SrcP:= (SourcePath + "/" + F)
			Local DstP$= (DestinationPath + "/" + F)

			Select (FileType(SrcP))
				Case FILETYPE_FILE
					If (Not CopyFile(SrcP, DstP)) Then
						Return False
					Endif
				Case FILETYPE_DIR
					If (Recursive And (Not CopyDir(SrcP, DstP, Recursive, Hidden))) Then
						Return False
					Endif
			End Select
		Next

		' Return the default response.
		Return True
	End

	Function DeleteDir:Bool(Path$, Recursive:Bool)
		If (Not Recursive) Then
			Return DeleteDir(Path)
		Endif

		Select (FileType(Path))
			Case FILETYPE_NONE
				Return True
			Case FILETYPE_FILE
				Return False
		End Select

		For Local F:= Eachin LoadDir(Path)
			If (F = "." Or F = "..") Then
				Continue
			Endif

			Local FPath:= (Path + "/" + F)

			If (FileType(FPath) = FILETYPE_DIR) Then
				If (Not DeleteDir(FPath, True)) Then
					Return False
				Endif
			Else
				If (Not DeleteFile(FPath)) Then
					Return False
				Endif
			Endif
		Next

		Return DeleteDir(Path)
	End

	#If Not VIRTUALOS_REAL_FILEPATH
		Function StripDir:String(Path:String)
			Local I:= Path.FindLast("/")

			If (I = STRING_INVALID_LOCATION) Then
				I = Path.FindLast( "\" )
			Endif

			If (I <> STRING_INVALID_LOCATION) Then
				Return Path[I+1..]
			Endif

			Return Path
		End

		Function ExtractDir:String(Path:String)
			Local I:= Path.FindLast("/")

			If (I = STRING_INVALID_LOCATION) Then
				I = Path.FindLast("\")
			Endif

			If (I <> STRING_INVALID_LOCATION) Then
				Return Path[..I]
			Endif

			Return ""
		End

		Function StripExt:String(Path:String)
			Local I:= Path.FindLast(".")
			Local SecondarySearchPos:= (I+1)

			If ((I <> STRING_INVALID_LOCATION) And (Path.Find("/", SecondarySearchPos) = STRING_INVALID_LOCATION) And (Path.Find("\", SecondarySearchPos)= STRING_INVALID_LOCATION)) Then
				Return Path[..I]
			Endif

			Return Path
		End

		Function ExtractExt:String(Path:String)
			Local I:= Path.FindLast(".")
			Local SecondarySearchPos:= (I+1)

			If ((I <> STRING_INVALID_LOCATION) And (Path.Find("/", SecondarySearchPos) = STRING_INVALID_LOCATION) And (Path.Find("\", SecondarySearchPos)= STRING_INVALID_LOCATION)) Then
				Return Path[SecondarySearchPos..]
			Endif

			Return ""
		End

		Function StripAll:String(Path:String)
			Return StripDir(StripExt(Path))
		End
	#End
#End

Strict

Public

' Preprocessor related:
#If TARGET = "html5"
	#VIRTUALOS_IMPLEMENTED = True
	#VIRTUALOS_MAP_ENV = True
#Elseif LANG = "cpp" And TARGET <> "win8"
	#VIRTUALOS_REAL = True
#End

' Imports (Public):
#If Not VIRTUALOS_IMPLEMENTED
	#If VIRTUALOS_REAL
		Import os
	#End
#Else
	Import "native/os.${LANG}"
#End

' Imports (Private):
Private

#If VIRTUALOS_IMPLEMENTED
	Import regal.stringutil
#End

Public

#If VIRTUALOS_IMPLEMENTED
	' Constant variable(s):
	Const FILETYPE_NONE:=		0
	Const FILETYPE_FILE:=		1
	Const FILETYPE_DIR:=		2

	' Functions (External):
	Extern

	Function HostOS:String()
	Function AppPath:String()
	Function AppArgs:String[]()
	Function RealPath:String(Path:String)
	Function FileType:Int(Path:String)
	Function FileSize:Int(Path:String)
	Function FileTime:Int(Path:String)
	Function CopyFile:Bool(Src:String, Dst:String)
	Function DeleteFile:Bool(Path:String)
	Function LoadString:String(Path:String)
	Function SaveString:Int(Str:String, Path:String)
	Function LoadDir:String[](Path:String)
	Function CreateDir:Bool(Path:String)
	Function DeleteDir:Bool(Path:String)
	Function ChangeDir:Int(Path:String)
	Function CurrentDir:String()

	#If Not VIRTUALOS_MAP_ENV
		Function SetEnv:Int(Name:String, Value:String)
		Function GetEnv:String(Name:String)
	#End

	Function Execute:Int(CMD:String)
	Function ExitApp:Int(RetCode:Int)

	Public

	' Global variable(s) (Private):
	Private

	Global __OS_Env:= New StringMap<String>()

	Public

	' Functions (Monkey):
	#If VIRTUALOS_MAP_ENV
		Function SetEnv:Void(name:String, value:String)
			__OS_Env.Set(name, value)

			Return
		End

		Function GetEnv:String(name:String)
			Return __OS_Env.Get(name)
		End
	#End

	Function LoadDir:String[](Path:String, Recursive:Bool, Hidden:Bool=False)
		Local Dirs:= New StringDeque()
		Local Files:= New StringDeque()

		' Add an initial value for safety.
		Dirs.PushFirst("")

		While (Not Dirs.IsEmpty)
			Local Dir:= Dirs.PopFirst()

			For Local F:= Eachin LoadDir(Path + "/" + Dir)
				If ((Not Hidden) And F.StartsWith(".")) Then
					Continue
				Endif

				If (Dir.Length > 0) Then
					F = Dir + "/" + F
				Endif

				Select (FileType(Path + "/" + F))
					Case FILETYPE_FILE
						Files.PushLast(F)
					Case FILETYPE_DIR
						If (Recursive) Then
							Dirs.PushLast(F)
						Else
							Files.PushLast(F)
						Endif
				End Select
			Next
		Wend

		Return Files.ToArray()
	End

	Function CopyDir:Bool(SourcePath:String, DestinationPath:String, Recursive:Bool=True, Hidden:Bool=False)
		If (FileType(SourcePath) <> FILETYPE_DIR) Then
			Return False
		Endif

		Local Files:= LoadDir(SourcePath)

		Select (FileType(DestinationPath))
			Case FILETYPE_NONE
				If (Not CreateDir(DestinationPath)) Then
					Return False
				Endif
			Case FILETYPE_FILE
				Return False
		End Select

		For Local F:= Eachin Files
			If ((Not Hidden) And F.StartsWith(".")) Then
				Continue
			Endif

			Local SrcP:= (SourcePath + "/" + F)
			Local DstP$= (DestinationPath + "/" + F)

			Select (FileType(SrcP))
				Case FILETYPE_FILE
					If (Not CopyFile(SrcP, DstP)) Then
						Return False
					Endif
				Case FILETYPE_DIR
					If (Recursive And (Not CopyDir(SrcP, DstP, Recursive, Hidden))) Then
						Return False
					Endif
			End Select
		Next

		' Return the default response.
		Return True
	End

	Function DeleteDir:Bool(Path$, Recursive:Bool)
		If (Not Recursive) Then
			Return DeleteDir(Path)
		Endif

		Select (FileType(Path))
			Case FILETYPE_NONE
				Return True
			Case FILETYPE_FILE
				Return False
		End Select

		For Local F:= Eachin LoadDir(Path)
			If (F = "." Or F = "..") Then
				Continue
			Endif

			Local FPath:= (Path + "/" + F)

			If (FileType(FPath) = FILETYPE_DIR) Then
				If (Not DeleteDir(FPath, True)) Then
					Return False
				Endif
			Else
				If (Not DeleteFile(FPath)) Then
					Return False
				Endif
			Endif
		Next

		Return DeleteDir(Path)
	End

	Function StripDir:String(Path:String)
		Local I:= Path.FindLast("/")

		If (I = STRING_INVALID_LOCATION) Then
			I = Path.FindLast( "\" )
		Endif

		If (I <> STRING_INVALID_LOCATION) Then
			Return Path[I+1..]
		Endif

		Return Path
	End

	Function ExtractDir:String(Path:String)
		Local I:= Path.FindLast("/")

		If (I = STRING_INVALID_LOCATION) Then
			I = Path.FindLast("\")
		Endif

		If (I <> STRING_INVALID_LOCATION) Then
			Return Path[..I]
		Endif

		Return ""
	End

	Function StripExt:String(Path:String)
		Local I:= Path.FindLast(".")
		Local SecondarySearchPos:= (I+1)

		If ((I <> STRING_INVALID_LOCATION) And (Path.Find("/", SecondarySearchPos) = STRING_INVALID_LOCATION) And (Path.Find("\", SecondarySearchPos)= STRING_INVALID_LOCATION)) Then
			Return Path[..I]
		Endif

		Return Path
	End

	Function ExtractExt:String(Path:String)
		Local I:= Path.FindLast(".")
		Local SecondarySearchPos:= (I+1)

		If ((I <> STRING_INVALID_LOCATION) And (Path.Find("/", SecondarySearchPos) = STRING_INVALID_LOCATION) And (Path.Find("\", SecondarySearchPos)= STRING_INVALID_LOCATION)) Then
			Return Path[SecondarySearchPos..]
		Endif

		Return ""
	End

	Function StripAll:String(Path:String)
		Return StripDir(StripExt(Path))
	End
#End


Fixed some errors in Matrix3D...

Fixed some errors in Matrix3D...

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Strict

Private
Import vector2d

Public
Class Matrix3D
Public
	' +-           -+
	' | M00 M01 M02 |
	' | M10 M11 M12 |
	' | M20 M21 M22 |
	' +-           -+
	Const M00:Int = 0 ' row 0, column 0
	Const M01:Int = 3 ' row 0, column 1
	Const M02:Int = 6 ' row 0, column 2
	Const M10:Int = 1 ' row 1, column 0
	Const M11:Int = 4 ' row 1, column 1
	Const M12:Int = 7 ' row 1, column 2
	Const M20:Int = 2 ' row 2, column 0
	Const M21:Int = 5 ' row 2, column 1
	Const M22:Int = 8 ' row 2, column 2

Private
	Field tmp:Float[] = New Float[9]

Public
	Field val:Float[] = New Float[9]

	Method New()
		Identity()
	End

	Method New(matrix:Matrix3D)
		SetToMatrix(matrix)
	End

	Method Identity:Matrix3D()
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0
		val[M01] = 0
		val[M11] = 1
		val[M21] = 0
		val[M02] = 0
		val[M12] = 0
		val[M22] = 1
		Return Self
	End

	Method CrossMultiply:Matrix3D(m:Matrix3D)
		Return CrossMultiply(m.val)
	End

	Method CrossMultiply:Matrix3D(arr:Float[])
		Local v00:Float = val[M00] * arr[M00] + val[M01] * arr[M10] + val[M02] * arr[M20]
		Local v01:Float = val[M00] * arr[M01] + val[M01] * arr[M11] + val[M02] * arr[M21]
		Local v02:Float = val[M00] * arr[M02] + val[M01] * arr[M12] + val[M02] * arr[M22]

		Local v10:Float = val[M10] * arr[M00] + val[M11] * arr[M10] + val[M12] * arr[M20]
		Local v11:Float = val[M10] * arr[M01] + val[M11] * arr[M11] + val[M12] * arr[M21]
		Local v12:Float = val[M10] * arr[M02] + val[M11] * arr[M12] + val[M12] * arr[M22]

		Local v20:Float = val[M20] * arr[M00] + val[M21] * arr[M10] + val[M22] * arr[M20]
		Local v21:Float = val[M20] * arr[M01] + val[M21] * arr[M11] + val[M22] * arr[M21]
		Local v22:Float = val[M20] * arr[M02] + val[M21] * arr[M12] + val[M22] * arr[M22]

		val[M00] = v00
		val[M10] = v10
		val[M20] = v20
		val[M01] = v01
		val[M11] = v11
		val[M21] = v21
		val[M02] = v02
		val[M12] = v12
		val[M22] = v22

		Return Self
	End

	Method SetToRotation:Matrix3D(degrees:Float)
		Local cos:Float = Cos(degrees)
		Local sin:Float = Sin(degrees)

		val[M00] = cos
		val[M10] = sin
		val[M20] = 0

		val[M01] = -sin
		val[M11] = cos
		val[M21] = 0

		val[M02] = 0
		val[M12] = 0
		val[M22] = 1

		Return Self
	End

	Method SetToTranslation:Matrix3D(x:Float, y:Float)
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0

		val[M01] = 0
		val[M11] = 1
		val[M21] = 0

		val[M02] = x
		val[M12] = y
		val[M22] = 1

		Return Self
	End

	Method SetToTranslation:Matrix3D(translation:Vector2D)
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0

		val[M01] = 0
		val[M11] = 1
		val[M21] = 0

		val[M02] = translation.x
		val[M12] = translation.y
		val[M22] = 1

		Return Self
	End

	Method SetToScaling:Matrix3D(scaleX:Float, scaleY:Float)
		val[M00] = scaleX
		val[M10] = 0
		val[M20] = 0
		val[M01] = 0
		val[M11] = scaleY
		val[M21] = 0
		val[M02] = 0
		val[M12] = 0
		val[M22] = 1
		Return Self
	End

	Method Determinant:Float()
		Return val[M00] * val[M11] * val[M22] + val[M01] * val[M12] * val[M20] + val[M02] * val[M10] * val[M21] - val[M00] * val[M12] * val[M21] - val[M01] * val[M10] * val[M22] - val[M02] * val[M11] * val[M20]
	End

	Method Inverse:Matrix3D()
		Local d:Float = Determinant()
		If d = 0 Then Return Self

		Local inv_det:Float = 1.0 / d

		tmp[M00] = val[M11] * val[M22] - val[M21] * val[M12]
		tmp[M10] = val[M20] * val[M12] - val[M10] * val[M22]
		tmp[M20] = val[M10] * val[M21] - val[M20] * val[M11]
		tmp[M01] = val[M21] * val[M02] - val[M01] * val[M22]
		tmp[M11] = val[M00] * val[M22] - val[M20] * val[M02]
		tmp[M21] = val[M20] * val[M01] - val[M00] * val[M21]
		tmp[M02] = val[M01] * val[M12] - val[M11] * val[M02]
		tmp[M12] = val[M10] * val[M02] - val[M00] * val[M12]
		tmp[M22] = val[M00] * val[M11] - val[M10] * val[M01]

		val[M00] = inv_det * tmp[M00]
		val[M10] = inv_det * tmp[M10]
		val[M20] = inv_det * tmp[M20]
		val[M01] = inv_det * tmp[M01]
		val[M11] = inv_det * tmp[M11]
		val[M21] = inv_det * tmp[M21]
		val[M02] = inv_det * tmp[M02]
		val[M12] = inv_det * tmp[M12]
		val[M22] = inv_det * tmp[M22]

		Return Self
	End

	Method SetToMatrix:Matrix3D(mat:Matrix3D)
		For Local i:Int = 0 Until val.Length
			val[i] = mat.val[i]
		Next
		Return Self
	End

	Method ScalarTranslate:Matrix3D(vector:Vector2D)
		val[M02] += vector.x
		val[M12] += vector.y
		Return Self
	End

	Method ScalarTranslate:Matrix3D(x:Float, y:Float)
		val[M02] += x
		val[M12] += y
		Return Self
	End

	Method Translate:Matrix3D(x:Float, y:Float)
		tmp[M00] = 1
		tmp[M10] = 0
		tmp[M20] = 0

		tmp[M01] = 0
		tmp[M11] = 1
		tmp[M21] = 0

		tmp[M02] = x
		tmp[M12] = y
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Translate:Matrix3D(translation:Vector2D)
		tmp[M00] = 1
		tmp[M10] = 0
		tmp[M20] = 0

		tmp[M01] = 0
		tmp[M11] = 1
		tmp[M21] = 0

		tmp[M02] = translation.x
		tmp[M12] = translation.y
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Rotate:Matrix3D(angle:Float)
		If angle = 0 Then Return Self
		Local cos:Float = Cos(angle)
		Local sin:Float = Sin(angle)

		tmp[M00] = cos
		tmp[M10] = sin
		tmp[M20] = 0

		tmp[M01] = -sin
		tmp[M11] = cos
		tmp[M21] = 0

		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Scale:Matrix3D(scaleX:Float, scaleY:Float)
		tmp[M00] = scaleX
		tmp[M10] = 0
		tmp[M20] = 0
		tmp[M01] = 0
		tmp[M11] = scaleY
		tmp[M21] = 0
		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Scale:Matrix3D(v:Vector2D)
		tmp[M00] = v.x
		tmp[M10] = 0
		tmp[M20] = 0
		tmp[M01] = 0
		tmp[M11] = v.y
		tmp[M21] = 0
		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method ScalarScale:Matrix3D(s:Float)
		val[M00] *= s
		val[M11] *= s
		Return Self
	End

	Method ScalarScale:Matrix3D(v:Vector2D)
		val[M00] *= v.x
		val[M11] *= v.y
		Return Self
	End

	Method Transpose:Matrix3D()
		Local v01:Float = val[M10]
		Local v02:Float = val[M20]
		Local v10:Float = val[M01]
		Local v12:Float = val[M21]
		Local v20:Float = val[M02]
		Local v21:Float = val[M12]
		val[M01] = v01
		val[M02] = v02
		val[M10] = v10
		val[M12] = v12
		val[M20] = v20
		val[M21] = v21
		Return Self
	End
End

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Strict

Private
Import vector2d

Public
Class Matrix3D
Public
	' +-           -+
	' | M00 M01 M02 |
	' | M10 M11 M12 |
	' | M20 M21 M22 |
	' +-           -+
	Const M00:Int = 0 ' row 0, column 0
	Const M01:Int = 3 ' row 0, column 1
	Const M02:Int = 6 ' row 0, column 2
	Const M10:Int = 1 ' row 1, column 0
	Const M11:Int = 4 ' row 1, column 1
	Const M12:Int = 7 ' row 1, column 2
	Const M20:Int = 2 ' row 2, column 0
	Const M21:Int = 5 ' row 2, column 1
	Const M22:Int = 8 ' row 2, column 2

Private
	Field tmp:Float[] = New Float[9]

Public
	Field val:Float[] = New Float[9]

	Method New()
		Identity()
	End

	Method New(matrix:Matrix3D)
		SetToMatrix(matrix)
	End

	Method Identity:Matrix3D()
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0
		val[M01] = 0
		val[M11] = 1
		val[M21] = 0
		val[M02] = 0
		val[M12] = 0
		val[M22] = 1
		Return Self
	End

	Method CrossMultiply:Matrix3D(m:Matrix3D)
		Local v00:Float = val[M00] * m.val[M00] + val[M01] * m.val[M10] + val[M02] * m.val[M20]
		Local v01:Float = val[M00] * m.val[M01] + val[M01] * m.val[M11] + val[M02] * m.val[M21]
		Local v02:Float = val[M00] * m.val[M02] + val[M01] * m.val[M12] + val[M02] * m.val[M22]

		Local v10:Float = val[M10] * m.val[M00] + val[M11] * m.val[M10] + val[M12] * m.val[M20]
		Local v11:Float = val[M10] * m.val[M01] + val[M11] * m.val[M11] + val[M12] * m.val[M21]
		Local v12:Float = val[M10] * m.val[M02] + val[M11] * m.val[M12] + val[M12] * m.val[M22]

		Local v20:Float = val[M20] * m.val[M00] + val[M21] * m.val[M10] + val[M22] * m.val[M20]
		Local v21:Float = val[M20] * m.val[M01] + val[M21] * m.val[M11] + val[M22] * m.val[M21]
		Local v22:Float = val[M20] * m.val[M02] + val[M21] * m.val[M12] + val[M22] * m.val[M22]

		val[M00] = v00
		val[M10] = v10
		val[M20] = v20
		val[M01] = v01
		val[M11] = v11
		val[M21] = v21
		val[M02] = v02
		val[M12] = v12
		val[M22] = v22

		Return Self
	End

	Method SetToRotation:Matrix3D(degrees:Float)
		Local cos:Float = Cos(degrees)
		Local sin:Float = Sin(degrees)

		val[M00] = cos
		val[M10] = sin
		val[M20] = 0

		val[M01] = -sin
		val[M11] = cos
		val[M21] = 0

		val[M02] = 0
		val[M12] = 0
		val[M22] = 1

		Return Self
	End

	Method SetToTranslation:Matrix3D(x:Float, y:Float)
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0

		val[M01] = 0
		val[M11] = 1
		val[M21] = 0

		val[M02] = x
		val[M12] = y
		val[M22] = 1

		Return Self
	End

	Method SetToTranslation:Matrix3D(translation:Vector2D)
		val[M00] = 1
		val[M10] = 0
		val[M20] = 0

		val[M01] = 0
		val[M11] = 1
		val[M21] = 0

		val[M02] = translation.x
		val[M12] = translation.y
		val[M22] = 1

		Return Self
	End

	Method SetToScaling:Matrix3D(scaleX:Float, scaleY:Float)
		val[M00] = scaleX
		val[M10] = 0
		val[M20] = 0
		val[M01] = 0
		val[M11] = scaleY
		val[M21] = 0
		val[M02] = 0
		val[M12] = 0
		val[M22] = 1
		Return Self
	End

	Method Determinant:Float()
		Return val[M00] * val[M11] * val[M22] + val[M01] * val[M12] * val[M20] + val[M02] * val[M10] * val[M21] - val[M00] * val[M12] * val[M21] - val[M01] * val[M10] * val[M22] - val[M02] * val[M11] * val[M20]
	End

	Method Inverse:Matrix3D()
		Local d:Float = Determinant()
		If d = 0 Then Return Self

		Local inv_det:Float = 1.0 / d

		tmp[M00] = val[M11] * val[M22] - val[M21] * val[M12]
		tmp[M10] = val[M20] * val[M12] - val[M10] * val[M22]
		tmp[M20] = val[M10] * val[M21] - val[M20] * val[M11]
		tmp[M01] = val[M21] * val[M02] - val[M01] * val[M22]
		tmp[M11] = val[M00] * val[M22] - val[M20] * val[M02]
		tmp[M21] = val[M20] * val[M01] - val[M00] * val[M21]
		tmp[M02] = val[M01] * val[M12] - val[M11] * val[M02]
		tmp[M12] = val[M10] * val[M02] - val[M00] * val[M12]
		tmp[M22] = val[M00] * val[M11] - val[M10] * val[M01]

		val[M00] = inv_det * tmp[M00]
		val[M10] = inv_det * tmp[M10]
		val[M20] = inv_det * tmp[M20]
		val[M01] = inv_det * tmp[M01]
		val[M11] = inv_det * tmp[M11]
		val[M21] = inv_det * tmp[M21]
		val[M02] = inv_det * tmp[M02]
		val[M12] = inv_det * tmp[M12]
		val[M22] = inv_det * tmp[M22]

		Return Self
	End

	Method SetToMatrix:Matrix3D(mat:Matrix3D)
		For Local i:Int = 0 Until val.Length
			val[i] = mat.val[i]
		Next
		Return Self
	End

	Method ScalarTranslate:Matrix3D(vector:Vector2D)
		val[M02] += vector.x
		val[M12] += vector.y
		Return Self
	End

	Method ScalarTranslate:Matrix3D(x:Float, y:Float)
		val[M02] += x
		val[M12] += y
		Return Self
	End

	Method Translate:Matrix3D(x:Float, y:Float)
		tmp[M00] = 1
		tmp[M10] = 0
		tmp[M20] = 0

		tmp[M01] = 0
		tmp[M11] = 1
		tmp[M21] = 0

		tmp[M02] = x
		tmp[M12] = y
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Translate:Matrix3D(translation:Vector2)
		tmp[M00] = 1
		tmp[M10] = 0
		tmp[M20] = 0

		tmp[M01] = 0
		tmp[M11] = 1
		tmp[M21] = 0

		tmp[M02] = translation.x
		tmp[M12] = translation.y
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Rotate:Matrix3D(angle:Float)
		If angle = 0 Then Return Self
		Local cos:Float = Cos(angle)
		Local sin:Float = Sin(angle)

		tmp[M00] = cos
		tmp[M10] = sin
		tmp[M20] = 0

		tmp[M01] = -sin
		tmp[M11] = cos
		tmp[M21] = 0

		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Scale:Matrix3D(scaleX:Float, scaleY:Float)
		tmp[M00] = scaleX
		tmp[M10] = 0
		tmp[M20] = 0
		tmp[M01] = 0
		tmp[M11] = scaleY
		tmp[M21] = 0
		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method Scale:Matrix3D(v:Vector2)
		tmp[M00] = v.x
		tmp[M10] = 0
		tmp[M20] = 0
		tmp[M01] = 0
		tmp[M11] = v.y
		tmp[M21] = 0
		tmp[M02] = 0
		tmp[M12] = 0
		tmp[M22] = 1
		CrossMultiply(tmp)
		Return Self
	End

	Method ScalarScale:Matrix3D(s:Float)
		val[M00] *= s
		val[M11] *= s
		Return Self
	End

	Method ScalarScale:Matrix3D(v:Vector2D)
		val[M00] *= v.x
		val[M11] *= v.y
		Return Self
	End

	Method Transpose:Matrix3D()
		Local v01:Float = val[M10]
		Local v02:Float = val[M20]
		Local v10:Float = val[M01]
		Local v12:Float = val[M21]
		Local v20:Float = val[M02]
		Local v21:Float = val[M12]
		val[M01] = v01
		val[M02] = v02
		val[M10] = v10
		val[M12] = v12
		val[M20] = v20
		val[M21] = v21
		Return Self
	End
End


Reflection functions to FlxU added

Reflection functions to FlxU added

Strict

Import mojo
Import reflection

Import flxextern
Import flxpoint
Import flxg

Alias MonkeyAbs = monkey.math.Abs
Alias MonkeyFloor = monkey.math.Floor
Alias MonkeyCeil = monkey.math.Ceil
Alias MonkeyMin = monkey.math.Min
Alias MonkeyMax = monkey.math.Max
Alias MonkeyGetClass = reflection.GetClass
Alias NativeOpenURL = flxextern.OpenURL

Class FlxU

	Function OpenURL:Void(url:String)
		NativeOpenURL(url)
	End Function

	Function Abs:Float(value:Float)
		Return MonkeyAbs(value)
	End Function

	Function Floor:Float(value:Float)
		Return MonkeyFloor(value)
	End Function

	Function Ceil:Float(value:Float)
		Return MonkeyCeil(value)
	End Function

	Function Round:Float(value:Float)
		If (value > 0) Then
			Return Int(value + .5)
		Else
			Return Int(value - .5)
		End If
	End Function

	Function Min:Float(number1:Float, number2:Float)
		Return MonkeyMin(number1, number2)
	End Function

	Function Max:Float(number1:Float, number2:Float)
		Return MonkeyMax(number1, number2)
	End Function

	Function Bound:Float(value:Float, min:Float, max:Float)
		Return Clamp(value, min, max)
	End Function

	Function Srand:Float(seed:Float)
		Return Float(seed Shr 8 & $ffffff)/$1000000
	End Function

	#Rem
	Function Shuffle:Object[](objects:Object[], howManyTimes:Int)
		Shuffle is not currently supported in Monkey. Use FlxArray.Shuffle method
	End Function
	#End

	#Rem
	Function GetRandom:FlxBasic(objects:FlxBasic[], startIndex:Int = 0, length:Int = 0)
		GetRandom is not currently supported in Monkey. Use FlxArray.GetRandom method
	End Function
	#End

	Function GetTicks:Int()
		Return Millisecs()
	End Function

	Function FormatTicks:String(startTicks:Int, endTicks:Int)
		Return ((endTicks - startTicks) / 1000) + "s"
	End Function

	Function MakeColor:Int(red:Int, green:Int, blue:Int, alpha:Float = 1)
		If (alpha <= 1) alpha *= 255
		Return (alpha & $FF) Shl 24 | (red & $FF) Shl 16 | (green & $FF) Shl 8 | (blue & $FF)
	End Function

	Function MakeColorFromHSB:Int(hue:Float, saturation:Float, brightness:Float, alpha:Float = 1)
		Local red:Float
		Local green:Float
		Local blue:Float

		If (saturation = 0)
			red = brightness
			green = brightness
			blue = brightness
		Else
			If (hue = 360) hue = 0

			Local slice:Int = hue / 60
			Local hf:Float = hue / 60 - slice
			Local aa:Float = brightness * (1 - saturation)
			Local bb:Float = brightness * (1 - saturation * hf)
			Local cc:Float = brightness * (1 - saturation * (1.0 - hf))

			Select (slice)
				Case 0
					red = brightness
					green = cc
					blue = aa

				Case 1
					red = bb
					green = brightness
					blue = aa

				Case 2
					red = aa
					green = brightness
					blue = cc

				Case 3
					red = aa
					green = bb
					blue = brightness

				Case 4
					red = cc
					green = aa
					blue = brightness

				Case 5
					red = brightness
					green = aa
					blue = bb

				Default
					red = 0
					green = 0
					blue = 0
			End Select
		End If

		If (alpha <= 1) alpha *= 255
		Return (alpha & $FF) Shl 24 | int(red * 255) Shl 16 | int(green * 255) Shl 8 | int(blue * 255)
	End Function

	Function GetRGBA:Float[](color:Int, resluts:Float[] = [])
		If (resluts.Length() <> 4) resluts = resluts.Resize(4)

		resluts[0] = (color Shr 16) & $FF
		resluts[1] = (color Shr 8) & $FF
		resluts[2] = color & $FF
		resluts[3] = Float((color Shr 24) & $FF) / 255
		Return resluts
	End Function

	Function GetHSB:Float[](color:Int, resluts:Float[] = [])
		If (resluts.Length() <> 4) resluts = resluts.Resize(4)

		Local red:Float = Float((color Shr 16) & $FF) / 255
		Local green:Float = Float((color Shr 8) & $FF) / 255
		Local blue:Float = Float(color & $FF) / 255

		Local m:Float = MonkeyMax(red, green)
		Local dmax:Float = MonkeyMax(m, blue)
		m = MonkeyMin(red, green)
		Local dmin:Float = MonkeyMin(m, blue)
		Local range:Float = dmax - dmin

		resluts[2] = dmax
		resluts[1] = 0
		resluts[0] = 0

		If (dmax <> 0) resluts[1] = range / dmax

		If (resluts[1] <> 0) Then
			If (red = dmax) Then
				resluts[0] = (green - blue) / range

			ElseIf (green = dmax) Then
				resluts[0] = 2 + (blue - red) / range

			ElseIf (blue = dmax) Then
				resluts[0] = 4 + (red - green) / range
			End If

			resluts[0] *= 60
			If (resluts[0] < 0) resluts[0] += 360
		End If

		resluts[3] = Float((color Shr 24) & $FF) / 255

		Return resluts
	End Function

	Function FromatTime:String(seconds:Float, showMS:Bool = False)
		Local timeString:StringStack = New StringStack()
		timeString.Push(Int(seconds / 60) + ":")

		Local timeStringHepler:Int = Int(seconds) Mod 60

		If (timeStringHepler < 10) Then
			timeString.Push("0")
		End If

		timeString.Push(timeStringHepler)

		If (showMS) Then
			timeString.Push(".")
			timeStringHepler = (seconds - Int(seconds)) * 100

			If (timeStringHepler < 10) Then
				timeString.Push("0")
			End If

			timeString.Push(timeStringHepler)
		End If

		Return timeString.Join("")
	End Function

	Function FromatMoney:String(amount:Float, showDecimal:Bool = True, englishStyle:Bool = True)
		Local helper:Int
		Local intAmount:Int = amount
		Local comma:String = ""
		Local result:String = ""
		Local zeroes:String = ""

		While (intAmount > 0)
			If (result.Length() > 0 And comma.Length() <= 0) Then
				If (englishStyle) Then
					comma = ","
				Else
					comma = "."
				End If
			End If

			helper = intAmount - Int(intAmount / 1000) * 1000
			intAmount /= 1000

			zeroes = ""

			If (intAmount > 0) Then
				If (helper < 100) Then
					zeroes = "0"
				End If

				If (helper < 10) Then
					zeroes = "00"
				End If
			End If

			result = zeroes + helper + comma + result
		Wend

		If (showDecimal) Then
			intAmount = Int(amount * 100) - (Int(amount) * 100)

			If (englishStyle) Then
				result += "." + intAmount
			Else
				result += "," + intAmount
			End If

			If (amount < 10) Then
				result += "0"
			End If
		End If

		Return result
	End Function

	Function GetClassName:String(obj:Object, simple:Bool = False)
		If (simple) Then
			Local name:String = MonkeyGetClass(obj).Name
			Return name[name.FindLast(".")+1..]
		End If

		Return MonkeyGetClass(obj).Name
	End Function

	Function CompareClassNames:Bool(object1:Object, object2:Object)
		Return (MonkeyGetClass(object1).Name = MonkeyGetClass(object2).Name)
	End Function

	Function GetClass:ClassInfo(name:String)
		Return MonkeyGetClass(name)
	End Function

	Function ComputeVelocity:Float(velocity:Float, acceleration:Float = 0, drag:Float = 0, max:Float = 10000)
		If (acceleration <> 0) Then
			velocity += acceleration * FlxG.Elapsed

		ElseIf (drag <> 0) Then
			drag *= FlxG.Elapsed

			If (velocity - drag > 0) Then
				velocity -= drag
			ElseIf (velocity + drag < 0) Then
				velocity += drag
			Else
				velocity = 0
			End If
		End If

		If (velocity <> 0 And max <> 10000) Then
			If (velocity > max) Then
				velocity = max
			ElseIf (velocity < -max) Then
				velocity = -max
			End If
		End If

		Return velocity
	End Function

	Function RotatePoint:FlxPoint(x:Float, y:Float, pivotX:Float, pivotY:Float, angle:Float, point:FlxPoint = Null)
		Local sin:Float = 0
		Local cos:Float = 0
		Local radians:Float = angle * -0.017453293

		While (radians < -PI)
			radians += TWOPI
		Wend

		While (radians > PI)
			radians = radians - TWOPI
		Wend

		If (radians < 0) Then
			sin =  1.27323954 * radians + .405284735 * radians * radians
			If (sin < 0) Then
				sin = .225 * (sin * -sin - sin) + sin
			Else
				sin = .225 * (sin * sin - sin) + sin
			End If
		Else
			sin = 1.27323954 * radians - 0.405284735 * radians * radians
			If (sin < 0) Then
				sin = .225 * (sin * -sin - sin) + sin
			Else
				sin = .225 * (sin * sin - sin) + sin
			End If
		End If

		radians += HALFPI
		If (radians > PI) radians -= TWOPI

		If (radians < 0) Then
			cos = 1.27323954 * radians + 0.405284735 * radians * radians
			If (cos < 0) Then
				cos = .225 * (cos * -cos - cos) + cos
			Else
				cos = .225 * (cos * cos - cos) + cos
			End If
		Else
			cos = 1.27323954 * radians - 0.405284735 * radians * radians
			If (cos < 0) Then
				cos = .225 * (cos * -cos - cos) + cos
			Else
				cos = .225 * (cos * cos - cos) + cos
			End If
		End If

		Local dx:Float = x - pivotX
		Local dy:Float = pivotY + y

		If (point = Null) point = New FlxPoint()
		point.x = pivotX + cos * dx - sin * dy
		point.y = pivotY - sin * dx - cos * dy

		Return point
	End Function

	Function GetAngle:Float(point1:FlxPoint, point2:FlxPoint)
		Local x:Float = point2.x - point1.x
		Local y:Float = point2.y - point1.y

		If (x = 0 And y = 0) Return 0

		Local c1:Float = PI * .25
		Local c2:Float = 3 * c1
		Local ay:Float = MonkeyAbs(y)
		Local angle:Float = 0

		If (x >= 0) Then
			angle = c1 - c1 * ((x - ay) / (x + ay))
		Else
			angle = c2 - c1 * ((x + ay) / (ay - x))
		End If

		If (y < 0) Then
			angle =	-angle * 57.2957796
		Else
			angle =	angle * 57.2957796
		End If

		If (angle > 90) Then
			angle = angle - 270
		Else
			angle += 90
		End If

		Return angle
	End Function

	Function GetDistance:Float(point1:FlxPoint, point2:FlxPoint)
		Local dx:Float = point1.x - point2.x
		Local dy:Float = point1.y - point2.y

		Return Sqrt(dx * dx + dy * dy)
	End Function

	Function GetDistance:Float(x1:Float, y1:Float, x2:Float, y2:Float)
		Local dx:Float = x1 - x2
		Local dy:Float = y1 - y2

		Return Sqrt(dx * dx + dy * dy)
	End Function

End Class

Strict

Import mojo
'Import reflection

Import flxextern
Import flxpoint
Import flxg

Alias MonkeyAbs = monkey.math.Abs
Alias MonkeyFloor = monkey.math.Floor
Alias MonkeyCeil = monkey.math.Ceil
Alias MonkeyMin = monkey.math.Min
Alias MonkeyMax = monkey.math.Max
'Alias MonkeyGetClass = reflection.GetClass
Alias NativeOpenURL = flxextern.OpenURL

Class FlxU

	Function OpenURL:Void(url:String)
		NativeOpenURL(url)
	End Function

	Function Abs:Float(value:Float)
		Return MonkeyAbs(value)
	End Function

	Function Floor:Float(value:Float)
		Return MonkeyFloor(value)
	End Function

	Function Ceil:Float(value:Float)
		Return MonkeyCeil(value)
	End Function

	Function Round:Float(value:Float)
		If (value > 0) Then
			Return Int(value + .5)
		Else
			Return Int(value - .5)
		End If
	End Function

	Function Min:Float(number1:Float, number2:Float)
		Return MonkeyMin(number1, number2)
	End Function

	Function Max:Float(number1:Float, number2:Float)
		Return MonkeyMax(number1, number2)
	End Function

	Function Bound:Float(value:Float, min:Float, max:Float)
		Return Clamp(value, min, max)
	End Function

	Function Srand:Float(seed:Float)
		Return Float(seed Shr 8 & $ffffff)/$1000000
	End Function

	#Rem
	Function Shuffle:Object[](objects:Object[], howManyTimes:Int)
		Shuffle is not currently supported in Monkey. Use FlxArray.Shuffle method
	End Function
	#End

	#Rem
	Function GetRandom:FlxBasic(objects:FlxBasic[], startIndex:Int = 0, length:Int = 0)
		GetRandom is not currently supported in Monkey. Use FlxArray.GetRandom method
	End Function
	#End

	Function GetTicks:Int()
		Return Millisecs()
	End Function

	Function FormatTicks:String(startTicks:Int, endTicks:Int)
		Return ((endTicks - startTicks) / 1000) + "s"
	End Function

	Function MakeColor:Int(red:Int, green:Int, blue:Int, alpha:Float = 1)
		If (alpha <= 1) alpha *= 255
		Return (alpha & $FF) Shl 24 | (red & $FF) Shl 16 | (green & $FF) Shl 8 | (blue & $FF)
	End Function

	Function MakeColorFromHSB:Int(hue:Float, saturation:Float, brightness:Float, alpha:Float = 1)
		Local red:Float
		Local green:Float
		Local blue:Float

		If (saturation = 0)
			red = brightness
			green = brightness
			blue = brightness
		Else
			If (hue = 360) hue = 0

			Local slice:Int = hue / 60
			Local hf:Float = hue / 60 - slice
			Local aa:Float = brightness * (1 - saturation)
			Local bb:Float = brightness * (1 - saturation * hf)
			Local cc:Float = brightness * (1 - saturation * (1.0 - hf))

			Select (slice)
				Case 0
					red = brightness
					green = cc
					blue = aa

				Case 1
					red = bb
					green = brightness
					blue = aa

				Case 2
					red = aa
					green = brightness
					blue = cc

				Case 3
					red = aa
					green = bb
					blue = brightness

				Case 4
					red = cc
					green = aa
					blue = brightness

				Case 5
					red = brightness
					green = aa
					blue = bb

				Default
					red = 0
					green = 0
					blue = 0
			End Select
		End If

		If (alpha <= 1) alpha *= 255
		Return (alpha & $FF) Shl 24 | int(red * 255) Shl 16 | int(green * 255) Shl 8 | int(blue * 255)
	End Function

	Function GetRGBA:Float[](color:Int, resluts:Float[] = [])
		If (resluts.Length() <> 4) resluts = resluts.Resize(4)

		resluts[0] = (color Shr 16) & $FF
		resluts[1] = (color Shr 8) & $FF
		resluts[2] = color & $FF
		resluts[3] = Float((color Shr 24) & $FF) / 255
		Return resluts
	End Function

	Function GetHSB:Float[](color:Int, resluts:Float[] = [])
		If (resluts.Length() <> 4) resluts = resluts.Resize(4)

		Local red:Float = Float((color Shr 16) & $FF) / 255
		Local green:Float = Float((color Shr 8) & $FF) / 255
		Local blue:Float = Float(color & $FF) / 255

		Local m:Float = MonkeyMax(red, green)
		Local dmax:Float = MonkeyMax(m, blue)
		m = MonkeyMin(red, green)
		Local dmin:Float = MonkeyMin(m, blue)
		Local range:Float = dmax - dmin

		resluts[2] = dmax
		resluts[1] = 0
		resluts[0] = 0

		If (dmax <> 0) resluts[1] = range / dmax

		If (resluts[1] <> 0) Then
			If (red = dmax) Then
				resluts[0] = (green - blue) / range

			ElseIf (green = dmax) Then
				resluts[0] = 2 + (blue - red) / range

			ElseIf (blue = dmax) Then
				resluts[0] = 4 + (red - green) / range
			End If

			resluts[0] *= 60
			If (resluts[0] < 0) resluts[0] += 360
		End If

		resluts[3] = Float((color Shr 24) & $FF) / 255

		Return resluts
	End Function

	Function FromatTime:String(seconds:Float, showMS:Bool = False)
		Local timeString:StringStack = New StringStack()
		timeString.Push(Int(seconds / 60) + ":")

		Local timeStringHepler:Int = Int(seconds) Mod 60

		If (timeStringHepler < 10) Then
			timeString.Push("0")
		End If

		timeString.Push(timeStringHepler)

		If (showMS) Then
			timeString.Push(".")
			timeStringHepler = (seconds - Int(seconds)) * 100

			If (timeStringHepler < 10) Then
				timeString.Push("0")
			End If

			timeString.Push(timeStringHepler)
		End If

		Return timeString.Join("")
	End Function

	Function FromatMoney:String(amount:Float, showDecimal:Bool = True, englishStyle:Bool = True)
		Local helper:Int
		Local intAmount:Int = amount
		Local comma:String = ""
		Local result:String = ""
		Local zeroes:String = ""

		While (intAmount > 0)
			If (result.Length() > 0 And comma.Length() <= 0) Then
				If (englishStyle) Then
					comma = ","
				Else
					comma = "."
				End If
			End If

			helper = intAmount - Int(intAmount / 1000) * 1000
			intAmount /= 1000

			zeroes = ""

			If (intAmount > 0) Then
				If (helper < 100) Then
					zeroes = "0"
				End If

				If (helper < 10) Then
					zeroes = "00"
				End If
			End If

			result = zeroes + helper + comma + result
		Wend

		If (showDecimal) Then
			intAmount = Int(amount * 100) - (Int(amount) * 100)

			If (englishStyle) Then
				result += "." + intAmount
			Else
				result += "," + intAmount
			End If

			If (amount < 10) Then
				result += "0"
			End If
		End If

		Return result
	End Function

	#Rem
	Function GetClassName:String(obj:Object)
		Return MonkeyGetClass(obj).Name
	End Function

	Function CompareClassNames:Bool(object1:Object, object2:Object)
		Return (MonkeyGetClass(object1).Name = MonkeyGetClass(object2).Name)
	End Function

	Function GetClass:ClassInfo(name:String)
		Return MonkeyGetClass(name)
	End Function
	#End

	Function ComputeVelocity:Float(velocity:Float, acceleration:Float = 0, drag:Float = 0, max:Float = 10000)
		If (acceleration <> 0) Then
			velocity += acceleration * FlxG.Elapsed

		ElseIf (drag <> 0) Then
			drag *= FlxG.Elapsed

			If (velocity - drag > 0) Then
				velocity -= drag
			ElseIf (velocity + drag < 0) Then
				velocity += drag
			Else
				velocity = 0
			End If
		End If

		If (velocity <> 0 And max <> 10000) Then
			If (velocity > max) Then
				velocity = max
			ElseIf (velocity < -max) Then
				velocity = -max
			End If
		End If

		Return velocity
	End Function

	Function RotatePoint:FlxPoint(x:Float, y:Float, pivotX:Float, pivotY:Float, angle:Float, point:FlxPoint = Null)
		Local sin:Float = 0
		Local cos:Float = 0
		Local radians:Float = angle * -0.017453293

		While (radians < -PI)
			radians += TWOPI
		Wend

		While (radians > PI)
			radians = radians - TWOPI
		Wend

		If (radians < 0) Then
			sin =  1.27323954 * radians + .405284735 * radians * radians
			If (sin < 0) Then
				sin = .225 * (sin * -sin - sin) + sin
			Else
				sin = .225 * (sin * sin - sin) + sin
			End If
		Else
			sin = 1.27323954 * radians - 0.405284735 * radians * radians
			If (sin < 0) Then
				sin = .225 * (sin * -sin - sin) + sin
			Else
				sin = .225 * (sin * sin - sin) + sin
			End If
		End If

		radians += HALFPI
		If (radians > PI) radians -= TWOPI

		If (radians < 0) Then
			cos = 1.27323954 * radians + 0.405284735 * radians * radians
			If (cos < 0) Then
				cos = .225 * (cos * -cos - cos) + cos
			Else
				cos = .225 * (cos * cos - cos) + cos
			End If
		Else
			cos = 1.27323954 * radians - 0.405284735 * radians * radians
			If (cos < 0) Then
				cos = .225 * (cos * -cos - cos) + cos
			Else
				cos = .225 * (cos * cos - cos) + cos
			End If
		End If

		Local dx:Float = x - pivotX
		Local dy:Float = pivotY + y

		If (point = Null) point = New FlxPoint()
		point.x = pivotX + cos * dx - sin * dy
		point.y = pivotY - sin * dx - cos * dy

		Return point
	End Function

	Function GetAngle:Float(point1:FlxPoint, point2:FlxPoint)
		Local x:Float = point2.x - point1.x
		Local y:Float = point2.y - point1.y

		If (x = 0 And y = 0) Return 0

		Local c1:Float = PI * .25
		Local c2:Float = 3 * c1
		Local ay:Float = MonkeyAbs(y)
		Local angle:Float = 0

		If (x >= 0) Then
			angle = c1 - c1 * ((x - ay) / (x + ay))
		Else
			angle = c2 - c1 * ((x + ay) / (ay - x))
		End If

		If (y < 0) Then
			angle =	-angle * 57.2957796
		Else
			angle =	angle * 57.2957796
		End If

		If (angle > 90) Then
			angle = angle - 270
		Else
			angle += 90
		End If

		Return angle
	End Function

	Function GetDistance:Float(point1:FlxPoint, point2:FlxPoint)
		Local dx:Float = point1.x - point2.x
		Local dy:Float = point1.y - point2.y

		Return Sqrt(dx * dx + dy * dy)
	End Function

	Function GetDistance:Float(x1:Float, y1:Float, x2:Float, y2:Float)
		Local dx:Float = x1 - x2
		Local dy:Float = y1 - y2

		Return Sqrt(dx * dx + dy * dy)
	End Function

End Class