--[[	Numines
		A Lumines clone for CC
		By NitrogenFingers

		Last Update: 24/02/2013
		Version: 0.1a
]]--

os.loadAPI(".numinesData/gameutils")

--  		*	Fields	*			  --

--The size of the playing field.
local pfX,pfY = 16,9
--The Numines playing field. Size standard at 16x9
--[[
	nil: Empty space
	1: A-Block
	2: A-Block, marked
	3: A-Block, erasing
	4: A-Block, destroyer
	5: A-Block, destroyer-marked
	6: B-Block
	7: B-Block, marked
	8: B-Block, erasing
	9: B-Block, destroyer
	10: B-Block, destroyer-marked
	
	Note we distinguish blocks marked by the destoryer for our flood algorithm, and for
	scoring. The v-bar and drawing however treat them the same
]]--
local playField = { }
--The "Gravity Field". These are marked with any blocks that are influenced by gravity, and hence are not
--considered when checking marks
local gravityField = { }

--The vertical bar's position relative to the play field.
local vBar = 1
--The number of sweeps remaining before the end of the stage
local sweepsRemaining = nil
--The current numine tetronimo
local currBrick = nil
--The current number of updates remaining before the tetronimo falls
local falltimeCurrent = nil
--The number of updates required for bricks to fall. This doesn't change between skins
local falltimerRate = 24
--Whether or not the brick is "rapid" falling. Rapid bricks fall at gravity rate, as opposed to sweep rate
local rapidFalling = false

--The sweep timer, which updates the vertical bar
local sweepTimer = nil
--The gravity timer, which is updated so long as there are gravity bricks
local gravityTimer = nil
--The standard update timer for gravity. This doesn't change between skins
local gravityRate = 0.1
--The list of every gravity block on the field. Kept in a table of X and Y
local gravityBlocks = { }
--The frequency with which destruction blocks appear. should be a fractioanl value of 1 (0.25 = 1 in 4 chance etc)
local destructionFrequency = 0.05

--The player's current score
local score = nil

--The skins in this game, which contain the main fields in the game:
--	background:table = the background to display behind the game as an NFA
--  bgupdate:number = the speed at which the background updates
--  aBrickColour:number = the colour of the A-Brick, at rest
--	aBrickMarkColour:number = the colour of the A-Brick when marked for removal
		--Note: this colour also used for the "destroy" marker
--  bBrickColour:number = the colour of the B-Brick, at rest
--  bBrickMarkColour:number = the colour of the B-Brick when marked for removal
		--Note: this colour also used for the "destroy" marker
--  vBarColour:number = the colour of the vertical bar
--	sweepSpeed:number = the number of seconds between each update of the sweeper bar
-- 	sweepCount:number = the number of sweeps in the stage
local skins = { }

--The list of states the game can have
local pgStates = { }
--The list of game modes available
local gameModes = { MARATHON = 1, CHALLENGE = 2, PUZZLE = 3 }

--The current skin being played with
local currentSkin = nil
--The game mode
local currentGameMode = nil
--Whether or not the program is running
local running = true

--[[			* Drawing * 		]]--

--Draws a single brick
local function drawBlock(x, y, val)
	local marked = false
	local erasing = false
	local destroyer = false
	
	if val < 5 then 
		term.setBackgroundColour(currentSkin.aBrickColour)		
		term.setTextColour(currentSkin.aBrickMarkColour)
	else 
		term.setBackgroundColour(currentSkin.bBrickColour)
		term.setTextColour(currentSkin.bBrickMarkColour)
	end
	
	if val % 5 == 2 or val % 5 == 0 then
		marked = true
	elseif val % 5 == 3 then
		erasing = true
	elseif val % 5 == 4 then
		destroyer = true
	end
	
	for i = 1,2 do
		term.setCursorPos(3 + (x-1) * 3, 2 + i + (y-1) * 2)
		local str = string.rep(" ", 3)
		if marked then str = string.rep("-", 3) end
		if destroyer and i == 2 then str = string.sub(str, 1, 1).."*"..string.sub(str, 3, 3) end
		if erasing then
			local xb = 3 + (x-1) * 3
			local diff = vBar - xb
			if diff > 3 then str = string.rep("#", 3)
			else str = string.sub(str, 1, diff)..string.rep("#", 3 - diff) end
		end
		term.write(str)
	end
end

--Draws all blocks on the playing field, according to their marking and value
local function drawPlayingField()
	for y=1, pfY do
		for x=1, pfX do
			if playField[y][x] then drawBlock(x, y, playField[y][x]) end
		end
	end
	
	--Draws the player's brick down here
	drawBlock(currBrick.x, currBrick.y, currBrick.top[1])
	drawBlock(currBrick.x + 1, currBrick.y, currBrick.top[2])
	drawBlock(currBrick.x, currBrick.y + 1, currBrick.bot[1])
	drawBlock(currBrick.x + 1, currBrick.y + 1, currBrick.bot[2])
