// Copyright 2019 David Conran

// Neoclima A/C support

// Analysis by crankyoldgit & AndreyShpilevoy
// Code by crankyoldgit
// Ref:
//    https://github.com/crankyoldgit/IRremoteESP8266/issues/764
//    https://drive.google.com/file/d/1kjYk4zS9NQcMQhFkak-L4mp4UuaAIesW/view


// Supports:
//   Brand: Neoclima,  Model: NS-09AHTI A/C
//   Brand: Neoclima,  Model: ZH/TY-01 remote

#include "ir_Neoclima.h"
#include <algorithm>
#include "IRrecv.h"
#include "IRsend.h"
#include "IRutils.h"

// Constants

const uint16_t kNeoclimaHdrMark = 6112;
const uint16_t kNeoclimaHdrSpace = 7391;
const uint16_t kNeoclimaBitMark = 537;
const uint16_t kNeoclimaOneSpace = 1651;
const uint16_t kNeoclimaZeroSpace = 571;
const uint32_t kNeoclimaMinGap = kDefaultMessageGap;

#if SEND_NEOCLIMA
// Send a Neoclima message.
//
// Args:
//   data: message to be sent.
//   nbytes: Nr. of bytes of the message to be sent.
//   repeat: Nr. of additional times the message is to be sent.
//
// Status: Beta / Known to be working.
//
// Ref:
//   https://github.com/crankyoldgit/IRremoteESP8266/issues/764
void IRsend::sendNeoclima(const unsigned char data[], const uint16_t nbytes,
                          const uint16_t repeat) {
  // Set IR carrier frequency
  enableIROut(38);

  for (uint16_t i = 0; i <= repeat; i++) {
    sendGeneric(kNeoclimaHdrMark, kNeoclimaHdrSpace,
                kNeoclimaBitMark, kNeoclimaOneSpace,
                kNeoclimaBitMark, kNeoclimaZeroSpace,
                kNeoclimaBitMark, kNeoclimaHdrSpace,
                data, nbytes, 38000, false, 0,  // Repeats are already handled.
                50);
     // Extra footer.
     mark(kNeoclimaBitMark);
     space(kNeoclimaMinGap);
  }
}
#endif  // SEND_NEOCLIMA

IRNeoclimaAc::IRNeoclimaAc(const uint16_t pin, const bool inverted,
                           const bool use_modulation)
    : _irsend(pin, inverted, use_modulation) {
  this->stateReset();
}

void IRNeoclimaAc::stateReset(void) {
  for (uint8_t i = 0; i < kNeoclimaStateLength; i++)
    remote_state[i] = 0x0;
  remote_state[7] = 0x6A;
  remote_state[8] = 0x00;
  remote_state[9] = 0x2A;
  remote_state[10] = 0xA5;
  // [11] is the checksum.
}

void IRNeoclimaAc::begin(void) { _irsend.begin(); }

uint8_t IRNeoclimaAc::calcChecksum(const uint8_t state[],
                                   const uint16_t length) {
  if (length == 0) return state[0];
  return sumBytes(state, length - 1);
}

bool IRNeoclimaAc::validChecksum(const uint8_t state[], const uint16_t length) {
  if (length < 2)
    return true;  // No checksum to compare with. Assume okay.
  return (state[length - 1] == calcChecksum(state, length));
}

// Update the checksum for the internal state.
void IRNeoclimaAc::checksum(uint16_t length) {
  if (length < 2) return;
  remote_state[length - 1] = calcChecksum(remote_state, length);
}

#if SEND_NEOCLIMA
void IRNeoclimaAc::send(const uint16_t repeat) {
  this->checksum();
  _irsend.sendNeoclima(remote_state, kNeoclimaStateLength, repeat);
}
#endif  // SEND_NEOCLIMA

uint8_t *IRNeoclimaAc::getRaw(void) {
  this->checksum();
  return remote_state;
}

void IRNeoclimaAc::setRaw(const uint8_t new_code[], const uint16_t length) {
  for (uint8_t i = 0; i < length && i < kNeoclimaStateLength; i++)
    remote_state[i] = new_code[i];
}


