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

namespace esphome::noblex {

static const char *const TAG = "noblex.climate";

const uint16_t NOBLEX_HEADER_MARK = 9000;
const uint16_t NOBLEX_HEADER_SPACE = 4500;
const uint16_t NOBLEX_BIT_MARK = 660;
const uint16_t NOBLEX_ONE_SPACE = 1640;
const uint16_t NOBLEX_ZERO_SPACE = 520;
const uint32_t NOBLEX_GAP = 20000;
const uint8_t NOBLEX_POWER = 0x10;

using IRNoblexMode = enum IRNoblexMode {
  IR_NOBLEX_MODE_AUTO = 0b000,
  IR_NOBLEX_MODE_COOL = 0b100,
  IR_NOBLEX_MODE_DRY = 0b010,
  IR_NOBLEX_MODE_FAN = 0b110,
  IR_NOBLEX_MODE_HEAT = 0b001,
};

using IRNoblexFan = enum IRNoblexFan {
  IR_NOBLEX_FAN_AUTO = 0b00,
  IR_NOBLEX_FAN_LOW = 0b10,
  IR_NOBLEX_FAN_MEDIUM = 0b01,
  IR_NOBLEX_FAN_HIGH = 0b11,
};

// Transmit via IR the state of this climate controller.
void NoblexClimate::transmit_state() {
  uint8_t remote_state[8] = {0x80, 0x10, 0x00, 0x0A, 0x50, 0x00, 0x20, 0x00};  // OFF, COOL, 24C, FAN_AUTO

  auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
  if (powered_on) {
    remote_state[0] |= 0x10;  // power bit
    remote_state[2] = 0x02;
  }
  if (powered_on != this->powered_on_assumed)
    this->powered_on_assumed = powered_on;

  auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX));
  remote_state[1] = reverse_bits(uint8_t((temp - NOBLEX_TEMP_MIN) & 0x0F));

  switch (this->mode) {
    case climate::CLIMATE_MODE_HEAT_COOL:
      remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_AUTO << 5);
      remote_state[1] = 0x90;  // NOBLEX_TEMP_MAP 25C
      break;
    case climate::CLIMATE_MODE_COOL:
      remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_COOL << 5);
      break;
    case climate::CLIMATE_MODE_DRY:
      remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_DRY << 5);
      break;
    case climate::CLIMATE_MODE_FAN_ONLY:
      remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_FAN << 5);
      break;
    case climate::CLIMATE_MODE_HEAT:
      remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_HEAT << 5);
      break;
    case climate::CLIMATE_MODE_OFF:
    default:
      powered_on = false;
      this->powered_on_assumed = powered_on;
      remote_state[0] &= 0xEF;
      remote_state[2] = 0x00;
      break;
  }

  switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
    case climate::CLIMATE_FAN_LOW:
      remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2);
      break;
    case climate::CLIMATE_FAN_MEDIUM:
      remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_MEDIUM << 2);
      break;
    case climate::CLIMATE_FAN_HIGH:
      remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_HIGH << 2);
      break;
    case climate::CLIMATE_FAN_AUTO:
    default:
      remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_AUTO << 2);
      break;
  }

  switch (this->swing_mode) {
    case climate::CLIMATE_SWING_VERTICAL:
      remote_state[0] |= 0x02;
      remote_state[4] = 0x58;
      break;
    case climate::CLIMATE_SWING_OFF:
    default:
      remote_state[0] &= 0xFD;
      remote_state[4] = 0x50;
      break;
  }

  uint8_t crc = 0;
  for (uint8_t i : remote_state) {
    crc += reverse_bits(i);
  }
  crc = reverse_bits(uint8_t(crc & 0x0F)) >> 4;

  ESP_LOGD(TAG, "Sending noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1],
           remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]);

  ESP_LOGV(TAG, "CRC: %01X", crc);

  auto transmit = this->transmitter_->transmit();
  auto *data = transmit.get_data();
  data->set_carrier_frequency(38000);

  // Header
  data->mark(NOBLEX_HEADER_MARK);
  data->space(NOBLEX_HEADER_SPACE);
  // Data (sent remote_state from the MSB to the LSB)
  for (int byte_idx = 0; byte_idx < 8; byte_idx++) {
    for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) {
      if ((byte_idx == 4) && (bit_idx == 4)) {
        // Header intermediate
        data->mark(NOBLEX_BIT_MARK);
        data->space(NOBLEX_GAP);  // gap en bit 36
      } else {
        data->mark(NOBLEX_BIT_MARK);
        bool bit = remote_state[byte_idx] & (1 << bit_idx);
        data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
      }
    }
  }
  // send crc
  for (int8_t i = 3; i >= 0; i--) {
    data->mark(NOBLEX_BIT_MARK);
    bool bit = crc & (1 << i);
    data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
  }
  // Footer
  data->mark(NOBLEX_BIT_MARK);

  transmit.perform();
}  // end transmit_state()

