Add write properties for Name and Value to XMLElement.

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

' 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

' 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 Value:String() Property
		Return 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


renamed Countdown stopOnFinish to pauseOnFinish

Strict

Private

Import bono

Public

Class Countdown Implements Updateable
    Private

    Field duration:Float
    Field elapsed:Float
    Field paused := False
    Field finishCallbacks := New StringMap<List<CountdownFinishCallback>>
    Field tickCallbacks := New StringMap<List<CountdownTickCallback>>
    Field finishTriggered := False

    Public

    Field pauseOnFinish := False

    Method New(duration:Float=0)
        Set(duration)
    End

    Method Pause:Void()
        paused = True
    End

    Method Play:Void()
        paused = False
    End

    Method PauseToggle:Void()
        paused = (Not paused)
    End

    Method Stop:Void()
        paused = True
        finishTriggered = False
        elapsed = 0
    End

    Method Restart:Void()
        Stop()
        Play()
    End

    Method IsPaused:Bool()
        Return paused
    End

    Method Set:Void(duration:Float)
        Self.duration = duration
        Restart()
    End

    Method Get:Float()
        Return (duration - elapsed)
    End

    Method Remaning:Float()
        Return Max(0.0, Get())
    End

    Method IsFinished:Bool()
        Return (Remaning() = 0)
    End

    Method OnUpdate:Void(timer:DeltaTimer)
        If paused Then Return

        elapsed += timer.frameTime
        TriggerTickCallbacks()
        TriggerFinishCallbacks()

        If pauseOnFinish And IsFinished() Then Pause()
    End

    Method Add:Void(duration:Float)
        elapsed += duration
    End

    Method Sub:Void(duration:Float)
        elapsed -= duration
    End

    ' --- FINISH CALLBACK

    Method AddFinishCallback:Void(name:String, obj:CountdownFinishCallback)
        If Not finishCallbacks.Contains(name)
            finishCallbacks.Set(name, New List<CountdownFinishCallback>)
        End

        If Not finishCallbacks.Get(name).Contains(obj)
            finishCallbacks.Get(name).AddLast(obj)
        End
    End

    Method RemoveFinishCallback:Void(name:String, obj:CountdownFinishCallback)
        If Not finishCallbacks.Contains(name) Then Return
        finishCallbacks.Get(name).RemoveEach(obj)
    End

    Method ClearFinishCallbacks:Void()
        finishCallbacks.Clear()
    End

    Method ClearFinishCallbacks:Void(name:String)
        If Not finishCallbacks.Contains(name) Then Return
        finishCallbacks.Remove(name)
    End

    ' --- TICK CALLBACK

    Method AddTickCallback:Void(name:String, obj:CountdownTickCallback)
        If Not tickCallbacks.Contains(name)
            tickCallbacks.Set(name, New List<CountdownTickCallback>)
        End

        If Not tickCallbacks.Get(name).Contains(obj)
            tickCallbacks.Get(name).AddLast(obj)
        End
    End

    Method RemoveTickCallback:Void(name:String, obj:CountdownTickCallback)
        If Not tickCallbacks.Contains(name) Then Return
        tickCallbacks.Get(name).RemoveEach(obj)
    End

    Method ClearTickCallbacks:Void()
        tickCallbacks.Clear()
    End

    Method ClearTickCallbacks:Void(name:String)
        If Not tickCallbacks.Contains(name) Then Return
        tickCallbacks.Remove(name)
    End

    Private

    Method TriggerTickCallbacks:Void()
        For Local name := EachIn tickCallbacks.Keys()
            For Local obj := EachIn tickCallbacks.Get(name)
                obj.OnCountdownTicked(Self, name, duration, elapsed)
            End
        End
    End

    Method TriggerFinishCallbacks:Void()
        If finishTriggered Then Return
        If elapsed < duration Then Return

        finishTriggered = True
        For Local name := EachIn finishCallbacks.Keys()
            For Local obj := EachIn finishCallbacks.Get(name)
                obj.OnCountdownFinished(Self, name, duration)
            End
        End
    End
End

Strict

Private

Import bono

Public