void IRNeoclimaAc::setButton(const uint8_t button) {
  switch (button) {
    case kNeoclimaButtonPower:
    case kNeoclimaButtonMode:
    case kNeoclimaButtonTempUp:
    case kNeoclimaButtonTempDown:
    case kNeoclimaButtonSwing:
    case kNeoclimaButtonFanSpeed:
    case kNeoclimaButtonAirFlow:
    case kNeoclimaButtonHold:
    case kNeoclimaButtonSleep:
    case kNeoclimaButtonLight:
    case kNeoclimaButtonEye:
    case kNeoclimaButtonFollow:
    case kNeoclimaButtonIon:
    case kNeoclimaButtonFresh:
    case kNeoclimaButton8CHeat:
    case kNeoclimaButtonTurbo:
      remote_state[5] &= ~kNeoclimaButtonMask;
      remote_state[5] |= button;
      break;
    default:
      this->setButton(kNeoclimaButtonPower);
  }
}

uint8_t IRNeoclimaAc::getButton(void) {
  return remote_state[5] & kNeoclimaButtonMask;
}

void IRNeoclimaAc::on(void) { this->setPower(true); }

void IRNeoclimaAc::off(void) { this->setPower(false); }

void IRNeoclimaAc::setPower(const bool on) {
  this->setButton(kNeoclimaButtonPower);
  if (on)
    remote_state[7] |= kNeoclimaPowerMask;
  else
    remote_state[7] &= ~kNeoclimaPowerMask;
}

bool IRNeoclimaAc::getPower(void) {
  return remote_state[7] & kNeoclimaPowerMask;
}

void IRNeoclimaAc::setMode(const uint8_t mode) {
  switch (mode) {
    case kNeoclimaDry:
      // In this mode fan speed always LOW
      this->setFan(kNeoclimaFanLow);
      // FALL THRU
    case kNeoclimaAuto:
    case kNeoclimaCool:
    case kNeoclimaFan:
    case kNeoclimaHeat:
      remote_state[9] &= ~kNeoclimaModeMask;
      remote_state[9] |= (mode << 5);
      this->setButton(kNeoclimaButtonMode);
      break;
    default:
      // If we get an unexpected mode, default to AUTO.
      this->setMode(kNeoclimaAuto);
  }
}

uint8_t IRNeoclimaAc::getMode(void) {
  return (remote_state[9] & kNeoclimaModeMask) >> 5;
}

// Convert a standard A/C mode into its native mode.
uint8_t IRNeoclimaAc::convertMode(const stdAc::opmode_t mode) {
  switch (mode) {
    case stdAc::opmode_t::kCool:
      return kNeoclimaCool;
    case stdAc::opmode_t::kHeat:
      return kNeoclimaHeat;
    case stdAc::opmode_t::kDry:
      return kNeoclimaDry;
    case stdAc::opmode_t::kFan:
      return kNeoclimaFan;
    default:
      return kNeoclimaAuto;
  }
}

// Convert a native mode to it's common equivalent.
stdAc::opmode_t IRNeoclimaAc::toCommonMode(const uint8_t mode) {
  switch (mode) {
    case kNeoclimaCool: return stdAc::opmode_t::kCool;
    case kNeoclimaHeat: return stdAc::opmode_t::kHeat;
    case kNeoclimaDry: return stdAc::opmode_t::kDry;
    case kNeoclimaFan: return stdAc::opmode_t::kFan;
    default: return stdAc::opmode_t::kAuto;
  }
}

// Set the temp. in deg C
void IRNeoclimaAc::setTemp(const uint8_t temp) {
  uint8_t oldtemp = this->getTemp();
  uint8_t newtemp = std::max(kNeoclimaMinTemp, temp);
  newtemp = std::min(kNeoclimaMaxTemp, newtemp);
  if (oldtemp > newtemp)
    this->setButton(kNeoclimaButtonTempDown);
  else if (newtemp > oldtemp)
    this->setButton(kNeoclimaButtonTempUp);
  remote_state[9] = (remote_state[9] & ~kNeoclimaTempMask) |
    (newtemp - kNeoclimaMinTemp);
}

// Return the set temp. in deg C
uint8_t IRNeoclimaAc::getTemp(void) {
  return (remote_state[9] & kNeoclimaTempMask) + kNeoclimaMinTemp;
}

// Set the speed of the fan, 0-3, 0 is auto, 1-3 is the speed
void IRNeoclimaAc::setFan(const uint8_t speed) {
  switch (speed) {
    case kNeoclimaFanAuto:
    case kNeoclimaFanHigh:
    case kNeoclimaFanMed:
      if (this->getMode() == kNeoclimaDry) {  // Dry mode only allows low speed.
        this->setFan(kNeoclimaFanLow);
        return;
      }
      // FALL-THRU
    case kNeoclimaFanLow:
      remote_state[7] &= ~kNeoclimaFanMask;
      remote_state[7] |= (speed << 5);
      this->setButton(kNeoclimaButtonFanSpeed);
      break;
    default:
      // If we get an unexpected speed, default to Auto.
      this->setFan(kNeoclimaFanAuto);
  }
}

