#include "tw7100.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/components/uart/uart.h"

#ifdef USE_SELECT
#include "select/tw7100_select.h"
#endif

namespace esphome {
namespace tw7100 {

bool waiting_for_answer = false;
bool waiting_for_error_report = false;
unsigned long waiting_for_answer_since = 0;
std::deque<std::string> cmd_queue;
unsigned long last_set_cmd_timestamp;

std::vector<std::string> pwr_off_query_cmds{ "PWR?", "LAMP?" };
std::vector<std::string> pwr_on_query_cmds{
#ifdef USE_SENSOR
  "SIGNAL?",
#endif
#ifdef USE_NUMBER
  "VOL?",
  "VKEYSTONE?",
  "HKEYSTONE?",
  "BRIGHT?",
  "CONTRAST?",
  "DENSITY?",
  "TINT?",
  "CTEMP?",
  "FCOLOR?",
  "NRS?",
  "MPEGNRS?",
  "OFFSETR?",
  "OFFSETG?",
  "OFFSETB?",
  "GAINR?",
  "GAING?",
  "GAINB?",
  "SHRF?",
  "SHRS?",
  "DERANGE?",
  "DESTRENGTH?",
#endif
#ifdef USE_SWITCH
  "BTAUDIO?",
  "4KENHANCE?",
  "IMGPROC?",
  "AUDIOOUT?",
  "HREVERSE?",
  "VREVERSE?",
  "ILLUM?",
  "STANDBYCONF?",
  "AUTOHOME?",
  "WLPWR?",
  "LOGTO?",
  "MUTE?",
#endif
#ifdef USE_SELECT
  "LUMINANCE?",
  "SOURCE?",
  "ASPECT?",
  "OVSCAN?",
  "CMODE?",
  "GAMMA?",
  "IMGPRESET?",
  "MCFI?",
  "CLRSPACE?",
  "DYNRANGE?",
  "MSEL?",
  "SPEED?",
#endif
// TODO:
//  "HDRPQ?", // number 01-16
//  "HDRHLG?", // number 01-16
//  "CSEL?",

//  "SNO?" // Pushed on demand if sensor value is empty (e.g. after boot-up)
/*
  "PRODUCT?", // produces ERR
  "SOURCELIST?" // Parser present in git history but removed until esphome can update selects after api connections are established
  "SOURCELISTA?", // same as SOURCELIST
  "QC?",
  "CORRECTMET?",
*/
};

void tw7100Component::setup() {
  static const char *const TAG = "setup()";
  ESP_LOGV(TAG, "SETUP");
#ifdef USE_SELECT
  if (source_select_ != nullptr) source_select_->setup();
  if (luminance_select_ != nullptr) luminance_select_->setup();
  if (aspect_select_ != nullptr) aspect_select_->setup();
  if (cmode_select_ != nullptr) cmode_select_->setup();
  if (gamma_select_ != nullptr) gamma_select_->setup();
  if (imgpreset_select_ != nullptr) imgpreset_select_->setup();
  if (mcfi_select_ != nullptr) mcfi_select_->setup();
  if (ovscan_select_ != nullptr) ovscan_select_->setup();
  if (clrspace_select_ != nullptr) clrspace_select_->setup();
  if (dynrange_select_ != nullptr) dynrange_select_->setup();
  if (msel_select_ != nullptr) msel_select_->setup();
  if (speed_select_ != nullptr) speed_select_->setup();
#endif
};

void tw7100Component::update() {
  static const char *const TAG = "update()";
  if (cmd_queue.empty()) {
    for (auto &cmd : pwr_off_query_cmds) {
      cmd_queue.push_back(cmd);
    }
    if ((powered_->state)) {
#ifdef USE_TEXT_SENSOR
      if (this->serial_->state.length() < 1) {
        cmd_queue.push_back("SNO?");
      }
#endif
      for (auto &cmd : pwr_on_query_cmds) {
        cmd_queue.push_back(cmd);
      }
    }
  }
};

void tw7100Component::loop() {
  static const char *const TAG = "loop()";
  unsigned long _startMillis;  // used for timeout measurement

  _startMillis = millis();
  do {
    std::string response = readStringUntil(':');

    if (response == "") { // no response on bus, we can use our time to do something else
      if (!cmd_queue.empty() && !waiting_for_answer && (millis() - last_set_cmd_timestamp > std::max(_timeout_after_cmd, _specialTimeoutMillis))) {
        std::string cmd = cmd_queue.front();
        cmd_queue.pop_front();
        ESP_LOGV(TAG, "sending out command: %s", cmd.c_str());
        write_str((cmd + "\r\n").c_str());
        flush();
        if (cmd.find("?") != std::string::npos) { // only wait for response on CMD? requests
          _specialTimeoutMillis = 0;
          waiting_for_answer = true;
          waiting_for_answer_since = millis();
        } else {
          last_set_cmd_timestamp = millis();
        }
      }
      if (waiting_for_answer && (millis() - waiting_for_answer_since > 1000)) {
        ESP_LOGE(TAG, "no response for last request since 1000ms, timing out!");
        waiting_for_answer = false;
      }
    } else { // response found, handle eventual errors
      ESP_LOGV(TAG, "buffer content: %s", response.c_str());
      std::pair<std::string, std::string> parsed_response = parse_response(response);
      if (parsed_response.first == "ERR" && parsed_response.second == "ERR") {
        if (powered_->state && !waiting_for_error_report) {
          ESP_LOGW(TAG, "found ERR response, pushing request to fetch full error");
          waiting_for_error_report = true;
          cmd_queue.push_front("ERR?");
        } else {
          ESP_LOGW(TAG, "got ERR while projector is off or waiting for a previous error report");
        }
      } else if (parsed_response.first == "ERR") {
        waiting_for_error_report = false;
        ESP_LOGE(TAG, "found ERR report, reason is: %s", parsed_response.second.c_str());
        // TODO: Handle error
      } else {
        update_sensor(parsed_response);
      }
      waiting_for_answer = false;
    }
    //ESP_LOGV(TAG, "read processing finished");
  } while (millis() - _startMillis < _max_loop_time);
  //ESP_LOGV(TAG, "inner timed loop done");
};

void tw7100Component::dump_config() {
  static const char *const TAG = "dump_config()";
  ESP_LOGCONFIG(TAG, "TW7100:");
  this->check_uart_settings(9600);
};

void tw7100Component::push_cmd(std::string cmd, std::string param) {
  static const char *const TAG = "push_cmd()";
  if (powered_->state) {
    if (cmd == "PWR" && param == "OFF") {
      _specialTimeoutMillis = 5000; // time to wait after PWR OFF until projector responds again
      cmd_queue.clear(); // clear eventual existing query commands to avoid errors
    }
    if ((cmd == "SOURCE")||(cmd == "MUTE")) {
      _specialTimeoutMillis = 4000; // time to wait after source change or AV mute
    }
    ESP_LOGV(TAG, "pushing priority cmd (%s) from external component", cmd.c_str());
    cmd_queue.push_front(cmd + "?");
    cmd_queue.push_front(cmd + " " + param);
  } else if (cmd == "PWR" && param == "ON") {
    _specialTimeoutMillis = 30000; // time to wait after PWR ON until projector responds again
    cmd_queue.clear(); // clear eventual existing query commands to avoid errors
    cmd_queue.push_front(cmd + "?");
    cmd_queue.push_front(cmd + " " + param);
  }
}

void tw7100Component::update_sensor(std::pair<std::string, std::string> data) {
  static const char *const TAG = "update_sensor()";
  std::string cmd = data.first;
  std::string value_string = data.second;
  if (cmd == "PWR") {
    ESP_LOGV(TAG, "updating power sensors"); 
    int value = std::stoi(value_string);
#ifdef USE_BINARY_SENSOR
    powered_->publish_state(value > 0);
#endif
#ifdef USE_SWITCH
    power_switch_->publish_state(value > 0); // ack previous cmd or update switch
#endif
#ifdef USE_SENSOR
    power_status_->publish_state(value);
  } else if (cmd == "LAMP") {
    ESP_LOGV(TAG, "updating lamp sensors");
    int value = std::stoi(value_string);
    lamp_hours_->publish_state(value);
  } else if (cmd == "SIGNAL") {
    ESP_LOGV(TAG, "updating signal sensors");
    int value = std::stoi(value_string);
    has_signal_->publish_state(value > 0);
    signal_status_->publish_state(value);
#endif
#ifdef USE_SELECT
  } else if (cmd == "LUMINANCE") {
    ESP_LOGV(TAG, "updating luminance sensors");
    int value = std::stoi(value_string);
    luminance_level_->publish_state(value);
    auto call = luminance_select_->make_call();
    call.set_option(luminance_select_->luminance[value_string]);
    call.perform();
  } else if (cmd == "SOURCE") {
    ESP_LOGV(TAG, "updating source select");
    auto call = source_select_->make_call();
    call.set_option(source_select_->sources[value_string]);
    call.perform();
  } else if (cmd == "ASPECT") {
    ESP_LOGV(TAG, "updating aspect select");
    if(aspect_select_->aspect.find(value_string) != aspect_select_->aspect.end()) {
      auto call = aspect_select_->make_call();
      call.set_option(aspect_select_->aspect[value_string]);
      call.perform();
    } else {
      ESP_LOGE(TAG, "Result %s not present in options", value_string);
    }
  } else if (cmd == "OVSCAN") {
    ESP_LOGV(TAG, "updating ovscan select");
  } else if (cmd == "CMODE") {
    ESP_LOGV(TAG, "updating cmode select");
  } else if (cmd == "GAMMA") {
    ESP_LOGV(TAG, "updating gamma select");
  } else if (cmd == "IMGPRESET") {
    ESP_LOGV(TAG, "updating imgpreset select");
  } else if (cmd == "MCFI") {
    ESP_LOGV(TAG, "updating mcfi select");
  } else if (cmd == "CLRSPACE") {
    ESP_LOGV(TAG, "updating clrspace select");
  } else if (cmd == "DYNRANGE") {
    ESP_LOGV(TAG, "updating dynrange select");
  } else if (cmd == "MSEL") {
    ESP_LOGV(TAG, "updating msel select");
  } else if (cmd == "SPEED") {
    ESP_LOGV(TAG, "updating speed select");
#endif
#ifdef USE_TEXT_SENSOR
  } else if (cmd == "SNO") {
    serial_->publish_state(value_string);
#endif
#ifdef USE_SWITCH
  } else if (cmd == "BTAUDIO") {
    ESP_LOGV(TAG, "updating btaudio switch");
    int value = std::stoi(value_string);
    btaudio_switch_->publish_state(value > 0);
  } else if (cmd == "4KENHANCE") {
    ESP_LOGV(TAG, "updating 4kenhance switch");
    int value = std::stoi(value_string);
    _4kenhance_switch_->publish_state(value > 0);
  } else if (cmd == "IMGPROC") {
    ESP_LOGV(TAG, "updating imgproc switch");
    int value = std::stoi(value_string);
    img_processing_switch_->publish_state(value > 1);
  } else if (cmd == "AUDIOOUT") {
    ESP_LOGV(TAG, "updating audioout switch");
    int value = std::stoi(value_string);
    audioout_switch_->publish_state(value == 11);
  } else if (cmd == "HREVERSE") {
    ESP_LOGV(TAG, "updating hflip switch");
    hflip_switch_->publish_state(value_string == "ON");
  } else if (cmd == "VREVERSE") {
    ESP_LOGV(TAG, "updating vflip switch");
    vflip_switch_->publish_state(value_string == "ON");
  } else if (cmd == "ILLUM") {
    ESP_LOGV(TAG, "updating illumination switch");
    int value = std::stoi(value_string);
    illumination_switch_->publish_state(value == 1);
  } else if (cmd == "STANDBYCONF") {
    ESP_LOGV(TAG, "updating standbyconf switch");
    int value = std::stoi(value_string);
    standbyconf_switch_->publish_state(value == 1);
  } else if (cmd == "AUTOHOME") {
    ESP_LOGV(TAG, "updating autohome switch");
    int value = std::stoi(value_string);
    autohome_switch_->publish_state(value == 1);
  } else if (cmd == "WLPWR") {
    ESP_LOGV(TAG, "updating wlan power switch");
    int value = std::stoi(value_string);
    wlan_power_switch_->publish_state(value == 1);
  } else if (cmd == "LOGTO") {
    ESP_LOGV(TAG, "updating logto switch");
    int value = std::stoi(value_string);
    logto_switch_->publish_state(value == 1);
  } else if (cmd == "MUTE") {
    ESP_LOGV(TAG, "updating mute switch with value %s", value_string.c_str());
    mute_switch_->publish_state(value_string == "ON");
#endif
#ifdef USE_NUMBER
  } else if (cmd == "VOL") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating volume number with value %s", value_string.c_str());
    volume_number_->publish_state(value);
  } else if (cmd == "VKEYSTONE") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating vkeystone number with value %s", value_string.c_str());
    vkeystone_number_->publish_state(value);
  } else if (cmd == "HKEYSTONE") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating hkeystone number with value %s", value_string.c_str());
    hkeystone_number_->publish_state(value);
  } else if (cmd == "BRIGHT") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating brightness number with value %s", value_string.c_str());
    if (brightness_number_ != nullptr) brightness_number_->publish_state(value);
  } else if (cmd == "CONTRAST") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating contrast number with value %s", value_string.c_str());
    contrast_number_->publish_state(value);
  } else if (cmd == "DENSITY") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating density number with value %s", value_string.c_str());
    density_number_->publish_state(value);
  } else if (cmd == "TINT") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating tint number with value %s", value_string.c_str());
    tint_number_->publish_state(value);
  } else if (cmd == "CTEMP") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating ctemp number with value %s", value_string.c_str());
    ctemp_number_->publish_state(value);
  } else if (cmd == "FCOLOR") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating fcolor number with value %s", value_string.c_str());
    fcolor_number_->publish_state(value);
  } else if (cmd == "NRS") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating nrs number with value %s", value_string.c_str());
    nrs_number_->publish_state(value);
  } else if (cmd == "MPEGNRS") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating mpegngs number with value %s", value_string.c_str());
    mpegnrs_number_->publish_state(value);
  } else if (cmd == "OFFSETR") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating offsetr number with value %s", value_string.c_str());
    offsetr_number_->publish_state(value);
  } else if (cmd == "OFFSETG") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating offsetg number with value %s", value_string.c_str());
    offsetg_number_->publish_state(value);
  } else if (cmd == "OFFSETB") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating offsetb number with value %s", value_string.c_str());
    oggsetb_number_->publish_state(value);
  } else if (cmd == "GAINR") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating gainr number with value %s", value_string.c_str());
    gainr_number_->publish_state(value);
  } else if (cmd == "GAING") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating gaing number with value %s", value_string.c_str());
    gaing_number_->publish_state(value);
  } else if (cmd == "GAINB") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating gainb number with value %s", value_string.c_str());
    gainb_number_->publish_state(value);
  } else if (cmd == "SHRF") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating shrf number with value %s", value_string.c_str());
    shrf_number_->publish_state(value);
  } else if (cmd == "SHRS") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating shrs number with value %s", value_string.c_str());
    shrs_number_->publish_state(value);
  } else if (cmd == "DERANGE") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating derange number with value %s", value_string.c_str());
    derange_number_->publish_state(value);
  } else if (cmd == "DESTRENGTH") {
    int value = std::stoi(value_string);
    ESP_LOGV(TAG, "updating destrength number with value %s", value_string.c_str());
    destrength_number_->publish_state(value);
#endif
  } else {
    ESP_LOGW(TAG, "Command %s unknown, skipping updating sensor with value", cmd.c_str());
  }
}

