--    ======================
--    Description
--    ======================
--
--    Logic manages distributed variable and decrement it until reaches 0. If changes any variable in settings.value list or in output.value variable,
--    then new value is copied to the all variables in settings.value list and to the output.value.
--
--    ======================
--    Parameters
--    ======================
--
--    Logic expects following variables:
--
--    setting.mode          - work mode. 0 - auto, 1 - manual. When selected manual, then always setting.manual.value is copied
--                            to the output.value. When selected auto, then logic traces changes in input.counters and output.value.
--                            If detected changes, then all variables in settings.value and output.value are set to new value. Logic also
--                            periodically decrements output.value  (each setting.cnt.period) in auto mode by setting.cnt.value until it
--                            reaches 0 and propagates new value.
--
--    setting.manual.value  - value, that is copied to the output.value when setting.mode is set to manual (1).
--
--    output.value          - main logic output value and bidirectional variable with lowest priority (lowest than variables in
--                            settings.value list). When logic is in auto mode (setting.mode = 0) and this value changes, then all variables
--                            in settings.value list are set to new value. This value is periodically decremented until it reaches 0.
--                            When this value is In manual mode, this variable is always overriden by value from setting.manual.value.
--                            This value can mapped by GUI application.
--
--    settings.value        - outputs - these parameters belongs to remote devices and are observed and driven (for example
--                            setting.vent.downtime registers of h4f6 device). when any variable in counters.value list changes, then new, related value is
--                            copied to the all variables in settings.value and to the output.value. When detected changes at more than
--                            one variable in counters.values list, then priority is determined by postfix ASCII order, but finally all variables will be
--                            processed
--
--    counters.value        - inputs - observed device values (for example counter.vent.downtime registers).
--
--    counters.lock         - list of variables in remote devices that are related by postfix with variables stored in settings.value and settings.lock lists.
--                            variables in this list tell that related variables in settings.value list are locked, what means that there is impossible
--                            to change its value. So if we want to change variable in settings.value list, then we have to write 0 to related by postfix variable in
--                            settings.lock list.
--
--    settings.lock         - list of variables in remote devices that are related by postfix with variables stored in settings.value and counters.lock lists. Writing
--                            0 to this variable causes reset counters.lock variable what in result allows write new value to the related variable in settings.value list.
--
--
--    setting.cnt.period    - period expressed in seconds, tells, how often decrement output.value by setting.cnt.value until it reaches 0.
--
--    setting.cnt.value     - value that is periodically substracted from output.value until it reaches 0.
--
--    counter.downtime      - time to decrease output.value if it is different than 0
--
--    ======================
--    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
--    ======================
--
--    2017-07-04 ver 0.0.1
--
--    # bugfix
--
--    2017-07-03 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 .. ";/ibsystem/ibmanager/logic/lua/utils/?.lua";
package.path = package.path .. ";/work/insbud/iblogics/Lua/Scripts/utils/?.lua";
package.path = package.path .. ";./logic/scripts/utils/?.lua";

-- use script - without .lua extension - Hysteresis and DownCounter classes
require("DownCounter");

-- ---------------------------------------------------------------------------------------------------------
-- global - enums
-- ---------------------------------------------------------------------------------------------------------
AUTO_MODE   = 0;
MANUAL_MODE = 1;

-- ---------------------------------------------------------------------------------------------------------
-- 
-- ---------------------------------------------------------------------------------------------------------


-- logic class
Logic = {};

Logic.__index = Logic;

-- ---------------------------------------------------------------------------------------------------------
-- 
-- ---------------------------------------------------------------------------------------------------------

