--    ======================
--    Description
--    ======================
--
--    Logic observes input.value list and when any item there changes or if in related list input.trigger any item is different than 0, then triggers all
--    variables in outputs list according to below rules:
--
--    lists input.value, input.active, setting.triggering.value and input.trigger contain items that are related by postfix (belongs to the same group - inputs).
--    "Changed input" is input, of which value in input.value list was changed or when value in input.trigger is different than 0 (logic at the end always resets
--    triggers). elements in input.active list tells if given input is active.
--
--    lists counters.cmp and outputs are related by postfix (belongs to the same group - outputs)
--
--    ======================
--    Parameters
--    ======================
--
--    setting.mode               - variable determines work mode
--                                 0 - AUTO_CP_INPUT - output variable in outputs list is triggered by changed input's value in input.value list.
--                                 1 - AUTO_CP_VALUE - output variable in outputs list is triggered by changed input's value in setting.triggering.value list
--                                 2 - MANUAL        - to the output variable in outputs list is all time writes value of related item from setting.manual.value
--                                 3 - ONLY_COPY     - to the output variable in outputs list is all time writes last changed input's value from input.value list
--
--    setting.cmp.mode           - variable determines feedback mechanism - comparing mode
--                                 0 - NO_CMP - logic sets one time output variable in outputs list - not recommended, but sometimes it is only way.
--                                 1 - CMP    - logic sets output value until cmp variable in counters.cmp list equals setting.cmp.value
--                                 2 - CMP_IN - logic sets output value utnil cmp variable in counters.cmp list equals last changed item in input.value list
--
--    input.active               - list belongs to grop "inputs" and tells if given input is active (1) or not (0)
--
--    input.value                - list belongs to group "inputs" and contains observed values. Logic watches elements in this list when one of them changes, then
--                                 logic triggers all variables in ouputs list.
--
--    input.trigger              - list belongs to group "inputs" and contains variables, that when one of them is different than 0, then output variables are
--                                 triggered according to currently rules. Finally elements in this list are cleared.
--
--    setting.triggering.value   - list belongs to group "inputs" and contains variables related with given input (by postfix), of whose values trigger outputs,
--                                 if given input changes, when logic works in AUTO_CP_VALUE mode.
--
--    setting.manual.value       - this variable is copied to the all variables in outputs list if logic works in MANUAL mode
--                                 (read setting.mode)
--
--    counter.selected.input     - postfix of last selected input.
--
--    setting.cmp.value          - when logic triggers given output variable in outputs list, usually make sures that variable
--                                 is triggered successfully (working in CMP comparing mode). Then observes variables in counters.cmp list
--                                 and triggers output variable until it reaches setting.cmp.value.
--
--    outputs                    - output variables list, belongs to "outputs" group. Variables in this list are triggered according to above rules
--
--    counters.cmp               - helper variables related with outputs list by postfix (belongs to "outputs" group). These variables are used to notify logic
--                                 that given variable in output list are really triggered.
--
--    ======================
--    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-12-21 ver 0.0.1
--
--    # input/output as a list
--
--    2017-07-10 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";
package.path = package.path .. ";/ibsystem/ibmanager/logic/lua/utils/?.lua";
package.path = package.path .. ";/work/insbud/iblogics/Lua/Scripts/utils/?.lua";

-- ---------------------------------------------------------------------------------------------------------
-- global - enums
-- ---------------------------------------------------------------------------------------------------------

-- work mode
WorkMode = {AUTO_CP_INPUT = 0, AUTO_CP_VALUE = 1, MANUAL = 2, ONLY_COPY = 3};

-- compare mode
CompareMode =  {NO_CMP = 0, CMP = 1, CMP_IN = 2};

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


-- logic class
Logic = {};

Logic.__index = Logic;

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

