--[=[
@c Time
@t ui
@mt mem
@d Represents a length of time and provides utilities for converting to and from
different formats. Supported units are: weeks, days, hours, minutes, seconds,
and milliseconds.
]=]

local class = require('class')
local constants = require('constants')

local MS_PER_S    =               constants.MS_PER_S
local MS_PER_MIN  = MS_PER_S    * constants.S_PER_MIN
local MS_PER_HOUR = MS_PER_MIN  * constants.MIN_PER_HOUR
local MS_PER_DAY  = MS_PER_HOUR * constants.HOUR_PER_DAY
local MS_PER_WEEK = MS_PER_DAY  * constants.DAY_PER_WEEK

local insert, concat = table.insert, table.concat
local modf, fmod = math.modf, math.fmod
local isInstance = class.isInstance

local function decompose(value, mult)
	return modf(value / mult), fmod(value, mult)
end

local units = {
	{'weeks', MS_PER_WEEK},
	{'days', MS_PER_DAY},
	{'hours', MS_PER_HOUR},
	{'minutes', MS_PER_MIN},
	{'seconds', MS_PER_S},
	{'milliseconds', 1},
}

local Time = class('Time')

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

function Time:__init(value)
	self._value = tonumber(value) or 0
end

function Time:__tostring()
	return 'Time: ' .. self:toString()
end

--[=[
@m toString
@r string
@d Returns a human-readable string built from the set of normalized time values
that the object represents.
]=]
function Time:toString()
	local ret = {}
	local ms = self:toMilliseconds()
	for _, unit in ipairs(units) do
		local n
		n, ms = decompose(ms, unit[2])
		if n == 1 then
			insert(ret, n .. ' ' .. unit[1]:sub(1, -2))
		elseif n > 0 then
			insert(ret, n .. ' ' .. unit[1])
		end
	end
	return #ret > 0 and concat(ret, ', ') or '0 milliseconds'
end

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

function Time:__lt(other) check(self, other)
	return self._value < other._value
end

function Time:__le(other) check(self, other)
	return self._value <= other._value
end

function Time:__add(other) check(self, other)
	return Time(self._value + other._value)
end

function Time:__sub(other) check(self, other)
	return Time(self._value - other._value)
end

function Time:__mul(other)
	if not isInstance(self, Time) then
		self, other = other, self
	end
	other = tonumber(other)
	if other then
		return Time(self._value * other)
	else
		return error('Cannot perform operation with non-numeric object')
	end
end

function Time:__div(other)
	if not isInstance(self, Time) then
		return error('Division with Time is not commutative')
	end
	other = tonumber(other)
	if other then
		return Time(self._value / other)
	else
		return error('Cannot perform operation with non-numeric object')
	end
end

--[=[
@m fromWeeks
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as weeks, where a week
is equal to 7 days.
]=]
function Time.fromWeeks(t)
	return Time(t * MS_PER_WEEK)
end

--[=[
@m fromDays
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as days, where a day is
equal to 24 hours.
]=]
function Time.fromDays(t)
	return Time(t * MS_PER_DAY)
end

--[=[
@m fromHours
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as hours, where an hour is
equal to 60 minutes.
]=]
function Time.fromHours(t)
	return Time(t * MS_PER_HOUR)
end

--[=[
@m fromMinutes
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as minutes, where a minute
is equal to 60 seconds.
]=]
function Time.fromMinutes(t)
	return Time(t * MS_PER_MIN)
end

--[=[
@m fromSeconds
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as seconds, where a second
is equal to 1000 milliseconds.
]=]
function Time.fromSeconds(t)
	return Time(t * MS_PER_S)
end

--[=[
@m fromMilliseconds
@t static
@p t number
@r Time
@d Constructs a new Time object from a value interpreted as milliseconds, the base
unit represented.
]=]
function Time.fromMilliseconds(t)
	return Time(t)
end

--[=[
@m fromTable
@t static
@p t table
@r Time
@d Constructs a new Time object from a table of time values where the keys are
defined in the constructors above (eg: `weeks`, `days`, `hours`).
]=]
function Time.fromTable(t)
	local n = 0
	for _, v in ipairs(units) do
		local m = tonumber(t[v[1]])
		if m then
			n = n + m * v[2]
		end
	end
	return Time(n)
end

--[=[
@m toWeeks
@r number
@d Returns the total number of weeks that the time object represents.
]=]
function Time:toWeeks()
	return self:toMilliseconds() / MS_PER_WEEK
end

--[=[
@m toDays
@r number
@d Returns the total number of days that the time object represents.
]=]
function Time:toDays()
	return self:toMilliseconds() / MS_PER_DAY
end

--[=[
@m toHours
@r number
@d Returns the total number of hours that the time object represents.
]=]
function Time:toHours()
	return self:toMilliseconds() / MS_PER_HOUR
end

--[=[
@m toMinutes
@r number
@d Returns the total number of minutes that the time object represents.
]=]
function Time:toMinutes()
	return self:toMilliseconds() / MS_PER_MIN
end

--[=[
@m toSeconds
@r number
@d Returns the total number of seconds that the time object represents.
]=]
function Time:toSeconds()
	return self:toMilliseconds() / MS_PER_S
end

--[=[
@m toMilliseconds
@r number
@d Returns the total number of milliseconds that the time object represents.
]=]
function Time:toMilliseconds()
	return self._value
end

--[=[
@m toTable
@r number
@d Returns a table of normalized time values that represent the time object in
a more accessible form.
]=]
function Time:toTable()
	local ret = {}
	local ms = self:toMilliseconds()
	for _, unit in ipairs(units) do
		ret[unit[1]], ms = decompose(ms, unit[2])
	end
	return ret
end

return Time
