-- LUALOCALS < ---------------------------------------------------------
local error, klots, math, minetest, type, vector
    = error, klots, math, minetest, type, vector
local math_abs, math_cos, math_pi, math_sin
    = math.abs, math.cos, math.pi, math.sin
-- LUALOCALS > ---------------------------------------------------------

local modname = minetest.get_current_modname()

local entname = modname .. ":vehicle"
minetest.register_entity(entname, {
		initial_properties = {
			visual = "sprite",
			textures = {"[combine:1x1", "[combine:1x1"},
			collisionbox = {-0.3, 0, -0.3, 0.3, 1.8, 0.3},
			is_visible = true,
			pointable = false,
			static_save = false,
			physical = true
		}
	})

local tau = math_pi * 2
local yawdiffmin = 0.05
local yawdiffmax = tau - yawdiffmin

local paramkey = modname .. "_player_control_param"

local function footstep(pos, walkable)
	local node = minetest.get_node(pos)
	local def = minetest.registered_nodes[node.name]
	if not def then return end
	if (not def.walkable) ~= (not walkable) then return end
	return klots.node_sound(pos, "footstep", "dig", "dug")
end

local function bouncestep(pos, min, max, delta)
	for dy = min, max, delta do
		if footstep({x = pos.x, y = pos.y + dy, z = pos.z}, true) then
			return
		end
	end
end

