require 'funcs'

local GameMode = require 'tetris.modes.gamemode'
local Piece = require 'tetris.components.piece'

local History6RollsRandomizer = require 'tetris.randomizers.history_6rolls'

local Marathon2020Game = GameMode:extend()

Marathon2020Game.name = "Marathon 2020"
Marathon2020Game.hash = "Marathon2020"
Marathon2020Game.description = "2020 levels of pure pain! Can you achieve the World Master rank?"
Marathon2020Game.tags = {"Marathon", "Cambridge"}

Marathon2020Game.highscore_format = {
	grade = {sort = 1},
	level = {sort = 2},
	frames = {fps = 60, sort = 3},
}

function Marathon2020Game:new()
	Marathon2020Game.super:new()

	self.lock_drop = true
	self.lock_hard_drop = true
	self.enable_hold = true
	self.next_queue_length = 3

	self.delay_level = 0
	self.roll_frames = 0
	self.no_roll_frames = 0
	self.combo = 1
	self.randomizer = History6RollsRandomizer()
	self.section_cool_count = 0
	self.section_status = { [0] = "none" }
	self.torikan_passed = {
		[500] = false, [900] = false,
		[1000] = false, [1500] = false, [1900] = false
	}
	self.torikan_hit = false

	self.grade = 0
	self.grade_points = 0
	self.grade_point_decay_counter = 0
	self.max_grade_points = 0

	self.cool_timer = 0
end

function Marathon2020Game:getARE()
		if self.delay_level < 1 then return 27
	elseif self.delay_level < 2 then return 24
	elseif self.delay_level < 3 then return 21
	elseif self.delay_level < 4 then return 18
	elseif self.delay_level < 5 then return 16
	elseif self.delay_level < 6 then return 14
	elseif self.delay_level < 7 then return 12
	elseif self.delay_level < 8 then return 10
	elseif self.delay_level < 9 then return 8
	elseif self.delay_level < 13 then return 6
	elseif self.delay_level < 15 then return 5
	else return 4 end
end

function Marathon2020Game:getLineARE()
	return self:getARE()
end

function Marathon2020Game:getDasLimit()
		if self.delay_level < 1 then return 15
	elseif self.delay_level < 3 then return 12
	elseif self.delay_level < 5 then return 9
	elseif self.delay_level < 8 then return 8
	elseif self.delay_level < 10 then return 7
	elseif self.delay_level < 13 then return 6
	elseif self.delay_level < 15 then return 5
	elseif self.delay_level < 20 then return 4
	else return 3 end
end

function Marathon2020Game:getLineClearDelay()
		if self.delay_level < 1 then return 40
	elseif self.delay_level < 3 then return 25
	elseif self.delay_level < 4 then return 20
	elseif self.delay_level < 5 then return 15
	elseif self.delay_level < 7 then return 12
	elseif self.delay_level < 9 then return 8
	elseif self.delay_level < 11 then return 6
	elseif self.delay_level < 14 then return 4
	else return 2 end
end

function Marathon2020Game:getLockDelay()
		if self.delay_level < 6 then return 30
	elseif self.delay_level < 7 then return 26
	elseif self.delay_level < 8 then return 22
	elseif self.delay_level < 9 then return 19
	elseif self.delay_level < 10 then return 17
	elseif self.delay_level < 16 then return 15
	elseif self.delay_level < 17 then return 13
	elseif self.delay_level < 18 then return 11
	elseif self.delay_level < 19 then return 10
	elseif self.delay_level < 20 then return 9
	else return 8 end
end

