Obligatory 'I don't remember if I updated this' commit.

Strict

Public

' Preprocessor related:
#Rem
#If TARGET = "glfw" Or TARGET = "sexy"
	#GLFW_TARGET = True
#End
#End

#If CONFIG = "debug"
	#DEBUG_PRINT = True
#Else
	#DEBUG_PRINT = False
#End

#If CONFIG = "release"
	#DEBUG_PRINT_ON_ERROR = True
#Else
	#DEBUG_PRINT_ON_ERROR = False
#End

#DEBUG_PRINT_QUOTES = False

#READ_LINE_QUICKLY = True

#If HOST = "winnt"
	#WRITE_LINE_ENDTYPE = "CRLF"
#Else
	#WRITE_LINE_ENDTYPE = "LF"
#End

' Imports (Public):
Import autostream

Import byteorder
Import imagedimensions
Import retrostrings
Import stringutil
Import vector

#If CONSOLE_IMPLEMENTED
	Import console
#End

Import sizeof
Import time

#If Not BRL_GAMETARGET_IMPLEMENTED
	Import mojoemulator
#End

' Imports (Private):
Private

' Official:

' BRL:
Import brl.databuffer
Import brl.stream
Import brl.filepath

Public

' Constant variable(s):

' Ascii codes:
Const ASCII_CARRIAGE_RETURN:Int = 13
Const ASCII_LINE_FEED:Int = 10

' Used mainly for external code where '-9999...' isn't ever going to be valid.
' You should stick to 'Null' in most cases; try not to use this.
Const NOVAR:Int = -999999

Const ErrorTemplate:String = "[ERROR] {Debug}: " ' + Space
Const LogTemplate:String = "[Info] {Debug}: " ' + Space

' Global variable(s) (Public):
#If CONSOLE_IMPLEMENTED
	Global DebugConsole:Console = Null
#End

' A global stack logging randomization seeds.
' This is commonly accessed by the 'PushSeed' and 'PopSeed' functions.
Global SeedStack:Stack<Int> = Null

' Global variable(s) (Private):
Private

' Nothing so far.

Public

' Functions:
Function FLAG:Int(ID:Int)
	Return Pow(2, ID)
End

' This command is little-endian only:
Function AsByte:Int(I:Int)
	Return ((I Shl 24) Shr 24)
End

' This command treats data as 8-bit (Turns something like $F7, into $FFFFFFF7)
' This is only applied on little-endian systems,
' please use this command for hex values (Monkey converts hex on compile-time).
Function PaddedHex:Int(I:Int)
	If (Not BigEndian()) Then
		Return AsByte(I)
	Endif

	Return I
End

Function Transfer:Bool(InputStream:Stream, OutputStream:Stream, DataSize:Int)
	' Check for errors:
	If (DataSize = 0) Then Return False
	If (InputStream = Null Or InputStream.Eof()) Then Return False
	If (OutputStream = Null) Then Return False

	' Local variable(s):
	#If FLAG_TEMP_BUFFERS
		Local Data:DataBuffer = New DataBuffer(DataSize)
		Local DataLength:= DataSize ' Data.Length()

		' Read from the input-stream, then write to the output-stream:
		InputStream.Read(Data, 0, DataLength)
		OutputStream.Write(Data, 0, DataLength)

		Data.Discard()
	#Else
		'Local Data:Int[DataSize]

		For Local I:Int = 1 To DataSize ' Data.Length()
			OutputStream.WriteByte(InputStream.ReadByte())
		Next
	#End

	' Return the default response.
	Return True
End

Function Sq:Float(Input:Float)
	Return Pow(Input, 2.0) ' Input * Input
End

Function Sq:Int(Input:Int)
	Return Pow(Input, 2)
End

Function WrapAngle:Float(A:Float)
	While (A < 0.0)
		A += 360.0
	Wend

	While (A > 360.0)
		A -= 360.0
	Wend

	Return A
End

Function WrapColor:Float(C:Float)
	#Rem
	While (C < 0.0)
		C += 255.0
	Wend

	While (C > 255.0)
		C -= 255.0
	Wend
	#End

	Return C Mod 256.0
End

' This command is a helper function for
Function ProcessColorLocation:Int(Point:Int)
	' Check for errors:
	If (Point < 0) Then
		Return 0
	Endif

	Return (SizeOf_Integer_InBits-(SizeOf_Octet_InBits*(Point+1))) ' ((SizeOf_Integer_InBits-SizeOf_Octet_InBits)-(SizeOf_Octet_InBits*Point))
End