end

--Rotates the player's brick clockwise
local function rotateBrick()
	local temp = currBrick.top[1]
	currBrick.top[1] = currBrick.bot[1]
	currBrick.bot[1] = currBrick.bot[2]
	currBrick.bot[2] = currBrick.top[2]
	currBrick.top[2] = temp
end

--[[The draw method for the  in-game display
	Params:	none
	Returns:nil
]]--
local function drawGame()
	currentSkin.background:draw()
	drawPlayingField()
	term.setBackgroundColour(currentSkin.vBarColour)
	for i=2, pfY*2+1 do
		term.setCursorPos(vBar + 1, i)
		term.write(" ")
	end
end

--[[		* Input *		]]--

--Gets the colour of a block
local function getColour(value)
	if not value then return "nil"
	elseif value < 6 then return "a"
	else return "b" end
end

local function mark(x,y)
	if getColour(playField[y][x]) == "a" then
		playField[y][x] = 2
	else
		playField[y][x] = 7
	end
end

local function delete(x,y)
	if getColour(playField[y][x]) == "a" then
		playField[y][x] = 3
	else
		playField[y][x] = 8
	end
end

--Creates a new brick, puts it at the top of the screen
local function generateBrick()
	local bricks = { math.random(0,1) * 5 + 1, 
		math.random(0,1) * 5 + 1, 
		math.random(0,1) * 5 + 1, 
		math.random(0,1) * 5 + 1
	}
	
	if math.random() < destructionFrequency then
		dBrick = math.random(1,4)
		bricks[dBrick] = bricks[dBrick] + 3
	end
	
	currBrick = {
		x = pfX/2;
		y = 0;
		top = { bricks[1], bricks[2] };
		bot = { bricks[3], bricks[4] };
	}
	falltimeCurrent = falltimerRate
	rapidFalling = false
end

--Takes a block at a given X and Y and checks if it has caused the marking of any nearby blocks
local function checkForDeletions(x,y)
	
	local qBlock = getColour(playField[y][x])
	
	topLeft = { x-1, y-1 } 
	topRight = { x+1, y-1 }
	botLeft = { x-1, y+1 }
	botRight = { x+1, y+1 }
	
	--It's still not great, but it's better than it was
	if x == 1 or getColour(playField[y][x-1]) ~= qBlock then
		topLeft = nil
		botLeft = nil
	end if x == pfX or getColour(playField[y][x+1]) ~= qBlock then
		topRight = nil
		botRight = nil
	end if y == 1 or getColour(playField[y-1][x]) ~= qBlock then
		topRight = nil
		topLeft = nil
	end if y == pfY or getColour(playField[y+1][x]) ~= qBlock then
		botRight = nil
		botLeft = nil
	end
	
	--So many checks in this game!
	if topLeft and getColour(playField[topLeft[2]][topLeft[1]]) == qBlock then
		mark(topLeft[1], topLeft[2])
		mark(x-1, y)
		mark(x, y-1)
		mark(x, y)
	end if topRight and getColour(playField[topRight[2]][topRight[1]]) == qBlock then
		mark(topRight[1], topRight[2])
		mark(x+1, y)
		mark(x, y-1)
		mark(x, y)
	end if botLeft and getColour(playField[botLeft[2]][botLeft[1]]) == qBlock then
		mark(botLeft[1], botLeft[2])
		mark(x-1, y)
		mark(x, y+1)
		mark(x, y)
	end if botRight and getColour(playField[botRight[2]][botRight[1]]) == qBlock then
		mark(botRight[1], botRight[2])
		mark(x+1, y)
		mark(x, y+1)
		mark(x, y)
	end
end

--Moves the bricks one step down, or places it if necessary
local function moveBrickDown()
	if currBrick.y == pfY - 1 then
		playField[currBrick.y][currBrick.x] = currBrick.top[1]
		playField[currBrick.y][currBrick.x + 1] = currBrick.top[2]
		playField[currBrick.y + 1][currBrick.x] = currBrick.bot[1]
		playField[currBrick.y + 1][currBrick.x + 1] = currBrick.bot[2]
		checkForDeletions(currBrick.x, currBrick.y)
		checkForDeletions(currBrick.x, currBrick.y + 1)
		checkForDeletions(currBrick.x + 1, currBrick.y)
		checkForDeletions(currBrick.x + 1, currBrick.y + 1)
		generateBrick()
	elseif playField[currBrick.y + 2][currBrick.x] ~= nil or
		playField[currBrick.y + 2][currBrick.x + 1] ~= nil then
		playField[currBrick.y][currBrick.x] = currBrick.top[1]
		playField[currBrick.y][currBrick.x + 1] = currBrick.top[2]
		playField[currBrick.y + 1][currBrick.x] = currBrick.bot[1]
		playField[currBrick.y + 1][currBrick.x + 1] = currBrick.bot[2]
		
		if playField[currBrick.y + 2][currBrick.x] ~= nil then
			checkForDeletions(currBrick.x, currBrick.y)
			checkForDeletions(currBrick.x, currBrick.y + 1)
		end if playField[currBrick.y + 2][currBrick.x + 1] ~= nil then
			checkForDeletions(currBrick.x + 1, currBrick.y)
			checkForDeletions(currBrick.x + 1, currBrick.y + 1)
		end
		
		generateBrick()
	else
		currBrick.y = currBrick.y + 1
	end
