--[=[
@c Color
@t ui
@mt mem
@p value number
@d Wrapper for 24-bit colors packed as a decimal value. See the static constructors for more information.
]=]

local class = require('class')

local format = string.format
local min, max, abs, floor = math.min, math.max, math.abs, math.floor
local lshift, rshift = bit.lshift, bit.rshift
local band, bor = bit.band, bit.bor
local bnot = bit.bnot
local isInstance = class.isInstance

local Color, get = class('Color')

local function check(self, other)
	if not isInstance(self, Color) or not isInstance(other, Color) then
		return error('Cannot perform operation with non-Color object', 2)
	end
end

local function clamp(n, mn, mx)
	return min(max(n, mn), mx)
end

function Color:__init(value)
	value = tonumber(value)
	self._value = value and band(value, 0xFFFFFF) or 0
end

function Color:__tostring()
	return format('Color: %s (%i, %i, %i)', self:toHex(), self:toRGB())
end

function Color:__eq(other) check(self, other)
	return self._value == other._value
end

function Color:__add(other) check(self, other)
	local r = clamp(self.r + other.r, 0, 0xFF)
	local g = clamp(self.g + other.g, 0, 0xFF)
	local b = clamp(self.b + other.b, 0, 0xFF)
	return Color.fromRGB(r, g, b)
end

function Color:__sub(other) check(self, other)
	local r = clamp(self.r - other.r, 0, 0xFF)
	local g = clamp(self.g - other.g, 0, 0xFF)
	local b = clamp(self.b - other.b, 0, 0xFF)
	return Color.fromRGB(r, g, b)
end

function Color:__mul(other)
	if not isInstance(self, Color) then
		self, other = other, self
	end
	other = tonumber(other)
	if other then
		local r = clamp(self.r * other, 0, 0xFF)
		local g = clamp(self.g * other, 0, 0xFF)
		local b = clamp(self.b * other, 0, 0xFF)
		return Color.fromRGB(r, g, b)
	else
		return error('Cannot perform operation with non-numeric object')
	end
end

function Color:__div(other)
	if not isInstance(self, Color) then
		return error('Division with Color is not commutative')
	end
	other = tonumber(other)
	if other then
		local r = clamp(self.r / other, 0, 0xFF)
		local g = clamp(self.g / other, 0, 0xFF)
		local b = clamp(self.b / other, 0, 0xFF)
		return Color.fromRGB(r, g, b)
	else
		return error('Cannot perform operation with non-numeric object')
	end
end

--[=[
@m fromHex
@t static
@p hex string
@r Color
@d Constructs a new Color object from a hexadecimal string. The string may or may
not be prefixed by `#`; all other characters are interpreted as a hex string.
]=]
function Color.fromHex(hex)
	return Color(tonumber(hex:match('#?(.*)'), 16))
end

--[=[
@m fromRGB
@t static
@p r number
@p g number
@p b number
@r Color
@d Constructs a new Color object from RGB values. Values are allowed to overflow
though one component will not overflow to the next component.
]=]
function Color.fromRGB(r, g, b)
	r = band(lshift(r, 16), 0xFF0000)
	g = band(lshift(g, 8), 0x00FF00)
	b = band(b, 0x0000FF)
	return Color(bor(bor(r, g), b))
end

local function fromHue(h, c, m)
	local x = c * (1 - abs(h / 60 % 2 - 1))
	local r, g, b
	if 0 <= h and h < 60 then
		r, g, b = c, x, 0
	elseif 60 <= h and h < 120 then
		r, g, b = x, c, 0
	elseif 120 <= h and h < 180 then
		r, g, b = 0, c, x
	elseif 180 <= h and h < 240 then
		r, g, b = 0, x, c
	elseif 240 <= h and h < 300 then
		r, g, b = x, 0, c
	elseif 300 <= h and h < 360 then
		r, g, b = c, 0, x
	end
	r = (r + m) * 0xFF
	g = (g + m) * 0xFF
	b = (b + m) * 0xFF
	return r, g, b
end

