#include "sim800l.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstring>

namespace esphome {
namespace sim800l {

static const char *const TAG = "sim800l";

const char ASCII_CR = 0x0D;
const char ASCII_LF = 0x0A;

void Sim800LComponent::update() {
  if (this->watch_dog_++ == 2) {
    this->state_ = STATE_INIT;
    this->write(26);
  }

  if (this->expect_ack_)
    return;

  if (state_ == STATE_INIT) {
    if (this->registered_ && this->send_pending_) {
      this->send_cmd_("AT+CSCS=\"GSM\"");
      this->state_ = STATE_SENDING_SMS_1;
    } else if (this->registered_ && this->dial_pending_) {
      this->send_cmd_("AT+CSCS=\"GSM\"");
      this->state_ = STATE_DIALING1;
    } else if (this->registered_ && this->connect_pending_) {
      this->connect_pending_ = false;
      ESP_LOGI(TAG, "Connecting");
      this->send_cmd_("ATA");
      this->state_ = STATE_ATA_SENT;
    } else if (this->registered_ && this->send_ussd_pending_) {
      this->send_cmd_("AT+CSCS=\"GSM\"");
      this->state_ = STATE_SEND_USSD1;
    } else if (this->registered_ && this->disconnect_pending_) {
      this->disconnect_pending_ = false;
      ESP_LOGI(TAG, "Disconnecting");
      this->send_cmd_("ATH");
    } else if (this->registered_ && this->call_state_ != 6) {
      send_cmd_("AT+CLCC");
      this->state_ = STATE_CHECK_CALL;
      return;
    } else {
      this->send_cmd_("AT");
      this->state_ = STATE_SETUP_CMGF;
    }
    this->expect_ack_ = true;
  } else if (state_ == STATE_RECEIVED_SMS) {
    // Serial Buffer should have flushed.
    // Send cmd to delete received sms
    char delete_cmd[20];  // "AT+CMGD=" (8) + uint8_t (max 3) + null = 12 <= 20
    buf_append_printf(delete_cmd, sizeof(delete_cmd), 0, "AT+CMGD=%d", this->parse_index_);
    this->send_cmd_(delete_cmd);
    this->state_ = STATE_CHECK_SMS;
    this->expect_ack_ = true;
  }
}

void Sim800LComponent::send_cmd_(const std::string &message) {
  ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_);
  this->watch_dog_ = 0;
  this->write_str(message.c_str());
  this->write_byte(ASCII_CR);
  this->write_byte(ASCII_LF);
}

void Sim800LComponent::parse_cmd_(std::string message) {
  if (message.empty())
    return;

  ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_);

  if (this->state_ != STATE_RECEIVE_SMS) {
    if (message == "RING") {
      // Incoming call...
      this->state_ = STATE_PARSE_CLIP;
      this->expect_ack_ = false;
    } else if (message == "NO CARRIER") {
      if (this->call_state_ != 6) {
        this->call_state_ = 6;
        this->call_disconnected_callback_.call();
      }
    }
  }

