#include "../ESPEasyCore/ESPEasyRules.h"

#include "../../_Plugin_Helper.h"

#include "../Commands/ExecuteCommand.h"
#include "../DataStructs/TimingStats.h"
#include "../DataTypes/EventValueSource.h"
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
#include "../ESPEasyCore/Serial.h"
#include "../Globals/Cache.h"
#include "../Globals/Device.h"
#include "../Globals/EventQueue.h"
#include "../Globals/Plugins.h"
#include "../Globals/Plugins_other.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/Settings.h"
#include "../Helpers/CRC_functions.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_time_calc.h"
#include "../Helpers/FS_Helper.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/RulesHelper.h"
#include "../Helpers/RulesMatcher.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringParser.h"


#include <math.h>
#include <vector>

#ifdef WEBSERVER_NEW_RULES
String EventToFileName(const String& eventName) {
  int size  = eventName.length();
  int index = eventName.indexOf('=');

  if (index > -1) {
    size = index;
  }
#if defined(ESP8266)
  String fileName = F("rules/");
#endif // if defined(ESP8266)
#if defined(ESP32)
  String fileName = F("/rules/");
#endif // if defined(ESP32)
  fileName += eventName.substring(0, size);
  fileName.replace('#', RULE_FILE_SEPARAROR);
  fileName.toLowerCase();
  return fileName;
}

String FileNameToEvent(const String& fileName) {
#if defined(ESP8266)
  String eventName = fileName.substring(6);
#endif // if defined(ESP8266)
#if defined(ESP32)
  String eventName = fileName.substring(7);
#endif // if defined(ESP32)
  eventName.replace(RULE_FILE_SEPARAROR, '#');
  return eventName;
}
#endif

void checkRuleSets() {
  Cache.rulesHelper.closeAllFiles();
}

/********************************************************************************************\
   Process next event from event queue
 \*********************************************************************************************/
bool processNextEvent() {
  if (Settings.UseRules)
  {
    String nextEvent;

    if (eventQueue.getNext(nextEvent)) {
      rulesProcessing(nextEvent);
      return true;
    }
  }

  // Just make sure any (accidentally) added or remaining events are not kept.
  eventQueue.clear();
  return false;
}

/********************************************************************************************\
   Rules processing
 \*********************************************************************************************/
void rulesProcessing(const String& event) {
  if (!Settings.UseRules) {
    return;
  }
  START_TIMER
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessing"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
#ifndef BUILD_NO_DEBUG
  const unsigned long timer = millis();
#endif // ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    addLogMove(LOG_LEVEL_INFO, concat(F("EVENT: "), event));
  }

  if (Settings.OldRulesEngine()) {
    bool eventHandled = false;

    if (Settings.EnableRulesCaching()) {
      String filename;
      size_t pos = 0;
      if (Cache.rulesHelper.findMatchingRule(event, filename, pos)) {
        const bool startOnMatched = true; // We already matched the event
        eventHandled = rulesProcessingFile(filename, event, pos, startOnMatched);
      }
    } else {
      for (uint8_t x = 0; x < RULESETS_MAX && !eventHandled; x++) {
        eventHandled = rulesProcessingFile(getRulesFileName(x), event);
      }
    }
  } else {
    #ifdef WEBSERVER_NEW_RULES
    String fileName = EventToFileName(event);

    // if exists processed the rule file
    if (fileExists(fileName)) {
      rulesProcessingFile(fileName, event);
    }
    # ifndef BUILD_NO_DEBUG
    else {
      addLog(LOG_LEVEL_DEBUG, strformat(F("EVENT: %s is ingnored. File %s not found."),
             event.c_str(), fileName.c_str()));
    }
    # endif    // ifndef BUILD_NO_DEBUG
    #endif // WEBSERVER_NEW_RULES
  }

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    addLogMove(LOG_LEVEL_DEBUG, strformat(F("EVENT: %s Processing: %d ms"), event.c_str(), timePassedSince(timer)));
  }
#endif // ifndef BUILD_NO_DEBUG
  STOP_TIMER(RULES_PROCESSING);
  backgroundtasks();
}

/********************************************************************************************\
   Rules processing
 \*********************************************************************************************/