function Marathon2020Game:getGravity()
		if self.level < 30  then return 4/256
	elseif self.level < 35  then return 6/256
	elseif self.level < 40  then return 8/256
	elseif self.level < 50  then return 10/256
	elseif self.level < 60  then return 12/256
	elseif self.level < 70  then return 16/256
	elseif self.level < 80  then return 32/256
	elseif self.level < 90  then return 48/256
	elseif self.level < 100 then return 64/256
	elseif self.level < 120 then return 80/256
	elseif self.level < 140 then return 96/256
	elseif self.level < 160 then return 112/256
	elseif self.level < 170 then return 128/256
	elseif self.level < 200 then return 144/256
	elseif self.level < 220 then return 4/256
	elseif self.level < 230 then return 32/256
	elseif self.level < 233 then return 64/256
	elseif self.level < 236 then return 96/256
	elseif self.level < 239 then return 128/256
	elseif self.level < 243 then return 160/256
	elseif self.level < 247 then return 192/256
	elseif self.level < 251 then return 224/256
	elseif self.level < 300 then return 1
	elseif self.level < 330 then return 2
	elseif self.level < 360 then return 3
	elseif self.level < 400 then return 4
	elseif self.level < 420 then return 5
	elseif self.level < 450 then return 4
	elseif self.level < 500 then return 3
	else return 20 end
end

local cleared_row_levels = {1, 2, 4, 6}

function Marathon2020Game:advanceOneFrame()
	if self.torikan_hit then
		self.no_roll_frames = self.no_roll_frames + 1
		if self.no_roll_frames > 120 then
			self.completed = true
		end
		return false
	elseif self.clear then
		self.roll_frames = self.roll_frames + 1
		if self.roll_frames < 0 then
			return false
		elseif self.roll_frames > 4000 then
			if self:qualifiesForMRoll() then self.grade = 31 end
			self.completed = true
		end
	elseif self.ready_frames == 0 then
		self.frames = self.frames + 1
	end
	return true
end

local cool_cutoffs = {
	[0] = frameTime(0,45,00),
	frameTime(0,41,50), frameTime(0,38,50), frameTime(0,35,00), frameTime(0,32,50), frameTime(0,29,20),
	frameTime(0,27,20), frameTime(0,24,80), frameTime(0,22,80), frameTime(0,20,60), frameTime(0,19,60),
	frameTime(0,19,40), frameTime(0,19,40), frameTime(0,18,40), frameTime(0,18,20), frameTime(0,16,20),
	frameTime(0,16,20), frameTime(0,16,20), frameTime(0,16,20), frameTime(0,16,20), frameTime(0,15,20)
}

local levels_for_cleared_rows = { 1, 2, 4, 6 }

function Marathon2020Game:onPieceEnter()
	self:updateLevel(1, false)
end

function Marathon2020Game:whilePieceActive()
	if not self.clear then
		self.grade_point_decay_counter = self.grade_point_decay_counter + self.grade + 2
	end
	if self.grade_point_decay_counter > 240 then
		self.grade_point_decay_counter = 0
		self.grade_points = math.max(0, self.grade_points - 1)
	end
end

function Marathon2020Game:onLineClear(cleared_row_count)
	self:updateLevel(levels_for_cleared_rows[cleared_row_count], true)
	self:updateGrade(cleared_row_count)
end

function Marathon2020Game:updateLevel(increment, line_clear)
	local new_level
	if self.torikan_passed[900] == false then
		if line_clear == false and (
			math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or
			self.level == 998
		) then
			new_level = math.min(998, self.level + (99 - self.level % 100))
		else
			new_level = math.min(999, self.level + increment)
		end
	elseif self.torikan_passed[1900] == false then
		if line_clear == false and (
			math.floor((self.level + increment) / 100) > math.floor(self.level / 100) or
			self.level == 1999
		) then
			new_level = math.min(1999, self.level + (99 - self.level % 100))
		else
			new_level = math.min(2000, self.level + increment)
		end
	else
		if line_clear == false and (
			self.level < 1900 and
			math.floor((self.level + increment) / 100) > math.floor(self.level / 100)
		) then
			new_level = self.level + (99 - self.level % 100)
		elseif line_clear == false and self.level + increment > 2019 then
			new_level = 2019
		else
			new_level = math.min(2020, self.level + increment)
		end
	end
	if not self.clear then
		self:updateSectionTimes(self.level, new_level)
		if not self.clear then
			self.level = new_level
		end
	end
end

local low_cleared_line_points = {10, 20, 30, 40}
local mid_cleared_line_points = {2, 6, 12, 24}
local high_cleared_line_points = {1, 4, 9, 20}

