--
-- (C) 2013-21 - ntop.org
--

local plugins_consts_utils = {}
local dirs = ntop.getDirs()

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

local AVAILABLE_CONSTS = {
   flow = {
      min_id = 1, max_id = 63, reuse_ids = true,
      reserved_ids = {
	 status_normal = 0,
      },
   },
   alert = {
      min_id = 0, max_id = 255, reuse_ids = false,
   }
}

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

local id_requests = {
   flow = {},
   alert = {}
}

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

local function assigned_ids_hname(const_type)
   return string.format("ntopng.prefs.plugins_consts_utils.assigned_ids.const_type_%s", const_type)
end

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

local function assigned_id_key(const_type, const_key)
   local hname = assigned_ids_hname(const_type)
   return string.format("%s.const_key_%s", hname, const_key)
end

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

local function get_assigned_ids(const_type)
   local hname = assigned_ids_hname(const_type)
   local assigned_ids = ntop.getHashAllCache(hname) or {}
   local cur_id_to_key = {}
   local cur_key_to_id = {}

   for const_id, const_key in pairs(assigned_ids) do
      local const_id_n = tonumber(const_id)

      if const_id_n < AVAILABLE_CONSTS[const_type]["min_id"] or const_id_n > AVAILABLE_CONSTS[const_type]["max_id"] then
	 traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Ignoring id %i outside boundaries for '%s'", const_id_n, const_type))
      else
	 cur_id_to_key[const_id_n] = const_key
	 cur_key_to_id[const_key] = const_id_n
      end
   end

   return cur_id_to_key, cur_key_to_id
end

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

local function commit_new_ids(const_type, new_ids)
   local hname = assigned_ids_hname(const_type)

   for const_key, const_id in pairs(new_ids) do
      local const_id_s = string.format("%i", const_id)

      -- Attempt to lookup for the id in the cache. Attempting to commit a new id to an existing
      -- it, causes the old key to be reused the new key. This is used for example when reusing ids
      local existing = ntop.getHashCache(hname, const_id)
      if not isEmptyString(existing) then
	 traceError(TRACE_NORMAL, TRACE_CONSOLE, string.format("Reusing id %i '%s' to '%s'", const_id, existing, const_key))
	 ntop.delHashCache(hname, const_id_s)

	 local existing_kname = assigned_id_key(const_type, existing)
	 ntop.delCache(existing_kname)
      end

      -- Now it's time to set the new id, both in the hash table...
      ntop.setHashCache(hname, const_id, const_key)

      -- ... and as a single key
      local kname = assigned_id_key(const_type, const_key)
      ntop.setPref(kname, const_id_s)
   end
end

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

function plugins_consts_utils.clear_id_requests(const_type)
   id_requests[const_type] = {}
end

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

function plugins_consts_utils.request_id(const_type, const_key)
   if not AVAILABLE_CONSTS[const_type] then
      traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Unknown const type '%s'", const_type))
      return
   end

   id_requests[const_type][#id_requests[const_type] + 1] = const_key
end

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

function plugins_consts_utils.assign_requested_ids(const_type)
   if not AVAILABLE_CONSTS[const_type] then
      traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Unknown const type '%s'", const_type))
      return
   end

   local cur_id_to_key, cur_key_to_id = get_assigned_ids(const_type)
   local available_ids = {}
   local used_ids = {}
   local new_ids = {}
   local found_ids = {}
   local deferred_requests = {}
   local reused_ids = {}

   -- Prepare used and available ids by iterating the current existing ids (i.e., those
   -- that have already been assigned)
   for i = AVAILABLE_CONSTS[const_type]["min_id"], AVAILABLE_CONSTS[const_type]["max_id"] do
      if cur_id_to_key[i] then
	 used_ids[#used_ids + 1] = i
      else
	 available_ids[#available_ids + 1] = i
      end
   end

   for _, req in ipairs(id_requests[const_type]) do
      if AVAILABLE_CONSTS[const_type]["reserved_ids"] and AVAILABLE_CONSTS[const_type]["reserved_ids"][req] then
	 -- Id among those reserved, so id is already assigned and there is nothing to do
      elseif cur_key_to_id[req] then
	 -- Id found: already assigned, nothing to do
	 found_ids[#found_ids + 1] = cur_key_to_id[req]
      elseif #available_ids > 0 then
	 -- At least an id is available in the pool of available ids
	 new_ids[req] = table.remove(available_ids, 1)
      elseif AVAILABLE_CONSTS[const_type]["reuse_ids"] then
	 -- No ids available but reuse of old ids is enabled
	 deferred_requests[#deferred_requests + 1] = req
      else
	 -- No ids available and reuse not allowed, unable to assign.
	 traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Unable to assign an id to '%s' for const type '%s'. No more ids available", req, const_type))
      end
   end

   commit_new_ids(const_type, new_ids)

   if #deferred_requests > 0 then
      for _, used_id in ipairs(used_ids) do
	 local used_id_found = false

	 for _, found_id in ipairs(found_ids) do
	    if used_id == found_id then
	       -- Used id found during this run, cannot reuse it
	       used_id_found = true
	       break
	    end
	 end

	 if not used_id_found then
	    -- This id is used but not found so it means no plugin is using it (during this run) and it can be reused
	    reused_ids[table.remove(deferred_requests, 1)] = used_id
	 end

	 if #deferred_requests == 0 then
	    -- No more work to do
	    break
	 end
      end

      commit_new_ids(const_type, reused_ids)

      if #deferred_requests > 0 then
	 for _, req in ipairs(deferred_requests) do
	    traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Unable to reuse any id for '%s' with const type '%s'.", req, const_type))
	 end
      end
   end
end

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

function plugins_consts_utils.get_assigned_id(const_type, const_key)
   if not AVAILABLE_CONSTS[const_type] then
      traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Unknown const type '%s'", const_type))
      return
   end

   if AVAILABLE_CONSTS[const_type]["reserved_ids"] and AVAILABLE_CONSTS[const_type]["reserved_ids"][const_key] then
      return AVAILABLE_CONSTS[const_type]["reserved_ids"][const_key]
   end

   -- Avoid reading the whole hash, just read the single key to re-use also ntopng internal caching mechanism
   local kname = assigned_id_key(const_type, const_key)
   local assigned_id = tonumber(ntop.getPref(kname))

   if assigned_id == nil then
     -- Not found in the internal cache, check the hash
     local cur_id_to_key, cur_key_to_id = get_assigned_ids(const_type)
     if cur_key_to_id[const_key] ~= nil then
        return cur_key_to_id[const_key]
     end
   end

   -- Make sure the index is within the configured bounds
   if assigned_id then
      if assigned_id < AVAILABLE_CONSTS[const_type]["min_id"] or assigned_id > AVAILABLE_CONSTS[const_type]["max_id"] then
	 traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Ignoring id %i outside boundaries for '%s'", assigned_id, const_type))
	 -- Clear it as it should have that value
	 assigned_id = nil
      end
   end

   return assigned_id
end

return plugins_consts_utils
