//note to self: cannot make an IsFile() method because io is part of base lua and lua just sees it as userdata, it'd get
//confused for lots of other things if i tried but thankfully there's no possible situation AFAIK where a user could construct
//and pass a file object into pre-written lua so i have total control over the state of a file object and can just check for nil
//if it doesn't exist or isn't a file

local function IsCVar(c)
	if(c==nil)
		return false
	end
	
	if(type(c)~="userdata")
		return false
	end
	
	if(userdataType(c)~="consvar_t")
		return false
	end
	
	return true
end

local function IsBool(b)
	if(b==nil)
		return false
	end
	
	if(type(b) ~= "boolean")
		return false
	end
	
	return true
end

local function IsNumber(num)
	if(num==nil)
		return false
	end
	
	if(type(num) ~= "number")
		return false
	end
	
	return true
end

//for states, sprites, and skincolours:
//since they have implicit conversion to integers, it doesn't actually
//work to check their type or userdataType to verify their integrity in
//the type-unsafe lua hellscape
//instead, just check they have an index in certain tables, that'll do just as well

local function IsState(s)
	if(IsNumber(s)~=true)
		return false
	end
	
	return s >= 1 and s <= #states
end

local function IsSprite(s)
	if(IsNumber(s)~=true)
		return false
	end
	
	return s >= 1 and s <= #spriteinfo-1
end

local function IsSkinColor(s)
	if(IsNumber(s)~=true)
		return false
	end
	
	return s >= 1 and s <= #skincolors-1
end

local function IsPlayer(player)
	if(player==nil)
		return false
	end
	
	if(type(player)~="userdata")
		return false
	end

	if(userdataType(player)~="player_t")
		return false
	end
	
	if(player.valid ~= true)
		return false
	end

	return true
end

local function IsMobj(mobj)
	if(mobj==nil)
		return false
	end
	
	if(type(mobj)~="userdata")
		return false
	end
	
	if(userdataType(mobj)~="mobj_t")
		return false
	end
	
	if(IsBool(mobj.valid)~=true)
		return false
	end
	
	if(mobj.valid~=true)
		return false
	end
	
	return true
end

local function IsTable(tbl)
	if(tbl==nil)
		return false
	end
	
	if(type(tbl)~="table")
		return false
	end
	
	return true
end

local function IsFunction(func)
	if(func==nil)
		return false
	end
	
	if(type(func)~="function")
		return false
	end
	
	return true
end

local function StringEmptyOrNil(str)
	if(str==nil)//'nil' can tostring() into "nil" but i don't want that
		return true
	end
	
	str = tostring(str)
	
	local nospaces=""
	
	for c in str.gmatch(str,".")//for every character in line ('.' matcher being 'any condition')
		if(c~=" ")
			nospaces = nospaces..c
		end
	end
	
	return string.len(nospaces) <= 0
end

local function GetGravityStatus_Error() return -1 end
local function GetGravityStatus_GroundedNormal() return 0 end
local function GetGravityStatus_GroundedAnti() return 1 end
local function GetGravityStatus_AirborneNormal() return 2 end
local function GetGravityStatus_AirborneAnti() return 3 end

local function GetGravityStatus(mobj)
	if(BonkerBox_G.IsMobj(mobj)~=true)
		return GetGravityStatus_Error()
	end
	
	if(P_IsObjectOnGround(mobj))
		if(mobj.eflags & MFE_VERTICALFLIP==0)
			return GetGravityStatus_GroundedNormal()
		else
			return GetGravityStatus_GroundedAnti()
		end
	else
		if(mobj.eflags & MFE_VERTICALFLIP==0)
			return GetGravityStatus_AirborneNormal()
		else
			return GetGravityStatus_AirborneAnti()
		end
	end
end

local function EqualsSPR2(cached,compare)
	if(BonkerBox_G.IsNumber(cached)~=true or BonkerBox_G.IsNumber(compare)~=true)
		return false
	end
	
	return cached & ~FF_SPR2SUPER == compare//specifically unsetting FF_SPR2SUPER as it is the only SPR2 flag, something more robust would be needed if there were ever to be more but that's very unlikely
end

local function NegativeAgnosticModulo(x,m)
	if(IsNumber(x)~=true or IsNumber(m)~=true)
		return 0
	end
	
	local xpos = abs(x)
	local mpos = abs(m)
	local mod = xpos%mpos
	
	if(x<0 or m<0)
		return mpos - mod
	else
		return mod
	end
