--
-- (C) 2017 - ntop.org
--

local ts_common = {}

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

ts_common.metrics = {}
ts_common.metrics.counter = "counter"
ts_common.metrics.gauge = "gauge"
ts_common.metrics.derivative = "derivative"

ts_common.aggregation = {}
ts_common.aggregation.mean = "mean"
ts_common.aggregation.max = "max"
ts_common.aggregation.min = "min"
ts_common.aggregation.last = "last"

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

-- Find the percentile of a list of values
-- N - A list of values.  N must be sorted.
-- P - A float value from 0.0 to 1.0
local function percentile(N, P)
  local n = math.floor(math.floor(P * #N + 0.5))
  return(N[n-1])
end

function ts_common.ninetififthPercentile(serie)
  if #serie <= 1 then
    return serie[1]
  end

  local N = {}

  for i, val in ipairs(serie) do
    if val ~= val then
      -- Convert NaN to 0
      val = 0
    end

    N[i] = val
  end

  table.sort(N) -- <<== Sort first
  return(percentile(N, 0.95))
end

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

function ts_common.calculateMinMax(total_serie)
  local min_val, max_val
  local min_val_pt, max_val_pt

  for idx, val in pairs(total_serie) do
    -- Exclude NaN points
    if val == val then
      if (min_val_pt == nil) or (val < min_val) then
        min_val = val
        min_val_pt = idx - 1
      end
      if (max_val_pt == nil) or (val > max_val) then
        max_val = val
        max_val_pt = idx - 1
      end
    end
  end

  return {
    min_val = min_val,
    max_val = max_val,
    min_val_idx = min_val_pt,
    max_val_idx = max_val_pt,
  }
end

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

function ts_common.calculateStatistics(total_serie, step, tdiff, data_type)
  local total = 0
  local pt_sum = 0

  for idx, val in pairs(total_serie) do
    if val ~= val then
      -- Convert NaN to 0
      val = 0
    end

    -- integrate
    total = total + val * step
    pt_sum = pt_sum + val
  end

  local avg = pt_sum / #total_serie

  if data_type == ts_common.metrics.gauge then
    -- no total for gauge values!
    total = nil
  end

  return {
    total = total,
    average = avg,
    ["95th_percentile"] = ts_common.ninetififthPercentile(total_serie),
  }
end

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

-- NOTE: this corresponds to graph_utils interpolateSerie
-- This is approximate.
-- Aproximate means it doesn't adjust the step.
-- TODO: take as input and adjust the step as well.
function ts_common.upsampleSerie(serie, num_points)
  if num_points <= #serie then
    return serie
  end

  local res = {}
  local intervals = num_points / #serie;

  local lerp = function(v0, v1, t)
    return (1 - t) * v0 + t * v1
  end

  if #serie == 0 then
    return res
  end

  for i=1,num_points do
    local index = (i-1) / (intervals)
    local _, t = math.modf(index)
    local prev_i = math.floor(index)
    local next_i = math.min(math.ceil(index), #serie - 1)

    local v = lerp(serie[prev_i+1], serie[next_i+1], t)
    res[i] = v
  end

  return res
end

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

-- If a point value exceeds this value, it should be discarded as invalid
function ts_common.getMaxPointValue(schema, metric, tags)
  if tags.ifid ~= nil then
    if string.contains(metric, "bytes") then
      local ifspeed = getInterfaceSpeed(tonumber(tags.ifid))

      if ifspeed ~= nil then
        -- bit/s
        return ifspeed * 1000 * 1000
      end
    elseif string.contains(metric, "packets") then
      local ifspeed = getInterfaceSpeed(tonumber(tags.ifid))

      if ifspeed ~= nil then
        -- mbit/s
        local speed_mbps = ifspeed
        local max_pps_baseline = 14881 -- for 10 mbps

        return speed_mbps / 10 * max_pps_baseline
      end
    end
  end

  return math.huge
end

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

function ts_common.normalizeVal(v, max_val, options)
  -- it's undesirable to normalize and set a 0 when
  -- the number is greater than max value
  -- this was causing missing points in the charts
  -- with ZMQ interface which can actually exceed the nominal value

  if v ~= v then
    -- NaN value
    v = options.fill_value
  elseif v < options.min_value then
    v = options.min_value
  end

  return v
end

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

function ts_common.calculateSampledTimeStep(raw_step, tstart, tend, options)
  local estimed_num_points = math.ceil((tend - tstart) / raw_step)
  local time_step = raw_step

  if estimed_num_points > options.max_num_points then
    -- downsample
    local num_samples = math.ceil(estimed_num_points / options.max_num_points)
    time_step = num_samples * raw_step
  end

  return time_step
end

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

function ts_common.fillSeries(schema, tstart, tend, time_step, fill_value)
  local count = math.ceil((tend-tstart) / time_step)
  local res = {}

  for idx, metric in ipairs(schema._metrics) do
    local data = {}

    for i=1, count do
      data[i] = fill_value
      i = i + 1
    end

    res[idx] = {label=metric, data=data}
  end

  return res, count
end

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

function ts_common.serieWithTimestamp(serie, tstart, tstep)
  local data = {}
  local t = tstart

  for i, pt in ipairs(serie) do
    data[t] = pt
    t = t + tstep
  end

  return data
end

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

local last_error = nil
local last_error_msg = nil
ts_common.ERR_OPERATION_TOO_SLOW = "OPERATION_TOO_SLOW"

function ts_common.clearLastError()
  err = nil
end

function ts_common.setLastError(err, msg)
  last_error = err
  last_error_msg = msg
end

function ts_common.getLastError()
  return last_error
end

function ts_common.getLastErrorMessage()
  return last_error_msg
end

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

return ts_common
