--- A 3-dimensional vector.
-- @class
local Vector3 = {}
Vector3.__index = Vector3

--- Creates a new Vector3 instance.
-- @param x		number	The x co-ordinate value.
-- @param y		number	The y co-ordinate value.
-- @param z		number	The z co-ordinate value.
function Vector3.new(x, y, z)
	x = x or 0
	y = y or 0
	z = z or 0
	
	local result = {
		x = x,
		y = y,
		z = z
	}
	setmetatable(result, Vector3)
	return result
end

--- Returns a new instance of this vector.
-- @param	a			Vector3		The vector to clone.
-- @returns	Vector3		A new vector whose values are identical to those of the original vector.
function Vector3.clone(a)
	return Vector3.new(a.x, a.y, a.z)
end

--- Adds the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be added to each of the components of the vector.
-- @param	a	Vector3|number	The first item to add.
-- @param	a	Vector3|number	The second item to add.
-- @returns	Vector3				The result as a new Vector3 object.
function Vector3.add(a, b)
	if type(a) == "number" then
		return Vector3.new(b.x + a, b.y + a, b.z + a)
	elseif type(b) == "number" then
		return Vector3.new(a.x + b, a.y + b, a.z + b)
	end
	return Vector3.new(a.x + b.x, a.y + b.y, a.z + b.z)
end

--- Subtracts the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be subtracted to each of the components of the vector.
-- @param	a	Vector3|number	The first item to subtract.
-- @param	a	Vector3|number	The second item to subtract.
-- @returns	Vector3				The result as a new Vector3 object.
function Vector3.subtract(a, b)
	if type(a) == "number" then
		return Vector3.new(b.x - a, b.y - a, b.z - a)
	elseif type(b) == "number" then
		return Vector3.new(a.x - b, a.y - b, a.z - b)
	end
	return Vector3.new(a.x - b.x, a.y - b.y, a.z - b.z)
end
--- Alias for Vector3.subtract.
function Vector3.sub(a, b) return Vector3.subtract(a, b) end

--- Multiplies the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be multiplied to each of the components of the vector.
--
-- If both of the inputs are vectors, then the components are multiplied
-- by each other (NOT the cross product). In other words:
-- a.x * b.x, a.y * b.y, a.z * b.z
--
-- @param	a	Vector3|number	The first item to multiply.
-- @param	a	Vector3|number	The second item to multiply.
-- @returns	Vector3				The result as a new Vector3 object.
function Vector3.multiply(a, b)
	if type(a) == "number" then
		return Vector3.new(b.x * a, b.y * a, b.z * a)
	elseif type(b) == "number" then
		return Vector3.new(a.x * b, a.y * b, a.z * b)
	end
	return Vector3.new(a.x * b.x, a.y * b.y, a.z * b.z)
end
--- Alias for Vector3.multiply.
function Vector3.mul(a, b) return Vector3.multiply(a, b) end

--- Divides the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be divided to each of the components of the vector.
-- @param	a	Vector3|number	The first item to divide.
-- @param	a	Vector3|number	The second item to divide.
-- @returns	Vector3				The result as a new Vector3 object.
function Vector3.divide(a, b)
	if type(a) == "number" then
		return Vector3.new(b.x / a, b.y / a, b.z / a)
	elseif type(b) == "number" then
		return Vector3.new(a.x / b, a.y / b, a.z / b)
	end
	return Vector3.new(a.x / b.x, a.y / b.y, a.z / b.z)
end
--- Alias for Vector3.divide.
function Vector3.div(a, b) return Vector3.divide(a, b) end


--- Rounds the components of this vector down.
-- @param 	a		Vector3		The vector to operate on.
-- @returns	Vector3	A new instance with the x/y/z components rounded down.
function Vector3.floor(a)
	return Vector3.new(math.floor(a.x), math.floor(a.y), math.floor(a.z))
end
--- Rounds the components of this vector up.
-- @param 	a		Vector3		The vector to operate on.
-- @returns	Vector3	A new instance with the x/y/z components rounded up.
function Vector3.ceil(a)
	return Vector3.new(math.ceil(a.x), math.ceil(a.y), math.ceil(a.z))
