#include "daikin_arc.h"

#include <cmath>

#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h"

namespace esphome::daikin_arc {

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

void DaikinArcClimate::setup() {
  climate_ir::ClimateIR::setup();

  // Never send nan to HA
  if (std::isnan(this->target_humidity))
    this->target_humidity = 0;
  if (std::isnan(this->current_temperature))
    this->current_temperature = 0;
  if (std::isnan(this->current_humidity))
    this->current_humidity = 0;
}

void DaikinArcClimate::transmit_query_() {
  uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};

  // Calculate checksum
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
  }

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

  data->mark(DAIKIN_ARC_PRE_MARK);
  data->space(DAIKIN_ARC_PRE_SPACE);

  data->mark(DAIKIN_HEADER_MARK);
  data->space(DAIKIN_HEADER_SPACE);

  for (uint8_t i : remote_header) {
    for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask
      data->mark(DAIKIN_BIT_MARK);
      bool bit = i & mask;
      data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
    }
  }
  data->mark(DAIKIN_BIT_MARK);
  data->space(0);

  transmit.perform();
}

void DaikinArcClimate::transmit_state() {
  // 0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00,
  // 0x42, 0x49, 0x05, 0xA2,
  uint8_t remote_header[20] = {0x11, 0xDA, 0x27, 0x00, 0x02, 0xd0, 0x02, 0x03, 0x80, 0x03, 0x82, 0x30, 0x41, 0x1f, 0x82,
                               0xf4,
                               /*                                                      とつど */
                               /*                                                       0x13 */
                               0x00, 0x24, 0x00, 0x00};

  // 05    0 [1:3]MODE   1 [OFF TMR] [ON TMR] Power
  // 06-07 TEMP
  // 08    [0:3] SPEED  [4:7] Swing
  // 09    00
  // 10    00
  // 11, 12: timer
  // 13    [0:6] 0000000 [7] POWERMODE
  // 14    0a
  // 15    c4
  // 16    [0:3] 8  00 [6:7] SENSOR WIND = 11 / NORMAL = 00
  // 17    24

  uint8_t remote_state[19] = {
      0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x0a, 0xC4,
      /*                               MODE  TEMP  HUMD  FANH  FANL
         パワフル音声応答     */
      /*                                                                           ON
         0x01入 0x0a     */
      /*                                                                           OF
         0x00切 0x02     */
      0x80, 0x24, 0x00
      /* センサー風         */
      /* ON 0x83           */
      /* OF 0x80           */
  };

  remote_state[5] = this->operation_mode_() | 0x08;
  remote_state[6] = this->temperature_();
  remote_state[7] = this->humidity_();
  if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) {
    ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]);
    remote_header[9] |= 0x10;
    this->last_humidity_ = remote_state[7];
  }
  uint16_t fan_speed = this->fan_speed_();
  remote_state[8] = fan_speed >> 8;
  remote_state[9] = fan_speed & 0xff;

  // Calculate checksum
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
  }

  // Calculate checksum
  for (int i = 0; i < DAIKIN_STATE_FRAME_SIZE - 1; i++) {
    remote_state[DAIKIN_STATE_FRAME_SIZE - 1] += remote_state[i];
  }

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

  data->mark(DAIKIN_ARC_PRE_MARK);
  data->space(DAIKIN_ARC_PRE_SPACE);

  data->mark(DAIKIN_HEADER_MARK);
  data->space(DAIKIN_HEADER_SPACE);

  for (uint8_t i : remote_header) {
    for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask
      data->mark(DAIKIN_BIT_MARK);
      bool bit = i & mask;
      data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
    }
  }
  data->mark(DAIKIN_BIT_MARK);
  data->space(DAIKIN_MESSAGE_SPACE);

  data->mark(DAIKIN_HEADER_MARK);
  data->space(DAIKIN_HEADER_SPACE);

  for (uint8_t i : remote_state) {
    for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask
      data->mark(DAIKIN_BIT_MARK);
      bool bit = i & mask;
      data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
    }
  }
  data->mark(DAIKIN_BIT_MARK);
  data->space(0);

  transmit.perform();
}