  bool ok = message == "OK";
  if (this->expect_ack_) {
    this->expect_ack_ = false;
    if (!ok) {
      if (this->state_ == STATE_SETUP_CMGF && message == "AT") {
        // Expected ack but AT echo received
        this->state_ = STATE_DISABLE_ECHO;
        this->expect_ack_ = true;
      } else {
        ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str());
        this->state_ = STATE_IDLE;  // Let it timeout
        return;
      }
    }
  } else if (ok && (this->state_ != STATE_PARSE_SMS_RESPONSE && this->state_ != STATE_CHECK_CALL &&
                    this->state_ != STATE_RECEIVE_SMS && this->state_ != STATE_DIALING2)) {
    ESP_LOGW(TAG, "Received unexpected OK. Ignoring");
    return;
  }

  switch (this->state_) {
    case STATE_INIT: {
      // While we were waiting for update to check for messages, this notifies a message
      // is available.
      bool message_available = message.compare(0, 6, "+CMTI:") == 0;
      if (!message_available) {
        if (message == "RING") {
          // Incoming call...
          this->state_ = STATE_PARSE_CLIP;
        } else if (message == "NO CARRIER") {
          if (this->call_state_ != 6) {
            this->call_state_ = 6;
            this->call_disconnected_callback_.call();
          }
        } else if (message.compare(0, 6, "+CUSD:") == 0) {
          // Incoming USSD MESSAGE
          this->state_ = STATE_CHECK_USSD;
        }
        break;
      }

      // Else fall thru ...
    }
    case STATE_CHECK_SMS:
      send_cmd_("AT+CMGL=\"ALL\"");
      this->state_ = STATE_PARSE_SMS_RESPONSE;
      this->parse_index_ = 0;
      break;
    case STATE_DISABLE_ECHO:
      send_cmd_("ATE0");
      this->state_ = STATE_SETUP_CMGF;
      this->expect_ack_ = true;
      break;
    case STATE_SETUP_CMGF:
      send_cmd_("AT+CMGF=1");
      this->state_ = STATE_SETUP_CLIP;
      this->expect_ack_ = true;
      break;
    case STATE_SETUP_CLIP:
      send_cmd_("AT+CLIP=1");
      this->state_ = STATE_CREG;
      this->expect_ack_ = true;
      break;
    case STATE_SETUP_USSD:
      send_cmd_("AT+CUSD=1");
      this->state_ = STATE_CREG;
      this->expect_ack_ = true;
      break;
    case STATE_SEND_USSD1:
      this->send_cmd_("AT+CUSD=1, \"" + this->ussd_ + "\"");
      this->state_ = STATE_SEND_USSD2;
      this->expect_ack_ = true;
      break;
    case STATE_SEND_USSD2:
      ESP_LOGD(TAG, "SendUssd2: '%s'", message.c_str());
      if (message == "OK") {
        // Dialing
        ESP_LOGD(TAG, "Dialing ussd code: '%s' done.", this->ussd_.c_str());
        this->state_ = STATE_CHECK_USSD;
        this->send_ussd_pending_ = false;
      } else {
        this->set_registered_(false);
        this->state_ = STATE_INIT;
        this->send_cmd_("AT+CMEE=2");
        this->write(26);
      }
      break;
    case STATE_CHECK_USSD:
      ESP_LOGD(TAG, "Check ussd code: '%s'", message.c_str());
      if (message.compare(0, 6, "+CUSD:") == 0) {
        this->state_ = STATE_RECEIVED_USSD;
        this->ussd_ = "";
        size_t start = 10;
        size_t end = message.find_last_of(',');
        if (end > start) {
          this->ussd_ = message.substr(start + 1, end - start - 2);
          this->ussd_received_callback_.call(this->ussd_);
        }
      }
      // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS
      if (message == "OK")
        this->state_ = STATE_INIT;
      break;
    case STATE_CREG:
      send_cmd_("AT+CREG?");
      this->state_ = STATE_CREG_WAIT;
      break;
    case STATE_CREG_WAIT: {
      // Response: "+CREG: 0,1" -- the one there means registered ok
      //           "+CREG: -,-" means not registered ok
      bool registered =
          message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
      if (registered) {
        if (!this->registered_) {
          ESP_LOGD(TAG, "Registered OK");
        }
        this->state_ = STATE_CSQ;
        this->expect_ack_ = true;
      } else {
        ESP_LOGW(TAG, "Registration Fail");
        if (message.size() > 7 && message[7] == '0') {  // Network registration is disabled, enable it
          send_cmd_("AT+CREG=1");
          this->expect_ack_ = true;
          this->state_ = STATE_SETUP_CMGF;
        } else {
          // Keep waiting registration
          this->state_ = STATE_INIT;
        }
      }
      set_registered_(registered);
      break;
    }
    case STATE_CSQ:
      send_cmd_("AT+CSQ");
      this->state_ = STATE_CSQ_RESPONSE;
      break;
    case STATE_CSQ_RESPONSE:
      if (message.compare(0, 5, "+CSQ:") == 0) {
        size_t comma = message.find(',', 6);
        if (comma != 6) {
          int rssi = parse_number<int>(message.substr(6, comma - 6)).value_or(0);

#ifdef USE_SENSOR
          if (this->rssi_sensor_ != nullptr) {
            this->rssi_sensor_->publish_state(rssi);
          } else {
            ESP_LOGD(TAG, "RSSI: %d", rssi);
          }
#else
          ESP_LOGD(TAG, "RSSI: %d", rssi);
#endif
        }
      }
      this->expect_ack_ = true;
      this->state_ = STATE_CHECK_SMS;
      break;
    case STATE_PARSE_SMS_RESPONSE:
      if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) {
        size_t start = 7;
        size_t end = message.find(',', start);
        uint8_t item = 0;
        while (end != start) {
          item++;
          if (item == 1) {  // Slot Index
            this->parse_index_ = parse_number<uint8_t>(message.substr(start, end - start)).value_or(0);
          }
          // item 2 = STATUS, usually "REC UNREAD"
          if (item == 3) {  // recipient
            // Add 1 and remove 2 from substring to get rid of "quotes"
            this->sender_ = message.substr(start + 1, end - start - 2);
            this->message_.clear();
            break;
          }
          // item 4 = ""
          // item 5 = Received timestamp
          start = end + 1;
          end = message.find(',', start);
        }

        if (item < 2) {
          ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str());
          return;
        }
        this->state_ = STATE_RECEIVE_SMS;
      }
      // Otherwise we receive another OK
      if (ok) {
        send_cmd_("AT+CLCC");
        this->state_ = STATE_CHECK_CALL;
      }
      break;
    case STATE_CHECK_CALL:
      if (message.compare(0, 6, "+CLCC:") == 0 && this->parse_index_ == 0) {
        this->expect_ack_ = true;
        size_t start = 7;
        size_t end = message.find(',', start);
        uint8_t item = 0;
        while (end != start) {
          item++;
          // item 1 call index for +CHLD
          // item 2 dir 0 Mobile originated; 1 Mobile terminated
          if (item == 3) {  // stat
            uint8_t current_call_state = parse_number<uint8_t>(message.substr(start, end - start)).value_or(6);
            if (current_call_state != this->call_state_) {
              if (current_call_state == 4) {
                ESP_LOGV(TAG, "Premature call state '4'. Ignoring, waiting for RING");
              } else {
                this->call_state_ = current_call_state;
                ESP_LOGD(TAG, "Call state is now: %d", current_call_state);
                if (current_call_state == 0)
                  this->call_connected_callback_.call();
              }
            }
            break;
          }
          // item 4 = ""
          // item 5 = Received timestamp
          start = end + 1;
          end = message.find(',', start);
        }

        if (item < 2) {
          ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str());
          return;
        }
      } else if (ok) {
        if (this->call_state_ != 6) {
          // no call in progress
          this->call_state_ = 6;  // Disconnect
          this->call_disconnected_callback_.call();
        }
      }
      this->state_ = STATE_INIT;
      break;
    case STATE_RECEIVE_SMS:
      /* Our recipient is set and the message body is in message
        kick ESPHome callback now
      */
      if (ok || message.compare(0, 6, "+CMGL:") == 0) {
        ESP_LOGD(TAG,
                 "Received SMS from: %s\n"
                 "  %s",
                 this->sender_.c_str(), this->message_.c_str());
        this->sms_received_callback_.call(this->message_, this->sender_);
        this->state_ = STATE_RECEIVED_SMS;
      } else {
        if (!this->message_.empty())
          this->message_ += "\n";
        this->message_ += message;
      }
      break;
    case STATE_RECEIVED_SMS:
    case STATE_RECEIVED_USSD:
      // Let the buffer flush. Next poll will request to delete the parsed index message.
      break;
    case STATE_SENDING_SMS_1:
      this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\"");
      this->state_ = STATE_SENDING_SMS_2;
      break;
    case STATE_SENDING_SMS_2:
      if (message == ">") {
        // Send sms body
        ESP_LOGI(TAG, "Sending to %s message: '%s'", this->recipient_.c_str(), this->outgoing_message_.c_str());
        this->write_str(this->outgoing_message_.c_str());
        this->write(26);
        this->state_ = STATE_SENDING_SMS_3;
      } else {
        set_registered_(false);
        this->state_ = STATE_INIT;
        this->send_cmd_("AT+CMEE=2");
        this->write(26);
      }
      break;
    case STATE_SENDING_SMS_3:
      if (message.compare(0, 6, "+CMGS:") == 0) {
        ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str());
        this->send_pending_ = false;
        this->state_ = STATE_CHECK_SMS;
        this->expect_ack_ = true;
      }
      break;
    case STATE_DIALING1:
      this->send_cmd_("ATD" + this->recipient_ + ';');
      this->state_ = STATE_DIALING2;
      break;
    case STATE_DIALING2:
      if (ok) {
        ESP_LOGI(TAG, "Dialing: '%s'", this->recipient_.c_str());
        this->dial_pending_ = false;
      } else {
        this->set_registered_(false);
        this->send_cmd_("AT+CMEE=2");
        this->write(26);
      }
      this->state_ = STATE_INIT;
      break;
    case STATE_PARSE_CLIP:
      if (message.compare(0, 6, "+CLIP:") == 0) {
        std::string caller_id;
        size_t start = 7;
        size_t end = message.find(',', start);
        uint8_t item = 0;
        while (end != start) {
          item++;
          if (item == 1) {  // Slot Index
            // Add 1 and remove 2 from substring to get rid of "quotes"
            caller_id = message.substr(start + 1, end - start - 2);
            break;
          }
          // item 4 = ""
          // item 5 = Received timestamp
          start = end + 1;
          end = message.find(',', start);
        }
        if (this->call_state_ != 4) {
          this->call_state_ = 4;
          ESP_LOGI(TAG, "Incoming call from %s", caller_id.c_str());
          incoming_call_callback_.call(caller_id);
        }
        this->state_ = STATE_INIT;
      }
      break;
    case STATE_ATA_SENT:
      ESP_LOGI(TAG, "Call connected");
      if (this->call_state_ != 0) {
        this->call_state_ = 0;
        this->call_connected_callback_.call();
      }
      this->state_ = STATE_INIT;
      break;
    default:
      ESP_LOGW(TAG, "Unhandled: %s - %d", message.c_str(), this->state_);
      break;
  }
}  // namespace sim800l