local function getGradeForGradePoints(points)
	return math.min(30, math.floor(math.sqrt((points / 50) * 8 + 1) / 2 - 0.5))
	-- Don't be afraid of the above function. All it does is make it so that
	-- you need 50 points to get to grade 1, 100 points to grade 2, etc.
end

function Marathon2020Game:updateGrade(cleared_lines)
	-- update grade points and max grade points
	if self.clear then return end
	local point_level = math.floor(self.level / 100) + self.delay_level
	local plus_points = math.max(
		low_cleared_line_points[cleared_lines],
		mid_cleared_line_points[cleared_lines] * (1 + point_level / 2),
		high_cleared_line_points[cleared_lines] * (point_level - 2),
		(self.level >= 1000 and cleared_lines == 4) and self.grade * 30 or 0
	)
	self.grade_points = self.grade_points + plus_points
	if self.grade_points > self.max_grade_points then
		self.max_grade_points = self.grade_points
	end
	self.grade = getGradeForGradePoints(self.max_grade_points)
end

function Marathon2020Game:getTotalGrade()
	return self.grade + self.section_cool_count
end

local function getSectionForLevel(level)
	if level < 2000 then
		return math.floor(level / 100) + 1
	elseif level < 2020 then
		return 20
	else
		return 21
	end
end

function Marathon2020Game:getEndOfSectionForSection(section)
		if self.torikan_passed[900] == false and section == 10 then return 999
	elseif self.torikan_passed[1900] == false and section == 20 then return 2000
	elseif section == 20 then return 2020
	else return section * 100 end
end

function Marathon2020Game:sectionPassed(old_level, new_level)
	if self.torikan_passed[900] == false then
		return (
			(math.floor(old_level / 100) < math.floor(new_level / 100)) or
			(new_level >= 999)
		)
	elseif self.torikan_passed[1900] == false then
		return (
			(math.floor(old_level / 100) < math.floor(new_level / 100)) or
			(new_level >= 2000)
		)
	else
		return (
			(new_level < 2000 and math.floor(old_level / 100) < math.floor(new_level / 100)) or
			(new_level >= 2020)
		)
	end
end

function Marathon2020Game:checkTorikan(section)
	if section == 5 and self.frames < frameTime(8,00,00) then self.torikan_passed[500] = true end
	if section == 9 and self.frames < frameTime(10,30,00) then self.torikan_passed[900] = true end
	if section == 10 and self.frames < frameTime(10,45,00) then self.torikan_passed[1000] = true end
	if section == 15 and self.frames < frameTime(12,30,00) then self.torikan_passed[1500] = true end
	if section == 19 and self.frames < frameTime(13,15,00) then self.torikan_passed[1900] = true end
end

function Marathon2020Game:checkClear(level)
	if (
		self.torikan_passed[500] == false and level >= 500 or
		self.torikan_passed[900] == false and level >= 999 or
		self.torikan_passed[1000] == false and level >= 1000 or
		self.torikan_passed[1500] == false and level >= 1500 or
		self.torikan_passed[1900] == false and level >= 2000 or
		level >= 2020
	) then

			if self.torikan_passed[500] == false then self.level = 500
		elseif self.torikan_passed[900] == false then self.level = 999
		elseif self.torikan_passed[1000] == false then self.level = 1000
		elseif self.torikan_passed[1500] == false then self.level = 1500
		elseif self.torikan_passed[1900] == false then self.level = 2000
		else self.level = 2020 end

		self.clear = true
		self.grid:clear()
		if (
			self.torikan_passed[900] == false and level >= 999 or
			level >= 2020
		) then
			self.roll_frames = -150
		else
			self.torikan_hit = true
			self.no_roll_frames = -150
		end
	end
end

