#include "_Plugin_Helper.h"
#ifdef USES_P087

// #######################################################################################################
// #################### Plugin 087 Serial Proxy ##########################################################
// #######################################################################################################
//
// Interact with a device connected to serial
// Allows to redirect data to a controller
//

/**
 * Changelog:
 * 2025-12-22 tonhuisman: Add support for receiving binary data, fixed length receive and sending events with hex data
 *                        Moved most #define variables to P087_data_struct.h
 * 2025-01-12 tonhuisman: Add support for MQTT AutoDiscovery (not supported for Serial Proxy)
 * 2024-02-27 tonhuisman: Always process the regular expression like 'Global Match' to enable retrieving the available values
 * 2024-02-26 tonhuisman: Apply log-string and other code optimizations
 * 2024-02-25 tonhuisman: Add command serialproxy_test,<testdata> to test as if serial data was received
 *                        Add Get Config Value support for retrieving the last regex-parsed data:
 *                        - By group: [<taskname>#group,<groupnr>] (groupnr is 0-base!)
 *                        - By name: [<taskname>#next,<data>] if the <data> is found, the next group-data is returned
 * 2023-03-25 tonhuisman: Change serialproxy_writemix to handle 0x00 also, by implementing parseHexTextData()
 * 2023-03-22 tonhuisman: Add command serialproxy_writemix to handle mixed hex characters and text to send
 *                        using parseHexTextString()
 *                        Format source using Uncrustify
 * 2022-07-08 tonhuisman: Allow baudrate lowest value to 300 (from 2400)
 *                        Don't trim off pre/post white-space from string to send
 * 2022-07-07 tonhuisman: Add selection for serial protocol configuration (databits, parity, nr. of stopbits)
 * 2022-07 First recorded changelog
 **/


# include "src/PluginStructs/P087_data_struct.h"

# include <Regexp.h>

# define PLUGIN_087
# define PLUGIN_ID_087           87
# define PLUGIN_NAME_087         "Communication - Serial Proxy"


// Plugin settings:
// Validate:
// - [0..9]
// - "+", "-", "."
// - [A..Z]
// - [a..z]
// - ASCII 32 - 217
// Sentence start:  char
// Sentence end:  CR/CRLF/LF/char
// Max length sentence: 1k max
// Interpret as:
// - Float
// - int
// - String
// Init string (incl parsing CRLF like characters)
// Timeout between sentences.


