-- Character Manager by j2b2
local CM = {
	VERSION_MAJOR = 0,
	VERSION_MINOR = 2,
	STATE_PRESSED = 0,
	STATE_HELD = 1,
	STATE_RELEASED = 2,
	skins = {}
}

if CharacterManager then
	if CharacterManager.VERSION_MAJOR > CM.VERSION_MAJOR or
	(CharacterManager.VERSION_MAJOR >= CM.VERSION_MAJOR and CharacterManager.VERSION_MINOR > CM.VERSION_MINOR) then
		print("Version "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR.." older than current version "..CharacterManager.VERSION_MAJOR.."."..CharacterManager.VERSION_MINOR)
		return
	else
		print("Updating CharacterManager from "..CharacterManager.VERSION_MAJOR.."."..CharacterManager.VERSION_MINOR.." to "..CM.VERSION_MAJOR.."."..CM.VERSION_MINOR)
		CM.skins = CharacterManager.skins
		CharacterManager = CM
	end
else
	print("No CharacterManager Loaded")
	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(player, button)
	local wasDown = (player.characterManager.oldButtons & button) > 0
	local isDown = (player.cmd.buttons & 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[pw_carry],
			oldSuper = player.powers[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 playerCharacterManager.airSpin and (not (newButtons & 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(player, BT_WEAPONNEXT)
		if state ~= nil then func(player, state) end
	end

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

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

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

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

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

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

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

	if oldButtons ~= newButtons then

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

		if (oldButtons & BT_JUMP) and not (newButtons & 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 & BT_WEAPONMASK)
			if slot > 0 then func(player, slot) end
		end

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

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

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

	local isSuper = player.powers[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

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

	playerCharacterManager.oldButtons = player.cmd.buttons
	playerCharacterManager.oldCarry = carry
	playerCharacterManager.oldSuper = isSuper

end

function CharacterManager.MobjLineCollide(playerMo, line, ...)
	local player = playerMo.player
	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
	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
	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
	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
	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 & 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 & 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 & PF_SPINDOWN then
			state = CharacterManager.STATE_HELD
		else
			state = CharacterManager.STATE_PRESSED
		end
		return func(player, state)
	end
end

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

addHook("PlayerThink", function(...) return CharacterManager.PlayerThink(...) end)
addHook("MobjLineCollide", function(...) return CharacterManager.MobjLineCollide(...) end, MT_PLAYER)
addHook("MobjDamage", function(...) return CharacterManager.MobjDamage(...) end, MT_PLAYER)
addHook("MobjDeath", function(...) return CharacterManager.MobjDeath(...) end, MT_PLAYER)
addHook("MobjFuse", function(...) return CharacterManager.MobjFuse(...) end, MT_PLAYER)
addHook("PlayerCanDamage", function(...) return CharacterManager.PlayerCanDamage(...) end)
addHook("PlayerSpawn", function(...) return CharacterManager.PlayerSpawn(...) end)
addHook("ShouldDamage", function(...) return CharacterManager.ShouldDamage(...) end, MT_PLAYER)
addHook("ShieldSpawn", function(...) return CharacterManager.ShieldSpawn(...) end)
addHook("ShieldSpecial", function(...) return CharacterManager.ShieldSpecial(...) end)
addHook("TouchSpecial", function(...) return CharacterManager.TouchSpecial(...) end)
addHook("AbilitySpecial", function(...) return CharacterManager.AbilitySpecial(...) end)
addHook("JumpSpecial", function(...) return CharacterManager.JumpSpecial(...) end)
addHook("JumpSpinSpecial", function(...) return CharacterManager.JumpSpinSpecial(...) end)
addHook("SpinSpecial", function(...) return CharacterManager.SpinSpecial(...) end)
hud.add(function(...) return CharacterManager.HUDDraw(...) end)
-- Character Manager by j2b2