void Sim800LComponent::loop() {
  // Read message
  while (this->available()) {
    uint8_t byte;
    this->read_byte(&byte);

    if (this->read_pos_ == SIM800L_READ_BUFFER_LENGTH)
      this->read_pos_ = 0;

    ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);  // NOLINT

    if (byte == ASCII_CR)
      continue;
    if (byte >= 0x7F)
      byte = '?';  // need to be valid utf8 string for log functions.
    this->read_buffer_[this->read_pos_] = byte;

    if (this->state_ == STATE_SENDING_SMS_2 && this->read_pos_ == 0 && byte == '>')
      this->read_buffer_[++this->read_pos_] = ASCII_LF;

    if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
      this->read_buffer_[this->read_pos_] = 0;
      this->read_pos_ = 0;
      this->parse_cmd_(this->read_buffer_);
    } else {
      this->read_pos_++;
    }
  }
  if (state_ == STATE_INIT && this->registered_ &&
      (this->call_state_ != 6  // A call is in progress
       || this->send_pending_ || this->dial_pending_ || this->connect_pending_ || this->disconnect_pending_)) {
    this->update();
  }
}

void Sim800LComponent::send_sms(const std::string &recipient, const std::string &message) {
  this->recipient_ = recipient;
  this->outgoing_message_ = message;
  this->send_pending_ = true;
}

void Sim800LComponent::send_ussd(const std::string &ussd_code) {
  ESP_LOGD(TAG, "Sending USSD code: %s", ussd_code.c_str());
  this->ussd_ = ussd_code;
  this->send_ussd_pending_ = true;
  this->update();
}
void Sim800LComponent::dump_config() {
  ESP_LOGCONFIG(TAG, "SIM800L:");
#ifdef USE_BINARY_SENSOR
  LOG_BINARY_SENSOR("  ", "Registered", this->registered_binary_sensor_);
#endif
#ifdef USE_SENSOR
  LOG_SENSOR("  ", "Rssi", this->rssi_sensor_);
#endif
}
void Sim800LComponent::dial(const std::string &recipient) {
  this->recipient_ = recipient;
  this->dial_pending_ = true;
}
void Sim800LComponent::connect() { this->connect_pending_ = true; }
void Sim800LComponent::disconnect() { this->disconnect_pending_ = true; }

void Sim800LComponent::set_registered_(bool registered) {
  this->registered_ = registered;
#ifdef USE_BINARY_SENSOR
  if (this->registered_binary_sensor_ != nullptr)
    this->registered_binary_sensor_->publish_state(registered);
#endif
}

}  // namespace sim800l
}  // namespace esphome
