-- Global to all starfalls
local checkluatype = SF.CheckLuaType
local dgetmeta = debug.getmetatable
local Unpack = FindMetaTable("Angle").Unpack
local SetUnpacked = FindMetaTable("Angle").SetUnpacked

--- Angle Type
-- @name Angle
-- @class type
-- @field p The -90 to 90 pitch value of the euler angle. Can also be indexed with [1]
-- @field y The -180 to 180 yaw value of the euler angle. Can also be indexed with [2]
-- @field r The -180 to 180 roll value of the euler angle. Can also be indexed with [3]
-- @libtbl ang_methods
-- @libtbl ang_meta
SF.RegisterType("Angle", nil, nil, FindMetaTable("Angle"), nil, function(checktype, ang_meta)
	return function(ang)
		return setmetatable({ Unpack(ang) }, ang_meta)
	end,
	function(obj)
		checktype(obj, ang_meta, 2)
		return Angle(obj[1], obj[2], obj[3])
	end
end)


return function(instance)
local checkpermission = instance.player ~= SF.Superuser and SF.Permissions.check or function() end

local ang_methods, ang_meta, awrap, unwrap = instance.Types.Angle.Methods, instance.Types.Angle, instance.Types.Angle.Wrap, instance.Types.Angle.Unwrap
local vec_meta, vwrap, vunwrap = instance.Types.Vector, instance.Types.Vector.Wrap, instance.Types.Vector.Unwrap
local function wrap(tbl)
	return setmetatable(tbl, ang_meta)
end

local vunwrap1
instance:AddHook("initialize", function()
	vunwrap1 = vec_meta.QuickUnwrap1
end)

local function QuickUnwrapper()
	local Ang = Angle()
	return function(v) SetUnpacked(Ang, v[1], v[2], v[3]) return Ang end
end
ang_meta.QuickUnwrap1 = QuickUnwrapper()
ang_meta.QuickUnwrap2 = QuickUnwrapper()
ang_meta.QuickUnwrap3 = QuickUnwrapper()

local unwrap1 = ang_meta.QuickUnwrap1

--- Creates an Angle struct.
-- @name builtins_library.Angle
-- @class function
-- @param number? p Pitch
-- @param number? y Yaw
-- @param number? r Roll
-- @return Angle Angle struct
instance.env.Angle = function (p, y, r)
	if p~=nil then checkluatype(p, TYPE_NUMBER) else p = 0 end
	if y~=nil then checkluatype(y, TYPE_NUMBER) else y = p end
	if r~=nil then checkluatype(r, TYPE_NUMBER) else r = p end
	return wrap({ p, y, r })
end

-- Lookup table.
-- Index 1->6 have associative pyr for use in __index. Saves lots of checks
-- String based indexing returns string, just a pass through.
local pyr = { p = 1, y = 2, r = 3, pitch = 1, yaw = 2, roll = 3 }

--- Sets a value at a key in the angle
-- @param Angle Ang
-- @param number|string Key
-- @param number Value
function ang_meta.__newindex(t, k, v)
	if pyr[k] then
		rawset(t, pyr[k], v)
	else
		rawset(t, k, v)
	end
end

--- Gets a value at a key in the angle
-- Can be indexed with: 1, 2, 3, p, y, r, pitch, yaw, roll. 1,2,3 is most efficient.
-- @param number|string Key
-- @return number Value
function ang_meta.__index(t, k)
	local method = ang_methods[k]
	if method then
		return method
	elseif pyr[k] then
		return rawget(t, pyr[k])
	end
end

local table_concat = table.concat

--- Turns an angle into a string.
-- @return string String representing the angle.
function ang_meta.__tostring(a)
	return table_concat(a, ' ', 1, 3)
end

--- Multiplication metamethod
-- @param number|Angle a1 Number or Angle multiplicand.
-- @param number|Angle a2 Number or Angle multiplier.
-- @return Angle Resultant angle.
function ang_meta.__mul(a, b)
	if isnumber(b) then
		return wrap({ a[1] * b, a[2] * b, a[3] * b })
	elseif isnumber(a) then
		return wrap({ b[1] * a, b[2] * a, b[3] * a })
	elseif dgetmeta(a) == ang_meta and dgetmeta(b) == ang_meta then
		return wrap({ a[1] * b[1], a[2] * b[2], a[3] * b[3] })
	elseif dgetmeta(a) == ang_meta then
		checkluatype(b, TYPE_NUMBER)
	else
		checkluatype(a, TYPE_NUMBER)
	end
end

