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

namespace esphome {
namespace tw7100 {

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

std::map<int, std::string> sourcelist;
std::vector<std::string> pwr_off_query_cmds{ "PWR?", "LAMP?" };
std::vector<std::string> pwr_on_query_cmds{
// Projection screen adjustment settings
/*
  "VKEYSTONE?",
  "HKEYSTONE?",
  "QC?",
  "CORRECTMET?",
  "ASPECT?",
  "LUMINANCE?",
  "OVSCAN?",
*/
//
// Source/Input/Resolution settings
//  "SOURCE?", // TODO: implement parser
// Image settings
/*
  "BRIGHT?",
  "CONTRAST?",
  "DENSITY?",
  "TINT?",
  "CTEMP?",
  "FCOLOR?",
  "CMODE?",
  "NRS?",
  "MPEGNRS?",
  "OFFSETR?",
  "OFFSETG?",
  "OFFSETB?",
  "GAINR?",
  "GAING?",
  "GAINB?",
  "GAMMA?",
  "CSEL?",
  "4KENHANCE?",
  "IMGPRESET?",
  "SHRF?",
  "SHRS?",
  "DERANGE?",
  "MCFI?",
  "CLRSPACE?",
  "DYNRANGE?",
  "HDRPQ?",
  "HDRHLG?",
  "IMGPROC?",
*/ 
//
// Sound settings
/*
  "VOL?",
  "AUDIOOUT?",
*/
//
//  "MUTE?", // TODO: implement parser
// Environment settings
/*
  "HREVERSE?",
  "VREVERSE?",
  "MSEL?",
  "SPEED?",
  "ILLUM?",
  "STANDBYCONF?",
*/
//  "PRODUCT?",
//
// Home Screen settings
//  "AUTOHOME?", // TODO: implement parser
// Network settings
//  "WLPWR?", // TODO: implement parser
// Bluetooth
  "BTAUDIO?", // TODO: implement parser
// Information
  "SIGNAL?",
//  "SOURCELIST?" // Pushed on demand if sensor value is empty (e.g. after boot-up)
//  "SOURCELISTA?", // same as SOURCELIST
//  "LOGTO?", // TODO: implement parser
//  "SNO?" // Pushed on demand if sensor value is empty (e.g. after boot-up)
};

void tw7100Component::setup() {
  static const char *const TAG = "setup()";
  ESP_LOGV(TAG, "SETUP");
};

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)) {
      if (this->serial_->state.length() < 1) {
        cmd_queue.push_back("SNO?");
      }
      if (sourcelist.empty()) {
        cmd_queue.push_back("SOURCELIST?");
      }
      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 > _timeout_after_cmd)) {
        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
          waiting_for_answer = true;
	} else {
          last_set_cmd_timestamp = millis();
	}
      }
    } 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) {;
          cmd_queue.push_back("ERR?"); // TODO: produces a ERR? loop on bootup if projector is turned off
        } else {
          ESP_LOGE(TAG, "got ERR while projector is off, check if PWR+LAMP is too much?");
        }
      } else if (parsed_response.first == "ERR") {
        // 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) {
    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") {
    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);
    powered_->publish_state(value > 0);
    power_switch_->publish_state(value > 0); // ack previous cmd or update switch
    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);
  } else if (cmd == "LUMINANCE") {
    ESP_LOGV(TAG, "updating luminance sensors");
    int value = std::stoi(value_string);
    luminance_level_->publish_state(value);
  } else if (cmd == "SOURCELIST") {
    std::string unparsed_result = value_string;
    int first_token_end;
    int second_token_end;

    uint8_t parse_rounds = 0; // safety fallback if data contains unexpected content
    do {
      first_token_end = unparsed_result.find(" ");
      second_token_end = unparsed_result.find(" ", first_token_end+1);
      ESP_LOGV(TAG, "first_token_end: %i, second_token_end: %i, unparsed_result.length(): %li", first_token_end, second_token_end, unparsed_result.length() );

      std::string key_string = unparsed_result.substr(0, first_token_end);
      std::string value = unparsed_result.substr(first_token_end+1, second_token_end-first_token_end-1);
      int key = std::stoi(key_string, nullptr, 16);
      sourcelist[key] = value;
      ESP_LOGV(TAG, "Added %s with key %i(0x%02x) to sourcelist", value.c_str(), key, key);

      unparsed_result.erase(0, second_token_end+1);
      ESP_LOGV(TAG, "rest string: %s", unparsed_result.c_str());

      parse_rounds+=1;
    } while (second_token_end > 0 && parse_rounds <= 50);
    // TODO: if we exceed parse_rounds we most likely encountered a critical issue, inform user somehow
  } else if (cmd == "SNO") {
    serial_->publish_state(value_string);
  } else if (cmd == "BTAUDIO") {
    ESP_LOGV(TAG, "updating btaudio switch");
    int value = std::stoi(value_string);
    btaudio_switch_->publish_state(value > 0);
  } 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