#include "src/Helpers/_CPlugin_Helper.h"

#ifdef USES_C018

// #######################################################################################################
// ########################### Controller Plugin 018: LoRa TTN - RN2483/RN2903 ###########################
// #######################################################################################################

# define CPLUGIN_018
# define CPLUGIN_ID_018         18
# define CPLUGIN_NAME_018       "LoRa TTN - RN2483/RN2903"


# include <ESPeasySerial.h>

# include "src/ControllerQueue/C018_queue_element.h"
# include "src/Controller_config/C018_config.h"
# include "src/Controller_struct/C018_data_struct.h"
# include "src/DataTypes/ESPEasy_plugin_functions.h"
# include "src/Globals/CPlugins.h"
# include "src/Helpers/_Plugin_Helper_serial.h"
# include "src/Helpers/StringGenerator_GPIO.h"
# include "src/WebServer/Markup.h"
# include "src/WebServer/Markup_Forms.h"
# include "src/WebServer/HTML_wrappers.h"


// Have this define after the includes, so we can set it in Custom.h
# ifndef C018_FORCE_SW_SERIAL
#  define C018_FORCE_SW_SERIAL false
# endif // ifndef C018_FORCE_SW_SERIAL


// FIXME TD-er: Must add a controller data struct vector, like with plugins.
C018_data_struct *C018_data = nullptr;


// Forward declarations
bool   C018_init(struct EventStruct *event);
String c018_add_joinChanged_script_element_line(const String& id,
                                                bool          forOTAA);