bool rulesProcessingFile(const String& fileName, 
                         const String& event, 
                         size_t pos,
                         bool   startOnMatched) {
  if (!Settings.UseRules || !fileExists(fileName)) {
    return false;
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessingFile"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
#ifndef BUILD_NO_DEBUG

  if (Settings.SerialLogLevel == LOG_LEVEL_DEBUG_DEV) {
    serialPrint(F("RuleDebug Processing:"));
    serialPrintln(fileName);
    serialPrintln(F("     flags CMI  parse output:"));
  }
#endif // ifndef BUILD_NO_DEBUG

  static uint8_t nestingLevel = 0;

  nestingLevel++;

  if (nestingLevel > RULES_MAX_NESTING_LEVEL) {
    addLog(LOG_LEVEL_ERROR, F("EVENT: Error: Nesting level exceeded!"));
    nestingLevel--;
    return false;
  }


  bool match     = false;
  bool codeBlock = false;
  bool isCommand = false;
  bool condition[RULES_IF_MAX_NESTING_LEVEL];
  bool ifBranche[RULES_IF_MAX_NESTING_LEVEL];
  uint8_t ifBlock     = 0;
  uint8_t fakeIfBlock = 0;


  bool moreAvailable = true;
  bool eventHandled = false;
  while (moreAvailable && !eventHandled) {
    const bool searchNextOnBlock = !codeBlock && !match;
    String line = Cache.rulesHelper.readLn(fileName, pos, moreAvailable, searchNextOnBlock);

    // Parse the line and extract the action (if there is any)
    String action;
    {
      START_TIMER
      const bool matched_before_parse = match;
      bool isOneLiner = false;
      parseCompleteNonCommentLine(line, event, action, match, codeBlock,
                                  isCommand, isOneLiner, condition, ifBranche, ifBlock,
                                  fakeIfBlock, startOnMatched);
      if ((matched_before_parse && !match) || isOneLiner) {
        // We were processing a matching event and now crossed the "endon"
        // Or just dealing with a oneliner.
        // So we're done processing
        eventHandled = true;
        backgroundtasks();
      }
      STOP_TIMER(RULES_PARSE_LINE);
    }

    if (match) // rule matched for one action or a block of actions
    {
      START_TIMER
      processMatchedRule(action, event,
                         isCommand, condition,
                         ifBranche, ifBlock, fakeIfBlock);
      STOP_TIMER(RULES_PROCESS_MATCHED);
    }
  }

/*
  if (f) {
    f.close();
  }
*/

  nestingLevel--;
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("rulesProcessingFile2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  backgroundtasks();
  return eventHandled; // && nestingLevel == 0;
}


/********************************************************************************************\
   Parse string commands
 \*********************************************************************************************/
bool get_next_inner_bracket(const String& line, unsigned int& startIndex, int& closingIndex, char closingBracket)
{
  if (line.length() <= 1) {
    // Not possible to have opening and closing bracket on a line this short.
    return false;
  }
  char openingBracket = closingBracket;

  switch (closingBracket) {
    case ']': openingBracket = '['; break;
    case '}': openingBracket = '{'; break;
    case ')': openingBracket = '('; break;
    default:
      // unknown bracket type
      return false;
  }
  // Closing bracket should not be found on the first position.
  closingIndex = line.indexOf(closingBracket, startIndex + 1);

  if (closingIndex == -1) { 
    // not found
    return false; 
  }

  for (int i = (closingIndex - 1); (i >= static_cast<int>(startIndex)) && (i >= 0); --i) {
    if (line[i] == openingBracket) {
      startIndex = i;
      return true;
    }
  }
  return false;
}

bool get_next_argument(const String& fullCommand, int& index, String& argument, char separator)
{
  if (index == -1) {
    return false;
  }
  int newIndex = fullCommand.indexOf(separator, index);

  if (newIndex == -1) {
    argument = fullCommand.substring(index);
  } else {
    argument = fullCommand.substring(index, newIndex);
  }

  if (argument.startsWith(String(separator))) {
    argument = argument.substring(1);
  }

  //  addLog(LOG_LEVEL_INFO, String("get_next_argument: ") + String(index) + " " + fullCommand + " " + argument);
  index = newIndex;

  if (index != -1) {
    ++index;
  }
  return argument.length() > 0;
}

const char bitwise_functions[] PROGMEM = "bitread|bitset|bitclear|bitwrite|xor|and|or";
enum class bitwise_functions_e {
  bitread,
  bitset,
  bitclear,
  bitwrite,
  xor_e,  // protected keywords, thus appended _e
  and_e,
  or_e
};


bool parse_bitwise_functions(const String& cmd_s_lower, const String& arg1, const String& arg2, const String& arg3, int64_t& result) {
  #ifndef BUILD_NO_DEBUG
  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("Bitwise: {");
    log += wrapIfContains(cmd_s_lower, ':', '\"');
    log += ':';
    log += wrapIfContains(arg1, ':', '\"');

    if (arg2.length() > 0) {
      log += ':';
      log += wrapIfContains(arg2, ':', '\"');

      if (arg3.length() > 0) {
        log += ':';
        log += wrapIfContains(arg3, ':', '\"');
      }
    }
    log += '}';
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #endif

  if (cmd_s_lower.length() < 2) {
    return false;
  }

  int command_i = GetCommandCode(cmd_s_lower.c_str(), bitwise_functions);
  if (command_i == -1) {
    // No matching function found
    return false;
  }
  
  if (cmd_s_lower.startsWith(F("bit"))) {
    uint32_t bitnr = 0;
    uint64_t iarg2 = 0;

    if (!validUIntFromString(arg1, bitnr) || !validUInt64FromString(arg2, iarg2)) {
      return false;
    }

    switch(static_cast<bitwise_functions_e>(command_i)) {
      case bitwise_functions_e::bitread:
        // Syntax like {bitread:0:123} to get a single decimal '1'
        result = bitRead(iarg2, bitnr);
        break;
      case bitwise_functions_e::bitset:
        // Syntax like {bitset:0:122} to set least significant bit of the given nr '122' to '1' => '123'
        result = iarg2;
        bitSetULL(result, bitnr);
        break;
      case bitwise_functions_e::bitclear:
        // Syntax like {bitclear:0:123} to set least significant bit of the given nr '123' to '0' => '122'
        result = iarg2;
        bitClearULL(result, bitnr);
        break;
      case bitwise_functions_e::bitwrite:
      {
        uint32_t iarg3 = 0;
        // Syntax like {bitwrite:0:122:1} to set least significant bit of the given nr '122' to '1' => '123'
        if (validUIntFromString(arg3, iarg3)) {
          const int bitvalue = (iarg3 & 1); // Only use the last bit of the given parameter
          result = iarg2;
          bitWriteULL(result, bitnr, bitvalue);
        } else {
          // Need 3 parameters, but 3rd one is not a valid uint
          return false;
        }
        break;
      }
      default: 
        return false;
    }

    // all functions starting with "bit" are checked
    return true;
  }

  uint64_t iarg1, iarg2 = 0;

  if (!validUInt64FromString(arg1, iarg1) || !validUInt64FromString(arg2, iarg2)) {
    return false;
  }

  switch(static_cast<bitwise_functions_e>(command_i)) {
    case bitwise_functions_e::xor_e:
      // Syntax like {xor:127:15} to XOR the binary values 1111111 and 1111 => 1110000
      result = iarg1 ^ iarg2;
      break;
    case bitwise_functions_e::and_e:
      // Syntax like {and:254:15} to AND the binary values 11111110 and 1111 => 1110
      result = iarg1 & iarg2;
      break;
    case bitwise_functions_e::or_e:
      // Syntax like {or:254:15} to OR the binary values 11111110 and 1111 => 11111111
      result = iarg1 | iarg2;
      break;
    default: 
      return false;

  }
  return true;
}

bool parse_math_functions(const String& cmd_s_lower, const String& arg1, const String& arg2, const String& arg3, ESPEASY_RULES_FLOAT_TYPE& result) {
  ESPEASY_RULES_FLOAT_TYPE farg1;
  float  farg2, farg3 = 0.0f;

  if (!cmd_s_lower.startsWith("crc") && !validDoubleFromString(arg1, farg1)) {
    return false;
  }

  if (equals(cmd_s_lower, F("constrain"))) {
    // Contrain a value X to be within range of A to B
    // Syntax like {constrain:x:a:b} to constrain x in range a...b
    if (validFloatFromString(arg2, farg2) && validFloatFromString(arg3, farg3)) {
      if (farg2 > farg3) {
        const float tmp = farg2;
        farg2 = farg3;
        farg3 = tmp;
      }
      result = constrain(farg1, farg2, farg3);
    } else {
      return false;
    }
  } else if (cmd_s_lower.startsWith("crc")) {
    std::vector<uint8_t> argument = parseHexTextData(arg1, 1);
    const String crctype          = cmd_s_lower.substring(3);

    if (argument.size() > 0) {
      if (equals(crctype, F("8"))) {
        result = calc_CRC8(&argument[0], argument.size());
      // } else if (equals(crctype, F("16"))) { // FIXME crc16 not supported until needed/used/tested
      //   result = calc_CRC16((const char *)argument.data(), argument.size());
      } else if (equals(crctype, F("32"))) {
        result = calc_CRC32(&argument[0], argument.size());
      } else {
        return false;
      }

      if (!arg2.isEmpty() && validDoubleFromString(arg2, farg1)) { // Optional expected crc value
        result = essentiallyEqual(result, farg1) ? 1.0 : 0.0; // Return 1 if the calculated crc == expected crc
      }
    } else {
      return false;
    }
  } else {
    // No matching function found
    return false;
  }
  return true;
}

const char string_commands[] PROGMEM = "substring|indexof|indexof_ci|equals|equals_ci|timetomin|timetosec|strtol|tobin|tohex|ord|urlencode"
  #if FEATURE_STRING_VARIABLES
  "|lookup"
  #endif // if FEATURE_STRING_VARIABLES
  ;
enum class string_commands_e {
  substring,
  indexof,
  indexof_ci,
  equals,
  equals_ci,
  timetomin,
  timetosec,
  strtol,
  tobin,
  tohex,
  ord,
  urlencode,
  #if FEATURE_STRING_VARIABLES
  lookup,
  #endif // if FEATURE_STRING_VARIABLES
};


void parse_string_commands(String& line) {
  unsigned int startIndex = 0;
  int closingIndex;

  bool mustReplaceMaskedChars = false;
  bool mustReplaceEscapedBracket = false;
  bool mustReplaceEscapedCurlyBracket = false;
  String MaskEscapedBracket;

  if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) {
    // replace the \( and \) with other characters to mask the escaped brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x11); // ASCII 0x11 = Device control 1
    line.replace(F("\\("), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x12); // ASCII 0x12 = Device control 2
    line.replace(F("\\)"), MaskEscapedBracket);
    mustReplaceEscapedBracket = true;
  }
  if (hasEscapedCharacter(line,'{') || hasEscapedCharacter(line,'}')) {
    // replace the \{ and \} with other characters to mask the escaped curly brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x13); // ASCII 0x13 = Device control 3
    line.replace(F("\\{"), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x14); // ASCII 0x14 = Device control 4
    line.replace(F("\\}"), MaskEscapedBracket);
    mustReplaceEscapedCurlyBracket = true;
  }

  while (get_next_inner_bracket(line, startIndex, closingIndex, '}')) {
    // Command without opening and closing brackets.
    const String fullCommand = line.substring(startIndex + 1, closingIndex);
    const String cmd_s_lower = parseString(fullCommand, 1, ':');
    const String arg1        = parseStringKeepCaseNoTrim(fullCommand, 2, ':');
    const String arg2        = parseStringKeepCaseNoTrim(fullCommand, 3, ':');
    const String arg3        = parseStringKeepCaseNoTrim(fullCommand, 4, ':');

    if (cmd_s_lower.length() > 0) {
      String replacement; // maybe just replace with empty to avoid looping?
      uint64_t iarg1, iarg2 = 0;
      ESPEASY_RULES_FLOAT_TYPE fresult{};
      int64_t  iresult = 0;
      int32_t startpos, endpos = -1;
      const bool arg1valid = validIntFromString(arg1, startpos);
      const bool arg2valid = validIntFromString(arg2, endpos);

      if (parse_math_functions(cmd_s_lower, arg1, arg2, arg3, fresult)) {
        const bool trimTrailingZeros = true;
        #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
        replacement = doubleToString(fresult, maxNrDecimals_fpType(fresult), trimTrailingZeros);
        #else
        replacement = floatToString(fresult, maxNrDecimals_fpType(fresult), trimTrailingZeros);
        #endif
      } else if (parse_bitwise_functions(cmd_s_lower, arg1, arg2, arg3, iresult)) {
        replacement = ull2String(iresult);
      } else {

        int command_i = GetCommandCode(cmd_s_lower.c_str(), string_commands);
        if (command_i != -1) {
          const string_commands_e command = static_cast<string_commands_e>(command_i);

              //  addLog(LOG_LEVEL_INFO, strformat(F("parse_string_commands cmd: %s %s %s %s"), 
              // cmd_s_lower.c_str(), arg1.c_str(), arg2.c_str(), arg3.c_str()));

          switch (command) {
            case string_commands_e::substring:
              // substring arduino style (first char included, last char excluded)
              // Syntax like 12345{substring:8:12:ANOTHER HELLO WORLD}67890

              if (arg1valid) {
                if (arg2valid){
                  replacement = arg3.substring(startpos, endpos);
                } else {
                  replacement = arg3.substring(startpos);
                }
              }
              break;
            #if FEATURE_STRING_VARIABLES
            case string_commands_e::lookup:
              if (arg1valid && arg2valid && startpos > -1 && endpos > -1) {
                replacement = arg3.substring(startpos * endpos, (startpos + 1) * endpos);
              }
              break;
            #endif // if FEATURE_STRING_VARIABLES
            case string_commands_e::indexof:
            case string_commands_e::indexof_ci:
              // indexOf arduino style (0-based position of first char returned, -1 if not found, case sensitive), 3rd argument is search-offset
              // indexOf_ci : case-insensitive
              // Syntax like {indexof:HELLO:"ANOTHER HELLO WORLD"} => 8, {indexof:hello:"ANOTHER HELLO WORLD"} => -1, {indexof_ci:Hello:"ANOTHER HELLO WORLD"} => 8
              // or like {indexof_ci:hello:"ANOTHER HELLO WORLD":10} => -1

              if (!arg1.isEmpty()
                  && !arg2.isEmpty()) {
                uint32_t offset = 0;
                validUIntFromString(arg3, offset);
                if (command == string_commands_e::indexof_ci) {
                  String arg1copy(arg1);
                  String arg2copy(arg2);
                  arg1copy.toLowerCase();
                  arg2copy.toLowerCase();
                  replacement = arg2copy.indexOf(arg1copy, offset);
                } else {
                  replacement = arg2.indexOf(arg1, offset);
                }
              }
              break;
            case string_commands_e::equals:
            case string_commands_e::equals_ci:
              // equals: compare strings 1 = equal, 0 = unequal (case sensitive)
              // equals_ci: case-insensitive compare
              // Syntax like {equals:HELLO:HELLO} => 1, {equals:hello:HELLO} => 0, {equals_ci:hello:HELLO} => 1, {equals_ci:hello:BLA} => 0

              if (!arg1.isEmpty()
                  && !arg2.isEmpty()) {
                if (command == string_commands_e::equals_ci) {
                  replacement = arg2.equalsIgnoreCase(arg1);
                } else {
                  replacement = arg2.equals(arg1);
                }
              }
              break;
            case string_commands_e::timetomin:
            case string_commands_e::timetosec:
              // time to minutes, transform a substring hh:mm to minutes
              // time to seconds, transform a substring hh:mm:ss to seconds
              // syntax similar to substring

              if (arg1valid
                  && arg2valid) {
                int timeSeconds = 0;
                String timeString;
                if(timeStringToSeconds(arg3.substring(startpos, endpos), timeSeconds, timeString)) {
                  if (command == string_commands_e::timetosec) {
                    replacement = timeSeconds;
                  } else { // timetomin
                    replacement = timeSeconds / 60;
                  }
                }
              }
              break;
            case string_commands_e::strtol:
              // string to long integer (from cstdlib)
              // Syntax like 1234{strtol:16:38}7890
              if (validUInt64FromString(arg1, iarg1)
                  && validUInt64FromString(arg2, iarg2)) {
                replacement = String(strtoul(arg2.c_str(), nullptr, iarg1));
              }
              break;
            case string_commands_e::tobin:
              // Convert to binary string
              // Syntax like 1234{tobin:15}7890
              if (validUInt64FromString(arg1, iarg1)) {
                replacement = ull2String(iarg1, BIN);
              }
              break;
            case string_commands_e::tohex:
              // Convert to HEX string
              // Syntax like 1234{tohex:15[,minHexDigits]}7890
              if (validUInt64FromString(arg1, iarg1)) {
                if (!validUInt64FromString(arg2, iarg2)) {
                  iarg2 = 0;
                }
                replacement = formatToHex_no_prefix(iarg1, iarg2);
              }
              break;
            case string_commands_e::ord:
              {
                // Give the ordinal/integer value of the first character of a string
                // Syntax like let 1,{ord:B}
                uint8_t uval = arg1.c_str()[0];
                replacement = String(uval);
              }
              break;
            case string_commands_e::urlencode:
              // Convert to url-encoded string
              // Syntax like {urlencode:"string to/encode"}
              if (!arg1.isEmpty()) {
                replacement = URLEncode(arg1);
              }
              break;
          }
        }
      }

      if (replacement.isEmpty()) {
        // part in braces is not a supported command.
        // replace the {} with other characters to mask the braces so we can continue parsing.
        // We have to unmask then after we're finished.
        // See: https://github.com/letscontrolit/ESPEasy/issues/2932#issuecomment-596139096
        replacement = line.substring(startIndex, closingIndex + 1);
        replacement.replace('{', static_cast<char>(0x02));
        replacement.replace('}', static_cast<char>(0x03));
        mustReplaceMaskedChars = true;
      }

      // Replace the full command including opening and closing brackets.
      line.replace(line.substring(startIndex, closingIndex + 1), replacement);

      /*
         if (replacement.length() > 0) {
         addLog(LOG_LEVEL_INFO, strformat(F("parse_string_commands cmd: %s -> %s"), fullCommand.c_str(), replacement.c_str());
         }
       */
    }
  }

  if (mustReplaceMaskedChars) {
    // We now have to check if we did mask some parts and unmask them.
    // Let's hope we don't mess up any Unicode here.
    line.replace(static_cast<char>(0x02), '{');
    line.replace(static_cast<char>(0x03), '}');
  }

  if (mustReplaceEscapedBracket) {
    // We now have to check if we did mask some escaped bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x11); // ASCII 0x11 = Device control 1
    line.replace(MaskEscapedBracket, F("\\("));
    MaskEscapedBracket = static_cast<char>(0x12); // ASCII 0x12 = Device control 2
    line.replace(MaskEscapedBracket, F("\\)"));
  }

  if (mustReplaceEscapedCurlyBracket) {
    // We now have to check if we did mask some escaped curly bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x13); // ASCII 0x13 = Device control 3
    line.replace(MaskEscapedBracket, F("\\{"));
    MaskEscapedBracket = static_cast<char>(0x14); // ASCII 0x14 = Device control 4
    line.replace(MaskEscapedBracket, F("\\}"));
  }
}

void substitute_eventvalue(String& line, const String& event) {
  if (substitute_eventvalue_CallBack_ptr != nullptr) {
    substitute_eventvalue_CallBack_ptr(line, event);
  }

  if (line.indexOf(F("%event")) != -1) {
    const int equalsPos = event.indexOf('=');

    if (event.charAt(0) == '!') {
      line.replace(F("%eventvalue%"), event); // substitute %eventvalue% with
                                              // literal event string if
                                              // starting with '!'
    } else {

      String argString;

      if (equalsPos > 0) {
        argString = event.substring(equalsPos + 1);
      }

      // Replace %eventvalueX% with the actual value of the event.
      // For compatibility reasons also replace %eventvalue%  (argc = 0)
      line.replace(F("%eventvalue%"), F("%eventvalue1%"));
      int eventvalue_pos = line.indexOf(F("%eventvalue"));

      while (eventvalue_pos != -1) {
        const int percent_pos = line.indexOf('%', eventvalue_pos + 1);

        if (percent_pos == -1) {
          // Found "%eventvalue" without closing %
          if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
            String log = F("Rules : Syntax error, missing '%' in '%eventvalue%': '");
            log += line;
            log += F("' %-pos: ");
            log += percent_pos;
            addLog(LOG_LEVEL_ERROR, log);
          }
          line.replace(F("%eventvalue"), F(""));
        } else {
          // Find the optional part for a default value when the asked for eventvalue does not exist.
          // Syntax: %eventvalueX|Y%
          // With: X = event value nr, Y = default value when eventvalue does not exist.
          String defaultValue('0');
          int or_else_pos = line.indexOf('|', eventvalue_pos);
          if ((or_else_pos == -1) || (or_else_pos > percent_pos)) {
            or_else_pos = percent_pos;
          } else {
            defaultValue = line.substring(or_else_pos + 1, percent_pos);
          }
          const String nr         = line.substring(eventvalue_pos + 11, or_else_pos);
          const String eventvalue = line.substring(eventvalue_pos, percent_pos + 1);
          int argc                = -1;

          if (equals(nr, '0')) {
            // Replace %eventvalue0% with the entire list of arguments.
            line.replace(eventvalue, argString);
          } else {
            argc = nr.toInt();
            
            // argc will be 0 on invalid int (0 was already handled)
            if (argc > 0) {
              String tmpParam;

              if (!GetArgv(argString.c_str(), tmpParam, argc)) {
                // Replace with default value for non existing event values
                tmpParam = parseTemplate(defaultValue);
              }
              line.replace(eventvalue, tmpParam);
            } else {
              // Just remove the invalid eventvalue variable
              if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
                addLog(LOG_LEVEL_ERROR, concat(F("Rules : Syntax error, invalid variable: "), eventvalue));
              }
              line.replace(eventvalue, EMPTY_STRING);
            }
          }
        }
        eventvalue_pos = line.indexOf(F("%eventvalue"));
      }
    }

    if ((line.indexOf(F("%eventname%")) != -1) ||
        (line.indexOf(F("%eventpar%")) != -1)) {
      const String eventName = equalsPos == -1 ? event : event.substring(0, equalsPos);

      // Replace %eventname% with the literal event
      line.replace(F("%eventname%"), eventName);

      // Part of %eventname% after the # char
      const int hash_pos = eventName.indexOf('#');
      line.replace(F("%eventpar%"), hash_pos == -1 ? EMPTY_STRING : eventName.substring(hash_pos + 1));
    }
  }
}