local getdata = klots.register_playerstep({
		on_init = function(player, data)
			local s = player:get_meta():get_string(paramkey) or ""
			data.param = s and s ~= "" and minetest.deserialize(s) or false
		end,
		on_leave = function(player, data)
			if data.vehicle then
				player:set_detach()
				local vpos = data.vehicle.get_pos()
				if vpos then
					player:set_pos(vpos)
					data.vehicle:remove()
				end
			end
		end,
		on_step = function(player, data, dtime)
			if klots.editmode then return end

			if data.vehicle and not data.vehicle:get_pos() then
				data.vehicle = nil
			end

			local vpos
			if data.param then
				if data.vehicle then
					vpos = data.vehicle:get_pos()
					if not vpos then data.vehicle = nil end
				end
				if not data.vehicle then
					vpos = player:get_pos()
					data.vehicle = minetest.add_entity(vpos, entname)
					if not data.vehicle then return end
					data.vehicle:set_acceleration({x = 0, y = 0, z = 0})
					data.vel = player:get_velocity()
					data.vehicle:set_velocity(data.vel)
					player:set_attach(data.vehicle, "", {x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
				end
			else
				data.vel = nil
				if data.vehicle then
					player:set_detach()
					vpos = data.vehicle:get_pos()
					if vpos then
						player:set_pos(vpos)
						data.vehicle:remove()
					end
					data.vehicle = nil
				end
				return
			end

			local yaw = player:get_look_horizontal()
			local yawdiff = math_abs((yaw - data.vehicle:get_yaw()) % tau)
			if yawdiff > yawdiffmin and yawdiff < yawdiffmax then
				data.vehicle:set_yaw(yaw)
				player:set_attach(data.vehicle, "", {x = 0, y = 0, z = 0},
					{x = 0, y = -yaw / math_pi * 180, z = 0})
			end

			local pvel = data.vel
			local vel = vector.multiply(data.vehicle:get_velocity(),
				data.param.momentum ^ dtime)

			local bouncerate = data.param.bouncerate
			if pvel and bouncerate and bouncerate ~= 0 then
				local bouncemin = data.param.bouncemin
				local bounced = nil
				if vel.x == 0 and math_abs(pvel.x) >= bouncemin then
					vel.x = -pvel.x * bouncerate
					bounced = true
				end
				if vel.y == 0 and math_abs(pvel.y) >= bouncemin then
					vel.y = -pvel.y * bouncerate
					bounced = true
				end
				if vel.z == 0 and math_abs(pvel.z) >= bouncemin then
					vel.z = -pvel.z * bouncerate
					bounced = true
				end
				if data.sndsquelch then
					data.sndsquelch = data.sndsquelch - dtime
					if data.sndsquelch <= 0 then data.sndsquelch = nil end
				elseif bounced then
					local bounceoffset = {
						x = (pvel.x >= 1) and 0.5 or (pvel.x <= -1) and -0.5 or 0,
						y = (pvel.y >= 1) and 0.5 or (pvel.y <= -1) and -0.5 or 0,
						z = (pvel.z >= 1) and 0.5 or (pvel.z <= -1) and -0.5 or 0,
					}
					if bounceoffset.x ~= 0 or bounceoffset.y ~= 0
					or bounceoffset.z ~= 0 then
						local bpos = vector.add(vpos, bounceoffset)
						if pvel.y > 0 then
							bouncestep(bpos, 1.8, 0, -0.9)
						else
							bouncestep(bpos, 0, 1.8, 0.9)
						end
						data.sndsquelch = 1
					end
				end
			end

			local moveaccel = data.param.moveaccel
			if moveaccel then
				local dv = {x = 0, y = 0, z = 0}
				if player:get_player_control().up then
					dv = vector.add(dv, {x = -math_sin(yaw), y = 0, z = math_cos(yaw)})
				end
				if player:get_player_control().down then
					dv = vector.add(dv, {x = math_sin(yaw), y = 0, z = -math_cos(yaw)})
				end
				if player:get_player_control().right then
					dv = vector.add(dv, {x = math_cos(yaw), y = 0, z = math_sin(yaw)})
				end
				if player:get_player_control().left then
					dv = vector.add(dv, {x = -math_cos(yaw), y = 0, z = -math_sin(yaw)})
				end
				if player:get_player_control().jump then
					dv = vector.add(dv, {x = 0, y = 1, z = 0})
				end
				if player:get_player_control().sneak then
					dv = vector.add(dv, {x = 0, y = -1, z = 0})
				end
				vel = vector.add(vel, vector.multiply(dv, dtime * moveaccel))
			end

			if not vector.equals(vel, pvel) then
				data.vehicle:set_velocity(vel)
				data.vel = vel
			end

			data.fspos = data.fspos or vpos
			if data.fspos and vector.distance(vpos, data.fspos) >= 1 then
				footstep(vpos, false)
				local apos = {x = vpos.x, y = vpos.y + 1, z = vpos.z}
				footstep(apos, false)
				local aapos = {x = vpos.x, y = vpos.y + 1.8, z = vpos.z}
				if vector.round(apos).y ~= vector.round(aapos).y then
					footstep(aapos, false)
				end
				data.fspos = vpos
			end
		end
	})

------------------------------------------------------------------------

local player_control_presets = {
	normal = false,
	zerograv = {
		momentum = 0.3,
		moveaccel = 8,
		bouncemin = 0.1,
		bouncerate = 0.5,
	},
	zerognofly = {
		momentum = 0.3,
		moveaccel = 0,
		bouncemin = 0.1,
		bouncerate = 0.5,
	},
	frozen = {
		momentum = 0,
		moveaccel = 0,
		bouncemin = 1,
		bouncerate = 0,
	}
}
klots.player_control_presets = player_control_presets

local function default_control_type(pos, selected)
	if selected ~= nil then return selected end
	if pos.y >= 10000 then return player_control_presets.frozen end
	if pos.y >= 128 then return player_control_presets.zerograv end
	return false
end
klots.default_control_type = default_control_type

local function getplayer(player_or_name)
	local player = type(player_or_name) == "string"
	and minetest.get_player_by_name(player_or_name) or player_or_name
	if not player then return error("invalid player") end

	local pname = type(player_or_name) == "string"
	and player_or_name or player_or_name:get_player_name()
	if not pname then return error("invalid player") end

	return player, pname
end

-- falsy = use standard built-in minetest player control
-- non-nil: zero gravity with given params
function klots.player_control_set(player_or_name, param)
	local player, pname = getplayer(player_or_name)
	if type(param) == "string" then
		param = klots.player_control_presets[param]
		if param == nil then return error("invalid param") end
	end
	(getdata(pname)).param = param
	player:get_meta():set_string(paramkey, minetest.serialize(param))
end

klots.player_control_get = getdata

function klots.player_control_apply(player_or_name, func)
	local player, pname = getplayer(player_or_name)
	local data = getdata(pname)
	return func(data.vehicle and data.vehicle or player, data)
end
