#include "../Helpers/StringParser.h"

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

#include "../Commands/GPIO.h"

#include "../DataStructs/TimingStats.h"

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

#include "../Globals/Cache.h"
#include "../Globals/Plugins_other.h"
#include "../Globals/RulesCalculate.h"
#include "../Globals/RuntimeData.h"

#include "../Helpers/_CPlugin_init.h"
#include "../Helpers/ESPEasy_math.h"
#include "../Helpers/ESPEasy_Storage.h"
#include "../Helpers/ESPEasy_UnitOfMeasure.h"
#include "../Helpers/Misc.h"
#include "../Helpers/Numerical.h"
#include "../Helpers/StringConverter.h"
#include "../Helpers/StringGenerator_GPIO.h"



/********************************************************************************************\
   Parse string template
 \*********************************************************************************************/
bool hasEscapedCharacter(String& str, const char EscapeChar)
{
  const String EscStr = concat(F("\\"), EscapeChar);
  return (str.indexOf(EscStr)>=0);
}

void stripEscapeCharacters(String& str)
{
  const char braces[]     = { '%', '[', ']', '{', '}', '(', ')' };
  constexpr uint8_t nrbraces = NR_ELEMENTS(braces);

  for (uint8_t i = 0; i < nrbraces; ++i) {
    const String s(concat(F("\\"), braces[i]));
    str.replace(s, s.substring(1));
  }
}

#if FEATURE_STRING_VARIABLES
String parseTemplateAndCalculate(String& tmpString) {
  stripEscapeCharacters(tmpString);
  String str = parseTemplate(tmpString);
  ESPEASY_RULES_FLOAT_TYPE result{};
  if (!str.isEmpty() && (str[0] == '=') && !isError(Calculate(str.substring(1), result, true))) {
    # if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    str = doubleToString(result, 6, true);
    # else // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
    str = floatToString(result, 6, true);
    # endif // if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
  }
  return str;
}

uint8_t getDerivedValueCountForTask(taskIndex_t taskIndex) {
  uint8_t derivedVars = 0;
  String postfix;
  const String search = getDerivedValueSearchAndPostfix(getTaskDeviceName(taskIndex), postfix);

  auto it = customStringVar.begin();

  while (it != customStringVar.end()) {
    if (it->first.startsWith(search) && it->first.endsWith(postfix)) {
      ++derivedVars;
    }
    else if (it->first.substring(0, search.length()).compareTo(search) > 0) {
      break;
    }
    ++it;
  }
  return derivedVars;
}

String getDerivedValueSearchAndPostfix(String taskName, String& postfix) {
  taskName.toLowerCase();
  const String search = strformat(F(TASK_VALUE_DERIVED_PREFIX_TEMPLATE), taskName.c_str(), FsP(F("X")));
  postfix = search.substring(search.indexOf('X') + 1);
  return search.substring(0, search.indexOf('X')); // Cut off left of valuename
}