void parseCompleteNonCommentLine(String& line, const String& event,
                                 String& action, bool& match,
                                 bool& codeBlock, bool& isCommand, bool& isOneLiner,
                                 bool condition[], bool ifBranche[],
                                 uint8_t& ifBlock, uint8_t& fakeIfBlock,
                                 bool   startOnMatched) {
  if (line.length() == 0) {
    return;
  }
  const bool lineStartsWith_on = line.substring(0, 3).equalsIgnoreCase(F("on "));

  if (!codeBlock && !match) {
    // We're looking for a new code block.
    // Continue with next line if none was found on current line.
    if (!lineStartsWith_on) {
      return;
    }
  }

  if (line.equalsIgnoreCase(F("endon"))) // Check if action block has ended, then we will
                                           // wait for a new "on" rule
  {
    isCommand   = false;
    codeBlock   = false;
    match       = false;
    ifBlock     = 0;
    fakeIfBlock = 0;
    return;
  }

  const bool lineStartsWith_pct_event = line.startsWith(F("%event"));;

  isCommand = true;

  if (match || !codeBlock) {
    // only parse [xxx#yyy] if we have a matching ruleblock or need to eval the
    // "on" (no codeBlock)
    // This to avoid wasting CPU time...
    if (match && !fakeIfBlock) {
      // substitution of %eventvalue% is made here so it can be used on if
      // statement too
      substitute_eventvalue(line, event);
    }

    if (match || lineStartsWith_on) {
      // Only parseTemplate when we are actually doing something with the line.
      // When still looking for the "on ... do" part, do not change it before we found the block.
      line = parseTemplate(line);
    }
  }

  if (!codeBlock) // do not check "on" rules if a block of actions is to be
                  // processed
  {
    action.clear();
    if (lineStartsWith_on) {
      ifBlock     = 0;
      fakeIfBlock = 0;

      String ruleEvent;
      if (getEventFromRulesLine(line, ruleEvent, action)) {
        START_TIMER
        match = startOnMatched || ruleMatch(event, ruleEvent);
        STOP_TIMER(RULES_MATCH);
      } else {
        match = false;
      }

      if (action.length() > 0) // single on/do/action line, no block
      {
        isCommand  = true;
        isOneLiner = true;
        codeBlock  = false;
      } else {
        isCommand = false;
        codeBlock = true;
      }
    }
  } else {
    #ifndef BUILD_NO_DEBUG
    if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
      // keep the line for the log
      action = line;
    } else {
      action = std::move(line);  
    }
    #else
    action = std::move(line);
    #endif
  }

  if (isCommand && lineStartsWith_pct_event) {
    action = concat(F("restrict,"), action);
    if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
      addLogMove(LOG_LEVEL_ERROR, 
        concat(F("Rules : Prefix command with 'restrict': "), action));
    }
  }

#ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
    String log = F("RuleDebug: ");
    log += codeBlock ? 0 : 1;
    log += match ? 0 : 1;
    log += isCommand ? 0 : 1;
    log += F(": ");
    log += line;
    addLogMove(LOG_LEVEL_DEBUG_DEV, log);
  }
#endif // ifndef BUILD_NO_DEBUG
}

void processMatchedRule(String& action, const String& event,
                        bool& isCommand, bool condition[], bool ifBranche[],
                        uint8_t& ifBlock, uint8_t& fakeIfBlock) {
  String lcAction = action;

  lcAction.toLowerCase();
  lcAction.trim();

  if (fakeIfBlock) {
    isCommand = false;
  }
  else if (ifBlock) {
    if (condition[ifBlock - 1] != ifBranche[ifBlock - 1]) {
      isCommand = false;
    }
  }
  int split =
    lcAction.startsWith(F("elseif ")) ? 0 : -1; // check for optional "elseif" condition

  if (split != -1) {
    // Found "elseif" condition
    isCommand = false;

    if (ifBlock && !fakeIfBlock) {
      if (ifBranche[ifBlock - 1]) {
        if (condition[ifBlock - 1]) {
          ifBranche[ifBlock - 1] = false;
        }
        else {
          String check = lcAction.substring(split + 7);
          check.trim();
          condition[ifBlock - 1] = conditionMatchExtended(check);
#ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            String log  = F("Lev.");
            log += String(ifBlock);
            log += F(": [elseif ");
            log += check;
            log += F("]=");
            log += boolToString(condition[ifBlock - 1]);
            addLogMove(LOG_LEVEL_DEBUG, log);
          }
#endif // ifndef BUILD_NO_DEBUG
        }
      }
    }
  } else {
    // check for optional "if" condition
    split = lcAction.startsWith(F("if ")) ? 0 : -1;

    if (split != -1) {
      if (ifBlock < RULES_IF_MAX_NESTING_LEVEL) {
        if (isCommand) {
          ifBlock++;
          String check = lcAction.substring(split + 3);
          check.trim();
          condition[ifBlock - 1] = conditionMatchExtended(check);
          ifBranche[ifBlock - 1] = true;
#ifndef BUILD_NO_DEBUG

          if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
            String log  = F("Lev.");
            log += String(ifBlock);
            log += F(": [if ");
            log += check;
            log += F("]=");
            log += boolToString(condition[ifBlock - 1]);
            addLogMove(LOG_LEVEL_DEBUG, log);
          }
#endif // ifndef BUILD_NO_DEBUG
        } else {
          fakeIfBlock++;
        }
      } else {
        fakeIfBlock++;

        if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
          String log  = F("Lev.");
          log += String(ifBlock);
          log += F(": Error: IF Nesting level exceeded!");
          addLogMove(LOG_LEVEL_ERROR, log);
        }
      }
      isCommand = false;
    }
  }

  if ((equals(lcAction, F("else"))) && !fakeIfBlock) // in case of an "else" block of
                                               // actions, set ifBranche to
                                               // false
  {
    ifBranche[ifBlock - 1] = false;
    isCommand              = false;
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log  = F("Lev.");
      log += String(ifBlock);
      log += F(": [else]=");
      log += boolToString(condition[ifBlock - 1] == ifBranche[ifBlock - 1]);
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
#endif // ifndef BUILD_NO_DEBUG
  }

  if (equals(lcAction, F("endif"))) // conditional block ends here
  {
    if (fakeIfBlock) {
      fakeIfBlock--;
    }
    else if (ifBlock) {
      ifBlock--;
    }
    isCommand = false;
  }

  // process the action if it's a command and unconditional, or conditional and
  // the condition matches the if or else block.
  if (isCommand) {
    substitute_eventvalue(action, event);

    const bool executeRestricted = equals(parseString(action, 1), F("restrict"));

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String actionlog = executeRestricted ? F("ACT  : (restricted) ") : F("ACT  : ");
      actionlog += action;
      addLogMove(LOG_LEVEL_INFO, actionlog);
    }

    if (executeRestricted) {
      ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_RULES_RESTRICTED, parseStringToEndKeepCase(action, 2)});
    } else {
      // Use action.c_str() here as we need to preserve the action string.
      ExecuteCommand_all({EventValueSource::Enum::VALUE_SOURCE_RULES, action.c_str()});
    }
    delay(0);
  }
}

