#ifndef RN2XX3_TX_STATE_H
#define RN2XX3_TX_STATE_H

#include <Arduino.h>

#include "rn2xx3_status.h"

// State machine for the RN2483/RN2903 modules
class rn2xx3_handler {
public:

  enum RN_state {
    idle,
    command_set_to_send,
    wait_for_reply,
    wait_for_reply_rx2,
    reply_received,
    reply_received_rx2,
    tx_success,
    tx_success_with_rx,
    reply_received_finished,
    join_accepted,
    timeout,
    duty_cycle_exceeded,
    max_attempt_reached,
    error,
    invalid_char_read,
    must_perform_init,
    must_pause
  };

  rn2xx3_handler(Stream& serial);

  String sendRawCommand(const __FlashStringHelper* command);
  String sendRawCommand(const String& command);

  bool   prepare_raw_command(const String& command);

  bool   prepare_tx_command(const String& command,
                            const String& data,
                            bool          shouldEncode,
                            uint8_t       port);

  bool     prepare_join(bool useOTAA);

  RN_state async_loop();

  // Wait for the command to be handled completely (including reply in RX2 window)
  RN_state wait_command_finished(unsigned long timeout = 10000);

  // Shorter wait, to be used in async mode.
  // This will return early when the message cannot be sent (e.g. due to duty cycle exceeded)
  // It will return when the state waiting for reply_received_rx2 has been reached, or the command has finished (due to error)
  RN_state wait_command_accepted(unsigned long timeout = 10000);

  // Check whether a command has finished.
  bool     command_finished() const;


  /*
   * Initialise the RN2xx3 and join the LoRa network (if applicable).
   * This function can only be called after calling initABP() or initOTAA().
   * The sole purpose of this function is to re-initialise the radio if it
   * is in an unknown state.
   */
  bool init();

  /*
   * Initialise the RN2xx3 and join a network using personalization.
   *
   * addr: The device address as a HEX string.
   *       Example "0203FFEE"
   * AppSKey: Application Session Key as a HEX string.
   *          Example "8D7FFEF938589D95AAD928C2E2E7E48F"
   * NwkSKey: Network Session Key as a HEX string.
   *          Example "AE17E567AECC8787F749A62F5541D522"
   */
  bool initABP(const String& addr,
               const String& AppSKey,
               const String& NwkSKey);

  // TODO: initABP(uint8_t * addr, uint8_t * AppSKey, uint8_t * NwkSKey)

  /*
   * Initialise the RN2xx3 and join a network using over the air activation.
   *
   * AppEUI: Application EUI as a HEX string.
   *         Example "70B3D57ED00001A6"
   * AppKey: Application key as a HEX string.
   *         Example "A23C96EE13804963F8C2BD6285448198"
   * DevEUI: Device EUI as a HEX string.
   *         Example "0011223344556677"
   * If the DevEUI parameter is omitted, the Hardware EUI from module will be used
   * If no keys, or invalid length keys, are provided, no keys
   * will be configured. If the module is already configured with some keys
   * they will be used. Otherwise the join will fail and this function
   * will return false.
   */
  bool initOTAA(const String& AppEUI = "",
                const String& AppKey = "",
                const String& DevEUI = "");

  /*
   * Initialise the RN2xx3 and join a network using over the air activation,
   * using byte arrays. This is useful when storing the keys in eeprom or flash
   * and reading them out in runtime.
   *
   * AppEUI: Application EUI as a uint8_t buffer
   * AppKey: Application key as a uint8_t buffer
   * DevEui: Device EUI as a uint8_t buffer (optional - set to 0 to use Hardware EUI)
   */
  bool initOTAA(uint8_t *AppEUI,
                uint8_t *AppKey,
                uint8_t *DevEui);


  RN2xx3_datatypes::TX_return_type txCommand(const __FlashStringHelper* command, 
                                             const String& data, 
                                             bool shouldEncode, 
                                             uint8_t port);


  RN2xx3_datatypes::TX_return_type txCommand(const String&,
                                             const String&,
                                             bool,
                                             uint8_t port = 1);

  bool setSF(uint8_t sf);
  uint8_t getSF(int& dr);


  /*
   * Change the datarate at which the RN2xx3 transmits.
   * A value of between 0 and 5 can be specified,
   * as is defined in the LoRaWan specs.
   * This can be overwritten by the network when using OTAA.
   * So to force a datarate, call this function after initOTAA().
   */
  bool setDR(int dr);

  void setAsyncMode(bool enabled);

  bool getAsyncMode() const;

  bool useOTAA() const;

  void setLastUsedJoinMode(bool isOTAA);

  // We can't read back from module, we send the one
  // we have memorized if it has been set
  String appkey() const {
    return _appkey;
  }

  String appskey() const {
    return _appskey;
  }

  RN2xx3_datatypes::Model moduleType()
  {
    return _moduleType;
  }

  bool setFrequencyPlan(RN2xx3_datatypes::Freq_plan fp, uint32_t rx2_freq = 0);

  bool setTTNstack(RN2xx3_datatypes::TTN_stack_version version);

private:

  // Return the data to send
  const String& get_send_data() const;

public:

  // Get the received data
  const String& get_received_data() const;

  const String& get_received_data(unsigned long& duration) const;

