--    ======================
--    Description
--    ======================
--
--    Są dwa źródła powietrza (źródło 0 i źródło 1). Domyślna konfiguracja: 0 - czerpnia; 1 - GWC
--
--    Logika ma za zadanie wybrać cieplejsze lub zimniejsze źródło.
--
--    W okresie grzewczym zawsze wybierane jest cieplejsze źródło.
--
--    W okresie letnim może być wybierane cieplejsze lub zimniejsze źródło w zależności od temperatury wewnętrznej
--    i nastawionej temperatury minimalnej.
--
--    Jeżeli temperatura wewnętrzna jest poniżej temperatury minimalnej wybierane jest źródło cieplejsze, jeżeli jest powyżej
--    to wybierane jest źródło zimniejsze.
--
--    Co określony czas (setting.test.idle.time) następuje próba pomiarowa mająca na celu wybranie bardziej korzystnego źródła 
--    (do grzania lub chłodzenia).
--
--    W trakcie próby pomiarowej następuje załączenie źródła 0 na określony czas (setting.source.0.test.time) i zapamiętanie
--    wartości pomiarowej z czujnika temperatury wejścia na rekuperator (input.vent.in.t) do przyszłego 
--    porównania (counter.source.0.compare.t.value).
--
--    Następnie jest wykonywana analogiczna próba na źródle 1. Czas przedmuchu służy do zbadania temperatury jaka może
--    dostarczyć odpowiednie źródło.
--
--    Jeżeli czas przedmuchu danego źródła jest ustawiony na 0, temperatura do celów porównawczych jest aktualizowana
--    stale i jest przypisywana odpowiednia wartość input.source.0.t.value lub input.source.1.t.value.
--
--    ======================
--    Parameters
--    ======================
--
--    SubLogic supports following variables:
--
--    input.vent.in.t.value
--    input.vent.in.t.err                     - temperatura wchodząca na rekuperator
--
--    input.source.0.t.value
--    input.source.0.t.err                    - temperatura źródła 0
--
--    input.source.1.t.value
--    input.source.1.t.err                    - temperatura źródła 1
--
--    input.season                            - 0 - zima, 1 - lato [0..1]
--
--    input.inside.t.min.status               - flaga: czy warunek minimalna temperatury wewnętrznej latem, poniżej której wybierane jest włączane ogrzewanie pasywne jest spełniony
--
--    output.source                           - wyjście określające źródło wentylacji
--
--    setting.source.dt                       - jeżeli różnica temperatur miedzy źródłami przekroczy ta wartość to dopiero jest możliwe przełączenie
--
--    setting.alert.t.output                  - wartość output.source w przypadku błędu temperatury
--
--    setting.manual.mode                     - tryb pracy. 0 - tryb automatyczny; 1 - tryb manualny. Tryb manualny jest nadrzędny względem 
--                                              setting.alert.t.output
--
--    setting.manual.output.source            - wartość wyjścia output.source w trybie manualnym.
--
--    setting.source.0.test.time              - czas testowania (przedmuchu) źródła 0 [s]
--
--    setting.source.1.test.time              - czas testowania (przedmuchu) źródła 1 [s]
--
--    setting.test.idle.time                  - czas przerwy między testem źródeł [s]
--
--    counter.alert.t.state                   - jeżeli któryś z czujników jest uszkodzony to ta flaga jest ustawiana na 1. 
--
--    counter.source.0.compare.t.value        - temperatura ze źródła 0 (powietrze dochodzące do rekuperatora) do celów porównawczych wyboru
--
--    counter.source.1.compare.t.value        - temperatura ze źródła 1 (powietrze dochodzące do rekuperatora) do celów porównawczych wyboru
--
--    counter.source.0.test.downcounter       - licznik testowania źródła 0
--
--    counter.source.1.test.downcounter       - licznik testowania źródła 1
--
--    counter.test.idle.downcounter           - licznik pozostały do rozpoczęcia testowania źródeł
--
--    counter.test.mode                       - tryb pracy testowej: 0 - testowanie źródła 0; 1 - testowanie źródła 1; 2 - normalna praca
--
--    counter.heat.mode                       - aktualny tryb pracy: 0 - chłodzenie; 1 - grzanie
--
--    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
--    ======================
--
--    2017-11-30 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_outputSource = nil;

