local CONST_TIME_SNAKE_SPEED = 300
local CONST_SPEED_PER_LEVEL = 0.5
local CONST_STARTING_SPEED = 2.0
local CONST_SPEED_CAP = 9.0
local CONST_RED_SKIN_LEVEL_REQ = 50
local CONST_RED_FOOD_MULT = 2.0
local CONST_RED_FOOD_APPEARANCE_FREQ = 10 -- the less the better

local function Point(x,y)
	return ffi.new("Point", {x, y})
end

local COLORS = {
	Board 	= 100,
	Blue	= 101,
	Green 	= 102,
	Blue2	= 103,
	Blue3	= 104,
	Green2 	= 105,
	Silver	= 107,
	Grey	= 108,
	Red		= 109,
	Red2	= 110,
	Red3	= 111,
	Gold	= 112,
	Gold2	= 113,
	Food	= 114,
	Gold3	= 115,
	Black	= 116,
	Green3	= 117,
	Food2	= 118
}

local DIRS = {
	Up = 0,
	Right = 1,
	Down = 2,
	Left = 3
}

local GAME = {
	Level = 1,
	Score = 0,
	MaxScore = 0,
	TimeDiff = 0,
	BoardColor = COLORS.Board
}

local SNAKE = {
	Speed = 5,
	Skin = {COLORS.Green, COLORS.Green2, COLORS.Green3},
	Direction = DIRS.Up,
	PrevDirection = DIRS.Up,
	Head = Point(29, 29),
	Body = {
		Point(29, 29),
		Point(29, 30),
		Point(29, 31),
		Point(29, 32)
	}
}

local GOODIE = {
	Type = 0,
	Pos = Point(math.random(4,20), math.random(4, 50))
}

local WALLS = {}

--------------------------------------------------------------- BOARD ----------------------------------------------------------------

local BOARD

function CreateBoard(plane)
	plane:CreateTileLayer(1, 1, plane.Width-1, plane.Height-1):Fill(COLORS.Black):Place()
	local screenCenter = GetCameraPoint()
	local topLeftCorner = ffi.new("Point", screenCenter.X - 311, screenCenter.Y - 224)
	BOARD = plane:CreateTileLayer(topLeftCorner.X/8, topLeftCorner.Y/8, 59, 59)
	BOARD:Fill(GAME.BoardColor)
	RefreshBoard()
end

function RefreshBoard()
	local skinCount = 1
	for _, point in ipairs(SNAKE.Body) do
		skinCount = skinCount + 1
		local skinColor = SNAKE.Skin[(skinCount)%2+1]
		BOARD:SetTile(skinColor, point.X, point.Y)
	end
	for _, point in ipairs(WALLS) do
		BOARD:SetTile(COLORS.Silver, point.X, point.Y)
	end
	if GOODIE.Type == 0 then
		BOARD:SetTile(COLORS.Food, GOODIE.Pos.X, GOODIE.Pos.Y)
	elseif GOODIE.Type == 1 then
		BOARD:SetTile(COLORS.Food2, GOODIE.Pos.X, GOODIE.Pos.Y)
	end
	BOARD:Place()
end

--------------------------------------------------------------- SCORE ----------------------------------------------------------------

function IncrementScore()
	if GOODIE.Type == 0 then
		GAME.Score = GAME.Score + GAME.Level
	elseif GOODIE.Type == 1 then
		GAME.Score = GAME.Score + CONST_RED_FOOD_MULT*GAME.Level
	end
end

function DecrementScore()
	GAME.Level = math.round(GAME.Level/2)
	GAME.Score = math.round(GAME.Score/2)
	if GAME.Score < 20 then
		GAME.Score = 1
		GAME.Level = 1
		for i, point in ipairs(WALLS) do
			BOARD:SetTile(GAME.BoardColor, point.X, point.Y)
			WALLS[i] = nil
		end
		for i, point in ipairs(SNAKE.Body) do
			if i > 4 then
				BOARD:SetTile(GAME.BoardColor, point.X, point.Y)
				SNAKE.Body[i] = nil
			end
		end
	end