end
--- Rounds the components of this vector.
-- @param 	a		Vector3		The vector to operate on.
-- @returns	Vector3	A new instance with the x/y/z components rounded.
function Vector3.round(a)
	return Vector3.new(math.floor(a.x+0.5), math.floor(a.y+0.5), math.floor(a.z+0.5))
end


--- Snaps this Vector3 to an imaginary square grid with the specified sized
-- squares.
-- @param 	a		Vector3		The vector to operate on.
-- @param	number	grid_size	The size of the squares on the imaginary grid to which to snap.
-- @returns	Vector3	A new Vector3 instance snapped to an imaginary grid of the specified size.
function Vector3.snap_to(a, grid_size)
	return (a / grid_size):round() * grid_size
end

--- Returns the area of this vector.
-- In other words, multiplies all the components together and returns a scalar value.
-- @param	a		Vector3		The vector to return the area of.
-- @returns	number	The area of this vector.
function Vector3.area(a)
	return a.x * a.y * a.z
end

--- Returns the scalar length of this vector squared.
-- @param	a		Vector3		The vector to operate on.
-- @returns	number	The length squared of this vector as a scalar value.
function Vector3.length_squared(a)
	return a.x * a.x + a.y * a.y + a.z * a.z
end

--- Square roots each component of this vector.
-- @param	a		Vector3		The vector to operate on.
-- @returns	number	A new vector with each component square rooted.
function Vector3.sqrt(a)
	return Vector3.new(math.sqrt(a.x), math.sqrt(a.y), math.sqrt(a.z))
end

--- Calculates the scalar length of this vector.
-- @param	a		Vector3		The vector to operate on.
-- @returns	number	The length of this vector as a scalar value.
function Vector3.length(a)
	return math.sqrt(a:length_squared())
end

--- Calculates the dot product of this vector and another vector.
-- @param	a		Vector3		The first vector to operate on.
-- @param	a		Vector3		The second vector to operate on.
-- @returns	number	The dot product of this vector as a scalar value.
function Vector3.dot(a, b)
	return a.x * b.x + a.y * b.y + a.z * b.z;
end
--- Alias of Vector3.dot.
function Vector3.dot_product(a, b)
	return Vector3.dot(a, b)
end

--- Determines if 2 vectors are equal to each other.
-- 2 vectors are equal if their values are identical.
-- @param	a		Vector3		The first vector to test.
-- @param	a		Vector3		The second vector to test.
-- @returns	bool	Whether the 2 vectors are equal or not.
function Vector3.equals(a, b)
	return a.x == b.x
		and a.y == b.y
		and a.z == b.z
end

--- Returns a new vector whose length clamped to the given length.
-- The direction in which the vector is pointing is not changed.
-- @param	a		Vector3		The vector to operate on.
-- @returns	Vector3	A new Vector3 instance limited to the specified length.
function Vector3.limit_to(a, length)
	if type(length) ~= "number" then error("Error: Expected number, but found "..type(length)..".") end
	
	if a:length() > length then
		return (a / a:length()) * length
	end
	return a:clone()
end

--- Returns a new vector whose length clamped to the given length.
-- The direction in which the vector is pointing is not changed.
-- @param	a		Vector3		The vector to operate on.
-- @returns	Vector3	A new Vector3 instance limited to the specified length.
function Vector3.set_to(a, length)
	if type(length) ~= "number" then error("Error: Expected number, but found "..type(length)..".") end
	
	return (a / a:length()) * length
end

--- Returns the unit vector of this vector.
-- The unit vector is a vector with a length of 1.
-- Returns a new vector.
-- Does not change the direction of the vector.
-- @param	a		Vector3		The vector to operate on.
-- @returns	Vector3	The unit vector of this vector.
function Vector3.unit(a)
	return a / a:length()
end
--- Alias of Vector3.unit.
function Vector3.normalise(a) return a:unit() end