Class Countdown Implements Updateable
    Private

    Field duration:Float
    Field elapsed:Float
    Field paused := False
    Field finishCallbacks := New StringMap<List<CountdownFinishCallback>>
    Field tickCallbacks := New StringMap<List<CountdownTickCallback>>
    Field finishTriggered := False

    Public

    Field stopOnFinish := False

    Method New(duration:Float=0)
        Set(duration)
    End

    Method Pause:Void()
        paused = True
    End

    Method Play:Void()
        paused = False
    End

    Method PauseToggle:Void()
        paused = (Not paused)
    End

    Method Stop:Void()
        paused = True
        finishTriggered = False
        elapsed = 0
    End

    Method Restart:Void()
        Stop()
        Play()
    End

    Method IsPaused:Bool()
        Return paused
    End

    Method Set:Void(duration:Float)
        Self.duration = duration
        Restart()
    End

    Method Get:Float()
        Return (duration - elapsed)
    End

    Method Remaning:Float()
        Return Max(0.0, Get())
    End

    Method IsFinished:Bool()
        Return (Remaning() = 0)
    End

    Method OnUpdate:Void(timer:DeltaTimer)
        If paused Then Return

        elapsed += timer.frameTime
        TriggerTickCallbacks()
        TriggerFinishCallbacks()

        If stopOnFinish And IsFinished() Then Stop()
    End

    Method Add:Void(duration:Float)
        elapsed += duration
    End

    Method Sub:Void(duration:Float)
        elapsed -= duration
    End

    ' --- FINISH CALLBACK

    Method AddFinishCallback:Void(name:String, obj:CountdownFinishCallback)
        If Not finishCallbacks.Contains(name)
            finishCallbacks.Set(name, New List<CountdownFinishCallback>)
        End

        If Not finishCallbacks.Get(name).Contains(obj)
            finishCallbacks.Get(name).AddLast(obj)
        End
    End

    Method RemoveFinishCallback:Void(name:String, obj:CountdownFinishCallback)
        If Not finishCallbacks.Contains(name) Then Return
        finishCallbacks.Get(name).RemoveEach(obj)
    End

    Method ClearFinishCallbacks:Void()
        finishCallbacks.Clear()
    End

    Method ClearFinishCallbacks:Void(name:String)
        If Not finishCallbacks.Contains(name) Then Return
        finishCallbacks.Remove(name)
    End

    ' --- TICK CALLBACK

    Method AddTickCallback:Void(name:String, obj:CountdownTickCallback)
        If Not tickCallbacks.Contains(name)
            tickCallbacks.Set(name, New List<CountdownTickCallback>)
        End

        If Not tickCallbacks.Get(name).Contains(obj)
            tickCallbacks.Get(name).AddLast(obj)
        End
    End

    Method RemoveTickCallback:Void(name:String, obj:CountdownTickCallback)
        If Not tickCallbacks.Contains(name) Then Return
        tickCallbacks.Get(name).RemoveEach(obj)
    End

    Method ClearTickCallbacks:Void()
        tickCallbacks.Clear()
    End

    Method ClearTickCallbacks:Void(name:String)
        If Not tickCallbacks.Contains(name) Then Return
        tickCallbacks.Remove(name)
    End

    Private

    Method TriggerTickCallbacks:Void()
        For Local name := EachIn tickCallbacks.Keys()
            For Local obj := EachIn tickCallbacks.Get(name)
                obj.OnCountdownTicked(Self, name, duration, elapsed)
            End
        End
    End

    Method TriggerFinishCallbacks:Void()
        If finishTriggered Then Return
        If elapsed < duration Then Return

        finishTriggered = True
        For Local name := EachIn finishCallbacks.Keys()
            For Local obj := EachIn finishCallbacks.Get(name)
                obj.OnCountdownFinished(Self, name, duration)
            End
        End
    End
End


Tween destroy is optionaly now

Tween destroy is optionaly now

#rem
	header:This module contains the FlxBasic class and FlxClass interface.
#end
Strict

Import reflection

Import flxextern
Import flxcamera
Import flxg
Import system.tweens.flxtween

Alias MonkeyGetClass = reflection.GetClass