String getDerivedValueNameUomAndVType(String taskName, String valueName, String& uom, String& vType) {
  taskName.toLowerCase();
  valueName.toLowerCase();
  vType = getCustomStringVar(strformat(F(TASK_VALUE_VTYPE_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
  uom   = getCustomStringVar(strformat(F(TASK_VALUE_UOM_PREFIX_TEMPLATE),
                                       taskName.c_str(), valueName.c_str()));
  return  getCustomStringVar(strformat(F(TASK_VALUE_NAME_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
}

String getDerivedValueName(String taskName, String valueName) {
  taskName.toLowerCase();
  valueName.toLowerCase();
  return  getCustomStringVar(strformat(F(TASK_VALUE_NAME_PREFIX_TEMPLATE), 
                                       taskName.c_str(), valueName.c_str()));
}
#endif // if FEATURE_STRING_VARIABLES

String parseTemplate(String& tmpString)
{
  return parseTemplate(tmpString, false);
}

String parseTemplate(String& tmpString, bool useURLencode)
{
  return parseTemplate_padded(tmpString, 0, useURLencode);
}

String parseTemplate_padded(String& tmpString, uint8_t minimal_lineSize)
{
  return parseTemplate_padded(tmpString, minimal_lineSize, false);
}

String parseTemplate_padded(String& tmpString, uint8_t minimal_lineSize, bool useURLencode)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate_padded"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  START_TIMER;

  // Keep current loaded taskSettings to restore at the end.
  const taskIndex_t currentTaskIndex = ExtraTaskSettings.TaskIndex;
  String newString;
  newString.reserve(minimal_lineSize); // Our best guess of the new size.

  if (parseTemplate_CallBack_ptr != nullptr) {
    parseTemplate_CallBack_ptr(tmpString, useURLencode);
  }
  parseSystemVariables(tmpString, useURLencode);

  int startpos = 0;
  int lastStartpos = 0;
  int endpos = 0;
  bool mustReplaceEscapedSquareBracket = false;
  String MaskEscapedBracket;

  if (hasEscapedCharacter(tmpString, '[') || hasEscapedCharacter(tmpString, ']')) {
    // replace the \[ and \] with other characters to mask the escaped square brackets so we can continue parsing.
    // We have to unmask then after we're finished.
    MaskEscapedBracket = static_cast<char>(0x05); // ASCII 0x05 = Enquiry ENQ
    tmpString.replace(F("\\["), MaskEscapedBracket);
    MaskEscapedBracket = static_cast<char>(0x06); // ASCII 0x06 = Acknowledge ACK
    tmpString.replace(F("\\]"), MaskEscapedBracket);
    mustReplaceEscapedSquareBracket = true;
  }

  {
    String deviceName, valueName, format;

    while (findNextDevValNameInString(tmpString, startpos, endpos, deviceName, valueName, format)) {
      // First copy all upto the start of the [...#...] part to be replaced.
      newString += tmpString.substring(lastStartpos, startpos);

      // deviceName is lower case, so we can compare literal string (no need for equalsIgnoreCase)
      const bool devNameEqInt = equals(deviceName, F("int"));
      #if FEATURE_STRING_VARIABLES
      const bool devNameEqStr    = equals(deviceName, F("str"));
      const bool devNameEqLength = equals(deviceName, F("length"));
      #endif // if FEATURE_STRING_VARIABLES
      if (devNameEqInt || equals(deviceName, F("var"))
         #if FEATURE_STRING_VARIABLES
         || devNameEqStr || devNameEqLength
         #endif // if FEATURE_STRING_VARIABLES
         )
      {
        // Address an internal variable either as float or as int
        // For example: Let,10,[VAR#9]
        // For example: Let,10,[INT#bla]

        if (!valueName.isEmpty()) {
         #if FEATURE_STRING_VARIABLES
         if (devNameEqStr) {
           String value(getCustomStringVar(valueName));
           transformValue(
              newString, 
              minimal_lineSize, 
              std::move(value), 
              format, 
              tmpString);
         } else
         if (devNameEqLength) {
           String value(getCustomStringVar(valueName).length());
           transformValue(
              newString, 
              minimal_lineSize, 
              std::move(value), 
              format, 
              tmpString);
         } else
         #endif
         {
          const ESPEASY_RULES_FLOAT_TYPE floatvalue = getCustomFloatVar(valueName);
          unsigned char nr_decimals = maxNrDecimals_fpType(floatvalue);
          bool trimTrailingZeros    = true;

          if (devNameEqInt) {
            nr_decimals = 0;
          } else if (!format.isEmpty())
          {
            // There is some formatting here, so do not throw away decimals
            trimTrailingZeros = false;
          }
          #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
          String value = doubleToString(floatvalue, nr_decimals, trimTrailingZeros);
          #else
          String value = floatToString(floatvalue, nr_decimals, trimTrailingZeros);
          #endif
          transformValue(
            newString, 
            minimal_lineSize, 
            std::move(value), 
            format, 
            tmpString);
         }
        }
      }
      else if (equals(deviceName, F("plugin")))
      {
        // Handle a plugin request.
        // For example: "[Plugin#GPIO#Pinstate#N]"
        // The command is stored in valueName & format
        String command = strformat(F("%s#%s"), valueName.c_str(), format.c_str());
        command.replace('#', ',');

        if (getGPIOPinStateValues(command)) {
          newString += command;
        }
  /* @giig1967g
        if (PluginCall(PLUGIN_REQUEST, 0, command))
        {
          // Do not call transformValue here.
          // The "format" is not empty so must not call the formatter function.
          newString += command;
        }
  */
      }
      else
      {
        // Address a value from a plugin.
        // For example: "[bme#temp]"
        // If value name is unknown, run a PLUGIN_GET_CONFIG_VALUE command.
        // For example: "[<taskname>#getLevel]"
        taskIndex_t taskIndex = findTaskIndexByName(deviceName, true); // Check for enabled/disabled is done separately

        if (validTaskIndex(taskIndex)) {
          bool isHandled = false;
          if (Settings.TaskDeviceEnabled[taskIndex]) {
            uint8_t valueNr = findDeviceValueIndexByName(valueName, taskIndex);

            if (valueNr != VARS_PER_TASK) {
              // here we know the task and value, so find the uservar
              // Try to format and transform the values
              bool   isvalid;
              String value = formatUserVar(taskIndex, valueNr, isvalid);

              if (isvalid) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString
                               #if FEATURE_STRING_VARIABLES
                               , taskIndex, valueNr, valueName // for handling $ format option
                               #endif // if FEATURE_STRING_VARIABLES
                              );
                isHandled = true;
              }
            } else {
              // try if this is a get config request
              struct EventStruct TempEvent(taskIndex);
              String tmpName = valueName;

              if (PluginCall(PLUGIN_GET_CONFIG_VALUE, &TempEvent, tmpName))
              {
                transformValue(newString, minimal_lineSize, std::move(tmpName), format, tmpString
                               #if FEATURE_STRING_VARIABLES
                               , taskIndex, INVALID_TASKVAR_INDEX, valueName // for handling $ format option
                               #endif // if FEATURE_STRING_VARIABLES
                              );
                isHandled = true;
              }
            }
          }
          if (!isHandled && valueName.startsWith(F("settings."))) {  // Task settings values
            String value;
            if (valueName.endsWith(F(".enabled"))) {           // Task state
              value = Settings.TaskDeviceEnabled[taskIndex] ? '1' : '0';
            } else if (valueName.endsWith(F(".interval"))) {   // Task interval
              value = Settings.TaskDeviceTimer[taskIndex];
            } else if (valueName.endsWith(F(".valuecount"))) { // Task value count
              value = getValueCountForTask(taskIndex);
            } else if ((valueName.indexOf(F(".controller")) == 8) && valueName.length() >= 20) { // Task controller values
              String ctrl = valueName.substring(19, 20);
              int32_t ctrlNr = 0;
              if (validIntFromString(ctrl, ctrlNr) && (ctrlNr >= 1) && (ctrlNr <= CONTROLLER_MAX) && 
                  Settings.ControllerEnabled[ctrlNr - 1]) { // Controller nr. valid and enabled
                if (valueName.endsWith(F(".enabled"))) {    // Task-controller enabled
                  value = Settings.TaskDeviceSendData[ctrlNr - 1][taskIndex];
                } else if (valueName.endsWith(F(".idx"))) { // Task-controller idx value
                  protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(ctrlNr - 1);

                  if (validProtocolIndex(ProtocolIndex) && 
                      getProtocolStruct(ProtocolIndex).usesID && (Settings.Protocol[ctrlNr - 1] != 0)) {
                    value = Settings.TaskDeviceID[ctrlNr - 1][taskIndex];
                  }
                }
              }
            }
            if (!value.isEmpty()) {
              transformValue(newString, minimal_lineSize, std::move(value), format, tmpString
                             #if FEATURE_STRING_VARIABLES
                             , taskIndex, INVALID_TASKVAR_INDEX, valueName // for handling $ format option
                             #endif // if FEATURE_STRING_VARIABLES
                            );
              // isHandled = true;
            }
          }
          #if FEATURE_STRING_VARIABLES
          if (!isHandled && Settings.TaskDeviceEnabled[taskIndex]) {
            String value;
            const String valName = parseString(valueName, 1);
            String derived = getCustomStringVar(strformat(F(TASK_VALUE_DERIVED_PREFIX_TEMPLATE), deviceName.c_str(), valName.c_str()));
            if (!derived.isEmpty()) {
              value = parseTemplateAndCalculate(derived);
              if (!value.isEmpty()) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString,
                               taskIndex, INVALID_TASKVAR_INDEX, valName // for handling $ format option
                              );
                isHandled = true;
              }
            }
          }
          #endif // if FEATURE_STRING_VARIABLES

          #if FEATURE_TASKVALUE_ATTRIBUTES
          if (!isHandled && valueName.indexOf('.') > -1) { // TaskValue specific attributes
            const String valName = parseString(valueName, 1, '.');
            const String command = parseString(valueName, 2, '.');
            String value;

            if (!command.isEmpty()) {
              const uint8_t valueCount = getValueCountForTask(taskIndex);

              for (taskVarIndex_t i = 0; i < valueCount; i++) {
                if (valName.equalsIgnoreCase(Cache.getTaskDeviceValueName(taskIndex, i))) {
                  #if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                  if (equals(command, F("uom"))) { // Fetch UnitOfMeasure
                    value = toUnitOfMeasureName(Cache.getTaskVarUnitOfMeasure(taskIndex, i));
                    isHandled = true; // Empty is a valid result
                    break;
                  } else
                  #endif // if FEATURE_TASKVALUE_UNIT_OF_MEASURE
                  if (equals(command, F("decimals"))) { // Fetch decimals
                    value = Cache.getTaskDeviceValueDecimals(taskIndex, i);
                    break;
                  } else
                  if (equals(command, F("hasformula"))) { // Fetch formula status
                    value = Cache.hasFormula(taskIndex, i);
                    break;
                  #if FEATURE_PLUGIN_STATS
                  } else
                  if (equals(command, F("statsenabled"))) { // Fetch Stats enabled
                    value = Cache.enabledPluginStats(taskIndex, i);
                    break;
                  #endif // if FEATURE_PLUGIN_STATS
                  }
                }
              }
              if (!value.isEmpty() || isHandled) {
                transformValue(newString, minimal_lineSize, std::move(value), format, tmpString);
                // isHandled = true;
              }
            }
          }
          #endif // if FEATURE_TASKVALUE_ATTRIBUTES

          #if FEATURE_STRING_VARIABLES
          if (!isHandled && valueName.indexOf('.') > -1) {
            String value;
            const String fullValueName = parseString(valueName, 1);
            const String valName       = parseString(fullValueName, 1, '.');
            const String command       = parseString(fullValueName, 2, '.');
            if (equals(command, F("uom"))) { // Fetch UnitOfMeasure
              value = getCustomStringVar(strformat(F(TASK_VALUE_UOM_PREFIX_TEMPLATE), deviceName.c_str(), valName.c_str()));
            }
            if (!value.isEmpty()) {
              transformValue(newString, minimal_lineSize, std::move(value), format, tmpString,
                              taskIndex, INVALID_TASKVAR_INDEX, valName
                            );
              // isHandled = true;
            }
          }
          #endif // if FEATURE_STRING_VARIABLES
        }
      }


      // Conversion is done (or impossible) for the found "[...#...]"
      // Continue with the next one.
      lastStartpos = endpos + 1;
      startpos     = endpos + 1;

      // This may have taken some time, so call delay()
      delay(0);
    }
  }

  // Copy the rest of the string (or all if no replacements were done)
  newString += tmpString.substring(lastStartpos);
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  if (mustReplaceEscapedSquareBracket) {
    // We now have to check if we did mask some escaped square bracket and unmask them.
    // Let's hope we don't mess up any Unicode here.
    MaskEscapedBracket = static_cast<char>(0x05); // ASCII 0x05 = Enquiry ENQ
    newString.replace(MaskEscapedBracket, F("\\["));
    MaskEscapedBracket = static_cast<char>(0x06); // ASCII 0x06 = Acknowledge ACK
    newString.replace(MaskEscapedBracket, F("\\]"));
  }

  // Restore previous loaded taskSettings
  if (validTaskIndex(currentTaskIndex))
  {
    LoadTaskSettings(currentTaskIndex);
  }

  parseStandardConversions(newString, useURLencode);

  // process other markups as well
  parse_string_commands(newString);

  // padding spaces
  while (newString.length() < minimal_lineSize) {
    newString += ' ';
  }

  STOP_TIMER(PARSE_TEMPLATE_PADDED);
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseTemplate3"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
  return newString;
}

