Fixed string comparison.

Fixed string comparison.

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

Strict

Import assert

#Rem
Monkey Collections Framework
Based loosely on the Java Collections Framework
#End

'''' Top level Collection stuff ''''

#Rem
	AbstractCollection
	Due to limitations with generics in Monkey, ToArray cannot work with an array of type E.  We must return Object[] instead.
#End
Class AbstractCollection<E> Abstract
Private
	' A custom comparator to use for sorting and comparing items.
	Field comparator:AbstractComparator = Null

Public
	' Property to read comparator
	Method Comparator:AbstractComparator() Property
		Return comparator
	End
	' Property to write comparator
	Method Comparator:Void(comparator:AbstractComparator) Property
		Self.comparator = comparator
	End

	Method Add:Bool(o:E) Abstract
	Method AddAll:Bool(c:AbstractCollection<E>) Abstract
	Method Clear:Void() Abstract
	Method Contains:Bool(o:E) Abstract
	Method ContainsAll:Bool(c:AbstractCollection<E>) Abstract
	Method Enumerator:AbstractEnumerator<E>() Abstract
	Method FillArray:Int(arr:Object[]) Abstract ' populates the passed array and returns the number of items filled. best used for android
	Method IsEmpty:Bool() Abstract
	Method Remove:Bool(o:E) Abstract
	Method RemoveAll:Bool(c:AbstractCollection<E>) Abstract
	Method RetainAll:Bool(c:AbstractCollection<E>) Abstract
	Method Size:Int() Property Abstract
	Method Sort:Void(reverse:Bool = False, comp:AbstractComparator = Null) Abstract
	Method ToArray:Object[]() Abstract ' creates a new array of the correct size and returns it

	Method ObjectEnumerator:AbstractEnumerator<E>()
		Return Enumerator()
	End
End

#Rem
	AbstractEnumerator
	Used in the ObjectEnumerator method for calls to EachIn.
	If retrieved and used manually, the HasPrevious/PreviousObject/Remove/First/Last methods can be called.
#End
Class AbstractEnumerator<E>
Public
	Method HasNext:Bool() Abstract
	Method HasPrevious:Bool() Abstract
	Method NextObject:E() Abstract
	Method PreviousObject:E() Abstract
	Method Remove:Void() Abstract
	Method First:Void() Abstract
	Method Last:Void() Abstract
End

#Rem
	AbstractComparator
	This is a way For developers to provide a custom comparison method for sorting lists.
	It's sort of like a function pointer.
#End
Class AbstractComparator Abstract
	Method Compare:Int(o1:Object, o2:Object) Abstract

	Method CompareBool:Bool(o1:Object, o2:Object)
		Return o1 = o2 Or Compare(o1, o2) = 0
	End

	Method HashCode:Int(o:Object)
		Return 0
	End
End

#Rem
	DefaultComparator
	Implements an AbstractComparator to handle primitive wrappers and strings.
#End
Global DEFAULT_COMPARATOR:DefaultComparator = New DefaultComparator
Class DefaultComparator Extends AbstractComparator
	Method Compare:Int(o1:Object, o2:Object)
		If IntObject(o1) <> Null And IntObject(o2) <> Null Then
			If IntObject(o1).value < IntObject(o2).value Then Return -1
			If IntObject(o1).value > IntObject(o2).value Then Return 1
			Return 0
		ElseIf FloatObject(o1) <> Null And FloatObject(o2) <> Null Then
			If FloatObject(o1).value < FloatObject(o2).value Then Return -1
			If FloatObject(o1).value > FloatObject(o2).value Then Return 1
			Return 0
		ElseIf StringObject(o1) <> Null And StringObject(o2) <> Null Then
			If StringObject(o1).value < StringObject(o2).value Then Return -1
			If StringObject(o1).value > StringObject(o2).value Then Return 1
			Return 0
		End
		If o1 = o2 Then Return 0
		If o1 = Null Then Return -1
		If o2 = Null Then Return 1
		Return 0 ' don't know what to do!
	End

	Method CompareBool:Bool(o1:Object, o2:Object)
		If IntObject(o1) <> Null And IntObject(o2) <> Null Then
			Return IntObject(o1).value = IntObject(o2).value
		ElseIf FloatObject(o1) <> Null And FloatObject(o2) <> Null Then
			Return FloatObject(o1).value = FloatObject(o2).value
		ElseIf StringObject(o1) <> Null And StringObject(o2) <> Null Then
			Return StringObject(o1).value = StringObject(o2).value
		End
		Return o1 = o2
	End
End

'''' List stuff ''''

#Rem
	AbstractList
	Extends AbstractCollection to implement some of the abstract methods that apply to any kind of list.
#End
Class AbstractList<E> Extends AbstractCollection<E> Abstract
Private
	' If true, bounds checking should be performed.
#if CONFIG="debug"
	Field rangeChecking:Bool = True
#else
	Field rangeChecking:Bool = False
#end

	' A counter for modifications to the list.  Used for concurrency checks.
	Field modCount:Int = 0

	' Performs a range check.
	Method RangeCheck:Void(index:Int)
		Local size:Int = Self.Size()
		' range check doesn't use assert, for speed
		If index < 0 Or index >= size Then Error("AbstractList.RangeCheck: Index out of bounds: " + index + " is not 0<=index<" + size)
	End

Public
	' Property to read rangeChecking
	Method RangeChecking:Bool() Property
		Return rangeChecking
	End
	' Property to write rangeChecking
	Method RangeChecking:Void(rangeChecking:Bool) Property
		Self.rangeChecking = rangeChecking
	End

	Method Enumerator:AbstractEnumerator<E>()
		Return New ListEnumerator<E>(Self)
	End

	Method Get:E(index:Int) Abstract
	Method Insert:Void(index:Int, o:E) Abstract
	Method InsertAll:Bool(index:Int, c:AbstractCollection<E>) Abstract
	Method IndexOf:Int(o:E) Abstract
	Method LastIndexOf:Int(o:E) Abstract
	Method RemoveAt:E(index:Int) Abstract ' Can't overload Remove since it's an inherited method, and Monkey doesn't support that.
	Method RemoveRange:Void(fromIndex:Int, toIndex:Int) Abstract
	Method Set:E(index:Int, o:E) Abstract
End

#Rem
	ListEnumerator
	Extends AbstractEnumerator to provide support for EachIn.  Blocks concurrent modification, but allows elements to be removed on the fly.
#End
Class ListEnumerator<E> Extends AbstractEnumerator<E>
Private
	Field lst:AbstractList<E>
	Field lastIndex:Int = 0
	Field index:Int = 0
	Field expectedModCount:Int = 0
Public
	Method New(lst:AbstractList<E>)
		Self.lst = lst
		expectedModCount = lst.modCount
	End

	Method CheckConcurrency:Void()
		' for speed we don't use assert
		If lst.modCount <> expectedModCount Then Error("ListEnumerator.CheckConcurrency: Concurrent list modification")
	End

	Method HasNext:Bool()
		CheckConcurrency()
		Return index < lst.Size
	End

	Method HasPrevious:Bool()
		CheckConcurrency()
		Return index > 0
	End

	Method NextObject:E()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex)
	End

	Method PreviousObject:E()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex)
	End

	Method Remove:Void()
		CheckConcurrency()
		lst.RemoveAt(lastIndex)
		If lastIndex < index Then index -= 1
		lastIndex = -1
		expectedModCount = lst.modCount
	End

	Method First:Void()
		CheckConcurrency()
		index = 0
	End

	Method Last:Void()
		CheckConcurrency()
		index = lst.Size
	End
End

#Rem
	ArrayListEnumerator
	Extends ListEnumerator to avoid some method calls.
#End
Class ArrayListEnumerator<E> Extends ListEnumerator<E>
	Field alst:ArrayList<E>

	Method New(lst:ArrayList<E>)
		Super.New(lst)
		Self.alst = lst
		expectedModCount = alst.modCount
	End

	Method HasNext:Bool()
		CheckConcurrency()
		Return index < alst.size
	End

	Method NextObject:E()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return alst.elements[lastIndex]
	End

	Method PreviousObject:E()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return alst.elements[lastIndex]
	End

	Method Last:Void()
		CheckConcurrency()
		index = alst.size
	End
End

Class IntListEnumerator Extends ListEnumerator<IntObject>
Public
	Method New(lst:AbstractList<IntObject>)
		Super.New(lst)
	End

	Method NextInt:Int()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousInt:Int()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

Class FloatListEnumerator Extends ListEnumerator<FloatObject>
Public
	Method New(lst:AbstractList<FloatObject>)
		Super.New(lst)
	End

	Method NextFloat:Float()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousFloat:Float()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

Class StringListEnumerator Extends ListEnumerator<StringObject>
Public
	Method New(lst:AbstractList<IntObject>)
		Super.New(lst)
	End

	Method NextString:String()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousString:String()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

#Rem
	ArrayList
	Concrete implementation of AbstractList that uses a dynamically sized array to store elements.
	Has best performance when it is initialised with a capacity large enough to hold the expected number of elements.
#End
Class ArrayList<E> Extends AbstractList<E>
Private
	' fields
	Field elements:Object[]
	Field size:Int = 0

	' resizes the elements array if necessary to ensure it can fit minCapacity elements
	Method EnsureCapacity:Void(minCapacity:Int)
		Local oldCapacity:Int = elements.Length
		If minCapacity > oldCapacity Then
			Local newCapacity:Int = (oldCapacity * 3) / 2 + 1
			If newCapacity < minCapacity Then newCapacity = minCapacity
			elements = elements.Resize(newCapacity)
			modCount += 1
		End
	End

	Method RangeCheck:Void(index:Int)
		' range check doesn't use assert, for speed
		If index < 0 Or index >= size Then Error("ArrayList.RangeCheck: Index out of bounds: " + index + " is not 0<=index<" + size)
	End

	Field tempArr:Object[] = New Object[128] ' temp array used for internal call to ToArray (so we don't create an object)

