--    ======================
--    Description
--    ======================
--
--    Logic manages distributed variable. If changes any variable in input.counters list or in output.value variable,
--    then new value is copied to the all variables in output.settings list and to the output.value.
--
--    In KVSettings can be written transition table in the integer values form, for example: <KVSetting Key="5" Value="6"/>. In keys are stored
--    values of items from input and output lists. In values are stored values of output.value variable. When logic reads any item from input list,
--    then it compares given item value with each key in table. if matches, then it takes related value in table and then works with it. The same, if
--    logic wants to write any variable to the output list, then it compares given value with each value in transition table. if matches, then it takes
--    related key and write it to the output item from list
--
--    ======================
--    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 output.settings and output.value are set to new value.
--
--    setting.manual.value  - value, that is copied to the output.value when setting.mode is set to manual (1). This variable keeps always
--                            values from output.value domain (read KVSettings).
--
--    output.value          - main logic output value and bidirectional variable with highest priority (highest than variables in
--                            input.counters list). When logic is in auto mode (setting.mode = 0) and this value changes, then all variables
--                            in output.settings list are set to new value. In manual mode, this variable is always overriden by
--                            value from setting.manual.value. This value can mapped by GUI application.
--
--    output.settings       - control variables - Writing any value to this list causes setting new value in related external device
--                            (H4F2 - setting.t.setpoint.value). In manual mode, to these variables are copied transformed by kvsetting value
--                            from setting.manual.value. In auto mode, functionality was described at input.counters.
--
--    input.counters        - when any variable in this list changes, then new value is copied to the all variables in output.settings and
--                            to the output.value. It import list and values in this list should be mapped directly to the remote variables,
--                            that are related with any devices (H4F2 - counter.t.setpoint). When detected changes at more than one variable
--                            in this list, then priority is determined by postfix ASCII order.
--
--    ======================
--    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-06-09 ver 0.0.4
--
--    # obsługa wielu wartości do jednego key'a
--
--    2017-06-07 ver 0.0.2.825
--
--    # obsługa automatycznego build'a
--    # opis logiki
--
--    2017-06-07 ver 0.0.1
--
--    # zmiana nazw inputów
--
--    2017-03-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 .. ";/ibsystem/ibmanager/logic/lua/utils/?.lua";
package.path = package.path .. ";/work/insbud/iblogics/Lua/Scripts/utils/?.lua";

-- ---------------------------------------------------------------------------------------------------------
-- 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.items          = {};
  --keys - outputs, values - items
  logic.outputsMapping = {};
  --keys - items, values - outputs
  logic.itemsMapping   = {};

  --transition table stored in KVSettings. Key - input, Value = output
  local kvsections     = getKvSettings();
  local i              = 0;
  for section, kvsettings in pairs(kvsections) do
    if section == "instance" then
      for k, v in pairs(kvsettings) do
        if k ~= "LuaScriptPath" then
          local itemValues = logic:parseValues(k);
          local outputValues = logic:parseValues(v);

          if itemValues.size == 0 then
            error("key parsing error in kvSettings");
          end
          if outputValues.size == 0 then
            error("value parsing error in kvSettings");
          end
          if outputValues.size > 1 and itemValues.size > 1 then
            error("mapping many to many in kvSettings: key = " .. k .. ", value = " .. v);
          end

          if outputValues.size == 1 and itemValues.size == 1 then
            --mapping one item value to one output value
            logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
            logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
          elseif outputValues.size > 1 then
            --mapping many output values to single item value
            --we take only first output in items mappings, rest are abandoned
            logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
            --we map all outputs to single item
            for _, ov in pairs(outputValues.arr) do
              logic.outputsMapping[ov] = itemValues.arr[0];
            end
            logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
          elseif itemValues.size > 1 then
            --mapping many item values to one output value
            --we take only first item in outputs mappings, rest are abandoned
            logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
            --we map all items to single output
            for _, iv in pairs(itemValues.arr) do
              logic.itemsMapping[iv] = outputValues.arr[0];
            end
            logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
          end
        end
      end
    end
  end

  --for item, output in pairs(logic.itemsMapping) do
  --  print ("item: " .. item .. " -> output: " .. output);
  --end
  --for output, item in pairs(logic.outputsMapping) do
  --  print ("output: " .. output .. " -> item: " .. item);
  --end

  -- adding items and filling counter and its value
  local counterListName = "input.counters";
  local lst = getVarList(counterListName);

  if lst ~= nil then
    for postfix, val in pairs(lst) do
      -- building logic counter variable name
      local cntVar = counterListName .. "." .. postfix;
      local item = { counterVar = cntVar, value = val, settingVar = nil };
      -- addint item
      logic.items[postfix] = item;
    end
  end

  -- filling settings in items
  local settingListName = "output.settings";
  lst = getVarList(settingListName);

  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.settingVar = settingListName .. "." .. postfix;
      end
    end
  end


  --checking if correctly defined variables in list
  for key, item in pairs(logic.items) do
    if (item.counterVar == nil) then
      error("Configuration error, defined " ..
      key .. " postfix in list " .. settingListName .. " but not defined them in list " .. counterListName);
    end

    if (item.settingVar == nil) then
      error("Configuration error, defined " ..
      key .. " postfix in list " .. counterListName .. " but not defined them in list " .. settingListName);
    end

    -- print ("postfix: " .. key .. ", setting: " .. item.settingVar .. ", counter: " .. item.counterVar .. ", counter val: " .. item.value);
  end


  return logic;