/********************************************************************************************\
   Transform values
 \*********************************************************************************************/

bool isTransformString(char c, bool logicVal, String& strValue)
{
  const __FlashStringHelper * value = F("");
  char value_ch = '\0';
  switch (c) {
    case 'O':
      value = logicVal == 0 ? F("OFF") : F(" ON"); // (equivalent to XOR operator)
      break;
    case 'C':
      value = logicVal == 0 ? F("CLOSE") : F(" OPEN");
      break;
    case 'c':
      value = logicVal == 0 ? F("CLOSED") : F("  OPEN");
      break;
    case 'M':
      value = logicVal == 0 ? F("AUTO") : F(" MAN");
      break;
    case 'm':
      value_ch = logicVal == 0 ? 'A' : 'M';
      break;
    case 'H':
      value = logicVal == 0 ? F("COLD") : F(" HOT");
      break;
    case 'U':
      value = logicVal == 0 ? F("DOWN") : F("  UP");
      break;
    case 'u':
      value_ch = logicVal == 0 ? 'D' : 'U';
      break;
    case 'Y':
      value = logicVal == 0 ? F(" NO") : F("YES");
      break;
    case 'y':
      value_ch = logicVal == 0 ? 'N' : 'Y';
      break;
    case 'X':
      value_ch = logicVal == 0 ? 'O' : 'X';
      break;
    case 'I':
      value = logicVal == 0 ? F("OUT") : F(" IN");
      break;
    case 'L':
      value = logicVal == 0 ? F(" LEFT") : F("RIGHT");
      break;
    case 'l':
      value_ch = logicVal == 0 ? 'L' : 'R';
      break;
    case 'Z': // return "0" or "1"
      value_ch = logicVal == 0 ? '0' : '1';
      break;
    default:
      return false;
  }
  if (value_ch != '\0') {
    strValue = value_ch;
  } else {
    strValue = value;
  }
  return true;
}


