local Object = require 'libs.classic'
local bit = require("bit")
local lualzw = require 'libs.lualzw'
local bitser = require 'libs.bitser'
require 'funcs'
require 'settings'

local playedReadySE = false
local playedGoSE = false

local Grid = require 'game.grid'
local Randomizer = require 'game.randomizer'
local Piece = require 'game.piece'

local GameMode = Object:extend()

function GameMode:new(player_name, input_file, replay_grade)
	if player_name == nil then self.training = true else self.training = false end
    if input_file ~= nil then
        input_file = love.filesystem.newFile(REPLAY_DIR..input_file, 'r'):read()
        input_file = lualzw.decompress(input_file)
        seed = self:getInputPieceSeq(input_file)
        self.replay_inputs = self:getReplayInputs(input_file)
        self.randomizer = Randomizer(false, tonumber(seed))
        self.input_playback = true
        self.grade = replay_grade
        self.frames = 1
	elseif self.training then
		player_name = 'TRN'
		replay_grade = 'N/A'
		self.randomizer = Randomizer(false, nil)
        self.input_playback = false
        self.frames = 0
    else
        self.randomizer = Randomizer(false, nil)
        self.input_playback = false
        self.frames = 0
    end
    self.player_name = player_name
    self.grid = Grid(10, 20)
    self.piece = nil
    self.ready_frames = 100
    self.game_over_frames = 0
    self.are = 0
    self.lcd = 0
    self.das = { direction = "none", frames = -1 }
    self.move = "none"
    self.prev_inputs = {}
    self.next_queue = {}
    self.game_over = false
    self.clear = false
    self.completed = false
    self.next_queue_length = 1
    self.irs = true
    self.drop_locked = false
    self.hard_drop_locked = false
    self.cleared_block_table = {}
    self.last_lcd = 0
    self.grade_score = 0
    self.did_grades = false
    self.active_frames = 0
    self.total_lines = 0
    self.lineClearPoints = {[0]=0, 0, 1667, 3750, 6668, 8335}
    self.gradeNames = {
        "19k", "18k", "17k", "16k", "15k", "14k", "13k", "12k", "11k", "10k", 
        "9k", "8k", "7k", "6k", "5k", "4k", "3k", "2k", "1k",
        "1D", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D"
        }
	self.promo_table = {
        26666, 53333, 79999, 106666, 133333, 159999, 186666, 213333, 239999, 266666, 
        293333, 319999, 346666, 373333, 399999, 426666, 453333, 479999, 506666, 
        533333, 559999, 586666, 613333, 639999, 666666, 693333, 719999, 719999
    }
	self.autopromo_table = {
        79999, 106666, 133333, 159999, 186666, 213333, 239999, 266666, 293333, 319999, 
        346666, 373333, 399999, 426666, 453333, 479999, 506666, 533333, 559999, 
        586666, 613333, 639999, 666666, 693333, 719999, 746666, 773333, 1000000
	}
	self.demo_table = {
        -1, 13332, 25000, 40000, 50000, 60000, 60000, 120000, 120000, 120000, 
        180000, 180000, 240000, 240000, 300000, 300000, 360000, 360000, 360000, 
        420000, 420000, 480000, 480000, 480000, 480000, 540000, 540000, 540000
    }
	self.speed_divisor = 10000
    self.line_clear_flash = 0
    self.lines_cleared = 0
    SOUNDS['bgm_firsthalf']:setVolume(0.3)
    SOUNDS['bgm_secondhalf']:setVolume(0.35)
    self.audio_stopped = false
    self.recorded_inputs = {}
    self.input_saved = false
    self.end_grid_clear = false
    self.last_active = 0
	self.move_count = 0
    self.target = 0
    self.last_percent = 0
    self.total_speed_loss = 0
    self.grade_change_flash = 1
    self.grade_change_color = {1,1,1,1}
    self.point_flash = 0
    self.point_flash_color = {1,1,1,1}
    self.end_game_sound = nil
    self.speed_level = 0
    self.last_level = 0
    self.nextbgmflag = false
    self.firstbgmlooped = false
    self.last_holes = 0
    self.last_meter = 0
    self.speed_table = {}
    self.holes_bonus_freeze = 0
    self.holes_bonus = 0
    self.last_speed = 0
    self.holes_bonus_lines = 0
    self.bonus_components = {speed=1666,clean=750}
    self.score_totals = {clean=0,speed=0,lines=0}
    self.score_percents = {clean=0,speed=0,lines=0}
    self.directions_pressed = {}
    self.lines_bonus = 0
    self.moved = false
    self.midspeed_flash = false
    self.lastdir = 1
    self.spin_rng = {0,0,0,0,0,0,0,0,0,0}
    self.lastlock = 0
    self.up_lock = false
    -- gravity, are, lock, das
    self.delay_table = {
        [0]={0.013, 20, 30, 12},{0.015, 20, 30, 12},{0.016, 20, 30, 12},{0.018, 20, 30, 12},{0.021, 20, 30, 12},{0.025, 20, 30, 12},
        {0.027, 20, 30, 12},{0.030, 20, 30, 12},{0.033, 20, 30, 12},{0.038, 20, 30, 12},{0.043, 20, 30, 12},{0.051, 20, 30, 12},
        {0.056, 20, 30, 12},{0.062, 20, 30, 12},{0.070, 20, 30, 12},{0.079, 20, 30, 12},{0.092, 20, 30, 12},{0.109, 20, 30, 12},
        {0.120, 20, 30, 12},{0.134, 20, 30, 12},{0.152, 20, 30, 12},{0.175, 20, 30, 12},{0.206, 20, 30, 12},{0.250, 20, 30, 12},
        {0.281, 20, 30, 12},{0.319, 20, 30, 12},{0.370, 20, 30, 12},{0.441, 20, 30, 12},{0.545, 20, 30, 12},{0.712, 20, 30, 12},
        {0.842, 20, 30, 12},{1.029, 20, 30, 12},{1.323, 20, 30, 12},{1.852, 20, 30, 11},{3.086, 20, 30, 10},{9.259, 20, 30,  9},
        {20, 20, 30,  8},{20, 20, 30,  8},{20, 19, 28,  8},{20, 18, 27,  8},{20, 17, 26,  8},{20, 17, 25,  8},
        {20, 16, 24,  8},{20, 16, 23,  8},{20, 15, 23,  8},{20, 15, 22,  8},{20, 14, 21,  8},{20, 14, 21,  8},
        {20, 14, 20,  8},{20, 13, 20,  8},{20, 13, 20,  8},{20, 13, 19,  8},{20, 13, 19,  8},{20, 13, 19,  8},
        {20, 12, 18,  8}
    }
    if not self.input_playback and not self.training and not PENTO_MODE then 
        self:readGradeHistory()
    end