--- Division metamethod
-- @param number|Angle a1 Number or Angle dividend.
-- @param number|Angle a2 Number or Angle divisor.
-- @return Angle Resultant angle.
function ang_meta.__div(a, b)
	if isnumber(b) then
		return wrap({ a[1] / b, a[2] / b, a[3] / b })
	elseif isnumber(a) then
		return wrap({ a / b[1], a / b[2], a / b[3] })
	elseif dgetmeta(a) == ang_meta and dgetmeta(b) == ang_meta then
		return wrap({ a[1] / b[1], a[2] / b[2], a[3] / b[3] })
	elseif dgetmeta(a) == ang_meta then
		checkluatype(b, TYPE_NUMBER)
	else
		checkluatype(a, TYPE_NUMBER)
	end
end

--- Unary Minus metamethod (Negative)
-- @return Angle Negative angle.
function ang_meta.__unm(a)
	return wrap({ -a[1], -a[2], -a[3] })
end

--- Equivalence metamethod
-- @param Angle a1 Initial angle.
-- @param Angle a2 Angle to check against.
-- @return boolean Whether their fields are equal
function ang_meta.__eq(a, b)
	return a[1]==b[1] and a[2]==b[2] and a[3]==b[3]
end

--- Addition metamethod
-- @param Angle a1 Initial angle.
-- @param Angle a2 Angle to add to the first.
-- @return Angle Resultant angle.
function ang_meta.__add(a, b)
	return wrap({ a[1] + b[1], a[2] + b[2], a[3] + b[3] })
end

--- Subtraction metamethod
-- @param Angle a1 Initial angle.
-- @param Angle a2 Angle to subtract.
-- @return Angle Resultant angle.
function ang_meta.__sub(a, b)
	return wrap({ a[1]-b[1], a[2]-b[2], a[3]-b[3] })
end

--- Returns if p,y,r are all 0.
-- @return boolean If they are all zero
function ang_methods:isZero()
	return self[1]==0 and self[2]==0 and self[3]==0
end

--- Return the Forward Vector ( direction the angle points ).
-- @return Vector Forward direction.
function ang_methods:getForward()
	return vwrap(unwrap1(self):Forward())
end

--- Return the Right Vector relative to the angle dir.
-- @return Vector Right direction.
function ang_methods:getRight()
	return vwrap(unwrap1(self):Right())
end

--- Return the Up Vector relative to the angle dir.
-- @return Vector Up direction.
function ang_methods:getUp()
	return vwrap(unwrap1(self):Up())
end

--- Return Rotated angle around the specified axis.
-- @param Vector v Vector axis
-- @param number? deg Number of degrees or nil if radians.
-- @param number? rad Number of radians or nil if degrees.
-- @return Angle The modified angle
function ang_methods:rotateAroundAxis(v, deg, rad)

	if rad then
		checkluatype (rad, TYPE_NUMBER)
		deg = math.deg(rad)
	else
		checkluatype (deg, TYPE_NUMBER)
	end

	local ret = unwrap1(self)
	ret:RotateAroundAxis(vunwrap1(v), deg)
	return awrap(ret)
end

--- Round the angle values.
-- Self-Modifies. Does not return anything
-- @param number? idp (Default 0) The integer decimal place to round to.
function ang_methods:round(idp)
	self[1] = math.Round(self[1], idp)
	self[2] = math.Round(self[2], idp)
	self[3] = math.Round(self[3], idp)
end

--- Copies p,y,r from angle and returns a new angle
-- @return Angle The copy of the angle
function ang_methods:clone()
	return wrap({ self[1], self[2], self[3] })
end

--- Copies p,y,r from angle to another.
-- Self-Modifies. Does not return anything
-- @param Angle b The angle to copy from.
function ang_methods:set(b)
	self[1] = b[1]
	self[2] = b[2]
	self[3] = b[3]
end

--- Sets p,y,r to 0. This is faster than doing it manually.
-- Self-Modifies. Does not return anything
function ang_methods:setZero()
	self[1] = 0
	self[2] = 0
	self[3] = 0
end

--- Set's the angle's pitch and returns self.
-- @param number p The pitch
-- @return Angle Angle after modification
function ang_methods:setP(p)
	self[1] = p
	return self
end

--- Set's the angle's yaw and returns self.
-- @param number y The yaw
-- @return Angle Angle after modification
function ang_methods:setY(y)
	self[2] = y
	return self
end

--- Set's the angle's roll and returns self.
-- @param number r The roll
-- @return Angle Angle after modification
function ang_methods:setR(r)
	self[3] = r
	return self
end

end