end

local function MobjFakeRandom(m,minim,maxim,inclusive)
	if(IsMobj(m)~=true)
		InternalError("MobjFakeRandom() expected 'm' to be a mobj")
		return 0
	end
	
	if(IsBool(inclusive)~=true)
		InternalError("MobjFakeRandom() expected 'inclusive' to be a boolean")
		return 0
	end
	
	local hardlimit_upper = INT16_MAX
	local hardlimit_lower = INT16_MIN
	
	if(IsNumber(minim)~=true)
		minim = hardlimit_lower
	end
	
	if(IsNumber(maxim)~=true)
		maxim = hardlimit_upper
	end
	
	local bigger = max(minim,maxim)
	local smaller = min(minim,maxim)
	bigger = bigger+1//because e.g. bigger==3 will only return values 0 through 2, we want to assume to include 3
	
	if(inclusive ~=true)
		smaller = smaller + 1
		bigger = bigger - 1
	end
	
	bigger = min(bigger,hardlimit_upper)
	smaller = max(smaller,hardlimit_lower)
	local span = bigger-smaller
	
	local x = m.x/FRACUNIT
	local y = m.y/FRACUNIT
	local z = m.z/FRACUNIT
	local c = m.color
	local a = m.angle
	
	local ret = x+y+z+c+a+gamemap//note: i was worried there'd be a chance that this would overflow and cause errors but as it turns out the game thankfully is silent upon an overflow which makes things much easier for me
	ret = NegativeAgnosticModulo(ret,span)
	ret = ret+smaller
	
	return ret
end