function Logic.create()

  --our new object
  local logic         = {};  
  setmetatable(logic, Logic);
  
  --our object fields initialization
  logic.lastOutput              = getLogicValue("output.value");
  logic.downCounter             = DownCounter.create();
  logic.items                   = {}

  -- adding items and filling settings and its value
  local settingListName = "settings.value";
  
  local lst = getVarList(settingListName);
  
  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      -- building logic counter variable name
      local sVar = settingListName .. "." .. postfix;
      local item = {settingVar = sVar, unlockingInProgress = false, lastSetValue = nil, lastChangedValue = nil};
      -- addint item
      logic.items[postfix] = item;
      --print("postfix: " .. postfix .. ", svar: " .. sVar .. ");
    end
  end
  
  local settingLockListName = "settings.lock";
  lst = getVarList(settingLockListName);
  
  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      -- building logic setting variable name
      local item = logic.items[postfix];
      if item ~= nil then
        item.settingLock = settingLockListName .. "." .. postfix;
      end
    end
  end
  
  local countersListName = "counters.value";
  lst = getVarList(countersListName);
  
  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      -- building logic setting variable name
      local item = logic.items[postfix];
      if item ~= nil then
        item.counterVar = countersListName .. "." .. postfix;
      end
    end
  end
  
  
  local counterLockListName = "counters.lock";
  lst = getVarList(counterLockListName);
  
  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      -- building logic setting variable name
      local item = logic.items[postfix];
      if item ~= nil then
        item.counterLock = counterLockListName .. "." .. postfix;
      end
    end
  end
  
    --checking if correctly defined variables in list
  for key, item in pairs(logic.items) do
  
    if (item.counterLock == nil) then
      error ("Configuration error, undefined " .. key .. " postfix in list " .. counterLockListName);
    end
    
    if (item.settingLock == nil) then
      error ("Configuration error, undefined " .. key .. " postfix in list " .. settingLockListName);
    end
  
    if (item.settingVar == nil) then
      error ("Configuration error, undefined " .. key .. " postfix in list " .. settingListName);
    end

    if (item.counterVar == nil) then
      error ("Configuration error, undefined " .. key .. " postfix in list " .. countersListName);
    end

    -- print ("postfix: " .. key .. ", settingVar: " .. item.settingVar .. ", counterLock: " .. item.counterLock .. ", settingLock: " .. item.settingLock);
    
  end
  
  return logic;
  
end

-- ---------------------------------------------------------------------------------------------------------
-- function returns first found item, that has set counterLock variable. If not foundm, then returns nil
-- ---------------------------------------------------------------------------------------------------------

function Logic:findChangedItem()
  
  for key, item in pairs(self.items) do
  
    local isChanged = getLogicValue(item.counterLock);
    if (isChanged ~= 0 or item.unlockingInProgress == true) then
      return item;
    end
    
  end

  return nil;

end

-- ---------------------------------------------------------------------------------------------------------
-- 
-- ---------------------------------------------------------------------------------------------------------

function Logic:call()

  local mode          = getLogicValue("setting.mode");
  local manualValue   = getLogicValue("setting.manual.value");
  local output        = getLogicValue("output.value");
  local decPeriod     = getLogicValue("setting.cnt.period") * 1000;
  local decValue      = getLogicValue("setting.cnt.value");
  local resetCounter  = false;
  local newValue      = mode == MANUAL_MODE and manualValue or nil;
  
  
  self.downCounter:updateParams(decPeriod);


  if (newValue == nil) then
  
    local changedItem = self:findChangedItem();
  
    if (changedItem ~= nil) then

      local value = getLogicValue(changedItem.counterVar);
      
      if (getLogicValue(changedItem.counterLock) ~= 0) then
      
        if (changedItem.unlockingInProgress == false) then
          -- first time detected, remember read value
          changedItem.lastChangedValue = value;
          changedItem.lastSetValue = value;
          changedItem.unlockingInProgress = true;
          --print("start unlocking");
        end
        -- reset lock, unlocking is in progress
        setLogicValue(changedItem.settingLock, 0);
        
      else
        --just unlocked
        changedItem.unlockingInProgress = false;
        --print("stop unlocking");
      end
      
      if (value ~= changedItem.lastChangedValue) then
        --detected next change
        newValue = value;
        changedItem.lastChangedValue = value;
      else
        --set new value to last set (downcounter could decrement it previously)
        newValue = changedItem.lastSetValue;
      end

    end

  end

  
  if newValue == nil then
    newValue = output;
  end  

  
  if self.downCounter:elapsed() and newValue ~= 0 and mode ~= MANUAL_MODE then
    newValue = newValue - decValue;
    newValue = newValue < 0 and 0 or newValue;
  end
  
  if (self.lastOutput ~= newValue) then
    self.downCounter:reset();
  end
  
  --print ("output =  " .. output .. ", newValue = " .. newValue);
  
  
  for key, item in pairs(self.items) do
  
    local isChanged = getLogicValue(item.counterLock);
    
    if (isChanged ~= 0 and mode == MANUAL_MODE) then
      setLogicValue(item.settingLock, 0);
    end
    
    setLogicValue(item.settingVar, newValue);
    item.lastSetValue = newValue;

  end
  
  self.lastOutput = newValue;
  setLogicValue("output.value", newValue);
  
  --update counter
  local time2Dec = self.downCounter:timeTo0() / 1000;
  time2Dec = time2Dec < 0 and 0 or time2Dec;
  --hide down counter if value equals 0
  time2Dec = newValue == 0 and 0 or time2Dec;

  
  setLogicValue("counter.downtime", time2Dec);
  
  
end

-- ---------------------------------------------------------------------------------------------------------

-- main logic state object
g_logic = nil;

SUPPORTED_SUBLOGIC_TYPE = "DistDownCnt";
SUPPORTED_SUBLOGIC_VERSION = "0.0.1";
g_versionChecked = false;
-- ---------------------------------------------------------------------------------------------------------

-- 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
  
  if g_logic == nil then
  
    g_logic = Logic.create();

  end
  
  
  g_logic:call()
  


end

-- ---------------------------------------------------------------------------------------------------------