end

local function updateGame()
	local id, p1 = os.pullEvent()
	
	if id == "timer" then
		if p1 == sweepTimer then
			vBar = vBar + 1
			if vBar > 48 then vBar = 1 end
			if falltimeCurrent == 0 then 
				if vBar % 3 == 0 and not rapidFalling then moveBrickDown() end
			else falltimeCurrent = falltimeCurrent - 1 end
			sweepTimer = os.startTimer(currentSkin.sweepSpeed)
			
			if vBar % 3 == 1 then
				local xTest = nil
				if vBar ==1 then xTest = pfX
				else xTest = (vBar-1)/3 end
				
				local deleteFound = false
				for y=pfY,1,-1 do
					if playField[y][xTest] and (playField[y][xTest] % 5 == 2 or playField[y][xTest] % 5 == 0) then
						delete(xTest, y)
						deleteFound = true
					end
				end
				
				if not deleteFound then
					for x = xTest-1,1,-1 do
						local destroyFound = false
						for y = pfY,1,-1 do
							if playField[y][x] and playField[y][x] % 5 == 3  then
								playField[y][x] = nil
								destroyFound = true
							end
						end
						if not destroyFound then break end
					end
				end
			end
		elseif p1 == gravityTimer then
			local landedGravList = { }
			for x=1,pfX do
				local gravApplies = false 
				local lastBrickGrounded = true
				for y=pfY,2,-1 do
					if playField[y][x] == nil then 
						gravApplies = true
						if y > pfY - 1 and playField[y-1][x] == nil then
							lastBrickGrounded = false
						end
					end
					if gravApplies and playField[y-1][x] ~= nil then
						playField[y][x] = playField[y-1][x]
						playField[y-1][x] = nil
						if lastBrickGrounded then
							table.insert(landedGravList, { x=x, y=y })
						end
					end
				end
			end		
			for i=1,#landedGravList do
				checkForDeletions(landedGravList[i].x, landedGravList[i].y)
			end
			
			if rapidFalling then moveBrickDown() end
			gravityTimer = os.startTimer(gravityRate)
		end
	elseif id == "key" then
		if p1 == keys.left and currBrick.x > 1 then
			currBrick.x = currBrick.x - 1
			rapidFalling = false
		elseif p1 == keys.right and currBrick.x < pfX -1 then
			currBrick.x = currBrick.x + 1
			rapidFalling = false
		elseif p1 == keys.up then
			rotateBrick()
		elseif p1 == keys.down and not rapidFalling then
			rapidFalling = true
			falltimeCurrent = 0
			if not gravityTimer then gravityTimer = os.startTimer(gravityRate) end
		end
	end
end

--[[		* Initialization and Main * 		]]--

--Initializes the pgStates and the skins
local function initialize()
	pgStates.main = {
		options = { "game", "challenge", "puzzle", "highscores", "quit" };
		selection = 1;
		draw = drawMain;
		update = updateMain;
	} 
	pgStates.ingame = { 
		options = nil;
		selection = nil;
		draw = drawGame;
		update = updateGame;
	}
	
	skins.minecraft = {
		background = gameutils.loadSprite(".numinesData/bg1.nfa", 1, 1);
		bgupdate = nil;
		aBrickColour = colours.white;
		aBrickMarkColour = colours.lightBlue;
		bBrickColour = colours.lightBlue;
		bBrickMarkColour = colours.white;
		vBarColour = colours.grey;
		sweepSpeed = 0.25;
		sweepCount = 64;
	}
end

--Creates a new game, with the specified skin
local function newGame(skin)
	if not skin then 
		currentGameMode = gameModes.MARATHON
		skin = skins.minecraft
	else currentGameMod = gameModes.CHALLENGE end
	currentSkin = skin
	
	for i=1,pfY do
		playField[i] = { }
		gravityField[i] = { }
	end
	
	sweepTimer = os.startTimer(skin.sweepSpeed)
	sweepsRemaining = skin.sweepCount
	gravityBlocks = { }
	generateBrick()
end

--The main function
local function main()
	initialize()
	newGame(skins.minecraft)
	
	while running do
		drawGame()
		updateGame()
	end
end

local ok,err = pcall(main)

if not ok then
running = false
os.queueEvent("terminate")
end