function Marathon2020Game:updateSectionTimes(old_level, new_level)
	local function sectionCool(section)
		self.section_cool_count = self.section_cool_count + 1
		if section <= 10 then
			self.delay_level = math.min(20, self.delay_level + 1)
		end
		table.insert(self.section_status, "cool")
		self.cool_timer = 300
	end

	local section = getSectionForLevel(old_level)

	if old_level % 100 < 70 and new_level >= math.floor(old_level / 100) * 100 + 70 then
		-- record section 70 time
		section_70_time = self.frames - self.section_start_time
		table.insert(self.secondary_section_times, section_70_time)
	end

	if self:sectionPassed(old_level, new_level) then
		-- record new section
		section_time = self.frames - self.section_start_time
		table.insert(self.section_times, section_time)
		self.section_start_time = self.frames

		if (
			(self.secondary_section_times[section] < cool_cutoffs[self.delay_level]) and
		  	(section == 1 or self.secondary_section_times[section] <= self.secondary_section_times[section - 1] + 120)
		) then
			sectionCool(section)
		else
			table.insert(self.section_status, "none")
		end

		if section > 5 then
			self.delay_level = math.min(20, self.delay_level + 1)
		end
		self:checkTorikan(section)
		self:checkClear(new_level)
	end
end

function Marathon2020Game:updateScore(level, drop_bonus, cleared_lines)
	if cleared_lines > 0 then
		self.score = self.score + (
			(math.ceil((level + cleared_lines) / 4) + drop_bonus) *
			cleared_lines * (cleared_lines * 2 - 1) * (self.combo * 2 - 1)
		)
		self.lines = self.lines + cleared_lines
		self.combo = self.combo + cleared_lines - 1
	else
		self.drop_bonus = 0
		self.combo = 1
	end
end

Marathon2020Game.rollOpacityFunction = function(age)
	if age > 300 then return 0
	elseif age < 240 then return 1
	else return (300 - age) / 60 end
end

Marathon2020Game.mRollOpacityFunction = function(age)
	if age > 4 then return 0
	else return 1 - age / 4 end
end

function Marathon2020Game:qualifiesForMRoll()
--[[

GM-roll requirements

You qualify for the GM roll if you:
- Reach level 2020
- with a grade of 50
- and at least 25,000 grade points
- in less than 13:30.00 total.

]]--

	return self.level >= 2020 and self:getTotalGrade() == 50 and self.grade_points >= 25000 and self.frames <= frameTime(13,30)
end

function Marathon2020Game:drawGrid()
	if self.clear and not (self.completed or self.game_over) then
		if self:qualifiesForMRoll() then
			self.grid:drawInvisible(self.mRollOpacityFunction)
		else
			self.grid:drawInvisible(self.rollOpacityFunction)
		end
	else
		self.grid:draw()
		if self.piece ~= nil and self.level < 100 then
			self:drawGhostPiece()
		end
	end
end

function Marathon2020Game:sectionColourFunction(section)
	if self.section_status[section] == "cool" then
		return { 0, 1, 0, 1 }
	else
		return { 1, 1, 1, 1 }
	end
end

function Marathon2020Game:drawScoringInfo()
	Marathon2020Game.super.drawScoringInfo(self)

	local current_section = getSectionForLevel(self.level)
	local text_x = config["side_next"] and 320 or 240

	love.graphics.setFont(font_3x5_2)
	love.graphics.printf("GRADE", text_x, 100, 40, "left")
	love.graphics.printf("GRADE PTS.", text_x, 200, 90, "left")
	love.graphics.printf("LEVEL", text_x, 320, 40, "left")

	self:drawSectionTimesWithSecondary(current_section, 20)

	if (self.cool_timer > 0) then
				love.graphics.printf("COOL!!", 64, 400, 160, "center")
				self.cool_timer = self.cool_timer - 1
		end

	love.graphics.setFont(font_3x5_3)

	local grade = self:getTotalGrade()
	love.graphics.printf(
		grade > 50 and "GM" or grade,
		text_x, 120, 90, "left"
	)

	love.graphics.printf(self.grade_points, text_x, 220, 90, "left")
	love.graphics.printf(self.level, text_x, 340, 50, "right")

	if self.clear then
		love.graphics.printf(self.level, text_x, 370, 50, "right")
	else
		love.graphics.printf(self:getEndOfSectionForSection(current_section), text_x, 370, 50, "right")
	end

end

function Marathon2020Game:getHighscoreData()
	return {
		grade = self:getTotalGrade(),
		level = self.level,
		frames = self.frames,
	}
end

function Marathon2020Game:getBackground()
	return math.min(19, math.floor(self.level / 100))
end

return Marathon2020Game