--- Return a vector that is amount distance towards b from a.
-- @param	a		Vector3		The vector to move from.
-- @param	b		Vector3		The vector to move towards.
-- @param	amount	number		The amount to move.
function Vector3.move_towards(a, b, amount)
	return a + (b - a):limit_to(amount)
end

--- Returns the value of the minimum component of the vector.
-- Returns a scalar value.
-- @param	a		Vector3		The vector to operate on.
-- @returns	number	The value of the minimum component of the vector.
function Vector3.min_component(a)
	return math.min(a.x, a.y, a.z)
end

--- Returns the value of the maximum component of the vector.
-- Returns a scalar value.
-- @param	a		Vector3		The vector to operate on.
-- @returns	number	The value of the maximum component of the vector.
function Vector3.max_component(a)
	return math.max(a.x, a.y, a.z)
end

--- Returns the absolute form of this vector.
-- In other words, it removes the minus sign from all components of the vector.
-- @param	a		Vector3		The vector to operate on.
-- @returns	Vector3	The absolute form of the given vector.
function Vector3.abs(a)
	return Vector3.new(math.abs(a.x), math.abs(a.y), math.abs(a.z))
end

--- Sorts the components of the given vectors.
-- pos1 will contain the minimum values, and pos2 the maximum values.
-- Returns 2 new vectors.
-- Note that for this specific function
-- the vectors provided do not *have* to be instances of Vector3.
-- It is only required that they have the keys x, y, and z.
-- Vector3 instances are always returned.
-- This enables convenient ingesting of positions from outside.
-- @param	pos1	Vector3		The first vector to operate on.
-- @param	pos2	Vector3		The second vector to operate on.
-- @returns	Vector3,Vector3		The 2 sorted vectors (min, max).
function Vector3.sort(pos1, pos2)
	local pos1_new = Vector3.clone(pos1) -- This way we can accept non-Vector3 instances
	local pos2_new = Vector3.clone(pos2) -- This way we can accept non-Vector3 instances
	if pos1_new.x > pos2_new.x then
		pos1_new.x, pos2_new.x = pos2_new.x, pos1_new.x
	end
	if pos1_new.y > pos2_new.y then
		pos1_new.y, pos2_new.y = pos2_new.y, pos1_new.y
	end
	if pos1_new.z > pos2_new.z then
		pos1_new.z, pos2_new.z = pos2_new.z, pos1_new.z
	end
	return pos1_new, pos2_new
end

--- Determines if this vector is contained within the region defined by the given vectors.
-- @param	a		Vector3		The target vector to check.
-- @param	pos1	Vector3		pos1 of the defined region.
-- @param	pos2	Vector3		pos2 of the defined region.
-- @return	boolean	Whether the given target is contained within the defined worldedit region.
function Vector3.is_contained(target, pos1, pos2)
	local pos1, pos2 = Vector3.sort(pos1, pos2)
	
	return pos1.x <= target.x
		and pos1.y <= target.y
		and pos1.z <= target.z
		and pos2.x >= target.x
		and pos2.y >= target.y
		and pos2.z >= target.z
end


--- Expands the defined region to include the given point.
-- @param	target	Vector3	The target vector to include.
-- @param	pos1	Vector3	pos1 of the defined region.
-- @param	pos2	Vector3	pos2 of the defined region.
-- @returns	Vector3,Vector3		2 vectors that represent the expand_region.
function Vector3.expand_region(target, pos1, pos2)
	local pos1, pos2 = Vector3.sort(pos1, pos2)
	
	if target.x < pos1.x then pos1.x = target.x end
	if target.y < pos1.y then pos1.y = target.y end
	if target.z < pos1.z then pos1.z = target.z end
	
	if target.x > pos2.x then pos2.x = target.x end
	if target.y > pos2.y then pos2.y = target.y end
	if target.z > pos2.z then pos2.z = target.z end
	
	return pos1, pos2
end

--- Returns the mean (average) of 2 positions.
-- In other words, returns the centre of 2 points.
-- @param	pos1	Vector3|number	pos1 of the defined region.
-- @param	pos2	Vector3|number	pos2 of the defined region.
-- @param	target	Vector3	Centre coordinates.
function Vector3.mean(pos1, pos2)
	return (pos1 + pos2) / 2
