--    ======================
--    Description
--    ======================
--
--    Logika ustala priorytet urządzeń w/g czasu pracy tak aby eksploatacja urządzeń była równomierna.
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic supports following variables:
--
--    input.devieces.on                       - ile urządzeń ma być uruchomionych zaczynając od urządzeń z najwyższym priorytetem
--
--    input.dev.worktimes                      - lista dla urządzeń - czas pracy urządzenia
--
--    output.dev.states                        - lista dla urządzeń - stan wyjścia dla urządzenia (0 - wyłączone; 1 - włączone)
--
--    counter.dev.priority.values              - lista dla urządzeń - wartość priorytetu załączenia (im niższa wartość tym wyższy priorytet)
--
--    counter.dev.priority.switch.downcounter  - lista dla urządzeń - licznik wymuszonego czasu pracy (w sekundach) dla urządzenia po zmianie jego priorytetu
--
--    setting.dev.priority.switch.time         - lista dla urządzeń - wartość wymuszonego czasu pracy dla urządzenia po zmianie jego priorytetu. Jeżeli urządzenie pracuje i nastąpi
--                                               zmiana jego priorytetu to przez ten czas urządzenie nie zostanie wyłączone. Ma to na celu zapewnienie ciągłości pracy układu
--                                               zanim inne uruchomione urządzenie nie osiągnie swojej pełnej mocy pracy.
--
--    setting.dev.enabled                      - lista dla urządzeń - czy dane urządzenie jest aktywne (może zostać załączone) (0 - nieaktywne; 1 - aktywne)
--
--    setting.dtime.value                      - maksymalna dopuszczalna różnica czasu pracy pomiędzy urządzeniami, musi to być ta sama jednostka co dla input.dev.worktimes
--
--    Logic uses lua language to implement own behaviour
--
--    ======================
--    mandatory variables
--    ======================
--
--    Logic expects following mandatory variables:
--
--    reload.trigger                          - causes reloading lua script
--
--    memcnt                                  - current amount of memory used by lua in bytes
--
--    Logic expects following kv settings:
--
--    LuaScriptPath                           - path to the lua script - must be absolute
--
--    ======================
--    ChangeLog
--    ======================
--
--    2018-10-17 ver 0.0.0
--
--    + First release
--
-- user can use some functions provided by ibmanager.

-- ibmanager provides following functions to use:
--
-- function returns value of required ibmanager variable
-- @param fullName - string - variable name - name of variable of which value must be returned, for example "rs.0.id.255.input.t.0.value"
-- @return         - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getValue(fullName)


-- function set value of given ibmanager variable
-- @param fullName - string - variable name - name of variable of which value we want to set, for example "rs.0.id.255.input.t.0.value"
-- @param value    - string, int or boolean - value to set
-- @return         - nothing
--
-- setValue(fullName, value)


-- function returns value of required ibmanager variable
-- @param key      - placed in xml logic configuration file:
--                   * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
--                   * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
--                     in the case of variables that are placed in lists
-- @return         - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getLogicValue(key)


-- function set value of given ibmanager variable
-- @param key      - placed in xml logic configuration file:
--                   * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
--                   * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
--                     in the case of variables that are placed in lists
-- @param value    - string, int or boolean - value to set
-- @return         - nothing
--
-- setLogicValue(key, value)

-- function returns two sections of kvsettings from xml configuration file
-- returned value is two element table, each of these elements is table too.
-- indices of returned table are strings and equal "instance" and "global"
-- values of returned tables are tables and contain KVsettings for applicable section.
-- nested tables have form key = value, where key is index in nested table and value is value.
-- example: {"instance" = {"ikey1" = "ivalue1", "ikey2" = "gvalue2"}, "global" = {"gkey1" = "gvalue1", "gkey2" = "gvalue2", "gkey3" = "gvalue3"}}
-- @return          - two dimensional array - kvsettings for global and instance sections
-- getKvSettings()


-- function schedules alert to send.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- scheduleAlert(id)

-- function cancels alert sending, if was previously scheduled. if not then only wakes up alerts handling thread, so if there is no need to call this function, then do not call it.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- cancelAlert(name)