' NOTE: This command will produce incorrect color values without all characters present in the encode-string.
Function ColorToString:String(Pixel:Int, Encoding:String="ARGB")
	' Ensure the encoding is always described as uppercase.
	Encoding = Encoding.ToUpper()

	' Return the encoded color-string.
	Return ("R: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("R"))) & $000000FF) +
			", G: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("G"))) & $000000FF) +
			", B: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("B"))) & $000000FF) +
			", A: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("A"))) & $000000FF))
End

Function PrintColor:Void(Pixel:Int, Encoding:String="ARGB")
	Print(ColorToString(Pixel, Encoding))

	Return
End

' This is just a wrapper for the main implementation:
Function PushSeed:Bool()
	Return PushSeed(Seed)
End

' The return value of this function specifies if the seed-stack already existed.
Function PushSeed:Bool(Seed:Int)
	' Local variable(s):
	Local Response:Bool = (SeedStack <> Null)

	If (Not Response) Then
		SeedStack = New IntStack()
	Endif

	SeedStack.Push(Seed)

	' Return the calculated response.
	Return Response
End

' This command retrieves a seed from the seed-stack,
' if no seed exists, the current randomization seed is returned.
Function PopSeed:Int()
	If (SeedStack <> Null) Then
		If (Not SeedStack.IsEmpty()) Then
			Return SeedStack.Pop()
		Endif
	Endif

	' Return the default response.
	Return Seed
End

' This command writes a standard line to a 'Stream'.
' This supports both 'LF', and 'CRLF' line endings currently, and this can be configured with 'WRITE_LINE_ENDTYPE'.
' Though character encoding is supported with this, only ASCII is supported by 'ReadLine'.
Function WriteLine:Bool(S:Stream, Line:String, CharacterEncoding:String="ascii")
	' Check for errors:
	If (S = Null) Then
		Return False
	Endif

	S.WriteString(Line, CharacterEncoding)

	#If WRITE_LINE_ENDTYPE = "CRLF"
		S.WriteByte(ASCII_CARRIAGE_RETURN)
	#End

	S.WriteByte(ASCII_LINE_FEED)

	' Return the default response.
	Return True
End

' This command reads a standard line from a 'Stream'. (This supports both 'LF', and 'CRLF' line endings currently)
' In addition, only 'ASCII' is currently supported.
Function ReadLine:String(S:Stream)
	' Local variable(s):
	Local Position:= S.Position()
	Local Padding:Int = 0 ' * SizeOf_Char
	Local Str:String

	#If Not READ_LINE_QUICKLY
		Local Size:Int = 0
	#End

	While (Not S.Eof())
		Local Char:= S.ReadByte()

		If (Char <> ASCII_LINE_FEED) Then
			If (Char <> ASCII_CARRIAGE_RETURN) Then
				#If READ_LINE_QUICKLY
					Str += String.FromChar(Char)
				#Else
					Size += 1
				#End
			Else
				Padding = 2*SizeOf_Char
			Endif
		Else
			#Rem
				If (Padding = 0) Then
					Padding = 1
				Endif
			#End

			Padding = Max(Padding, 1*SizeOf_Char)

			Exit
		Endif
	Wend

	#If Not READ_LINE_QUICKLY
		S.Seek(Position)

		Str = S.ReadString(Size, "ascii")

		S.Seek(S.Position()+Padding)
	#End

	' Return the string we read from the stream.
	Return Str
End

Function ResizeBuffer:DataBuffer(Buffer:DataBuffer, Size:Int, CopyData:Bool=True, DiscardOldBuffer:Bool=False)
	' Allocate a new data-buffer.
	Local B:= New DataBuffer(Size)

	' Copy the buffer's bytes over to 'B'.
	If (CopyData) Then
		Buffer.CopyBytes(0, B, 0, Buffer.Length())
	Endif

	If (DiscardOldBuffer) Then
		Buffer.Discard()
	Endif

	' Return the new buffer ('B').
	Return B
End

#If CONSOLE_IMPLEMENTED
	Function DebugBind:Bool(C:Console)
		If (C = Null) Then Return False

		DebugConsole = C

		' Return the default response.
		Return True
	End
#End

Function DebugError:Void(E:Throwable, StopExecution:Bool=True)
	#If CONFIG = "debug"
		DebugStop()
	#Else
		DebugPrint("Unknown exception has been thrown; continuing anyway.")
	#End

	Return
End

Function DebugError:Void(Msg:String, StopExecution:Bool=True)
	#If DEBUG_PRINT
		' Local variable(s):
		Local TempMsg:String

		If (StopExecution) Then
			TempMsg = ErrorTemplate
		Else
			TempMsg = LogTemplate
		Endif

		#If DEBUG_PRINT_QUOTES
			Msg = Tempmsg + Quote + Msg + Quote
		#Else
			Msg = TempMsg + Msg
		#End

		#If CONSOLE_IMPLEMENTED
			If (DebugConsole <> Null) Then
				DebugConsole.WriteLine(Msg, True)
			Else
		#End

		Print(Msg)

		#If CONSOLE_IMPLEMENTED
			Endif
		#End
	#Else
		' This may change later:
		If (StopExecution) Then
			Msg = ErrorTemplate + Msg
		Endif
	#End

	If (StopExecution) Then
		#If CONFIG = "debug"
			#If DEBUG_PRINT_ON_ERROR
				DebugStop()
			#Else
				Error(Msg)
			#End
		#Else
			#If DEBUG_PRINT
				Local FinalStr:String = (ErrorTemplate + Quote + "Attempt to stop execution failed. (Reason: Release-mode)" + Quote)

				#If CONSOLE_IMPLEMENTED
					If (DebugConsole <> Null) Then
						DebugConsole.WriteLine(FinalStr, True)
					Else
				#End

				Print(FinalStr)

				#If CONSOLE_IMPLEMENTED
					Endif
				#End
			#Else
				Error(Msg)
			#End
		#End
	Endif

	#If CONFIG = "debug"
		' Nothing so far.
	#Else
		' Nothing so far.
	#End

	Return
End

Function DebugPrint:Void(Str:String, StopExecution:Bool=False)
	DebugError(Str, StopExecution)

	Return
End

' Classes:
Class GenericUtilities<T>
	' Constant variable(s):

	' Comparison response codes (Positive numbers are for error-reporting):
	Const COMPARE_RESPONSE_IDENTICAL:Int			= -1
	Const COMPARE_RESPONSE_WRONG_LENGTH:Int			= -2

	' Other:
	Const AUTO:Int = -1

	' Global variable(s):

	' For internal and extern use. That being said, please do not modify this value.
	Global NIL:T

	' Functions:
	Function AsString:String(Input:Bool)
		Return BoolToString(Input)
	End

	Function AsString:String(S:Stream, Length:Int=AUTO, Encoding:String="utf8")
		If (Length = AUTO) Then
			Length = S.Length()
		Endif

		Return S.ReadString(Length, Encoding)
	End

	Function AsString:String(Input:T[], Offset:Int=0, Length:Int=AUTO, AddSpaces:Bool=True)
		' Local variable(s):
		Local Output:String = LeftBracket

		' If no length was specified, use the array's length.
		If (Length = AUTO) Then
			Length = Input.Length()
		Endif

		Local VirtualLength:= Length+Offset

		For Local Index:Int = Offset Until VirtualLength
			Output += Input[Index]

			If (Index+1 < VirtualLength) Then
				Output += Comma

				If (AddSpaces) Then
					Output += Space
				Endif
			Endif
		Next

		Return Output + RightBracket
	End

	Function CopyStringToArray:T[](S:String, Input:T[], Offset:Int=0, Length:Int=AUTO)
		If (Length = AUTO) Then
			Length = Min(S.Length(), Input.Length())
		Endif

		For Local I:Int = Offset Until Length
			Input[I] = S[I]
		Next

		' Just for the sake of convenience, return the 'Input' array.
		Return Input
	End

	Function IndexOfList:T(L:List<T>, Index:Int=0)
		' Local variable(s):
		Local Data:list.Node<T> = L.FirstNode()

		For Local I:= 0 Until Index
			Data = Data.NextNode()

			' Not my favorite method, but it works:
			If (Data = Null) Then
				Return NIL
			Endif
		Next

		' Return the value of the assessed node.
		Return Data.Value()
	End

	Function Zero:T[](Input:T[])
		Return Nil(Input)
	End

	Function Nil:T[](Input:T[])
		For Local Index:= 0 Until Input.Length()
			Input[Index] = NIL
		Next

		Return Input
	End

	Function PrintArray:Void(Input:T[])
		Print(AsString(Input))

		Return
	End

	Function CopyArray:T[](Source:T[], Destination:T[])
		For Local Index:Int = 0 Until Min(Source.Length(), Destination.Length())
			Destination[Index] = Source[Index]
		Next

		Return Destination
	End

	Function CopyArray:T[](Source:T[])
		Return CloneArray(Source)
	End

	Function CloneArray:T[](Source:T[])
		Return CopyArray(Source, New T[Source.Length()])
	End

	' This command returns a positive number upon an error,
	' otherwise a 'response code' will be given (See the "Contant variable(s)" section for details).
	Function Compare:Int(A1:T[], A2:T[], CheckLength:Bool=True)
		' Local variable(s):
		Local A1_Length:= A1.Length
		Local A2_Length:= A2.Length

		' Check for errors:
		If (CheckLength) Then
			' Make sure they're both the same length.
			If (A1_Length <> A2_Length) Then
				Return COMPARE_RESPONSE_WRONG_LENGTH
			Endif
		Endif

		For Local I:= 0 Until Min(A1_Length, A2_Length)
			If (A1[I] <> A2[I]) Then
				' Not the best of escape methods, but it works.
				Return I
			Endif
		Next

		' Return the default response.
		Return COMPARE_RESPONSE_IDENTICAL
	End

	' This function returns positive if both arrays are identical.
	Function SimpleCompare:Bool(A1:T[], A2:T[], CheckLength:Bool=True)
		Return (Compare(A1, A2, CheckLength) = COMPARE_RESPONSE_IDENTICAL)
	End

	Function Read:T(S:Stream)
		Return Read(S, NIL)
	End

	Function Read:T[](S:Stream, DataArray:T[], Size:Int=AUTO)
		If (Size = AUTO) Then Size = S.ReadInt()

		If (Size > 0) Then
			If (DataArray.Length() = 0) Then
				DataArray = New T[Size]
			Endif
		Endif

		' Local variable(s):
		Local DLength:Int = Min(DataArray.Length, Size)

		For Local Index:= 0 Until DLength
			DataArray[Index] = Read(S, NIL)
		Next

		Return DataArray
	End

	Function Write:Void(S:Stream, DataArray:T[], WriteLength:Bool=True)
		' Local variable(s):
		Local DLength:= DataArray.Length()

		If (WriteLength) Then S.WriteInt(DLength)

		For Local Index:= 0 Until DLength
			Write(S, DataArray[Index])
		Next

		Return
	End

	Function Read:Int(S:Stream, Indentifier:Int, Size:Int=4)
		' Local variable(s):
		Local Data:Int = 0

		If (Size > 1) Then
			If (Size < 4) Then
				Data = S.ReadShort()
			Else
				If (Size > 4) Then
					S.ReadInt()
				Endif

				Data = S.ReadInt()
			Endif
		Else
			Data = S.ReadByte()
		Endif

		Return Data
	End

	Function Write:Void(S:Stream, Data:Int, Size:Int=4)
		Size = IntSize(Size)

		If (Size > 1) Then
			If (Size < 4) Then
				S.WriteShort(Data)
			Else
				If (Size > 4) Then
					S.WriteInt(0)
				Endif

				S.WriteInt(Data)
			Endif
		Else
			S.WriteByte(Data)
		Endif

		Return
	End

	#Rem
	Function Read:Bool(S:Stream, Identifier:Bool)
		Return Read(S, Int(Identifier), 1)
	End

	Function Write:Void(S:Stream, B:Bool)
		Write(S, Int(B), 1)

		Return
	End
	#End

	Function Read:Float(S:Stream, Identifier:Float)
		Return S.ReadFloat()
	End

	Function Write:Void(S:Stream, Data:Float)
		S.WriteFloat(Data)

		Return
	End

	Function Read:String(S:Stream, Data:String, Encoding:String="utf8", Size:Int=AUTO)
		If (Size = AUTO) Then Size = S.ReadInt()

		Data = S.ReadString(Size, Encoding)

		' Return the newly read string.
		Return Data
	End

	Function Write:Void(S:Stream, Data:String, Encoding:String="utf8", WriteSize:Bool=True)
		If (WriteSize) Then S.WriteInt(Data.Length())
		S.WriteString(Data, Encoding)

		Return
	End
End

Strict

Public

' Preprocessor related:
#Rem
#If TARGET = "glfw" Or TARGET = "sexy"
	#GLFW_TARGET = True
#End
#End

#If CONFIG = "debug"
	#DEBUG_PRINT = True
#Else
	#DEBUG_PRINT = False
#End

#DEBUG_PRINT_QUOTES = False

#READ_LINE_QUICKLY = True

#If HOST = "winnt"
	#WRITE_LINE_ENDTYPE = "CRLF"
#Else
	#WRITE_LINE_ENDTYPE = "LF"
#End

' Imports (Public):
Import autostream

Import byteorder
Import imagedimensions
Import retrostrings
Import stringutil
Import vector

#If CONSOLE_IMPLEMENTED
	Import console
#End

Import sizeof
Import time

#If Not BRL_GAMETARGET_IMPLEMENTED
	Import mojoemulator
#End

' Imports (Private):
Private

' Official:

' BRL:
Import brl.databuffer
Import brl.stream
Import brl.filepath

Public

' Constant variable(s):

' Ascii codes:
Const ASCII_CARRIAGE_RETURN:Int = 13
Const ASCII_LINE_FEED:Int = 10

' Used mainly for external code where '-9999...' isn't ever going to be valid.
' You should stick to 'Null' in most cases; try not to use this.
Const NOVAR:Int = -999999

Const ErrorTemplate:String = "[ERROR] {Debug}: " ' + Space
Const LogTemplate:String = "[Info] {Debug}: " ' + Space

' Global variable(s) (Public):
#If CONSOLE_IMPLEMENTED
	Global DebugConsole:Console = Null
#End

' A global stack logging randomization seeds.
' This is commonly accessed by the 'PushSeed' and 'PopSeed' functions.
Global SeedStack:Stack<Int> = Null

' Global variable(s) (Private):
Private

' Nothing so far.

Public

' Functions:
Function FLAG:Int(ID:Int)
	Return Pow(2, ID)
End

' This command is little-endian only:
Function AsByte:Int(I:Int)
	Return ((I Shl 24) Shr 24)
End

' This command treats data as 8-bit (Turns something like $F7, into $FFFFFFF7)
' This is only applied on little-endian systems,
' please use this command for hex values (Monkey converts hex on compile-time).
Function PaddedHex:Int(I:Int)
	If (Not BigEndian()) Then
		Return AsByte(I)
	Endif

	Return I
End

Function Transfer:Bool(InputStream:Stream, OutputStream:Stream, DataSize:Int)
	' Check for errors:
	If (DataSize = 0) Then Return False
	If (InputStream = Null Or InputStream.Eof()) Then Return False
	If (OutputStream = Null) Then Return False

	' Local variable(s):
	#If FLAG_TEMP_BUFFERS
		Local Data:DataBuffer = New DataBuffer(DataSize)
		Local DataLength:= DataSize ' Data.Length()

		' Read from the input-stream, then write to the output-stream:
		InputStream.Read(Data, 0, DataLength)
		OutputStream.Write(Data, 0, DataLength)

		Data.Discard()
	#Else
		'Local Data:Int[DataSize]

		For Local I:Int = 1 To DataSize ' Data.Length()
			OutputStream.WriteByte(InputStream.ReadByte())
		Next
	#End

	' Return the default response.
	Return True
End

Function Sq:Float(Input:Float)
	Return Pow(Input, 2.0) ' Input * Input
End

Function Sq:Int(Input:Int)
	Return Pow(Input, 2)
End

Function WrapAngle:Float(A:Float)
	While (A < 0.0)
		A += 360.0
	Wend

	While (A > 360.0)
		A -= 360.0
	Wend

	Return A
End

Function WrapColor:Float(C:Float)
	#Rem
	While (C < 0.0)
		C += 255.0
	Wend

	While (C > 255.0)
		C -= 255.0
	Wend
	#End

	Return C Mod 256.0
End

' This command is a helper function for
Function ProcessColorLocation:Int(Point:Int)
	' Check for errors:
	If (Point < 0) Then
		Return 0
	Endif

	Return (SizeOf_Integer_InBits-(SizeOf_Octet_InBits*(Point+1))) ' ((SizeOf_Integer_InBits-SizeOf_Octet_InBits)-(SizeOf_Octet_InBits*Point))
End

' NOTE: This command will produce incorrect color values without all characters present in the encode-string.
Function ColorToString:String(Pixel:Int, Encoding:String="ARGB")
	' Ensure the encoding is always described as uppercase.
	Encoding = Encoding.ToUpper()

	' Return the encoded color-string.
	Return ("R: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("R"))) & $000000FF) +
			", G: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("G"))) & $000000FF) +
			", B: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("B"))) & $000000FF) +
			", A: " + ((Pixel Shr ProcessColorLocation(Encoding.Find("A"))) & $000000FF))
End

Function PrintColor:Void(Pixel:Int, Encoding:String="ARGB")
	Print(ColorToString(Pixel, Encoding))

	Return
End

' This is just a wrapper for the main implementation:
Function PushSeed:Bool()
	Return PushSeed(Seed)
End

' The return value of this function specifies if the seed-stack already existed.
Function PushSeed:Bool(Seed:Int)
	' Local variable(s):
	Local Response:Bool = (SeedStack <> Null)

	If (Not Response) Then
		SeedStack = New IntStack()
	Endif

	SeedStack.Push(Seed)

	' Return the calculated response.
	Return Response
End

' This command retrieves a seed from the seed-stack,
' if no seed exists, the current randomization seed is returned.
Function PopSeed:Int()
	If (SeedStack <> Null) Then
		If (Not SeedStack.IsEmpty()) Then
			Return SeedStack.Pop()
		Endif
	Endif

	' Return the default response.
	Return Seed
End

' This command writes a standard line to a 'Stream'.
' This supports both 'LF', and 'CRLF' line endings currently, and this can be configured with 'WRITE_LINE_ENDTYPE'.
' Though character encoding is supported with this, only ASCII is supported by 'ReadLine'.
Function WriteLine:Bool(S:Stream, Line:String, CharacterEncoding:String="ascii")
	' Check for errors:
	If (S = Null) Then
		Return False
	Endif

	S.WriteString(Line, CharacterEncoding)

	#If WRITE_LINE_ENDTYPE = "CRLF"
		S.WriteByte(ASCII_CARRIAGE_RETURN)
	#End

	S.WriteByte(ASCII_LINE_FEED)

	' Return the default response.
	Return True
End

' This command reads a standard line from a 'Stream'. (This supports both 'LF', and 'CRLF' line endings currently)
' In addition, only 'ASCII' is currently supported.
Function ReadLine:String(S:Stream)
	' Local variable(s):
	Local Position:= S.Position()
	Local Padding:Int = 0 ' * SizeOf_Char
	Local Str:String

	#If Not READ_LINE_QUICKLY
		Local Size:Int = 0
	#End

	While (Not S.Eof())
		Local Char:= S.ReadByte()

		If (Char <> ASCII_LINE_FEED) Then
			If (Char <> ASCII_CARRIAGE_RETURN) Then
				#If READ_LINE_QUICKLY
					Str += String.FromChar(Char)
				#Else
					Size += 1
				#End
			Else
				Padding = 2*SizeOf_Char
			Endif
		Else
			#Rem
				If (Padding = 0) Then
					Padding = 1
				Endif
			#End

			Padding = Max(Padding, 1*SizeOf_Char)

			Exit
		Endif
	Wend

	#If Not READ_LINE_QUICKLY
		S.Seek(Position)

		Str = S.ReadString(Size, "ascii")

		S.Seek(S.Position()+Padding)
	#End

	' Return the string we read from the stream.
	Return Str
End

Function ResizeBuffer:DataBuffer(Buffer:DataBuffer, Size:Int, CopyData:Bool=True, DiscardOldBuffer:Bool=False)
	' Allocate a new data-buffer.
	Local B:= New DataBuffer(Size)

	' Copy the buffer's bytes over to 'B'.
	If (CopyData) Then
		Buffer.CopyBytes(0, B, 0, Buffer.Length())
	Endif

	If (DiscardOldBuffer) Then
		Buffer.Discard()
	Endif

	' Return the new buffer ('B').
	Return B
End

#If CONSOLE_IMPLEMENTED
	Function DebugBind:Bool(C:Console)
		If (C = Null) Then Return False

		DebugConsole = C

		' Return the default response.
		Return True
	End
#End

Function DebugError:Void(E:Throwable, StopExecution:Bool=True)
	#If CONFIG = "debug"
		DebugStop()
	#Else
		DebugPrint("Unknown exception has been thrown; continuing anyway.")
	#End

	Return
End

Function DebugError:Void(Msg:String, StopExecution:Bool=True)
	#If DEBUG_PRINT
		' Local variable(s):
		Local TempMsg:String

		If (StopExecution) Then
			TempMsg = ErrorTemplate
		Else
			TempMsg = LogTemplate
		Endif

		#If DEBUG_PRINT_QUOTES
			Msg = Tempmsg + Quote + Msg + Quote
		#Else
			Msg = TempMsg + Msg
		#End

		#If CONSOLE_IMPLEMENTED
			If (DebugConsole <> Null) Then
				DebugConsole.WriteLine(Msg, True)
			Else
		#End

		Print(Msg)

		#If CONSOLE_IMPLEMENTED
			Endif
		#End
	#Else
		' This may change later:
		If (StopExecution) Then
			Msg = ErrorTemplate + Msg
		Endif
	#End

	If (StopExecution) Then
		#If CONFIG = "debug"
			#If DEBUG_PRINT
				DebugStop()
			#Else
				Error(Msg)
			#End
		#Else
			#If DEBUG_PRINT
				Local FinalStr:String = (ErrorTemplate + Quote + "Attempt to stop execution failed. (Reason: Release-mode)" + Quote)

				#If CONSOLE_IMPLEMENTED
					If (DebugConsole <> Null) Then
						DebugConsole.WriteLine(FinalStr, True)
					Else
				#End

				Print(FinalStr)

				#If CONSOLE_IMPLEMENTED
					Endif
				#End
			#Else
				Error(Msg)
			#End
		#End
	Endif

	#If CONFIG = "debug"
		' Nothing so far.
	#Else
		' Nothing so far.
	#End

	Return
End

Function DebugPrint:Void(Str:String, StopExecution:Bool=False)
	DebugError(Str, StopExecution)

	Return
End

' Classes:
Class GenericUtilities<T>
	' Constant variable(s):

	' Comparison response codes (Positive numbers are for error-reporting):
	Const COMPARE_RESPONSE_IDENTICAL:Int			= -1
	Const COMPARE_RESPONSE_WRONG_LENGTH:Int			= -2

	' Other:
	Const AUTO:Int = -1

	' Global variable(s):

	' For internal and extern use. That being said, please do not modify this value.
	Global NIL:T

	' Functions:
	Function AsString:String(Input:Bool)
		Return BoolToString(Input)
	End

	Function AsString:String(S:Stream, Length:Int=AUTO, Encoding:String="utf8")
		If (Length = AUTO) Then
			Length = S.Length()
		Endif

		Return S.ReadString(Length, Encoding)
	End

	Function AsString:String(Input:T[], Offset:Int=0, Length:Int=AUTO, AddSpaces:Bool=True)
		' Local variable(s):
		Local Output:String = LeftBracket

		' If no length was specified, use the array's length.
		If (Length = AUTO) Then
			Length = Input.Length()
		Endif

		Local VirtualLength:= Length+Offset

		For Local Index:Int = Offset Until VirtualLength
			Output += Input[Index]

			If (Index+1 < VirtualLength) Then
				Output += Comma

				If (AddSpaces) Then
					Output += Space
				Endif
			Endif
		Next

		Return Output + RightBracket
	End

	Function CopyStringToArray:T[](S:String, Input:T[], Offset:Int=0, Length:Int=AUTO)
		If (Length = AUTO) Then
			Length = Min(S.Length(), Input.Length())
		Endif

		For Local I:Int = Offset Until Length
			Input[I] = S[I]
		Next

		' Just for the sake of convenience, return the 'Input' array.
		Return Input
	End

	Function IndexOfList:T(L:List<T>, Index:Int=0)
		' Local variable(s):
		Local Data:list.Node<T> = L.FirstNode()

		For Local I:= 0 Until Index
			Data = Data.NextNode()

			' Not my favorite method, but it works:
			If (Data = Null) Then
				Return NIL
			Endif
		Next

		' Return the value of the assessed node.
		Return Data.Value()
	End

	Function Zero:T[](Input:T[])
		Return Nil(Input)
	End

	Function Nil:T[](Input:T[])
		For Local Index:= 0 Until Input.Length()
			Input[Index] = NIL
		Next

		Return Input
	End

	Function PrintArray:Void(Input:T[])
		Print(AsString(Input))

		Return
	End

	Function CopyArray:T[](Source:T[], Destination:T[])
		For Local Index:Int = 0 Until Min(Source.Length(), Destination.Length())
			Destination[Index] = Source[Index]
		Next

		Return Destination
	End

	Function CopyArray:T[](Source:T[])
		Return CloneArray(Source)
	End

	Function CloneArray:T[](Source:T[])
		Return CopyArray(Source, New T[Source.Length()])
	End

	' This command returns a positive number upon an error,
	' otherwise a 'response code' will be given (See the "Contant variable(s)" section for details).
	Function Compare:Int(A1:T[], A2:T[], CheckLength:Bool=True)
		' Local variable(s):
		Local A1_Length:= A1.Length
		Local A2_Length:= A2.Length

		' Check for errors:
		If (CheckLength) Then
			' Make sure they're both the same length.
			If (A1_Length <> A2_Length) Then
				Return COMPARE_RESPONSE_WRONG_LENGTH
			Endif
		Endif

		For Local I:= 0 Until Min(A1_Length, A2_Length)
			If (A1[I] <> A2[I]) Then
				' Not the best of escape methods, but it works.
				Return I
			Endif
		Next

		' Return the default response.
		Return COMPARE_RESPONSE_IDENTICAL
	End

	' This function returns positive if both arrays are identical.
	Function SimpleCompare:Bool(A1:T[], A2:T[], CheckLength:Bool=True)
		Return (Compare(A1, A2, CheckLength) = COMPARE_RESPONSE_IDENTICAL)
	End

	Function Read:T(S:Stream)
		Return Read(S, NIL)
	End

	Function Read:T[](S:Stream, DataArray:T[], Size:Int=AUTO)
		If (Size = AUTO) Then Size = S.ReadInt()

		If (Size > 0) Then
			If (DataArray.Length() = 0) Then
				DataArray = New T[Size]
			Endif
		Endif

		' Local variable(s):
		Local DLength:Int = Min(DataArray.Length, Size)

		For Local Index:= 0 Until DLength
			DataArray[Index] = Read(S, NIL)
		Next

		Return DataArray
	End

	Function Write:Void(S:Stream, DataArray:T[], WriteLength:Bool=True)
		' Local variable(s):
		Local DLength:= DataArray.Length()

		If (WriteLength) Then S.WriteInt(DLength)

		For Local Index:= 0 Until DLength
			Write(S, DataArray[Index])
		Next

		Return
	End

	Function Read:Int(S:Stream, Indentifier:Int, Size:Int=4)
		' Local variable(s):
		Local Data:Int = 0

		If (Size > 1) Then
			If (Size < 4) Then
				Data = S.ReadShort()
			Else
				If (Size > 4) Then
					S.ReadInt()
				Endif

				Data = S.ReadInt()
			Endif
		Else
			Data = S.ReadByte()
		Endif

		Return Data
	End

	Function Write:Void(S:Stream, Data:Int, Size:Int=4)
		Size = IntSize(Size)

		If (Size > 1) Then
			If (Size < 4) Then
				S.WriteShort(Data)
			Else
				If (Size > 4) Then
					S.WriteInt(0)
				Endif

				S.WriteInt(Data)
			Endif
		Else
			S.WriteByte(Data)
		Endif

		Return
	End

	#Rem
	Function Read:Bool(S:Stream, Identifier:Bool)
		Return Read(S, Int(Identifier), 1)
	End

	Function Write:Void(S:Stream, B:Bool)
		Write(S, Int(B), 1)

		Return
	End
	#End

	Function Read:Float(S:Stream, Identifier:Float)
		Return S.ReadFloat()
	End

	Function Write:Void(S:Stream, Data:Float)
		S.WriteFloat(Data)

		Return
	End

	Function Read:String(S:Stream, Data:String, Encoding:String="utf8", Size:Int=AUTO)
		If (Size = AUTO) Then Size = S.ReadInt()

		Data = S.ReadString(Size, Encoding)

		' Return the newly read string.
		Return Data
	End

	Function Write:Void(S:Stream, Data:String, Encoding:String="utf8", WriteSize:Bool=True)
		If (WriteSize) Then S.WriteInt(Data.Length())
		S.WriteString(Data, Encoding)

		Return
	End
End


Fixed a couple of stupid storyboard bugs.

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

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import diddy.functions
Import diddy.containers
Import diddy.xml
Import diddy.format

Public
Import diddy.exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 7

Class Storyboard
Private
	Field sprites:DiddyStack<StoryboardSprite> = New DiddyStack<StoryboardSprite>
	Field sounds:DiddyStack<StoryboardSound> = New DiddyStack<StoryboardSound>
	Field musics:DiddyStack<StoryboardMusic> = New DiddyStack<StoryboardMusic>
	Field effects:DiddyStack<StoryboardEffect> = New DiddyStack<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String="storyboard.xml")
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile(filename)
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Push(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Push(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Push(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Push(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:DiddyStack<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:DiddyStack<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:DiddyStack<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:DiddyStack<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Count()
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(Self, currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Count()
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(Self, currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Count()
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(Self, currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Count()
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(Self, currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Count()
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Count()
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

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

	Method New()
		id = nextId
		nextId += 1
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:DiddyStack<StoryboardSpriteKeyframe> = New DiddyStack<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Push(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.CompareTo(other)
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Count()
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]

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

' summary: Diddy Storyboarding Module
' This module lets you create a storyboard consisting of sprites and sounds.  Sprites can have
' timed transformations applied to their position, scale, alpha, rotation, and colour.  These
' values can optionally be tweened with an easing function.

Strict

Private
Import mojo
Import diddy.functions
Import diddy.containers
Import diddy.xml
Import diddy.format

Public
Import diddy.exception

Const KEYFRAME_POSITION:Int = 0
Const KEYFRAME_ALPHA:Int = 1
Const KEYFRAME_SCALE:Int = 2
Const KEYFRAME_SCALE_VECTOR:Int = 3
Const KEYFRAME_ROTATION:Int = 4
Const KEYFRAME_COLOR:Int = 5
Const KEYFRAME_HANDLE:Int = 6
Const KEYFRAME_COUNT:Int = 6

Class Storyboard
Private
	Field sprites:DiddyStack<StoryboardSprite> = New DiddyStack<StoryboardSprite>
	Field sounds:DiddyStack<StoryboardSound> = New DiddyStack<StoryboardSound>
	Field musics:DiddyStack<StoryboardMusic> = New DiddyStack<StoryboardMusic>
	Field effects:DiddyStack<StoryboardEffect> = New DiddyStack<StoryboardEffect>
	Field debugMode:Bool = False
	Field name:String
	Field width:Float
	Field height:Float
	Field length:Int
	Field currentTime:Int
	Field playing:Bool = False
	Field playSpeed:Int
	Field renderer:StoryboardRenderer

Public
	Function LoadXML:Storyboard(filename:String)
		' open the xml file
		Local parser:XMLParser = New XMLParser
		Local doc:XMLDocument = parser.ParseFile("storyboard.xml")
		Local root:XMLElement = doc.Root

		' create a new storyboard
		Local sb:Storyboard = New Storyboard

		' read the attributes from the root node
		sb.name = root.GetAttribute("name","")
		sb.width = Float(root.GetAttribute("width","640"))
		sb.height = Float(root.GetAttribute("height","480"))
		sb.length = Int(root.GetAttribute("length","0"))

		' loop on each child
		For Local node:XMLElement = Eachin root.Children
			' sprites node
			If node.Name = "sprites" Then
				' layers
				For Local layerNode:XMLElement = EachIn node.Children
					' if it's not layer, there's something wrong!
					If layerNode.Name = "layer" Then
						' get the layer index
						Local index:Int = Int(node.GetAttribute("index","0"))
						' loop on every sprite in the layer
						For Local spriteNode:XMLElement = EachIn layerNode.Children
							If spriteNode.Name = "sprite" Then
								' create a new sprite and add it
								Local sprite:StoryboardSprite = New StoryboardSprite(spriteNode)
								sprite.layer = index
								sb.sprites.Push(sprite)
							End
						Next
					End
				Next
			' sounds node
			ElseIf node.Name = "sounds" Then
				' loop on children
				For Local soundNode:XMLElement = EachIn node.Children
					If soundNode.Name = "sound" Then
						' create a new sound and add it
						sb.sounds.Push(New StoryboardSound(soundNode))
					ElseIf soundNode.Name = "music" Then
						' create a new music and add it
						sb.musics.Push(New StoryboardMusic(soundNode))
					End
				Next
			' effects node
			ElseIf node.Name = "effects" Then
				' loop on children
				For Local effectNode:XMLElement = EachIn node.Children
					If effectNode.Name = "flash" Then
						sb.effects.Push(New StoryboardFlash(effectNode))
					End
				Next
			End
		Next

		' sort the sprites by layer
		sb.sprites.Sort()

		' we're done!
		Return sb
	End

	' Allows the developer to use a custom renderer without extending Storyboard
	Method Renderer:StoryboardRenderer() Property
		If Not renderer Then renderer = New StoryboardRenderer
		Return renderer
	End

	Method Renderer:Void(renderer:StoryboardRenderer) Property Self.renderer = renderer End

	' The sprite list (read only)
	Method Sprites:DiddyStack<StoryboardSprite>() Property Return sprites End

	' The effect list (read only)
	Method Effects:DiddyStack<StoryboardEffect>() Property Return effects End

	' The sound list (read only)
	Method Sounds:DiddyStack<StoryboardSound>() Property Return sounds End

	' The music list (read only)
	Method Musics:DiddyStack<StoryboardMusic>() Property Return musics End

	' DebugMode enables the timeline and time counter
	Method DebugMode:Bool() Property Return debugMode End
	Method DebugMode:Void(debugMode:Bool) Property Self.debugMode = debugMode End

	' The storyboard's name is mostly for the developer's use
	Method Name:String() Property Return name End
	Method Name:Void(name:String) Property Self.name = name End

	' The native width/height of the storyboard, before scaling
	Method Width:Float() Property Return width End
	Method Height:Float() Property Return height End

	' The length of the storyboard in milliseconds
	Method Length:Int() Property Return length End

	' The current time in millis
	Method CurrentTime:Int() Property Return currentTime End

	' The current play speed (negative values play in reverse)
	Method PlaySpeed:Int() Property Return playSpeed End
	Method PlaySpeed:Void(playSpeed:Int) Property Self.playSpeed = playSpeed End

	' Starts or resumes playback from the current position
	Method Play:Void()
		If Not playing Then
			If playSpeed = 0 Then playSpeed = 1
			playing = True
			StoryboardMusic.musicPlaying = False
			StopMusic()
		End
	End

	' Starts or pauses playback (toggle)
	Method PlayPause:Void()
		If playing Then
			Pause()
		Else
			Play()
		End
	End

	' Pauses playback
	Method Pause:Void()
		playing = False
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Stops playback, and rewinds to the start
	Method Stop:Void()
		playing = False
		playSpeed = 1
		currentTime = 0
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Jumps to the specified time
	Method SeekTo:Void(time:Int)
		currentTime = time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Adds the specified offset (negative to jump backward)
	Method SeekForward:Void(time:Int)
		currentTime += time
		StoryboardMusic.musicPlaying = False
		StopMusic()
	End

	' Updates all the transformations, increasing the current play time based on dt.frametime and the play speed.
	Method Update:Void(updateTime:Bool=True)
		' update the current time based on the millis since the last frame and the play speed, if we should
		If updateTime And playing Then Self.currentTime += dt.frametime * playSpeed

		' update the current values for each sprite
		For Local i:Int = 0 Until sprites.Count()
			Local sprite:StoryboardSprite = sprites.Get(i)
			sprite.Update(Self, currentTime)
		Next

		' only do sounds etc. if playing
		If playing And updateTime Then
			' update sounds
			For Local i:Int = 0 Until sounds.Count()
				Local sound:StoryboardSound = sounds.Get(i)
				sound.Update(Self, currentTime)
			Next

			' update music
			For Local i:Int = 0 Until musics.Count()
				Local music:StoryboardMusic = musics.Get(i)
				music.Update(Self, currentTime)
			Next

			' need to seek music here
			If StoryboardMusic.musicPlaying And StoryboardMusic.performSeek Then
#If TARGET<>"bmax" Then
				StoryboardMusic.performSeek = Not SeekMusic(StoryboardMusic.performSeekTime)
#End
			End
		End

		' update effects
		For Local i:Int = 0 Until effects.Count()
			Local effect:StoryboardEffect = effects.Get(i)
			effect.Update(Self, currentTime)
		Next
	End

	Method Render:Void(x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		Renderer.Render(Self, x, y, width, height)
	End
End

Class StoryboardRenderer
	Field mtx:Float[] = New Float[6]
	' Renders all the sprites within the specified area, scaling for aspect ratio and setting a scissor.
	Method Render:Void(sb:Storyboard, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' if width/height not supplied, assume native storyboard width
		If width <= 0 Then width = sb.Width
		If height <= 0 Then height = sb.Height

		' get the aspect ratios
		Local targetAR:Float = width/height
		Local sourceAR:Float = sb.Width/sb.Height

		' fix target width/height based on aspect ratio (letterboxed)
		If targetAR > sourceAR Then
			' bars on left/right
			x += (width-(height*sourceAR))/2
			width = height*sourceAR
		ElseIf targetAR < sourceAR Then
			y += (height-(width/sourceAR))/2
			height = width/sourceAR
		End

		' push the old matrix and apply correct translation and scales
		PushMatrix
		Translate x, y
		Scale width/sb.Width, height/sb.Height

		' get the current matrix so we can work out the correct scissor
		GetMatrix(mtx)
		Local sx:Float = mtx[4]
		Local sy:Float = mtx[5]
		Local sw:Float = sb.Width*mtx[0]+sb.Height*mtx[2]
		Local sh:Float = sb.Width*mtx[1]+sb.Height*mtx[3]

		' set the scissor
		SetScissor(sx, sy, sw, sh)

		' render all the sprites
		For Local i:Int = 0 Until sb.Sprites.Count()
			Local sprite:StoryboardSprite = sb.Sprites.Get(i)
			sprite.Render(sb, Self, 0, 0, width, height)
		Next

		' render effects
		For Local i:Int = 0 Until sb.Effects.Count()
			Local effect:StoryboardEffect = sb.Effects.Get(i)
			effect.Render(sb, Self, 0, 0, width, height)
		Next

		' reset scissor
		SetScissor(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)

		' draw lines if in debug
		If sb.DebugMode Then
			SetAlpha(1)
			SetColor(255,255,255)
			DrawLine(0,0,sb.Width,0)
			DrawLine(sb.Width,0,sb.Width,sb.Height)
			DrawLine(0,0,0,sb.Height)
			DrawLine(0,sb.Height,sb.Width,sb.Height)
		End

		' pop the matrix
		PopMatrix

		' draw time and timeline if debug
		If sb.DebugMode Then
			' draw time
			Local millis:Int = sb.CurrentTime Mod 1000
			Local secs:Int = (sb.CurrentTime / 1000) Mod 60
			Local mins:Int = (sb.CurrentTime / 60000)
			SetAlpha(1)
			SetColor(255,255,255)
			DrawText(Format("%02d:%02d:%03d", mins, secs, millis), 0, 0)

			' draw timeline bar
			DrawLine 10,SCREEN_HEIGHT-10,10,SCREEN_HEIGHT-30
			DrawLine SCREEN_WIDTH-10,SCREEN_HEIGHT-10,SCREEN_WIDTH-10,SCREEN_HEIGHT-30
			DrawLine 10,SCREEN_HEIGHT-20,SCREEN_WIDTH-10,SCREEN_HEIGHT-20

			' draw timeline ticks
			For Local i:Int = 0 Until sb.Length Step 5000
				Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(i)/sb.Length)
				Local y:Int = SCREEN_HEIGHT-22
				If i Mod 30000 = 0 Then y -= 2
				If i Mod 60000 = 0 Then y -= 2
				DrawLine(x, y, x, SCREEN_HEIGHT-20)
			Next

			' draw current time bar
			Local x:Int = 10+Int((SCREEN_WIDTH-20)*Float(sb.CurrentTime)/sb.Length)
			SetColor 255,0,0
			DrawLine x,SCREEN_HEIGHT-28,x,SCREEN_HEIGHT-12
			SetColor 255,255,255
		End
	End

	Method PreRenderSprite:Bool(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderSprite:Void(sb:Storyboard, sprite:StoryboardSprite, x:Int, y:Int, width:Int, height:Int)
	End

	Method PreRenderEffect:Bool(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
		Return True
	End

	Method PostRenderEffect:Void(sb:Storyboard, effect:StoryboardEffect, x:Int, y:Int, width:Int, height:Int)
	End
End

Class StoryboardElement Implements IComparable Abstract
Private
	Global nextId:Int = 0
	Field id:Int
	Field currentTime:Int
	Field name:String

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

	Method New()
		id = nextId
		nextId += 1
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardElement = StoryboardElement(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If id < o.id Then Return -1
		If id > o.id Then Return 1
		Return 0
	End

	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1) Abstract
End

Class StoryboardSound Extends StoryboardElement
Private
	Const SOUND_THRESHOLD:Int = 100
	Field soundName:String
	Field sound:GameSound
	Field time:Int

Public
	Method New(node:XMLElement)
		soundName = node.GetAttribute("soundName","")
		time = Int(node.GetAttribute("time","0"))
		name = node.GetAttribute("name","")
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' if we've gone past this sound, check the delta to make sure it's not huge (to stop spam)
		Local shouldPlay:Bool = currentTime >= time And Self.currentTime < time And currentTime - time < SOUND_THRESHOLD
		Self.currentTime = currentTime
		If shouldPlay Then
			If Not sound Then sound = diddyGame.sounds.Find(soundName)
			If Not sound Then Return
			sound.Play()
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render a sound.... yet ;)
	End
End

Class StoryboardMusic Extends StoryboardElement
Private
	Global musicPlaying:Bool = False
	Global performSeek:Bool = False
	Global performSeekTime:Int = 0
	Const MUSIC_THRESHOLD:Int = 50
	Field musicName:String
	Field time:Int
	Field length:Int
	Field channel:Int
	Field loop:Bool = False

Public
	Method New(node:XMLElement)
		musicName = node.GetAttribute("musicName","")
		time = Int(node.GetAttribute("time","0"))
		length = Int(node.GetAttribute("length","0"))
		name = node.GetAttribute("name","")
		loop = node.GetAttribute("loop","true").ToLower() = "true"
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		' find out if we're inside the music
		If currentTime >= Self.time And (currentTime < Self.time+Self.length Or Self.loop) Then
			' if we're not playing, need to start playing
			If Not musicPlaying Then
				musicPlaying = True
				StopMusic()
				performSeekTime = currentTime-Self.time
				If loop Then
					diddyGame.MusicPlay(musicName, 1)
					performSeekTime = performSeekTime Mod Self.length
				Else
					diddyGame.MusicPlay(musicName, 0)
				End
				performSeek = performSeekTime >= MUSIC_THRESHOLD
			End
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		' can't render music.... yet ;)
	End
End

Class StoryboardSprite Extends StoryboardElement
Private
	Field layer:Int
	Field imageName:String
	Field image:GameImage

	' these are read from the <sprite> tag and are used as the first keyframe
	Field firstX:Float=0, firstY:Float=0
	field firstScaleX:Float=1, firstScaleY:Float=1
	Field firstScale:Float=1, firstRotation:Float=0
	Field firstRed:Float=255, firstGreen:Float=255, firstBlue:Float=255, firstAlpha:Float=1

	' these are the values at the current time
	Field x:Float=0, y:Float=0
	field scaleX:Float=1, scaleY:Float=1
	Field scale:Float=1, rotation:Float=0
	Field red:Float=255, green:Float=255, blue:Float=255, alpha:Float=1

	Field keyframes:DiddyStack<StoryboardSpriteKeyframe> = New DiddyStack<StoryboardSpriteKeyframe>
	Field previousKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]
	Field nextKeyframes:StoryboardSpriteKeyframe[] = New StoryboardSpriteKeyframe[KEYFRAME_COUNT]

Public
	Method SpriteImage:GameImage() Property
		If Not image Then image = diddyGame.images.Find(imageName)
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return Null
		End
		Return image
	End

	Method X:Float() Property Return x End
	Method Y:Float() Property Return y End

	Method Width:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Width()
		Return 0
	End

	Method Height:Float() Property
		Local image:GameImage = SpriteImage
		If image Then Return image.image.Height()
		Return 0
	End

	Method New(node:XMLElement)
		imageName = node.GetAttribute("imageName","")
		name = node.GetAttribute("name","")
		firstX = Float(node.GetAttribute("x","0"))
		firstY = Float(node.GetAttribute("y","0"))
		firstScaleX = Float(node.GetAttribute("scaleX","1"))
		firstScaleY = Float(node.GetAttribute("scaleY","1"))
		firstScale = Float(node.GetAttribute("scale","1"))
		firstRotation = Float(node.GetAttribute("rotation","0"))
		firstRed = Float(node.GetAttribute("red","255"))
		firstGreen = Float(node.GetAttribute("green","255"))
		firstBlue = Float(node.GetAttribute("blue","255"))
		firstAlpha = Float(node.GetAttribute("alpha","0"))
		CreateKeyframesFromXML(node)
		keyframes.Sort()
	End

	Method CreateKeyframesFromXML:Void(node:XMLElement, timeOffset:Int=0)
		For Local childNode:XMLElement = EachIn node.Children
			Local name:String = childNode.Name
			If name = "group" Then
				Local loopCount:Int = Int(childNode.GetAttribute("loopCount","1"))
				Local time:Int = Int(childNode.GetAttribute("time","0"))
				Local length:Int = Int(childNode.GetAttribute("length","0"))
				Local myOffset:Int = timeOffset + time
				For Local i:Int = 0 Until loopCount
					CreateKeyframesFromXML(childNode, myOffset)
					myOffset += length
				Next
			Else
				keyframes.Push(New StoryboardSpriteKeyframe(childNode, timeOffset))
			End
		Next
	End

	Method Layer:Int() Property Return layer End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSprite = StoryboardSprite(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If layer < o.layer Then Return -1
		If layer > o.layer Then Return 1
		Return Super.CompareTo(other)
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Self.currentTime = currentTime
		x = firstX; y = firstY
		scaleX = firstScaleX; scaleY = firstScaleY
		scale = firstScale; rotation = firstRotation
		red = firstRed; green = firstGreen; blue = firstBlue; alpha = firstAlpha

		' clear the keyframe arrays
		For Local i:Int = 0 Until previousKeyframes.Length
			previousKeyframes[i] = Null
			nextKeyframes[i] = Null
		Next

		' find the keyframes either side of the current time
		For Local i:Int = 0 Until keyframes.Count()
			Local kf:StoryboardSpriteKeyframe = keyframes.Get(i)
			' if we've already found the next one, skip it
			If nextKeyframes[kf.keyframeType] Then Continue
			' set the next keyframe if we should
			If currentTime <= kf.time Then
				nextKeyframes[kf.keyframeType] = kf
			Else
				previousKeyframes[kf.keyframeType] = kf
			End
		Next

		' loop through each of the keyframe types
		For Local i:Int = 0 Until previousKeyframes.Length
			Local prevKF:StoryboardSpriteKeyframe = previousKeyframes[i]
			Local nextKF:StoryboardSpriteKeyframe = nextKeyframes[i]
			' if we have no previous, we do nothing (can't tween from the start, yet)
			If Not prevKF Then Continue
			' if we have no next, or the next doesn't tween, we just use the previous
			If Not nextKF Or Not nextKF.Tween Then nextKF = prevKF
			' interp and apply
			nextKF.Apply(Self, prevKF, currentTime)
		Next
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If alpha = 0 Then Return
		If Not renderer Then renderer = sb.Renderer
		Local image:GameImage = SpriteImage
		If Not image Then
			Print "Couldn't load "+imageName+" for sprite."
			Return
		End
		' translation, scale, rotation, handle, other effects
		PushMatrix
		Translate Self.x, Self.y
		Scale scaleX, scaleY
		Scale scale, scale
		Rotate rotation
		SetColor red, green, blue
		SetAlpha alpha
		If renderer.PreRenderSprite(sb, Self, x, y, width, height) Then
			image.Draw(0, 0)
			renderer.PostRenderSprite(sb, Self, x, y, width, height)
		End
		PopMatrix
	End
End

Class StoryboardSpriteKeyframe Implements IComparable
Private
	Field keyframeType:Int
	Field values:Float[]
	Field tween:Bool = True
	Field ease:Int = EASE_NONE
	Field time:Int

Public
	Method KeyframeType:Int() Property Return keyframeType End
	Method KeyframeType:Void(keyframeType:Int) Property
		Init(keyframeType)
	End

	Method Tween:Bool() Property Return tween End
	Method Tween:Void(tween:Bool) Self.tween = tween End

	Method Ease:Int() Property Return ease End
	Method Ease:Void(ease:Int) Self.ease = ease End

	Method Time:Int() Property Return time End
	Method Time:Void(time:Int) Property Self.time = time End

	Method X:Float() Property Return values[0] End
	Method X:Void(val:Float) Property values[0] = val End
	Method Y:Float() Property Return values[1] End
	Method Y:Void(val:Float) Property values[1] = val End

	Method ScaleX:Float() Property Return values[0] End
	Method ScaleX:Void(val:Float) Property values[0] = val End
	Method ScaleY:Float() Property Return values[1] End
	Method ScaleY:Void(val:Float) Property values[1] = val End

	Method Scale:Float() Property Return values[0] End
	Method Scale:Void(val:Float) Property values[0] = val End

	Method Rotation:Float() Property Return values[0] End
	Method Rotation:Void(val:Float) Property values[0] = val End

	Method Alpha:Float() Property Return values[0] End
	Method Alpha:Void(val:Float) Property values[0] = val End

	Method Hue:Float() Property Return values[0] End
	Method Hue:Void(val:Float) Property values[0] = val End
	Method Saturation:Float() Property Return values[1] End
	Method Saturation:Void(val:Float) Property values[1] = val End
	Method Luminance:Float() Property Return values[2] End
	Method Luminance:Void(val:Float) Property values[2] = val End

	' Constructor for manually building a storyboard
	Method New(keyframeType:Int)
		Init(keyframeType)
	End

	' Constructor for reading from xml
	Method New(node:XMLElement, timeOffset:Int=0)
		Local name:String = node.Name
		Self.tween = node.GetAttribute("tween","false").ToLower() = "true"
		Self.time = Int(node.GetAttribute("time","0"))+timeOffset

		Local easeStr:String = node.GetAttribute("ease","")
		Local easeType:Int = EASE_NONE
		Select easeStr.ToLower()
			Case "in", ""+EASE_IN
				Self.ease = EASE_IN
			Case "inhalf", ""+EASE_IN_HALF
				Self.ease = EASE_IN_HALF
			Case "indouble", ""+EASE_IN_DOUBLE
				Self.ease = EASE_IN_DOUBLE
			Case "out", ""+EASE_OUT
				Self.ease = EASE_OUT
			Case "outhalf", ""+EASE_OUT_HALF
				Self.ease = EASE_OUT_HALF
			Case "outdouble", ""+EASE_OUT_DOUBLE
				Self.ease = EASE_OUT_DOUBLE
			Case "inout", ""+EASE_IN_OUT
				Self.ease = EASE_IN_OUT
			Default
				Self.ease = EASE_NONE
		End

		If name = "scale" Then
			Local scale:Float = Float(node.GetAttribute("scale","1"))
			Init(KEYFRAME_SCALE, scale)
		ElseIf name = "scaleVector" Then
			Local scaleX:Float = Float(node.GetAttribute("scaleX","1"))
			Local scaleY:Float = Float(node.GetAttribute("scaleY","1"))
			Init(KEYFRAME_SCALE_VECTOR, scaleX, scaleY)
		ElseIf name = "alpha" Then
			Local alpha:Float = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
			Init(KEYFRAME_ALPHA, alpha)
		ElseIf name = "rotation" Then
			Local rotation:Float = Float(node.GetAttribute("rotation","0"))
			Init(KEYFRAME_ROTATION, rotation)
		ElseIf name = "position" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_POSITION, x, y)
		ElseIf name = "handle" Then
			Local x:Float = Float(node.GetAttribute("x","0"))
			Local y:Float = Float(node.GetAttribute("y","0"))
			Init(KEYFRAME_HANDLE, x, y)
		ElseIf name = "color" Then
			If node.HasAttribute("red") Or node.HasAttribute("green") Or node.HasAttribute("blue") Then
				Local red:Float = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
				Local green:Float = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
				Local blue:Float = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
				RGBtoHSL(red, green, blue, hslArray)
			Else
				hslArray[0] = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
				hslArray[1] = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
				hslArray[2] = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			End
			Init(KEYFRAME_COLOR, hslArray[0], hslArray[1], hslArray[2])
		End
	End

	Method Apply:Void(sprite:StoryboardSprite, prevKF:StoryboardSpriteKeyframe, currentTime:Int)
		Local progress:Float = 0
		If Self.time >= prevKF.time Then progress = Float(currentTime-prevKF.time)/(Self.time-prevKF.time)
		Select keyframeType
			Case KEYFRAME_ALPHA
				sprite.alpha = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_SCALE
				sprite.scale = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_ROTATION
				sprite.rotation = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
			Case KEYFRAME_POSITION
				sprite.x = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.y = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_SCALE_VECTOR
				sprite.scaleX = InterpolateWithEase(prevKF.values[0], values[0], progress, ease)
				sprite.scaleY = InterpolateWithEase(prevKF.values[1], values[1], progress, ease)
			Case KEYFRAME_COLOR
				HSLtoRGB(
					InterpolateWithEase(prevKF.values[0], values[0], progress, ease),
					InterpolateWithEase(prevKF.values[1], values[1], progress, ease),
					InterpolateWithEase(prevKF.values[2], values[2], progress, ease), rgbArray)
				sprite.red = rgbArray[0]
				sprite.green = rgbArray[1]
				sprite.blue = rgbArray[2]
			Case KEYFRAME_HANDLE
				If sprite And sprite.image And sprite.image.image Then
					sprite.image.image.SetHandle(InterpolateWithEase(prevKF.values[0], values[0], progress, ease), InterpolateWithEase(prevKF.values[1], values[1], progress, ease))
				End
		End
	End

	Method CompareTo:Int(other:Object)
		Local o:StoryboardSpriteKeyframe = StoryboardSpriteKeyframe(other)
		If Not o Then Return -1
		If o = Self Then Return 0
		If time < o.time Then Return -1
		If time > o.time Then Return 1
		If keyframeType < o.keyframeType Then Return -1
		If keyframeType > o.keyframeType Then Return 1
		Return 0
	End

Private
	Method Init:Void(keyframeType:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		Self.keyframeType = keyframeType
		Select keyframeType
			Case KEYFRAME_ROTATION, KEYFRAME_SCALE, KEYFRAME_ALPHA
				InitArray(1, value1)
			Case KEYFRAME_POSITION, KEYFRAME_SCALE_VECTOR
				InitArray(2, value1, value2)
			Case KEYFRAME_COLOR
				InitArray(3, value1, value2, value3)
		End
	End

	Method InitArray:Void(elementCount:Int, value1:Float=0, value2:Float=0, value3:Float=0, value4:Float=0, value5:Float=0)
		values = New Float[elementCount]
		If elementCount >= 1 Then values[0] = value1
		If elementCount >= 2 Then values[1] = value2
		If elementCount >= 3 Then values[2] = value3
		If elementCount >= 4 Then values[3] = value4
		If elementCount >= 5 Then values[4] = value5
	End
End

Class StoryboardEffect Abstract
Public
	Method Update:Void(sb:Storyboard, currentTime:Int) Abstract
	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float, y:Float, width:Float, height:Float) Abstract
End

Class StoryboardFlash Extends StoryboardEffect
Private
	Field leadIn:Int ' typically short, ~50ms
	Field leadOut:Int ' typically long, ~300ms
	Field time:Int ' the time of peak alpha
	Field alpha:Float ' the peak alpha
	Field red:Int
	Field green:Int
	Field blue:Int

	Field currentAlpha:Float

Public
	Method New(node:XMLElement, timeOffset:Int=0)
		time = Int(node.GetAttribute("time","0"))
		leadIn = Int(node.GetAttribute("leadIn","0"))
		leadOut = Int(node.GetAttribute("leadOut","0"))
		alpha = Clamp(Float(node.GetAttribute("alpha","1")),0.0,1.0)
		If node.HasAttribute("hue") Or node.HasAttribute("saturation") Or node.HasAttribute("luminance") Then
			Local hue:Float = Clamp(Float(node.GetAttribute("hue","0")),0.0,1.0)
			Local saturation:Float = Clamp(Float(node.GetAttribute("saturation","0")),0.0,1.0)
			Local luminance:Float = Clamp(Float(node.GetAttribute("luminance","0")),0.0,1.0)
			HSLtoRGB(hue, saturation, luminance, rgbArray)
			red = rgbArray[0]; green = rgbArray[1]; blue = rgbArray[2]
		Else
			red = Clamp(Float(node.GetAttribute("red","255")),0.0,255.0)
			green = Clamp(Float(node.GetAttribute("green","255")),0.0,255.0)
			blue = Clamp(Float(node.GetAttribute("blue","255")),0.0,255.0)
		End
	End

	Method Update:Void(sb:Storyboard, currentTime:Int)
		Local leadInTime:Int = time - leadIn, leadOutTime:Int = time + leadOut
		If currentTime < leadInTime Or currentTime > leadOutTime Then
			currentAlpha = 0
		ElseIf currentTime < time Then
			currentAlpha = Lerp(0, alpha, Float(currentTime-leadInTime)/leadIn)
		ElseIf currentTime > time Then
			currentAlpha = Lerp(alpha, 0, Float(currentTime-time)/leadOut)
		Else
			currentAlpha = alpha
		End
	End

	Method Render:Void(sb:Storyboard, renderer:StoryboardRenderer=Null, x:Float=0, y:Float=0, width:Float=-1, height:Float=-1)
		If Not renderer Then renderer = sb.Renderer
		If renderer.PreRenderEffect(sb, Self, x, y, width, height) And currentAlpha <> 0 Then
			SetAlpha(currentAlpha)
			SetColor(red, green, blue)
			DrawRect(x, y, width, height)
			renderer.PostRenderEffect(sb, Self, x, y, width, height)
		End
	End
End

Private
Global hslArray:Float[] = New Float[3]
Global rgbArray:Int[] = New Int[3]


Don't share Sprite instances through SpriteAtlas

* Always return a new instance in Get()
* ObjectEnumerator removed - use GetNames() and Get()

Strict

Private

Import bono
Import bono.vendor.skn3.xml
Import mojo.app

Public

Class SpriteAtlas
    Private

    Field atlasSprite:Sprite
    Field childSprites:StringMap<Sprite> = New StringMap<Sprite>()
    Field xml:XMLDoc

    Public

    Method New(filename:String)
        LoadXml(filename)
        CheckXml()

        LoadAtlasSprite()
        LoadSprites()

        FreeXmlRecursive(xml)
        xml = Null
    End

    Method Get:Sprite(name:String)
        If Not childSprites.Contains(name)
            Throw New InvalidArgumentException(
                "There is no sprite named: " + name)
        End

        Return childSprites.Get(name).Copy()
    End

    Method GetNames:MapKeys<String, Sprite>()
        Return childSprites.Keys()
    End

    Method Count:Int()
        Return childSprites.Count()
    End

    Method IsEmpty:Bool()
        Return childSprites.IsEmpty()
    End

    Method Contains:Bool(name:String)
        Return childSprites.Contains(name)
    End

    Private

    Method LoadXml:Void(filename:String)
        Local content:String = LoadString(filename)
        If Not content Or content = ""
            Throw New RuntimeException(
                "Unable to load definition from file: " + filename)
        End

        Local error:XMLError = New XMLError
        xml = ParseXML(content, error)

        If Not xml Or error.error
            Throw New RuntimeException(
                "Unable to parse XML: " + error)
        End
    End

    Method CheckXml:Void()
        If Not xml.HasChildren()
            Throw New InvalidSpriteAtlasXmlException(
                "Given xml file seems to be empty")
        End

        If xml.name <> "textureatlas"
            Throw New InvalidSpriteAtlasXmlException(
                "First node must be 'TextureAtlas'")
        End

        If Not xml.HasAttribute("imagepath")
            Throw New InvalidSpriteAtlasXmlException(
                "Missing attribute 'imagepath' in 'TextureAtlas' node")
        End
    End

    Method LoadAtlasSprite:Void()
        atlasSprite = New Sprite(xml.GetAttribute("imagepath"))

        If xml.HasAttribute("width")
            Assert.AssertEquals(
                atlasSprite.GetSize().x,
                Float(xml.GetAttribute("width")))
        End

        If xml.HasAttribute("height")
            Assert.AssertEquals(
                atlasSprite.GetSize().y,
                Float(xml.GetAttribute("height")))
        End
    End

    Method LoadSprites:Void()
        Local name:String
        Local src:Vector2D = New Vector2D()
        Local size:Vector2D = New Vector2D()
        Local rotation:Int

        For Local child:XMLNode = EachIn xml.children
            CheckChildXml(child)

            name = child.GetAttribute("n")
            src.x = Int(child.GetAttribute("x"))
            src.y = Int(child.GetAttribute("y"))
            size.x = Int(child.GetAttribute("w"))
            size.y = Int(child.GetAttribute("h"))

            If child.GetAttribute("r", "n") = "y"
                rotation = 90
            Else
                rotation = 0
            End

            childSprites.Set(
                name,
                atlasSprite.GrabSprite(name, src, size, rotation))
        End
    End

    Method CheckChildXml:Void(child:XMLNode)
        If child.name <> "sprite"
            Throw New InvalidSpriteAtlasXmlException(
                "Unknown node '" + child.name + "' found")
        End

        For Local attr:String = EachIn ["n", "x", "y", "w", "h"]
            If Not child.HasAttribute(attr)
                Throw New InvalidSpriteAtlasXmlException(
                    "Missing attribute '" + attr + "' in sprite node")
            End
        End

        If child.HasAttribute("r") And child.GetAttribute("r") <> "y"
            Throw New InvalidSpriteAtlasXmlException(
                "Invalid value for attribute 'r' in sprite node found")
        End
    End

    Method FreeXmlRecursive:Void(xml:XMLNode)
        If Not xml Then Return

        For Local child:XMLNode = EachIn xml.children
            FreeXmlRecursive(child)
            xml.RemoveChild(child)
        End

        xml.ClearAttributes()
        xml.ClearChildren()
    End
End

Strict

Private

Import bono
Import bono.vendor.skn3.xml
Import mojo.app

Public

Class SpriteAtlas
    Private

    Field atlasSprite:Sprite
    Field childSprites:StringMap<Sprite> = New StringMap<Sprite>()
    Field xml:XMLDoc

    Public

    Method New(filename:String)
        LoadXml(filename)
        CheckXml()

        LoadAtlasSprite()
        LoadSprites()

        FreeXmlRecursive(xml)
        xml = Null
    End

    Method Get:Sprite(name:String)
        If Not childSprites.Contains(name)
            Throw New InvalidArgumentException(
                "There is no sprite named: " + name)
        End

        Return childSprites.Get(name)
    End

    Method GetNames:MapKeys<String, Sprite>()
        Return childSprites.Keys()
    End

    Method ObjectEnumerator:ValueEnumerator<String, Sprite>()
        Return childSprites.Values().ObjectEnumerator()
    End

    Method Count:Int()
        Return childSprites.Count()
    End

    Method IsEmpty:Bool()
        Return childSprites.IsEmpty()
    End

    Method Contains:Bool(name:String)
        Return childSprites.Contains(name)
    End

    Private

    Method LoadXml:Void(filename:String)
        Local content:String = LoadString(filename)
        If Not content Or content = ""
            Throw New RuntimeException(
                "Unable to load definition from file: " + filename)
        End

        Local error:XMLError = New XMLError
        xml = ParseXML(content, error)

        If Not xml Or error.error
            Throw New RuntimeException(
                "Unable to parse XML: " + error)
        End
    End

    Method CheckXml:Void()
        If Not xml.HasChildren()
            Throw New InvalidSpriteAtlasXmlException(
                "Given xml file seems to be empty")
        End

        If xml.name <> "textureatlas"
            Throw New InvalidSpriteAtlasXmlException(
                "First node must be 'TextureAtlas'")
        End

        If Not xml.HasAttribute("imagepath")
            Throw New InvalidSpriteAtlasXmlException(
                "Missing attribute 'imagepath' in 'TextureAtlas' node")
        End
    End

    Method LoadAtlasSprite:Void()
        atlasSprite = New Sprite(xml.GetAttribute("imagepath"))

        If xml.HasAttribute("width")
            Assert.AssertEquals(
                atlasSprite.GetSize().x,
                Float(xml.GetAttribute("width")))
        End

        If xml.HasAttribute("height")
            Assert.AssertEquals(
                atlasSprite.GetSize().y,
                Float(xml.GetAttribute("height")))
        End
    End

    Method LoadSprites:Void()
        Local name:String
        Local src:Vector2D = New Vector2D()
        Local size:Vector2D = New Vector2D()
        Local rotation:Int

        For Local child:XMLNode = EachIn xml.children
            CheckChildXml(child)

            name = child.GetAttribute("n")
            src.x = Int(child.GetAttribute("x"))
            src.y = Int(child.GetAttribute("y"))
            size.x = Int(child.GetAttribute("w"))
            size.y = Int(child.GetAttribute("h"))

            If child.GetAttribute("r", "n") = "y"
                rotation = 90
            Else
                rotation = 0
            End

            childSprites.Set(
                name,
                atlasSprite.GrabSprite(name, src, size, rotation))
        End
    End

    Method CheckChildXml:Void(child:XMLNode)
        If child.name <> "sprite"
            Throw New InvalidSpriteAtlasXmlException(
                "Unknown node '" + child.name + "' found")
        End

        For Local attr:String = EachIn ["n", "x", "y", "w", "h"]
            If Not child.HasAttribute(attr)
                Throw New InvalidSpriteAtlasXmlException(
                    "Missing attribute '" + attr + "' in sprite node")
            End
        End

        If child.HasAttribute("r") And child.GetAttribute("r") <> "y"
            Throw New InvalidSpriteAtlasXmlException(
                "Invalid value for attribute 'r' in sprite node found")
        End
    End

    Method FreeXmlRecursive:Void(xml:XMLNode)
        If Not xml Then Return

        For Local child:XMLNode = EachIn xml.children
            FreeXmlRecursive(child)
            xml.RemoveChild(child)
        End

        xml.ClearAttributes()
        xml.ClearChildren()
    End
End