// Syntax: [task#value#transformation#justification]
// valueFormat="transformation#justification"
void transformValue(
  String      & newString,
  uint8_t       lineSize,
  String        value,
  String      & valueFormat,
  const String& tmpString
  #if FEATURE_STRING_VARIABLES
  , taskIndex_t taskIndex
  , uint8_t     valueIndex
  , String      valueName
  #endif // if FEATURE_STRING_VARIABLES
  )
{
  // FIXME TD-er: This function does append to newString and uses its length to perform right aling.
  // Is this the way it is intended to use?
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("transformValue"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  // start changes by giig1967g - 2018-04-20
  // Syntax: [task#value#transformation#justification]
  // valueFormat="transformation#justification"
  if (valueFormat.length() > 0) // do the checks only if a Format is defined to optimize loop
  {
    String valueJust;

    int hashtagIndex = valueFormat.indexOf('#');

    if (hashtagIndex >= 0)
    {
      valueJust   = valueFormat.substring(hashtagIndex + 1); // Justification part
      valueFormat = valueFormat.substring(0, hashtagIndex);  // Transformation part
    }

    // valueFormat="transformation"
    // valueJust="justification"
    if (valueFormat.length() > 0) // do the checks only if a Format is defined to optimize loop
    {
      int logicVal    = 0;
      ESPEASY_RULES_FLOAT_TYPE valFloat{};

      if (validDoubleFromString(value, valFloat))
      {
        // to be used for binary values (0 or 1)
        logicVal = lround(static_cast<float>(valFloat)) == 0 ? 0 : 1;
      } else {
        if (value.length() > 0) {
          logicVal = 1;
        }
      }
      String tempValueFormat = valueFormat;
      {
        const int invertedIndex = tempValueFormat.indexOf('!');

        if (invertedIndex != -1) {
          // We must invert the value.
          logicVal = (logicVal == 0) ? 1 : 0;

          // Remove the '!' from the string.
          tempValueFormat.remove(invertedIndex, 1);
        }
      }

      const int  rightJustifyIndex = tempValueFormat.indexOf('R');
      const bool rightJustify      = rightJustifyIndex >= 0 ? 1 : 0;

      if (rightJustify) {
        tempValueFormat.remove(rightJustifyIndex, 1);
      }

      const int tempValueFormatLength = tempValueFormat.length();

      // Check Transformation syntax
      if (tempValueFormatLength > 0)
      {
        if (!isTransformString(tempValueFormat[0], logicVal, value)) {
          switch (tempValueFormat[0])
          {
            case 'V': // value = value without transformations
              break;
            case 'p': // Password hide using asterisks or custom character: pc
            {
              char maskChar = '*';

              if (tempValueFormatLength > 1)
              {
                maskChar = tempValueFormat[1];
              }

              if (equals(value, '0')) {
                free_string(value);
              } else {
                const int valueLength = value.length();

                for (int i = 0; i < valueLength; i++) {
                  value[i] = maskChar;
                }
              }
              break;
            }
            case 'D': // Dx.y min 'x' digits zero filled & 'y' decimal fixed digits
            case 'd': // like above but with spaces padding
            {
              int x = 0;
              int y = 0;

              switch (tempValueFormatLength)
              {
                case 2: // Dx

                  if (isDigit(tempValueFormat[1]))
                  {
                    x = static_cast<int>(tempValueFormat[1]) - '0';
                  }
                  break;
                case 3: // D.y

                  if ((tempValueFormat[1] == '.') && isDigit(tempValueFormat[2]))
                  {
                    y = static_cast<int>(tempValueFormat[2]) - '0';
                  }
                  break;
                case 4: // Dx.y

                  if (isDigit(tempValueFormat[1]) && (tempValueFormat[2] == '.') && isDigit(tempValueFormat[3]))
                  {
                    x = static_cast<int>(tempValueFormat[1]) - '0';
                    y = static_cast<int>(tempValueFormat[3]) - '0';
                  }
                  break;
                case 1:  // D
                default: // any other combination x=0; y=0;
                  break;
              }
              bool trimTrailingZeros = false;
#if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = doubleToString(valFloat, y, trimTrailingZeros);
#else
              value = floatToString(valFloat, y, trimTrailingZeros);
#endif
              int indexDot = value.indexOf('.');

              if (indexDot == -1) {
                indexDot = value.length();
              }

              for (uint8_t f = 0; f < (x - indexDot); f++) {
                value = (tempValueFormat[0] == 'd' ? ' ' : '0') + value;
              }
              break;
            }
            case 'F': // FLOOR (round down)
            #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = static_cast<int>(floor(valFloat));
            #else
              value = static_cast<int>(floorf(valFloat));
            #endif
              break;
            case 'E': // CEILING (round up)
            #if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE
              value = static_cast<int>(ceil(valFloat));
            #else
              value = static_cast<int>(ceilf(valFloat));
            #endif
              break;
            #if FEATURE_STRING_VARIABLES
            case TASK_VALUE_PRESENTATION_PREFIX_CHAR: // '$' Apply presentation format
            {
              if (validTaskIndex(taskIndex) && (validTaskVarIndex(valueIndex) || !valueName.isEmpty())) {
                const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);
                bool hasPresentation = false;
                EventStruct TempEvent(taskIndex);
                const String presentation = formatUserVarForPresentation(&TempEvent, valueIndex, hasPresentation, value, DeviceIndex, valueName);
                if (hasPresentation) {
                  value = presentation;
                }
              }
              break;
            }
            #endif // if FEATURE_STRING_VARIABLES
            default:
              value = F("ERR");
              break;
          }
        }

        // Check Justification syntax
        const int valueJustLength = valueJust.length();

        if (valueJustLength > 0) // do the checks only if a Justification is defined to optimize loop
        {
          value.trim();          // remove right justification spaces for backward compatibility

          switch (valueJust[0])
          {
            case 'P': // Prefix Fill with n spaces: Pn

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]))                          // Check Pn where n is between 0 and 9
                {
                  int filler = valueJust[1] - value.length() - '0'; // char '0' = 48; char '9' = 58

                  for (uint8_t f = 0; f < filler; f++) {
                    newString += ' ';
                  }
                }
              }
              break;
            case 'S': // Suffix Fill with n spaces: Sn

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]))                          // Check Sn where n is between 0 and 9
                {
                  int filler = valueJust[1] - value.length() - '0'; // 48

                  for (uint8_t f = 0; f < filler; f++) {
                    value += ' ';
                  }
                }
              }
              break;
            case 'L': // left part of the string

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1])) // Check n where n is between 0 and 9
                {
                  value = value.substring(0, static_cast<int>(valueJust[1]) - '0');
                }
              }
              break;
            case 'R': // Right part of the string

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1])) // Check n where n is between 0 and 9
                {
                  value = value.substring(std::max(0, static_cast<int>(value.length()) - (static_cast<int>(valueJust[1]) - '0')));
                }
              }
              break;
            case 'U': // Substring Ux.y where x=firstChar and y=number of characters

              if (valueJustLength > 1)
              {
                if (isDigit(valueJust[1]) && (valueJust[2] == '.') && isDigit(valueJust[3]) && (valueJust[1] > '0') && (valueJust[3] > '0'))
                {
                  value = value.substring(std::min(static_cast<int>(value.length()), static_cast<int>(valueJust[1]) - '0' - 1),
                                          static_cast<int>(valueJust[1]) - '0' - 1 + static_cast<int>(valueJust[3]) - '0');
                }
                else
                {
                  newString += F("ERR");
                }
              }
              break;
            case 'C': // Capitalize First Word-Character value (space/period are checked)

              if (value.length() > 0) {
                value.toLowerCase();
                bool nextCapital = true;

                for (uint8_t i = 0; i < value.length(); i++) {
                  if (nextCapital) {
                    value[i] = toupper(value[i]);
                  }
                  nextCapital = (value[i] == ' ' || value[i] == '.'); // Very simple, capitalize-first-after-space/period
                }
              }
              break;
            case 'u': // Uppercase
              value.toUpperCase();
              break;
            case 'l': // Lowercase
              value.toLowerCase();
              break;
            default:
              newString += F("ERR");
              break;
          }
        }
      }

      if (rightJustify)
      {
        int filler = lineSize - newString.length() - value.length() - tmpString.length();

        for (uint8_t f = 0; f < filler; f++) {
          newString += ' ';
        }
      }
      {
#ifndef BUILD_NO_DEBUG

        if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
          String logFormatted = F("DEBUG: Formatted String='");
          logFormatted += newString;
          logFormatted += value;
          logFormatted += '\'';
          addLogMove(LOG_LEVEL_DEBUG, logFormatted);
        }