Public
	' constructors
	Method New()
		Self.elements = New Object[10]
	End

	Method New(initialCapacity:Int)
		AssertGreaterThanOrEqual(initialCapacity, 0, "ArrayList.New: Illegal Capacity:")
		Self.elements = New Object[initialCapacity]
	End

	Method New(c:AbstractCollection<E>)
		elements = c.ToArray()
		size = elements.Length
	End

	'implements AbstractCollection

	Method Add:Bool(o:E)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = o
		size+=1
		modCount += 1
		Return True
	End

	Method AddAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return False
		Local newItemCount:Int = c.Size
		If size + newItemCount > elements.Length Then EnsureCapacity(size+newItemCount)
		If tempArr.Length < newItemCount Then tempArr = tempArr.Resize(newItemCount)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			elements[size] = tempArr[i]
			size+=1
		End
		modCount += 1
		Return newItemCount <> 0
	End

	Method Clear:Void()
		For Local i:Int = 0 Until size
			elements[i] = Null
		Next
		modCount += 1
		size = 0
	End

	Method Contains:Bool(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then Return True
		Next
		Return False
	End

	Method ContainsAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return True
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Not Self.Contains(E(tempArr[i])) Then Return False
		Next
		Return True
	End

	Method FillArray:Int(arr:Object[])
		AssertGreaterThanOrEqual(arr.Length, size, "ArrayList.FillArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = elements[i]
		Next
		Return size
	End

	Method IsEmpty:Bool()
		Return size = 0
	End

	Method Remove:Bool(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method RemoveAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return False
		Local modified:Bool = False
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Self.Contains(E(tempArr[i])) Then
				Self.Remove(E(tempArr[i]))
			End
		Next
		If modified Then modCount += 1
		Return modified
	End

	Method RetainAll:Bool(c:AbstractCollection<E>)
		Local modified:Bool = False
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Not Self.Contains(E(tempArr[i])) Then
				Self.Remove(E(tempArr[i]))
				modified = True
			End
		Next
		If modified Then modCount += 1
		Return modified
	End

	Method Size:Int() Property
		Return size
	End

	Method Sort:Void(reverse:Bool = False, comp:AbstractComparator = Null)
		If size <= 1 Then Return ' can't sort 0 or 1 elements
		If comp = Null Then comp = Self.Comparator
		If comp = Null Then comp = DEFAULT_COMPARATOR
		QuickSort(elements, 0, size-1, comp, reverse)
		modCount += 1
	End

	Method ToArray:Object[]()
		Local arr:Object[] = New Object[size]
		For Local i:Int = 0 Until size
			arr[i] = elements[i]
		Next
		Return arr
	End

	' implements AbstractList

	Method Get:E(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return E(elements[index])
	End

	Method Insert:Void(index:Int, o:E)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = o
		size+=1
		modCount += 1
	End

	Method InsertAll:Bool(index:Int, c:AbstractCollection<E>)
		Local newItemCount:Int = c.Size
		If newItemCount = 0 Then Return False
		If size + newItemCount > elements.Length Then EnsureCapacity(size+newItemCount)
		If tempArr.Length < newItemCount Then tempArr = tempArr.Resize(newItemCount)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = size - 1 To index Step -1
			elements[i+newItemCount] = elements[i]
		Next
		For Local i:Int = 0 Until newItemCount
			elements[index+i] = tempArr[i]
		End
		size += newItemCount
		modCount += 1
		Return newItemCount <> 0
	End

	Method IndexOf:Int(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOf:Int(o:E)
		For Local i:Int = size-1 To 0 Step -1
			If elements[i] = o Then Return i
		Next
		Return -1
	End

	Method RemoveAt:E(index:Int)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:E = E(elements[index])
		For Local i:Int = index Until size-1
			elements[i] = elements[i+1]
		Next
		elements[size-1] = Null
		size-=1
		modCount += 1
		Return oldValue
	End

	Method RemoveRange:Void(fromIndex:Int, toIndex:Int)
		AssertLessThanOrEqual(fromIndex, toIndex, "ArrayList.RemoveRange: fromIndex > toIndex:")
		If rangeChecking Then
			RangeCheck(fromIndex)
			RangeCheck(toIndex)
		End
		For Local i:Int = 0 Until toIndex - fromIndex
			RemoveAt(fromIndex)
		Next
	End

	Method Set:E(index:Int, o:E)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:E = E(elements[index])
		elements[index] = o
		modCount += 1
		Return oldValue
	End

	Method Enumerator:AbstractEnumerator<E>()
		Return New ArrayListEnumerator<E>(Self)
	End
End

' ArrayList wrapper classes

Class IntArrayList Extends ArrayList<IntObject>
Public
	Method AddInt:Bool(o:Int)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = New IntObject(o)
		size+=1
		modCount += 1
		Return True
	End

	Method ContainsInt:Bool(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<IntObject>()
		Return New IntArrayListEnumerator(Self)
	End

	Method RemoveInt:Bool(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToIntArray:Int[]()
		Local arr:Int[] = New Int[size]
		For Local i:Int = 0 Until size
			arr[i] = IntObject(elements[i]).value
		Next
		Return arr
	End

	Method GetInt:Int(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return IntObject(elements[index]).value
	End

	Method InsertInt:Void(index:Int, o:Int)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New IntObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfInt:Int(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfInt:Int(o:E)
		For Local i:Int = size-1 To 0 Step -1
			If IntObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetInt:Int(index:Int, o:Int)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:Int = IntObject(elements[index]).value
		If elements[index] <> Null Then
			IntObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillIntArray:Int(arr:Int[])
		AssertLessThan(arr.Length, size, "IntArrayList.FillIntArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = IntObject(elements[i]).value
		Next
		Return size
	End
End

Class FloatArrayList Extends ArrayList<FloatObject>
Public
	Method AddFloat:Bool(o:Float)
		If size + 1 > elements.Length Then EnsureCapacity(size + 1)
		elements[size] = New FloatObject(o)
		size += 1
		modCount += 1
		Return True
	End

	Method ContainsFloat:Bool(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<FloatObject>()
		Return New FloatArrayListEnumerator(Self)
	End

	Method RemoveFloat:Bool(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToFloatArray:Float[]()
		Local arr:Float[] = New Float[size]
		For Local i:Int = 0 Until size
			arr[i] = FloatObject(elements[i]).value
		Next
		Return arr
	End

	Method GetFloat:Float(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return FloatObject(elements[index]).value
	End

	Method InsertFloat:Void(index:Int, o:Float)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New FloatObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfFloat:Int(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfFloat:Int(o:Float)
		For Local i:Int = size-1 To 0 Step -1
			If FloatObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetFloat:Float(index:Int, o:Float)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:Float = FloatObject(elements[index]).value
		If elements[index] <> Null Then
			FloatObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillFloatArray:Int(arr:Float[])
		AssertLessThan(arr.Length, size, "FloatArrayList.FillFloatArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = FloatObject(elements[i]).value
		Next
		Return size
	End
End

Class StringArrayList Extends ArrayList<StringObject>
Public
	Method AddString:Bool(o:String)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = New StringObject(o)
		size+=1
				modCount += 1
				Return True
			End

	Method ContainsString:Bool(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<StringObject>()
		Return New StringArrayListEnumerator(Self)
	End

	Method RemoveString:Bool(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToStringArray:String[]()
		Local arr:String[] = New String[size]
		For Local i:Int = 0 Until size
			arr[i] = StringObject(elements[i]).value
		Next
		Return arr
	End

	Method GetString:String(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return StringObject(elements[index]).value
	End

	Method InsertString:Void(index:Int, o:String)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New StringObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfString:Int(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfString:Int(o:String)
		For Local i:Int = size-1 To 0 Step -1
			If StringObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetString:String(index:Int, o:String)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:String = StringObject(elements[index]).value
		If elements[index] <> Null Then
			StringObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillStringArray:Int(arr:String[])
		AssertLessThan(arr.Length, size, "StringArrayList.FillStringArray: Array too small:")
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = value Then
				Remove(elements[i])
				modCount += 1
				Return True
			End
		Next
		Return False
	End
End

Function QuickSort:Void(arr:Object[], left:Int, right:Int, comp:AbstractComparator, reverse:Bool = False)
	If right > left Then
		Local pivotIndex:Int = left + (right-left)/2
		Local pivotNewIndex:Int = QuickSortPartition(arr, left, right, pivotIndex, comp, reverse)
		QuickSort(arr, left, pivotNewIndex - 1, comp, reverse)
		QuickSort(arr, pivotNewIndex + 1, right, comp, reverse)
	End
End

Function QuickSortPartition:Int(arr:Object[], left:Int, right:Int, pivotIndex:Int, comp:AbstractComparator, reverse:Bool = False)
	Local pivotValue:Object = arr[pivotIndex]
	arr[pivotIndex] = arr[right]
	arr[right] = pivotValue
	Local storeIndex:Int = left, val:Object
	For Local i:Int = left Until right
		If Not reverse And comp.Compare(arr[i], pivotValue) <= 0 Or reverse And comp.Compare(arr[i], pivotValue) >= 0 Then
			val = arr[i]
			arr[i] = arr[storeIndex]
			arr[storeIndex] = val
			storeIndex += 1
		End
	Next
	val = arr[storeIndex]
	arr[storeIndex] = arr[right]
	arr[right] = val
	Return storeIndex
End

Strict

Import assert

#Rem
Monkey Collections Framework
Based loosely on the Java Collections Framework
#End

'''' Top level Collection stuff ''''

#Rem
	AbstractCollection
	Due to limitations with generics in Monkey, ToArray cannot work with an array of type E.  We must return Object[] instead.
#End
Class AbstractCollection<E> Abstract
Private
	' A custom comparator to use for sorting and comparing items.
	Field comparator:AbstractComparator = Null

Public
	' Property to read comparator
	Method Comparator:AbstractComparator() Property
		Return comparator
	End
	' Property to write comparator
	Method Comparator:Void(comparator:AbstractComparator) Property
		Self.comparator = comparator
	End

	Method Add:Bool(o:E) Abstract
	Method AddAll:Bool(c:AbstractCollection<E>) Abstract
	Method Clear:Void() Abstract
	Method Contains:Bool(o:E) Abstract
	Method ContainsAll:Bool(c:AbstractCollection<E>) Abstract
	Method Enumerator:AbstractEnumerator<E>() Abstract
	Method FillArray:Int(arr:Object[]) Abstract ' populates the passed array and returns the number of items filled. best used for android
	Method IsEmpty:Bool() Abstract
	Method Remove:Bool(o:E) Abstract
	Method RemoveAll:Bool(c:AbstractCollection<E>) Abstract
	Method RetainAll:Bool(c:AbstractCollection<E>) Abstract
	Method Size:Int() Property Abstract
	Method Sort:Void(reverse:Bool = False, comp:AbstractComparator = Null) Abstract
	Method ToArray:Object[]() Abstract ' creates a new array of the correct size and returns it

	Method ObjectEnumerator:AbstractEnumerator<E>()
		Return Enumerator()
	End
End

#Rem
	AbstractEnumerator
	Used in the ObjectEnumerator method for calls to EachIn.
	If retrieved and used manually, the HasPrevious/PreviousObject/Remove/First/Last methods can be called.
#End
Class AbstractEnumerator<E>
Public
	Method HasNext:Bool() Abstract
	Method HasPrevious:Bool() Abstract
	Method NextObject:E() Abstract
	Method PreviousObject:E() Abstract
	Method Remove:Void() Abstract
	Method First:Void() Abstract
	Method Last:Void() Abstract
End

#Rem
	AbstractComparator
	This is a way For developers to provide a custom comparison method for sorting lists.
	It's sort of like a function pointer.
#End
Class AbstractComparator Abstract
	Method Compare:Int(o1:Object, o2:Object) Abstract

	Method CompareBool:Bool(o1:Object, o2:Object)
		Return o1 = o2 Or Compare(o1, o2) = 0
	End

	Method HashCode:Int(o:Object)
		Return 0
	End
End

#Rem
	DefaultComparator
	Implements an AbstractComparator to handle primitive wrappers and strings.
#End
Global DEFAULT_COMPARATOR:DefaultComparator = New DefaultComparator
Class DefaultComparator Extends AbstractComparator
	Method Compare:Int(o1:Object, o2:Object)
		If IntObject(o1) <> Null And IntObject(o2) <> Null Then
			If IntObject(o1).value < IntObject(o2).value Then Return -1
			If IntObject(o1).value > IntObject(o2).value Then Return 1
			Return 0
		ElseIf FloatObject(o1) <> Null And FloatObject(o2) <> Null Then
			If FloatObject(o1).value < FloatObject(o2).value Then Return -1
			If FloatObject(o1).value > FloatObject(o2).value Then Return 1
			Return 0
		ElseIf StringObject(o1) <> Null And StringObject(o2) <> Null Then
			' TODO: string comparison
			'If StringObject(o1).value < StringObject(o2).value Then Return -1
			'If StringObject(o1).value > StringObject(o2).value Then Return 1
			Return 0
		End
		If o1 = o2 Then Return 0
		If o1 = Null Then Return -1
		If o2 = Null Then Return 1
		Return 0 ' don't know what to do!
	End

	Method CompareBool:Bool(o1:Object, o2:Object)
		If IntObject(o1) <> Null And IntObject(o2) <> Null Then
			Return IntObject(o1).value = IntObject(o2).value
		ElseIf FloatObject(o1) <> Null And FloatObject(o2) <> Null Then
			Return FloatObject(o1).value = FloatObject(o2).value
		ElseIf StringObject(o1) <> Null And StringObject(o2) <> Null Then
			Return StringObject(o1).value = StringObject(o2).value
		End
		Return o1 = o2
	End
End

'''' List stuff ''''

#Rem
	AbstractList
	Extends AbstractCollection to implement some of the abstract methods that apply to any kind of list.
#End
Class AbstractList<E> Extends AbstractCollection<E> Abstract
Private
	' If true, bounds checking should be performed.
#if CONFIG="debug"
	Field rangeChecking:Bool = True
#else
	Field rangeChecking:Bool = False
#end

	' A counter for modifications to the list.  Used for concurrency checks.
	Field modCount:Int = 0

	' Performs a range check.
	Method RangeCheck:Void(index:Int)
		Local size:Int = Self.Size()
		' range check doesn't use assert, for speed
		If index < 0 Or index >= size Then Error("AbstractList.RangeCheck: Index out of bounds: " + index + " is not 0<=index<" + size)
	End

Public
	' Property to read rangeChecking
	Method RangeChecking:Bool() Property
		Return rangeChecking
	End
	' Property to write rangeChecking
	Method RangeChecking:Void(rangeChecking:Bool) Property
		Self.rangeChecking = rangeChecking
	End

	Method Enumerator:AbstractEnumerator<E>()
		Return New ListEnumerator<E>(Self)
	End

	Method Get:E(index:Int) Abstract
	Method Insert:Void(index:Int, o:E) Abstract
	Method InsertAll:Bool(index:Int, c:AbstractCollection<E>) Abstract
	Method IndexOf:Int(o:E) Abstract
	Method LastIndexOf:Int(o:E) Abstract
	Method RemoveAt:E(index:Int) Abstract ' Can't overload Remove since it's an inherited method, and Monkey doesn't support that.
	Method RemoveRange:Void(fromIndex:Int, toIndex:Int) Abstract
	Method Set:E(index:Int, o:E) Abstract
End

#Rem
	ListEnumerator
	Extends AbstractEnumerator to provide support for EachIn.  Blocks concurrent modification, but allows elements to be removed on the fly.
#End
Class ListEnumerator<E> Extends AbstractEnumerator<E>
Private
	Field lst:AbstractList<E>
	Field lastIndex:Int = 0
	Field index:Int = 0
	Field expectedModCount:Int = 0
Public
	Method New(lst:AbstractList<E>)
		Self.lst = lst
		expectedModCount = lst.modCount
	End

	Method CheckConcurrency:Void()
		' for speed we don't use assert
		If lst.modCount <> expectedModCount Then Error("ListEnumerator.CheckConcurrency: Concurrent list modification")
	End

	Method HasNext:Bool()
		CheckConcurrency()
		Return index < lst.Size
	End

	Method HasPrevious:Bool()
		CheckConcurrency()
		Return index > 0
	End

	Method NextObject:E()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex)
	End

	Method PreviousObject:E()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex)
	End

	Method Remove:Void()
		CheckConcurrency()
		lst.RemoveAt(lastIndex)
		If lastIndex < index Then index -= 1
		lastIndex = -1
		expectedModCount = lst.modCount
	End

	Method First:Void()
		CheckConcurrency()
		index = 0
	End

	Method Last:Void()
		CheckConcurrency()
		index = lst.Size
	End
End

#Rem
	ArrayListEnumerator
	Extends ListEnumerator to avoid some method calls.
#End
Class ArrayListEnumerator<E> Extends ListEnumerator<E>
	Field alst:ArrayList<E>

	Method New(lst:ArrayList<E>)
		Super.New(lst)
		Self.alst = lst
		expectedModCount = alst.modCount
	End

	Method HasNext:Bool()
		CheckConcurrency()
		Return index < alst.size
	End

	Method NextObject:E()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return alst.elements[lastIndex]
	End

	Method PreviousObject:E()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return alst.elements[lastIndex]
	End

	Method Last:Void()
		CheckConcurrency()
		index = alst.size
	End
End

Class IntListEnumerator Extends ListEnumerator<IntObject>
Public
	Method New(lst:AbstractList<IntObject>)
		Super.New(lst)
	End

	Method NextInt:Int()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousInt:Int()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

Class FloatListEnumerator Extends ListEnumerator<FloatObject>
Public
	Method New(lst:AbstractList<FloatObject>)
		Super.New(lst)
	End

	Method NextFloat:Float()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousFloat:Float()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

Class StringListEnumerator Extends ListEnumerator<StringObject>
Public
	Method New(lst:AbstractList<IntObject>)
		Super.New(lst)
	End

	Method NextString:String()
		CheckConcurrency()
		lastIndex = index
		index += 1
		Return lst.Get(lastIndex).value
	End

	Method PreviousString:String()
		CheckConcurrency()
		index -= 1
		lastIndex = index
		Return lst.Get(lastIndex).value
	End
End

#Rem
	ArrayList
	Concrete implementation of AbstractList that uses a dynamically sized array to store elements.
	Has best performance when it is initialised with a capacity large enough to hold the expected number of elements.
#End
Class ArrayList<E> Extends AbstractList<E>
Private
	' fields
	Field elements:Object[]
	Field size:Int = 0

	' resizes the elements array if necessary to ensure it can fit minCapacity elements
	Method EnsureCapacity:Void(minCapacity:Int)
		Local oldCapacity:Int = elements.Length
		If minCapacity > oldCapacity Then
			Local newCapacity:Int = (oldCapacity * 3) / 2 + 1
			If newCapacity < minCapacity Then newCapacity = minCapacity
			elements = elements.Resize(newCapacity)
			modCount += 1
		End
	End

	Method RangeCheck:Void(index:Int)
		' range check doesn't use assert, for speed
		If index < 0 Or index >= size Then Error("ArrayList.RangeCheck: Index out of bounds: " + index + " is not 0<=index<" + size)
	End

	Field tempArr:Object[] = New Object[128] ' temp array used for internal call to ToArray (so we don't create an object)

Public
	' constructors
	Method New()
		Self.elements = New Object[10]
	End

	Method New(initialCapacity:Int)
		AssertGreaterThanOrEqual(initialCapacity, 0, "ArrayList.New: Illegal Capacity:")
		Self.elements = New Object[initialCapacity]
	End

	Method New(c:AbstractCollection<E>)
		elements = c.ToArray()
		size = elements.Length
	End

	'implements AbstractCollection

	Method Add:Bool(o:E)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = o
		size+=1
		modCount += 1
		Return True
	End

	Method AddAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return False
		Local newItemCount:Int = c.Size
		If size + newItemCount > elements.Length Then EnsureCapacity(size+newItemCount)
		If tempArr.Length < newItemCount Then tempArr = tempArr.Resize(newItemCount)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			elements[size] = tempArr[i]
			size+=1
		End
		modCount += 1
		Return newItemCount <> 0
	End

	Method Clear:Void()
		For Local i:Int = 0 Until size
			elements[i] = Null
		Next
		modCount += 1
		size = 0
	End

	Method Contains:Bool(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then Return True
		Next
		Return False
	End

	Method ContainsAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return True
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Not Self.Contains(E(tempArr[i])) Then Return False
		Next
		Return True
	End

	Method FillArray:Int(arr:Object[])
		AssertGreaterThanOrEqual(arr.Length, size, "ArrayList.FillArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = elements[i]
		Next
		Return size
	End

	Method IsEmpty:Bool()
		Return size = 0
	End

	Method Remove:Bool(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method RemoveAll:Bool(c:AbstractCollection<E>)
		If c.IsEmpty() Then Return False
		Local modified:Bool = False
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Self.Contains(E(tempArr[i])) Then
				Self.Remove(E(tempArr[i]))
			End
		Next
		If modified Then modCount += 1
		Return modified
	End

	Method RetainAll:Bool(c:AbstractCollection<E>)
		Local modified:Bool = False
		If tempArr.Length < c.Size Then tempArr = tempArr.Resize(c.Size)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = 0 Until len
			If Not Self.Contains(E(tempArr[i])) Then
				Self.Remove(E(tempArr[i]))
				modified = True
			End
		Next
		If modified Then modCount += 1
		Return modified
	End

	Method Size:Int() Property
		Return size
	End

	Method Sort:Void(reverse:Bool = False, comp:AbstractComparator = Null)
		If size <= 1 Then Return ' can't sort 0 or 1 elements
		If comp = Null Then comp = Self.Comparator
		If comp = Null Then comp = DEFAULT_COMPARATOR
		QuickSort(elements, 0, size-1, comp, reverse)
		modCount += 1
	End

	Method ToArray:Object[]()
		Local arr:Object[] = New Object[size]
		For Local i:Int = 0 Until size
			arr[i] = elements[i]
		Next
		Return arr
	End

	' implements AbstractList

	Method Get:E(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return E(elements[index])
	End

	Method Insert:Void(index:Int, o:E)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = o
		size+=1
		modCount += 1
	End

	Method InsertAll:Bool(index:Int, c:AbstractCollection<E>)
		Local newItemCount:Int = c.Size
		If newItemCount = 0 Then Return False
		If size + newItemCount > elements.Length Then EnsureCapacity(size+newItemCount)
		If tempArr.Length < newItemCount Then tempArr = tempArr.Resize(newItemCount)
		Local len:Int = c.FillArray(tempArr)
		For Local i:Int = size - 1 To index Step -1
			elements[i+newItemCount] = elements[i]
		Next
		For Local i:Int = 0 Until newItemCount
			elements[index+i] = tempArr[i]
		End
		size += newItemCount
		modCount += 1
		Return newItemCount <> 0
	End

	Method IndexOf:Int(o:E)
		For Local i:Int = 0 Until size
			If elements[i] = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOf:Int(o:E)
		For Local i:Int = size-1 To 0 Step -1
			If elements[i] = o Then Return i
		Next
		Return -1
	End

	Method RemoveAt:E(index:Int)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:E = E(elements[index])
		For Local i:Int = index Until size-1
			elements[i] = elements[i+1]
		Next
		elements[size-1] = Null
		size-=1
		modCount += 1
		Return oldValue
	End

	Method RemoveRange:Void(fromIndex:Int, toIndex:Int)
		AssertLessThanOrEqual(fromIndex, toIndex, "ArrayList.RemoveRange: fromIndex > toIndex:")
		If rangeChecking Then
			RangeCheck(fromIndex)
			RangeCheck(toIndex)
		End
		For Local i:Int = 0 Until toIndex - fromIndex
			RemoveAt(fromIndex)
		Next
	End

	Method Set:E(index:Int, o:E)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:E = E(elements[index])
		elements[index] = o
		modCount += 1
		Return oldValue
	End

	Method Enumerator:AbstractEnumerator<E>()
		Return New ArrayListEnumerator<E>(Self)
	End
End

' ArrayList wrapper classes

Class IntArrayList Extends ArrayList<IntObject>
Public
	Method AddInt:Bool(o:Int)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = New IntObject(o)
		size+=1
		modCount += 1
		Return True
	End

	Method ContainsInt:Bool(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<IntObject>()
		Return New IntArrayListEnumerator(Self)
	End

	Method RemoveInt:Bool(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToIntArray:Int[]()
		Local arr:Int[] = New Int[size]
		For Local i:Int = 0 Until size
			arr[i] = IntObject(elements[i]).value
		Next
		Return arr
	End

	Method GetInt:Int(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return IntObject(elements[index]).value
	End

	Method InsertInt:Void(index:Int, o:Int)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New IntObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfInt:Int(o:Int)
		For Local i:Int = 0 Until size
			If IntObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfInt:Int(o:E)
		For Local i:Int = size-1 To 0 Step -1
			If IntObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetInt:Int(index:Int, o:Int)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:Int = IntObject(elements[index]).value
		If elements[index] <> Null Then
			IntObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillIntArray:Int(arr:Int[])
		AssertLessThan(arr.Length, size, "IntArrayList.FillIntArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = IntObject(elements[i]).value
		Next
		Return size
	End
End

Class FloatArrayList Extends ArrayList<FloatObject>
Public
	Method AddFloat:Bool(o:Float)
		If size + 1 > elements.Length Then EnsureCapacity(size + 1)
		elements[size] = New FloatObject(o)
		size += 1
		modCount += 1
		Return True
	End

	Method ContainsFloat:Bool(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<FloatObject>()
		Return New FloatArrayListEnumerator(Self)
	End

	Method RemoveFloat:Bool(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToFloatArray:Float[]()
		Local arr:Float[] = New Float[size]
		For Local i:Int = 0 Until size
			arr[i] = FloatObject(elements[i]).value
		Next
		Return arr
	End

	Method GetFloat:Float(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return FloatObject(elements[index]).value
	End

	Method InsertFloat:Void(index:Int, o:Float)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New FloatObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfFloat:Int(o:Float)
		For Local i:Int = 0 Until size
			If FloatObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfFloat:Int(o:Float)
		For Local i:Int = size-1 To 0 Step -1
			If FloatObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetFloat:Float(index:Int, o:Float)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:Float = FloatObject(elements[index]).value
		If elements[index] <> Null Then
			FloatObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillFloatArray:Int(arr:Float[])
		AssertLessThan(arr.Length, size, "FloatArrayList.FillFloatArray: Array too small:")
		For Local i:Int = 0 Until size
			arr[i] = FloatObject(elements[i]).value
		Next
		Return size
	End
End

Class StringArrayList Extends ArrayList<StringObject>
Public
	Method AddString:Bool(o:String)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		elements[size] = New StringObject(o)
		size+=1
				modCount += 1
				Return True
			End

	Method ContainsString:Bool(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then Return True
		Next
		Return False
	End

	Method Enumerator:AbstractEnumerator<StringObject>()
		Return New StringArrayListEnumerator(Self)
	End

	Method RemoveString:Bool(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then
				RemoveAt(i)
				modCount += 1
				Return True
			End
		Next
		Return False
	End

	Method ToStringArray:String[]()
		Local arr:String[] = New String[size]
		For Local i:Int = 0 Until size
			arr[i] = StringObject(elements[i]).value
		Next
		Return arr
	End

	Method GetString:String(index:Int)
		If rangeChecking Then RangeCheck(index)
		Return StringObject(elements[index]).value
	End

	Method InsertString:Void(index:Int, o:String)
		If rangeChecking Then RangeCheck(index)
		If size+1 > elements.Length Then EnsureCapacity(size+1)
		For Local i:Int = size Until index Step -1
			elements[i] = elements[i-1]
		Next
		elements[index] = New StringObject(o)
		size+=1
		modCount += 1
	End

	Method IndexOfString:Int(o:String)
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method LastIndexOfString:Int(o:String)
		For Local i:Int = size-1 To 0 Step -1
			If StringObject(elements[i]).value = o Then Return i
		Next
		Return -1
	End

	Method SetString:String(index:Int, o:String)
		If rangeChecking Then RangeCheck(index)
		Local oldValue:String = StringObject(elements[index]).value
		If elements[index] <> Null Then
			StringObject(elements[index]).value = o ' XXX: not sure whether we can change value on the fly
		End
		modCount += 1
		Return oldValue
	End

	Method FillStringArray:Int(arr:String[])
		AssertLessThan(arr.Length, size, "StringArrayList.FillStringArray: Array too small:")
		For Local i:Int = 0 Until size
			If StringObject(elements[i]).value = value Then
				Remove(elements[i])
				modCount += 1
				Return True
			End
		Next
		Return False
	End
End

Function QuickSort:Void(arr:Object[], left:Int, right:Int, comp:AbstractComparator, reverse:Bool = False)
	If right > left Then
		Local pivotIndex:Int = left + (right-left)/2
		Local pivotNewIndex:Int = QuickSortPartition(arr, left, right, pivotIndex, comp, reverse)
		QuickSort(arr, left, pivotNewIndex - 1, comp, reverse)
		QuickSort(arr, pivotNewIndex + 1, right, comp, reverse)
	End
End

Function QuickSortPartition:Int(arr:Object[], left:Int, right:Int, pivotIndex:Int, comp:AbstractComparator, reverse:Bool = False)
	Local pivotValue:Object = arr[pivotIndex]
	arr[pivotIndex] = arr[right]
	arr[right] = pivotValue
	Local storeIndex:Int = left, val:Object
	For Local i:Int = left Until right
		If Not reverse And comp.Compare(arr[i], pivotValue) <= 0 Or reverse And comp.Compare(arr[i], pivotValue) >= 0 Then
			val = arr[i]
			arr[i] = arr[storeIndex]
			arr[storeIndex] = val
			storeIndex += 1
		End
	Next
	val = arr[storeIndex]
	arr[storeIndex] = arr[right]
	arr[right] = val
	Return storeIndex
End


Removed stupid merge code and changed field types back to Int.

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

Strict

Import monkey.map
Import collections
Import framework
Import xml

' TileMapPropertyContainer
' Classes that extend this will automatically instantiate a property container and expose it.
Class TileMapPropertyContainer
Private
	Field properties:TileMapProperties = New TileMapProperties

Public
	Method Properties:TileMapProperties() Property
		Return properties
	End
End

' TileMapPostLoad
' Classes that implement this interface will have PostLoad automatically called once the object has been created.
Interface TileMapPostLoad
	Method PostLoad:Void()
End

' TileMapReader
' This can be extended to handle any map file format.
Class TileMapReader Abstract
	Field tileMap:TileMap

	Method LoadMap:TileMap(filename:String) Abstract

	' override this to create a custom tilemap class
	Method CreateMap:TileMap()
		Return New TileMap
	End
End

' TiledTileMapReader
' Extends TileMapReader to add support for the Tiled map editor.
Class TiledTileMapReader Extends TileMapReader
	Field doc:XMLDocument

	' Overrides TileMapReader
	Method LoadMap:TileMap(filename:String)
		' open file and get root node
		Local parser:XMLParser = New XMLParser
		doc = parser.ParseString(LoadString(filename)) ' TODO: at the moment it assumes filename is actually the loaded string
		Return ReadMap(doc.Root)
	End

	Method ReadMap:TileMap(node:XMLElement)
		tileMap = CreateMap()
		ReadProperties(node, tileMap)

		' extract map properties
		If tileMap.properties.Has(PROP_MAP_WRAP_X) Then tileMap.wrapX = tileMap.properties.Get(PROP_MAP_WRAP_X).GetBool()
		If tileMap.properties.Has(PROP_MAP_WRAP_Y) Then tileMap.wrapY = tileMap.properties.Get(PROP_MAP_WRAP_Y).GetBool()

		' read root node's attributes
		If node.HasAttribute(ATTR_MAP_VERSION) Then tileMap.version = node.GetAttribute(ATTR_MAP_VERSION)
		If node.HasAttribute(ATTR_MAP_ORIENTATION) Then tileMap.orientation = node.GetAttribute(ATTR_MAP_ORIENTATION)
		If node.HasAttribute(ATTR_MAP_WIDTH) Then tileMap.width = Int(node.GetAttribute(ATTR_MAP_WIDTH))
		If node.HasAttribute(ATTR_MAP_HEIGHT) Then tileMap.height = Int(node.GetAttribute(ATTR_MAP_HEIGHT))
		If node.HasAttribute(ATTR_MAP_TILEWIDTH) Then tileMap.tileWidth = Int(node.GetAttribute(ATTR_MAP_TILEWIDTH))
		If node.HasAttribute(ATTR_MAP_TILEHEIGHT) Then tileMap.tileHeight = Int(node.GetAttribute(ATTR_MAP_TILEHEIGHT))

		tileMap.maxTileWidth = tileMap.tileWidth
		tileMap.maxTileHeight = tileMap.tileHeight

		' parse children
		If Not node.Children.IsEmpty() Then
			For Local mapchild:XMLElement = EachIn node.Children
				' tileset
				If mapchild.Name = NODE_TILESET Then
					Local ts:TileMapTileset = ReadTileset(mapchild)
					tileMap.tilesets.Set(ts.name, ts)

				' tile layer
				ElseIf mapchild.Name = NODE_LAYER Then
					Local layer:TileMapLayer = ReadTileLayer(mapchild)
					tileMap.layers.Add(layer)

				' object layer
				ElseIf mapchild.Name = NODE_OBJECTGROUP Then
					Local layer:TileMapLayer = ReadObjectLayer(mapchild)
					tileMap.layers.Add(layer)
				EndIf
			Next
		EndIf

		DoPostLoad(tileMap)

		Return tileMap
	End

	Method DoPostLoad:Void(obj:Object)
		If TileMapPostLoad(obj) <> Null Then TileMapPostLoad(obj).PostLoad()
	End

	Method ReadProperties:Void(node:XMLElement, obj:Object)
		Local cont:TileMapPropertyContainer = TileMapPropertyContainer(obj)
		If cont <> Null Then
			For Local propNode:XMLElement = EachIn node.Children
				If propNode.Name = NODE_PROPERTIES Then
					For Local child:XMLElement = EachIn propNode.Children
						If child.Name = NODE_PROPERTY Then
							Local prop:TileMapProperty = ReadProperty(child)
							cont.properties.props.Set(prop.name, prop)
						EndIf
					Next
					Return
				End
			Next
		End
	End

	Method ReadProperty:TileMapProperty(node:XMLElement)
		Return New TileMapProperty(node.GetAttribute(ATTR_PROPERTY_NAME, "default"), node.GetAttribute(ATTR_PROPERTY_VALUE, ""))
	End

	Method ReadTileset:TileMapTileset(node:XMLElement, target:TileMapTileset=Null)
		Local rv:TileMapTileset = target
		ReadProperties(node, rv)
		If rv = Null Then rv = tileMap.CreateTileset()
		If node.HasAttribute(ATTR_TILESET_FIRSTGID) Then rv.firstGid = Int(node.GetAttribute(ATTR_TILESET_FIRSTGID))

		If node.HasAttribute(ATTR_TILESET_SOURCE) Then
			rv.source = node.GetAttribute(ATTR_TILESET_SOURCE)
			Local parser:XMLParser = New XMLParser
			Local tilesetdoc:XMLDocument = parser.ParseFile(rv.source)
			Return ReadTileset(tilesetdoc.Root, rv)
		Else
			If node.HasAttribute(ATTR_TILESET_NAME) Then rv.name = node.GetAttribute(ATTR_TILESET_NAME)
			If node.HasAttribute(ATTR_TILESET_TILEWIDTH) Then rv.tileWidth = Int(node.GetAttribute(ATTR_TILESET_TILEWIDTH))
			If node.HasAttribute(ATTR_TILESET_TILEHEIGHT) Then rv.tileHeight = Int(node.GetAttribute(ATTR_TILESET_TILEHEIGHT))
			If node.HasAttribute(ATTR_TILESET_SPACING) Then rv.spacing = Int(node.GetAttribute(ATTR_TILESET_SPACING))
			If node.HasAttribute(ATTR_TILESET_MARGIN) Then rv.margin = Int(node.GetAttribute(ATTR_TILESET_MARGIN))

			If Not node.Children.IsEmpty() Then
				For Local child:XMLElement = EachIn node.Children
					If child.Name = NODE_IMAGE Then
						rv.imageNode = ReadImage(child)
					ElseIf child.Name = NODE_TILE Then
						rv.tileNodes.Add(ReadTile(child))
					End
				Next
			End
		End
		DoPostLoad(rv)
		Return rv
	End

	Method ReadLayerAttributes:Void(node:XMLElement, layer:TileMapLayer)
		If node.HasAttribute(ATTR_LAYER_NAME) Then layer.name = node.GetAttribute(ATTR_LAYER_NAME)
		If node.HasAttribute(ATTR_LAYER_WIDTH) Then layer.width = Int(node.GetAttribute(ATTR_LAYER_WIDTH))
		If node.HasAttribute(ATTR_LAYER_HEIGHT) Then layer.height = Int(node.GetAttribute(ATTR_LAYER_HEIGHT))
		layer.visible = Not node.HasAttribute(ATTR_LAYER_VISIBLE) Or Int(node.GetAttribute(ATTR_LAYER_VISIBLE)) <> 0
		If node.HasAttribute(ATTR_LAYER_OPACITY) Then layer.opacity = Float(node.GetAttribute(ATTR_LAYER_OPACITY))
	End

	Method ReadTileLayer:TileMapTileLayer(node:XMLElement)
		Local rv:TileMapTileLayer = tileMap.CreateTileLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_X) Then rv.parallaxOffsetX = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_Y) Then rv.parallaxOffsetY = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_Y).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_X) Then rv.parallaxScaleX = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_Y) Then rv.parallaxScaleY = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_Y).GetFloat()

		For Local child:XMLElement = EachIn node.Children
			If child.Name = NODE_DATA Then
				rv.mapData = ReadTileData(child, rv)
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadObjectLayer:TileMapObjectLayer(node:XMLElement)
		Local rv:TileMapObjectLayer = tileMap.CreateObjectLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If node.HasAttribute(ATTR_OBJECTGROUP_COLOR) Then rv.color = ColorToInt(node.GetAttribute(ATTR_OBJECTGROUP_COLOR))

		For Local child:XMLElement = EachIn node.Children
			If child.Name = NODE_OBJECT Then
				rv.objects.Add(ReadObject(child, rv))
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadImage:TileMapImage(node:XMLElement)
		Local rv:TileMapImage = tileMap.CreateImage()
		ReadProperties(node, rv)

		If node.HasAttribute(ATTR_IMAGE_SOURCE) Then rv.source = StripDir(node.GetAttribute(ATTR_IMAGE_SOURCE))
		If node.HasAttribute(ATTR_IMAGE_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_IMAGE_WIDTH))
		If node.HasAttribute(ATTR_IMAGE_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_IMAGE_HEIGHT))
		If node.HasAttribute(ATTR_IMAGE_TRANS) Then rv.trans = node.GetAttribute(ATTR_IMAGE_TRANS)
		If rv.trans.Length > 0 Then
			rv.transR = HexToDec(rv.trans[0..2])
			rv.transG = HexToDec(rv.trans[2..4])
			rv.transB = HexToDec(rv.trans[4..6])
		EndIf

		DoPostLoad(rv)
		Return rv
	End

	Method ReadTile:TileMapTile(node:XMLElement)
		Local id:Int = Int(node.GetAttribute(ATTR_TILE_ID, "0"))
		Local rv:TileMapTile = tileMap.CreateTile(id)
		ReadProperties(node, rv)
		DoPostLoad(rv)
		Return rv
	End

	Method ReadObject:TileMapObject(node:XMLElement, layer:TileMapObjectLayer)
		Local rv:TileMapObject = tileMap.CreateObject()
		ReadProperties(node, rv)
		If node.HasAttribute(ATTR_OBJECT_NAME) Then rv.name = node.GetAttribute(ATTR_OBJECT_NAME)
		If node.HasAttribute(ATTR_OBJECT_TYPE) Then rv.objectType = node.GetAttribute(ATTR_OBJECT_TYPE)
		If node.HasAttribute(ATTR_OBJECT_X) Then rv.x = Int(node.GetAttribute(ATTR_OBJECT_X))
		If node.HasAttribute(ATTR_OBJECT_Y) Then rv.y = Int(node.GetAttribute(ATTR_OBJECT_Y))
		If node.HasAttribute(ATTR_OBJECT_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_OBJECT_WIDTH))
		If node.HasAttribute(ATTR_OBJECT_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_OBJECT_HEIGHT))
		DoPostLoad(rv)
		Return rv
	End

	Method ReadTileData:TileMapData(node:XMLElement, layer:TileMapTileLayer)
		Local rv:TileMapData = tileMap.CreateData(layer.width, layer.height)

		' default to raw xml (ugly)
		Local encoding$ = DATA_ENCODING_RAW
		If node.HasAttribute(ATTR_DATA_ENCODING) Then encoding = node.GetAttribute(ATTR_DATA_ENCODING)

		If encoding = DATA_ENCODING_RAW Then
			' TODO: raw xml
		ElseIf encoding = DATA_ENCODING_CSV Then
			' TODO: csv encoding
		ElseIf encoding = DATA_ENCODING_BASE64 Then
			Local bytes:Int[] = DecodeBase64Bytes(node.Value)
			If node.HasAttribute(ATTR_DATA_COMPRESSION) Then
				' TODO: compression
			End
			For Local i% = 0 Until bytes.Length Step 4
				' little endian
				Local gid% = bytes[i]
				gid += bytes[i + 1] Shl 8
				gid += bytes[i + 2] Shl 16
				gid += bytes[i + 3] Shl 24
				rv.tiles[i / 4] = gid
				rv.cells[i / 4] = tileMap.CreateCell(gid, (i / 4) Mod rv.width, (i / 4) / rv.width)
			Next
		End
		Return rv
	End
End

' TileMapProperties
' Container for properties.
Class TileMapProperties
	Field props:StringMap<TileMapProperty> = New StringMap<TileMapProperty>

	Method Has:Bool(name:String)
		Return props.Contains(name)
	End

	Method Get:TileMapProperty(name:String)
		Return props.Get(name)
	End

	Method Set:Void(name:String, prop:TileMapProperty)
		props.Set(name, prop)
	End
End

' TileMap
' The main Map class.
Class TileMap Extends TileMapPropertyContainer Implements TileMapPostLoad

	' attributes
	Field version:String = "1.0"
	Field orientation:String = MAP_ORIENTATION_ORTHOGONAL
	Field width:Int
	Field height:Int
	Field tileWidth:Int = 32
	Field tileHeight:Int = 32

	' children
	Field tilesets:StringMap<TileMapTileset> = New StringMap<TileMapTileset>
	Field layers:ArrayList<TileMapLayer> = New ArrayList<TileMapLayer>

	' post-load
	Field layerNames:StringMap<TileMapLayer> = New StringMap<TileMapLayer>
	Field tiles:TileMapTile[]
	Field maxTileWidth:Int
	Field maxTileHeight:Int
	Field wrapX:Bool = False
	Field wrapY:Bool = False

	' override this to configure a layer (called on every render)
	Method ConfigureLayer:Void(tileLayer:TileMapLayer)
	End

	' override this to draw a tile
	Method DrawTile:Void(tileLayer:TileMapTileLayer, mapTile:TileMapTile, x:Int, y:Int)
	End

	' override this to create a custom tile class
	Method CreateTile:TileMapTile(id:Int)
		Return New TileMapTile(id)
	End

	' override this to create a custom tileset class
	Method CreateTileset:TileMapTileset()
		Return New TileMapTileset
	End

	' override this to create a custom tile layer class
	Method CreateTileLayer:TileMapTileLayer()
		Return New TileMapTileLayer
	End

	' override this to create a custom object layer class
	Method CreateObjectLayer:TileMapObjectLayer()
		Return New TileMapObjectLayer
	End

	' override this to create a custom image class
	Method CreateImage:TileMapImage()
		Return New TileMapImage
	End

	' override this to create a custom object class
	Method CreateObject:TileMapObject()
		Return New TileMapObject
	End

	' override this to create a custom cell class
	Method CreateCell:TileMapCell(gid:Int, x:Int, y:Int)
		Return New TileMapCell(gid, x, y)
	End

	' override this to create a custom data class
	Method CreateData:TileMapData(width:Int, height:Int)
		Return New TileMapData(width, height)
	End

	' override this to perform additional post-loading functionality (remember to call Super.PostLoad() first)
	Method PostLoad:Void()
		Local totaltiles% = 0, ts:TileMapTileset
		Local alltiles:ArrayList<TileMapTile> = New ArrayList<TileMapTile>
		For Local ts:TileMapTileset = EachIn tilesets.Values()
			' load the image
			ts.image = game.images.LoadTileset(ts.imageNode.source, ts.tileWidth, ts.tileHeight, ts.margin, ts.spacing)
			' get the cell count
			ts.tileCount = ts.image.tileCount

			' update max tile size
			If maxTileWidth < ts.tileWidth Then maxTileWidth = ts.tileWidth
			If maxTileHeight < ts.tileHeight Then maxTileHeight = ts.tileHeight

			' build tile list
			ts.tiles = New TileMapTile[ts.tileCount]
			For Local t:TileMapTile = EachIn ts.tileNodes
				ts.tiles[t.id] = t
			Next
			For Local i% = 0 Until ts.tiles.Length
				If ts.tiles[i] = Null Then
					ts.tiles[i] = CreateTile(i)
				End
				ts.tiles[i].gid = ts.firstGid + i
				ts.tiles[i].image = ts.image
				ts.tiles[i].width = ts.tileWidth
				ts.tiles[i].height = ts.tileHeight
				alltiles.Add(ts.tiles[i])
			Next
			' update total tiles
			totaltiles += ts.tileCount
		Next

		' make our cache
		tiles = New TileMapTile[totaltiles]
		For Local t:TileMapTile = EachIn alltiles
			tiles[t.gid - 1] = t
		Next

		' calculate the max tile size per layer
		For Local l:TileMapLayer = EachIn layers
			If TileMapTileLayer(l) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(l)
				For Local i% = 0 Until tl.mapData.tiles.Length
					If tl.mapData.tiles[i] > 0 Then
						If tl.maxTileWidth < tiles[tl.mapData.tiles[i] - 1].width Then tl.maxTileWidth = tiles[tl.mapData.tiles[i] - 1].width
						If tl.maxTileHeight < tiles[tl.mapData.tiles[i] - 1].height Then tl.maxTileHeight = tiles[tl.mapData.tiles[i] - 1].height
					End
				Next
			End
		Next
	End

	#Rem
	Method WorldToMap:TPoint(worldPoint:TPoint)
		If worldPoint.X < 0 Or worldPoint.Y < 0 Or worldPoint.X > Width * TileWidth Or worldPoint.Y > Height * TileHeight Then
			' out of range!
			Return Null
		EndIf
		Local rv:TPoint = New TPoint.CreatePoint(worldPoint.X / TileWidth, worldPoint.Y / TileHeight)
		If worldPoint.X = Width * TileWidth Then rv.X:-1
		If worldPoint.Y = Height * TileHeight Then rv.Y:-1
		Return rv
	End
	#End

	Method GetAllObjects:ArrayList<TileMapObject>()
		#Rem
		Local layer:TTiledLayer
		Local obj:TTiledObject
		Local rv:TList = New TList
		For layer = EachIn Layers
			If TTiledObjectLayer(layer) <> Null Then
				For obj = EachIn TTiledObjectLayer(layer).Objects
					rv.AddLast(obj)
				Next
			EndIf
		Next
		Return rv
		#End
	End

	Method FindObjectByName:TileMapObject(name:String)
		#Rem
		Local layer:TTiledLayer, obj:TTiledObject
		For layer = EachIn Layers
			If TTiledObjectLayer(layer) <> Null Then
				For obj = EachIn TTiledObjectLayer(layer).Objects
					If obj.Name = name Then Return obj
				Next
			EndIf
		Next
		#End
		Return Null
	End

	' bx,by,bw,bh = render bounds (screen)
	' sx,sy = scale x/y (float, defaults to 1) i'll do this later
	' wx,wy = wrap x/y (boolean, defaults to false)
	Method RenderMap:Void(bx%, by%, bw%, bh%, sx# = 1, sy# = 1)
		Local x%, y%, rx%, ry%, mx%, my%, mx2%, my2%, modx%, mody%
		For Local layer:TileMapLayer = EachIn layers
			If layer.visible And TileMapTileLayer(layer) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(layer)
				Local mapTile:TileMapTile, gid%
				ConfigureLayer(layer)
				' ortho
				If orientation = MAP_ORIENTATION_ORTHOGONAL Then
					modx = (bx * tl.parallaxScaleX) mod tileWidth
					mody = (by * tl.parallaxScaleY) mod tileHeight
					y = by
					my = Int(Floor(Float(by * tl.parallaxScaleY) / Float(tileHeight)))
					While y < by + bh + tl.maxTileHeight
						x = bx - tl.maxTileWidth
						mx = Int(Floor(Float(bx * tl.parallaxScaleX) / Float(tileWidth)))
						While x < bx + bw + tl.maxTileWidth
							If (wrapX Or (mx >= 0 And mx < width)) And (wrapY Or (my >= 0 And my < height)) Then
								mx2 = mx
								my2 = my
								While mx2 < 0
									mx2 += width
								End
								While mx2 >= width
									mx2 -= width
								End
								While my2 < 0
									my2 += height
								End
								While my2 >= height
									my2 -= height
								End
								gid = tl.mapData.GetCell(mx2, my2).gid
								If gid > 0 Then
									mapTile = tiles[gid - 1]

									If modx < 0 Then modx += tileWidth
									If mody < 0 Then mody += tileHeight
									rx = x - modx - bx
									ry = y - mody - by
									DrawTile(tl, mapTile, rx, ry)
								End
							End
							x += tileWidth
							mx += 1
						End
						y += tileHeight
						my += 1
					End

				' iso
				ElseIf orientation = MAP_ORIENTATION_ISOMETRIC Then
					' TODO: wrapping
					For y = 0 Until tl.width + tl.height
						ry = y
						rx = 0
						While ry >= tl.height
							ry -= 1
							rx += 1
						Wend
						While ry >= 0 And rx < tl.width
							gid = tl.mapData.GetCell(rx, ry).gid
							If gid > 0 Then
								mapTile = tiles[gid - 1]
								DrawTile(tl, mapTile, (rx - ry - 1) * tileWidth / 2, (rx + ry + 2) * tileHeight / 2 - mapTile.height)
							EndIf
							ry -= 1
							rx += 1
						End
					Next
				End
			End
		Next
	End

	Method GetBounds:TileMapRect()
		Local rv:TileMapRect = New TileMapRect
		If orientation = MAP_ORIENTATION_ORTHOGONAL Then
			rv.x = 0
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 1) * tileWidth + maxTileWidth
			rv.h = (height - 1) * tileHeight + maxTileHeight
		ElseIf orientation = MAP_ORIENTATION_ISOMETRIC Then
			rv.x = -height * tileWidth / 2
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 2) * tileWidth / 2 + maxTileWidth - rv.X
			rv.h = (width + height) * tileHeight / 2 - rv.Y
		EndIf
		Return rv
	End

	Method UpdateAnimation:Void(timePassed:Int)
		Local layer:TileMapLayer, tl:TileMapTileLayer, cell:TileMapCell, t:TileMapTile
		' loop on each layer
		For layer = EachIn layers
			' cast
			tl = TileMapTileLayer(layer)

			' if the layer is a tile layer
			If tl <> Null Then
				' loop on each cell
				For cell = EachIn tl.mapData.cells
					' if the cell exists and has a value
					If cell <> Null And cell.gid > 0 Then
						' get the tile
						t = tiles[cell.gid-1]

						' if the direction is 0 (paused), do nothing
						If t <> Null And cell.direction <> 0 Then
							' add our time to the time passed for the cell
							cell.timePassed += timePassed

							' if this tile has animation information
							If t.animated Then
								' get the new direction if we have one
								If t.hasAnimDirection Then cell.direction = t.animDirection

								' while it's not paused and we're not up to the current frame
								While cell.direction <> 0 And cell.timePassed >= t.animDelay
									' move to the next frame
									cell.timePassed -= t.animDelay
									cell.gid += cell.direction * t.animNext

									' get the tile for the new frame
									t = tiles[cell.gid-1]

									' if there's animation information, get it
									If t <> Null And t.animated Then
										' if the tile has a direction assigned to it, use it (we might be turning around)
										If t.hasAnimDirection Then cell.direction = t.animDirection

									' otherwise, pause and die
									Else
										cell.direction = 0
										Exit
									End
								End

							' no animation information, so pause
							Else
								cell.direction = 0
							End
						End
					End
				Next
			End
		Next
	End
End

Class TileMapTileset Implements TileMapPostLoad
	' attributes
	Field firstGid:Int
	Field name:String
	Field tileWidth:Int
	Field tileHeight:Int
	Field spacing:Int
	Field margin:Int
	Field source:String

	' children
	Field imageNode:TileMapImage
	Field tileNodes:ArrayList<TileMapTile> = New ArrayList<TileMapTile>

	' post load
	Field tiles:TileMapTile[]
	Field image:GameImage
	Field tileCount:Int

	Method PostLoad:Void()
	End
End

Class TileMapImage Implements TileMapPostLoad
	' attributes
	Field source$
	Field width%
	Field height%
	Field trans$ = ""

	' post-load
	Field transR%, transG%, transB%

	Method PostLoad:Void()
	End
End

'<layer> and <objectgroup>
Class TileMapLayer Extends TileMapPropertyContainer Implements TileMapPostLoad Abstract
	' attributes
	Field name$
	Field width%
	Field height%
	Field visible% = True
	Field opacity:Float = 1

	Method PostLoad:Void()
	End
End

Class TileMapData
	Field width%
	Field height%
	Field tiles:Int[]
	Field cells:TileMapCell[]

	Method New(width%, height%)
		Self.width = width
		Self.height = height
		Self.tiles = New Int[width * height]
		Self.cells = New TileMapCell[width * height]
	End Method

	Method Get%(x%, y%)
		Return tiles[x + y * width]
	End

	Method Set:Void(x%, y%, gid%)
		tiles[x + y * width] = gid
	End

	Method GetCell:TileMapCell(x%, y%)
		Return cells[x + y * width]
	End

	Method SetCell:Void(x%, y%, cell:TileMapCell)
		cells[x + y * width] = cell
	End
End

Class TileMapTileLayer Extends TileMapLayer
	Field mapData:TileMapData
	Field maxTileWidth%
	Field maxTileHeight%
	Field parallaxOffsetX# = 0
	Field parallaxOffsetY# = 0
	Field parallaxScaleX# = 1
	Field parallaxScaleY# = 1
End

Class TileMapObjectLayer Extends TileMapLayer
	' attributes
	Field color%
	' children
	Field objects:ArrayList<TileMapObject> = New ArrayList<TileMapObject>
End

Class TileMapObject Extends TileMapPropertyContainer
	' attributes
	Field name:String
	Field objectType:String
	Field x:Int
	Field y:Int
	Field width:Int
	Field height:Int
End

Class TileMapProperty
Private
	Field valueType% = 0 ' 0=int, 1=float, 2=bool, 3=string
	Field name$ = ""
	Field rawValue$ = ""

Public
	Method New(name:String="default", value:String="")
		Self.name = name
		Self.rawValue = value
		Self.valueType = 3
	End

	Method Set:Void(value:Int)
		rawValue = value
		valueType = 0
	End

	Method Set:Void(value:Float)
		RawValue = value
		ValueType = 1
	End

	Method Set:Void(value:Bool)
		If value Then
			rawValue = "true"
		Else
			rawValue = "false"
		EndIf
		ValueType = 2
	End

	Method Set:Void(value:String)
		rawValue = value
		valueType = 3
	End

	Method GetInt:Int()
		Return Int(rawValue)
		'Local rv% = 0
		'rv = RawValue.ToInt()
		'Catch ex:Object
		'End
		'Return rv
	End

	Method GetFloat:Float()
		Return Float(rawValue)
		'Local rv:Float = 0
		'Try
		'	rv = RawValue.ToFloat()
		'Catch ex:Object
		'EndTry
		'Return rv
	End

	Method GetBool:Bool()
		Local val:String = rawValue.ToLower()
		If val = "true" Or val = "t" Or val = "y" Then Return True
		Return False
		'If val = "false" Or val = "f" Or val = "n" Then Return False
		'Return GetInt() <> 0
	End

	Method GetString:String()
		Return rawValue
	End
End

Class TileMapTile Extends TileMapPropertyContainer Implements TileMapPostLoad
	Field id%
	Field image:GameImage
	Field width:Int
	Field height:Int
	Field gid:Int

	Field animDelay:Int
	Field animNext:Int
	Field animDirection:Int
	Field hasAnimDirection:Bool
	Field animated:Bool

	Method New(id:Int)
		Self.id = id
	End

	Method PostLoad:Void()
		If properties.Has(PROP_TILE_ANIM_DELAY) Then
			animDelay = game.CalcAnimLength(properties.Get(PROP_TILE_ANIM_DELAY).GetInt())
			animated = True
		End
		If properties.Has(PROP_TILE_ANIM_NEXT) Then animNext = properties.Get(PROP_TILE_ANIM_NEXT).GetInt()
		If properties.Has(PROP_TILE_ANIM_DIRECTION) Then
			animDirection = properties.Get(PROP_TILE_ANIM_DIRECTION).GetInt()
			hasAnimDirection = True
		End
	End
End

Class TileMapCell
	' mandatory fields
	Field gid%
	Field x%
	Field y%

	' animation stuff
	Field originalGid%
	Field timePassed:Int = 0 ' the time that has passed since the last frame change
	Field direction% = 1 ' >0 = forward, <0 = backward, 0 = paused

	Method New(gid%, x%, y%)
		Self.gid = gid
		Self.x = x
		Self.y = y
		originalGid = gid
	End
End

Class TileMapRect
	Field x%, y%, w%, h%
End

Function ColorToInt%(str$)
	' TODO: convert a hex color string #rrggbb to a number
	Return 0
End

Function HexToDec%(hexstr$)
	Local chars$ = "0123456789abcdef"
	Local rv% = 0
	hexstr = hexstr.ToLower()
	For Local i% = 0 To hexstr.Length - 1
		rv = rv Shl 4
		Local idx% = chars.Find(hexstr[i])
		If idx >= 0 Then rv += idx
	Next
	Return rv
End

Const PROP_MAP_WRAP_X$ = "wrap_x"
Const PROP_MAP_WRAP_Y$ = "wrap_y"

Const PROP_LAYER_PARALLAX_OFFSET_X$ = "parallax_offset_x"
Const PROP_LAYER_PARALLAX_OFFSET_Y$ = "parallax_offset_y"
Const PROP_LAYER_PARALLAX_SCALE_X$  = "parallax_scale_x"
Const PROP_LAYER_PARALLAX_SCALE_Y$  = "parallax_scale_y"

Const PROP_TILE_ANIM_DELAY$     = "anim_delay"
Const PROP_TILE_ANIM_NEXT$      = "anim_next"
Const PROP_TILE_ANIM_DIRECTION$ = "anim_direction"

Const DATA_ENCODING_RAW$    = ""
Const DATA_ENCODING_CSV$    = "csv"
Const DATA_ENCODING_BASE64$ = "base64"

Const MAP_ORIENTATION_ORTHOGONAL$   = "orthogonal"
Const MAP_ORIENTATION_ISOMETRIC$    = "isometric"

Const ATTR_MAP_VERSION$         = "version"
Const ATTR_MAP_ORIENTATION$     = "orientation"
Const ATTR_MAP_WIDTH$           = "width"
Const ATTR_MAP_HEIGHT$          = "height"
Const ATTR_MAP_TILEWIDTH$       = "tilewidth"
Const ATTR_MAP_TILEHEIGHT$      = "tileheight"
Const ATTR_TILESET_FIRSTGID$    = "firstgid"
Const ATTR_TILESET_NAME$        = "name"
Const ATTR_TILESET_TILEWIDTH$   = "tilewidth"
Const ATTR_TILESET_TILEHEIGHT$  = "tileheight"
Const ATTR_TILESET_SPACING$     = "spacing"
Const ATTR_TILESET_MARGIN$      = "margin"
Const ATTR_TILESET_SOURCE$      = "source"
Const ATTR_IMAGE_SOURCE$        = "source"
Const ATTR_IMAGE_WIDTH$         = "width"
Const ATTR_IMAGE_HEIGHT$        = "height"
Const ATTR_IMAGE_TRANS$         = "trans"
Const ATTR_TILE_ID$             = "id"
Const ATTR_LAYER_NAME$          = "name"
Const ATTR_LAYER_WIDTH$         = "width"
Const ATTR_LAYER_HEIGHT$        = "height"
Const ATTR_LAYER_VISIBLE$       = "visible"
Const ATTR_LAYER_OPACITY$       = "opacity"
Const ATTR_DATA_ENCODING$       = "encoding"
Const ATTR_DATA_COMPRESSION$    = "compression"
Const ATTR_PROPERTY_NAME$       = "name"
Const ATTR_PROPERTY_VALUE$      = "value"
Const ATTR_OBJECTGROUP_COLOR$   = "color"
Const ATTR_OBJECTGROUP_NAME$    = "name"
Const ATTR_OBJECTGROUP_WIDTH$   = "width"
Const ATTR_OBJECTGROUP_HEIGHT$  = "height"
Const ATTR_OBJECT_NAME$         = "name"
Const ATTR_OBJECT_TYPE$         = "type"
Const ATTR_OBJECT_X$            = "x"
Const ATTR_OBJECT_Y$            = "y"
Const ATTR_OBJECT_WIDTH$        = "width"
Const ATTR_OBJECT_HEIGHT$       = "height"

Const NODE_MAP$                 = "map"
Const NODE_TILESET$             = "tileset"
Const NODE_IMAGE$               = "image"
Const NODE_TILE$                = "tile"
Const NODE_LAYER$               = "layer"
Const NODE_DATA$                = "data"
Const NODE_PROPERTIES$          = "properties"
Const NODE_PROPERTY$            = "property"
Const NODE_OBJECTGROUP$         = "objectgroup"
Const NODE_OBJECT$              = "object"

Strict

Import monkey.map
Import collections
Import framework
Import xml

' TileMapPropertyContainer
' Classes that extend this will automatically instantiate a property container and expose it.
Class TileMapPropertyContainer
Private
	Field properties:TileMapProperties = New TileMapProperties

Public
	Method Properties:TileMapProperties() Property
		Return properties
	End
End

' TileMapPostLoad
' Classes that implement this interface will have PostLoad automatically called once the object has been created.
Interface TileMapPostLoad
	Method PostLoad:Void()
End

' TileMapReader
' This can be extended to handle any map file format.
Class TileMapReader Abstract
	Field tileMap:TileMap

	Method LoadMap:TileMap(filename:String) Abstract

	' override this to create a custom tilemap class
	Method CreateMap:TileMap()
		Return New TileMap
	End
End

' TiledTileMapReader
' Extends TileMapReader to add support for the Tiled map editor.
Class TiledTileMapReader Extends TileMapReader
	Field doc:XMLDocument

	' Overrides TileMapReader
	Method LoadMap:TileMap(filename:String)
		' open file and get root node
		Local parser:XMLParser = New XMLParser
		doc = parser.ParseString(LoadString(filename)) ' TODO: at the moment it assumes filename is actually the loaded string
		Return ReadMap(doc.Root)
	End

	Method ReadMap:TileMap(node:XMLElement)
		tileMap = CreateMap()
		ReadProperties(node, tileMap)

		' extract map properties
		If tileMap.properties.Has(PROP_MAP_WRAP_X) Then tileMap.wrapX = tileMap.properties.Get(PROP_MAP_WRAP_X).GetBool()
		If tileMap.properties.Has(PROP_MAP_WRAP_Y) Then tileMap.wrapY = tileMap.properties.Get(PROP_MAP_WRAP_Y).GetBool()

		' read root node's attributes
		If node.HasAttribute(ATTR_MAP_VERSION) Then tileMap.version = node.GetAttribute(ATTR_MAP_VERSION)
		If node.HasAttribute(ATTR_MAP_ORIENTATION) Then tileMap.orientation = node.GetAttribute(ATTR_MAP_ORIENTATION)
		If node.HasAttribute(ATTR_MAP_WIDTH) Then tileMap.width = Int(node.GetAttribute(ATTR_MAP_WIDTH))
		If node.HasAttribute(ATTR_MAP_HEIGHT) Then tileMap.height = Int(node.GetAttribute(ATTR_MAP_HEIGHT))
		If node.HasAttribute(ATTR_MAP_TILEWIDTH) Then tileMap.tileWidth = Int(node.GetAttribute(ATTR_MAP_TILEWIDTH))
		If node.HasAttribute(ATTR_MAP_TILEHEIGHT) Then tileMap.tileHeight = Int(node.GetAttribute(ATTR_MAP_TILEHEIGHT))

		tileMap.maxTileWidth = tileMap.tileWidth
		tileMap.maxTileHeight = tileMap.tileHeight

		' parse children
		If Not node.Children.IsEmpty() Then
			For Local mapchild:XMLElement = EachIn node.Children
				' tileset
				If mapchild.Name = NODE_TILESET Then
					Local ts:TileMapTileset = ReadTileset(mapchild)
					tileMap.tilesets.Set(ts.name, ts)

				' tile layer
				ElseIf mapchild.Name = NODE_LAYER Then
					Local layer:TileMapLayer = ReadTileLayer(mapchild)
					tileMap.layers.Add(layer)

				' object layer
				ElseIf mapchild.Name = NODE_OBJECTGROUP Then
					Local layer:TileMapLayer = ReadObjectLayer(mapchild)
					tileMap.layers.Add(layer)
				EndIf
			Next
		EndIf

		DoPostLoad(tileMap)

		Return tileMap
	End

	Method DoPostLoad:Void(obj:Object)
		If TileMapPostLoad(obj) <> Null Then TileMapPostLoad(obj).PostLoad()
	End

	Method ReadProperties:Void(node:XMLElement, obj:Object)
		Local cont:TileMapPropertyContainer = TileMapPropertyContainer(obj)
		If cont <> Null Then
			For Local propNode:XMLElement = EachIn node.Children
				If propNode.Name = NODE_PROPERTIES Then
					For Local child:XMLElement = EachIn propNode.Children
						If child.Name = NODE_PROPERTY Then
							Local prop:TileMapProperty = ReadProperty(child)
							cont.properties.props.Set(prop.name, prop)
						EndIf
					Next
					Return
				End
			Next
		End
	End

	Method ReadProperty:TileMapProperty(node:XMLElement)
		Return New TileMapProperty(node.GetAttribute(ATTR_PROPERTY_NAME, "default"), node.GetAttribute(ATTR_PROPERTY_VALUE, ""))
	End

	Method ReadTileset:TileMapTileset(node:XMLElement, target:TileMapTileset=Null)
		Local rv:TileMapTileset = target
		ReadProperties(node, rv)
		If rv = Null Then rv = tileMap.CreateTileset()
		If node.HasAttribute(ATTR_TILESET_FIRSTGID) Then rv.firstGid = Int(node.GetAttribute(ATTR_TILESET_FIRSTGID))

		If node.HasAttribute(ATTR_TILESET_SOURCE) Then
			rv.source = node.GetAttribute(ATTR_TILESET_SOURCE)
			Local parser:XMLParser = New XMLParser
			Local tilesetdoc:XMLDocument = parser.ParseFile(rv.source)
			Return ReadTileset(tilesetdoc.Root, rv)
		Else
			If node.HasAttribute(ATTR_TILESET_NAME) Then rv.name = node.GetAttribute(ATTR_TILESET_NAME)
			If node.HasAttribute(ATTR_TILESET_TILEWIDTH) Then rv.tileWidth = Int(node.GetAttribute(ATTR_TILESET_TILEWIDTH))
			If node.HasAttribute(ATTR_TILESET_TILEHEIGHT) Then rv.tileHeight = Int(node.GetAttribute(ATTR_TILESET_TILEHEIGHT))
			If node.HasAttribute(ATTR_TILESET_SPACING) Then rv.spacing = Int(node.GetAttribute(ATTR_TILESET_SPACING))
			If node.HasAttribute(ATTR_TILESET_MARGIN) Then rv.margin = Int(node.GetAttribute(ATTR_TILESET_MARGIN))

			If Not node.Children.IsEmpty() Then
				For Local child:XMLElement = EachIn node.Children
					If child.Name = NODE_IMAGE Then
						rv.imageNode = ReadImage(child)
					ElseIf child.Name = NODE_TILE Then
						rv.tileNodes.Add(ReadTile(child))
					End
				Next
			End
		End
		DoPostLoad(rv)
		Return rv
	End

	Method ReadLayerAttributes:Void(node:XMLElement, layer:TileMapLayer)
		If node.HasAttribute(ATTR_LAYER_NAME) Then layer.name = node.GetAttribute(ATTR_LAYER_NAME)
		If node.HasAttribute(ATTR_LAYER_WIDTH) Then layer.width = Int(node.GetAttribute(ATTR_LAYER_WIDTH))
		If node.HasAttribute(ATTR_LAYER_HEIGHT) Then layer.height = Int(node.GetAttribute(ATTR_LAYER_HEIGHT))
		layer.visible = Not node.HasAttribute(ATTR_LAYER_VISIBLE) Or Int(node.GetAttribute(ATTR_LAYER_VISIBLE)) <> 0
		If node.HasAttribute(ATTR_LAYER_OPACITY) Then layer.opacity = Float(node.GetAttribute(ATTR_LAYER_OPACITY))
	End

	Method ReadTileLayer:TileMapTileLayer(node:XMLElement)
		Local rv:TileMapTileLayer = tileMap.CreateTileLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_X) Then rv.parallaxOffsetX = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_OFFSET_Y) Then rv.parallaxOffsetY = rv.properties.Get(PROP_LAYER_PARALLAX_OFFSET_Y).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_X) Then rv.parallaxScaleX = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_X).GetFloat()
		If rv.properties.Has(PROP_LAYER_PARALLAX_SCALE_Y) Then rv.parallaxScaleY = rv.properties.Get(PROP_LAYER_PARALLAX_SCALE_Y).GetFloat()

		For Local child:XMLElement = EachIn node.Children
			If child.Name = NODE_DATA Then
				rv.mapData = ReadTileData(child, rv)
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadObjectLayer:TileMapObjectLayer(node:XMLElement)
		Local rv:TileMapObjectLayer = tileMap.CreateObjectLayer()
		ReadProperties(node, rv)
		ReadLayerAttributes(node, rv)

		If node.HasAttribute(ATTR_OBJECTGROUP_COLOR) Then rv.color = ColorToInt(node.GetAttribute(ATTR_OBJECTGROUP_COLOR))

		For Local child:XMLElement = EachIn node.Children
			If child.Name = NODE_OBJECT Then
				rv.objects.Add(ReadObject(child, rv))
			End
		Next

		DoPostLoad(rv)
		Return rv
	End

	Method ReadImage:TileMapImage(node:XMLElement)
		Local rv:TileMapImage = tileMap.CreateImage()
		ReadProperties(node, rv)

		If node.HasAttribute(ATTR_IMAGE_SOURCE) Then rv.source = StripDir(node.GetAttribute(ATTR_IMAGE_SOURCE))
		If node.HasAttribute(ATTR_IMAGE_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_IMAGE_WIDTH))
		If node.HasAttribute(ATTR_IMAGE_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_IMAGE_HEIGHT))
		If node.HasAttribute(ATTR_IMAGE_TRANS) Then rv.trans = node.GetAttribute(ATTR_IMAGE_TRANS)
		If rv.trans.Length > 0 Then
			rv.transR = HexToDec(rv.trans[0..2])
			rv.transG = HexToDec(rv.trans[2..4])
			rv.transB = HexToDec(rv.trans[4..6])
		EndIf

		DoPostLoad(rv)
		Return rv
	End

	Method ReadTile:TileMapTile(node:XMLElement)
		Local id:Int = Int(node.GetAttribute(ATTR_TILE_ID, "0"))
		Local rv:TileMapTile = tileMap.CreateTile(id)
		ReadProperties(node, rv)
		DoPostLoad(rv)
		Return rv
	End

	Method ReadObject:TileMapObject(node:XMLElement, layer:TileMapObjectLayer)
		Local rv:TileMapObject = tileMap.CreateObject()
		ReadProperties(node, rv)
		If node.HasAttribute(ATTR_OBJECT_NAME) Then rv.name = node.GetAttribute(ATTR_OBJECT_NAME)
		If node.HasAttribute(ATTR_OBJECT_TYPE) Then rv.objectType = node.GetAttribute(ATTR_OBJECT_TYPE)
		If node.HasAttribute(ATTR_OBJECT_X) Then rv.x = Int(node.GetAttribute(ATTR_OBJECT_X))
		If node.HasAttribute(ATTR_OBJECT_Y) Then rv.y = Int(node.GetAttribute(ATTR_OBJECT_Y))
		If node.HasAttribute(ATTR_OBJECT_WIDTH) Then rv.width = Int(node.GetAttribute(ATTR_OBJECT_WIDTH))
		If node.HasAttribute(ATTR_OBJECT_HEIGHT) Then rv.height = Int(node.GetAttribute(ATTR_OBJECT_HEIGHT))
		DoPostLoad(rv)
		Return rv
	End

	Method ReadTileData:TileMapData(node:XMLElement, layer:TileMapTileLayer)
		Local rv:TileMapData = tileMap.CreateData(layer.width, layer.height)

		' default to raw xml (ugly)
		Local encoding$ = DATA_ENCODING_RAW
		If node.HasAttribute(ATTR_DATA_ENCODING) Then encoding = node.GetAttribute(ATTR_DATA_ENCODING)

		If encoding = DATA_ENCODING_RAW Then
			' TODO: raw xml
		ElseIf encoding = DATA_ENCODING_CSV Then
			' TODO: csv encoding
		ElseIf encoding = DATA_ENCODING_BASE64 Then
			Local bytes:Int[] = DecodeBase64Bytes(node.Value)
			If node.HasAttribute(ATTR_DATA_COMPRESSION) Then
				' TODO: compression
			End
			For Local i% = 0 Until bytes.Length Step 4
				' little endian
				Local gid% = bytes[i]
				gid += bytes[i + 1] Shl 8
				gid += bytes[i + 2] Shl 16
				gid += bytes[i + 3] Shl 24
				rv.tiles[i / 4] = gid
				rv.cells[i / 4] = tileMap.CreateCell(gid, (i / 4) Mod rv.width, (i / 4) / rv.width)
			Next
		End
		Return rv
	End
End

' TileMapProperties
' Container for properties.
Class TileMapProperties
	Field props:StringMap<TileMapProperty> = New StringMap<TileMapProperty>

	Method Has:Bool(name:String)
		Return props.Contains(name)
	End

	Method Get:TileMapProperty(name:String)
		Return props.Get(name)
	End

	Method Set:Void(name:String, prop:TileMapProperty)
		props.Set(name, prop)
	End
End

' TileMap
' The main Map class.
Class TileMap Extends TileMapPropertyContainer Implements TileMapPostLoad

	' attributes
	Field version:String = "1.0"
	Field orientation:String = MAP_ORIENTATION_ORTHOGONAL
	Field width:Int
	Field height:Int
	Field tileWidth:Int = 32
	Field tileHeight:Int = 32

	' children
	Field tilesets:StringMap<TileMapTileset> = New StringMap<TileMapTileset>
	Field layers:ArrayList<TileMapLayer> = New ArrayList<TileMapLayer>

	' post-load
	Field layerNames:StringMap<TileMapLayer> = New StringMap<TileMapLayer>
	Field tiles:TileMapTile[]
	Field maxTileWidth:Int
	Field maxTileHeight:Int
	Field wrapX:Bool = False
	Field wrapY:Bool = False

	' override this to configure a layer (called on every render)
	Method ConfigureLayer:Void(tileLayer:TileMapLayer)
	End

	' override this to draw a tile
	Method DrawTile:Void(tileLayer:TileMapTileLayer, mapTile:TileMapTile, x:Int, y:Int)
	End

	' override this to create a custom tile class
	Method CreateTile:TileMapTile(id:Int)
		Return New TileMapTile(id)
	End

	' override this to create a custom tileset class
	Method CreateTileset:TileMapTileset()
		Return New TileMapTileset
	End

	' override this to create a custom tile layer class
	Method CreateTileLayer:TileMapTileLayer()
		Return New TileMapTileLayer
	End

	' override this to create a custom object layer class
	Method CreateObjectLayer:TileMapObjectLayer()
		Return New TileMapObjectLayer
	End

	' override this to create a custom image class
	Method CreateImage:TileMapImage()
		Return New TileMapImage
	End

	' override this to create a custom object class
	Method CreateObject:TileMapObject()
		Return New TileMapObject
	End

	' override this to create a custom cell class
	Method CreateCell:TileMapCell(gid:Int, x:Int, y:Int)
		Return New TileMapCell(gid, x, y)
	End

	' override this to create a custom data class
	Method CreateData:TileMapData(width:Int, height:Int)
		Return New TileMapData(width, height)
	End

	' override this to perform additional post-loading functionality (remember to call Super.PostLoad() first)
	Method PostLoad:Void()
		Local totaltiles% = 0, ts:TileMapTileset
		Local alltiles:ArrayList<TileMapTile> = New ArrayList<TileMapTile>
		For Local ts:TileMapTileset = EachIn tilesets.Values()
			' load the image
			ts.image = game.images.LoadTileset(ts.imageNode.source, ts.tileWidth, ts.tileHeight, ts.margin, ts.spacing)
			' get the cell count
			ts.tileCount = ts.image.tileCount

			' update max tile size
			If maxTileWidth < ts.tileWidth Then maxTileWidth = ts.tileWidth
			If maxTileHeight < ts.tileHeight Then maxTileHeight = ts.tileHeight

			' build tile list
			ts.tiles = New TileMapTile[ts.tileCount]
			For Local t:TileMapTile = EachIn ts.tileNodes
				ts.tiles[t.id] = t
			Next
			For Local i% = 0 Until ts.tiles.Length
				If ts.tiles[i] = Null Then
					ts.tiles[i] = CreateTile(i)
				End
				ts.tiles[i].gid = ts.firstGid + i
				ts.tiles[i].image = ts.image
				ts.tiles[i].width = ts.tileWidth
				ts.tiles[i].height = ts.tileHeight
				alltiles.Add(ts.tiles[i])
			Next
			' update total tiles
			totaltiles += ts.tileCount
		Next

		' make our cache
		tiles = New TileMapTile[totaltiles]
		For Local t:TileMapTile = EachIn alltiles
			tiles[t.gid - 1] = t
		Next

		' calculate the max tile size per layer
		For Local l:TileMapLayer = EachIn layers
			If TileMapTileLayer(l) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(l)
				For Local i% = 0 Until tl.mapData.tiles.Length
					If tl.mapData.tiles[i] > 0 Then
						If tl.maxTileWidth < tiles[tl.mapData.tiles[i] - 1].width Then tl.maxTileWidth = tiles[tl.mapData.tiles[i] - 1].width
						If tl.maxTileHeight < tiles[tl.mapData.tiles[i] - 1].height Then tl.maxTileHeight = tiles[tl.mapData.tiles[i] - 1].height
					End
				Next
			End
		Next
	End

	#Rem
	Method WorldToMap:TPoint(worldPoint:TPoint)
		If worldPoint.X < 0 Or worldPoint.Y < 0 Or worldPoint.X > Width * TileWidth Or worldPoint.Y > Height * TileHeight Then
			' out of range!
			Return Null
		EndIf
		Local rv:TPoint = New TPoint.CreatePoint(worldPoint.X / TileWidth, worldPoint.Y / TileHeight)
		If worldPoint.X = Width * TileWidth Then rv.X:-1
		If worldPoint.Y = Height * TileHeight Then rv.Y:-1
		Return rv
	End
	#End

	Method GetAllObjects:ArrayList<TileMapObject>()
		#Rem
		Local layer:TTiledLayer
		Local obj:TTiledObject
		Local rv:TList = New TList
		For layer = EachIn Layers
			If TTiledObjectLayer(layer) <> Null Then
				For obj = EachIn TTiledObjectLayer(layer).Objects
					rv.AddLast(obj)
				Next
			EndIf
		Next
		Return rv
		#End
	End

	Method FindObjectByName:TileMapObject(name:String)
		#Rem
		Local layer:TTiledLayer, obj:TTiledObject
		For layer = EachIn Layers
			If TTiledObjectLayer(layer) <> Null Then
				For obj = EachIn TTiledObjectLayer(layer).Objects
					If obj.Name = name Then Return obj
				Next
			EndIf
		Next
		#End
		Return Null
	End

	' bx,by,bw,bh = render bounds (screen)
	' sx,sy = scale x/y (float, defaults to 1) i'll do this later
	' wx,wy = wrap x/y (boolean, defaults to false)
	Method RenderMap:Void(bx%, by%, bw%, bh%, sx# = 1, sy# = 1)
		Local x%, y%, rx%, ry%, mx%, my%, mx2%, my2%, modx%, mody%
		For Local layer:TileMapLayer = EachIn layers
			If layer.visible And TileMapTileLayer(layer) <> Null Then
				Local tl:TileMapTileLayer = TileMapTileLayer(layer)
				Local mapTile:TileMapTile, gid%
				ConfigureLayer(layer)
				' ortho
				If orientation = MAP_ORIENTATION_ORTHOGONAL Then
					modx = (bx * tl.parallaxScaleX) mod tileWidth
					mody = (by * tl.parallaxScaleY) mod tileHeight
					y = by
					my = Int(Floor(Float(by * tl.parallaxScaleY) / Float(tileHeight)))
					While y < by + bh + tl.maxTileHeight
						x = bx - tl.maxTileWidth
						mx = Int(Floor(Float(bx * tl.parallaxScaleX) / Float(tileWidth)))
						While x < bx + bw + tl.maxTileWidth
							If (wrapX Or (mx >= 0 And mx < width)) And (wrapY Or (my >= 0 And my < height)) Then
								mx2 = mx
								my2 = my
								While mx2 < 0
									mx2 += width
								End
								While mx2 >= width
									mx2 -= width
								End
								While my2 < 0
									my2 += height
								End
								While my2 >= height
									my2 -= height
								End
								gid = tl.mapData.GetCell(mx2, my2).gid
								If gid > 0 Then
									mapTile = tiles[gid - 1]

									If modx < 0 Then modx += tileWidth
									If mody < 0 Then mody += tileHeight
									rx = x - modx - bx
									ry = y - mody - by
									DrawTile(tl, mapTile, rx, ry)
									'DrawText(my, rx, ry + 10)
								End
							End
							x += tileWidth
							mx += 1
						End
						y += tileHeight
						my += 1
					End

				' iso
				ElseIf orientation = MAP_ORIENTATION_ISOMETRIC Then
					' TODO: wrapping
					For y = 0 Until tl.width + tl.height
						ry = y
						rx = 0
						While ry >= tl.height
							ry -= 1
							rx += 1
						Wend
						While ry >= 0 And rx < tl.width
							'gid = tl.MapData.Get(rx, ry)
							gid = tl.mapData.GetCell(rx, ry).gid
							If gid > 0 Then
								mapTile = tiles[gid - 1]
								DrawTile(tl, mapTile, (rx - ry - 1) * tileWidth / 2, (rx + ry + 2) * tileHeight / 2 - mapTile.height)
							EndIf
							ry -= 1
							rx += 1
						End
					Next
				End
			End
		Next
	End

	Method GetBounds:TileMapRect()
		Local rv:TileMapRect = New TileMapRect
		If orientation = MAP_ORIENTATION_ORTHOGONAL Then
			rv.x = 0
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 1) * tileWidth + maxTileWidth
			rv.h = (height - 1) * tileHeight + maxTileHeight
		ElseIf orientation = MAP_ORIENTATION_ISOMETRIC Then
			rv.x = -height * tileWidth / 2
			rv.y = tileHeight - maxTileHeight
			rv.w = (width - 2) * tileWidth / 2 + maxTileWidth - rv.X
			rv.h = (width + height) * tileHeight / 2 - rv.Y
		EndIf
		Return rv
	End

	#Rem
	Method ForEachMapCell(func:Int(layer:TTiledTileLayer, cell:TTiledMapCell, delta:Long), delta:Long)
		Local layer:TTiledTileLayer
		Local i%
		For layer = EachIn Layers
			For i = 0 To layer.MapData.Cells.Length - 1
				func(layer, layer.MapData.Cells[i], delta)
			Next
		Next
	End
	#End

<<<<<<< .mine
	Method UpdateAnimation:Void(timePassed:Int)
=======
	Method UpdateAnimation:Void(delta:Float)
>>>>>>> .r140
		Local layer:TileMapLayer, tl:TileMapTileLayer, cell:TileMapCell, t:TileMapTile
		' loop on each layer
		For layer = EachIn layers
			' cast
			tl = TileMapTileLayer(layer)

			' if the layer is a tile layer
			If tl <> Null Then
				' loop on each cell
				For cell = EachIn tl.mapData.cells
					' if the cell exists and has a value
					If cell <> Null And cell.gid > 0 Then
						' get the tile
						t = tiles[cell.gid-1]

						' if the direction is 0 (paused), do nothing
						If t <> Null And cell.direction <> 0 Then
							' add our time to the time passed for the cell
							cell.timePassed += timePassed

							' if this tile has animation information
							If t.animated Then
								' get the new direction if we have one
								If t.hasAnimDirection Then cell.direction = t.animDirection

								' while it's not paused and we're not up to the current frame
								While cell.direction <> 0 And cell.timePassed >= t.animDelay
									' move to the next frame
									cell.timePassed -= t.animDelay
									cell.gid += cell.direction * t.animNext

									' get the tile for the new frame
									t = tiles[cell.gid-1]

									' if there's animation information, get it
									If t <> Null And t.animated Then
										' if the tile has a direction assigned to it, use it (we might be turning around)
										If t.hasAnimDirection Then cell.direction = t.animDirection

									' otherwise, pause and die
									Else
										cell.direction = 0
										Exit
									End
								End

							' no animation information, so pause
							Else
								cell.direction = 0
							End
						End
					End
				Next
			End
		Next
	End
End

Class TileMapTileset Implements TileMapPostLoad
	' attributes
	Field firstGid:Int
	Field name:String
	Field tileWidth:Int
	Field tileHeight:Int
	Field spacing:Int
	Field margin:Int
	Field source:String

	' children
	Field imageNode:TileMapImage
	Field tileNodes:ArrayList<TileMapTile> = New ArrayList<TileMapTile>

	' post load
	Field tiles:TileMapTile[]
	Field image:GameImage
	Field tileCount:Int

	Method PostLoad:Void()
	End
End

Class TileMapImage Implements TileMapPostLoad
	' attributes
	Field source$
	Field width%
	Field height%
	Field trans$ = ""

	' post-load
	Field transR%, transG%, transB%

	Method PostLoad:Void()
	End
End

'<layer> and <objectgroup>
Class TileMapLayer Extends TileMapPropertyContainer Implements TileMapPostLoad Abstract
	' attributes
	Field name$
	Field width%
	Field height%
	Field visible% = True
	Field opacity:Float = 1

	Method PostLoad:Void()
	End
End

Class TileMapData
	Field width%
	Field height%
	Field tiles:Int[]
	Field cells:TileMapCell[]

	Method New(width%, height%)
		Self.width = width
		Self.height = height
		Self.tiles = New Int[width * height]
		Self.cells = New TileMapCell[width * height]
	End Method

	Method Get%(x%, y%)
		Return tiles[x + y * width]
	End

	Method Set:Void(x%, y%, gid%)
		tiles[x + y * width] = gid
	End

	Method GetCell:TileMapCell(x%, y%)
		Return cells[x + y * width]
	End

	Method SetCell:Void(x%, y%, cell:TileMapCell)
		cells[x + y * width] = cell
	End
End

Class TileMapTileLayer Extends TileMapLayer
	Field mapData:TileMapData
	Field maxTileWidth%
	Field maxTileHeight%
	Field parallaxOffsetX# = 0
	Field parallaxOffsetY# = 0
	Field parallaxScaleX# = 1
	Field parallaxScaleY# = 1
End

Class TileMapObjectLayer Extends TileMapLayer
	' attributes
	Field color%
	' children
	Field objects:ArrayList<TileMapObject> = New ArrayList<TileMapObject>
End

Class TileMapObject Extends TileMapPropertyContainer
	' attributes
	Field name:String
	Field objectType:String
	Field x:Int
	Field y:Int
	Field width:Int
	Field height:Int
End

Class TileMapProperty
Private
	Field valueType% = 0 ' 0=int, 1=float, 2=bool, 3=string
	Field name$ = ""
	Field rawValue$ = ""

Public
	Method New(name:String="default", value:String="")
		Self.name = name
		Self.rawValue = value
		Self.valueType = 3
	End

	Method Set:Void(value:Int)
		rawValue = value
		valueType = 0
	End

	Method Set:Void(value:Float)
		RawValue = value
		ValueType = 1
	End

	Method Set:Void(value:Bool)
		If value Then
			rawValue = "true"
		Else
			rawValue = "false"
		EndIf
		ValueType = 2
	End

	Method Set:Void(value:String)
		rawValue = value
		valueType = 3
	End

	Method GetInt:Int()
		Return Int(rawValue)
		'Local rv% = 0
		'rv = RawValue.ToInt()
		'Catch ex:Object
		'End
		'Return rv
	End

	Method GetFloat:Float()
		Return Float(rawValue)
		'Local rv:Float = 0
		'Try
		'	rv = RawValue.ToFloat()
		'Catch ex:Object
		'EndTry
		'Return rv
	End

	Method GetBool:Bool()
		Local val:String = rawValue.ToLower()
		If val = "true" Or val = "t" Or val = "y" Then Return True
		Return False
		'If val = "false" Or val = "f" Or val = "n" Then Return False
		'Return GetInt() <> 0
	End

	Method GetString:String()
		Return rawValue
	End
End

Class TileMapTile Extends TileMapPropertyContainer Implements TileMapPostLoad
	Field id%
	Field image:GameImage
	Field width:Int
	Field height:Int
	Field gid:Int

	Field animDelay:Float
	Field animNext:Int
	Field animDirection:Int
	Field hasAnimDirection:Bool
	Field animated:Bool

	Method New(id:Int)
		Self.id = id
	End

	Method PostLoad:Void()
		If properties.Has(PROP_TILE_ANIM_DELAY) Then
			animDelay = game.CalcAnimLength(properties.Get(PROP_TILE_ANIM_DELAY).GetInt())
			animated = True
		End
		If properties.Has(PROP_TILE_ANIM_NEXT) Then animNext = properties.Get(PROP_TILE_ANIM_NEXT).GetInt()
		If properties.Has(PROP_TILE_ANIM_DIRECTION) Then
			animDirection = properties.Get(PROP_TILE_ANIM_DIRECTION).GetInt()
			hasAnimDirection = True
		End
	End
End

Class TileMapCell
	' mandatory fields
	Field gid%
	Field x%
	Field y%

	' animation stuff
	Field originalGid%
	Field timePassed:Float = 0 ' the time that has passed since the last frame change
	Field direction% = 1 ' >0 = forward, <0 = backward, 0 = paused

	Method New(gid%, x%, y%)
		Self.gid = gid
		Self.x = x
		Self.y = y
		originalGid = gid
	End
End

Class TileMapRect
	Field x%, y%, w%, h%
End

Function ColorToInt%(str$)
	' TODO: convert a hex color string #rrggbb to a number
	Return 0
End

Function HexToDec%(hexstr$)
	Local chars$ = "0123456789abcdef"
	Local rv% = 0
	hexstr = hexstr.ToLower()
	For Local i% = 0 To hexstr.Length - 1
		rv = rv Shl 4
		Local idx% = chars.Find(hexstr[i])
		If idx >= 0 Then rv += idx
	Next
	Return rv
End

Const PROP_MAP_WRAP_X$ = "wrap_x"
Const PROP_MAP_WRAP_Y$ = "wrap_y"

Const PROP_LAYER_PARALLAX_OFFSET_X$ = "parallax_offset_x"
Const PROP_LAYER_PARALLAX_OFFSET_Y$ = "parallax_offset_y"
Const PROP_LAYER_PARALLAX_SCALE_X$  = "parallax_scale_x"
Const PROP_LAYER_PARALLAX_SCALE_Y$  = "parallax_scale_y"

Const PROP_TILE_ANIM_DELAY$     = "anim_delay"
Const PROP_TILE_ANIM_NEXT$      = "anim_next"
Const PROP_TILE_ANIM_DIRECTION$ = "anim_direction"

Const DATA_ENCODING_RAW$    = ""
Const DATA_ENCODING_CSV$    = "csv"
Const DATA_ENCODING_BASE64$ = "base64"

Const MAP_ORIENTATION_ORTHOGONAL$   = "orthogonal"
Const MAP_ORIENTATION_ISOMETRIC$    = "isometric"

Const ATTR_MAP_VERSION$         = "version"
Const ATTR_MAP_ORIENTATION$     = "orientation"
Const ATTR_MAP_WIDTH$           = "width"
Const ATTR_MAP_HEIGHT$          = "height"
Const ATTR_MAP_TILEWIDTH$       = "tilewidth"
Const ATTR_MAP_TILEHEIGHT$      = "tileheight"
Const ATTR_TILESET_FIRSTGID$    = "firstgid"
Const ATTR_TILESET_NAME$        = "name"
Const ATTR_TILESET_TILEWIDTH$   = "tilewidth"
Const ATTR_TILESET_TILEHEIGHT$  = "tileheight"
Const ATTR_TILESET_SPACING$     = "spacing"
Const ATTR_TILESET_MARGIN$      = "margin"
Const ATTR_TILESET_SOURCE$      = "source"
Const ATTR_IMAGE_SOURCE$        = "source"
Const ATTR_IMAGE_WIDTH$         = "width"
Const ATTR_IMAGE_HEIGHT$        = "height"
Const ATTR_IMAGE_TRANS$         = "trans"
Const ATTR_TILE_ID$             = "id"
Const ATTR_LAYER_NAME$          = "name"
Const ATTR_LAYER_WIDTH$         = "width"
Const ATTR_LAYER_HEIGHT$        = "height"
Const ATTR_LAYER_VISIBLE$       = "visible"
Const ATTR_LAYER_OPACITY$       = "opacity"
Const ATTR_DATA_ENCODING$       = "encoding"
Const ATTR_DATA_COMPRESSION$    = "compression"
Const ATTR_PROPERTY_NAME$       = "name"
Const ATTR_PROPERTY_VALUE$      = "value"
Const ATTR_OBJECTGROUP_COLOR$   = "color"
Const ATTR_OBJECTGROUP_NAME$    = "name"
Const ATTR_OBJECTGROUP_WIDTH$   = "width"
Const ATTR_OBJECTGROUP_HEIGHT$  = "height"
Const ATTR_OBJECT_NAME$         = "name"
Const ATTR_OBJECT_TYPE$         = "type"
Const ATTR_OBJECT_X$            = "x"
Const ATTR_OBJECT_Y$            = "y"
Const ATTR_OBJECT_WIDTH$        = "width"
Const ATTR_OBJECT_HEIGHT$       = "height"

Const NODE_MAP$                 = "map"
Const NODE_TILESET$             = "tileset"
Const NODE_IMAGE$               = "image"
Const NODE_TILE$                = "tile"
Const NODE_LAYER$               = "layer"
Const NODE_DATA$                = "data"
Const NODE_PROPERTIES$          = "properties"
Const NODE_PROPERTY$            = "property"
Const NODE_OBJECTGROUP$         = "objectgroup"
Const NODE_OBJECT$              = "object"