end

function GameMode:readGradeHistory()
    if love.filesystem.getInfo(SAVE_DIR..self.player_name.."_grade_history.sav") then
        self.grade_history = bitser.loadLoveFile(SAVE_DIR..self.player_name.."_grade_history.sav")
    else 
        self.grade_history = {1,2,0,0}
    end
    self.grade = self.grade_history[1]
    self.starting_grade = self.grade
    if self.grade > 1 then
        temp_grade = copy(self.grade_history)
        temp_grade[2] = 0
        bitser.dumpLoveFile(SAVE_DIR..self.player_name.."_grade_history.sav", temp_grade)
        end
end

function GameMode:readHiScores()
    if love.filesystem.getInfo(HIscoreFILE) then
        self.hi_scores = bitser.loadLoveFile(HIscoreFILE)
    else 
        self.hi_scores = {"TRO",0,"MIT",0,"ROM",0,"ITR",0,"OMI",0}
    end
end

function GameMode:updateGradeHistory()
    promo_string = ""
    if self.grade_score >= self.promo_table[self.grade] then
		if (self.grade == 28 and self.grade_history[2] < 4) or self.grade < 28 then self.grade_history[2] = self.grade_history[2] + 1 end
		self.point_flash = 60
		self.point_flash_color = {0,1,0,1}
	elseif self.grade_score <= self.demo_table[self.grade] then
		if (self.grade == 1 and self.grade_history[2] > 0) or self.grade > 1 then self.grade_history[2] = self.grade_history[2] - 1 end
		self.point_flash = 60
		self.point_flash_color = {1,0,0,1}
	end
	local auto_flag = false
    while self.grade_score >= self.autopromo_table[self.grade] and self.grade < 28 do
        self.grade = self.grade + 1
        self.point_flash = 1
        self.grade_change_flash = 120
        self.grade_change_color = {0,0,1,1}
        self.end_game_sound = "autopromote"
        auto_flag = true
	end
    if self.grade_history[2] >= 5 and self.grade < 28 and auto_flag == false then
        self.grade = self.grade + 1
        self.point_flash = 1
        self.grade_change_flash = 120
        self.grade_change_color = {0,1,0,1}
        self.end_game_sound = "promote"
    elseif self.grade_history[2] < 0 and self.grade > 1 and auto_flag == false then
        self.grade = self.grade - 1
        self.point_flash = 1
        self.grade_change_flash = 120
        self.grade_change_color = {1,0,0,1}
        self.end_game_sound = "demote"
    end
    if self.starting_grade ~= self.grade then
        self.grade_history[1] = self.grade
		self.grade_history[2] = 2
    end
    self.grade_history[4] = self.grade_history[4] + 1
    return promo_string
end

function GameMode:updateHiScores()
    self:readHiScores()
    hiscore_pos = {0,0}
    i = 2
    score_position = -1
    while i <= 10 do
        if score_position == -1 and self.hi_scores[i] < self.grade_score then
            score_position = i
        end
        i = i + 2
    end
    if score_position ~= -1 then
        i = 8
        while i >= score_position-1 do
            self.hi_scores[i+2] = self.hi_scores[i]
            i = i - 1
        end
        self.hi_scores[score_position-1] = self.player_name:upper()
        self.hi_scores[score_position] = self.grade_score
        hiscore_pos = {score_position-1, score_position}
    end
    bitser.dumpLoveFile(HIscoreFILE, self.hi_scores)
    return hiscore_pos
end

