-- Character Manager by j2b2

--[[
	TODO
	* Enter/Exit Water
	* Flip Gravity
	* Finish level
]]

-- Cache global constants as accessing global variables is very slow
local L_BT_WEAPONMASK = BT_WEAPONMASK
local L_BT_WEAPONNEXT = BT_WEAPONNEXT
local L_BT_WEAPONPREV = BT_WEAPONPREV
local L_BT_ATTACK = BT_ATTACK
local L_BT_SPIN = BT_SPIN
local L_BT_TOSSFLAG = BT_TOSSFLAG
local L_BT_JUMP = BT_JUMP
local L_BT_FIRENORMAL = BT_FIRENORMAL
local L_BT_CUSTOM1 = BT_CUSTOM1
local L_BT_CUSTOM2 = BT_CUSTOM2
local L_BT_CUSTOM3 = BT_CUSTOM3

local L_MFE_JUSTHITFLOOR = MFE_JUSTHITFLOOR

local L_PF_SPINDOWN = PF_SPINDOWN
local L_PF_JUMPDOWN = PF_JUMPDOWN

local l_pw_carry = pw_carry
local l_pw_super = pw_super

local L_CR_NONE = CR_NONE

local CM = {
	VERSION_MAJOR = 0,
	VERSION_MINOR = 3,
	STATE_PRESSED = 0,
	STATE_HELD = 1,
	STATE_RELEASED = 2,
	skins = {},
	hooks = {
		"PlayerThink",
		"MobjLineCollide",
		"MobjDamage",
		"MobjDeath",
		"MobjFuse",
		"PlayerCanDamage",
		"PlayerSpawn",
		"ShouldDamage",
		"ShieldSpawn",
		"ShieldSpecial",
		"TouchSpecial",
		"AbilitySpecial",
		"JumpSpecial",
		"JumpSpinSpecial",
		"SpinSpecial",
		"BotTiccmd",
		"BotRespawn"
	}
}

local function print2(...)
	local args = {...}
	local str_args = {}
	for _, v in ipairs(args) do
		table.insert(str_args, tostring(v))
	end
	print("CharacterManager: "..table.concat(str_args, "    "))
end

local function tableHas(tbl, value)
	for _, v in pairs(tbl) do
		if v == value then return true end
	end
	return false
end

local function AddCMHook(hookName)
	addHook(hookName, function(...) return CharacterManager[hookName](...) end, MT_PLAYER)
end

local oldCM = CharacterManager
if oldCM then
	if oldCM.VERSION_MAJOR > CM.VERSION_MAJOR or
	(oldCM.VERSION_MAJOR == CM.VERSION_MAJOR and oldCM.VERSION_MINOR > CM.VERSION_MINOR) then
		print2("Version "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR.." older than current version "..oldCM.VERSION_MAJOR.."."..oldCM.VERSION_MINOR)
		return
	elseif oldCM.VERSION_MAJOR == CM.VERSION_MAJOR and oldCM.VERSION_MINOR == CM.VERSION_MINOR then
		print2("Version "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR.." already loaded.")
		return
	else
		print2("Updating from version "..oldCM.VERSION_MAJOR.."."..oldCM.VERSION_MINOR.." to "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR)
		CM.skins = CharacterManager.skins
		CharacterManager = CM
	end
else
	print2("No CharacterManager Loaded. Loading version "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR)
	rawset(_G,"CharacterManager", CM)
end

local function typeCheck(var, expectedType, argument, func)
	local varType = type(var)
	if varType == "userdata" then
		varType = userdataType(var)
	end
	if varType ~= expectedType then
		error("Bad argument #"..tostring(argument).." to '"..tostring(func).."', '"..tostring(expectedType).."' expected got '"..varType.."'.")
	end
end

local function lerp(fixed1, fixed2, percent)
	local lowest
	if fixed1 < fixed2 then
		lowest = fixed1
	else
		lowest = fixed2
	end
	return lowest+FixedMul(abs(fixed1-fixed2),percent)
end