#endif // ifndef BUILD_NO_DEBUG
      }
    }
  }

  // end of changes by giig1967g - 2018-04-18

  newString += value;
  {
#ifndef BUILD_NO_DEBUG

    if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) {
      String logParsed = F("DEBUG DEV: Parsed String='");
      logParsed += newString;
      logParsed += '\'';
      addLogMove(LOG_LEVEL_DEBUG_DEV, logParsed);
    }
#endif // ifndef BUILD_NO_DEBUG
  }
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("transformValue2"));
  #endif // ifndef BUILD_NO_RAM_TRACKER
}

// Find the first (enabled) task with given name
// Return INVALID_TASK_INDEX when not found, else return taskIndex
taskIndex_t findTaskIndexByName(String deviceName, bool allowDisabled)
{
  deviceName.toLowerCase();
  // cache this, since LoadTaskSettings does take some time.
  auto result = Cache.taskIndexName.find(deviceName);

  if (result != Cache.taskIndexName.end()) {
    return result->second;
  }

  for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++)
  {
    if (Settings.TaskDeviceEnabled[taskIndex] || allowDisabled) {
      String taskDeviceName = getTaskDeviceName(taskIndex);

      if (!taskDeviceName.isEmpty())
      {
        // Use entered taskDeviceName can have any case, so compare case insensitive.
        if (deviceName.equalsIgnoreCase(taskDeviceName))
        {
          Cache.taskIndexName.emplace(
            std::make_pair(
              std::move(deviceName), 
              taskIndex));
          return taskIndex;
        }
      }
    }
  }
  return INVALID_TASK_INDEX;
}