  // Get the downlink message, received during RX2 after TX command.
  const String& get_rx_message() const;

  // Look at the last error, without clearing it.
  String        peekLastError() const;

  // get and clear the last error.
  String        getLastError();

  // Set specific error string.
  void          setLastError(const __FlashStringHelper* error);
  void          setLastError(const String& error);

  RN_state      get_state() const;

  uint8_t       get_busy_count() const;

  String        sysver();

  // delay from last moment of sending to receive RX1 and RX2 window
  bool          getRxDelayValues(uint32_t& rxdelay1,
                                 uint32_t& rxdelay2);

  // Compute the air time for a packet in msec.
  // Formula used from https://www.loratools.nl/#/airtime
  // @param pl   Payload length in bytes
  float getLoRaAirTime(uint8_t  pl) const;


private:

  RN2xx3_datatypes::Model configureModuleType();


  bool                    resetModule();

  // Check to see if the set activation values are of the right size and in HEX notation.
  bool                    check_set_keys();

  void                    set_state(RN_state state);

  // read all available data from serial until '\n'
  bool                    read_line();

  void                    set_timeout(unsigned long timeout);

  bool                    time_out_reached() const;

  void                    handle_reply_received();

  void                    clearSerialBuffer();

  // Read the internal status of the module
  // @retval true when update was successful
  bool updateStatus();

  bool saveUpdatedStatus();


  enum Active_cmd {
    none,
    TX,
    join,
    other
  };


  // OTAA values:
  String _deveui;
  String _appeui;
  String _appkey;

  // ABP values:
  String _nwkskey;
  String _appskey;
  String _devaddr;


  String _receivedData;              // Used as a receive buffer to collect replies from the module
  String _sendData;                  // Complete command to send to the module
  String _rxMessenge;                // Message received (during RX2 window) after a TX
  String _lastError;                 // Last error message received from module (or set by user)
  unsigned long _start_prep  = 0;    // timestamp of preparing command
  unsigned long _start       = 0;    // timestamp of last set timeout
  unsigned long _timeout     = 100;  // timeout duration
  uint32_t _rxdelay1         = 5000; // delay from last moment of sending to receive RX1 window
  uint32_t _rxdelay2         = 6000; // delay from last moment of sending to receive RX2 window
  uint8_t _busy_count        = 0;    // Number of times the module replied with "busy"
  uint8_t _retry_count       = 0;    // Number of retries of current TX command
  RN_state _state            = RN_state::idle;
  Active_cmd _processing_cmd = Active_cmd::none;
  bool _invalid_char_read    = false;
  bool _extensive_debug      = false; // Set this to true to log all steps in _lastError
  uint16_t _max_received_length = 0;


  RN2xx3_datatypes::Firmware _firmware = RN2xx3_datatypes::Firmware::unknown;
  RN2xx3_datatypes::Model _moduleType = RN2xx3_datatypes::Model::RN_NA;
  RN2xx3_datatypes::Freq_plan _fp     = RN2xx3_datatypes::Freq_plan::TTN_EU;
  uint32_t _rx2_frequency             = 0;
  uint8_t _sf                         = 7;
  uint8_t _dr                         = 5;

  bool _otaa      = true;  // Switch between OTAA or ABP activation (default OTAA)
  bool _asyncMode = false; // When set, the user must call async_loop() frequently

public:

  RN2xx3_status Status; // Cached result of "mac get status"
  Stream& _serial;      // The serial port used for this module.


  // Convenience functions

  int  readIntValue(const __FlashStringHelper* command);
  int  readIntValue(const String& command);


  bool readUIntMacGet(const __FlashStringHelper* param,
                      uint32_t    & value);
  bool readUIntMacGet(const String& param,
                      uint32_t    & value);

  // All "mac set ..." commands return either "ok" or "invalid_param"
  bool sendMacSet(const __FlashStringHelper* param,
                  const String& value);
  bool sendMacSet(const String& param,
                  const String& value);
  bool sendMacSetEnabled(const __FlashStringHelper* param,
                         bool          enabled);
  bool sendMacSetCh(const String& param,
                    unsigned int  channel,
                    const String& value);
  bool sendMacSetCh(const String& param,
                    unsigned int  channel,
                    uint32_t      value);
  bool sendMacSetCh(const __FlashStringHelper* param,
                    unsigned int  channel,
                    uint32_t      value);
  bool setChannelDutyCycle(unsigned int channel,
                           unsigned int dutyCycle);
  bool setChannelFrequency(unsigned int channel,
                           uint32_t     frequency);
  bool setChannelDataRateRange(unsigned int channel,
                               unsigned int minRange,
                               unsigned int maxRange);

  // Set channel enabled/disabled.
  // Frequency, data range, duty cycle must be issued prior to enabling the status of that channel
  bool setChannelEnabled(unsigned int channel,
                         bool         enabled);

  bool set2ndRecvWindow(unsigned int dataRate,
                        uint32_t     frequency);
  bool setAdaptiveDataRate(bool enabled);
  bool setAutomaticReply(bool enabled);
  bool setTXoutputPower(int pwridx);

  // Send the autobaud sequence to try and wake the module.
  void sendWakeSequence();
};


#endif // RN2XX3_TX_STATE_H
