--
-- (C) 2019-24 - ntop.org
--

local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/datamodel/?.lua;" .. package.path

-- ##############################################

-- Import the classes library.
local classes = require "classes"
-- Import some colors for eyecandy
local colors = require "graph_utils".graph_colors
local rest_utils = require "rest_utils"

-- ##############################################

local slices = classes.class()

-- ##############################################

-- This is the base REST prefix for all the available datasources
slices.BASE_REST_PREFIX = "/lua/rest/v2/get/datasource/"

-- ##############################################

-- Default values
local default_max_num_slices      = 10 -- Maximum number of slices handled
local default_other_threshold_pct = 3  -- Percentage under which the slice is ignored and value added to a slice 'other'

-- ##############################################

-- @brief Datasource constructor
function slices:init(max_num_slices, other_threshold_pct)
   self.max_num_slices = max_num_slices or default_max_num_slices
   self.other_threshold_pct = other_threshold_pct or default_other_threshold_pct

   self.column_labels = labels
   self._data = {}
end

-- #######################################################

-- @brief append data to the model.
-- @param data_key The key identifying the data, i.e., a timestamp (for timeseries) or a string (for histograms)
-- @param data_values The value or values associated to the data key. Can be a salar or an array.
-- @param data_url A URL associated to this data identified with `data_key` (optional)
-- @param data_color A color associated to this data identified with `data_color` (optional)
--        OVERRIDE
function slices:append(data_key, data_values, data_url, data_color)
   -- Always append ordered data
   self._data[#self._data + 1] = {
      k = data_key,       -- The Key
      v = data_values,    -- The Values
      url = data_url,
      color = colors[(#self._data % #colors) + 1],
   }
end

-- #######################################################

-- @brief Aggregates `append`ed data, enforcing `max_num_slices` and `other_threshold_pct`
function slices:aggregate()
   local aggregated = {}
   local other
   local total_value = 0
   local cur_slice = 1

   -- Compute the total
   for _, slice in ipairs(self._data) do
      total_value = total_value + slice.v
   end

   -- Sort by descending `v`alue of slice
   for _, slice in ipairs(self._data) do
      local slice_key = slice.k

      if cur_slice < self.max_num_slices and slice.v / total_value * 100 > self.other_threshold_pct then
	 -- Preserve this slice
	 aggregated[#aggregated + 1] = slice
      elseif slice.v > 0 then
	 -- Start adding to the 'other' slice
	 if not other then
	    other = slice
	    other.k = 'other'
	 else
	    -- Sum the current `other` value with the value for this slice
	    other.v = other.v + slice.v
	 end
      end

      cur_slice = cur_slice + 1
   end

   if other then
      aggregated[#aggregated + 1] = other
   end

   self._data = aggregated
end

-- #######################################################

function slices:_get_data_keys()
   local res = {}

   for _, data in ipairs(self._data) do
      res[#res + 1] = data.k
   end

   return res
end

-- #######################################################

function slices:_get_data_values()
   local res = {}

   for _, data in ipairs(self._data) do
      res[#res + 1] = data.v
   end

   return res
end

-- #######################################################

function slices:_get_data_colors()
   local res = {}

   for _, data in ipairs(self._data) do
      res[#res + 1] = data.color
   end

   return res
end

-- #######################################################

-- Transform and return datamodel data
-- @brief Transform data according to the specified transformation
-- @param transformation The transformation to be applied
-- @return transformed data
function slices:transform(transformation)
   if transformation == "aggregate" then
      -- Transform, enforce maximum number of results, % returned
      self:aggregate()
   else -- transformation == "none", i.e., all data is returned, no filtering, no reduction

   end

   return {
      label  = self.column_labels,
      keys   = self:_get_data_keys(),
      values = self:_get_data_values(),
      colors = self:_get_data_colors()
   }
end

-- ##############################################

function slices:set_label(label)
   self.column_labels = label
end

-- ##############################################

-- ##############################################

-- @brief Parses params
-- @param params_table A table with submitted params
-- @return True if parameters parsing is successful, false otherwise
function slices:read_params(params_table)
   if not params_table then
      self.parsed_params = nil
      return false
   end

   self.parsed_params = {}
   for _, param in pairs(self.meta.params or {}) do
      local parsed_param = params_table[param]

      -- Assumes all params mandatory and not empty
      -- May override this behavior in subclasses
      if isEmptyString(parsed_param) then
	 -- Reset any possibly set param
	 self.parsed_params = nil

	 return false
      end

      self.parsed_params[param] = parsed_param
   end

   -- Ok, parsin has been successful
   return true
end

-- ##############################################

-- @brief Parses params submitted along with the REST endpoint request. If parsing fails, a REST error is sent.
-- @param params_table A table with submitted params, either _POST or _GET
-- @return True if parameters parsing is successful, false otherwise
function slices:_rest_read_params(params_table)
   if not self:read_params(params_table) then
      rest_utils.answer(rest_utils.consts.err.widgets_missing_datasource_params)
      return false
   end

   return true
end

-- ##############################################

-- @brief Send slices data via REST
function slices:rest_send_response()
   -- Make sure this is a direct REST request and not just a require() that needs this class
   if not _SERVER -- Not executing a Lua script initiated from the web server (i.e., backend execution)
   or not _SERVER["URI"] -- Cannot reliably determine if this is a REST request
   or not _SERVER["URI"]:starts(slices.BASE_REST_PREFIX) -- Web Lua script execution but not for this REST endpoint
   then
      -- Don't send any REST response
      return
   end

   if not self:_rest_read_params(_POST) then
      -- Params parsing has failed, error response already sent by the caller
      return
   end

   self:fetch()

   rest_utils.answer(
      rest_utils.consts.success.ok,
      self._data
   )
end

-- ##############################################

-- @brief Returns instance metadata, which depends on the current instance and parsed_params
function slices:get_metadata()
   local res = {}

   -- Render a url with submitted parsed_params
   -- TODO: add url if necessary

   return res
end

-- #######################################################

return slices