uint8_t DaikinArcClimate::operation_mode_() {
  uint8_t operating_mode = DAIKIN_MODE_ON;
  switch (this->mode) {
    case climate::CLIMATE_MODE_COOL:
      operating_mode |= DAIKIN_MODE_COOL;
      break;
    case climate::CLIMATE_MODE_DRY:
      operating_mode |= DAIKIN_MODE_DRY;
      break;
    case climate::CLIMATE_MODE_HEAT:
      operating_mode |= DAIKIN_MODE_HEAT;
      break;
    case climate::CLIMATE_MODE_HEAT_COOL:
      operating_mode |= DAIKIN_MODE_AUTO;
      break;
    case climate::CLIMATE_MODE_FAN_ONLY:
      operating_mode |= DAIKIN_MODE_FAN;
      break;
    case climate::CLIMATE_MODE_OFF:
    default:
      operating_mode = DAIKIN_MODE_OFF;
      break;
  }

  return operating_mode;
}

uint16_t DaikinArcClimate::fan_speed_() {
  uint16_t fan_speed;
  switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
    case climate::CLIMATE_FAN_LOW:
      fan_speed = DAIKIN_FAN_1 << 8;
      break;
    case climate::CLIMATE_FAN_MEDIUM:
      fan_speed = DAIKIN_FAN_3 << 8;
      break;
    case climate::CLIMATE_FAN_HIGH:
      fan_speed = DAIKIN_FAN_5 << 8;
      break;
    case climate::CLIMATE_FAN_AUTO:
    default:
      fan_speed = DAIKIN_FAN_AUTO << 8;
  }

  // If swing is enabled switch first 4 bits to 1111
  switch (this->swing_mode) {
    case climate::CLIMATE_SWING_VERTICAL:
      fan_speed |= 0x0F00;
      break;
    case climate::CLIMATE_SWING_HORIZONTAL:
      fan_speed |= 0x000F;
      break;
    case climate::CLIMATE_SWING_BOTH:
      fan_speed |= 0x0F0F;
      break;
    default:
      break;
  }
  return fan_speed;
}

uint8_t DaikinArcClimate::temperature_() {
  // Force special temperatures depending on the mode
  switch (this->mode) {
    case climate::CLIMATE_MODE_FAN_ONLY:
      return 0x32;
    case climate::CLIMATE_MODE_HEAT_COOL:
    case climate::CLIMATE_MODE_DRY:
      return 0xc0;
    default:
      float new_temp = clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX);
      uint8_t temperature = (uint8_t) std::floor(new_temp);
      return temperature << 1 | (new_temp - temperature > 0 ? 0x01 : 0);
  }
}

uint8_t DaikinArcClimate::humidity_() {
  if (this->target_humidity == 39) {
    return 0;
  } else if (this->target_humidity <= 40 || this->target_humidity == 44) {
    return 40;
  } else if (this->target_humidity <= 45 || this->target_humidity == 49)  // 41 - 45
  {
    return 45;
  } else if (this->target_humidity <= 50 || this->target_humidity == 52)  // 45 - 50
  {
    return 50;
  } else {
    return 0xff;
  }
}

climate::ClimateTraits DaikinArcClimate::traits() {
  climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
  traits.set_visual_min_humidity(38);
  traits.set_visual_max_humidity(52);
  return traits;
}

bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
  uint8_t checksum = 0;
  for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
    checksum += frame[i];
  }
  if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) {
    ESP_LOGI(TAG, "checksum error");
    return false;
  }

  char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
  size_t pos = 0;
  for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
    pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]);
  }
  ESP_LOGD(TAG, "FRAME %s", buf);

  uint8_t mode = frame[5];
  if (mode & DAIKIN_MODE_ON) {
    switch (mode & 0xF0) {
      case DAIKIN_MODE_COOL:
        this->mode = climate::CLIMATE_MODE_COOL;
        break;
      case DAIKIN_MODE_DRY:
        this->mode = climate::CLIMATE_MODE_DRY;
        break;
      case DAIKIN_MODE_HEAT:
        this->mode = climate::CLIMATE_MODE_HEAT;
        break;
      case DAIKIN_MODE_AUTO:
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
        break;
      case DAIKIN_MODE_FAN:
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
        break;
    }
  } else {
    this->mode = climate::CLIMATE_MODE_OFF;
  }
  uint8_t temperature = frame[6];
  if (!(temperature & 0xC0)) {
    this->target_temperature = temperature >> 1;
    this->target_temperature += (temperature & 0x1) ? 0.5 : 0;
  }
  this->target_humidity = frame[7];  // 0, 40, 45, 50, 0xff
  uint8_t fan_mode = frame[8];
  uint8_t swing_mode = frame[9];
  if (fan_mode & 0xF && swing_mode & 0xF) {
    this->swing_mode = climate::CLIMATE_SWING_BOTH;
  } else if (fan_mode & 0xF) {
    this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
  } else if (swing_mode & 0xF) {
    this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
  } else {
    this->swing_mode = climate::CLIMATE_SWING_OFF;
  }
  switch (fan_mode & 0xF0) {
    case DAIKIN_FAN_1:
    case DAIKIN_FAN_2:
    case DAIKIN_FAN_SILENT:
      this->fan_mode = climate::CLIMATE_FAN_LOW;
      break;
    case DAIKIN_FAN_3:
      this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
      break;
    case DAIKIN_FAN_4:
    case DAIKIN_FAN_5:
      this->fan_mode = climate::CLIMATE_FAN_HIGH;
      break;
    case DAIKIN_FAN_AUTO:
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
      break;
  }
  /*
  05    0 [1:3]MODE   1 [OFF TMR] [ON TMR] Power
  06-07 TEMP
  08    [0:3] SPEED  [4:7] Swing
  09    00
  10    00
  11, 12: timer
  13    [0:6] 0000000 [7] POWERMODE
  14    0a
  15    c4
  16    [0:3] 8  00 [6:7] SENSOR WIND = 11 / NORMAL = 00
  17    24
                             05 06 07 08 09 10 11 12 13 14 15 16 17 18
  None  FRAME 11 da 27 00 00 49 2e 00 b0 00 00 06 60 00 0a c4 80 24 11
  1H    FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 c6 30 00 2a c4 80 24 c5
  1H30  FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 a6 32 00 2a c4 80 24 a7
  2H    FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 86 34 00 2a c4 80 24 89

  */
  this->publish_state();
  return true;
}

bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
  uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};

  bool valid_daikin_frame = false;
  if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
    valid_daikin_frame = true;
    size_t bytes_count = data.size() / 2 / 8;
    // Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf
    char buf[40 * 3 + 1] = {};
    constexpr size_t buf_size = sizeof(buf);
    size_t buf_pos = 0;
    for (size_t i = 0; i < bytes_count; i++) {
      uint8_t byte = 0;
      for (int8_t bit = 0; bit < 8; bit++) {
        if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
          byte |= 1 << bit;
        } else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
          valid_daikin_frame = false;
          break;
        }
      }
      buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte);
    }
    ESP_LOGD(TAG, "WHOLE FRAME %s  size: %d", buf, data.size());
  }
  if (!valid_daikin_frame) {
    char sbuf[16 * 10 + 1] = {0};
    size_t sbuf_pos = 0;
    for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
      if ((j - 2) % 16 == 0) {
        if (j > 0) {
          ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
        }
        sbuf_pos = 0;
      }
      char type_ch = ' ';
      // debug_tolerance = 25%

      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
        type_ch = 'P';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
        type_ch = 'a';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
        type_ch = 'H';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
        type_ch = 'h';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
        type_ch = 'B';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
        type_ch = '1';
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
        type_ch = '0';

      if (abs(data[j]) > 100000) {
        sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch);
      } else {
        sbuf_pos =
            buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch);
      }
      if (j + 1 == static_cast<size_t>(data.size())) {
        ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
      }
    }
  }

  data.reset();

  if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
    ESP_LOGI(TAG, "non daikin_arc expect item");
    return false;
  }

  for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
    uint8_t byte = 0;
    for (int8_t bit = 0; bit < 8; bit++) {
      if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
        byte |= 1 << bit;
      } else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
        ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
        return false;
      }
    }
    state_frame[pos] = byte;
    if (pos == 0) {
      // frame header
      if (byte != 0x11) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    } else if (pos == 1) {
      // frame header
      if (byte != 0xDA) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    } else if (pos == 2) {
      // frame header
      if (byte != 0x27) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    } else if (pos == 3) {  // NOLINT(bugprone-branch-clone)
      // frame header
      if (byte != 0x00) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    } else if (pos == 4) {
      // frame type
      if (byte != 0x00) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    } else if (pos == 5) {
      if (data.size() == 385) {
        /*
        11 da 27 00 00 1a 0c 04 2c 21 61 07 00 07 0c 00 18 00 0e 3c 00 6c 1b 61
                       Inside Temp
                          Outside Temp
                              Humdidity

        */
        this->current_temperature = state_frame[5];  // Inside temperature
        // this->current_temperature = state_frame[6]; // Outside temperature
        this->publish_state();
        return true;
      } else if ((byte & 0x40) != 0x40) {
        ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
        return false;
      }
    }
  }
  return this->parse_state_frame_(state_frame);
}

void DaikinArcClimate::control(const climate::ClimateCall &call) {
  auto target_humidity = call.get_target_humidity();
  if (target_humidity.has_value()) {
    this->target_humidity = *target_humidity;
  }
  climate_ir::ClimateIR::control(call);
}

}  // namespace esphome::daikin_arc