uint8_t IRNeoclimaAc::getFan(void) {
  return (remote_state[7] & kNeoclimaFanMask) >> 5;
}

// Convert a standard A/C Fan speed into its native fan speed.
uint8_t IRNeoclimaAc::convertFan(const stdAc::fanspeed_t speed) {
  switch (speed) {
    case stdAc::fanspeed_t::kMin:
    case stdAc::fanspeed_t::kLow:
      return kNeoclimaFanLow;
    case stdAc::fanspeed_t::kMedium:
      return kNeoclimaFanMed;
    case stdAc::fanspeed_t::kHigh:
    case stdAc::fanspeed_t::kMax:
      return kNeoclimaFanHigh;
    default:
      return kNeoclimaFanAuto;
  }
}

// Convert a native fan speed to it's common equivalent.
stdAc::fanspeed_t IRNeoclimaAc::toCommonFanSpeed(const uint8_t speed) {
  switch (speed) {
    case kNeoclimaFanHigh: return stdAc::fanspeed_t::kMax;
    case kNeoclimaFanMed: return stdAc::fanspeed_t::kMedium;
    case kNeoclimaFanLow: return stdAc::fanspeed_t::kMin;
    default: return stdAc::fanspeed_t::kAuto;
  }
}

void IRNeoclimaAc::setSleep(const bool on) {
  this->setButton(kNeoclimaButtonSleep);
  if (on)
    remote_state[7] |= kNeoclimaSleepMask;
  else
    remote_state[7] &= ~kNeoclimaSleepMask;
}

bool IRNeoclimaAc::getSleep(void) {
  return remote_state[7] & kNeoclimaSleepMask;
}

// A.k.a. Swing
void IRNeoclimaAc::setSwingV(const bool on) {
  this->setButton(kNeoclimaButtonSwing);
  remote_state[7] &= ~kNeoclimaSwingVMask;
  remote_state[7] |= on ? kNeoclimaSwingVOn : kNeoclimaSwingVOff;
}

bool IRNeoclimaAc::getSwingV(void) {
  return (remote_state[7] & kNeoclimaSwingVMask) == kNeoclimaSwingVOn;
}

// A.k.a. Air Flow
void IRNeoclimaAc::setSwingH(const bool on) {
  this->setButton(kNeoclimaButtonAirFlow);
  if (on)
    remote_state[7] &= ~kNeoclimaSwingHMask;
  else
    remote_state[7] |= kNeoclimaSwingHMask;
}

bool IRNeoclimaAc::getSwingH(void) {
  return !(remote_state[7] & kNeoclimaSwingHMask);
}

void IRNeoclimaAc::setTurbo(const bool on) {
  this->setButton(kNeoclimaButtonTurbo);
  if (on)
    remote_state[3] |= kNeoclimaTurboMask;
  else
    remote_state[3] &= ~kNeoclimaTurboMask;
}

bool IRNeoclimaAc::getTurbo(void) {
  return remote_state[3] & kNeoclimaTurboMask;
}

void IRNeoclimaAc::setFresh(const bool on) {
  this->setButton(kNeoclimaButtonFresh);
  if (on)
    remote_state[5] |= kNeoclimaFreshMask;
  else
    remote_state[5] &= ~kNeoclimaFreshMask;
}

bool IRNeoclimaAc::getFresh(void) {
  return remote_state[5] & kNeoclimaFreshMask;
}

void IRNeoclimaAc::setHold(const bool on) {
  this->setButton(kNeoclimaButtonHold);
  if (on)
    remote_state[3] |= kNeoclimaHoldMask;
  else
    remote_state[3] &= ~kNeoclimaHoldMask;
}

bool IRNeoclimaAc::getHold(void) {
  return remote_state[3] & kNeoclimaHoldMask;
}

void IRNeoclimaAc::setIon(const bool on) {
  this->setButton(kNeoclimaButtonIon);
  if (on)
    remote_state[1] |= kNeoclimaIonMask;
  else
    remote_state[1] &= ~kNeoclimaIonMask;
}