/********************************************************************************************\
   Check if an event matches to a given rule
 \*********************************************************************************************/


/********************************************************************************************\
   Check expression
 \*********************************************************************************************/
bool conditionMatchExtended(String& check) {
  int  condAnd   = -1;
  int  condOr    = -1;
  bool rightcond = false;
  bool leftcond  = conditionMatch(check); // initial check

  #ifndef BUILD_NO_DEBUG
  String debugstr;

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    debugstr += boolToString(leftcond);
  }
  #endif // ifndef BUILD_NO_DEBUG

  do {
    #ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
      String log = F("conditionMatchExtended: ");
      log += debugstr;
      log += ' ';
      log += wrap_String(check, '"');
      addLogMove(LOG_LEVEL_DEBUG, log);
    }
    #endif // ifndef BUILD_NO_DEBUG
    condAnd = check.indexOf(F(" and "));
    condOr  = check.indexOf(F(" or "));

    if ((condAnd > 0) || (condOr > 0)) {                             // we got AND/OR
      if ((condAnd > 0) && (((condOr < 0) /*&& (condOr < condAnd)*/) ||
                            ((condOr > 0) && (condOr > condAnd)))) { // AND is first
        check     = check.substring(condAnd + 5);
        rightcond = conditionMatch(check);
        leftcond  = (leftcond && rightcond);

        #ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          debugstr += F(" && ");
        }
        #endif // ifndef BUILD_NO_DEBUG
      } else { // OR is first
        check     = check.substring(condOr + 4);
        rightcond = conditionMatch(check);
        leftcond  = (leftcond || rightcond);

        #ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          debugstr += F(" || ");
        }
        #endif // ifndef BUILD_NO_DEBUG
      }

      #ifndef BUILD_NO_DEBUG

      if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
        debugstr += boolToString(rightcond);
      }
      #endif // ifndef BUILD_NO_DEBUG
    }
  } while (condAnd > 0 || condOr > 0);

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    check = debugstr;
  }
  #endif // ifndef BUILD_NO_DEBUG
  return leftcond;
}


