-- LUALOCALS < ---------------------------------------------------------
local math, minetest, pairs, piredo_api, piredo_player, piredo_schems,
      string, tonumber, vector
    = math, minetest, pairs, piredo_api, piredo_player, piredo_schems,
      string, tonumber, vector
local math_ceil, math_log, math_random, string_format, string_sub
    = math.ceil, math.log, math.random, string.format, string.sub
-- LUALOCALS > ---------------------------------------------------------

local api = piredo_api.get_mod_api()
local statedb = api.statedb

-- Smallest whole number that starts players in a "normal" domestic room
-- (dining) instead of halls or more exotic rooms.
local rngseed = 7

-- Las Vegas algorithm super-deterministic RNG for "next room"
-- selection, which should yield the exact same sequence of rooms
-- for a given navigation path through the game, with no
-- platform-specific differences and no modulo bias.
local function staterand(state, min, max)
	statedb.rng = minetest.sha1((statedb.rng or rngseed)
		.. statedb.moveqty .. statedb.moves)
	local diff = max - min
	if diff <= 0 then return min end
	local digits = math_ceil(math_log(diff + 1) / math_log(16))
	local picked = tonumber(string_sub(statedb.rng, 1, digits), 16)
	if picked <= diff then return picked + min end
	return staterand(state, min, max)
end

local navkeys = {
	SNSN = "large_forrest_maze",
	NSNS = "large_forrest_maze",
	WEWE = "large_forrest_maze",
	EWEW = "large_forrest_maze",
	WSEN = "large_forge",
	SENW = "large_forge",
	ENWS = "large_forge",
	NWSE = "large_forge",
	ENWEN = "special_candle_room",
	SWEXNWSE = "large_clock_room"
}
for k in pairs({ESWN = 1, SWNE = 1, WNES = 1, NESW = 1}) do
	navkeys[k] = "large_throneroom"
	navkeys["EEEX" .. k] = "large_tower"
	navkeys["NNNX" .. k] = "large_lake"
	navkeys["WWWX" .. k] = "large_graveyard"
	navkeys["SSSX" .. k] = "special_final_room"
end

local function addmove(key)
	statedb.moves = key .. (statedb.moves
		and string_sub(statedb.moves, 1, 20) or "")
end

api.chooseroom = function()
	if statedb.moves == "N" then
		return "special_blocked"
	end

	for k, v in pairs(navkeys) do
		if piredo_api.starts_with(statedb.moves, k) then
			if v == "special_final_room" and statedb.final_done then
				return "special_final_room_2"
			end
			return v, true
		end
	end

	local forced = statedb.forced
	if forced then
		statedb.forced = nil
		return forced
	end

	if statedb.moveqty > 5 and staterand(statedb, 1, 5) == 1 then
		local quads = api.find_schematics("quad_hall")
		return quads[staterand(statedb, 1, #quads)]
	end

	if not (statedb.queue and statedb.queue[1]) then
		statedb.queue = api.find_schematics("normal_")

		-- Secret bonus for people who stick around after
		-- uncovering the exit.
		if api.statedb.final_done then
			statedb.queue[#statedb.queue + 1] = "mystery_gold"
		end
	end
	local pickid = staterand(statedb, 1, #statedb.queue)
	local pick = statedb.queue[pickid]
	statedb.queue[pickid] = statedb.queue[#statedb.queue]
	statedb.queue[#statedb.queue] = nil
	return pick
end

local putschem = api.putschem
local clearschems = api.clearschems
local function moveroom(movekey, ppos, roompos)
	if ppos then piredo_player.save_player_pos(ppos) end

	statedb.lastmove = movekey
	statedb.lastpos = roompos
	addmove(movekey)
	statedb.moveqty = (statedb.moveqty or 0) + 1
	local room, special = api.chooseroom()
	if special then addmove("X") end
	api.savedb()

	local big = piredo_api.starts_with(room, "large_")
	minetest.log("action", string_format("entering room %q%s",
			room, big and " (large)" or ""))

	do
		local dt = piredo_schems[room] and piredo_schems[room].daytime
		if dt and dt >= 0.3 and dt <= 0.7 then
			dt = dt - 0.05 + math_random() * 0.1
		end
		statedb.daytime = dt
	end

	if big and (movekey == "W" or movekey == "S") then
		roompos = vector.offset(roompos, -20, 0, -20)
	end

	clearschems()

	putschem(roompos, big and "clear_40" or "clear_20")
	putschem(roompos, room)

	putschem(vector.offset(roompos, -20, 0, 0), "hall_x_minus")
	putschem(vector.offset(roompos, 0, 0, -20), "hall_z_minus")
	if big then
		putschem(vector.offset(roompos, -20, 0, 20), "hall_x_minus")
		putschem(vector.offset(roompos, 20, 0, -20), "hall_z_minus")
		putschem(vector.offset(roompos, 40, 0, 0), "hall_x_plus")
		putschem(vector.offset(roompos, 40, 0, 20), "hall_x_plus")
		putschem(vector.offset(roompos, 0, 0, 40), "hall_z_plus")
		putschem(vector.offset(roompos, 20, 0, 40), "hall_z_plus")
	else
		putschem(vector.offset(roompos, 20, 0, 0), "hall_x_plus")
		putschem(vector.offset(roompos, 0, 0, 20), "hall_z_plus")
	end

	return api.set_current_room(roompos, room)
end

do
	local old = piredo_player.restore_player_pos
	function piredo_player.restore_player_pos(...)
		if statedb.lastmove and statedb.lastpos then
			moveroom(statedb.lastmove, nil, statedb.lastpos)
		end
		return old(...)
	end
end

local function triggernode(pos)
	local node = minetest.get_node_or_nil(pos)
	local def = node and minetest.registered_nodes[node.name]
	if not def then return end
	local pend = statedb.pendmove
	if def.piredo_map_trigger_move and not pend then
		minetest.log("action", "entered trigger "
			.. def.piredo_map_trigger_move.dir)
		statedb.pendmove = {
			dir = def.piredo_map_trigger_move.dir,
			from = pos,
			pos = vector.offset(pos,
				def.piredo_map_trigger_move.x,
				def.piredo_map_trigger_move.y,
				def.piredo_map_trigger_move.z
			)
		}
		api.savedb()
	elseif statedb.pendmove and not def.piredo_map_trigger_move then
		minetest.log("action", "exited trigger")
		if (pend.dir == "N" and pos.z > pend.from.z)
		or (pend.dir == "S" and pos.z < pend.from.z)
		or (pend.dir == "E" and pos.x > pend.from.x)
		or (pend.dir == "W" and pos.x < pend.from.x)
		then moveroom(pend.dir, pos, pend.pos) end
		statedb.pendmove = nil
		api.savedb()
	end
end

do
	local oldpos
	piredo_player.register_playerstep(function(player)
			local pos = player:get_pos()
			pos.y = pos.y + 0.25
			pos = vector.round(pos)
			if not oldpos then
				oldpos = pos
				return
			end
			if vector.equals(pos, oldpos) then return end

			local diff = vector.subtract(pos, oldpos)
			local len = vector.length(diff)
			local dir = vector.multiply(diff, 1 / len)

			local spos = oldpos
			for i = 0.25, len, 0.25 do
				local npos = vector.round(vector.add(oldpos, vector.multiply(dir, i)))
				if not vector.equals(spos, npos) then
					spos = npos
					if triggernode(npos) then break end
				end
			end

			oldpos = pos
		end)
end