#Rem
summary:This is a useful "generic" Flixel object.
Both [a flxobject.monkey.html]FlxObject[/a] and [a flxgroup.monkey.html]FlxGroup[/a] extend this class,
as do the plugins.  Has no size, position or graphical data.
#End
Class FlxBasic

	Global ClassObject:Object

	Global _ActiveCount:Int

	Global _VisibleCount:Int

	#Rem
	summary:IDs seem like they could be pretty useful, huh?
	They're not actually used for anything yet though.
	#End
	Field ID:Int

	#Rem
	summary:See details.
	Controls whether [a #Update]Update()[/a] and [a #Draw]Draw()[/a] are automatically called by [a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field exists:Bool

	#Rem
	summary:See details.
	Controls whether [a #Update]Update()[/a] is automatically called by [a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field active:Bool

	#Rem
	summary:See details.
	Controls whether [a #Draw]Draw()[/a] is automatically called by[a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field visible:Bool

	#Rem
	summary:Useful state for many game objects - "dead" (!alive) vs alive.
	[a #Kill]Kill()[/a] and [a #Recive]Revive()[/a] both flip this switch (along with exists, but you can override that).
	#End
	Field alive:Bool

	#Rem
	summary:Setting this to true will prevent the object from appearing when the visual debug mode in the debugger overlay is toggled on.
	#End
	Field ignoreDrawDebug:Bool

	Field autoClear:Bool

	Field _cameras:IntSet

Private
	Field _tweens:List<FlxTween>

	Field _classInfo:ClassInfo

	#Rem
	summary:Instantiate the basic flixel object.
	#End
	Method New()
		ID = -1
		exists = True
		active = True
		visible = True
		alive = True
		ignoreDrawDebug = False
		autoClear = True

		_classInfo = MonkeyGetClass(Object(Self))
	End Method

	#Rem
	summary:See details.
	Override this function to null out variables or manually call [a #Destroy]Destroy()[/a] on class members if necessary.
	Don't forget to call [b]super.Destroy()[/b]!
	#End
	Method Destroy:Void()
		If (autoClear And HasTween) Then
			ClearTweens(True)
		End If

		_classInfo = Null
	End Method

	#Rem
	summary:See details.
	Pre-update is called right before [a #Update]Update()[/a] on each object in the game loop.
	#End
	Method PreUpdate:Void()
		_ActiveCount+=1
	End Method

	#Rem
	summary:Override this function to update your class's position and appearance.
	This is where most of your game rules and behavioral code will go.
	#End
	Method Update:Void()
	End Method

	#Rem
	summary:See details.
	Post-update is called right after [a #Update]Update()[/a] on each object in the game loop.
	#End
	Method PostUpdate:Void()
	End Method

	#Rem
	summary:Override this function to control how the object is drawn.
	Overriding [a #Draw]Draw()[/a] is rarely necessary, but can be very useful.
	#End
	Method Draw:Void()
		_VisibleCount+=1
	End Method

	#Rem
	summary:Override this function to draw custom "debug mode" graphics to the specified camera while the debugger's visual mode is toggled on.
	Params:
	[list]
	[*][a flxcamera.monkey.html]camera:FlxCamera[/a] - which camera to draw the debug visuals to.
	[/list]
	#End
	Method DrawDebug:Void(camera:FlxCamera = Null)
	End Method

	#Rem
	summary: Handy function for "killing" game objects.
	Default behavior is to flag them as nonexistent AND dead.
	However, if you want the "corpse" to remain in the game,
	like to animate an effect or whatever, you should override this,
	setting only alive to false, and leaving exists true.
	#End
	Method Kill:Void()
		alive = False
		exists = False
	End Method

	#Rem
	summary:Handy function for bringing game objects "back to life". Just sets alive and exists back to true.
	In practice, this function is most often called by [a flxobject.monkey.html#Reset]FlxObject.Reset()[/a].
	#End
	Method Revive:Void()
		alive = True
		exists = True
	End Method

	Method Cameras:Void(cameras:Int[]) Property
		If (cameras.Length() = 0) Then
			_cameras = Null
			Return
		End If

		If (_cameras = Null) _cameras = New IntSet()

		Local l:Int = cameras.Length()
		Local i:Int = 0

		While (i < l)
			_cameras.Insert(cameras[i])
			i += 1
		Wend
	End Method

	Method AddTween:FlxTween(tween:FlxTween, start:Bool = False)
		If (tween._parent <> Null) Then
			FlxG.Log("WARNING: Cannot add a FlxTween object more than once")
			Return tween
		End If

		tween._parent = Self

		If (_tweens = Null) _tweens = New List<FlxTween>
		_tweens.AddLast(tween)

		If (start) tween.Start()

		Return tween
	End Method

	Method RemoveTween:FlxTween(tween:FlxTween, destroy:Bool = False)
		If (tween._parent <> Self) Then
			FlxG.Log("WARNING: Core object does not contain FlxTween")
			Return tween
		End If

		_tweens.RemoveEach(tween)

		If (destroy) tween.Destroy()
		tween.active = False

		Return tween
	End Method

	Method ClearTweens:Void(destroy:Bool = False)
		Local node:= _tweens.FirstNode()
		Local tween:FlxTween

		While (node <> Null)
			tween = node.Value()
			If (destroy) tween.Destroy()
			tween.active = False
			node = node.NextNode()
		Wend

		_tweens = Null
	End Method

	Method UpdateTweens:Void()
		Local node:= _tweens.FirstNode()
		Local tween:FlxTween

		While (node <> Null)
			tween = node.Value()

			If (tween.active) Then
				tween.Update()

				If (tween._finish) Then
					tween.Finish()
				End If
			End If

			node = node.NextNode()
		Wend
	End Method

	Method HasTween:Bool() Property
		Return(_tweens <> Null)
	End Method

	Method GetClass:ClassInfo()
		Return _classInfo
	End Method

	#Rem
	summary:Convert object to readable string name.  Useful for debugging, save games, etc.
	#End
	Method ToString:String()
		Return _classInfo.Name[_classInfo.Name.FindLast(".")+1..]
	End Method

End Class

#Rem
footer:Flixel is an open source game-making library that is completely free for personal or commercial use.
[quote]Copyright: Flixel - 2009-2011 Adam 'Atomic' Saltsman
Copyright: Monkey port - 2011 Arthur 'devolonter' Bikmullin

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.

NOTE FROM THE AUTHOR: As far as I know, you only have to include
this license if you are redistributing source code that includes
the Flixel library.  There is no need (or way, afaik) to include
it in your compiled flash games and apps![/quote]
#End

#rem
	header:This module contains the FlxBasic class and FlxClass interface.
#end
Strict

Import reflection

Import flxextern
Import flxcamera
Import flxg
Import system.tweens.flxtween

Alias MonkeyGetClass = reflection.GetClass

#Rem
summary:This is a useful "generic" Flixel object.
Both [a flxobject.monkey.html]FlxObject[/a] and [a flxgroup.monkey.html]FlxGroup[/a] extend this class,
as do the plugins.  Has no size, position or graphical data.
#End
Class FlxBasic

	Global ClassObject:Object

	Global _ActiveCount:Int

	Global _VisibleCount:Int

	#Rem
	summary:IDs seem like they could be pretty useful, huh?
	They're not actually used for anything yet though.
	#End
	Field ID:Int

	#Rem
	summary:See details.
	Controls whether [a #Update]Update()[/a] and [a #Draw]Draw()[/a] are automatically called by [a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field exists:Bool

	#Rem
	summary:See details.
	Controls whether [a #Update]Update()[/a] is automatically called by [a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field active:Bool

	#Rem
	summary:See details.
	Controls whether [a #Draw]Draw()[/a] is automatically called by[a flxstate.monkey.html]FlxState[/a]/[a flxgroup.monkey.html]FlxGroup[/a].
	#End
	Field visible:Bool

	#Rem
	summary:Useful state for many game objects - "dead" (!alive) vs alive.
	[a #Kill]Kill()[/a] and [a #Recive]Revive()[/a] both flip this switch (along with exists, but you can override that).
	#End
	Field alive:Bool

	#Rem
	summary:Setting this to true will prevent the object from appearing when the visual debug mode in the debugger overlay is toggled on.
	#End
	Field ignoreDrawDebug:Bool

	Field autoClear:Bool

	Field _cameras:IntSet

Private
	Field _tweens:List<FlxTween>

	Field _classInfo:ClassInfo

	#Rem
	summary:Instantiate the basic flixel object.
	#End
	Method New()
		ID = -1
		exists = True
		active = True
		visible = True
		alive = True
		ignoreDrawDebug = False
		autoClear = True

		_classInfo = MonkeyGetClass(Object(Self))
	End Method

	#Rem
	summary:See details.
	Override this function to null out variables or manually call [a #Destroy]Destroy()[/a] on class members if necessary.
	Don't forget to call [b]super.Destroy()[/b]!
	#End
	Method Destroy:Void()
		If (autoClear And HasTween) Then
			ClearTweens()
		End If

		_classInfo = Null
	End Method

	#Rem
	summary:See details.
	Pre-update is called right before [a #Update]Update()[/a] on each object in the game loop.
	#End
	Method PreUpdate:Void()
		_ActiveCount+=1
	End Method

	#Rem
	summary:Override this function to update your class's position and appearance.
	This is where most of your game rules and behavioral code will go.
	#End
	Method Update:Void()
	End Method

	#Rem
	summary:See details.
	Post-update is called right after [a #Update]Update()[/a] on each object in the game loop.
	#End
	Method PostUpdate:Void()
	End Method

	#Rem
	summary:Override this function to control how the object is drawn.
	Overriding [a #Draw]Draw()[/a] is rarely necessary, but can be very useful.
	#End
	Method Draw:Void()
		_VisibleCount+=1
	End Method

	#Rem
	summary:Override this function to draw custom "debug mode" graphics to the specified camera while the debugger's visual mode is toggled on.
	Params:
	[list]
	[*][a flxcamera.monkey.html]camera:FlxCamera[/a] - which camera to draw the debug visuals to.
	[/list]
	#End
	Method DrawDebug:Void(camera:FlxCamera = Null)
	End Method

	#Rem
	summary: Handy function for "killing" game objects.
	Default behavior is to flag them as nonexistent AND dead.
	However, if you want the "corpse" to remain in the game,
	like to animate an effect or whatever, you should override this,
	setting only alive to false, and leaving exists true.
	#End
	Method Kill:Void()
		alive = False
		exists = False
	End Method

	#Rem
	summary:Handy function for bringing game objects "back to life". Just sets alive and exists back to true.
	In practice, this function is most often called by [a flxobject.monkey.html#Reset]FlxObject.Reset()[/a].
	#End
	Method Revive:Void()
		alive = True
		exists = True
	End Method

	Method Cameras:Void(cameras:Int[]) Property
		If (cameras.Length() = 0) Then
			_cameras = Null
			Return
		End If

		If (_cameras = Null) _cameras = New IntSet()

		Local l:Int = cameras.Length()
		Local i:Int = 0

		While (i < l)
			_cameras.Insert(cameras[i])
			i += 1
		Wend
	End Method

	Method AddTween:FlxTween(tween:FlxTween, start:Bool = False)
		If (tween._parent <> Null) Then
			FlxG.Log("WARNING: Cannot add a FlxTween object more than once")
			Return tween
		End If

		tween._parent = Self

		If (_tweens = Null) _tweens = New List<FlxTween>
		_tweens.AddLast(tween)

		If (start) tween.Start()

		Return tween
	End Method

	Method RemoveTween:FlxTween(tween:FlxTween)
		If (tween._parent <> Self) Then
			FlxG.Log("WARNING: Core object does not contain FlxTween")
			Return tween
		End If

		_tweens.RemoveEach(tween)

		tween.Destroy()
		tween.active = False

		Return tween
	End Method

	Method ClearTweens:Void()
		Local node:= _tweens.FirstNode()
		Local tween:FlxTween

		While (node <> Null)
			tween = node.Value()
			tween.Destroy()
			tween.active = False
			node = node.NextNode()
		Wend

		_tweens = Null
	End Method

	Method UpdateTweens:Void()
		Local node:= _tweens.FirstNode()
		Local tween:FlxTween

		While (node <> Null)
			tween = node.Value()

			If (tween.active) Then
				tween.Update()

				If (tween._finish) Then
					tween.Finish()
				End If
			End If

			node = node.NextNode()
		Wend
	End Method

	Method HasTween:Bool() Property
		Return(_tweens <> Null)
	End Method

	Method GetClass:ClassInfo()
		Return _classInfo
	End Method

	#Rem
	summary:Convert object to readable string name.  Useful for debugging, save games, etc.
	#End
	Method ToString:String()
		Return _classInfo.Name[_classInfo.Name.FindLast(".")+1..]
	End Method

End Class

#Rem
footer:Flixel is an open source game-making library that is completely free for personal or commercial use.
[quote]Copyright: Flixel - 2009-2011 Adam 'Atomic' Saltsman
Copyright: Monkey port - 2011 Arthur 'devolonter' Bikmullin

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.

NOTE FROM THE AUTHOR: As far as I know, you only have to include
this license if you are redistributing source code that includes
the Flixel library.  There is no need (or way, afaik) to include
it in your compiled flash games and apps![/quote]
#End


Fixed a typo in 'StringStream'.

Fixed a typo in 'StringStream'.

Strict

Public

' Imports (Public):
Import brl.stream

' Imports (Private):
Private

Import publicdatastream

Public

' Classes:
Class StringStream Extends PublicDataStream
	' Constant variable(s):

	' ASCII:
	Const ASCII_CARRIAGE_RETURN:= 13
	Const ASCII_LINE_FEED:= 10

	Const ASCII_QUOTE:= 34

	' Global variable(s):

	' Defaults:
	Global Default_Size:= 1024 ' 1KB

	' Constructor(s):
	Method New(Message:String, Encoding:String="utf8", FixByteOrder:Bool=Default_BigEndianStorage, InitSize:Int=Default_Size, SizeLimit:Int=NOLIMIT)
		Super.New(InitSize, FixByteOrder, True, SizeLimit)

		WriteString(Message, Encoding)

		'Self.Encoding = Encoding
	End

	Method New(Size:Int=Default_Size, FixByteOrder:Bool=Default_BigEndianStorage, SizeLimit:Int=NOLIMIT)
		Super.New(Size, FixByteOrder, True, SizeLimit)
	End

	' Methods:

	#Rem
		Method ToString:String() ' Property
			Return Echo()
		End
	#End

	' This echoes the raw content of this stream.
	' To echo up to the current position, use 'EchoHere'.
	Method Echo:String(Encoding:String="utf8")
		Local P:= Position

		Seek(0)

		Local Output:= ReadString(Encoding)

		Seek(P)

		Return Output
	End

	' This echoes using the position specified.
	Method Echo:String(Position:Int, Encoding:String="utf8")
		Local P:= Self.Position

		Seek(0)

		Local Output:= ReadString(Position, Encoding)

		Seek(P)

		Return Output
	End

	' This echoes using the current position.
	Method EchoHere:String(Encoding:String="utf8")
		Return Echo(Self.Position, Encoding)
	End

	Method WriteChar:Void(Value:Int)
		WriteByte(Value)
		'WriteShort(Value)

		Return
	End

	Method WriteChars:Void(Chars:Int[], Offset:Int, Length:Int)
		' Not the most optimal, but it works:
		For Local I:= Offset Until Length
			WriteChar(Chars[I])
		Next

		Return
	End

	Method WriteChars:Void(Chars:Int[], Offset:Int=0)
		WriteChars(Chars, Offset, Chars.Length)

		Return
	End

	Method EndLine:Void()
		WriteChar(ASCII_CARRIAGE_RETURN)
		WriteChar(ASCII_LINE_FEED)

		Return
	End

	Method WriteQuote:Void()
		WriteChar(ASCII_QUOTE)

		Return
	End

	' Fields:
	'Field Encoding:String
End

Strict

Public

' Imports (Public):
Import brl.stream

' Imports (Private):
Private

Import publicdatastream

Public

' Classes:
Class StringStream Extends PublicDataStream
	' Constant variable(s):

	' ASCII:
	Const ASCII_CARRIAGE_RETURN:= 13
	Const ASCII_LINE_FEED:= 10

	Const ASCII_QUOTE:= 34

	' Global variable(s):

	' Defaults:
	Global Default_Size:= 1024 ' 1KB

	' Constructor(s):
	Method New(Message:String, Encoding:String="utf8", FixByteOrder:Bool=Default_BigEndianStorage, InitSize:Int=Default_Size, SizeLimit:Int=NOLIMIT)
		Super.New(InitSize, FixByteOrder, True, SizeLimit)

		WriteString(Message, Encoding)

		'Self.Encoding = Encoding
	End

	Method New(Size:Int=Default_Size, FixByteOrder:Bool=Default_BigEndianStorage, SizeLimit:Int=NOLIMIT)
		Super.New(Size, FixByteOrder, True, SizeLimit)
	End

	' Methods:

	#Rem
		Method ToString:String() ' Property
			Return Echo()
		End
	#End

	' This echoes the raw content of this stream.
	' To echo up to the current position, use 'EchoHere'.
	Method Echo:String(Encoding:String="utf8")
		Local P:= Position

		Seek(0)

		Local Output:= ReadString(Encoding)

		Seek(P)

		Return Output
	End

	' This echoes using the position specified.
	Method Echo:String(Position:Int, Encoding:String="utf8")
		Local P:= Self.Position

		Seek(0)

		Local Output:= ReadString(Position, Encoding)

		Seek(P)

		Return Output
	End

	' This echoes using the current position.
	Method EchoHere:String(Encoding:String="utf8")
		Return EchoHere(Self.Position, Encoding)
	End

	Method WriteChar:Void(Value:Int)
		WriteByte(Value)
		'WriteShort(Value)

		Return
	End

	Method WriteChars:Void(Chars:Int[], Offset:Int, Length:Int)
		' Not the most optimal, but it works:
		For Local I:= Offset Until Length
			WriteChar(Chars[I])
		Next

		Return
	End

	Method WriteChars:Void(Chars:Int[], Offset:Int=0)
		WriteChars(Chars, Offset, Chars.Length)

		Return
	End

	Method EndLine:Void()
		WriteChar(ASCII_CARRIAGE_RETURN)
		WriteChar(ASCII_LINE_FEED)

		Return
	End

	Method WriteQuote:Void()
		WriteChar(ASCII_QUOTE)

		Return
	End

	' Fields:
	'Field Encoding:String
End


Make DeltaTimer of Director public

Make DeltaTimer of Director public

Strict

Private

Import deltatimer
Import directorevents
Import inputcontroller
Import mojo
Import sizeable
Import util
Import vector2d

Public

Class Director Extends App Implements Sizeable
    Private

    Field _center:Vector2D
    Field _device:Vector2D = New Vector2D(0, 0)
    Field _inputController:InputController = New InputController()
    Field _scale:Vector2D
    Field _size:Vector2D
    Field _handler:DirectorEvents
    Field onCreateDispatched:Bool
    Field appOnCreateCatched:Bool

    Public

    Field deltaTimer:DeltaTimer

    Method New(width:Int, height:Int)
        Super.New()
        size = New Vector2D(width, height)
    End

    Method OnCreate:Int()
        _device = New Vector2D(DeviceWidth(), DeviceHeight())
        If Not size Then size = _device.Copy()
        RecalculateScale()

        inputController.scale = scale
        Seed = GetTimestamp()

        deltaTimer = New DeltaTimer(30)
        SetUpdateRate(60)

        appOnCreateCatched = True
        DispatchOnCreate()
        Return 0
    End

    Method OnLoading:Int()
        If _handler Then _handler.OnLoading()
        Return 0
    End

    Method OnUpdate:Int()
        deltaTimer.OnUpdate()
        If _handler
            _handler.OnUpdate(deltaTimer)
            inputController.OnUpdate(_handler)
        End
        Return 0
    End

    Method OnResume:Int()
        If _handler Then _handler.OnResume(0)
        Return 0
    End

    Method OnSuspend:Int()
        If _handler Then _handler.OnSuspend()
        Return 0
    End

    Method OnRender:Int()
        PushMatrix()
            Scale(_scale.x, _scale.y)
            SetScissor(0, 0, _device.x, _device.y)
            Cls(0, 0, 0)

            PushMatrix()
                If _handler Then _handler.OnRender()
            PopMatrix()
        PopMatrix()
        Return 0
    End

    Method Run:Void(_handler:DirectorEvents)
        Self._handler = _handler
        DispatchOnCreate()
    End

    Method center:Vector2D() Property
        Return _center
    End

    Method device:Vector2D() Property
        Return _device
    End

    Method scale:Vector2D() Property
        Return _scale
    End

    Method size:Vector2D() Property
        Return _size
    End

    Method size:Void(newSize:Vector2D) Property
        _size = newSize
        _center = _size.Copy().Div(2)
        RecalculateScale()
    End

    Method inputController:InputController() Property
        Return _inputController
    End

    Method handler:DirectorEvents() Property
        Return _handler
    End

    Private

    Method RecalculateScale:Void()
        _scale = _device.Copy().Div(_size)
    End

    Method DispatchOnCreate:Void()
        If onCreateDispatched Then Return
        If Not _handler Then Return
        If Not appOnCreateCatched Then Return

        _handler.OnCreate(Self)
        onCreateDispatched = True
    End
End

Strict

Private

Import deltatimer
Import directorevents
Import inputcontroller
Import mojo
Import sizeable
Import util
Import vector2d

Public

Class Director Extends App Implements Sizeable
    Private

    Field _center:Vector2D
    Field _device:Vector2D = New Vector2D(0, 0)
    Field _inputController:InputController = New InputController()
    Field _scale:Vector2D
    Field _size:Vector2D
    Field deltaTimer:DeltaTimer
    Field _handler:DirectorEvents
    Field onCreateDispatched:Bool
    Field appOnCreateCatched:Bool

    Public

    Method New(width:Int, height:Int)
        Super.New()
        size = New Vector2D(width, height)
    End

    Method OnCreate:Int()
        _device = New Vector2D(DeviceWidth(), DeviceHeight())
        If Not size Then size = _device.Copy()
        RecalculateScale()

        inputController.scale = scale
        Seed = GetTimestamp()

        deltaTimer = New DeltaTimer(30)
        SetUpdateRate(60)

        appOnCreateCatched = True
        DispatchOnCreate()
        Return 0
    End

    Method OnLoading:Int()
        If _handler Then _handler.OnLoading()
        Return 0
    End

    Method OnUpdate:Int()
        deltaTimer.OnUpdate()
        If _handler
            _handler.OnUpdate(deltaTimer)
            inputController.OnUpdate(_handler)
        End
        Return 0
    End

    Method OnResume:Int()
        If _handler Then _handler.OnResume(0)
        Return 0
    End

    Method OnSuspend:Int()
        If _handler Then _handler.OnSuspend()
        Return 0
    End

    Method OnRender:Int()
        PushMatrix()
            Scale(_scale.x, _scale.y)
            SetScissor(0, 0, _device.x, _device.y)
            Cls(0, 0, 0)

            PushMatrix()
                If _handler Then _handler.OnRender()
            PopMatrix()
        PopMatrix()
        Return 0
    End

    Method Run:Void(_handler:DirectorEvents)
        Self._handler = _handler
        DispatchOnCreate()
    End

    Method center:Vector2D() Property
        Return _center
    End

    Method device:Vector2D() Property
        Return _device
    End

    Method scale:Vector2D() Property
        Return _scale
    End

    Method size:Vector2D() Property
        Return _size
    End

    Method size:Void(newSize:Vector2D) Property
        _size = newSize
        _center = _size.Copy().Div(2)
        RecalculateScale()
    End

    Method inputController:InputController() Property
        Return _inputController
    End

    Method handler:DirectorEvents() Property
        Return _handler
    End

    Private

    Method RecalculateScale:Void()
        _scale = _device.Copy().Div(_size)
    End

    Method DispatchOnCreate:Void()
        If onCreateDispatched Then Return
        If Not _handler Then Return
        If Not appOnCreateCatched Then Return

        _handler.OnCreate(Self)
        onCreateDispatched = True
    End
End


Animation.IsPlaying() added

Animation.IsPlaying() added

Strict

Private

Import directorevents
Import fader
Import fanout
Import transition

Public

Class Animation Extends FanOut
    Private

    Field startValue:Float
    Field endValue:Float
    Field duration:Float
    Field animationTime:Float
    Field finished:Bool
    Field _value:Float

    Public

    Field transition:Transition = New TransitionLinear()
    Field effect:Fader

    Method New(startValue:Float, endValue:Float, duration:Float)
        Self.startValue = startValue
        Self.endValue = endValue
        Self.duration = duration
    End

    Method Pause:Void()
        finished = True
    End

    Method Play:Void()
        finished = False
    End

    Method Restart:Void()
        animationTime = 0
        Play()
    End

    Method IsPlaying:Bool()
        Return (Not finished)
    End

    Method OnUpdate:Void(delta:Float, frameTime:Float)
        Super.OnUpdate(delta, frameTime)

        If finished Then Return
        animationTime += frameTime

        Local progress:Float = Min(1.0, animationTime / duration)
        Local t:Float = transition.Calculate(progress)
        _value = (startValue * (1.0 - t)) + (endValue * t)

        If animationTime >= duration
            animationTime = duration
            finished = True
        End
    End

    Method OnRender:Void()
        If Not effect
            Super.OnRender()
            Return
        End

        If Count() = 0 Then Return
        effect.PreRender(_value)

        For Local obj:DirectorEvents = EachIn Self
            effect.PreNode(_value, obj)
            obj.OnRender()
            effect.PostNode(_value, obj)
        End

        effect.PostRender(_value)
    End

    Method value:Float() Property
        Return _value
    End
End

Strict

Private

Import directorevents
Import fader
Import fanout
Import transition

Public

Class Animation Extends FanOut
    Private

    Field startValue:Float
    Field endValue:Float
    Field duration:Float
    Field animationTime:Float
    Field finished:Bool
    Field _value:Float

    Public

    Field transition:Transition = New TransitionLinear()
    Field effect:Fader

    Method New(startValue:Float, endValue:Float, duration:Float)
        Self.startValue = startValue
        Self.endValue = endValue
        Self.duration = duration
    End

    Method Pause:Void()
        finished = True
    End

    Method Play:Void()
        finished = False
    End

    Method Restart:Void()
        animationTime = 0
        Play()
    End

    Method OnUpdate:Void(delta:Float, frameTime:Float)
        Super.OnUpdate(delta, frameTime)

        If finished Then Return
        animationTime += frameTime

        Local progress:Float = Min(1.0, animationTime / duration)
        Local t:Float = transition.Calculate(progress)
        _value = (startValue * (1.0 - t)) + (endValue * t)

        If animationTime >= duration
            animationTime = duration
            finished = True
        End
    End

    Method OnRender:Void()
        If Not effect
            Super.OnRender()
            Return
        End

        If Count() = 0 Then Return
        effect.PreRender(_value)

        For Local obj:DirectorEvents = EachIn Self
            effect.PreNode(_value, obj)
            obj.OnRender()
            effect.PostNode(_value, obj)
        End

        effect.PostRender(_value)
    End

    Method value:Float() Property
        Return _value
    End
End