---@class DataPoint
ExtraClasses.DataPoint = {
	a = false, -- is_additive
	r = 1, -- result
	f = {}, -- factors
	o_c = {}, -- on_changed
}

function ExtraClasses.DataPoint:has_factor(name)
	return self.f[name] ~= nil
end

function ExtraClasses.DataPoint:add_factor(name, factor)
	if self.f[name] == factor then return false end
	self.f[name] = factor
	self:_on_changed()
	return true
end

function ExtraClasses.DataPoint:remove_factor(name)
	if self.f[name] == nil then return false end
	self.f[name] = nil
	self:_on_changed()
	return true
end

function ExtraClasses.DataPoint:remove_all_factors()
	self.f = {}
	self:_on_changed()
end

function ExtraClasses.DataPoint:calculate_factor()
	local result = 1
	for name, factor in pairs(self.f) do
		result = result * factor
	end
	return result
end

function ExtraClasses.DataPoint:calculate_sum()
	local result = 0
	for name, factor in pairs(self.f) do
		result = result + factor
	end
	return result
end

function ExtraClasses.DataPoint:register_on_changed(callback)
	table.insert(self.o_c, callback)
end

function ExtraClasses.DataPoint:_on_changed()
	local new_value = 0
	if self.a then
		new_value = self:calculate_sum()
	else
		new_value = self:calculate_factor()
	end
	local old_value = self.r
	self.r = new_value
	for i, callback in ipairs(self.o_c) do
		callback(self, new_value, old_value)
	end
	return true
end

function ExtraClasses.DataPoint:get_result()
	return self.r
end

function ExtraClasses.DataPoint.__sub(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a - b
end
function ExtraClasses.DataPoint.__add(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a + b
end
function ExtraClasses.DataPoint.__mul(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a * b
end
function ExtraClasses.DataPoint.__div(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a / b
end
function ExtraClasses.DataPoint.__unm(a)
	return -a.r
end
function ExtraClasses.DataPoint.__mod(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a % b
end
function ExtraClasses.DataPoint.__pow(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return a ^ b
end
function ExtraClasses.DataPoint.__eq(a, b)
	return a.r == b.r
end
function ExtraClasses.DataPoint.__lt(a, b)
	return a.r < b.r
end
function ExtraClasses.DataPoint.__le(a, b)
	return a.r <= b.r
end
function ExtraClasses.DataPoint.__len(a)
	return a.r
end
function ExtraClasses.DataPoint.__tostring(a)
	return tostring(a.r)
end
function ExtraClasses.DataPoint.__concat(a, b)
	if getmetatable(a) == ExtraClasses.DataPoint then a = a.r end
	if getmetatable(b) == ExtraClasses.DataPoint then b = b.r end
	return tostring(a) .. tostring(b)
end

rawset(ExtraClasses.DataPoint, "__index", ExtraClasses.DataPoint)
ExtraClasses.DataPoint.__call = ExtraClasses.DataPoint.get_result

---@param input any
---@return DataPoint
function ExtraClasses.DataPoint.new(input)
	local input_type = type(input)
	-- copy, to avoid conflicting state
	local self = ((input_type == "table") and table.copy(input)) or {}
	-- start at 0 for additive, or 1 for multiplicative
	if (self.a == nil) and (input_type == "boolean") then self.a = input end
	self.r = (not self.a) and 1 or 0
	self.f = self.f or {}
	self.o_c = self.o_c or {}
	self = setmetatable(self, ExtraClasses.DataPoint)
	self:_on_changed()
	return self
end

--[[
local deser_example = {
	a = true,
	r = 2,
	f = {
		test1 = 4,
		test2 = 6,
	},
}
core.log("DataPoint: " .. dump(DataPoint.new(deser_example)()))
--]]
