local leaderboard = nil
local maps
local mapIndex = 1
local scrollPos = 1
local modes = nil
local mode = 1
local prefMode = nil

local function mapIndexOffset(n)
	return (mapIndex + n + #maps - 1) % #maps + 1
end

local function getMap(offset)
	return maps[mapIndexOffset(offset or 0)]
end

local function updateModes()
	-- set available modes for this map
	modes = {}
	for mode, scoreTable in pairs(leaderboard) do
		if scoreTable[getMap()] then
			table.insert(modes, mode)
		end
	end
	table.sort(modes)

	mode = 1
	-- select pref mode
	for i, m in ipairs(modes) do
		if m == prefMode then
			mode = i
			break
		end
	end
end

local function updateMapIndex(n)
	mapIndex = mapIndexOffset(n)
	scrollPos = 1

	updateModes()
end

local scalar = 2
local hlfScrnWdth = 320 / 2
local mappY = 26
local ttlY = mappY + FixedMul(30, FRACUNIT / scalar)
local scoresY =  ttlY + 16

local function drawMapPatch(v, offset)
	local scale = FRACUNIT / (abs(offset) + scalar)
	local mapName = G_BuildMapName(getMap(offset))
	local patchName = mapName.."P"
	local mapp = v.patchExists(patchName) and v.cachePatch(patchName) or v.cachePatch("BLANKLVL")

	local scaledWidth = FixedMul(mapp.width, scale)
	local scaledHeight = FixedMul(mapp.height, scale)

	v.drawScaled(
		(hlfScrnWdth + offset * scaledWidth - scaledWidth / 2) * FRACUNIT,
		(mappY - scaledHeight / 2) * FRACUNIT,
		scale,
		mapp
	)
end

local colors = {
	[0] = 0,
	[1] = 215
}
local function drawMapBorder(v)
	local mapWidth = FixedMul(160, FRACUNIT / scalar)
	local mapHeight = FixedMul(100, FRACUNIT / scalar)
	v.drawFill(
		hlfScrnWdth - mapWidth / 2 - 1,
		mappY - mapHeight / 2 -1,
		mapWidth + 2,
		mapHeight + 2,
		colors[leveltime / 4 % 2]
	)
end

local function zoneAct(map)
	local z = ""
	if map.zonttl != "" then
		z = " " + map.zonttl
	elseif not(map.levelflags & LF_NOZONE) then
		z = " Zone"
	end
	if map.actnum != "" then
		z = $ + " " + map.actnum
	end

	return z
end

local function drawMapStrings(v)
	local map = mapheaderinfo[getMap()]
	local titleWidth = v.stringWidth(map.lvlttl)

	-- title
	v.drawString(
		hlfScrnWdth,
		ttlY,
		map.lvlttl,
		V_SKYMAP,
		"center"
	)

	-- zone/act
	local zone = zoneAct(map)
	local zoneWidth = v.stringWidth(zone)
	v.drawString(
		hlfScrnWdth + titleWidth / 2,
		ttlY + 8,
		zone,
		V_SKYMAP,
		"right"
	)

	-- subtitle
	v.drawString(
		hlfScrnWdth + titleWidth / 2 - zoneWidth,
		ttlY + 8,
		map.subttl,
		V_MAGENTAMAP,
		"small-right"
	)
end

local F_SPBATK = 0x1
local F_SPBJUS = 0x2
local F_SPBBIG = 0x4
local F_SPBEXP = 0x8
local F_ENCORE = 0x80

local function drawGamemode(v)
	local m = modes[mode] or 0

	local modeX = 20
	local modeY = scoresY
	local scale = FRACUNIT / 2

	if m == 0 then
		local clockp = v.cachePatch("K_LAPE02")
		v.drawScaled(
			modeX * FRACUNIT,
			modeY * FRACUNIT,
			scale,
			clockp
		)

		v.drawString(
			modeX,
			modeY,
			"Time Attack!"
		)
	elseif m & F_SPBATK then
		local scaledHalf = FixedMul(50 * FRACUNIT, scale) / 2
		local xoff = 0
		if m & F_SPBBIG then
			xoff = $ + scaledHalf
		end
		if m & F_SPBEXP then
			xoff = $ + scaledHalf
		end

		if m & F_SPBBIG then
			local growp = v.cachePatch("K_ITGROW")
			v.drawScaled(
				modeX * FRACUNIT - scaledHalf + xoff,
				modeY * FRACUNIT - scaledHalf,
				scale,
				growp
			)

			xoff = $ - scaledHalf
		end

		if m & F_SPBEXP then
			local invp = v.cachePatch("K_ITINV"..(leveltime / 6 % 7 + 1))
			v.drawScaled(
				modeX * FRACUNIT - scaledHalf + xoff,
				modeY * FRACUNIT - scaledHalf,
				scale,
				invp
			)
		end

		local spbp = v.cachePatch("K_ITSPB")
		v.drawScaled(
			modeX * FRACUNIT - scaledHalf,
			modeY * FRACUNIT - scaledHalf,
			scale,
			spbp
		)

		v.drawString(
			modeX,
			modeY,
			"SPB Attack!"
		)
	end
end

local function drawFlags(v, x, y, flags)
	local nx = x * FRACUNIT
	local ny = y * FRACUNIT + 2 * FRACUNIT
	local margin = 4 * FRACUNIT
	if flags & F_ENCORE then
		local encp = v.cachePatch("RUBYICON")
		v.drawScaled(
			nx,
			ny + 2 * FRACUNIT,
			FRACUNIT / 5,
			encp
		)
		nx = $ + margin
	end
	if flags & F_SPBATK then
		local scale = FRACUNIT / 3
		local shift = 6 * FRACUNIT
		nx = $ - shift
		ny = $ - shift
		if flags & F_SPBJUS then
			local hyup = v.cachePatch("K_ISHYUD")
			v.drawScaled(nx, ny, scale, hyup)
			nx = $ + margin
		end
		if flags & F_SPBBIG then
			local growp = v.cachePatch("K_ISGROW")
			v.drawScaled(nx - FRACUNIT / 2, ny, scale, growp)
			nx = $ + margin
		end
		if flags & F_SPBEXP then
			local invp = v.cachePatch("K_ISINV"..(leveltime / 6 % 6 + 1))
			v.drawScaled(nx, ny, scale, invp)
			nx = $ + margin
		end
	end
end

local function drawStats(v, x, y, skin, stats)
	local s = skins[skin]
	if not (s
		and s.kartspeed == stats["speed"]
		and s.kartweight == stats["weight"]
		)
		and stats then
		v.drawString(x-2, y-2, stats["speed"], V_ALLOWLOWERCASE, "thin")
		v.drawString(x + 13, y + 9, stats["weight"], V_ALLOWLOWERCASE, "thin") 
	end
end

-- draw in columns
-- pos, facerank, name, time, flags
-- ______________________________________________
-- | 3|[O]|InsertNameHere     | 01:02:03 | EXB |
-- ----------------------------------------------
-- defined are widths of each column, x value is calculated below
local column = {
	[1] = 18,   -- facerank, pos, drawNum is right aligned
	[2] = 170,  -- name
	[3] = 60, -- time
	[4] = 0  -- flags
}
do
	local w = 32 -- starting offset
	local t
	for i = 1, #column do
		t = column[i]
		column[i] = w
		w = $ + t
	end
end

local colorFlags = {
	[0] = V_SKYMAP,
	[1] = 0
}
local function drawScore(v, i, pos, score)
	local y = scoresY + i * 18
	local textFlag = colorFlags[pos%2]

	-- position
	v.drawNum(column[1], y, pos)

	-- facerank
	local skin = skins[score["skin"]]
	local facerank = skin and v.cachePatch(skin.facerank) or v.cachePatch("M_NORANK")
	v.draw(column[1], y, facerank, 0, v.getColormap("sonic", score["color"]))

	-- stats
	drawStats(v, column[1], y, score["skin"], score["stat"])
	-- name
	v.drawString(column[2], y, score["name"], V_ALLOWLOWERCASE | textFlag)
	-- time
	v.drawString(column[3], y, rawget(_G, "TicsToTime")(score["time"]), textFlag)
	-- flags
	drawFlags(v, column[4], y, score["flags"])
end

local function drawBrowser(v)
	if not leaderboard then return end

	v.fadeScreen(0xFF00, 16)

	-- previous, next maps
	for i = 5, 1, -1 do
		drawMapPatch(v, -i)
		drawMapPatch(v, i)
	end

	-- draw map border
	drawMapBorder(v)

	-- current map
	drawMapPatch(v, 0)
	drawMapStrings(v)
	drawGamemode(v)

	if not modes then return end

	local gamemode = leaderboard[modes[mode]]
	if not gamemode then return end

	local scoreTable = gamemode[getMap()]
	if not scoreTable then return end

	local scores = #scoreTable
	scrollPos = max(min(scrollPos, scores - 3), 1)
	local endi = min(scrollPos + 7, scores)
	for i = scrollPos, endi do
		drawScore(v, i - scrollPos + 1, i, scoreTable[i])
	end
end
rawset(_G, "DrawBrowser", drawBrowser)

local function initBrowser(lb)
	leaderboard = lb

	-- set mapIndex to current map
	for i, m in ipairs(maps) do
		if m == gamemap then
			mapIndex = i
			break
		end
	end

	scrollPos = 1
	updateModes()
end
rawset(_G, "InitBrowser", initBrowser)

-- initialize maps with racemaps only
local function loadMaps()
	maps = {}
	for i = 0, #mapheaderinfo do
		local map = mapheaderinfo[i]
		if map and map.typeoflevel & TOL_RACE then
			table.insert(maps, i)
		end
	end
end
addHook("MapLoad", loadMaps)

local repeatCount = 0
local keyRepeat = 0

local function updateKeyRepeat()
	S_StartSound(nil, 143)
	if repeatCount < 1 then
		keyRepeat = TICRATE / 4
	else
		keyRepeat = TICRATE / 15
	end
	repeatCount = $ + 1
end

local function resetKeyRepeat()
	keyRepeat = 0
	repeatCount = 0
end

-- return value indicates we want to exit the browser
local function controller(player)
	keyRepeat = max(0, $ - 1)

	if not (player.cmd.driftturn or player.cmd.buttons) then
		resetKeyRepeat()
	end

	local cmd = player.cmd
	if not keyRepeat then
		if cmd.driftturn > 0 then
			updateMapIndex(-1)
			updateKeyRepeat()
		elseif cmd.driftturn < 0 then
			updateMapIndex(1)
			updateKeyRepeat()
		elseif cmd.buttons & BT_FORWARD then
			scrollPos = $ - 1
			updateKeyRepeat()
		elseif cmd.buttons & BT_BACKWARD then
			scrollPos = $ + 1
			updateKeyRepeat()
		elseif cmd.buttons & BT_DRIFT then
			scrollPos = 1
			if modes then
				mode = $ % #modes + 1
				prefMode = modes[mode]
			end
			updateKeyRepeat()
		elseif cmd.buttons & BT_BRAKE then
			S_StartSound(nil, 115)
			return true
		elseif cmd.buttons & BT_ACCELERATE then
			S_StartSound(nil, 143)
			COM_BufInsertText(player, "changelevel "..G_BuildMapName(maps[mapIndex]))
			return true
		end
	end
end
rawset(_G, "BrowserController", controller)


local function netvars(net)
	maps = net($)
	mapIndex = net($)
	modes = net($)
	mode = net($)
	prefMode = net($)
	scrollPos = net($)
	leaderboard = net($)
end
addHook("NetVars", netvars)