local function LineHeightCheck(mobj, line)
	for _, sec in ipairs({line.frontsector, line.backsector})
		for fof in sec.ffloors() do
			if not (fof.flags & FF_EXISTS) then continue end -- Does it exist?
			if not (fof.flags & FF_SOLID) then continue end -- Is it solid?
			if (fof.t_slope) or (fof.b_slope) then continue end -- Is it sloped?

			if mobj.z + mobj.height - MAXSTEPMOVE < fof.bottomheight then continue end -- Are we too low?
			if mobj.z + MAXSTEPMOVE > fof.topheight then continue end -- Are we too high?

			return true
		end
		if (mobj.z + mobj.height - MAXSTEPMOVE < sec.ceilingheight or sec.c_slope) and  (mobj.z + MAXSTEPMOVE >= sec.floorheight or sec.f_slope) then continue end -- Are we too high?
		return true
	end
	return false
end

local function FixedDot(x1, y1, x2, y2) return FixedMul(x1, x2) + FixedMul(y1, y2) end

local function GetSkinTable(player)
	-- typeCheck(player, "player_t", 1, "GetSkinTable")
	if player.realmo and player.realmo.valid then
		return CharacterManager.skins[player.realmo.skin]
	end
end

local function GetSkinFunction(player, functionName)
	-- typeCheck(player, "player_t", 1, "GetSkinFunction")
	-- typeCheck(functionName, "string", 2, "GetSkinFunction")
	local skinTable = GetSkinTable(player)
	if skinTable ~= nil then
		return skinTable[functionName]
	end
end

local function ButtonUpdate(button, oldButtons, newButtons)
	local wasDown = (oldButtons & button) > 0
	local isDown = (newButtons & button) > 0
	if not wasDown and isDown then
		return CharacterManager.STATE_PRESSED
	elseif wasDown and isDown then
		return CharacterManager.STATE_HELD
	elseif wasDown and not isDown then
		return CharacterManager.STATE_RELEASED
	end
end

function CharacterManager.Register(characterTable)
	if CharacterManager.skins[characterTable.name] == nil then
		CharacterManager.skins[characterTable.name] = characterTable
	else
		error("Tried to register a skin twice.")
	end
end

-- Hook Functions