-- function returns table, containing Variables that belongs to required list.
-- @param listName - Name attribute of VarList, RemoteVarList or ImportVarList elemetnst in configurationfile
-- @return array of key-value pairs. Key - variable postfix, Value - Variable value
-- getVarList(listName)

-- function returns monotonic system clock value, that elapsed since specific epoch
-- returned time is expressed in milliseconds.
-- getClock()

-- function logs message to file, if defined in configuration file log level is less than passed to this function
-- @param logLevel - one of:
--        LogLevel.TraceLo
--        LogLevel.Trace
--        LogLevel.TraceHi
--        LogLevel.DebugLo
--        LogLevel.Debug
--        LogLevel.DebugHi
--        LogLevel.Info
--        LogLevel.Notice
--        LogLevel.Warning
--        LogLevel.Error
--        LogLevel.Critical
-- @param logMessage - string to log
-- log(logLevel, logMessage)


-- ibmanager provides following global variables:

-- logic type, (in this case it will always be "Lua") - the same as in logic configuration file in section: <Logic Type="Lua" ...
-- LOGIC_TYPE

-- logic version, the same as in logic configuration file in section: <Logic ... Version="x.y.z" ...
-- LOGIC_VERSION

-- logic sub-type, the same as in logic configuration file in section: <Logic ... SubType="Hysteresis" ...
-- LOGIC_SUBTYPE

-- logic sub-version, the same as in logic configuration file in section: <Logic ... SubVersion="x.y.z" ...
-- LOGIC_SUBVERSION

-- logic instance name - the same as in logic configuration file, in section: <Instance Name="0">
-- LOGIC_INSTANCE_NAME

-- add script directory to package path
package.path = package.path .. ";./logic/scripts/utils/?.lua";

-- use script - without .lua extension - delta class
require("State");
require("DownCounter");

g_outputDevStates = {};
g_counterDevPriorityValues = {};
g_counterDevPrioritySwitchDowncounters = {};
g_sortedInputDevWorktimes = {};
g_settingDevEnabled = {};

g_setPriorities = 1;
g_prioritiesChanged = 0;
g_inputDeviecesOn_old = 0;

SUPPORTED_SUBLOGIC_TYPE = "SourcePriority";
SUPPORTED_SUBLOGIC_VERSION = "0.0.0";
g_versionChecked = false;

function getKeysSortedByValue(tbl, sortFunction) 
  local keys = {}
  for key in pairs(tbl) do
    table.insert(keys, key)
  end
  
  table.sort(keys, function(a, b)
    return sortFunction(tbl[a], tbl[b])
  end)

  return keys
end