local function toHue(r, g, b)
	r = r / 0xFF
	g = g / 0xFF
	b = b / 0xFF
	local mn = min(r, g, b)
	local mx = max(r, g, b)
	local d = mx - mn
	local h
	if d == 0 then
		h = 0
	elseif mx == r then
		h = (g - b) / d % 6
	elseif mx == g then
		h = (b - r) / d + 2
	elseif mx == b then
		h = (r - g) / d + 4
	end
	h = floor(h * 60 + 0.5)
	return h, d, mx, mn
end

--[=[
@m fromHSV
@t static
@p h number
@p s number
@p v number
@r Color
@d Constructs a new Color object from HSV values. Hue is allowed to overflow
while saturation and value are clamped to [0, 1].
]=]
function Color.fromHSV(h, s, v)
	h = h % 360
	s = clamp(s, 0, 1)
	v = clamp(v, 0, 1)
	local c = v * s
	local m = v - c
	local r, g, b = fromHue(h, c, m)
	return Color.fromRGB(r, g, b)
end

--[=[
@m fromHSL
@t static
@p h number
@p s number
@p l number
@r Color
@d Constructs a new Color object from HSL values. Hue is allowed to overflow
while saturation and lightness are clamped to [0, 1].
]=]
function Color.fromHSL(h, s, l)
	h = h % 360
	s = clamp(s, 0, 1)
	l = clamp(l, 0, 1)
	local c = (1 - abs(2 * l - 1)) * s
	local m = l - c * 0.5
	local r, g, b = fromHue(h, c, m)
	return Color.fromRGB(r, g, b)
end

--[=[
@m toHex
@r string
@d Returns a 6-digit hexadecimal string that represents the color value.
]=]
function Color:toHex()
	return format('#%06X', self._value)
end

--[=[
@m toRGB
@r number
@r number
@r number
@d Returns the red, green, and blue values that are packed into the color value.
]=]
function Color:toRGB()
	return self.r, self.g, self.b
end

--[=[
@m toHSV
@r number
@r number
@r number
@d Returns the hue, saturation, and value that represents the color value.
]=]
function Color:toHSV()
	local h, d, mx = toHue(self.r, self.g, self.b)
	local v = mx
	local s = mx == 0 and 0 or d / mx
	return h, s, v
end

--[=[
@m toHSL
@r number
@r number
@r number
@d Returns the hue, saturation, and lightness that represents the color value.
]=]
function Color:toHSL()
	local h, d, mx, mn = toHue(self.r, self.g, self.b)
	local l = (mx + mn) * 0.5
	local s = d == 0 and 0 or d / (1 - abs(2 * l - 1))
	return h, s, l
end

--[=[@p value number The raw decimal value that represents the color value.]=]
function get.value(self)
	return self._value
end

local function getByte(value, offset)
	return band(rshift(value, offset), 0xFF)
end

--[=[@p r number The value that represents the color's red-level.]=]
function get.r(self)
	return getByte(self._value, 16)
end

--[=[@p g number The value that represents the color's green-level.]=]
function get.g(self)
	return getByte(self._value, 8)
end

--[=[@p b number The value that represents the color's blue-level.]=]
function get.b(self)
	return getByte(self._value, 0)
end

local function setByte(value, offset, new)
	local byte = lshift(0xFF, offset)
	value = band(value, bnot(byte))
	return bor(value, band(lshift(new, offset), byte))
end

--[=[
@m setRed
@r nil
@d Sets the color's red-level.
]=]
function Color:setRed(r)
	self._value = setByte(self._value, 16, r)
end

--[=[
@m setGreen
@r nil
@d Sets the color's green-level.
]=]
function Color:setGreen(g)
	self._value = setByte(self._value, 8, g)
end

--[=[
@m setBlue
@r nil
@d Sets the color's blue-level.
]=]
function Color:setBlue(b)
	self._value = setByte(self._value, 0, b)
end

--[=[
@m copy
@r Color
@d Returns a new copy of the original color object.
]=]
function Color:copy()
	return Color(self._value)
end

return Color