end

function SetLevelAndMaxScore(hdc)
	if GAME.Score > GAME.Level^2 then
		GAME.Level = GAME.Level + 1
	end
	GAME.MaxScore = GAME.Score > GAME.MaxScore and GAME.Score or GAME.MaxScore
end

function SetSnakeSpeed()
	local speed = GAME.Level * CONST_SPEED_PER_LEVEL + CONST_STARTING_SPEED
	SNAKE.Speed = GAME.Level >= 50 and CONST_SPEED_CAP + 1 or speed > CONST_SPEED_CAP and CONST_SPEED_CAP or speed
end

---------------------------------------------------------------- HUD -----------------------------------------------------------------

function DrawNumbersOnTheRight(hdc, numbers, offset)
	numbers = tostring(numbers)
	local coords = ffi.cast("CPlane*", Game(9,23)).Screen
	local centerX = math.round(coords.Right/2)
	local centerY = math.round(coords.Bottom/2)
	local textBoxLeft = centerX + 190
	local textBoxRight = centerX + 270
	local textBoxTop = centerY - 140
	local textBoxBottom = centerY - 110
	local ptrRect = ffi.new("Rect[1]")
	ptrRect[0] = {
		textBoxLeft,
		textBoxTop + offset,
		textBoxRight,
		textBoxBottom + offset
	}
	
	ffi.C.DrawTextA(hdc, numbers, #numbers, ptrRect, 2) -- 2 is align to right
end

function DrawHUD(hdc)
	ffi.C.SetTextColor(hdc, 0xFFFFFF)
	DrawNumbersOnTheRight(hdc, GAME.Score, 128)
	DrawNumbersOnTheRight(hdc, GAME.MaxScore, 256)
	DrawNumbersOnTheRight(hdc, GAME.Level, 0)
end

--------------------------------------------------------------- SNAKE ----------------------------------------------------------------

function MoveSnake()
	BOARD:SetTile(GAME.BoardColor, SNAKE.Body[#SNAKE.Body].X, SNAKE.Body[#SNAKE.Body].Y) -- clean the tail
	local snakeDir = SNAKE.Direction
	SNAKE.PrevDirection = snakeDir
	local addHor = snakeDir == DIRS.Left and -1 or snakeDir == DIRS.Right and 1 or 0
	local addVer = snakeDir == DIRS.Up and -1 or snakeDir == DIRS.Down and 1 or 0
	local newX = SNAKE.Head.X + addHor
	local newY = SNAKE.Head.Y + addVer
	newX = newX < 0 and BOARD.Width - 1 or newX >= BOARD.Width and 0 or newX
	newY = newY < 0 and BOARD.Height - 1 or newY >= BOARD.Height and 0 or newY
	SNAKE.Head = Point(newX, newY)
	if newX == GOODIE.Pos.X and newY == GOODIE.Pos.Y then
		FeedSnake()
		return
	end
	local nextPoint = Point(SNAKE.Head.X, SNAKE.Head.Y)
	local prevPoint = Point(0,0)
	for _, point in ipairs(SNAKE.Body) do
		prevPoint.X = point.X
		prevPoint.Y = point.Y
		point.X = nextPoint.X
		point.Y = nextPoint.Y
		nextPoint.X = prevPoint.X
		nextPoint.Y = prevPoint.Y
	end
	for index, point in ipairs(SNAKE.Body) do
		if index > 1 and SNAKE.Head.X == point.X and SNAKE.Head.Y == point.Y then
			CollideSnakeSnake(index)
		end
	end
	for index, point in ipairs(WALLS) do
		if index > 1 and SNAKE.Head.X == point.X and SNAKE.Head.Y == point.Y then
			CollideWallSnake(index)
		end
	end
end

function FeedSnake()
	IncrementScore()
	local newBody = {Point(SNAKE.Head.X, SNAKE.Head.Y)}
	for _, point in ipairs(SNAKE.Body) do
		table.insert(newBody, point)
	end
	SNAKE.Body = newBody
	BOARD:SetTile(SNAKE.Skin[3], SNAKE.Head.X, SNAKE.Head.Y)
	PlaySound("LEVEL_CHAMELEON_TONGUESTRIKE", nil, 0, GAME.Level)
	PlaceFood()
end

function SetSnakeSkin()
	if SNAKE.Speed < CONST_SPEED_CAP then
		SNAKE.Skin = {COLORS.Green, COLORS.Green2, COLORS.Green3}
	elseif GAME.Score > 9000 then
		SNAKE.Skin = {COLORS.Gold, COLORS.Gold2, COLORS.Gold3}
	elseif GAME.Level >= CONST_RED_SKIN_LEVEL_REQ then
		SNAKE.Skin = {COLORS.Red, COLORS.Red2, COLORS.Red3}
	else
		SNAKE.Skin = {COLORS.Blue, COLORS.Blue2, COLORS.Blue3}
	end
end

------------------------------------------------------------- COLLISION --------------------------------------------------------------

function CollideSnakeSnake(collisionIndex)
	local newBody = {}
	for index, point in ipairs(SNAKE.Body) do
		if index < collisionIndex then
			table.insert(newBody, point)
		elseif index > collisionIndex then
			table.insert(WALLS, point)
		end
	end
	SNAKE.Body = newBody
	DecrementScore()
end

function CollideWallSnake(collisionIndex)
	table.remove(WALLS, collisionIndex)
	DecrementScore()
end

-------------------------------------------------------------- GOODIES ---------------------------------------------------------------

function PlaceFood()
	local goodPlacement, newX, newY
	repeat
		goodPlacement = true
		newX = math.random(0, BOARD.Width-1)
		newY = math.random(0, BOARD.Height-1)
		if BOARD:GetTile(newX, newY) ~= GAME.BoardColor then
			goodPlacement = false
		end
	until goodPlacement
	GOODIE.Type = math.random(CONST_RED_FOOD_APPEARANCE_FREQ) == 1 and 1 or 0
	GOODIE.Pos = Point(newX, newY)
end

------------------------------------------------------------- MOVEMENT ---------------------------------------------------------------

function ControlMovement()
	for key, val in pairs(DIRS) do
		if GetInput(InputFlags[key]) and SNAKE.PrevDirection ~= (val+2)%4 then -- if PrevDir ~= opposite direction
			SNAKE.Direction = DIRS[key]
		end
	end
end

---------------------------------------------------------------- CORE ----------------------------------------------------------------

function OnMapLoad2()
	if version < 1453 then
		local curVersion = string.gsub(tostring(version), "%d", "%0%.")
		MessageBox("Snake requires CrazyHook version 1.4.5.3 or later.\nYour current version is " .. curVersion)
		ffi.C.PostMessageA(nRes(1,1), 0x111, _message.ExitLevel, 0)
	else
		local plane = GetPlane"SnakeAction"
		if plane ~= nil then
			CreateBoard(plane)
		else
			MessageBox("Critical error.")
			ffi.C.PostMessageA(nRes(1,1), 0x111, _message.ExitLevel, 0)
		end
	end
end

function OnGameplay(hdc)
	GetClaw().State = ClawStates.Freeze
	GetClaw().Health = -1
	ControlMovement()
	SetLevelAndMaxScore(hdc)
	SetSnakeSkin()
	SetSnakeSpeed()
	DrawHUD(hdc)

	if GetTime() > GAME.TimeDiff then
		MoveSnake()
		RefreshBoard()
		GAME.TimeDiff = GetTime() + CONST_TIME_SNAKE_SPEED/SNAKE.Speed
	end
end