std::pair<std::string, std::string> tw7100Component::parse_response(std::string data) {
  static const char *const TAG = "parse_response()";
  std::string key = "";
  std::string value = "";
  data.erase(std::remove(data.begin(), data.end(), '\r'), data.cend());
  data.erase(std::remove(data.begin(), data.end(), '\n'), data.cend());
  if (data != "") {
    key = data.substr(0, data.find("="));
    value = data.substr(data.find("=") + 1, data.length());
  }

  ESP_LOGV(TAG, "parsed response into (key: %s, value: %s)", key.c_str(), value.c_str());
  return std::make_pair(key, value);
}

int tw7100Component::timedRead() {
  unsigned long _startMillis;  // used for timeout measurement
  int c;
  _startMillis = millis();
  do {
    if (this->available() > 0) {
      c = this->read();
      //ESP_LOGV("timedRead()", "timed read byte: %c (%x)", c, c);
      if (c >= 0) {
        return c;
      }
    }
  } while (millis() - _startMillis < _readStringUntil_timeout);
  return -1;  // -1 indicates timeout
}

std::string tw7100Component::readStringUntil(char terminator) {
  std::string line="";
  int c = timedRead();
  while (c >= 0 && (char)c != terminator) {
    line += (char)c;
    c = timedRead();
  }
  return line;
};

}  // namespace tw7100
}  // namespace esphome