function Logic.create()

  --our new object
  local logic         = {};  
  setmetatable(logic, Logic);

  --our object fields initialization
  logic.outItems          = {};
  logic.inItems           = {};
  logic.lastChangedInput = nil;


  -- adding outItems and filling counter and its value
  local outputsListName = "outputs";
  local lst = getVarList(outputsListName);

  if lst ~= nil then
    for postfix, val in pairs(lst) do
      local varName = outputsListName .. "." .. postfix;
      local item = 
      {
        outputVarName = varName, 
        cmpVarName = nil, 
        update = true
      };
      logic.outItems[postfix] = item;
    end
  end

  local cmpListName = "counters.cmp";
  lst = getVarList(cmpListName);

  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      local item = logic.outItems[postfix];
      if item ~= nil then
        item.cmpVarName = cmpListName .. "." .. postfix;
      else
        error ("Configuration error, defined " .. postfix .. " postfix in list " .. cmpListName .. " but not defined them in list " .. outputsListName);
      end
    end
  end


  local lastChangedInput = nil;
  
  -- adding inItems and input.value list
  local inValuesListName = "input.value";
  lst = getVarList(inValuesListName);

  --print("init order:");
  if lst ~= nil then
    for postfix, val in pairs(lst) do
      --print(postfix);
      local varName = inValuesListName .. "." .. postfix;
      local item = 
      {
        inValueVarName = varName, 
        inActiveVarName = nil, 
        inTriggerVarName = nil, 
        trigValueVarName = nil,
        lastValue = getLogicValue(varName),
        id = postfix
      };
      logic.inItems[postfix] = item;
      
      --unfortunately order is random
      if (lastChangedInput == nil) then
        lastChangedInput = logic.inItems[postfix];
      end
      
    end
  end

  -- adding input.active list elements
  local inActiveListName = "input.active";
  lst = getVarList(inActiveListName);

  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      local item = logic.inItems[postfix];
      if item ~= nil then
        item.inActiveVarName = inActiveListName .. "." .. postfix;
      else
        error ("Configuration error, defined " .. postfix .. " postfix in list " .. inActiveListName .. " but not defined them in list " .. inValuesListName);
      end
    end
  end

  -- adding input.trigger list elements
  local inTriggerListName = "input.trigger";
  lst = getVarList(inTriggerListName);

  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      local item = logic.inItems[postfix];
      if item ~= nil then
        item.inTriggerVarName = inTriggerListName .. "." .. postfix;
        --print(item.inTriggerVarName);
      else
        error ("Configuration error, defined " .. postfix .. " postfix in list " .. inTriggerListName .. " but not defined them in list " .. inValuesListName);
      end
    end
  end

  -- adding setting.triggering.value list elements
  local trigValueListName = "setting.triggering.value";
  lst = getVarList(trigValueListName);

  if lst ~= nil then
    for postfix, _ in pairs(lst) do
      local item = logic.inItems[postfix];
      if item ~= nil then
        item.trigValueVarName = trigValueListName .. "." .. postfix;
      else
        error ("Configuration error, defined " .. postfix .. " postfix in list " .. trigValueListName .. " but not defined them in list " .. inValuesListName);
      end
    end
  end


  --checking correctness
  for postfix, item in pairs(logic.inItems) do
    if (item.trigValueVarName == nil) then
      error ("Configuration error, not defined  " .. postfix .. " postfix in list " .. trigValueListName);
    end
  end

  logic.lastChangedInput = lastChangedInput;
  
  return logic;

end

-- ---------------------------------------------------------------------------------------------------------
-- function determines last changed input and returns boolead value that tells if outputs should be refreshed
-- last changed input is stored in self.lastChangedInput
-- ---------------------------------------------------------------------------------------------------------

function Logic:handleInputs()


  --determining last changed input. first on list has higher priority
  
  local changedInput = nil;
  
  for _, item in pairs(self.inItems) do
  
    local value = getLogicValue(item.inValueVarName);
    local active = getLogicValue(item.inActiveVarName);
    -- print("item: " .. item.id .. " active: " .. active .. " value: " .. value);
    
    --input.active list element is optional, so if is nil, then below condition is true
    if (active ~= 0) then
    
      if (changedInput == nil) then
      
        if (value ~= item.lastValue) then
          -- input changed, remember
          -- print ("changed item: " .. item.id .. " from " .. item.lastValue .. " to " .. value);
          changedInput = item;
        elseif (item.inTriggerVarName ~= nil) then
          if (getLogicValue(item.inTriggerVarName) ~= 0) then
            -- someone marked current input as changed, remember it
            -- print ("forced outputs to update by item: " .. item.id .. " with value" .. value);
            changedInput = item;
          end
        end
    
      end

    end
    
    -- remember current value
    item.lastValue = value;
    
    -- clearing trigger if trigger variable defined for given input
    if (item.inTriggerVarName ~= nil) then
      setLogicValue(item.inTriggerVarName, 0);
    end
    
  end
  
  if (changedInput ~= nil) then
    self.lastChangedInput = changedInput;
    return true;
  else
    return false;
  end

end

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


function Logic:call()

  local workMode              = getLogicValue("setting.mode");
  local inputChanged          = self:handleInputs();
  
  if (self.lastChangedInput == nil) then
    -- no input items
    return;
  end
  
  
  if (workMode == WorkMode.ONLY_COPY) then

    for _, item in pairs(self.outItems) do
  
      setLogicValue(item.outputVarName, self.lastChangedInput.lastValue);
    
    end

  elseif (workMode == WorkMode.MANUAL) then
  
    local manualValue           = getLogicValue("setting.manual.value");
    
    for _, item in pairs(self.outItems) do
  
      setLogicValue(item.outputVarName, manualValue);
    
    end

  else
  
    --auto mode
    local settingCmpMode        = getLogicValue("setting.cmp.mode");
    local valueToSet            = workMode == WorkMode.AUTO_CP_INPUT and self.lastChangedInput.lastValue or getLogicValue(self.lastChangedInput.trigValueVarName);
    local cmpValue              = settingCmpMode == CompareMode.CMP_IN and self.lastChangedInput.lastValue or getLogicValue("setting.cmp.value");
    
    for key, item in pairs(self.outItems) do
    
      if (inputChanged) then
  
        --set always when input changed
        setLogicValue(item.outputVarName, valueToSet);
        --print("set value");
        item.update = true;
      
      elseif (settingCmpMode ~= CompareMode.NO_CMP) then
      
        if (item.cmpVarName == nil) then
        
          error("Selected comparing mode but not set variable to compare in counters.cmp list for postfix " .. key);
        
        elseif (item.update) then
        
          local compareCntr = getLogicValue(item.cmpVarName);
          
          if (compareCntr ~= cmpValue) then
          
            setLogicValue(item.outputVarName, valueToSet);
            --print("set value again");
          
          else
          
            item.update = false;
          
          end
        
        end
      
      end
    
    end
  
  end

  setLogicValue("counter.selected.input", self.lastChangedInput.id);
  
end

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

-- main logic state object
g_logic = nil;

SUPPORTED_SUBLOGIC_TYPE = "Trigger";
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

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