// Find the first device value index of a taskIndex.
// Return VARS_PER_TASK if none found.
uint8_t findDeviceValueIndexByName(const String& valueName, taskIndex_t taskIndex)
{
  const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex);

  if (!validDeviceIndex(deviceIndex)) { return VARS_PER_TASK; }

  #ifdef USE_SECOND_HEAP
  HeapSelectDram ephemeral;
  #endif


  // cache this, since LoadTaskSettings does take some time.
  // We need to use a cache search key including the taskIndex,
  // to allow several tasks to have the same value names.
  String cache_valueName = strformat(
    F("%s#%d"),                   // The '#' cannot exist in a value name, use it in the cache key.
    valueName.c_str(),
    static_cast<int>(taskIndex));
  cache_valueName.toLowerCase();  // No need to store multiple versions of the same entry with only different case.
  
  auto result = Cache.taskIndexValueName.find(cache_valueName);

  if (result != Cache.taskIndexValueName.end()) {
    return result->second;
  }
  const uint8_t valCount = getValueCountForTask(taskIndex);

  for (uint8_t valueNr = 0; valueNr < valCount; valueNr++)
  {
    // Check case insensitive, since the user entered value name can have any case.
    if (valueName.equalsIgnoreCase(Cache.getTaskDeviceValueName(taskIndex, valueNr)))
    {
      Cache.taskIndexValueName.emplace(
        std::make_pair(
          std::move(cache_valueName), 
          valueNr));
      return valueNr;
    }
  }
  return VARS_PER_TASK;
}