function GameMode:getARR() return 1 end
function GameMode:getDropSpeed() return 1 end
function GameMode:getARE()
	if self.training then return 20 end
	if self.speed_level <= #self.delay_table then return self.delay_table[self.speed_level][2]
    else return self.delay_table[#self.delay_table][2] end
end

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

function GameMode:getLockDelay() 
	if self.training then return 99999999999 end
	if self.speed_level <= #self.delay_table then return self.delay_table[self.speed_level][3]
    else return self.delay_table[#self.delay_table][3] end
end

function GameMode:getLineClearDelay() return self:getARE() end

function GameMode:getDasLimit()
	if self.training then return 8 end
	if self.speed_level <= #self.delay_table then return self.delay_table[self.speed_level][4]
    else return self.delay_table[#self.delay_table][4] end
end
function GameMode:getDasCutDelay() return 0 end

function GameMode:getGravity()
	if self.training then return 20 end
	if self.speed_level <= #self.delay_table then return self.delay_table[self.speed_level][1]
    else return self.delay_table[#self.delay_table][1] end
end

function GameMode:getNextPiece(ruleset)
    local shape = self.randomizer:nextPiece()
    return {
        skin = self:getSkin(),
        shape = shape,
        orientation = ruleset:getDefaultOrientation(shape),
    }
end

function GameMode:getSkin()
    return "2tie"
end

function GameMode:initialize(ruleset)
    self.ruleset = ruleset
    for i = 1, math.max(self.next_queue_length, 1) do
        table.insert(self.next_queue, self:getNextPiece(ruleset))
    end
end

function GameMode:saveInputs()
    input_string = 'NEW'..string.format("%16d", self.randomizer.seed)..';'
    for frame, input_set in pairs(self.recorded_inputs) do
        input_string = input_string..string.format('%03d', tostring(input_set))
    end
    local inputfile = love.filesystem.newFile(REPLAY_DIR..tostring(os.time()).."_"..self.player_name.."_"..self.starting_grade.."_"..self.grade_score.."_replay"..'.sav', 'w')
    inputfile:write(lualzw.compress(input_string))
    inputfile:close()
end

function GameMode:storeInput(input_set)
    input_right     = 1
    input_rotright2 = 2
    input_up        = 4
    input_rotleft   = 8
    input_left      = 16
    input_down      = 32
    input_rotright  = 64
    input_rotleft2  = 128
    if not input_set['right'] then input_right = 0 end
    if not input_set['rotate_right2'] then input_rotright2 = 0 end
    if not input_set['up'] then input_up = 0 end
    if not input_set['rotate_left'] then input_rotleft = 0 end
    if not input_set['left'] then input_left = 0 end
    if not input_set['down'] then input_down = 0 end
    if not input_set['rotate_right'] then input_rotright = 0 end
    if not input_set['rotate_left2'] then input_rotleft2 = 0 end
    self.recorded_inputs[self.frames] = bit.bor(input_right, input_rotright2, input_up, input_rotleft, input_left, input_down, input_rotright, input_rotleft2)
end

function GameMode:getInputPieceSeq(input_file)
    local seed = ''
    if string.sub(input_file, 1, 3) == 'NEW' then
        string_start = 4
        self.old_replay = false
    else 
        string_start = 1
        self.old_replay = true
    end
    for i = string_start, #input_file do
        c = string.sub(input_file, i, i)
        if c == ';' then break
        else seed = seed..c
        end
    end
    return seed
end

function GameMode:getReplayInputs(input_file)
    input_right     = 1
    input_rotright2 = 2
    input_up        = 4
    input_rotleft   = 8
    input_left      = 16
    input_down      = 32
    input_rotright  = 64
    input_rotleft2  = 128
    replay_inputs = {}
    reached_inputs = false
    i = 1
    while not reached_inputs do
        c = string.sub(input_file,i,i)
        if c == ';' then
            reached_inputs = true
        end
        i = i+1
    end
    if reached_inputs then
        while i <= #input_file - 3 do
            input_list = {right = false, rotate_right2 = false, up = false, rotate_left = false, left = false, down = false, rotate_right = false, rotate_left2 = false}
            coded_input = tonumber(string.sub(input_file, i, i+2))
            if bit.band(coded_input, input_right) == 1 then input_list["right"] = true end
            if bit.band(coded_input, input_rotright2) == 2 then input_list["rotate_right2"] = true end
            if bit.band(coded_input, input_up) == 4 then input_list["up"] = true end
            if bit.band(coded_input, input_rotleft) == 8 then input_list["rotate_left"] = true end
            if bit.band(coded_input, input_left) == 16 then input_list["left"] = true end
            if bit.band(coded_input, input_down) == 32 then input_list["down"] = true end
            if bit.band(coded_input, input_rotright) == 64 then input_list["rotate_right"] = true end
            if bit.band(coded_input, input_rotleft2) == 128 then input_list["rotate_left2"] = true end
            table.insert(replay_inputs, input_list)
            i = i + 3
        end
    end
    return replay_inputs
end

function GameMode:update(inputs, ruleset)
    if self.input_playback and self.replay_inputs[self.frames] ~= nil then
        inputs = self.replay_inputs[self.frames]
    end
    if self.grade_change_flash > 0 and self.game_over_frames >= 2 then self.grade_change_flash = self.grade_change_flash - 1 end
    if self.point_flash > 0 and self.game_over_frames >= 2 then self.point_flash = self.point_flash - 1 end
    if self.game_over_frames == 2 then
		PlaySEOnce(self.end_game_sound)
	end
    if self.game_over or self.completed then
        self.game_over_frames = self.game_over_frames + 1
        if self.game_over_frames == 1 then self:storeInput(inputs) end
        if self.lcd > 0 then 
            self.lcd = self.lcd - 1
        elseif self.game_over_frames == 30 then
            local cleared_row_count = self.grid:getClearedRowCount()
            self.grid:clearClearedRows()
            self:afterLineClear(cleared_row_count)
        elseif not self.input_playback and not self.input_saved and not self.training and not PENTO_MODE and self.game_over_frames == 50 then
            self:saveInputs()
            self.input_saved = true
        end
        return
    end
    if inputs["up"] and self.training and not self.up_lock then 
        table.remove(self.next_queue, 1)
        table.insert(self.next_queue, self:getNextPiece(ruleset))
        self.up_lock = true
    end
    if not inputs["up"] then self.up_lock = false end
	local dir_list = {"down", "left", "right"}
	for i = 1, #dir_list do
		if inputs[dir_list[i]] and not table.contains(self.directions_pressed, dir_list[i]) then 
			table.insert(self.directions_pressed, dir_list[i])
		elseif not inputs[dir_list[i]] then
			for j=1, #self.directions_pressed do
				if self.directions_pressed[j] == dir_list[i] then table.remove(self.directions_pressed, j) end
			end
		end
	end
	if #self.directions_pressed > 0 then
		for i=1, #self.directions_pressed-1 do
			inputs[self.directions_pressed[i]] = false
		end
		inputs[self.directions_pressed[#self.directions_pressed]] = true
		if inputs['left'] then self.lastdir = -1
		elseif inputs['right'] then self.lastdir = 1 end
	end

    -- advance one frame
    
    if self:advanceOneFrame(inputs, ruleset) == false then return end
    
    self:chargeDAS(inputs, self:getDasLimit(), self:getARR())

    -- set attempt flags
    if inputs["left"] or inputs["right"] then self:onAttemptPieceMove(self.piece, self.grid) end
    if (
        inputs["rotate_left"] or inputs["rotate_right"] or
        inputs["rotate_left2"] or inputs["rotate_right2"]
    ) then
        self:onAttemptPieceRotate(self.piece, self.grid)
    end
   
    if self.piece == nil then
        self:processDelays(inputs, ruleset)
    else
        self:whilePieceActive()
        local gravity = self:getGravity()

        -- diff vars to use in checks
        local piece_y = self.piece.position.y
        local piece_x = self.piece.position.x
        local piece_rot = self.piece.rotation
        ruleset:processPiece(
            inputs, self.piece, self.grid, gravity, self.prev_inputs, self.move,
            self:getLockDelay(), self:getDropSpeed(),
            self.drop_locked, self.hard_drop_locked,
            false, false, false, self.lastdir
        )

        local piece_dy = self.piece.position.y - piece_y
        local piece_dx = self.piece.position.x - piece_x
        local piece_drot = self.piece.rotation - piece_rot
        
        -- das cut
        if (
            (piece_dy ~= 0 and (inputs.up or inputs.down)) or
            (piece_drot ~= 0 and (
                inputs.rotate_left or inputs.rotate_right or
                inputs.rotate_left2 or inputs.rotate_right2
            ))
        ) then
            self:dasCut()
        end

        if (piece_dx ~= 0) then
            self.piece.last_rotated = false
            self:onPieceMove(self.piece, self.grid, piece_dx)
        end
        if (piece_dy ~= 0) then
            self.piece.last_rotated = false
            self:onPieceDrop(self.piece, self.grid, piece_dy)
        end
        if (piece_drot ~= 0) then
            self.piece.last_rotated = true
            self:onPieceRotate(self.piece, self.grid, piece_drot)
        end

        if inputs["down"] == true then
            if self.piece:isDropBlocked(self.grid) and not self.drop_locked then
                self.lastlock = self.piece.lock_delay
                self.piece.locked = true
                self.piece_soft_locked = true
            end
        end

        if self.piece.locked == true then
            local last_x = self.piece.position.x
            self.grid:applyPiece(self.piece)

            local cleared_row_count = self.grid:getClearedRowCount()
            self:onPieceLock(self.piece, cleared_row_count)
            if not self.training then self:updateScore(cleared_row_count) end

            self.cleared_block_table = self.grid:markClearedRows()
            self.piece = nil

            if cleared_row_count > 0 then
                for i=1, #self.spin_rng do
                    local spin = math.abs(i-(last_x+3))
                    --if spin < 0 then spin = 0 end
                    self.spin_rng[i] = spin
                end
                PlaySE("erase")
                self.lcd = self:getLineClearDelay()
                self.last_lcd = self.lcd
                self.are = self:getLineARE()
                if self.lcd == 0 then
                    self.grid:clearClearedRows()
                    self:afterLineClear(cleared_row_count)
                    if self.are == 0 then
                        self:initializeOrHold(inputs, ruleset)
                    end
                end
                self:onLineClear(cleared_row_count)
            else
                self.are = self:getARE()
            end
        
        end
    end
    self.moved = false
    if not self.input_playback and not self.game_over and not self.completed and not self.training and not PENTO_MODE then self:storeInput(inputs) end
    self.prev_inputs = inputs
end

function GameMode:detectHoles(x, y)
    if y < 1 then return true end -- reached the top, no hole
    local cell = self.grid:getCell(x, y)
    if table.contains(self.visited, x..'.'..y) then 
        return false
    else
        table.insert(self.visited, x..'.'..y)
    end
    if cell.colour ~= "" or cell.oob then return false end
    if self:detectHoles(x+1, y, attempt_num) then return true end
    if self:detectHoles(x-1, y, attempt_num) then return true end
    if self:detectHoles(x, y+1, attempt_num) then return true end
    if self:detectHoles(x, y-1, attempt_num) then return true end
    return false
end

function GameMode:stackQualityCheck()
    local hole_num = 0
    self.visited = {}
    for x=1, 10 do
        for y = 1, 20 do
            if self.grid:getCell(x, y).colour == "" and not table.contains(self.visited, x..'.'..y) then
                stack_clean = self:detectHoles(x, y)
                if not stack_clean then
                    hole_num = hole_num + 1
				end
            end
        end
    end
    return hole_num
end

function GameMode:doStackQuality()
    local total_speed = 0
    local contiguous_holes = self:stackQualityCheck()
    local total_speed = 0
    if contiguous_holes > self.last_holes then 
        self.speed_table[1] = 0
        self.last_percent = 0
    end
    for i=1, #self.speed_table do
        total_speed = total_speed + self.speed_table[i]
    end
    if total_speed <= 0 then total_speed = 0.001 end
    self.total_speed_loss = total_speed / #self.speed_table
    if #self.speed_table == 0 then self.total_speed_loss = 0 end
	self.last_holes = contiguous_holes
end

function GameMode:doSpeedCheck()
	self.target = self.move_count
	local speed = (self.target/self.active_frames)
	if speed > 1 then speed = 1 end
	table.insert(self.speed_table, 1, speed)
    while #self.speed_table > 50 do
        table.remove(self.speed_table, #self.speed_table)
    end
	local total_speed = 0
	for i=1, #self.speed_table do
		total_speed = total_speed + self.speed_table[i]
	end
    if total_speed <= 0 then total_speed = 0.001 end
	self.total_speed_loss = total_speed / #self.speed_table
    if #self.speed_table == 0 then self.total_speed_loss = 0 end
	self.last_active = self.active_frames
	self.last_percent = speed
    self.active_frames = 0
    self.move_count = 0
end

function GameMode:updateScore(cleared_lines)
    self:doSpeedCheck()
    if cleared_lines >= 1 then
        while cleared_lines+self.total_lines > 300 do
            cleared_lines = cleared_lines - 1
        end
		self.last_speed = math.ceil(self.total_speed_loss * self.bonus_components['speed'] * cleared_lines)
        self.line_clear_flash = 240
        self.lines_cleared = cleared_lines
        self.score_to_add = self.lineClearPoints[cleared_lines] + self.last_speed
        self.grade_score = self.grade_score + self.score_to_add
        self.last_meter = self.holes_bonus + self.last_speed
        self.speed_level = math.floor(self.grade_score / self.speed_divisor)
        self.score_totals['speed'] = self.score_totals['speed'] + self.last_speed
        self.score_totals['lines'] = self.score_totals['lines'] + self.lineClearPoints[cleared_lines]
    end
end

function GameMode:advanceOneFrame()
    if self.ready_frames ~= 0 and not self.audio_stopped and not self.training then
        if SOUNDS['bgm_title']:getVolume() > 0.01 then SOUNDS['bgm_title']:setVolume(SOUNDS['bgm_title']:getVolume()-0.01) end
        if self.ready_frames == 5 then
            love.audio.stop()
            self.audio_stopped = true
        end
	end
    if self.training and not SOUNDS['bgm_title']:isPlaying() and SETTINGS["music"] then SOUNDS['bgm_title']:play() end
	if not self.training then
		if self.nextbgmflag and SOUNDS['bgm_firsthalf']:isPlaying() then
			if SOUNDS['bgm_firsthalf']:getVolume() > 0.1 then
				SOUNDS['bgm_firsthalf']:setVolume(SOUNDS['bgm_firsthalf']:getVolume()-0.01)
			else
				SOUNDS['bgm_firsthalf']:stop()
			end
		end
		if self.ready_frames < 1 and not SOUNDS['bgm_firsthalf']:isPlaying() and not SOUNDS['bgm_secondhalf']:isPlaying() and SETTINGS["music"] then
			if not self.nextbgmflag then SOUNDS['bgm_firsthalf']:play()
			elseif self.total_lines < 296 then SOUNDS['bgm_secondhalf']:play() end
		end
		if self.total_lines >= 296 then
			SOUNDS['bgm_firsthalf']:stop()
			SOUNDS['bgm_secondhalf']:stop()
		end
	end
    if self.clear then 
        self.completed = true 
    end
    self.frames = self.frames + 1
    if self.input_playback == true and self.old_replay == true and self.ready_frames ~= 0  then
        self.frames = 0
    end
    if self.line_clear_flash > 0 then self.line_clear_flash = self.line_clear_flash - 1 end
    if self.line_clear_flash == 0 then 
        self.lines_cleared = 0
        self.score_to_add = 0
        self.last_meter = 0
        self.holes_bonus = 0
        self.last_speed = 0
        self.holes_bonus_lines = 0
        self.lines_bonus_lines = 0
        self.lines_bonus = 0
    end
    return true
end

-- event functions
function GameMode:whilePieceActive()
    self.active_frames = self.active_frames + 1
end
function GameMode:onAttemptPieceMove(piece, grid) end
function GameMode:onAttemptPieceRotate(piece, grid) end
function GameMode:onPieceMove(piece, grid, dx)
	if not self.moved then
		self.move_count = self.move_count + 1
		self.moved = true
	end
end
function GameMode:onPieceRotate(piece, grid, drot)
	if not self.moved then
		self.move_count = self.move_count + 1
		self.moved = true
	end
end
function GameMode:onPieceDrop(piece, grid, dy)
	if not self.moved then
		self.move_count = self.move_count + 1
		self.moved = true
	end
end
function GameMode:onPieceLock(piece, cleared_row_count) 
	if not self.moved then
		self.move_count = self.move_count + 1
		self.moved = true
	end
	self.lastdir = 0
    PlaySE("lock")
end

function GameMode:onLineClear(cleared_row_count)
    if not self.training then self.total_lines = math.min(self.total_lines + cleared_row_count, 300) end
    if self.total_lines == 300 and not self.clear then
        self.clear = true
    end
end

function GameMode:afterLineClear(cleared_row_count) end

function GameMode:onPieceEnter()
    self:doStackQuality()
end

function GameMode:onGameOver()
    if not self.input_playback and not self.training and not PENTO_MODE then
        if not self.did_grades then
			self.grade_score = self.grade_score + self.speed_level
            promo_string = self:updateGradeHistory()
            hiscore_pos = self:updateHiScores()
            bitser.dumpLoveFile(SAVE_DIR..self.player_name.."_grade_history.sav", self.grade_history)
            self.did_grades = true
        end
        self:drawEndScoringInfo()
    elseif not self.did_grades then
		self.grade_score = self.grade_score + self.speed_level
		self.did_grades = true
	end
    if PENTO_MODE and not self.training then self:drawEndScoringInfo() end
end

function GameMode:drawEndScoringInfo()
    love.graphics.setColor(1, 1, 1, 1)
	drawText("Score: ", 247, 135, 1000, "left")
	drawBigText(string.format("%s", self.grade_score), 247, 150, 100, "center")
    if not PENTO_MODE then
        drawText("Best scores:", 247, 220, 1000, "left")
        
        i = 2
        while i <= 10 do
            if i == hiscore_pos[1] or i == hiscore_pos[2] then
                drawText(self.hi_scores[i-1]..' - '..self.hi_scores[i], 255, 220+(i*10), 1000, "left", {0,1,0,1})
            else
                drawText(self.hi_scores[i-1]..' - '..self.hi_scores[i], 255, 220+(i*10), 1000, "left")
            end
            i = i + 2
        end
    end
end

function GameMode:onGameComplete()
    self:onGameOver()
end

-- DAS functions

function GameMode:startRightDAS()
    self.move = "right"
    self.das = { direction = "right", frames = 0 }
    if self:getDasLimit() == 0 then
        self:continueDAS()
    end
end

function GameMode:startLeftDAS()
    self.move = "left"
    self.das = { direction = "left", frames = 0 }
    if self:getDasLimit() == 0 then
        self:continueDAS()
    end
end

function GameMode:continueDAS()
    local das_frames = self.das.frames + 1
    if das_frames >= self:getDasLimit() then
        if self.das.direction == "left" then
            self.move = (self:getARR() == 0 and "speed" or "") .. "left"
            self.das.frames = self:getDasLimit() - self:getARR()
        elseif self.das.direction == "right" then
            self.move = (self:getARR() == 0 and "speed" or "") .. "right"
            self.das.frames = self:getDasLimit() - self:getARR()
        end
    else
        self.move = "none"
        self.das.frames = das_frames
    end
end

function GameMode:stopDAS()
    self.move = "none"
    self.das = { direction = "none", frames = -1 }
end

function GameMode:chargeDAS(inputs)
    if inputs[self.das.direction] == true then
        self:continueDAS()
    elseif inputs["right"] == true then
        self:startRightDAS()
    elseif inputs["left"] == true then
        self:startLeftDAS()
    else
        self:stopDAS()
    end
end

function GameMode:dasCut()
    self.das.frames = math.max(
        self.das.frames - self:getDasCutDelay(),
        -(self:getDasCutDelay() + 1)
    )
end

function GameMode:processDelays(inputs, ruleset, drop_speed)
    if self.ready_frames == 100 then
        playedReadySE = false
        playedGoSE = false
    end
    if self.ready_frames > 0 then
        if not playedReadySE then
            playedReadySE = true
            PlaySEOnce("ready")
        end
        self.ready_frames = self.ready_frames - 1
        if self.ready_frames == 0 then
            self:initializeOrHold(inputs, ruleset)
        end
    elseif self.lcd > 0 then
        self.lcd = self.lcd - 1
        if self.lcd == 0 then
            local cleared_row_count = self.grid:getClearedRowCount()
            self.grid:clearClearedRows()
            self:afterLineClear(cleared_row_count)
            PlaySE("fall")
            if self.are == 0 then
                self:initializeOrHold(inputs, ruleset)
            end
        end
    elseif self.are > 0 then
        self.are = self.are - 1
        if self.are == 0 then
            self:initializeOrHold(inputs, ruleset)
        end
    end
end

function GameMode:initializeOrHold(inputs, ruleset)
    self:initializeNextPiece(inputs, ruleset, self.next_queue[1])
    self:onPieceEnter()
    if not self.grid:canPlacePiece(self.piece) then
        self.game_over = true
    end
    ruleset:dropPiece(
        inputs, self.piece, self.grid, self:getGravity(),
        self:getDropSpeed(), self.drop_locked, self.hard_drop_locked
    )
end

function GameMode:initializeNextPiece(inputs, ruleset, piece_data, generate_next_piece)
    self.piece = ruleset:initializePiece(
        inputs, piece_data, self.grid, self:getGravity(),
        self.prev_inputs, self.move,
        self:getLockDelay(), self:getDropSpeed(),
        self.drop_locked, self.hard_drop_locked, false,
        (
            self.frames == 0 or self:getARE() ~= 0
        ) and self.irs or false, self.lastdir
    )
    self.piece_hard_dropped = false
    self.piece_soft_locked = false
    if self.piece:isDropBlocked(self.grid) and
       self.grid:canPlacePiece(self.piece) then
        PlaySE("bottom")
    end
    if generate_next_piece == nil then
        table.remove(self.next_queue, 1)
        table.insert(self.next_queue, self:getNextPiece(ruleset))
    end
end

function GameMode:animation(x, y, skin, colour)
    return {
        1, 1, 1,
        -0.25 + 1.25 * (self.lcd / self.last_lcd),
        skin, colour,
        200 + x * 16, y * 16
    }
end

function GameMode:canDrawLCA()
    return self.lcd > 0
end

function GameMode:drawLineClearAnimation()
    local grid_position = {x=200,y=64}
    for y, row in pairs(self.cleared_block_table) do
        for x, block in pairs(row) do
            local real_x = (x * 16) + grid_position['x']
            local real_y = (y * 16) + grid_position['y']
            local drift = self.spin_rng[x]*-1
            local fall_timer = (self:getLineClearDelay()-self.lcd)/2
            fall_timer = (fall_timer*fall_timer)+drift
            fade_timer = self.lcd/20
            love.graphics.setColor(1,1,1,fade_timer)
            love.graphics.draw(BLOCKS[block.skin][block.colour..'_d'], real_x, real_y+fall_timer)
			if self.lcd > self:getLineClearDelay() - 5 then
				love.graphics.setColor(1,1,1,fade_timer*0.3)
				love.graphics.draw(BLOCKS[block.skin]['W'], real_x, real_y+fall_timer)
			end
        end
    end
end

function GameMode:drawPiece()
    if self.piece ~= nil then
        local b = (
            1 - (self.piece.lock_delay / self:getLockDelay())
        )
        self.piece:draw(0.50 + 0.50 * b, 0.50 + 0.50 * b, self.grid)
    end
end

function GameMode:drawNextQueue(ruleset)
    local colourscheme
    function drawPiece(piece, skin, offsets, pos_x, pos_y)
        for index, offset in pairs(offsets) do
            local x = offset.x + ruleset:getDrawOffset(piece, rotation).x + ruleset.spawn_positions[piece].x
            local y = offset.y + ruleset:getDrawOffset(piece, rotation).y + 4.7
            love.graphics.draw(BLOCKS[skin][COLOUR_SCHEMES.Arika[piece]], pos_x+x*16, pos_y+y*16)
        end
    end
    for i = 1, self.next_queue_length do
        love.graphics.setColor(1, 1, 1, 1)
        local highness = -54
        local next_piece = self.next_queue[i].shape
        local skin = self.next_queue[i].skin
        local rotation = self.next_queue[i].orientation
        drawPiece(next_piece, skin, ruleset.block_offsets[next_piece][rotation], 136+i*80, highness)
    end
    return false
end

function GameMode:getBackground()
    bg = math.floor(self.speed_level/6)
    if bg > 9 then bg = 9 end
    if (self.total_lines >= 130 or self.frames >= 23640) and self.last_level ~= bg then self.nextbgmflag = true end
    self.last_level = bg
    return bg
end

function GameMode:drawGrid()
    local greyscale = false
    if self.game_over or self.completed then greyscale = true end
    self.grid:draw(greyscale, (self.game_over_frames/50))
end

function GameMode:drawInputDisplay(left, top)
	if self.replay_inputs[self.frames] ~= nil then
		drawText("+", left+10, top+8, 1000, "left")
		drawText("v", left+10, top+18, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['down']),1,1-boolToInt(self.replay_inputs[self.frames]['down']),1})
		drawText("<", left, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['left']),1,1-boolToInt(self.replay_inputs[self.frames]['left']),1})
		drawText(">", left+20, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['right']),1,1-boolToInt(self.replay_inputs[self.frames]['right']),1})
		drawText("L", left+35, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['rotate_left']),1,1-boolToInt(self.replay_inputs[self.frames]['rotate_left']),1})
		drawText("R", left+50, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['rotate_right']),1,1-boolToInt(self.replay_inputs[self.frames]['rotate_right']),1})
		drawText("L", left+65, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['rotate_left2']),1,1-boolToInt(self.replay_inputs[self.frames]['rotate_left2']),1})
		drawText("R", left+80, top+8, 1000, "left",{1-boolToInt(self.replay_inputs[self.frames]['rotate_right2']),1,1-boolToInt(self.replay_inputs[self.frames]['rotate_right2']),1})
	else
		drawText("+", left+10, top+8, 1000, "left")
		drawText("v", left+10, top+18, 1000, "left")
		drawText("<", left, top+8, 1000, "left")
		drawText(">", left+20, top+8, 1000, "left")
		drawText("L", left+35, top+8, 1000, "left")
		drawText("R", left+50, top+8, 1000, "left")
		drawText("L", left+65, top+8, 1000, "left")
		drawText("R", left+80, top+8, 1000, "left")
	end