local function GetMobjFakeRandomColor(m)
	//even though lua tables begin at 1 and you'd think the range would
	//be SKINCOLOR_NONE+1 -> #skincolors, im not accessing a lua table when i use this
	//method's return value, im calling R_GetNameByColor(), which stores
	//the colors in hardcode where things are indexed at 0
	//this call assumes SKINCOLOR_NONE will always be 0 as 0 in the color cache
	//in hardcode is considered the nil color and every 1 through to #skincolors-1
	//is a valid color
	//in short, lua go fuck yourself for being stupid and beginning at 1 it makes
	//so little sense i will never not be annoyed abt it
	return MobjFakeRandom(m,SKINCOLOR_NONE+1,#skincolors-1,true)
end

//imagine my surprise finding out that whilst several things exist in base lua to do this (table.getn(), #table, etc.)
//they are all some variation of broken depending on the data in the table passed
//this is honestly shocking and stupid, so here's the method
local function CountTable(tbl)
	local c = 0
	
	if(IsTable(tbl)~=true)
		return c
	end
	
	//pairs() goes through every key-value pair in a table, ipairs() only goes through the integer indexed ones
	//(https://stackoverflow.com/questions/27674367/lua-check-if-a-table-can-be-looped-through-via-ipairs-ipairs-starting-at-0)
	for a,b in pairs(tbl)
		c = c+1
	end
	
	return c
end

local function SomeKindaMessage(player,msg,errorname,escapeseq)
	if(StringEmptyOrNil(escapeseq)==true)
		escapeseq = "\x80"//white
	end

	if(StringEmptyOrNil(msg)==true)
		msg=""
	else
		msg = tostring(msg)
	end
	
	if(StringEmptyOrNil(errorname)==true)
		errorname=""
	else
		errorname = string.upper(tostring(errorname))
	end
	
	local printstr = escapeseq.."BONKERBOX "..errorname..": "..msg
	
	if(IsPlayer(player)==true)
		COM_BufInsertText(player,"cls")
		CONS_Printf(player,printstr)
	else
		print(printstr)
	end
end

local function InternalError(player,msg)
	SomeKindaMessage(player,msg,"internal error","\x85")
end

local function UserFacingError(player,msg)
	SomeKindaMessage(player,msg,"error","\x85")
end

local function UserFacingWarning(player,msg)
	SomeKindaMessage(player,msg,"warning","\x82")
end

local function UserFacingInfo(player,msg)
	SomeKindaMessage(player,msg,"info","\x80")
end

//NOTE: Loosely based upon hardcode P_SpawnGhostMobj() but there was just
//enough about it that didn't suit my needs that I made my own version
//I will need to check upon updates to Ring Racers if the source code method has changed
local function TrackMobj(player,subjectmobj,targetmobj,possmoothamt)
	//not going to IsPlayer() player here because its just used to pass through to InternalError()
	//where it itself can decide what to do with it

	if(IsMobj(subjectmobj)~=true or IsMobj(targetmobj)~=true)
		InternalError(player,"Cannot track subjectmobj to targetmobj because one or both of them don't exist")
		return
	end
	
	local x = nil
	local y = nil
	local z = nil
	
	if(IsNumber(possmoothamt)~=true)
		possmoothamt = -1
	end
	
	if(possmoothamt <FRACUNIT and possmoothamt >=0)//explicit direct set of each position instead of calling e() because inner workings of ease. library cause flickering even with dispoffset when two mobjs are at the same position
		local e  = function(a,b) return ease.linear(possmoothamt,a,b) end
		x = e(subjectmobj.x,targetmobj.x)
		y = e(subjectmobj.y,targetmobj.y)
		z = e(subjectmobj.z,targetmobj.z)
	else
		x = targetmobj.x
		y = targetmobj.y
		z = targetmobj.z
	end
	
	P_MoveOrigin(subjectmobj,x,y,z)
	subjectmobj.target = targetmobj

	P_SetScale(subjectmobj, targetmobj.scale)
	subjectmobj.scalespeed = targetmobj.scalespeed
	subjectmobj.destscale = targetmobj.scale

	if (targetmobj.eflags & MFE_VERTICALFLIP ~=0)
		subjectmobj.eflags = subjectmobj.eflags | MFE_VERTICALFLIP

		P_MoveOrigin(subjectmobj,subjectmobj.x,subjectmobj.y,subjectmobj.z+ (targetmobj.height - subjectmobj.height))
	else
		subjectmobj.eflags = subjectmobj.eflags & ~MFE_VERTICALFLIP
	end
	
	if(IsPlayer(targetmobj.player)==true)
		subjectmobj.angle = targetmobj.player.drawangle
	else
		subjectmobj.angle = targetmobj.angle
	end
	
	subjectmobj.roll = targetmobj.roll
	subjectmobj.pitch = targetmobj.pitch
	
	subjectmobj.renderflags = targetmobj.renderflags

	subjectmobj.fuse = subjectmobj.info.damage//idk what this does
	//subjectmobj.standingslope = targetmobj.standingslope //not allowed to set directly through lua but as far as i can tell it doesn't ruin the track effect

	subjectmobj.sprxoff = targetmobj.sprxoff
	subjectmobj.spryoff = targetmobj.spryoff
	subjectmobj.sprzoff = targetmobj.sprzoff
	subjectmobj.rollangle = targetmobj.rollangle

	subjectmobj.spritexscale = targetmobj.spritexscale
	subjectmobj.spriteyscale = targetmobj.spriteyscale
	subjectmobj.spritexoffset = targetmobj.spritexoffset
	subjectmobj.spriteyoffset = targetmobj.spriteyoffset

	if (targetmobj.flags2 & MF2_OBJECTFLIP~=0)
		subjectmobj.flags2 = subjectmobj.flags | MF2_OBJECTFLIP
	else
		subjectmobj.flags2 = subjectmobj.flags & ~MF2_OBJECTFLIP
	end

	if (targetmobj.flags & MF_DONTENCOREMAP==0)
		subjectmobj.flags = subjectmobj.flags & ~MF_DONTENCOREMAP
	else
		subjectmobj.flags = subjectmobj.flags | MF_DONTENCOREMAP
	end

	// Copy interpolation data :)
	subjectmobj.old_x = targetmobj.old_x2;
	subjectmobj.old_y = targetmobj.old_y2;
	subjectmobj.old_z = targetmobj.old_z2;
	
	if(IsPlayer(targetmobj.player)==true)
		subjectmobj.old_angle = targetmobj.player.old_drawangle2
	else
		subjectmobj.old_angle = targetmobj.old_angle2
	end

	subjectmobj.old_pitch = targetmobj.old_pitch2
	subjectmobj.old_roll = targetmobj.old_roll2
	subjectmobj.old_scale = targetmobj.old_scale2
	
	subjectmobj.owner = targetmobj
	subjectmobj.reappear = targetmobj.reappear
	subjectmobj.punt_ref = targetmobj.punt_ref
end

local function Clamp(x,a,b)
	if(IsNumber(x)~=true or IsNumber(a)~=true or IsNumber(b)~=true)
		return 0
	end
	
	if(a >=b)
		local c = a
		a = b
		b = c
	end
	
	return max(min(x,b),a)
end

local function FixedInverseLerp(x,a,b)
	if(IsNumber(x)~=true or IsNumber(a)~=true or IsNumber(b)~=true)
		return 0
	end
	
	local totalspan = b-a
	local xspan = x-a
	
	return FixedMul(FixedDiv(FRACUNIT,totalspan),xspan)
end

local function FixedLerp(x,a,b)
	if(IsNumber(x)~=true or IsNumber(a)~=true or IsNumber(b)~=true)
		return 0
	end
	
	return a + FixedMul(b-a,x)
end

//specially designed to circumvent the fact that dedicated servers count as a
//non-spectator player in players.iterate (yes, really)
local function PlayerPhysicallyExists(p)
	if(IsPlayer(p) ~= true)
		return false
	end
	
	if(p.spectator == true)
		return false
	end
	
	if(IsMobj(p.mo) ~= true)
		return false
	end
	
	return true
end

local function IsPlayingAs(player,playingas)
	if(PlayerPhysicallyExists(player) ~= true)
		return false
	end
	
	if(StringEmptyOrNil(playingas)==true)
		return false
	end

	return player.mo.skin == playingas// see SetFakePlayerSkin() in C source to prove that SF_IRONMAN characters are compatible with this check
end

local function PlayerIsSuffering(player)
	if(PlayerPhysicallyExists(player) ~= true)
		return false
	end

	return P_PlayerInPain(player)==true or player.mo.state == S_KART_DEAD
end

local function DeepCopyTable(table)

	if(IsTable(table)~=true)
		return {}
	end
	
	local layer = nil//got to declare in advance to do recursion
	
	layer = function(t)
		local ret = {}
		
		for k,v in pairs(t)
		
			if(IsTable(v)~=true)
				ret[k] = v
			else
				ret[k] = layer(r)
			end
			
		end
		
		return ret
	end
	
	return layer(table)
end

local bbg = {}
bbg.InternalError = InternalError
bbg.UserFacingError = UserFacingError
bbg.UserFacingWarning = UserFacingWarning
bbg.UserFacingInfo = UserFacingInfo
bbg.IO_ROOT = "client/bonkerbox/"
bbg.TrackMobj = TrackMobj
bbg.COMHelpGen = COMHelpGen
bbg.CountTable = CountTable
bbg.IsPlayer = IsPlayer
bbg.IsMobj = IsMobj
bbg.IsTable = IsTable
bbg.IsFunction = IsFunction
bbg.StringEmptyOrNil = StringEmptyOrNil
bbg.IsNumber = IsNumber
bbg.IsState = IsState
bbg.IsSprite = IsSprite
bbg.IsSkinColor = IsSkinColor
bbg.IsBool = IsBool
bbg.IsCVar = IsCVar
bbg.MobjFakeRandom = MobjFakeRandom
bbg.NegativeAgnosticModulo = NegativeAgnosticModulo
bbg.GetMobjFakeRandomColor = GetMobjFakeRandomColor
bbg.GetGravityStatus = GetGravityStatus
bbg.GetGravityStatus_Error = GetGravityStatus_Error
bbg.GetGravityStatus_GroundedNormal = GetGravityStatus_GroundedNormal
bbg.GetGravityStatus_GroundedAnti = GetGravityStatus_GroundedAnti
bbg.GetGravityStatus_AirborneNormal = GetGravityStatus_AirborneNormal
bbg.GetGravityStatus_AirborneAnti = GetGravityStatus_AirborneAnti
bbg.EqualsSPR2 = EqualsSPR2
bbg.Clamp = Clamp
bbg.FixedInverseLerp = FixedInverseLerp
bbg.FixedLerp = FixedLerp
bbg.PlayerPhysicallyExists = PlayerPhysicallyExists
bbg.IsPlayingAs = IsPlayingAs
bbg.PlayerIsSuffering = PlayerIsSuffering
bbg.DeepCopyTable = DeepCopyTable
rawset(_G,"BonkerBox_G",bbg)