#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 powered = false;
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?",
#ifdef USE_SENSOR
  "LAMP?"
#endif
};
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_) source_select_->setup();
  if (luminance_select_) luminance_select_->setup();
  if (aspect_select_) aspect_select_->setup();
  if (cmode_select_) cmode_select_->setup();
  if (gamma_select_) gamma_select_->setup();
  if (imgpreset_select_) imgpreset_select_->setup();
  if (mcfi_select_) mcfi_select_->setup();
  if (ovscan_select_) ovscan_select_->setup();
  if (clrspace_select_) clrspace_select_->setup();
  if (dynrange_select_) dynrange_select_->setup();
  if (msel_select_) msel_select_->setup();
  if (speed_select_) 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) {
#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 && !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) {
    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()";
  const std::string cmd = data.first;
  const std::string value_string = data.second;
  if (cmd == "PWR") {
    ESP_LOGV(TAG, "updating power sensors"); 
    int value = std::stoi(value_string);
    powered = (value > 0);
#ifdef USE_BINARY_SENSOR
    //if (powered_) powered_->publish_state(value > 0);
#endif
#ifdef USE_SWITCH
    if (power_switch_) power_switch_->publish_state(value > 0); // ack previous cmd or update switch
#endif
#ifdef USE_SENSOR
    if (power_status_) power_status_->publish_state(value);
#endif
  } else if (cmd == "LAMP") {
    ESP_LOGV(TAG, "updating lamp sensors");
#ifdef USE_SENSOR
    int value = std::stoi(value_string);
    if (lamp_hours_) lamp_hours_->publish_state(value);
#endif
  } else if (cmd == "SIGNAL") {
    ESP_LOGV(TAG, "updating signal sensors");
    int value = std::stoi(value_string);
    has_signal_->publish_state(value > 0);
    if (signal_status_) signal_status_->publish_state(value);
  } else if (cmd == "LUMINANCE") {
    ESP_LOGV(TAG, "updating luminance sensors");
#ifdef USE_SENSOR
    int value = std::stoi(value_string);
    if (luminance_level_) luminance_level_->publish_state(value);
#endif
#ifdef USE_SELECT
    if (luminance_select_) {
      if(tw7100Select::luminance.find(value_string) != tw7100Select::luminance.end()) {
        auto call = luminance_select_->make_call();
        call.set_option(tw7100Select::luminance[value_string]);
        call.perform();
      } else {
        ESP_LOGE(TAG, "Result %s not present in options", value_string);
      }
    }
#endif
  } else if (cmd == "SOURCE") {
    ESP_LOGV(TAG, "updating source select");
#ifdef USE_SELECT
    if (source_select_) {
      if(tw7100Select::sources.find(value_string) != tw7100Select::sources.end()) {
        auto call = source_select_->make_call();
        call.set_option(tw7100Select::sources[value_string]);
        call.perform();
      } else {
        ESP_LOGE(TAG, "Result %s not present in options", value_string);
      }
    }
#endif
  } else if (cmd == "ASPECT") {
    ESP_LOGV(TAG, "updating aspect select");
#ifdef USE_SELECT
    if (aspect_select_) {
      if(tw7100Select::aspect.find(value_string) != tw7100Select::aspect.end()) {
        auto call = aspect_select_->make_call();
        call.set_option(tw7100Select::aspect[value_string]);
        call.perform();
      } else {
        ESP_LOGE(TAG, "Result %s not present in options", value_string);
      }
    }
#endif
  } 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");
#ifdef USE_TEXT_SENSOR
  } else if (cmd == "SNO") {
    if (serial_ != nullptr) serial_->publish_state(value_string);
#endif
  } else if (cmd == "BTAUDIO") {
    ESP_LOGV(TAG, "updating btaudio switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (btaudio_switch_) btaudio_switch_->publish_state(value > 0);
#endif
  } else if (cmd == "4KENHANCE") {
    ESP_LOGV(TAG, "updating 4kenhance switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (_4kenhance_switch_) _4kenhance_switch_->publish_state(value > 0);
#endif
  } else if (cmd == "IMGPROC") {
    ESP_LOGV(TAG, "updating imgproc switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (img_processing_switch_) img_processing_switch_->publish_state(value > 1);
#endif
  } else if (cmd == "AUDIOOUT") {
    ESP_LOGV(TAG, "updating audioout switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (audioout_switch_) audioout_switch_->publish_state(value == 11);
#endif
  } else if (cmd == "HREVERSE") {
    ESP_LOGV(TAG, "updating hflip switch");
#ifdef USE_SWITCH
    if (hflip_switch_) hflip_switch_->publish_state(value_string == "ON");
#endif
  } else if (cmd == "VREVERSE") {
    ESP_LOGV(TAG, "updating vflip switch");
#ifdef USE_SWITCH
    if (vflip_switch_) vflip_switch_->publish_state(value_string == "ON");
#endif
  } else if (cmd == "ILLUM") {
    ESP_LOGV(TAG, "updating illumination switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (illumination_switch_) illumination_switch_->publish_state(value == 1);
#endif
  } else if (cmd == "STANDBYCONF") {
    ESP_LOGV(TAG, "updating standbyconf switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (standbyconf_switch_) standbyconf_switch_->publish_state(value == 1);
#endif
  } else if (cmd == "AUTOHOME") {
    ESP_LOGV(TAG, "updating autohome switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (autohome_switch_) autohome_switch_->publish_state(value == 1);
#endif
  } else if (cmd == "WLPWR") {
    ESP_LOGV(TAG, "updating wlan power switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (wlan_power_switch_) wlan_power_switch_->publish_state(value == 1);
#endif
  } else if (cmd == "LOGTO") {
    ESP_LOGV(TAG, "updating logto switch");
#ifdef USE_SWITCH
    int value = std::stoi(value_string);
    if (logto_switch_) logto_switch_->publish_state(value == 1);
#endif
  } else if (cmd == "MUTE") {
    ESP_LOGV(TAG, "updating mute switch with value %s", value_string.c_str());
#ifdef USE_SWITCH
    if (mute_switch_) mute_switch_->publish_state(value_string == "ON");
#endif
  } else if (cmd == "VOL") {
    ESP_LOGV(TAG, "updating volume number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (volume_number_) volume_number_->publish_state(value);
#endif
  } else if (cmd == "VKEYSTONE") {
    ESP_LOGV(TAG, "updating vkeystone number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (vkeystone_number_) vkeystone_number_->publish_state(value);
#endif
  } else if (cmd == "HKEYSTONE") {
    ESP_LOGV(TAG, "updating hkeystone number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (hkeystone_number_) hkeystone_number_->publish_state(value);
#endif
  } else if (cmd == "BRIGHT") {
    ESP_LOGV(TAG, "updating brightness number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (brightness_number_) brightness_number_->publish_state(value);
#endif
  } else if (cmd == "CONTRAST") {
    ESP_LOGV(TAG, "updating contrast number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (contrast_number_) contrast_number_->publish_state(value);
#endif
  } else if (cmd == "DENSITY") {
    ESP_LOGV(TAG, "updating density number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (density_number_) density_number_->publish_state(value);
#endif
  } else if (cmd == "TINT") {
    ESP_LOGV(TAG, "updating tint number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (tint_number_) tint_number_->publish_state(value);
#endif
  } else if (cmd == "CTEMP") {
    ESP_LOGV(TAG, "updating ctemp number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (ctemp_number_) ctemp_number_->publish_state(value);
#endif
  } else if (cmd == "FCOLOR") {
    ESP_LOGV(TAG, "updating fcolor number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (fcolor_number_) fcolor_number_->publish_state(value);
#endif
  } else if (cmd == "NRS") {
    ESP_LOGV(TAG, "updating nrs number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (nrs_number_) nrs_number_->publish_state(value);
#endif
  } else if (cmd == "MPEGNRS") {
    ESP_LOGV(TAG, "updating mpegngs number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (mpegnrs_number_) mpegnrs_number_->publish_state(value);
#endif
  } else if (cmd == "OFFSETR") {
    ESP_LOGV(TAG, "updating offsetr number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (offsetr_number_) offsetr_number_->publish_state(value);
#endif
  } else if (cmd == "OFFSETG") {
    ESP_LOGV(TAG, "updating offsetg number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (offsetg_number_) offsetg_number_->publish_state(value);
#endif
  } else if (cmd == "OFFSETB") {
    ESP_LOGV(TAG, "updating offsetb number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (offsetb_number_) offsetb_number_->publish_state(value);
#endif
  } else if (cmd == "GAINR") {
    ESP_LOGV(TAG, "updating gainr number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (gainr_number_) gainr_number_->publish_state(value);
#endif
  } else if (cmd == "GAING") {
    ESP_LOGV(TAG, "updating gaing number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (gaing_number_) gaing_number_->publish_state(value);
#endif
  } else if (cmd == "GAINB") {
    ESP_LOGV(TAG, "updating gainb number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (gainb_number_) gainb_number_->publish_state(value);
#endif
  } else if (cmd == "SHRF") {
    ESP_LOGV(TAG, "updating shrf number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (shrf_number_) shrf_number_->publish_state(value);
#endif
  } else if (cmd == "SHRS") {
    ESP_LOGV(TAG, "updating shrs number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (shrs_number_) shrs_number_->publish_state(value);
#endif
  } else if (cmd == "DERANGE") {
    ESP_LOGV(TAG, "updating derange number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (derange_number_) derange_number_->publish_state(value);
#endif
  } else if (cmd == "DESTRENGTH") {
    ESP_LOGV(TAG, "updating destrength number with value %s", value_string.c_str());
#ifdef USE_NUMBER
    int value = std::stoi(value_string);
    if (destrength_number_) 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