end

function GameMode:drawSpeedStats(left, top)
	love.graphics.setColor(0,0,0,0.5)
	love.graphics.rectangle("fill", left+3, top+3, 190, 145, 10, 10)
	love.graphics.setColor(0.05,0.05,0.05,1)
	love.graphics.rectangle("fill", left, top, 190, 145, 10, 10)
    drawText("Efficiency Bonus: ", left+15, top+10, 1000, "left")
    local lines = self.total_lines
	if lines == 0 then lines = 1 end
	if self.move_count == 0 then
		drawText(string.format("  %4d     Num. of Moves", self.target), left+15, top+25, 1000, "left")
	else
		drawText(string.format("  %4d     Num. of Moves", self.move_count), left+15, top+25, 1000, "left")
	end
	drawText(string.format("/ %4d     Active Frames", self.last_active), left+15, top+40, 1000, "left")
	drawText(string.format("=  %1.2f\n  (0 added for hole)\n   %1.2f   %dpc Average\nx %s x Lines\n+ %4d", self.last_percent, self.total_speed_loss, #self.speed_table, self.bonus_components['speed'], self.last_speed), left+15, top+55, 1000, "left")
end

function GameMode:drawLinesStats(left, top)
	love.graphics.setColor(0,0,0,0.5)
	love.graphics.rectangle("fill", left+3, top+3, 190, 90, 10, 10)
	love.graphics.setColor(0.05,0.05,0.05,1)
	love.graphics.rectangle("fill", left, top, 190, 90, 10, 10)
    local lines = self.total_lines
	if lines == 0 then lines = 1 end
    drawText("Lines Bonus: ", left+15, top+10, 1000, "left")
	--drawText(string.format("+ %4d              %3d%%", self.lineClearPoints[self.lines_cleared], (self.score_totals['lines']/(self.lineClearPoints[4]*math.ceil(lines/4)))*100), left+15, top+25, 1000, "left")
	drawText(string.format("2 x Lines = %d\n3 x Lines = %d\n4 x Lines = %d", self.lineClearPoints[2], self.lineClearPoints[3], self.lineClearPoints[4]), left+15, top+25, 1000, "left")
end

function GameMode:drawScoringInfo()
    love.graphics.setColor(0,0,0,0.5)
    love.graphics.rectangle("fill", 98, 83, 110, 180, 10, 10) --name, grade
	love.graphics.setColor(0,0,0,0.5)
	love.graphics.rectangle("fill", 241, 407, 116, 40, 5, 5) --lines
	love.graphics.setColor(0.05,0.05,0.05,1)
	love.graphics.rectangle("fill", 239, 405, 116, 40, 5, 5) --lines
    love.graphics.rectangle("fill", 95, 80, 110, 180, 10, 10) --name, grade
	drawText(string.format("Level:  %2d\nLines: %3d/300", self:getBackground(), self.total_lines), 249, 408, 1000, "left")
    if not PENTO_MODE then drawText("Grade:", 100, 128, 1000, "left") end
    if self.input_playback then
		love.graphics.setColor(0,0,0,0.5)
		love.graphics.rectangle("fill", 68, 270, 140, 190, 10, 10)
		love.graphics.setColor(0.05,0.05,0.05,1)
		love.graphics.rectangle("fill", 65, 267, 140, 190, 10, 10)
        drawText(string.format("Replay in progress\nRotate Left:\n   Pause/Frame Step\nRotate Right:\n   Unpause\nLeft:\n   Rewind 5 sec\nRight:\n   FF 10 secs\n%s", formatTime(self.frames)), 70, 275, 1000, "left")
        drawBigText(string.format("%s", self.grade), 100, 143, 1000, "left")
        self:drawInputDisplay(103,185)
	elseif self.training or PENTO_MODE then
    else
		if math.mod(self.grade_change_flash, 5) ~= 0 then 
			drawBigText(string.format("%s", self.gradeNames[self.grade]), 100, 143, 1000, "left", self.grade_change_color)
		else
			drawBigText(string.format("%s", self.gradeNames[self.grade]), 100, 143, 1000, "left")
		end
		local points_text = nil
        if self.grade == 1 and self.grade_history[2] == 2 then
            points_text = "  |.."
        elseif self.grade == 1 and self.grade_history[2] == 3 then
            points_text = "  .|."
        elseif self.grade == 1 and self.grade_history[2] == 4 then
            points_text = "  ..|"
		elseif self.grade_history[2] == 0 then
			points_text = "|...."
		elseif self.grade_history[2] == 1 then
			points_text = ".|..."
		elseif self.grade_history[2] == 2 then
			points_text = "..|.."
		elseif self.grade_history[2] == 3 then
			points_text = "...|."
		elseif self.grade_history[2] == 4 then
			points_text = "....|"
		end
		if self.grade > 1 then points_text = '-'..points_text
		else points_text = ' '..points_text end
		if self.grade < 28 then points_text = points_text..'+' end
		drawText("Promotion\nMeter:", 100, 174, 1000, "left")
		if self.point_flash > 0 then
			drawBigText(points_text, 100, 208, 1000, "left", self.point_flash_color)
		else
			drawBigText(points_text, 100, 208, 1000, "left")
		end
    end
    if (self.game_over or self.completed) and self.game_over_frames <= 50 and not self.input_playback and not self.training and not PENTO_MODE then
        drawText("SAVING, PLEASE WAIT", 232, 460, 1000, "left")
    end
    drawText("Name:", 100, 83, 1000, "left")
    drawBigText(self.player_name:upper(), 100, 98, 1000, "left")
    drawText("Ver. 2", 550, 435, 1000, "left")
    if self.input_playback then
		self:drawSpeedStats(385, 99)
		self:drawLinesStats(385, 251)
        love.graphics.setColor(0,0,0,0.5)
        love.graphics.rectangle("fill", 385+3, 348+3, 190, 50, 10, 10)
        love.graphics.setColor(0.05,0.05,0.05,1)
        love.graphics.rectangle("fill", 385, 348, 190, 50, 10, 10)
        drawText(string.format("Added: %d\nScore: %d",self.score_to_add, self.grade_score), 392, 356, 1000, "left")
    end
    

    if self.clear or self.game_over then
        SOUNDS['bgm_firsthalf']:stop()
        SOUNDS['bgm_secondhalf']:stop()
    end
end

function GameMode:drawBackground()
    local bg = self:getBackground()
    local brightness = 0.9
    local limit = 4
    if self.training then bg = 0 end
    --if self.training then bg = math.mod(math.floor(((self.frames+1)/1200)), 10) end
    if bg == 0 or bg == 6 or bg == 9 then limit = 5 end
    if bg == 5 then limit = 4.76 end
    if not BACKGROUNDS[bg]:isPlaying() then
        BACKGROUNDS[bg]:play()
    end
    if BACKGROUNDS[bg]:tell() >= limit then
            BACKGROUNDS[bg]:rewind()
    end
	if bg == 0 or bg == 8 or bg == 9 or bg == 3 then brightness = 0.7 end
	love.graphics.setColor(brightness, brightness, brightness, 1)
    love.graphics.draw(
        BACKGROUNDS[bg],
        0, 0, 0,
        0.5, 0.5
    )
end

function GameMode:drawFrame()
	love.graphics.setColor(1, 1, 1, 1)
	love.graphics.line(216,80,216,80+(16*self.grid.height))
	love.graphics.line(216+(16*self.grid.width),80,216+(16*self.grid.width),80+(16*self.grid.height))
	love.graphics.line(216,80+(16*self.grid.height),216+(16*self.grid.width),80+(16*self.grid.height))
	love.graphics.line(216,80,216+(16*self.grid.width),80)
    love.graphics.setColor(0, 0, 0, 1)
    love.graphics.rectangle(
        "fill", 216, 80,
        16 * self.grid.width, 16 * (self.grid.height)
    )
end

function GameMode:drawReadyGo()
    -- ready/go graphics
    love.graphics.setColor(1, 1, 1, 1)

    if self.ready_frames <= 100 and self.ready_frames > 52 then
        drawBigText("Ready...", 246, 240 - 14, 1000)
    elseif self.ready_frames <= 50 and self.ready_frames > 2 then
        drawBigText("Go!", 276, 240 - 14, 1000)
    end
end

function GameMode:drawCustom() end

function GameMode:draw(paused)
    self:drawBackground()
    self:drawFrame()
    self:drawGrid()
    self:drawPiece()
    self:drawNextQueue(self.ruleset)
    if not self.training then self:drawScoringInfo() end
    self:drawReadyGo()
    self:drawCustom()
    if self:canDrawLCA() then
        self:drawLineClearAnimation()
    end

    if self.completed then
        self:onGameComplete()
    elseif self.game_over then
        self:onGameOver()
    end
end

return GameMode