// Handle received IR Buffer
bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
  if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) {
    // First part: header + first 36 bits, followed by 20ms gap
    data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE);
    ESP_LOGV(TAG, "Header");
    this->receiving_ = false;
    memset(this->remote_state_, 0, sizeof(this->remote_state_));
    for (int i = 0; i < 5; i++) {
      for (int j = 7; j >= 0; j--) {
        if ((i == 4) && (j == 4)) {
          this->remote_state_[i] |= 1 << j;
          ESP_LOGVV(TAG, "GAP");
          this->receiving_ = true;
          return false;
        } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
          this->remote_state_[i] |= 1 << j;
        } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
          ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
          return false;
        }
      }
      ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
    }
    return false;
  }

  // Second part: remaining 28 bits + 4-bit CRC + footer
  if (!this->receiving_) {
    return false;
  }
  this->receiving_ = false;
  for (int i = 4; i < 8; i++) {
    for (int j = 7; j >= 0; j--) {
      if ((i == 4) && (j >= 4)) {
        // already decoded in first part
      } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
        this->remote_state_[i] |= 1 << j;
      } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
        ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
        return false;
      }
    }
    ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
  }

  // Read CRC
  uint8_t crc = 0;
  for (int i = 3; i >= 0; i--) {
    if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
      crc |= 1 << i;
    } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
      ESP_LOGVV(TAG, "Bit %d CRC fail", i);
      return false;
    }
  }
  ESP_LOGV(TAG, "CRC %02X", crc);

  // Validate footer
  if (!data.expect_mark(NOBLEX_BIT_MARK)) {
    ESP_LOGV(TAG, "Footer fail");
    return false;
  }

  // Validate CRC
  uint8_t crc_calculated = 0;
  for (uint8_t i : this->remote_state_)
    crc_calculated += reverse_bits(i);
  crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4;
  ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated);

  if (crc != crc_calculated) {
    ESP_LOGV(TAG, "CRC fail");
    return false;
  }

  ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0],
           this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4],
           this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]);

  auto powered_on = false;
  if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) {
    powered_on = true;
    this->powered_on_assumed = powered_on;
  } else {
    powered_on = false;
    this->powered_on_assumed = powered_on;
    this->mode = climate::CLIMATE_MODE_OFF;
  }
  // powr on/off button
  ESP_LOGV(TAG, "Power: %01X", powered_on);

  // Set received mode
  if (powered_on_assumed) {
    auto mode = (this->remote_state_[0] & 0xE0) >> 5;
    ESP_LOGV(TAG, "Mode: %02X", mode);
    switch (mode) {
      case IRNoblexMode::IR_NOBLEX_MODE_AUTO:
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
        break;
      case IRNoblexMode::IR_NOBLEX_MODE_COOL:
        this->mode = climate::CLIMATE_MODE_COOL;
        break;
      case IRNoblexMode::IR_NOBLEX_MODE_DRY:
        this->mode = climate::CLIMATE_MODE_DRY;
        break;
      case IRNoblexMode::IR_NOBLEX_MODE_FAN:
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
        break;
      case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
        this->mode = climate::CLIMATE_MODE_HEAT;
        break;
    }
  }

  // Set received temp
  uint8_t temp = this->remote_state_[1];
  ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);

  temp = 0x0F & reverse_bits(temp);
  temp += NOBLEX_TEMP_MIN;
  ESP_LOGV(TAG, "Temperature Climate: %u", temp);
  this->target_temperature = temp;

  // Set received fan speed
  auto fan = (this->remote_state_[0] & 0x0C) >> 2;
  ESP_LOGV(TAG, "Fan: %02X", fan);
  switch (fan) {
    case IRNoblexFan::IR_NOBLEX_FAN_HIGH:
      this->fan_mode = climate::CLIMATE_FAN_HIGH;
      break;
    case IRNoblexFan::IR_NOBLEX_FAN_MEDIUM:
      this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
      break;
    case IRNoblexFan::IR_NOBLEX_FAN_LOW:
      this->fan_mode = climate::CLIMATE_FAN_LOW;
      break;
    case IRNoblexFan::IR_NOBLEX_FAN_AUTO:
    default:
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
      break;
  }

  // Set received swing status
  if (this->remote_state_[0] & 0x02) {
    ESP_LOGV(TAG, "Swing vertical");
    this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
  } else {
    ESP_LOGV(TAG, "Swing OFF");
    this->swing_mode = climate::CLIMATE_SWING_OFF;
  }

  this->publish_state();
  return true;
}  // end on_receive()

}  // namespace esphome::noblex