// Find positions of [...#...] in the given string.
// Only update pos values on success.
// Return true when found.
bool findNextValMarkInString(const String& input, int& startpos, int& hashpos, int& endpos) {
  int tmpStartpos = input.indexOf('[', startpos);

  if (tmpStartpos == -1) { return false; }
  const int tmpHashpos = input.indexOf('#', tmpStartpos);

  if (tmpHashpos == -1) { return false; }

  // We found a hash position, check if there is another '[' inbetween.
  for (int i = tmpStartpos; i < tmpHashpos; ++i) {
    if (input[i] == '[') {
      tmpStartpos = i;
    }
  }

  const int tmpEndpos = input.indexOf(']', tmpStartpos);

  if (tmpEndpos == -1) { return false; }

  if (tmpHashpos >= tmpEndpos) {
    return false;
  }

  hashpos  = tmpHashpos;
  startpos = tmpStartpos;
  endpos   = tmpEndpos;
  return true;
}

// Find [deviceName#valueName] or [deviceName#valueName#format]
// DeviceName and valueName will be returned in lower case.
// Format may contain case sensitive formatting syntax.
bool findNextDevValNameInString(const String& input, int& startpos, int& endpos, String& deviceName, String& valueName, String& format) {
  int hashpos;

  if (!findNextValMarkInString(input, startpos, hashpos, endpos)) { return false; }

  move_special(deviceName, input.substring(startpos + 1, hashpos));
  move_special(valueName , input.substring(hashpos + 1, endpos));
  hashpos = valueName.indexOf('#');

  if (hashpos != -1) {
    // Found an extra '#' in the valueName, will split valueName and format.
    move_special(format,    valueName.substring(hashpos + 1));
    move_special(valueName, valueName.substring(0, hashpos));
  } else {
    free_string(format);
  }
  deviceName.toLowerCase();
  valueName.toLowerCase();
  return true;
}