g_counterSource0TestDowncounter = nil;
g_counterSource1TestDowncounter = nil;
g_counterTestIdleDowncounter = nil;

g_counterSource0CompareTValue = 0;
g_counterSource1CompareTValue = 0;
g_counterTestMode = 0;

SUPPORTED_SUBLOGIC_TYPE = "GHE01";
SUPPORTED_SUBLOGIC_VERSION = "0.0.0";
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

  local inputVentInTValue = getLogicValue("input.vent.in.t.value");
  local inputVentInTErr = getLogicValue("input.vent.in.t.err");
  local inputSource0TValue = getLogicValue("input.source.0.t.value");
  local inputSource0TErr = getLogicValue("input.source.0.t.err");
  local inputSource1TValue = getLogicValue("input.source.1.t.value");
  local inputSource1TErr = getLogicValue("input.source.1.t.err");
  local inputSeason = getLogicValue("input.season");
  local inputInsideTMinStatus = getLogicValue("input.inside.t.min.status");
  
  local settingSourceDt = getLogicValue("setting.source.dt");
  local settingAlertTOutput = getLogicValue("setting.alert.t.output");
  local settingManualMode = getLogicValue("setting.manual.mode");
  local settingManualOutputSource = getLogicValue("setting.manual.output.source");
  local settingSource0TestTime = getLogicValue("setting.source.0.test.time");
  local settingSource1TestTime = getLogicValue("setting.source.1.test.time");
  local settingTestIdleTime = getLogicValue("setting.test.idle.time");

  local counterAlertTState = 0;
  local counterSource0TestDowncounter = 0;
  local counterSource1TestDowncounter = 0;
  local counterTestIdleDowncounter = 0;
  local counterHeatMode = 0;

  -- states
  if g_outputSource == nil or firstCall then
    g_outputSource = State.create(0, 0, 1, true);
  end
  g_outputSource:call();
  
  -- timers

  if g_counterSource0TestDowncounter == nil or firstCall then
    g_counterSource0TestDowncounter = DownCounter.create();
  end
  g_counterSource0TestDowncounter:updateParams(settingSource0TestTime * 1000);

--  if g_counterSource0TestDowncounter:elapsed() then
--    g_counterSource0TestDowncounter:reset();
--  end

  counterSource0TestDowncounter = g_counterSource0TestDowncounter:timeTo0() / 1000;
  counterSource0TestDowncounter = (counterSource0TestDowncounter < 0) and 0 or counterSource0TestDowncounter;

  if g_counterSource1TestDowncounter == nil or firstCall then
    g_counterSource1TestDowncounter = DownCounter.create();
  end
  g_counterSource1TestDowncounter:updateParams(settingSource1TestTime * 1000);

--  if g_counterSource1TestDowncounter:elapsed() then
--    g_counterSource1TestDowncounter:reset();
--  end

  counterSource1TestDowncounter = g_counterSource1TestDowncounter:timeTo0() / 1000;
  counterSource1TestDowncounter = (counterSource1TestDowncounter < 0) and 0 or counterSource1TestDowncounter;

  if g_counterTestIdleDowncounter == nil or firstCall then
    g_counterTestIdleDowncounter = DownCounter.create();
  end
  g_counterTestIdleDowncounter:updateParams(settingTestIdleTime * 1000);