boolean Plugin_087(uint8_t function, struct EventStruct *event, String& string) {
  boolean success = false;

  switch (function) {
    case PLUGIN_DEVICE_ADD: {
      auto& dev = Device[++deviceCount];
      dev.Number         = PLUGIN_ID_087;
      dev.Type           = DEVICE_TYPE_SERIAL;
      dev.VType          = Sensor_VType::SENSOR_TYPE_STRING;
      dev.ValueCount     = 1;
      dev.SendDataOption = true;
      dev.TimerOption    = true;

      // FIXME TD-er: Not sure if access to any existing task data is needed when saving
      dev.ExitTaskBeforeSave = false;
      break;
    }

    case PLUGIN_GET_DEVICENAME: {
      string = F(PLUGIN_NAME_087);
      break;
    }

    case PLUGIN_GET_DEVICEVALUENAMES: {
      for (uint8_t i = 0; i < VARS_PER_TASK; ++i) {
        if (i < P087_NR_OUTPUT_VALUES) {
          const uint8_t pconfigIndex = i + P087_QUERY1_CONFIG_POS;
          uint8_t choice             = PCONFIG(pconfigIndex);
          ExtraTaskSettings.setTaskDeviceValueName(i, Plugin_087_valuename(choice, false));
        } else {
          ExtraTaskSettings.clearTaskDeviceValueName(i);
        }
      }
      break;
    }

    # if FEATURE_MQTT_DISCOVER
    case PLUGIN_GET_DISCOVERY_VTYPES:
    {
      event->Par1 = static_cast<int>(Sensor_VType::SENSOR_TYPE_NONE); // Not yet supported
      success     = true;
      break;
    }
    # endif // if FEATURE_MQTT_DISCOVER

    case PLUGIN_GET_DEVICEGPIONAMES: {
      serialHelper_getGpioNames(event, false, true); // TX optional
      break;
    }

    case PLUGIN_WEBFORM_SHOW_VALUES:
    {
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if ((nullptr != P087_data) && P087_data->isInitialized()) {
        uint32_t success, error, length_last;
        P087_data->getSentencesReceived(success, error, length_last);
        uint8_t varNr = VARS_PER_TASK;
        pluginWebformShowValue(event->TaskIndex, varNr++, F("Success"),     String(success));
        pluginWebformShowValue(event->TaskIndex, varNr++, F("Error"),       String(error));
        pluginWebformShowValue(event->TaskIndex, varNr++, F("Length Last"), String(length_last), true);

        // success = true;
      }
      break;
    }

    case PLUGIN_SET_DEFAULTS:
    {
      P087_BAUDRATE = P087_DEFAULT_BAUDRATE;

      success = true;
      break;
    }

    case PLUGIN_WEBFORM_SHOW_CONFIG:
    {
      string += serialHelper_getSerialTypeLabel(event);
      success = true;
      break;
    }

    case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS:
    {
      addFormNumericBox(F("Baudrate"), P087_BAUDRATE_LABEL, P087_BAUDRATE, 300, 115200);
      addUnit(F("baud"));
      const uint8_t serialConfChoice = serialHelper_convertOldSerialConfig(P087_SERIAL_CONFIG);
      serialHelper_serialconfig_webformLoad(event, serialConfChoice);
      break;
    }

    case PLUGIN_WEBFORM_LOAD: {
      addFormSubHeader(F("Filtering"));
      P087_html_show_matchForms(event);

      addFormSubHeader(F("Statistics"));
      P087_html_show_stats(event);

      # ifndef LIMIT_BUILD_SIZE
      addFormSubHeader(F("Data options"));
      addFormCheckBox(F("Receive binary data"), P087_READ_BIN_LABEL, P087_CONFIG_GET_READ_BIN);
      addFormNumericBox(F("Fixed length input data"), P087_FIXED_LENGTH_LABEL, P087_CONFIG_GET_FIXED_LENGTH, 0, 255);
      addUnit(F("0 = off, max. 255"));
      addFormCheckBox(F("Event with hex. data (no prefix)"), P087_EVENT_HEX_LABEL, P087_CONFIG_GET_EVENT_HEX);
      # endif // ifndef LIMIT_BUILD_SIZE

      success = true;
      break;
    }

    case PLUGIN_WEBFORM_SAVE: {
      P087_BAUDRATE      = getFormItemInt(P087_BAUDRATE_LABEL);
      P087_SERIAL_CONFIG = serialHelper_serialconfig_webformSave();
      # ifndef LIMIT_BUILD_SIZE
      P087_CONFIG_SET_READ_BIN(isFormItemChecked(P087_READ_BIN_LABEL));
      P087_CONFIG_SET_FIXED_LENGTH(getFormItemInt(P087_FIXED_LENGTH_LABEL));
      P087_CONFIG_SET_EVENT_HEX(isFormItemChecked(P087_EVENT_HEX_LABEL));
      # endif // ifndef LIMIT_BUILD_SIZE

      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if (nullptr != P087_data) {
        for (uint8_t varNr = 0; varNr < P87_Nlines; ++varNr)
        {
          P087_data->setLine(varNr, webArg(getPluginCustomArgName(varNr)));
        }

        addHtmlError(SaveCustomTaskSettings(event->TaskIndex, P087_data->_lines, P87_Nlines, 0));
        success = true;
      }

      break;
    }

    case PLUGIN_INIT: {
      const int16_t serial_rx      = CONFIG_PIN1;
      const int16_t serial_tx      = CONFIG_PIN2;
      const ESPEasySerialPort port = static_cast<ESPEasySerialPort>(CONFIG_PORT);
      initPluginTaskData(event->TaskIndex, new (std::nothrow) P087_data_struct());
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if (nullptr == P087_data) {
        return success;
      }

      if (P087_data->init(port, serial_rx, serial_tx, P087_BAUDRATE, static_cast<uint8_t>(P087_SERIAL_CONFIG))) {
        LoadCustomTaskSettings(event->TaskIndex, P087_data->_lines, P87_Nlines, 0);
        P087_data->post_init();
        # ifndef LIMIT_BUILD_SIZE
        P087_data->setHandleBinary(P087_CONFIG_GET_READ_BIN);
        P087_data->setEventAsHex(P087_CONFIG_GET_EVENT_HEX);

        if (P087_CONFIG_GET_FIXED_LENGTH > 0) {
          P087_data->setMaxLength(P087_CONFIG_GET_FIXED_LENGTH);
        }
        P087_data->setFixedLength(P087_CONFIG_GET_FIXED_LENGTH);
        # endif // ifndef LIMIT_BUILD_SIZE
        success = true;
        serialHelper_log_GpioDescription(port, serial_rx, serial_tx);
      } else {
        clearPluginTaskData(event->TaskIndex);
      }
      break;
    }

    case PLUGIN_FIFTY_PER_SECOND: {
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if ((nullptr != P087_data) && P087_data->loop()) {
        Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
        delay(0); // Processing a full sentence may take a while, run some
                  // background tasks.
      }
      success = true;
      break;
    }

    case PLUGIN_READ: {
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if ((nullptr != P087_data) && P087_data->getSentence(event->String2)) {
        if (Plugin_087_match_all(event->TaskIndex, event->String2)) {
          //          sendData(event);
          # ifndef LIMIT_BUILD_SIZE

          if (P087_data->isEventAsHex() && !event->String2.isEmpty()) { // Convert to hex without prefix
            event->String2 = formatToHex_array(reinterpret_cast<const uint8_t *>(&event->String2[0]), event->String2.length());
          }
          # endif // ifndef LIMIT_BUILD_SIZE
          # ifndef BUILD_NO_DEBUG
          addLog(LOG_LEVEL_DEBUG, event->String2);
          # endif // ifndef BUILD_NO_DEBUG
          success = true;
        }
      }

      break;
    }

    case PLUGIN_WRITE: {
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if (nullptr != P087_data) {
        const String cmd = parseString(string, 1);

        if (equals(cmd, F("serialproxy_write"))) {
          String param1 = parseStringKeepCaseNoTrim(string, 2); // Don't trim off white-space
          parseSystemVariables(param1, false);                  // FIXME tonhuisman: Doesn't seem to be needed?
          P087_data->sendString(param1);
          addLogMove(LOG_LEVEL_INFO, param1);                   // FIXME tonhuisman: Should we always want to write to the log?
          success = true;
        } else
        if (equals(cmd, F("serialproxy_writemix"))) {
          std::vector<uint8_t> param1 = parseHexTextData(string);

          if (param1.size()) {
            P087_data->sendData(&param1[0], param1.size());
          }
          success = true;
        } else
        if (equals(cmd, F("serialproxy_test"))) { // Test-parse data as if received via serial
          const String param1 = parseStringKeepCaseNoTrim(string, 2);

          if (!param1.isEmpty()) {
            P087_data->setLastSentence(param1);
            Scheduler.schedule_task_device_timer(event->TaskIndex, millis() + 10);
            delay(0); // Processing a full sentence may take a while, run some background tasks.
          }
          success = true;
        }
      }

      break;
    }

    case PLUGIN_GET_CONFIG_VALUE:
    {
      P087_data_struct *P087_data =
        static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

      if (nullptr != P087_data) {
        success = P087_data->plugin_get_config_value(event, string);
      }
      break;
    }
  }
  return success;
}