end


--- Returns a vector of the min components of 2 vectors.
-- @param	pos1	Vector3|number	The first vector to operate on.
-- @param	pos2	Vector3|number	The second vector to operate on.
-- @return	Vector3	The minimum values from the input vectors
function Vector3.min(pos1, pos2)
	if type(pos1) == "number" then
		pos1 = Vector3.new(pos1, pos1, pos1)
	end
	if type(pos2) == "number" then
		pos2 = Vector3.new(pos2, pos2, pos2)
	end
	return Vector3.new(
		math.min(pos1.x, pos2.x),
		math.min(pos1.y, pos2.y),
		math.min(pos1.z, pos2.z)
	)
end

--- Returns a vector of the max values of 2 vectors.
-- @param	pos1	Vector3		The first vector to operate on.
-- @param	pos2	Vector3		The second vector to operate on.
-- @return	Vector3		The maximum values from the input vectors.
function Vector3.max(pos1, pos2)
	if type(pos1) == "number" then
		pos1 = Vector3.new(pos1, pos1, pos1)
	end
	if type(pos2) == "number" then
		pos2 = Vector3.new(pos2, pos2, pos2)
	end
	return Vector3.new(
		math.max(pos1.x, pos2.x),
		math.max(pos1.y, pos2.y),
		math.max(pos1.z, pos2.z)
	)
end

--- Given 2 angles and a length, return a Vector3 pointing in that direction.
-- Consider a sphere, with the 2 angles defining a point on the sphere's surface.
-- This function returns that point as a Vector3.
-- @source	https://math.stackexchange.com/a/1881767/221181
-- @param	angle_x		number	The X angle.
-- @param	angle_y		number	The Y angle.
-- @param	length		number	The radius of the sphere in question.
-- @returns	Vector3		The point on the sphere defined by the aforementioned parameters.
function Vector3.fromBearing(angle_x, angle_y, length)
	return Vector3.new( -- X and Y swapped
		length * math.cos(angle_x),
		length * math.sin(angle_x) * math.sin(angle_y),
		length * math.sin(angle_x) * math.cos(angle_y)
	)
end



--  ██████  ██████  ███████ ██████   █████  ████████  ██████  ██████
-- ██    ██ ██   ██ ██      ██   ██ ██   ██    ██    ██    ██ ██   ██
-- ██    ██ ██████  █████   ██████  ███████    ██    ██    ██ ██████
-- ██    ██ ██      ██      ██   ██ ██   ██    ██    ██    ██ ██   ██
--  ██████  ██      ███████ ██   ██ ██   ██    ██     ██████  ██   ██
--
--  ██████  ██    ██ ███████ ██████  ██████  ██ ██████  ███████ ███████
-- ██    ██ ██    ██ ██      ██   ██ ██   ██ ██ ██   ██ ██      ██
-- ██    ██ ██    ██ █████   ██████  ██████  ██ ██   ██ █████   ███████
-- ██    ██  ██  ██  ██      ██   ██ ██   ██ ██ ██   ██ ██           ██
--  ██████    ████   ███████ ██   ██ ██   ██ ██ ██████  ███████ ███████

function Vector3.__call(x, y, z) return Vector3.new(x, y, z) end

function Vector3.__add(a, b)
	return Vector3.add(a, b)
end

function Vector3.__sub(a, b)
	return Vector3.sub(a, b)
end

function Vector3.__mul(a, b)
	return Vector3.mul(a, b)
end

function Vector3.__div(a, b)
	return Vector3.divide(a, b)
end

function Vector3.__eq(a, b)
	return Vector3.equals(a, b)
end

--- Returns the current Vector3 as a string.
function Vector3.__tostring(a)
	return "("..a.x..", "..a.y..", "..a.z..")"
end

function Vector3.__concat(a, b)
	if type(a) ~= "string" then a = tostring(a) end
	if type(b) ~= "string" then b = tostring(b) end
	return a .. b
end


return Vector3