bool CPlugin_018(CPlugin::Function function, struct EventStruct *event, String& string)
{
  bool success = false;

  switch (function)
  {
    case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:
    {
      ProtocolStruct& proto = getProtocolStruct(event->idx); //        = CPLUGIN_ID_018;
      proto.usesMQTT       = false;
      proto.usesAccount    = true;
      proto.usesPassword   = true;
      proto.defaultPort    = 1;
      proto.usesID         = true;
      proto.usesHost       = false;
      proto.usesCheckReply = false;
      proto.usesTimeout    = false;
      proto.usesSampleSets = true;
      proto.needsNetwork   = false;
      break;
    }

    case CPlugin::Function::CPLUGIN_GET_DEVICENAME:
    {
      string = F(CPLUGIN_NAME_018);
      break;
    }

    case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG:
    {
      if ((C018_data != nullptr) && C018_data->isInitialized()) {
        string  = F("Dev addr: ");
        string += C018_data->getDevaddr();
        string += C018_data->useOTAA() ? F(" (OTAA)") : F(" (ABP)");
      } else {
        string = F("-");
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_INIT:
    {
      success = init_c018_delay_queue(event->ControllerIndex);

      if (success) {
        C018_init(event);
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_EXIT:
    {
      if (C018_data != nullptr) {
        C018_data->reset();
        delete C018_data;
        C018_data = nullptr;
      }
      exit_c018_delay_queue();
      break;
    }

    case CPlugin::Function::CPLUGIN_WEBFORM_LOAD:
    {
      {
        // Script to toggle visibility of OTAA/ABP field, based on the activation method selector.
        protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex);
        html_add_script(false);
        addHtml(F("function joinChanged(elem){ var styleOTAA = elem.value == 0 ? '' : 'none'; var styleABP = elem.value == 1 ? '' : 'none';"));
        addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex,
                                                                                            ControllerSettingsStruct::CONTROLLER_USER),
                                                         true));
        addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex,
                                                                                            ControllerSettingsStruct::CONTROLLER_PASS),
                                                         true));
        addHtml(c018_add_joinChanged_script_element_line(F("deveui"), true));
        addHtml(c018_add_joinChanged_script_element_line(F("deveui_note"), true));

        addHtml(c018_add_joinChanged_script_element_line(F("devaddr"), false));
        addHtml(c018_add_joinChanged_script_element_line(F("nskey"), false));
        addHtml(c018_add_joinChanged_script_element_line(F("appskey"), false));
        addHtml('}');
        html_add_script_end();
      }

      {
        // Keep this object in a small scope so we can destruct it as soon as possible again.

        constexpr unsigned size = sizeof(C018_ConfigStruct);
        void *ptr               = special_calloc(1, size);
      
        if (ptr == nullptr) {
          break;
        }
        std::shared_ptr<C018_ConfigStruct> customConfig(new (ptr) C018_ConfigStruct);

        if (!customConfig) {
          break;
        }
        LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast<uint8_t *>(customConfig.get()), sizeof(C018_ConfigStruct));
        customConfig->webform_load(C018_data);
      }

      break;
    }
    case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:
    {
      constexpr unsigned size = sizeof(C018_ConfigStruct);
      void *ptr               = special_calloc(1, size);
    
      if (ptr == nullptr) {
        break;
      }
      std::shared_ptr<C018_ConfigStruct> customConfig(new (ptr) C018_ConfigStruct);

      if (customConfig) {
        customConfig->webform_save();
        SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast<const uint8_t *>(customConfig.get()),
                                     sizeof(C018_ConfigStruct));
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME:
    {
      success = true;

      switch (event->idx) {
        case ControllerSettingsStruct::CONTROLLER_USER:
          string = F("AppEUI");
          break;
        case ControllerSettingsStruct::CONTROLLER_PASS:
          string = F("AppKey");
          break;
        case ControllerSettingsStruct::CONTROLLER_TIMEOUT:
          string = F("Module Timeout");
          break;
        case ControllerSettingsStruct::CONTROLLER_PORT:
          string = F("Port");
          break;
        default:
          success = false;
          break;
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:
    {
      if (C018_DelayHandler == nullptr) {
        break;
      }

      if (C018_DelayHandler->queueFull(event->ControllerIndex)) {
        break;
      }

      if (C018_data != nullptr) {
        {
          constexpr unsigned size = sizeof(C018_queue_element);
          void *ptr               = special_calloc(1, size);
        
          if (ptr == nullptr) {
            break;
          }
    
          std::unique_ptr<C018_queue_element> element(new (ptr) C018_queue_element(event, C018_data->getSampleSetCount(event->TaskIndex)));
          success = C018_DelayHandler->addToQueue(std::move(element));
          Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE,
                                           C018_DelayHandler->getNextScheduleTime());
        }

        if (!C018_data->isInitialized()) {
          // Sometimes the module does need some time after power on to respond.
          // So it may not be initialized well at the call of CPLUGIN_INIT
          // We try to trigger its init again when sending data.
          C018_init(event);
        }
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_PROTOCOL_RECV:
    {
      // FIXME TD-er: WHen should this be scheduled?
      // protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex);
      // schedule_controller_event_timer(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_RECV, event);
      break;
    }

    case CPlugin::Function::CPLUGIN_WRITE:
    {
      if (C018_data != nullptr) {
        if (C018_data->isInitialized())
        {
          const String command = parseString(string, 1);

          if (equals(command, F("lorawan"))) {
            const String subcommand = parseString(string, 2);

            if (equals(subcommand, F("write"))) {
              const String loraWriteCommand = parseStringToEnd(string, 3);
              const String res              = C018_data->sendRawCommand(loraWriteCommand);
              String logstr                 = F("LoRaWAN cmd: ");
              logstr += loraWriteCommand;
              logstr += F(" -> ");
              logstr += res;
              addLog(LOG_LEVEL_INFO, logstr);
              SendStatus(event, logstr);
              success = true;
            }
          }
        }
      }
      break;
    }

    case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND:
    {
      if (C018_data != nullptr) {
        C018_data->async_loop();
      }

      // FIXME TD-er: Handle reading error state or return values.
      break;
    }

    case CPlugin::Function::CPLUGIN_FLUSH:
    {
      process_c018_delay_queue();
      delay(0);
      break;
    }

    default:
      break;
  }
  return success;
}

bool C018_init(struct EventStruct *event) {
  String AppEUI;
  String AppKey;
  taskIndex_t  SampleSetInitiator = INVALID_TASK_INDEX;
  unsigned int Port               = 0;

  // Check if the object is already created.
  // If so, delete it to make sure the module is initialized according to the full set parameters.
  if (C018_data != nullptr) {
    C018_data->reset();
    delete C018_data;
    C018_data = nullptr;
  }

  {
    constexpr unsigned size = sizeof(C018_data_struct);
    void *ptr               = special_calloc(1, size);

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

    C018_data = new (ptr) C018_data_struct;

    if (C018_data == nullptr) {
      return false;
    }
  }
  {
    // Allocate ControllerSettings object in a scope, so we can destruct it as soon as possible.
    MakeControllerSettings(ControllerSettings); // -V522

    if (!AllocatedControllerSettings()) {
      return false;
    }

    LoadControllerSettings(event->ControllerIndex, *ControllerSettings);
    C018_DelayHandler->cacheControllerSettings(*ControllerSettings);
    AppEUI             = getControllerUser(event->ControllerIndex, *ControllerSettings);
    AppKey             = getControllerPass(event->ControllerIndex, *ControllerSettings);
    SampleSetInitiator = ControllerSettings->SampleSetInitiator;
    Port               = ControllerSettings->Port;
  }

  constexpr unsigned size = sizeof(C018_ConfigStruct);
  void *ptr               = special_calloc(1, size);

  if (ptr == nullptr) {
    return false;
  }
  std::shared_ptr<C018_ConfigStruct> customConfig(new (ptr) C018_ConfigStruct);

  if (!customConfig) {
    return false;
  }
  LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast<uint8_t *>(customConfig.get()), sizeof(C018_ConfigStruct));
  customConfig->validate();

  if (!C018_data->init(customConfig->serialPort, customConfig->rxpin, customConfig->txpin, customConfig->baudrate,
                       (customConfig->joinmethod == C018_USE_OTAA),
                       SampleSetInitiator, customConfig->resetpin))
  {
    return false;
  }

  C018_data->setFrequencyPlan(static_cast<RN2xx3_datatypes::Freq_plan>(customConfig->frequencyplan), customConfig->rx2_freq);

  if (!C018_data->setSF(customConfig->sf)) {
    return false;
  }

  if (!C018_data->setAdaptiveDataRate(customConfig->adr != 0)) {
    return false;
  }

  if (!C018_data->setTTNstack(static_cast<RN2xx3_datatypes::TTN_stack_version>(customConfig->stackVersion))) {
    return false;
  }

  if (customConfig->joinmethod == C018_USE_OTAA) {
    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("OTAA: AppEUI: ");
      log += AppEUI;
      log += F(" AppKey: ");
      log += AppKey;
      log += F(" DevEUI: ");
      log += customConfig->DeviceEUI;

      addLogMove(LOG_LEVEL_INFO, log);
    }

    if (!C018_data->initOTAA(AppEUI, AppKey, customConfig->DeviceEUI)) {
      return false;
    }
  }
  else {
    if (!C018_data->initABP(customConfig->DeviceAddr, customConfig->AppSessionKey, customConfig->NetworkSessionKey)) {
      return false;
    }
  }


  if (!C018_data->txUncnf(F("ESPeasy (TTN)"), Port)) {
    return false;
  }
  return true;
}