bool Plugin_087_match_all(taskIndex_t taskIndex, String& received)
{
  P087_data_struct *P087_data =
    static_cast<P087_data_struct *>(getPluginTaskData(taskIndex));

  if ((nullptr == P087_data)) {
    return false;
  }


  if (P087_data->disableFilterWindowActive()) {
    addLog(LOG_LEVEL_INFO, F("Serial Proxy: Disable Filter Window active"));
    return true;
  }

  const bool res = P087_data->matchRegexp(received);

  if (P087_data->invertMatch()) {
    addLog(LOG_LEVEL_INFO, F("Serial Proxy: invert filter"));
    return !res;
  }
  return res;
}

String Plugin_087_valuename(uint8_t value_nr, bool displayString) {
  switch (value_nr) {
    case P087_QUERY_VALUE: return displayString ? F("Value") : F("v");
  }
  return EMPTY_STRING;
}

void P087_html_show_matchForms(struct EventStruct *event) {
  P087_data_struct *P087_data =
    static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

  if ((nullptr != P087_data)) {
    addFormTextBox(F("RegEx"), getPluginCustomArgName(P087_REGEX_POS), P087_data->getRegEx(), P87_Nchars);
    addFormNote(F("Captures are specified using round brackets."));

    addFormNumericBox(F("Nr Chars use in regex"), getPluginCustomArgName(P087_NR_CHAR_USE_POS), P087_data->getRegExpMatchLength(), 0, 1024);
    addFormNote(F("0 = Use all of the received string."));

    addFormNumericBox(F("Filter Off Window after send"),
                      getPluginCustomArgName(P087_FILTER_OFF_WINDOW_POS),
                      P087_data->getFilterOffWindowTime(),
                      0,
                      60000);
    addUnit(F("msec"));
    addFormNote(F("0 = Do not turn off filter after sending to the connected device."));

    {
      const __FlashStringHelper *options[P087_Match_Type_NR_ELEMENTS];
      int optionValues[P087_Match_Type_NR_ELEMENTS];

      for (int i = 0; i < P087_Match_Type_NR_ELEMENTS; ++i) {
        P087_Match_Type matchType = static_cast<P087_Match_Type>(i);
        options[i]      = P087_data_struct::MatchType_toString(matchType);
        optionValues[i] = matchType;
      }
      P087_Match_Type choice = P087_data->getMatchType();
      const FormSelectorOptions selector(
        P087_Match_Type_NR_ELEMENTS,
        options,
        optionValues);
      selector.addFormSelector(
        F("Match Type"),
        getPluginCustomArgName(P087_MATCH_TYPE_POS),
        choice);
      addFormNote(F("Capture filter can only be used on Global Match"));
    }


    uint8_t lineNr              = 0;
    uint8_t capture             = 0;
    P087_Filter_Comp comparator = P087_Filter_Comp::Equal;
    String filter;

    for (uint8_t varNr = P087_FIRST_FILTER_POS; varNr < P87_Nlines; ++varNr)
    {
      const String id = getPluginCustomArgName(varNr);

      switch (varNr % 3) {
        case 0:
        {
          // Label + first parameter
          filter = P087_data->getFilter(lineNr, capture, comparator);
          ++lineNr;
          addRowLabel_tr_id(concat(F("Capture Filter "), lineNr), id);

          addNumericBox(id, capture, -1, P87_MAX_CAPTURE_INDEX);
          break;
        }
        case 1:
        {
          // Comparator
          const __FlashStringHelper *options[2];
          options[P087_Filter_Comp::Equal]    = F("==");
          options[P087_Filter_Comp::NotEqual] = F("!=");
          const int optionValues[] = { P087_Filter_Comp::Equal, P087_Filter_Comp::NotEqual };
          FormSelectorOptions selector(2, options, optionValues);
          selector.clearClassName();
          selector.addSelector(id, static_cast<int>(comparator));
          break;
        }
        case 2:
        {
          // Compare with
          addTextBox(id, filter, 32, F(""));
          break;
        }
      }
    }
  }
}

void P087_html_show_stats(struct EventStruct *event) {
  P087_data_struct *P087_data =
    static_cast<P087_data_struct *>(getPluginTaskData(event->TaskIndex));

  if ((nullptr == P087_data) || !P087_data->isInitialized()) {
    return;
  }
  {
    addRowLabel(F("Current Sentence"));
    String sentencePart;
    P087_data->getSentence(sentencePart);
    addHtml(sentencePart);
  }

  {
    addRowLabel(F("Sentences (pass/fail)"));
    uint32_t success, error, length_last;
    P087_data->getSentencesReceived(success, error, length_last);
    addHtml(strformat(F("%d/%d"), success, error));
    addRowLabel(F("Length Last Sentence"));
    addHtmlInt(length_last);
  }
}

#endif // USES_P087