void logtimeStringToSeconds(const String& tBuf, int hours, int minutes, int seconds, bool valid)
{
  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log;
    log  = F("timeStringToSeconds: ");
    log += wrap_String(tBuf, '"');
    log += F(" --> ");
    if (valid) {
      log += formatIntLeadingZeroes(hours, 2);
      log += ':';
      log += formatIntLeadingZeroes(minutes, 2);
      log += ':';
      log += formatIntLeadingZeroes(seconds, 2);
    } else {
      log += F("invalid");
    }
    addLogMove(LOG_LEVEL_DEBUG, log);
  }

  #endif // ifndef BUILD_NO_DEBUG
}

// convert old and new time string to nr of seconds
// return whether it should be considered a time string.
bool timeStringToSeconds(const String& tBuf, int& time_seconds, String& timeString) {
  {
    // Make sure we only check for expected characters
    // e.g. if 10:11:12 > 7:07 and 10:11:12 < 20:09:04
    // Should only try to match "7:07", not "7:07 and 10:11:12" 
    // Or else it will find "7:07:11"
    bool done = false;
    for (uint8_t pos = 0; !done && timeString.length() < 8 && pos < tBuf.length(); ++pos) {
      char c = tBuf[pos];
      if (isdigit(c) || c == ':') {
        timeString += c;
      } else {
        done = true;
      }
    }
  }

  time_seconds = -1;
  int32_t hours   = 0;
  int32_t minutes = 0;
  int32_t seconds = 0;

  int tmpIndex = 0;
  String hours_str, minutes_str, seconds_str;
  bool   validTime = false;

  if (get_next_argument(timeString, tmpIndex, hours_str, ':')) {
    if (validIntFromString(hours_str, hours)) {
      validTime = true;

      if ((hours < 0) || (hours > 24)) {
        validTime = false;
      } else {
        time_seconds = hours * 60 * 60;
      }

      if (validTime && get_next_argument(timeString, tmpIndex, minutes_str, ':')) {
        if (validIntFromString(minutes_str, minutes)) {
          if ((minutes < 0) || (minutes > 59)) {
            validTime = false;
          } else {
            time_seconds += minutes * 60;
          }

          if (validTime && get_next_argument(timeString, tmpIndex, seconds_str, ':')) {
            // New format, only HH:MM:SS
            if (validIntFromString(seconds_str, seconds)) {
              if ((seconds < 0) || (seconds > 59)) {
                validTime = false;
              } else {
                time_seconds += seconds;
              }
            }
          } else {
            // Old format, only HH:MM
          }
        }
      } else {
        // It is a valid time string, but could also be just a numerical.
        // We mark it here as invalid, meaning the 'other' time to compare it to must contain more than just the hour.
        validTime = false;
      }
    }
  }
  logtimeStringToSeconds(timeString, hours, minutes, seconds, validTime);
  return validTime;
}