end

-- ---------------------------------------------------------------------------------------------------------
-- function returns tables of numbers. these numbers are placed in string and separated by comma
-- ---------------------------------------------------------------------------------------------------------
function Logic:parseValues(str)
  local array = {};
  local i = 0;
  local pos = 1;
  local len = str:len();

  if (len == 0) then
    error("kvsettings item is empty");
  end

  while pos ~= nil do
    local ret = str:match("%d+", pos);

    if ret ~= nil then
      array[i] = tonumber(ret);
      --print("arr[" .. i .. "] = " .. array[i]);
      i = i + 1;
    end

    pos = str:find(",", pos + 1)
  end

  local pair = { size = i, arr = array };
  return pair;
end

-- ---------------------------------------------------------------------------------------------------------
-- function looks for given output value among values in kVSettings and if find, then returns related key (item)
-- if not find, then returns what it takes - outputValue
-- ---------------------------------------------------------------------------------------------------------

function Logic:getItemValue(outputValue, prefferedItemValue)
  if prefferedItemValue ~= nil then
    for item, output in pairs(self.itemsMapping) do
      if item == prefferedItemValue and outputValue == output then
        return prefferedItemValue;
      end
    end
  end

  local foundItem = nil;
  for output, item in pairs(self.outputsMapping) do
    if outputValue == output then
      foundItem = item;
      break;
    end
  end

  if foundItem == nil then
    return outputValue;
  end

  return foundItem;
end

-- ---------------------------------------------------------------------------------------------------------
-- function looks for given item among keys in kVSettings and if find, then returns related value (output)
-- if not find, then returns what it takes - inputValue
-- ---------------------------------------------------------------------------------------------------------

function Logic:getOutputValue(itemValue, prefferedOutputValue)
  if prefferedOutputValue ~= nil then
    for output, item in pairs(self.outputsMapping) do
      if prefferedOutputValue == output and item == itemValue then
        return prefferedOutputValue;
      end
    end
  end

  local foundOutput = nil;
  for item, output in pairs(self.itemsMapping) do
    if item == itemValue then
      foundOutput = output;
      break;
    end
  end

  if foundOutput == nil then
    return itemValue;
  end

  return foundOutput;
end

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

function Logic:call()
  local mode               = getLogicValue("setting.mode");
  local manualValue        = mode == MANUAL_MODE and getLogicValue("setting.manual.value") or nil;
  local output             = getLogicValue("output.value");
  local intermediateValue  = getLogicValue("counter.intermediate.output");
  local changedItemValue   = nil;
  local changedOutputValue = output ~= self.lastOutput and output or nil;
  local changedItemKey     = nil;
  local oldItemValue       = nil;


  --reading counters from device
  for key, item in pairs(self.items) do
    local logicItemValue = getLogicValue(item.counterVar);
    if changedItemValue == nil and logicItemValue ~= item.value then
      changedItemValue = logicItemValue;
      changedItemKey = key;
      oldItemValue = item.value;
    end
    item.value = logicItemValue;
  end

  local newOutputValue = nil;
  local newIntermediateValue = nil;

  if manualValue ~= nil then
    newOutputValue = manualValue;
    newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
    --print("output: " .. output .. " -> " .. newOutputValue);
  elseif changedOutputValue ~= nil then
    newOutputValue = changedOutputValue;
    newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
    --print("output: " .. output .. " -> " .. newOutputValue);
  elseif changedItemValue ~= nil then
    newIntermediateValue = changedItemValue;
    newOutputValue = self:getOutputValue(newIntermediateValue, output);
    --print("item (" .. changedItemKey .. "): " .. oldItemValue .. " -> " .. newIntermediateValue .. ", new output: " .. newOutputValue .. ", new inter: " .. newIntermediateValue .. ", curr out: " .. output);
  else
    newOutputValue = output;
    newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
  end

  self.lastOutput = newOutputValue;
  setLogicValue("output.value", newOutputValue);
  setLogicValue("counter.intermediate.output", newIntermediateValue);

  for _, item in pairs(self.items) do
    setLogicValue(item.settingVar, newIntermediateValue);
  end
end

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

-- main logic state object
g_logic = nil;

SUPPORTED_SUBLOGIC_TYPE = "DistVar";
SUPPORTED_SUBLOGIC_VERSION = "0.0.4";
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

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