#include "optoma_rs232.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"

namespace esphome {
namespace optoma_rs232 {

[[maybe_unused]] static const char *const TAG = "optoma_rs232";

enum QUERIES {
  INFO = 0,
  TEMP,
  LAMP_TIME,
  FAN_1,
};

constexpr const char *QUERY_DATA[] = {
    "~00150 1\r",   // Info
    "~00150 18\r",  // Temp
    "~00108 1\r",   // Lamp time
    "~00351 1\r",   // Fan 1
};

template<typename C, typename M> static void publish(C *c, const M &m) {
  if (c)
    c->publish_state(m);
}

void OptomaRS232Component::publish_input_(const std::string &state) const {
  publish(beamer_input_select_, state);
  publish(beamer_input_text_sensor_, state);
}

void OptomaRS232Component::publish_power_(const bool state) const {
  publish(beamer_power_binary_sensor_, state);
  publish(beamer_power_switch_, state);
}

void OptomaRS232Component::dump_config() {
  ESP_LOGCONFIG(TAG, "Optoma RS232:");
  check_uart_settings(9600);
}

void OptomaRS232Component::loop() {
  if (!available())
    return;

  // pos = 0 and crlf: none
  // pos > 0 and crlf: send
  // pos < end and not crlf: add
  // pos == end and not crlf: discard
  while (available()) {
    uint8_t c;
    if (!read_byte(&c))
      continue;
    if (!c)
      continue;

    if (c == '\r' || c == '\n') {
      if (cursor_ > 0) {
        buffer_[cursor_] = 0;
        process_line_(buffer_);
        cursor_ = 0;
      }
    } else if (cursor_ < sizeof(buffer_) - 1) {
      buffer_[cursor_++] = toupper(c);
    }
  }
}

void OptomaRS232Component::update() {
  last_query_ = (last_query_ + 1) % std::size(QUERY_DATA);
  write_array(reinterpret_cast<const uint8_t *>(QUERY_DATA[last_query_]), strlen(QUERY_DATA[last_query_]));
}

void OptomaRS232Component::beamer_input_select_changed(const std::string &state, size_t) {
  const char *data;
  Inputs inp{UNKNOWN};
  if (state == "Unknown")
    return;
  if (state == "HDMI 1") {
    inp = HDMI_1;
    data = "~0012 1\r";
  } else if (state == "HDMI 2") {
    inp = HDMI_2;
    data = "~0012 15\r";
  } else if (state == "VGA") {
    inp = VGA;
    data = "~0012 5\r";  // VGA 1
  } else {
    return;
  }
  if (inp == current_input_)
    return;
  current_input_ = inp;
  write_array(reinterpret_cast<const uint8_t *>(data), strlen(data));
}

void OptomaRS232Component::beamer_power_switch_changed(const bool state) {
  const char *cmd = state ? "~0000 1\r" : "~0000 2\r";
  write_array(reinterpret_cast<const uint8_t *>(cmd), strlen(cmd));
}

void OptomaRS232Component::process_line_(const std::string &str) {
  // if we are waiting for the projector to respond to a command.
  // it will respond P (pass) or F (fail) before giving us the actual response to the command.
  if (str == "P" || str == "F")
    return;

  // assuming any commands have been dealt with above, we listen for messages from the projector
  // the OK-something messages are in response to status queries, sometimes these are in caps, sometimes not
  // hence the toUpperCase call earlier
  // the INFO messages come in automatically when the projector changes state
  if (                   // x == "OK1"   ||                    // status query returned power on
      str == "INFO1") {  // warming up
    publish_power_(true);
    return;
  }

  if (                   // x == "OK0"   ||                    // status query returned power off
      str == "INFO2" ||  // cooling down
      str == "INFO0") {  // going into standby
    publish_power_(false);
    return;
  }

  if (str_startswith(str, "OK")) {
    ESP_LOGD(TAG, "OK-message: %s", str.c_str());
    process_query_response_(str);
    return;
  }

  // ESP_LOGD("projector", "unhandled message: %s", str.c_str());
}

void OptomaRS232Component::process_query_response_(const std::string &str) {
  if (str.length() >= 3) {
    switch (last_query_) {
      case QUERIES::TEMP:
        publish(beamer_temp_sensor_, strtol(str.c_str() + 2, 0, 10));
        break;
      case QUERIES::LAMP_TIME:
        if (str.length() == 7)
          publish(beamer_lamp_time_sensor_, strtol(str.c_str() + 2, 0, 10));
        break;
      case QUERIES::FAN_1:
        publish(beamer_fan1_sensor_, strtol(str.c_str() + 2, 0, 10));
        break;
      case QUERIES::INFO:
        if (str.length() >= 13) {
          char buf[17]{};
          strncpy(buf, str.c_str(), sizeof(buf));
          const auto color_mode = strtol(buf + 14, 0, 10);
          buf[14] = 0;
          const auto firmware = strtol(buf + 10, 0, 10);
          buf[10] = 0;
          const auto input = strtol(buf + 8, 0, 10);
          buf[8] = 0;
          const auto lamp_time = strtol(buf + 3, 0, 10);
          buf[3] = 0;
          const auto power = strtol(buf + 2, 0, 10);

          publish_power_(power);
          // publish(beamer_firmware_, firmware);
          if (power)
            publish(beamer_lamp_time_sensor_, lamp_time);
          publish(beamer_color_mode_sensor_, color_mode);

          switch (input) {
            case Inputs::HDMI_1:
              current_input_ = Inputs::HDMI_1;
              publish_input_("HDMI 1");
              break;
            case Inputs::HDMI_2:
              current_input_ = Inputs::HDMI_2;
              publish_input_("HDMI 2");
              break;
            case Inputs::VGA:
              current_input_ = Inputs::VGA;
              publish_input_("VGA");
              break;
            default:
            case Inputs::UNKNOWN:
              current_input_ = Inputs::UNKNOWN;
              publish_input_("Unknown");
              break;
          }
          break;
        }
      default:;
    }
  }
}

#ifdef USE_SELECT
void InputSelect::control(const std::string &value) {
  this->publish_state(value);
  auto index = this->index_of(value);
  if (index.has_value() && this->parent_)
    this->parent_->beamer_input_select_changed(value, *index);
}
#endif

#ifdef USE_SWITCH
void PowerSwitch::write_state(const bool state) {
  this->publish_state(state);
  if (this->parent_)
    this->parent_->beamer_power_switch_changed(state);
}
#endif

}  // namespace optoma_rs232
}  // namespace esphome