// Uncrustify may change this into multi line, which will result in failed builds
// *INDENT-OFF*
bool do_process_c018_delay_queue(cpluginID_t cpluginID, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) {
  const C018_queue_element& element = static_cast<const C018_queue_element&>(element_base);
// *INDENT-ON*
  uint8_t pl           = (element.packed.length() / 2);
  float   airtime_ms   = C018_data->getLoRaAirTime(pl);
  bool    mustSetDelay = false;
  bool    success      = false;

  if (!C018_data->command_finished()) {
    mustSetDelay = true;
  } else {
    success = C018_data->txHexBytes(element.packed, ControllerSettings.Port);

    if (success) {
      if (airtime_ms > 0.0f) {
        ADD_TIMER_STAT(C018_AIR_TIME, static_cast<unsigned long>(airtime_ms * 1000));

        if (loglevelActiveFor(LOG_LEVEL_INFO)) {
          String log = F("LoRaWAN : Payload Length: ");
          log += pl + 13; // We have a LoRaWAN header of 13 bytes.
          log += F(" Air Time: ");
          log += toString(airtime_ms, 3);
          log += F(" ms");
          addLogMove(LOG_LEVEL_INFO, log);
        }
      }
    }
  }
  String error = C018_data->getLastError(); // Clear the error string.

  if (error.indexOf(F("no_free_ch")) != -1) {
    mustSetDelay = true;
  }

  if (loglevelActiveFor(LOG_LEVEL_INFO)) {
    String log = F("C018 : Sent: ");
    log += element.packed;
    log += F(" length: ");
    log += String(element.packed.length());

    if (success) {
      log += F(" (success) ");
    }
    log += error;
    addLogMove(LOG_LEVEL_INFO, log);
  }

  if (mustSetDelay) {
    // Module is still sending, delay for 10x expected air time, which is equivalent of 10% air time duty cycle.
    // This can be retried a few times, so at most 10 retries like these are needed to get below 1% air time again.
    // Very likely only 2 - 3 of these delays are needed, as we have 8 channels to send from and messages are likely sent in bursts.
    C018_DelayHandler->setAdditionalDelay(10 * airtime_ms);

    if (loglevelActiveFor(LOG_LEVEL_INFO)) {
      String log = F("LoRaWAN : Unable to send. Delay for ");
      log += 10 * airtime_ms;
      log += F(" ms");
      addLogMove(LOG_LEVEL_INFO, log);
    }
  }

  return success;
}

String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA) {
  String result = F("document.getElementById('tr_");

  result += id;
  result += F("').style.display = style");
  result += forOTAA ? F("OTAA") : F("ABP");
  result += ';';
  return result;
}

#endif // ifdef USES_C018