// Balance the count of parentheses (aka round braces) by adding the missing left or right parentheses, if any
// Returns the number of added parentheses, < 0 is left parentheses added, > 0 is right parentheses added
int balanceParentheses(String& string) {
  int left = 0;
  int right = 0;
  for (unsigned int i = 0; i < string.length(); i++) {
    if (string[i] == '(') {
      left++;
    } else if (string[i] == ')') {
      right++;
    }
  }
  if (left != right) {
    string.reserve(string.length() + abs(right - left)); // Re-allocate max. once
  }
  if (left > right) {
    for (int i = 0; i < left - right; i++) {
      string += ')';
    }
  } else if (right > left) {
    for (int i = 0; i < right - left; i++) {
      string = String('(') + string; // This is quite 'expensive'
    }
  }
  return left - right;
}

bool conditionMatch(const String& check) {
  int  posStart, posEnd;
  char compare;

  if (!findCompareCondition(check, compare, posStart, posEnd)) {
    return false;
  }

  String tmpCheck1 = check.substring(0, posStart);
  String tmpCheck2 = check.substring(posEnd);

  tmpCheck1.trim();
  tmpCheck2.trim();
  ESPEASY_RULES_FLOAT_TYPE Value1{};
  ESPEASY_RULES_FLOAT_TYPE Value2{};

  int  timeInSec1 = 0;
  int  timeInSec2 = 0;
  String timeString1, timeString2;
  bool validTime1 = timeStringToSeconds(tmpCheck1, timeInSec1, timeString1);
  bool validTime2 = timeStringToSeconds(tmpCheck2, timeInSec2, timeString2);
  bool result     = false;

  bool compareTimes = false;

  if ((validTime1 || validTime2) && (timeInSec1 != -1) && (timeInSec2 != -1))
  {
    // At least one is a time containing ':' separator
    // AND both can be considered a time, so use it as a time and compare seconds.
    compareTimes = true;
    result       = compareIntValues(compare, timeInSec1, timeInSec2);
    tmpCheck1    = timeString1;
    tmpCheck2    = timeString2;
  } else {
    int condAnd = tmpCheck2.indexOf(F(" and "));
    int condOr  = tmpCheck2.indexOf(F(" or "));
    if (condAnd > -1 || condOr > -1) {            // Only parse first condition, rest will be parsed 'later'
      if (condAnd > -1 && (condOr == -1 || condAnd < condOr)) {
        tmpCheck2 = tmpCheck2.substring(0, condAnd);
      } else if (condOr > -1) {
        tmpCheck2 = tmpCheck2.substring(0, condOr);
      }
      tmpCheck2.trim();
    }
    balanceParentheses(tmpCheck1);
    balanceParentheses(tmpCheck2);
    if (isError(Calculate(tmpCheck1, Value1
                          #if FEATURE_STRING_VARIABLES
                          , false // suppress logging specific errors when parsing strings
                          #endif // if FEATURE_STRING_VARIABLES
                         )) ||
        isError(Calculate(tmpCheck2, Value2
                          #if FEATURE_STRING_VARIABLES
                          , false // suppress logging specific errors when parsing strings
                          #endif // if FEATURE_STRING_VARIABLES
                         )))
    {
      #if FEATURE_STRING_VARIABLES
      result = compareStringValues(compare, tmpCheck1, tmpCheck2);
      #else // if FEATURE_STRING_VARIABLES
      return false;
      #endif // if FEATURE_STRING_VARIABLES
    }
    else {
      result = compareDoubleValues(compare, Value1, Value2);
    }
  }

  #ifndef BUILD_NO_DEBUG

  if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
    String log = F("conditionMatch: ");
    log += wrap_String(check, '"');
    log += F(" --> ");

    log += wrap_String(tmpCheck1, '"');
    log += wrap_String(check.substring(posStart, posEnd), ' '); // Compare
    log += wrap_String(tmpCheck2, '"');

    log += F(" --> ");
    log += boolToString(result);
    log += ' ';

    log += '(';
    const bool trimTrailingZeros = true;
    log += compareTimes ? String(timeInSec1) : 
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    doubleToString(Value1, 6, trimTrailingZeros);
#else
    floatToString(Value1, 6, trimTrailingZeros);
#endif
    log += wrap_String(check.substring(posStart, posEnd), ' '); // Compare
    log += compareTimes ? String(timeInSec2) : 
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    doubleToString(Value2, 6, trimTrailingZeros);
#else
    floatToString(Value2, 6, trimTrailingZeros);
#endif    
    log += ')';
    addLogMove(LOG_LEVEL_DEBUG, log);
  }
  #else // ifndef BUILD_NO_DEBUG
  (void)compareTimes; // To avoid compiler warning
  #endif // ifndef BUILD_NO_DEBUG
  return result;
}