--  if g_counterTestIdleDowncounter:elapsed() then
--    g_counterTestIdleDowncounter:reset();
--  end

  counterTestIdleDowncounter = g_counterTestIdleDowncounter:timeTo0() / 1000;
  counterTestIdleDowncounter = (counterTestIdleDowncounter < 0) and 0 or counterTestIdleDowncounter;

  -- obsługa trybu ręcznego
  if ((settingManualMode == 1)) then
    g_outputSource:setValue(settingManualOutputSource);
  end

  -- obsługa alarmu temperatur
  if ((inputVentInTErr ~= 0) and ((settingSource0TestTime ~= 0) or (settingSource1TestTime ~= 0))) then
      counterAlertTState = 1;
  end
  
  if ((inputSource0TErr ~= 0) and (settingSource0TestTime == 0)) then
      counterAlertTState = 1;
  end
  
  if ((inputSource1TErr ~= 0) and (settingSource1TestTime == 0)) then
      counterAlertTState = 1;
  end

  if ((counterAlertTState ~= 0)) then
    g_outputSource:setValue(settingAlertTOutput);
  end
  
  -- tryby testowe i liczniki
  if (g_counterTestMode == 0) then
    g_counterSource1TestDowncounter:reset();
    g_counterTestIdleDowncounter:reset();
    g_outputSource:setValue(0);
    
    if g_counterSource0TestDowncounter:elapsed() then
      g_counterTestMode = 1;
    end
    
  elseif (g_counterTestMode == 1) then
    g_counterSource0TestDowncounter:reset();
    g_counterTestIdleDowncounter:reset();
    g_outputSource:setValue(1);
    
    if g_counterSource1TestDowncounter:elapsed() then
      g_counterTestMode = 2;
    end
  else 
    g_counterSource0TestDowncounter:reset();
    g_counterSource1TestDowncounter:reset();

    if g_counterTestIdleDowncounter:elapsed() then
      g_counterTestMode = 0;
    end
  end
  
  if (g_outputSource:getValue() == 0) then
    g_counterSource0CompareTValue = inputVentInTValue;
  else
    g_counterSource1CompareTValue = inputVentInTValue;
  end
  
  -- jeżeli nie ma zwłoki przypisz z danego źródła na bieżąco
  if (settingSource0TestTime == 0) then
    g_counterSource0CompareTValue = inputSource0TValue;
  end
  
  if (settingSource1TestTime == 0) then
    g_counterSource1CompareTValue = inputSource1TValue;
  end
  
  -- ustalenie trybu grzania lub chłodzenia
  
  if (inputSeason == 0) then
    -- w sezonie grzewczym zawsze tryb grzania
    counterHeatMode = 1;
  else
    -- poza sezonem grzewczym tryb grzania lub chłodzenia w zależności od temperatury wewnętrznej
    counterHeatMode = inputInsideTMinStatus;
  end
  
  -- w normalnym trybie działania
  if (g_counterTestMode == 2) then
    if (counterHeatMode == 0) then
      -- chłodzenie
      if (g_outputSource:getValue() == 0) then
        -- jeżeli źródłem jest 0 i temperatura porównawcza na 0 jest większa od 1 o dt
        -- to przełącz źródło i zresetuj licznik zwłoki
        if (g_counterSource0CompareTValue - g_counterSource1CompareTValue >= settingSourceDt) then
          g_outputSource:setValue(1);
          g_counterTestIdleDowncounter:reset();
        end       
      else
        -- jeżeli źródłem jest 1 i temperatura porównawcza na 1 jest większa od 0 o dt
        -- to przełącz źródło i zresetuj licznik zwłoki
        if (g_counterSource1CompareTValue - g_counterSource0CompareTValue >= settingSourceDt) then
          g_outputSource:setValue(0);
          g_counterTestIdleDowncounter:reset();
        end       
      end
    else
      -- ogrzewanie
      if (g_outputSource:getValue() == 0) then
        -- jeżeli źródłem jest 0 i temperatura porównawcza na 1 jest większa od 0 o dt
        -- to przełącz źródło i zresetuj licznik zwłoki
        if (g_counterSource1CompareTValue - g_counterSource0CompareTValue >= settingSourceDt) then
          g_outputSource:setValue(1);
          g_counterTestIdleDowncounter:reset();
        end       
      else
        -- jeżeli źródłem jest 1 i temperatura porównawcza na 0 jest większa od 1 o dt
        -- to przełącz źródło i zresetuj licznik zwłoki
        if (g_counterSource0CompareTValue - g_counterSource1CompareTValue >= settingSourceDt) then
          g_outputSource:setValue(0);
          g_counterTestIdleDowncounter:reset();
        end       
      end
    end 
  end
    
  -- send logic variables to ibmanager
  setLogicValue("output.source", g_outputSource:getValue());
  setLogicValue("counter.alert.t.state", counterAlertTState); 
  setLogicValue("counter.source.0.compare.t.value", g_counterSource0CompareTValue);
  setLogicValue("counter.source.1.compare.t.value", g_counterSource1CompareTValue);
  setLogicValue("counter.source.0.test.downcounter", counterSource0TestDowncounter);
  setLogicValue("counter.source.1.test.downcounter", counterSource1TestDowncounter);
  setLogicValue("counter.test.idle.downcounter", counterTestIdleDowncounter);
  setLogicValue("counter.test.mode", g_counterTestMode);
  setLogicValue("counter.heat.mode", counterHeatMode);

end