-- entry point to the logic
-- @param firstCall - tells if logic is called first time
-- @return        - nothing
function onLogicCall(firstCall)

  if not g_versionChecked then
    -- checking sublogic type and sublogic version
    if LOGIC_SUBTYPE ~= SUPPORTED_SUBLOGIC_TYPE then
      error("Wrong logic sub-type. expected " .. SUPPORTED_SUBLOGIC_TYPE .. " but used " .. LOGIC_SUBTYPE);
    end
    local versionWithoutBuild = string.match(LOGIC_SUBVERSION, "[0-9]+%.[0-9]+%.[0-9]+");
    if versionWithoutBuild ~= SUPPORTED_SUBLOGIC_VERSION then
      error("Wrong logic sub-version. expected " .. SUPPORTED_SUBLOGIC_VERSION .. " but used " .. LOGIC_SUBVERSION);
    end
    g_versionChecked = true;
  end

  local inputDeviecesOn = getLogicValue("input.devieces.on");
  local settingDtimeValue = getLogicValue("setting.dtime.value");
  
  local inputDevWorktimes = getVarList("input.dev.worktimes");
  local settingDevPrioritySwitchTimeL = getVarList("setting.dev.priority.switch.time");
  local settingDevEnabledL = getVarList("setting.dev.enabled");
   
  local devCount = 0;
  local tableDevPostfix = {};
  
  local inputDevWorktime = {};
  local settingDevPrioritySwitchTime = {};
  local settingDevEnabled = {};
  local counterDevPrioritySwitchDowncounter = {};

  local maxDevWorktime = 0;
  local minDevWorktime = 0;
  local i = 0;
  
  if (firstCall) then
    g_inputDeviecesOn_old = inputDeviecesOn;
  end
  
  -- tablica wartości czasów pracy urządzeń
  if inputDevWorktimes ~= nil then
    for key, value in pairs(inputDevWorktimes) do
      inputDevWorktime['' .. key] = value;     
 
      tableDevPostfix[devCount] = key
      devCount = devCount + 1;
    end
  end

  -- tablica aktywnych urządzeń
  if settingDevEnabledL ~= nil then
    for postfix, value in pairs(settingDevEnabledL) do
      settingDevEnabled['' .. postfix] = value;     
    end
  end

  -- tablica wartości wymuszonego czasu pracy dla urządzenia po zmianie jego priorytetu
  if settingDevPrioritySwitchTimeL ~= nil then
    for postfix, value in pairs(settingDevPrioritySwitchTimeL) do
      settingDevPrioritySwitchTime['' .. postfix] = value;     
    end
  end

  if devCount == 0 then
    error("VarList input.dev.worktimes not initialized in instance section of configuration.");
  end
   
  -- states 
  if g_outputDevStates == nil or firstCall then
    for i = 0, devCount-1, 1 do 
      g_outputDevStates['' .. tableDevPostfix[i]] = State.create(0, 0, 1, true);
    end
  end
  
  for i = 0, devCount-1, 1 do 
    g_outputDevStates['' .. tableDevPostfix[i]]:call();
  end
  
  if g_counterDevPriorityValues == nil or firstCall then
    for i = 0,devCount-1,1 do 
      g_counterDevPriorityValues['' .. tableDevPostfix[i]] = State.create(0, 0, devCount-1, true);
    end
  end
  
  for i = 0, devCount-1, 1 do 
    g_counterDevPriorityValues['' .. tableDevPostfix[i]]:call();
  end
    
  if g_settingDevEnabled == nil or firstCall then
    for i = 0,devCount-1,1 do 
      g_settingDevEnabled['' .. tableDevPostfix[i]] = State.create(0, 0, devCount-1, true);
    end
  end
  
  for i = 0, devCount-1, 1 do 
    g_settingDevEnabled['' .. tableDevPostfix[i]]:call();
    g_settingDevEnabled['' .. tableDevPostfix[i]]:setValue(settingDevEnabled['' .. tableDevPostfix[i]]);
  
    -- jeśli został zmieniony stan aktywności urządzenia to należy ponownie przeliczyć priorytety
    if g_settingDevEnabled['' .. tableDevPostfix[i]]:isChanged() then
      g_setPriorities = 1;   
    end
  end

  -- timers

  if g_counterDevPrioritySwitchDowncounters == nil or firstCall then
    for i = 0, devCount-1, 1 do 
      g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]] = DownCounter.create();
      g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]]:updateParams(0);
    end
  end

  for i = 0, devCount-1, 1 do 
--    g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]]:updateParams(settingDevPrioritySwitchTime['' .. tableDevPostfix[i]] * 1000);
    