bool IRNeoclimaAc::getIon(void) {
  return remote_state[1] & kNeoclimaIonMask;
}

void IRNeoclimaAc::setLight(const bool on) {
  this->setButton(kNeoclimaButtonLight);
  if (on)
    remote_state[3] |= kNeoclimaLightMask;
  else
    remote_state[3] &= ~kNeoclimaLightMask;
}

bool IRNeoclimaAc::getLight(void) {
  return remote_state[3] & kNeoclimaLightMask;
}

// This feature maintains the room temperature steadily at 8°C and prevents the
// room from freezing by activating the heating operation automatically when
// nobody is at home over a longer period during severe winter.
void IRNeoclimaAc::set8CHeat(const bool on) {
  this->setButton(kNeoclimaButton8CHeat);
  if (on)
    remote_state[1] |= kNeoclima8CHeatMask;
  else
    remote_state[1] &= ~kNeoclima8CHeatMask;
}

bool IRNeoclimaAc::get8CHeat(void) {
  return remote_state[1] & kNeoclima8CHeatMask;
}

void IRNeoclimaAc::setEye(const bool on) {
  this->setButton(kNeoclimaButtonEye);
  if (on)
    remote_state[3] |= kNeoclimaEyeMask;
  else
    remote_state[3] &= ~kNeoclimaEyeMask;
}

bool IRNeoclimaAc::getEye(void) {
  return remote_state[3] & kNeoclimaEyeMask;
}

/* DISABLED
   TODO(someone): Work out why "on" is either 0x5D or 0x5F
void IRNeoclimaAc::setFollow(const bool on) {
  this->setButton(kNeoclimaButtonFollow);
  if (on)
    remote_state[8] = kNeoclimaFollowMe;
  else
    remote_state[8] = 0;
}
*/

bool IRNeoclimaAc::getFollow(void) {
  return (remote_state[8] & kNeoclimaFollowMe) == kNeoclimaFollowMe;
}

// Convert the A/C state to it's common equivalent.
stdAc::state_t IRNeoclimaAc::toCommon(void) {
  stdAc::state_t result;
  result.protocol = decode_type_t::NEOCLIMA;
  result.model = -1;  // No models used.
  result.power = this->getPower();
  result.mode = this->toCommonMode(this->getMode());
  result.celsius = true;
  result.degrees = this->getTemp();
  result.fanspeed = this->toCommonFanSpeed(this->getFan());
  result.swingv = this->getSwingV() ? stdAc::swingv_t::kAuto
                                    : stdAc::swingv_t::kOff;
  result.swingh = this->getSwingH() ? stdAc::swingh_t::kAuto
                                    : stdAc::swingh_t::kOff;
  result.turbo = this->getTurbo();
  result.light = this->getLight();
  result.filter = this->getIon();
  result.sleep = this->getSleep() ? 0 : -1;
  // Not supported.
  result.quiet = false;
  result.econo = false;
  result.clean = false;
  result.beep = false;
  result.clock = -1;
  return result;
}