/********************************************************************************************\
   Check to see if a given argument is a valid taskIndex (argc = 0 => command)
 \*********************************************************************************************/
taskIndex_t parseCommandArgumentTaskIndex(const String& string, unsigned int argc)
{
  taskIndex_t taskIndex = INVALID_TASK_INDEX;
  const int   ti        = parseCommandArgumentInt(string, argc);

  if (ti > 0) {
    // Task Index used as argument in commands start at 1.
    taskIndex = static_cast<taskIndex_t>(ti - 1);
  }
  return taskIndex;
}

/********************************************************************************************\
   Get int from command argument (argc = 0 => command)
 \*********************************************************************************************/
int parseCommandArgumentInt(const String& string, unsigned int argc,
                            int errorValue)
{
  int value = 0;

  if (argc > 0) {
    // No need to check for the command (argc == 0)
    String TmpStr;

    if (GetArgv(string.c_str(), TmpStr, argc + 1)) {
      value = CalculateParam(TmpStr, errorValue);
    }
  }
  return value;
}

/********************************************************************************************\
   Parse a command string to event struct
 \*********************************************************************************************/
void parseCommandString(struct EventStruct *event, const String& string)
{
  #ifndef BUILD_NO_RAM_TRACKER
  checkRAM(F("parseCommandString"));
  #endif // ifndef BUILD_NO_RAM_TRACKER

  for (uint8_t i = 0; i < 5; ++i) {
    event->ParN[i] = parseCommandArgumentInt(string, i + 1);
  }
}
