--UI FUNCTIONS

function refreshUI()
	--print("<refreshUI>")
	displayLines()
	displayScore()
	displayNext()
	displayLevel()
end

function displayLines()
	--print("<displayLines>")
	Media.FloatingText("LINES - " .. lineNum, WPos.New(7 * 1024, 5 * 1024, 1024), 1)
end

function displayScore()
	--print("<displayScore>")
	Media.FloatingText("SCORE: " .. score, WPos.New(16 * 1024, 5 * 1024, 1024), 1)
end

function displayNext()
	--print("<displayNext>")
	Media.FloatingText("NEXT: ", WPos.New(16 * 1024, 11 * 1024, 1024), 1)
end

function updateNextShape()
	--print("<updateNextShape>")
	for x, t in ipairs(nextShapeDisplay) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				v.Destroy()
			end
		end
	end
	nextShapeDisplay = createShape(nextShapeIndex, CPos.New(15,11), true)
end

function displayLevel()
	--print("<displayLevel>")
	Media.FloatingText("LEVEL: " .. levelNum, WPos.New(16 * 1024, 19 * 1024, 1024), 1)
end

--GAME FUNCTIONS

function getNextShape()
	--print("<getNextShape>")
	local newIndex = Utils.RandomInteger(1, #shapePrefabs + 1)
	nextShapeIndex = newIndex
end

function updateLevel()
	levelNum = math.floor(score/levelIncrement)
	if levelNum < 15 then
		playDelay = 20 - levelNum
	else
		playDelay = 5
	end
end

function initPlayArea()
	--print("<initPlayArea>")
	for r = 1, 20 do
		playArea[r] = {}
		for a = 1, 10 do
			playArea[r][a] = false
		end
	end
end

function initControl()
	--print("<initControl>")
	controlActor = Actor.Create("control", false, {Owner = player, Location = CPos.New(0,0)})
	Trigger.OnProduction(controlActor, function(actor, move)
				if (shape ~= nil) then
					if (move.Type == "move_left") then
						if (canMoveShapeLeft()) then
							moveShapeLeft()
						end
					elseif (move.Type == "move_right") then
						if (canMoveShapeRight()) then
							moveShapeRight()
						end
					elseif (move.Type == "rotate") then
						if (shape["rotates"] ~= false) then
							rotateShape()
						end
					elseif (move.Type == "move_down") then
						tickCounter = 0
						if (not shapeAtBottom()) then
							if (not blocksBelowShape()) then
								moveShapeDown()
							else
								freezeShape()
								destroyFullRows()
							end
						else
							freezeShape()
							destroyFullRows()
						end
					elseif (move.Type == "hard_drop") then
						tickCounter = 0
						while (not shapeAtBottom()) do
							if (not blocksBelowShape()) then
								moveShapeDown()
							else
								break
							end
						end
						freezeShape()
						destroyFullRows()
					end
				end
				move.Destroy()
			end)
end

function countdown()
	--print("<countdown>")
	local countdownPrelim = 4 - math.floor(tickCounter/30)
	if (countdownPrelim ~= countdownCounter) then
		countdownCounter = countdownPrelim
		if (countdownCounter == 4) then
			UserInterface.SetMissionText("3")
		elseif (countdownCounter == 3) then
			UserInterface.SetMissionText("2")
		elseif (countdownCounter == 2) then
			UserInterface.SetMissionText("1")
		elseif (countdownCounter == 1) then
			UserInterface.SetMissionText("BEGIN!")
			playMusic()
		else
			UserInterface.SetMissionText("")
			startPlaying()
		end
	end
end

function startPlaying()
	--print("<startPlaying>")
	playing = true
	objectiveID = player.AddPrimaryObjective("Don't allow the blocks to overflow!")
end

function playMusic()
	Media.SetBackgroundMusic("tetris")
	Media.StopMusic()
end

function play()
	--print("<play>")
	if (shape == nil) then
		shape = createShape(nextShapeIndex, blockSpawnArea, false)
		getNextShape()
		updateNextShape()
	elseif (tickCounter % playDelay == 0) then
		tickCounter = 0
		if (shapeAtBottom()) then
			freezeShape()
			destroyFullRows()
		elseif (blocksBelowShape()) then
			freezeShape()
			destroyFullRows()
		else
			moveShapeDown()
		end
	end
end

function blocksBelowShape()
	--print("<blocksBelowShape>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local checkPosition = CPos.New(v.Location.X, v.Location.Y + 1)
				if (actorAt(checkPosition)) then
					return true
				end
			end
		end
	end
	return false
end

function shapeAtBottom()
	--print("<shapeAtBottom>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if v ~=false then
				local playAreaY = playAreaBottomRight.Y - v.Location.Y + 1
				if (playAreaY == 1) then
					return true
				end
			end
		end
	end
	return false	
end

function actorAt(position)
	--print("<actorAt>")
	local playAreaX = position.X - playAreaTopLeft.X + 1
	local playAreaY = playAreaBottomRight.Y - position.Y + 1
	if (playAreaY <= 20) then
		if (playArea[playAreaY][playAreaX] == false) then
			return false
		end
	else
		return false
	end
	return true
end

function freezeShape()
	--print("<freezeShapes>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local playAreaX = v.Location.X - playAreaTopLeft.X + 1
				local playAreaY = playAreaBottomRight.Y - v.Location.Y + 1
				if (playAreaY > 20) then
					failed = true
					player.MarkFailedObjective(objectiveID)
					Trigger.ClearAll(controlActor)
					destroyAllBlocks()
					destroyShape(shape)
					destroyShape(nextShapeDisplay)
					Media.SetBackgroundMusic()
					return
				end
				local playAreaPos = playArea[playAreaY][playAreaX]
				if (playAreaPos == false) then
					playArea[playAreaY][playAreaX] = v
					v.Owner = Neutral
				else
					print("BLOCKS TAKING UP SAME SPACE")
				end
			end
		end
	end
	shape = nil
end

function destroyFullRows()
	--print("<destroyFullRows>")
	local destroyedRows = 0
	for y, t in ipairs(playArea) do
		if rowFull(t) then
			for i, a in ipairs(t) do
				a.Kill()
				t[i] = false
			end
			lineNum = lineNum + 1
			destroyedRows = destroyedRows + 1
		end
	end
	score = score + ((math.pow(destroyedRows, 2) * 100) * 0.5)
	if (score >= winThreshold) then
		player.MarkCompletedObjective(objectiveID)
		Trigger.ClearAll(controlActor)
		UserInterface.SetMissionText("CONGRATULATIONS!")
		destroyAllBlocks()
		destroyShape(shape)
		destroyShape(nextShapeDisplay)
		return
	end
	updateLevel()
	if (destroyedRows > 0) then
		moveDownRows()
	end
end

function moveDownRows()
	--print("<moveDownRows>")
	for i = 2, 20 do
		for j = i, 2, -1 do
			if (rowEmpty(playArea[j - 1]) and not rowEmpty(playArea[j])) then
				moveDownRow(j)
			end
		end
	end
	destroyFullRows()
end

function moveDownRow(rowIndex)
	--print("<moveDownRow>")
	t = playArea[rowIndex]
	r = {}
	for i, v in ipairs(playArea[rowIndex]) do
		if (v ~= false) then
			local newPosition = CPos.New(v.Location.X, v.Location.Y + 1)
			local blockType = v.Type
			v.Destroy()
			playArea[rowIndex][i] = false
			r[i] = Actor.Create(blockType, true, {Owner = Neutral, Location = newPosition})
		else
			r[i] = false
		end
	end
	playArea[rowIndex - 1] = r
end

function rowFull(row)
	--print("<rowFull>")
	for i = 1, 10 do
		if row[i] == false then
			return false
		end
	end
	return true
end

function rowEmpty(row)
	--print("<rowEmpty>")
	for i = 1, 10 do
		if row[i] ~= false then
			return false
		end
	end
	return true
end

function moveShapeDown()
	--print("<moveShapeDown>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local newLocation = CPos.New(v.Location.X, v.Location.Y + 1)
				local inWorld = inPlayArea(newLocation)
				local blockType = v.Type
				v.Destroy()
				t[y] = Actor.Create(blockType, inWorld, {Owner = Neutral, Location = newLocation})
			end
		end
	end
end

function inPlayArea(position)
	--print("<inPlayArea>")
	return ((position.X >= playAreaTopLeft.X) and (position.X <= playAreaBottomRight.X) and (position.Y >= playAreaTopLeft.Y) and (position.Y <= playAreaBottomRight.Y))
end

function canMoveShapeLeft()
	--print("<canMoveShapeLeft>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local newX = v.Location.X - 1
				if (newX < playAreaTopLeft.X) then
					return false
				elseif (newX >= playAreaTopLeft.X) then
					if (actorAt(CPos.New(newX, v.Location.Y))) then
						return false
					end
				end
			end
		end
	end
	return true
end

function moveShapeLeft()
	--print("<moveShapeLeft>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local newX = v.Location.X - 1
				local newY = v.Location.Y
				local newPosition = CPos.New(newX, newY)
				local inWorld = inPlayArea(newPosition)
				local blockType = v.Type
				v.Destroy()
				t[y] = Actor.Create(blockType, inWorld, {Owner = Neutral, Location = newPosition})
			end
		end
	end
end

function canMoveShapeRight()
	--print("<canMoveShapeRight>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local newX = v.Location.X + 1
				if (newX > playAreaBottomRight.X) then
					return false
				elseif (newX <= playAreaBottomRight.X) then
					if (actorAt(CPos.New(newX, v.Location.Y))) then
						return false
					end
				end
			end
		end
	end
	return true
end

function moveShapeRight()
	--print("<moveShapeRight>")
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				local newX = v.Location.X + 1
				local newY = v.Location.Y
				local newPosition = CPos.New(newX, newY)
				local inWorld = inPlayArea(newPosition)
				local blockType = v.Type
				v.Destroy()
				t[y] = Actor.Create(blockType, inWorld, {Owner = Neutral, Location = newPosition})
			end
		end
	end
end

function rotateShape()
	--print("<rotateShape>")
	local templateShape = createPositioningTemplate()
	if (validTemplate(templateShape)) then
		for x, t in ipairs(shape) do
			--print("KEY(x): " .. x)	
			for y, v in ipairs(t) do
				--print("KEY(y): " .. y)
				if (v ~= false) then
					local newPosition = templateShape[x][y]
					local inWorld = inPlayArea(newPosition)
					local blockType = v.Type
					v.Destroy()
					t[y] = Actor.Create(blockType, inWorld, {Owner = Neutral, Location = newPosition})
				end
			end
		end
	end
end

function createPositioningTemplate()
	--print("<createPositioningTemplate>")
	local template = {}
	local axisBlock = shape[shape["axisBlock"].X][shape["axisBlock"].Y]
	local centreX
	local centreY
	if (shape["rotationOffset"] ~= false) then
		centreX = axisBlock.Location.X + shape["rotationOffset"].X
		centreY = axisBlock.Location.Y + shape["rotationOffset"].Y
	else
		centreX = axisBlock.Location.X
		centreY = axisBlock.Location.Y
	end
	for x, t in ipairs(shape) do
		template[x] = {}
		for y, v in ipairs(t) do
			if (v ~= false) then
				local relativeX = v.Location.X - centreX
				local relativeY = v.Location.Y - centreY
				local newX = -relativeY + centreX
				local newY = relativeX + centreY
				template[x][y] = CPos.New(newX, newY)
			else
				template[x][y] = false
			end
		end
	end
	return template
end

function validTemplate(template)
	--print("<validateTemplate>")
	for x, t in ipairs(template) do
		for y, v in ipairs(t) do
			if (v ~=  false) then
				if not inPlayArea(v) then
					return false
				elseif(actorAt(v)) then
					return false
				end
			end
		end
	end
	return true
end

function destroyAllBlocks()
	for x, t in ipairs(playArea) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				v.Kill()
			end
		end
	end
end

function destroyShape(shape)
	for x, t in ipairs(shape) do
		for y, v in ipairs(t) do
			if (v ~= false) then
				v.Kill()
			end
		end
	end
end

--SHAPE FUNCTIONS

function createShape(shapeIndex, spawnPosition, addToWorld)
	--print("<createShape>")
	local shapePrefab = shapePrefabs[shapeIndex]
	local shape = {}
	shape["rotates"] = shapePrefab["rotates"]
	shape["rotationOffset"] = shapePrefab["rotationOffset"]
	shape["axisBlock"] = shapePrefab["axisBlock"]
	local blockType = shapePrefab["colour"] .. "_block"
	for x, t in ipairs(shapePrefab) do
		shape[x] = {}
		for y, v in ipairs(t) do
			if (v) then
				local position = CPos.New(spawnPosition.X + (x - 1), spawnPosition.Y + (y - 1))
				shape[x][y] = Actor.Create(blockType, addToWorld, {Owner = Neutral, Location = position})
			else
				shape[x][y] = false
			end
		end
	end
	return shape
end

--UTILITY FUNCTIONS

function printCPos(CPosLocation)
	--print("<printCPos>")
	--print("CPOS:")
	--print("\tx: " .. CPosLocation.X)
	--print("\ty: " .. CPosLocation.Y)
end

--OPENRA FUNCTIONS

function WorldLoaded()
	--print("<WorldLoaded>")
	lineNum = 0
	score = 0
	levelNum = 0
	playing = false
	failed = false
	initPlayArea()
	initControl()
	getNextShape()
end

function Tick()
	--print("<Tick>")
	refreshUI()
	tickCounter = tickCounter + 1
	
	if (playing and not failed) then
		play()
	elseif (not failed) then
		countdown()
	end
end

--VARIABLES

lineNum = nil
score = nil
levelNum = nil
playing = nil
shape = nil
playArea = {}
controlActor = nil
failed = nil
Neutral = Player.GetPlayer("Neutral")
player = Player.GetPlayer("Multi0")
tickCounter = 0
playDelay = 20
rotationGive = 1
levelIncrement = 500
winThreshold = 10000
nextShapeIndex = nil
nextShapeDisplay = {}
countdownCounter = nil

--POSITION VARIABLES

playAreaTopLeft = CPos.New(2, 9)
playAreaBottomRight = CPos.New(11, 28)
blockSpawnArea = CPos.New(5, 7)

--OBJECTIVE VARIABLES

objectiveID = nil

--PREFABS

shapePrefabs = { 
				{colour = "cyan", rotationOffset = {X = 0, Y = 0}, axisBlock = {X = 2, Y = 1}, {true}, {true}, {true}, {true}},
				{colour = "purple", rotationOffset = false, axisBlock = {X = 2, Y = 2}, {false, true}, {true, true}, {false, true}},
				{colour = "yellow", rotates = false, {true, true}, {true, true}},
				{colour = "blue", rotationOffset = false, axisBlock = {X = 2, Y = 2}, {true, true}, {false, true}, {false, true}},
				{colour = "orange", rotationOffset = false, axisBlock = {X = 2, Y = 2}, {false, true}, {false, true}, {true, true}},
				{colour = "lime", rotationOffset = false, axisBlock = {X = 2, Y = 2}, {false, true}, {true, true}, {true, false}},
				{colour = "red", rotationOffset = false, axisBlock = {X = 2, Y = 2}, {true, false}, {true, true}, {false, true}}
				}