/********************************************************************************************\
   Generate rule events based on task refresh
 \*********************************************************************************************/
void createRuleEvents(struct EventStruct *event) {
  if (!Settings.UseRules) {
    return;
  }
  const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex);

  if (!validDeviceIndex(DeviceIndex)) { return; }

  const uint8_t valueCount = getValueCountForTask(event->TaskIndex);
  String taskName = getTaskDeviceName(event->TaskIndex);
  #if FEATURE_STRING_VARIABLES
  String postfix;
  const String search = getDerivedValueSearchAndPostfix(taskName, postfix);
  #endif // if FEATURE_STRING_VARIABLES

  // Small optimization as sensor type string may result in large strings
  // These also only yield a single value, so no need to check for combining task values.
  if (event->getSensorType() == Sensor_VType::SENSOR_TYPE_STRING) {
    size_t expectedSize = 2 + getTaskDeviceName(event->TaskIndex).length();
    expectedSize += Cache.getTaskDeviceValueName(event->TaskIndex, 0).length();
   
    bool appendCompleteStringvalue = false;

    String eventString;
    if (reserve_special(eventString, expectedSize + event->String2.length())) {
      appendCompleteStringvalue = true;
    } else if (!reserve_special(eventString, expectedSize + 24)) {
      // No need to continue as we can't even allocate the event, we probably also cannot process it
      addLog(LOG_LEVEL_ERROR, F("Not enough memory for event"));
      return;
    }
    eventString += taskName;
    eventString += '#';
    eventString += Cache.getTaskDeviceValueName(event->TaskIndex, 0);
    eventString += '=';
    eventString += '`';
    if (appendCompleteStringvalue) {
      eventString += event->String2;
    } else {
      eventString += event->String2.substring(0, 10);
      eventString += F("...");
      eventString += event->String2.substring(event->String2.length() - 10);
    }
    eventString += '`';
    eventQueue.addMove(std::move(eventString));    
  } else if (Settings.CombineTaskValues_SingleEvent(event->TaskIndex)) {
    String eventvalues;
    reserve_special(eventvalues, 32); // Enough for most use cases, prevent lots of memory allocations.

    uint8_t varNr = 0;
    for (; varNr < valueCount; ++varNr) {
      if (varNr != 0) {
        eventvalues += ',';
      }
      eventvalues += formatUserVarNoCheck(event, varNr);
    }
    #if FEATURE_STRING_VARIABLES
    if (Settings.EventAndLogDerivedTaskValues(event->TaskIndex)) {

      auto it = customStringVar.begin();
      while (it != customStringVar.end()) {
        if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
          if (!it->second.isEmpty()) {
            String value(it->second);
            value = parseTemplateAndCalculate(value);
            if (varNr != 0) {
              eventvalues += ',';
            }
            eventvalues += value;
            ++varNr;
          }
        }
        else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
          break;
        }
        ++it;
      }
    }
    #endif // if FEATURE_STRING_VARIABLES
    eventQueue.add(event->TaskIndex, F("All"), eventvalues);
  } else {
    for (uint8_t varNr = 0; varNr < valueCount; varNr++) {
      eventQueue.add(event->TaskIndex, Cache.getTaskDeviceValueName(event->TaskIndex, varNr), formatUserVarNoCheck(event, varNr));
    }
    #if FEATURE_STRING_VARIABLES
    if (Settings.EventAndLogDerivedTaskValues(event->TaskIndex)) {
      taskName.toLowerCase();

      auto it = customStringVar.begin();
      while (it != customStringVar.end()) {
        if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
          String valueName = it->first.substring(search.length(), it->first.indexOf('-'));
          const String vname2 = getDerivedValueName(taskName, valueName);
          if (!vname2.isEmpty()) {
            valueName = vname2;
          }
          if (!it->second.isEmpty()) {
            String value(it->second);
            value = parseTemplateAndCalculate(value);
            eventQueue.add(event->TaskIndex, valueName, value);
          }
        }
        else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
          break;
        }
        ++it;
      }
    }
    #endif // if FEATURE_STRING_VARIABLES
  }
}