function CharacterManager.PlayerThink(player)

	if player.characterManager == nil then
		player.characterManager = {
			oldButtons = player.cmd.buttons,
			oldCarry = player.powers[l_pw_carry],
			oldSuper = player.powers[l_pw_super] > 0
		}
	end

	local playerCharacterManager = player.characterManager
	local playerRealMo = player.realmo
	local playerRealMoValid = false
	if playerRealMo ~= nil then playerRealMoValid = playerRealMo.valid end

	if playerRealMoValid then
		local oldSkin = playerCharacterManager.oldSkin
		local newSkin = playerRealMo.skin
		if oldSkin ~= newSkin then
			if oldSkin ~= nil then
				local skinTable = CharacterManager.skins[oldSkin]
				if skinTable ~= nil then
					local func = skinTable["UnEquipSkin"]
					if func then func(player, oldSkin, newSkin) end
				end
			end

			local func = GetSkinFunction(player, "EquipSkin")
			if func then func(player, oldSkin, newSkin) end

			playerCharacterManager.oldSkin = newSkin
		end
	end

	local skinTable = GetSkinTable(player)
	if skinTable == nil then return end

	local playerMo = player.mo

	local playerMoValid = false
	if playerMo ~= nil then playerMoValid = playerMo.valid end
	local oldButtons = playerCharacterManager.oldButtons
	local newButtons = player.cmd.buttons

	if playerMoValid and (playerMo.eflags & L_MFE_JUSTHITFLOOR) then
		local func = skinTable.Land
		if func then func(player) end
	end

	local carry = player.powers[l_pw_carry]
	local oldCarry = playerCharacterManager.oldCarry
	if carry ~= oldCarry then
		if oldCarry ~= L_CR_NONE then
			local func = skinTable.CarryEnd
			if func then func(player, oldCarry) end
		end
		if carry ~= L_CR_NONE then
			local func = skinTable.CarryStart
			if func then func(player, carry) end
		end
	end

	local isSuper = player.powers[l_pw_super] > 0
	if isSuper ~= playerCharacterManager.oldSuper then
		local func
		if isSuper then
			func = skinTable.SuperStart
		else
			func = skinTable.SuperEnd
		end
		if func then func(player) end
	end

	if playerCharacterManager.airSpin and (not (newButtons & L_BT_SPIN) or (playerMoValid and P_IsObjectOnGround(playerMo))) then
		playerCharacterManager.airSpin = nil
		local func = skinTable.AirSpin
		if func then func(player, CharacterManager.STATE_RELEASED) end
	end

	local func = skinTable.WeaponNext
	if func then
		local state = ButtonUpdate(L_BT_WEAPONNEXT, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.WeaponPrev
	if func then
		local state = ButtonUpdate(L_BT_WEAPONPREV, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.Fire
	if func then
		local state = ButtonUpdate(L_BT_ATTACK, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.TossFlag
	if func then
		local state = ButtonUpdate(L_BT_TOSSFLAG, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.FireNormal
	if func then
		local state = ButtonUpdate(L_BT_FIRENORMAL, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.Custom1
	if func then
		local state = ButtonUpdate(L_BT_CUSTOM1, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.Custom2
	if func then
		local state = ButtonUpdate(L_BT_CUSTOM2, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	local func = skinTable.Custom3
	if func then
		local state = ButtonUpdate(L_BT_CUSTOM3, oldButtons, newButtons)
		if state ~= nil then func(player, state) end
	end

	if oldButtons ~= newButtons then

		if (oldButtons & L_BT_SPIN) and not (newButtons & L_BT_SPIN) then
			local func = skinTable.SpinButton
			if func then func(player, CharacterManager.STATE_RELEASED) end
		end

		if (oldButtons & L_BT_JUMP) and not (newButtons & L_BT_JUMP) then
			local func = skinTable.JumpButton
			if func then func(player, CharacterManager.STATE_RELEASED) end
		end

		local func = skinTable.WeaponSlot
		if func then
			local slot = (newButtons & L_BT_WEAPONMASK)
			if slot > 0 then func(player, slot) end
		end

		local func = skinTable.ButtonsChanged
		if func then func(player, oldButtons, newButtons) end
	end

	local func = skinTable.Think
	if func then func(player) end

	playerCharacterManager.oldButtons = newButtons
	playerCharacterManager.oldCarry = carry
	playerCharacterManager.oldSuper = isSuper

end

function CharacterManager.MobjLineCollide(playerMo, line, ...)
	local player = playerMo.player
	if player == nil or player.valid == false then return end
	local skinTable = GetSkinTable(player)
	if not skinTable then return end
	local func = skinTable.WallCollide
	local ret
	if func then
		if LineHeightCheck(playerMo, line) then
			local uX = line.v2.y - line.v1.y
			local uY = line.v1.x - line.v2.x
			local uLen = FixedHypot(uX, uY)
			local nX = FixedDiv(uX, uLen)
			local nY = FixedDiv(uY, uLen)
			local angle = R_PointToAngle2(line.v1.x, line.v1.y, line.v2.x, line.v2.y)
			local normal
			local dir = FixedDot(playerMo.x - line.v1.x, playerMo.y - line.v1.y, nX, nY)
			if dir > 0 then
				normal = R_PointToAngle2(0, 0, nX, nY)
			else
				normal = R_PointToAngle2(0, 0, -nX, -nY)
			end
			ret = func(player, playerMo, line, angle, normal)
			if ret ~= nil then return ret end
		end
	end
	func = skinTable.LineCollide
	if func then return func(player, playerMo, line, ...) end
end

function CharacterManager.MobjDamage(playerMo, ...)
	local player = playerMo.player
	if player == nil or player.valid == false then return end
	local func = GetSkinFunction(player, "Damage")
	if func then return func(player, playerMo, ...) end
end

function CharacterManager.MobjDeath(playerMo, ...)
	local player = playerMo.player
	local func = GetSkinFunction(player, "Death")
	if func then return func(player, playerMo, ...) end
end

function CharacterManager.MobjFuse(playerMo ,...)
	local player = playerMo.player
	if player == nil or player.valid == false then return end
	local func = GetSkinFunction(player, "Fuse")
	if func then return func(player, playerMo, ...) end
end

function CharacterManager.PlayerCanDamage(player, ...)
	local func = GetSkinFunction(player, "CanDamage")
	if func then return func(player, ...) end
end

function CharacterManager.PlayerSpawn(player, ...)
	local func = GetSkinFunction(player, "Spawn")
	if func then return func(player, ...) end
end

function CharacterManager.ShouldDamage(playerMo, ...)
	local player = playerMo.player
	if player == nil or player.valid == false then return end
	local func = GetSkinFunction(player, "ShouldBeDamaged")
	if func then return func(player, playerMo, ...) end
end

function CharacterManager.ShieldSpawn(player, ...)
	local func = GetSkinFunction(player, "GetShield")
	if func then return func(player, ...) end
end

function CharacterManager.ShieldSpecial(player)
	local func = GetSkinFunction(player, "UseShield")
	if func then return func(player) end
end

function CharacterManager.TouchSpecial(mobj, playerMo)
	local player = playerMo.player
	if player == nil or player.valid == false then return end
	local func = GetSkinFunction(player, "TouchSpecial")
	if func then return func(player, playerMo, mobj) end
end

function CharacterManager.AbilitySpecial(player)
	local func = GetSkinFunction(player, "JumpAbility")
	if func then return func(player) end
end

function CharacterManager.JumpSpecial(player)
	local func = GetSkinFunction(player, "JumpButton")
	if func then
		local state
		if player.pflags & L_PF_JUMPDOWN then
			state = CharacterManager.STATE_HELD
		else
			state = CharacterManager.STATE_PRESSED
		end
		return func(player, state)
	end
end

function CharacterManager.JumpSpinSpecial(player)
	local func = GetSkinFunction(player, "AirSpin")
	if func then
		local state
		if player.pflags & L_PF_SPINDOWN then
			state = CharacterManager.STATE_HELD
		else
			state = CharacterManager.STATE_PRESSED
		end
		player.characterManager.airSpin = true
		return func(player, state)
	end
end

function CharacterManager.SpinSpecial(player)
	local func = GetSkinFunction(player, "SpinButton")
	if func then
		local state
		if player.pflags & L_PF_SPINDOWN then
			state = CharacterManager.STATE_HELD
		else
			state = CharacterManager.STATE_PRESSED
		end
		return func(player, state)
	end
end

function CharacterManager.BotTiccmd(player, ...)
	local func = GetSkinFunction(player, "BotTiccmd")
	if func then return func(player, ...) end
end

function CharacterManager.BotRespawn(playerMobj, botMobj, ...)
	if botMobj == nil or botMobj.valid == false then return end
	local botPlayer = botMobj.player
	if botPlayer == nil or botPlayer.valid == false then return end
	local func = GetSkinFunction(botPlayer, "BotRespawn")
	if func then return func(botPlayer, botMobj, playerMobj, ...) end
end

function CharacterManager.HUDDraw(v, player, cam)
	local func = GetSkinFunction(player, "HUDDraw")
	if func then func(v, player, cam) end
end

-- If BotAI hook is defined it must be given values. Due to this it is not implemented in Character Manager.

if not oldCM then
	print2("Adding Hooks...")
	for _, hook in ipairs(CharacterManager.hooks) do
		AddCMHook(hook)
		print2("Adding hook \""..hook.."\"")
	end
	hud.add(function(...) return CharacterManager.HUDDraw(...) end)
else
	for _, hook in ipairs(CharacterManager.hooks) do
		if not tableHas(oldCM.hooks, hook) then
			AddCMHook(hook)
			print2("Adding new hook \""..hook.."\"")
		end
	end
end
-- Character Manager by j2b2