--    if g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]]:elapsed() then
--      g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]]:reset();
--    end

    counterDevPrioritySwitchDowncounter['' .. tableDevPostfix[i]] = g_counterDevPrioritySwitchDowncounters['' .. tableDevPostfix[i]]:timeTo0() / 1000;
    counterDevPrioritySwitchDowncounter['' .. tableDevPostfix[i]] = (counterDevPrioritySwitchDowncounter['' .. tableDevPostfix[i]] < 0) and 0 or counterDevPrioritySwitchDowncounter['' .. tableDevPostfix[i]];

  end

  
  -- logika właściwa
  
  -- jeżeli urządzenie nieaktywne to je wyłącz, wykasuj czas pracy z tablicy priorytetów, ustaw najwyższy priorytet
  for i = 0, devCount - 1, 1 do
    if g_settingDevEnabled['' .. tableDevPostfix[i]] == nil then
      error("VarListItem setting.dev.enabled witch postfix " .. tableDevPostfix[i] .. " not initialized in instance section of configuration.");
    else
      if g_settingDevEnabled['' .. tableDevPostfix[i]]:getValue() == 0 then
        g_outputDevStates['' .. tableDevPostfix[i]]:setValue(0);
        inputDevWorktimes['' .. tableDevPostfix[i]] = nil;
        g_counterDevPriorityValues['' .. tableDevPostfix[i]]:setValue(devCount);
      end
    end
  end
  
  -- poustawiaj priorytety
  if ((firstCall) or (g_setPriorities == 1)) then
    -- posortowana tablica z postfixami tabeli z czasy pracy urządzeń (najmniejsza wartość na początku)
    g_sortedInputDevWorktimes = getKeysSortedByValue(inputDevWorktimes, function(a, b) return a < b end)
    
    -- ustaw priorytety w/g czasu pracy
    i = 0;
    for _, key in ipairs(g_sortedInputDevWorktimes) do
      g_counterDevPriorityValues['' .. key]:setValue(i);
      
      i = i + 1;
    end
    
    g_setPriorities = 0;
    g_prioritiesChanged = 1;
  end;

  -- jeżeli różnica pomiędzy czasem pracy urządzenia z najniższym priorytetem i najwyższym priorytetem przekracza dopuszczalną wartośc to przelicz ponownie priorytet
  if inputDevWorktimes ~= nil then
    maxDevWorktime = inputDevWorktimes[tableDevPostfix[0]]   
    minDevWorktime = inputDevWorktimes[tableDevPostfix[0]]   
  
    for k, v in pairs(inputDevWorktimes) do
      if inputDevWorktimes[k] > maxDevWorktime then
          maxDevWorktime = v;
      end
      
      if inputDevWorktimes[k] < minDevWorktime then
          minDevWorktime = v;
      end
    end 
  end
  
  if ((maxDevWorktime - minDevWorktime) > settingDtimeValue) then
    g_setPriorities = 1;
  end
  
  -- włącz odpowiednie urządzenia
  i = 1;
  for _, key in ipairs(g_sortedInputDevWorktimes) do

    if (counterDevPrioritySwitchDowncounter['' .. key] >= 1) then
      g_outputDevStates['' .. key]:setValue(1);
    end
  
    if inputDeviecesOn >= i then
      g_outputDevStates['' .. key]:setValue(1);
    else

    -- jeżeli zmienił się priorytet i nie nastąpiła zmiana ilości żądanych urządzeń do uruchomienia i nie została uruchomiona sekwencja
    -- wyłączenia urządzenia po czasie i to urządzenie aktualnie pracuje to rozpocznij sekwencje wyłączania urządzenia po czasie
    if  (g_prioritiesChanged == 1) and 
        (inputDeviecesOn == g_inputDeviecesOn_old) and 
        (counterDevPrioritySwitchDowncounter['' .. key] == 0) and 
        (g_outputDevStates['' .. key]:getValue() == 1) and 
        (firstCall == false) then
  
      g_counterDevPrioritySwitchDowncounters['' .. key]:updateParams(settingDevPrioritySwitchTime['' .. key] * 1000);
      g_counterDevPrioritySwitchDowncounters['' .. key]:reset();
  
    else
      g_outputDevStates['' .. key]:setValue(0);
    end

    end
    i = i + 1;
  end
  
--  print("------");
--  for _, key in ipairs(g_sortedInputDevWorktimes) do
--    print(key, inputDevWorktimes['' .. key])
--  end
--  print("=====");
--  print(minDevWorktime, maxDevWorktime)
--  print(g_inputDeviecesOn_old, inputDeviecesOn)

  -- send logic variables to ibmanager
  for i = 0,devCount-1,1 do 
    setLogicValue("output.dev.states." .. tableDevPostfix[i], g_outputDevStates['' .. tableDevPostfix[i]]:getValue());
    setLogicValue("counter.dev.priority.values." .. tableDevPostfix[i], g_counterDevPriorityValues['' .. tableDevPostfix[i]]:getValue());
    setLogicValue("counter.dev.priority.switch.downcounter." .. tableDevPostfix[i], counterDevPrioritySwitchDowncounter['' .. tableDevPostfix[i]]);
  end
  
  g_prioritiesChanged = 0;
  g_inputDeviecesOn_old = inputDeviecesOn;

end