// Convert the internal state into a human readable string.
String IRNeoclimaAc::toString(void) {
  String result = "";
  result.reserve(100);  // Reserve some heap for the string to reduce fragging.
  result += F("Power: ");
  result += this->getPower() ? F("On") : F("Off");
  result += F(", Mode: ");
  result += uint64ToString(this->getMode());
  switch (this->getMode()) {
    case kNeoclimaAuto:
      result += F(" (AUTO)");
      break;
    case kNeoclimaCool:
      result += F(" (COOL)");
      break;
    case kNeoclimaHeat:
      result += F(" (HEAT)");
      break;
    case kNeoclimaDry:
      result += F(" (DRY)");
      break;
    case kNeoclimaFan:
      result += F(" (FAN)");
      break;
    default:
      result += F(" (UNKNOWN)");
  }
  result += F(", Temp: ");
  result += uint64ToString(this->getTemp());
  result += F("C, Fan: ");
  result += uint64ToString(this->getFan());
  switch (this->getFan()) {
    case kNeoclimaFanAuto:
      result += F(" (Auto)");
      break;
    case kNeoclimaFanHigh:
      result += F(" (High)");
      break;
    case kNeoclimaFanMed:
      result += F(" (Med)");
      break;
    case kNeoclimaFanLow:
      result += F(" (Low)");
      break;
  }
  result += F(", Swing(V): ");
  result += this->getSwingV() ? F("On") : F("Off");
  result += F(", Swing(H): ");
  result += this->getSwingH() ? F("On") : F("Off");
  result += F(", Sleep: ");
  result += this->getSleep() ? F("On") : F("Off");
  result += F(", Turbo: ");
  result += this->getTurbo() ? F("On") : F("Off");
  result += F(", Hold: ");
  result += this->getHold() ? F("On") : F("Off");
  result += F(", Ion: ");
  result += this->getIon() ? F("On") : F("Off");
  result += F(", Eye: ");
  result += this->getEye() ? F("On") : F("Off");
  result += F(", Light: ");
  result += this->getLight() ? F("On") : F("Off");
  result += F(", Follow: ");
  result += this->getFollow() ? F("On") : F("Off");
  result += F(", 8C Heat: ");
  result += this->get8CHeat() ? F("On") : F("Off");
  result += F(", Fresh: ");
  result += this->getFresh() ? F("On") : F("Off");
  result += F(", Button: ");
  result += uint64ToString(this->getButton());
  result += F(" (");
  switch (this->getButton()) {
    case kNeoclimaButtonPower:    result += F("Power"); break;
    case kNeoclimaButtonMode:     result += F("Mode"); break;
    case kNeoclimaButtonTempUp:   result += F("Temp Up"); break;
    case kNeoclimaButtonTempDown: result += F("Temp Down"); break;
    case kNeoclimaButtonSwing:    result += F("Swing"); break;
    case kNeoclimaButtonFanSpeed: result += F("Speed"); break;
    case kNeoclimaButtonAirFlow:  result += F("Air Flow"); break;
    case kNeoclimaButtonHold:     result += F("Hold"); break;
    case kNeoclimaButtonSleep:    result += F("Sleep"); break;
    case kNeoclimaButtonLight:    result += F("Light"); break;
    case kNeoclimaButtonEye:      result += F("Eye"); break;
    case kNeoclimaButtonFollow:   result += F("Follow"); break;
    case kNeoclimaButtonIon:      result += F("Ion"); break;
    case kNeoclimaButtonFresh:    result += F("Fresh"); break;
    case kNeoclimaButton8CHeat:   result += F("8C Heat"); break;
    case kNeoclimaButtonTurbo:    result += F("Turbo"); break;
    default:
      result += F("UNKNOWN");
  }
  result += F(")");
  return result;
}

#if DECODE_NEOCLIMA
// Decode the supplied Neoclima message.
//
// Args:
//   results: Ptr to the data to decode and where to store the decode result.
//   nbits:   Nr. of data bits to expect. Typically kNeoclimaBits.
//   strict:  Flag indicating if we should perform strict matching.
// Returns:
//   boolean: True if it can decode it, false if it can't.
//
// Status: BETA / Known working
//
// Ref:
//   https://github.com/crankyoldgit/IRremoteESP8266/issues/764
bool IRrecv::decodeNeoclima(decode_results *results, const uint16_t nbits,
                            const bool strict) {
  // Compliance
  if (strict && nbits != kNeoclimaBits)
    return false;  // Incorrect nr. of bits per spec.

  uint16_t offset = kStartOffset;
  // Match Main Header + Data + Footer
  uint16_t used;
  used = matchGeneric(results->rawbuf + offset, results->state,
                      results->rawlen - offset, nbits,
                      kNeoclimaHdrMark, kNeoclimaHdrSpace,
                      kNeoclimaBitMark, kNeoclimaOneSpace,
                      kNeoclimaBitMark, kNeoclimaZeroSpace,
                      kNeoclimaBitMark, kNeoclimaHdrSpace, false,
                      kTolerance, 0, false);
  if (!used) return false;
  offset += used;
  // Extra footer.
  uint64_t unused;
  if (!matchGeneric(results->rawbuf + offset, &unused,
                    results->rawlen - offset, 0, 0, 0, 0, 0, 0, 0,
                    kNeoclimaBitMark, kNeoclimaHdrSpace, true)) return false;

  // Compliance
  if (strict) {
    // Check we got a valid checksum.
    if (!IRNeoclimaAc::validChecksum(results->state, nbits / 8)) return false;
  }

  // Success
  results->decode_type = decode_type_t::NEOCLIMA;
  results->bits = nbits;
  // No need to record the state as we stored it as we decoded it.
  // As we use result->state, we don't record value, address, or command as it
  // is a union data type.
  return true;
}
#endif  // DECODE_NEOCLIMA
