// Copyright 2019 David Conran

/// @file
/// @brief Support for Mitsubishi Heavy Industry protocols.
/// Code to emulate Mitsubishi Heavy Industries A/C IR remote control units.
/// @note This code was *heavily* influenced by ToniA's great work & code,
///   but it has been written from scratch.
///   Nothing was copied other than constants and message analysis.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/660
/// @see https://github.com/ToniA/Raw-IR-decoder-for-Arduino/blob/master/MitsubishiHeavy.cpp
/// @see https://github.com/ToniA/arduino-heatpumpir/blob/master/MitsubishiHeavyHeatpumpIR.cpp

#include "ir_MitsubishiHeavy.h"
#include <algorithm>
#include <cstring>
#include "IRremoteESP8266.h"
#include "IRtext.h"
#include "IRutils.h"
#ifndef ARDUINO
#include <string>
#endif

// Constants
const uint16_t kMitsubishiHeavyHdrMark = 3140;
const uint16_t kMitsubishiHeavyHdrSpace = 1630;
const uint16_t kMitsubishiHeavyBitMark = 370;
const uint16_t kMitsubishiHeavyOneSpace = 420;
const uint16_t kMitsubishiHeavyZeroSpace = 1220;
const uint32_t kMitsubishiHeavyGap = kDefaultMessageGap;  // Just a guess.

using irutils::addBoolToString;
using irutils::addIntToString;
using irutils::addLabeledString;
using irutils::addModeToString;
using irutils::addSwingHToString;
using irutils::addSwingVToString;
using irutils::addTempToString;
using irutils::checkInvertedBytePairs;
using irutils::invertBytePairs;

#if SEND_MITSUBISHIHEAVY
/// Send a MitsubishiHeavy 88-bit A/C message.
/// Status: BETA / Appears to be working. Needs testing against a real device.
/// @param[in] data The message to be sent.
/// @param[in] nbytes The number of bytes of message to be sent.
/// @param[in] repeat The number of times the command is to be repeated.
void IRsend::sendMitsubishiHeavy88(const unsigned char data[],
                                   const uint16_t nbytes,
                                   const uint16_t repeat) {
  if (nbytes < kMitsubishiHeavy88StateLength)
    return;  // Not enough bytes to send a proper message.
  sendGeneric(kMitsubishiHeavyHdrMark, kMitsubishiHeavyHdrSpace,
              kMitsubishiHeavyBitMark, kMitsubishiHeavyOneSpace,
              kMitsubishiHeavyBitMark, kMitsubishiHeavyZeroSpace,
              kMitsubishiHeavyBitMark, kMitsubishiHeavyGap,
              data, nbytes, 38000, false, repeat, kDutyDefault);
}

/// Send a MitsubishiHeavy 152-bit A/C message.
/// Status: BETA / Appears to be working. Needs testing against a real device.
/// @param[in] data The message to be sent.
/// @param[in] nbytes The number of bytes of message to be sent.
/// @param[in] repeat The number of times the command is to be repeated.
void IRsend::sendMitsubishiHeavy152(const unsigned char data[],
                                    const uint16_t nbytes,
                                    const uint16_t repeat) {
  if (nbytes < kMitsubishiHeavy152StateLength)
    return;  // Not enough bytes to send a proper message.
  sendMitsubishiHeavy88(data, nbytes, repeat);
}
#endif  // SEND_MITSUBISHIHEAVY

// Class for decoding and constructing MitsubishiHeavy152 AC messages.

/// Class constructor
/// @param[in] pin GPIO to be used when sending.
/// @param[in] inverted Is the output signal to be inverted?
/// @param[in] use_modulation Is frequency modulation to be used?
IRMitsubishiHeavy152Ac::IRMitsubishiHeavy152Ac(const uint16_t pin,
                                               const bool inverted,
                                               const bool use_modulation)
    : _irsend(pin, inverted, use_modulation) { stateReset(); }

/// Set up hardware to be able to send a message.
void IRMitsubishiHeavy152Ac::begin(void) { _irsend.begin(); }

#if SEND_MITSUBISHIHEAVY
/// Send the current internal state as an IR message.
/// @param[in] repeat Nr. of times the message will be repeated.
void IRMitsubishiHeavy152Ac::send(const uint16_t repeat) {
  _irsend.sendMitsubishiHeavy152(getRaw(), kMitsubishiHeavy152StateLength,
                                 repeat);
}
#endif  // SEND_MITSUBISHIHEAVY

/// Reset the state of the remote to a known good state/sequence.
void IRMitsubishiHeavy152Ac::stateReset(void) {
  std::memcpy(_.raw, kMitsubishiHeavyZmsSig, kMitsubishiHeavySigLength);
  for (uint8_t i = kMitsubishiHeavySigLength;
       i < kMitsubishiHeavy152StateLength - 3; i += 2) _.raw[i] = 0;
  _.raw[17] = 0x80;
}

/// Get a PTR to the internal state/code for this protocol.
/// @return PTR to a code for this protocol based on the current internal state.
uint8_t *IRMitsubishiHeavy152Ac::getRaw(void) {
  checksum();
  return _.raw;
}

/// Set the internal state from a valid code for this protocol.
/// @param[in] data A valid code for this protocol.
void IRMitsubishiHeavy152Ac::setRaw(const uint8_t *data) {
  std::memcpy(_.raw, data, kMitsubishiHeavy152StateLength);
}

/// Set the requested power state of the A/C to on.
void IRMitsubishiHeavy152Ac::on(void) { setPower(true); }

/// Set the requested power state of the A/C to off.
void IRMitsubishiHeavy152Ac::off(void) { setPower(false); }

/// Change the power setting.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setPower(const bool on) {
  _.Power = on;
}

/// Get the value of the current power setting.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getPower(void) const {
  return _.Power;
}

/// Set the temperature.
/// @param[in] temp The temperature in degrees celsius.
void IRMitsubishiHeavy152Ac::setTemp(const uint8_t temp) {
  uint8_t newtemp = temp;
  newtemp = std::min(newtemp, kMitsubishiHeavyMaxTemp);
  newtemp = std::max(newtemp, kMitsubishiHeavyMinTemp);
  _.Temp = newtemp - kMitsubishiHeavyMinTemp;
}

/// Get the current temperature setting.
/// @return The current setting for temp. in degrees celsius.
uint8_t IRMitsubishiHeavy152Ac::getTemp(void) const {
  return _.Temp + kMitsubishiHeavyMinTemp;
}

/// Set the speed of the fan.
/// @param[in] speed The desired setting.
void IRMitsubishiHeavy152Ac::setFan(const uint8_t speed) {
  uint8_t newspeed = speed;
  switch (speed) {
    case kMitsubishiHeavy152FanLow:
    case kMitsubishiHeavy152FanMed:
    case kMitsubishiHeavy152FanHigh:
    case kMitsubishiHeavy152FanMax:
    case kMitsubishiHeavy152FanEcono:
    case kMitsubishiHeavy152FanTurbo: break;
    default: newspeed = kMitsubishiHeavy152FanAuto;
  }
  _.Fan = newspeed;
}

/// Get the current fan speed setting.
/// @return The current fan speed/mode.
uint8_t IRMitsubishiHeavy152Ac::getFan(void) const {
  return _.Fan;
}

/// Set the operating mode of the A/C.
/// @param[in] mode The desired operating mode.
void IRMitsubishiHeavy152Ac::setMode(const uint8_t mode) {
  uint8_t newmode = mode;
  switch (mode) {
    case kMitsubishiHeavyCool:
    case kMitsubishiHeavyDry:
    case kMitsubishiHeavyFan:
    case kMitsubishiHeavyHeat:
      break;
    default:
      newmode = kMitsubishiHeavyAuto;
  }
  _.Mode = newmode;
}

/// Get the operating mode setting of the A/C.
/// @return The current operating mode setting.
uint8_t IRMitsubishiHeavy152Ac::getMode(void) const {
  return _.Mode;
}

/// Set the Vertical Swing mode of the A/C.
/// @param[in] pos The position/mode to set the swing to.
void IRMitsubishiHeavy152Ac::setSwingVertical(const uint8_t pos) {
  _.SwingV = std::min(pos, kMitsubishiHeavy152SwingVOff);
}

/// Get the Vertical Swing mode of the A/C.
/// @return The native position/mode setting.
uint8_t IRMitsubishiHeavy152Ac::getSwingVertical(void) const {
  return _.SwingV;
}

/// Set the Horizontal Swing mode of the A/C.
/// @param[in] pos The position/mode to set the swing to.
void IRMitsubishiHeavy152Ac::setSwingHorizontal(const uint8_t pos) {
  _.SwingH = std::min(pos, kMitsubishiHeavy152SwingHOff);
}

/// Get the Horizontal Swing mode of the A/C.
/// @return The native position/mode setting.
uint8_t IRMitsubishiHeavy152Ac::getSwingHorizontal(void) const {
  return _.SwingH;
}

/// Set the Night (Sleep) mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setNight(const bool on) {
  _.Night = on;
}

/// Get the Night (Sleep) mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getNight(void) const {
  return _.Night;
}

/// Set the 3D mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::set3D(const bool on) {
  if (on)
    { _.Three = 1; _.D = 1; }
  else
    { _.Three = 0; _.D = 0; }
}

/// Get the 3D mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::get3D(void) const {
  return _.Three && _.D;
}

/// Set the Silent (Quiet) mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setSilent(const bool on) {
  _.Silent = on;
}

/// Get the Silent (Quiet) mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getSilent(void) const {
  return _.Silent;
}

/// Set the Filter mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setFilter(const bool on) {
  _.Filter = on;
}

/// Get the Filter mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getFilter(void) const {
  return _.Filter;
}

/// Set the Clean mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setClean(const bool on) {
  _.Filter = on;
  _.Clean = on;
}

/// Get the Clean mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getClean(void) const {
  return _.Clean && _.Filter;
}

/// Set the Turbo mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setTurbo(const bool on) {
  if (on)
    setFan(kMitsubishiHeavy152FanTurbo);
  else if (getTurbo()) setFan(kMitsubishiHeavy152FanAuto);
}

/// Get the Turbo mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getTurbo(void) const {
  return _.Fan == kMitsubishiHeavy152FanTurbo;
}

/// Set the Economical mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy152Ac::setEcono(const bool on) {
  if (on)
    setFan(kMitsubishiHeavy152FanEcono);
  else if (getEcono()) setFan(kMitsubishiHeavy152FanAuto);
}

/// Get the Economical mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy152Ac::getEcono(void) const {
  return _.Fan == kMitsubishiHeavy152FanEcono;
}

/// Verify the given state has a ZM-S signature.
/// @param[in] state A ptr to a state to be checked.
/// @return true, the check passed. Otherwise, false.
bool IRMitsubishiHeavy152Ac::checkZmsSig(const uint8_t *state) {
  for (uint8_t i = 0; i < kMitsubishiHeavySigLength; i++)
    if (state[i] != kMitsubishiHeavyZmsSig[i]) return false;
  return true;
}

/// Calculate the checksum for the current internal state of the remote.
/// Note: Technically it has no checksum, but does have inverted byte pairs.
void IRMitsubishiHeavy152Ac::checksum(void) {
  const uint8_t kOffset = kMitsubishiHeavySigLength - 2;
  invertBytePairs(_.raw + kOffset, kMitsubishiHeavy152StateLength - kOffset);
}

/// Verify the checksum is valid for a given state.
/// @param[in] state The array to verify the checksum of.
/// @param[in] length The length/size of the state array.
/// @return true, if the state has a valid checksum. Otherwise, false.
/// Note: Technically it has no checksum, but does have inverted byte pairs.
bool IRMitsubishiHeavy152Ac::validChecksum(const uint8_t *state,
                                           const uint16_t length) {
  // Assume anything too short is fine.
  if (length < kMitsubishiHeavySigLength) return true;
  const uint8_t kOffset = kMitsubishiHeavySigLength - 2;
  return checkInvertedBytePairs(state + kOffset, length - kOffset);
}

/// Convert a stdAc::opmode_t enum into its native mode.
/// @param[in] mode The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy152Ac::convertMode(const stdAc::opmode_t mode) {
  switch (mode) {
    case stdAc::opmode_t::kCool: return kMitsubishiHeavyCool;
    case stdAc::opmode_t::kHeat: return kMitsubishiHeavyHeat;
    case stdAc::opmode_t::kDry:  return kMitsubishiHeavyDry;
    case stdAc::opmode_t::kFan:  return kMitsubishiHeavyFan;
    default:                     return kMitsubishiHeavyAuto;
  }
}

/// Convert a stdAc::fanspeed_t enum into it's native speed.
/// @param[in] speed The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy152Ac::convertFan(const stdAc::fanspeed_t speed) {
  switch (speed) {
    // Assumes Econo is slower than Low.
    case stdAc::fanspeed_t::kMin:    return kMitsubishiHeavy152FanEcono;
    case stdAc::fanspeed_t::kLow:    return kMitsubishiHeavy152FanLow;
    case stdAc::fanspeed_t::kMedium: return kMitsubishiHeavy152FanMed;
    case stdAc::fanspeed_t::kHigh:   return kMitsubishiHeavy152FanHigh;
    case stdAc::fanspeed_t::kMax:    return kMitsubishiHeavy152FanMax;
    default:                         return kMitsubishiHeavy152FanAuto;
  }
}

/// Convert a stdAc::swingv_t enum into it's native setting.
/// @param[in] position The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy152Ac::convertSwingV(const stdAc::swingv_t position) {
  switch (position) {
    case stdAc::swingv_t::kAuto:    return kMitsubishiHeavy152SwingVAuto;
    case stdAc::swingv_t::kHighest: return kMitsubishiHeavy152SwingVHighest;
    case stdAc::swingv_t::kHigh:    return kMitsubishiHeavy152SwingVHigh;
    case stdAc::swingv_t::kMiddle:  return kMitsubishiHeavy152SwingVMiddle;
    case stdAc::swingv_t::kLow:     return kMitsubishiHeavy152SwingVLow;
    case stdAc::swingv_t::kLowest:  return kMitsubishiHeavy152SwingVLowest;
    default:                        return kMitsubishiHeavy152SwingVOff;
  }
}

/// Convert a stdAc::swingh_t enum into it's native setting.
/// @param[in] position The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy152Ac::convertSwingH(const stdAc::swingh_t position) {
  switch (position) {
    case stdAc::swingh_t::kAuto:     return kMitsubishiHeavy152SwingHAuto;
    case stdAc::swingh_t::kLeftMax:  return kMitsubishiHeavy152SwingHLeftMax;
    case stdAc::swingh_t::kLeft:     return kMitsubishiHeavy152SwingHLeft;
    case stdAc::swingh_t::kMiddle:   return kMitsubishiHeavy152SwingHMiddle;
    case stdAc::swingh_t::kRight:    return kMitsubishiHeavy152SwingHRight;
    case stdAc::swingh_t::kRightMax: return kMitsubishiHeavy152SwingHRightMax;
    default:                         return kMitsubishiHeavy152SwingHOff;
  }
}

/// Convert a native mode into its stdAc equivalent.
/// @param[in] mode The native setting to be converted.
/// @return The stdAc equivalent of the native setting.
stdAc::opmode_t IRMitsubishiHeavy152Ac::toCommonMode(const uint8_t mode) {
  switch (mode) {
    case kMitsubishiHeavyCool: return stdAc::opmode_t::kCool;
    case kMitsubishiHeavyHeat: return stdAc::opmode_t::kHeat;
    case kMitsubishiHeavyDry:  return stdAc::opmode_t::kDry;
    case kMitsubishiHeavyFan:  return stdAc::opmode_t::kFan;
    default:                   return stdAc::opmode_t::kAuto;
  }
}

/// Convert a native fan speed into its stdAc equivalent.
/// @param[in] spd The native setting to be converted.
/// @return The stdAc equivalent of the native setting.
stdAc::fanspeed_t IRMitsubishiHeavy152Ac::toCommonFanSpeed(const uint8_t spd) {
  switch (spd) {
    case kMitsubishiHeavy152FanMax:   return stdAc::fanspeed_t::kMax;
    case kMitsubishiHeavy152FanHigh:  return stdAc::fanspeed_t::kHigh;
    case kMitsubishiHeavy152FanMed:   return stdAc::fanspeed_t::kMedium;
    case kMitsubishiHeavy152FanLow:   return stdAc::fanspeed_t::kLow;
    case kMitsubishiHeavy152FanEcono: return stdAc::fanspeed_t::kMin;
    default:                          return stdAc::fanspeed_t::kAuto;
  }
}

/// Convert a native horizontal swing postion to it's common equivalent.
/// @param[in] pos A native position to convert.
/// @return The common horizontal swing position.
stdAc::swingh_t IRMitsubishiHeavy152Ac::toCommonSwingH(const uint8_t pos) {
  switch (pos) {
    case kMitsubishiHeavy152SwingHLeftMax:  return stdAc::swingh_t::kLeftMax;
    case kMitsubishiHeavy152SwingHLeft:     return stdAc::swingh_t::kLeft;
    case kMitsubishiHeavy152SwingHMiddle:   return stdAc::swingh_t::kMiddle;
    case kMitsubishiHeavy152SwingHRight:    return stdAc::swingh_t::kRight;
    case kMitsubishiHeavy152SwingHRightMax: return stdAc::swingh_t::kRightMax;
    case kMitsubishiHeavy152SwingHOff:      return stdAc::swingh_t::kOff;
    default:                                return stdAc::swingh_t::kAuto;
  }
}

/// Convert a native vertical swing postion to it's common equivalent.
/// @param[in] pos A native position to convert.
/// @return The common vertical swing position.
stdAc::swingv_t IRMitsubishiHeavy152Ac::toCommonSwingV(const uint8_t pos) {
  switch (pos) {
    case kMitsubishiHeavy152SwingVHighest: return stdAc::swingv_t::kHighest;
    case kMitsubishiHeavy152SwingVHigh:    return stdAc::swingv_t::kHigh;
    case kMitsubishiHeavy152SwingVMiddle:  return stdAc::swingv_t::kMiddle;
    case kMitsubishiHeavy152SwingVLow:     return stdAc::swingv_t::kLow;
    case kMitsubishiHeavy152SwingVLowest:  return stdAc::swingv_t::kLowest;
    case kMitsubishiHeavy152SwingVOff:     return stdAc::swingv_t::kOff;
    default:                               return stdAc::swingv_t::kAuto;
  }
}

/// Convert the current internal state into its stdAc::state_t equivalent.
/// @return The stdAc equivalent of the native settings.
stdAc::state_t IRMitsubishiHeavy152Ac::toCommon(void) const {
  stdAc::state_t result{};
  result.protocol = decode_type_t::MITSUBISHI_HEAVY_152;
  result.model = -1;  // No models used.
  result.power = _.Power;
  result.mode = toCommonMode(_.Mode);
  result.celsius = true;
  result.degrees = getTemp();
  result.fanspeed = toCommonFanSpeed(_.Fan);
  result.swingv = toCommonSwingV(_.SwingV);
  result.swingh = toCommonSwingH(_.SwingH);
  result.turbo = getTurbo();
  result.econo = getEcono();
  result.clean = getClean();
  result.quiet = _.Silent;
  result.filter = _.Filter;
  result.sleep = _.Night ? 0 : -1;
  // Not supported.
  result.light = false;
  result.beep = false;
  result.clock = -1;
  return result;
}

/// Convert the internal state into a human readable string.
/// @return A string containing the settings in human-readable form.
String IRMitsubishiHeavy152Ac::toString(void) const {
  String result = "";
  result.reserve(180);  // Reserve some heap for the string to reduce fragging.
  result += addBoolToString(_.Power, kPowerStr, false);
  result += addModeToString(_.Mode, kMitsubishiHeavyAuto,
                            kMitsubishiHeavyCool, kMitsubishiHeavyHeat,
                            kMitsubishiHeavyDry, kMitsubishiHeavyFan);
  result += addTempToString(getTemp());
  result += addIntToString(_.Fan, kFanStr);
  result += kSpaceLBraceStr;
  switch (_.Fan) {
    case kMitsubishiHeavy152FanAuto:
      result += kAutoStr;
      break;
    case kMitsubishiHeavy152FanHigh:
      result += kHighStr;
      break;
    case kMitsubishiHeavy152FanLow:
      result += kLowStr;
      break;
    case kMitsubishiHeavy152FanMed:
      result += kMedStr;
      break;
    case kMitsubishiHeavy152FanMax:
      result += kMaxStr;
      break;
    case kMitsubishiHeavy152FanEcono:
      result += kEconoStr;
      break;
    case kMitsubishiHeavy152FanTurbo:
      result += kTurboStr;
      break;
    default:
      result += kUnknownStr;
  }
  result += ')';
  result += addSwingVToString(_.SwingV, kMitsubishiHeavy152SwingVAuto,
                              kMitsubishiHeavy152SwingVHighest,
                              kMitsubishiHeavy152SwingVHigh,
                              kMitsubishiHeavy152SwingVAuto,  // UpperMid unused
                              kMitsubishiHeavy152SwingVMiddle,
                              kMitsubishiHeavy152SwingVAuto,  // LowerMid unused
                              kMitsubishiHeavy152SwingVLow,
                              kMitsubishiHeavy152SwingVLowest,
                              kMitsubishiHeavy152SwingVOff,
                              // Below are unused.
                              kMitsubishiHeavy152SwingVAuto,
                              kMitsubishiHeavy152SwingVAuto,
                              kMitsubishiHeavy152SwingVAuto);
  result += addSwingHToString(_.SwingH, kMitsubishiHeavy152SwingHAuto,
                              kMitsubishiHeavy152SwingHLeftMax,
                              kMitsubishiHeavy152SwingHLeft,
                              kMitsubishiHeavy152SwingHMiddle,
                              kMitsubishiHeavy152SwingHRight,
                              kMitsubishiHeavy152SwingHRightMax,
                              kMitsubishiHeavy152SwingHOff,
                              kMitsubishiHeavy152SwingHLeftRight,
                              kMitsubishiHeavy152SwingHRightLeft,
                              // Below are unused.
                              kMitsubishiHeavy152SwingHAuto,
                              kMitsubishiHeavy152SwingHAuto);
  result += addBoolToString(_.Silent, kSilentStr);
  result += addBoolToString(getTurbo(), kTurboStr);
  result += addBoolToString(getEcono(), kEconoStr);
  result += addBoolToString(_.Night, kNightStr);
  result += addBoolToString(_.Filter, kFilterStr);
  result += addBoolToString(get3D(), k3DStr);
  result += addBoolToString(getClean(), kCleanStr);
  return result;
}


// Class for decoding and constructing MitsubishiHeavy88 AC messages.

/// Class constructor
/// @param[in] pin GPIO to be used when sending.
/// @param[in] inverted Is the output signal to be inverted?
/// @param[in] use_modulation Is frequency modulation to be used?
IRMitsubishiHeavy88Ac::IRMitsubishiHeavy88Ac(const uint16_t pin,
                                             const bool inverted,
                                             const bool use_modulation)
    : _irsend(pin, inverted, use_modulation) { stateReset(); }

/// Set up hardware to be able to send a message.
void IRMitsubishiHeavy88Ac::begin(void) { _irsend.begin(); }

#if SEND_MITSUBISHIHEAVY
/// Send the current internal state as an IR message.
/// @param[in] repeat Nr. of times the message will be repeated.
void IRMitsubishiHeavy88Ac::send(const uint16_t repeat) {
  _irsend.sendMitsubishiHeavy88(getRaw(), kMitsubishiHeavy88StateLength,
                                repeat);
}
#endif  // SEND_MITSUBISHIHEAVY

/// Reset the state of the remote to a known good state/sequence.
void IRMitsubishiHeavy88Ac::stateReset(void) {
  std::memcpy(_.raw, kMitsubishiHeavyZjsSig, kMitsubishiHeavySigLength);
  for (uint8_t i = kMitsubishiHeavySigLength; i < kMitsubishiHeavy88StateLength;
       i++) _.raw[i] = 0;
}

/// Get a PTR to the internal state/code for this protocol.
/// @return PTR to a code for this protocol based on the current internal state.
uint8_t *IRMitsubishiHeavy88Ac::getRaw(void) {
  checksum();
  return _.raw;
}

/// Set the internal state from a valid code for this protocol.
/// @param[in] data A valid code for this protocol.
void IRMitsubishiHeavy88Ac::setRaw(const uint8_t *data) {
  std::memcpy(_.raw, data, kMitsubishiHeavy88StateLength);
}

/// Set the requested power state of the A/C to on.
void IRMitsubishiHeavy88Ac::on(void) { setPower(true); }

/// Set the requested power state of the A/C to off.
void IRMitsubishiHeavy88Ac::off(void) { setPower(false); }

/// Change the power setting.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy88Ac::setPower(const bool on) {
  _.Power = on;
}

/// Get the value of the current power setting.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy88Ac::getPower(void) const {
  return _.Power;
}

/// Set the temperature.
/// @param[in] temp The temperature in degrees celsius.
void IRMitsubishiHeavy88Ac::setTemp(const uint8_t temp) {
  uint8_t newtemp = temp;
  newtemp = std::min(newtemp, kMitsubishiHeavyMaxTemp);
  newtemp = std::max(newtemp, kMitsubishiHeavyMinTemp);
  _.Temp = newtemp - kMitsubishiHeavyMinTemp;
}

/// Get the current temperature setting.
/// @return The current setting for temp. in degrees celsius.
uint8_t IRMitsubishiHeavy88Ac::getTemp(void) const {
  return _.Temp + kMitsubishiHeavyMinTemp;
}

/// Set the speed of the fan.
/// @param[in] speed The desired setting.
void IRMitsubishiHeavy88Ac::setFan(const uint8_t speed) {
  uint8_t newspeed = speed;
  switch (speed) {
    case kMitsubishiHeavy88FanLow:
    case kMitsubishiHeavy88FanMed:
    case kMitsubishiHeavy88FanHigh:
    case kMitsubishiHeavy88FanTurbo:
    case kMitsubishiHeavy88FanEcono: break;
    default: newspeed = kMitsubishiHeavy88FanAuto;
  }
  _.Fan = newspeed;
}

/// Get the current fan speed setting.
/// @return The current fan speed/mode.
uint8_t IRMitsubishiHeavy88Ac::getFan(void) const {
  return _.Fan;
}

/// Set the operating mode of the A/C.
/// @param[in] mode The desired operating mode.
void IRMitsubishiHeavy88Ac::setMode(const uint8_t mode) {
  uint8_t newmode = mode;
  switch (mode) {
    case kMitsubishiHeavyCool:
    case kMitsubishiHeavyDry:
    case kMitsubishiHeavyFan:
    case kMitsubishiHeavyHeat:
      break;
    default:
      newmode = kMitsubishiHeavyAuto;
  }
  _.Mode = newmode;
}

/// Get the operating mode setting of the A/C.
/// @return The current operating mode setting.
uint8_t IRMitsubishiHeavy88Ac::getMode(void) const {
  return _.Mode;
}

/// Set the Vertical Swing mode of the A/C.
/// @param[in] pos The position/mode to set the swing to.
void IRMitsubishiHeavy88Ac::setSwingVertical(const uint8_t pos) {
  uint8_t newpos;
  switch (pos) {
    case kMitsubishiHeavy88SwingVAuto:
    case kMitsubishiHeavy88SwingVHighest:
    case kMitsubishiHeavy88SwingVHigh:
    case kMitsubishiHeavy88SwingVMiddle:
    case kMitsubishiHeavy88SwingVLow:
    case kMitsubishiHeavy88SwingVLowest: newpos = pos; break;
    default: newpos = kMitsubishiHeavy88SwingVOff;
  }
  _.SwingV5 = newpos;
  _.SwingV7 = (newpos >> kMitsubishiHeavy88SwingVByte5Size);
}

/// Get the Vertical Swing mode of the A/C.
/// @return The native position/mode setting.
uint8_t IRMitsubishiHeavy88Ac::getSwingVertical(void) const {
  return _.SwingV5 | (_.SwingV7 << kMitsubishiHeavy88SwingVByte5Size);
}

/// Set the Horizontal Swing mode of the A/C.
/// @param[in] pos The position/mode to set the swing to.
void IRMitsubishiHeavy88Ac::setSwingHorizontal(const uint8_t pos) {
  uint8_t newpos;
  switch (pos) {
    case kMitsubishiHeavy88SwingHAuto:
    case kMitsubishiHeavy88SwingHLeftMax:
    case kMitsubishiHeavy88SwingHLeft:
    case kMitsubishiHeavy88SwingHMiddle:
    case kMitsubishiHeavy88SwingHRight:
    case kMitsubishiHeavy88SwingHRightMax:
    case kMitsubishiHeavy88SwingHLeftRight:
    case kMitsubishiHeavy88SwingHRightLeft:
    case kMitsubishiHeavy88SwingH3D: newpos = pos; break;
    default:                         newpos = kMitsubishiHeavy88SwingHOff;
  }
  _.SwingH1 = newpos;
  _.SwingH2 = (newpos >> kMitsubishiHeavy88SwingHSize);
}

/// Get the Horizontal Swing mode of the A/C.
/// @return The native position/mode setting.
uint8_t IRMitsubishiHeavy88Ac::getSwingHorizontal(void) const {
  return _.SwingH1 | (_.SwingH2 << kMitsubishiHeavy88SwingHSize);
}

/// Set the Turbo mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy88Ac::setTurbo(const bool on) {
  if (on)
    setFan(kMitsubishiHeavy88FanTurbo);
  else if (getTurbo()) setFan(kMitsubishiHeavy88FanAuto);
}

/// Get the Turbo mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy88Ac::getTurbo(void) const {
  return _.Fan == kMitsubishiHeavy88FanTurbo;
}

/// Set the Economical mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy88Ac::setEcono(const bool on) {
  if (on)
    setFan(kMitsubishiHeavy88FanEcono);
  else if (getEcono()) setFan(kMitsubishiHeavy88FanAuto);
}

/// Get the Economical mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy88Ac::getEcono(void) const {
  return _.Fan == kMitsubishiHeavy88FanEcono;
}

/// Set the 3D mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy88Ac::set3D(const bool on) {
  if (on)
    setSwingHorizontal(kMitsubishiHeavy88SwingH3D);
  else if (get3D())
    setSwingHorizontal(kMitsubishiHeavy88SwingHOff);
}

/// Get the 3D mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy88Ac::get3D(void) const {
  return getSwingHorizontal() == kMitsubishiHeavy88SwingH3D;
}

/// Set the Clean mode of the A/C.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRMitsubishiHeavy88Ac::setClean(const bool on) {
  _.Clean = on;
}

/// Get the Clean mode of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRMitsubishiHeavy88Ac::getClean(void) const {
  return _.Clean;
}

/// Verify the given state has a ZJ-S signature.
/// @param[in] state A ptr to a state to be checked.
/// @return true, the check passed. Otherwise, false.
bool IRMitsubishiHeavy88Ac::checkZjsSig(const uint8_t *state) {
  for (uint8_t i = 0; i < kMitsubishiHeavySigLength; i++)
    if (state[i] != kMitsubishiHeavyZjsSig[i]) return false;
  return true;
}

/// Calculate the checksum for the current internal state of the remote.
/// Note: Technically it has no checksum, but does have inverted byte pairs.
void IRMitsubishiHeavy88Ac::checksum(void) {
  const uint8_t kOffset = kMitsubishiHeavySigLength - 2;
  invertBytePairs(_.raw + kOffset, kMitsubishiHeavy88StateLength - kOffset);
}

/// Verify the checksum is valid for a given state.
/// @param[in] state The array to verify the checksum of.
/// @param[in] length The length/size of the state array.
/// @return true, if the state has a valid checksum. Otherwise, false.
/// Note: Technically it has no checksum, but does have inverted byte pairs.
bool IRMitsubishiHeavy88Ac::validChecksum(const uint8_t *state,
                                           const uint16_t length) {
  return IRMitsubishiHeavy152Ac::validChecksum(state, length);
}

/// Convert a stdAc::opmode_t enum into its native mode.
/// @param[in] mode The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy88Ac::convertMode(const stdAc::opmode_t mode) {
  return IRMitsubishiHeavy152Ac::convertMode(mode);
}

/// Convert a stdAc::fanspeed_t enum into it's native speed.
/// @param[in] speed The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy88Ac::convertFan(const stdAc::fanspeed_t speed) {
  switch (speed) {
    // Assumes Econo is slower than Low.
    case stdAc::fanspeed_t::kMin:    return kMitsubishiHeavy88FanEcono;
    case stdAc::fanspeed_t::kLow:    return kMitsubishiHeavy88FanLow;
    case stdAc::fanspeed_t::kMedium: return kMitsubishiHeavy88FanMed;
    case stdAc::fanspeed_t::kHigh:   return kMitsubishiHeavy88FanHigh;
    case stdAc::fanspeed_t::kMax:    return kMitsubishiHeavy88FanTurbo;
    default:                         return kMitsubishiHeavy88FanAuto;
  }
}

/// Convert a stdAc::swingv_t enum into it's native setting.
/// @param[in] position The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy88Ac::convertSwingV(const stdAc::swingv_t position) {
  switch (position) {
    case stdAc::swingv_t::kAuto:    return kMitsubishiHeavy88SwingVAuto;
    case stdAc::swingv_t::kHighest: return kMitsubishiHeavy88SwingVHighest;
    case stdAc::swingv_t::kHigh:    return kMitsubishiHeavy88SwingVHigh;
    case stdAc::swingv_t::kMiddle:  return kMitsubishiHeavy88SwingVMiddle;
    case stdAc::swingv_t::kLow:     return kMitsubishiHeavy88SwingVLow;
    case stdAc::swingv_t::kLowest:  return kMitsubishiHeavy88SwingVLowest;
    default:                        return kMitsubishiHeavy88SwingVOff;
  }
}

/// Convert a stdAc::swingh_t enum into it's native setting.
/// @param[in] position The enum to be converted.
/// @return The native equivalent of the enum.
uint8_t IRMitsubishiHeavy88Ac::convertSwingH(const stdAc::swingh_t position) {
  switch (position) {
    case stdAc::swingh_t::kAuto:     return kMitsubishiHeavy88SwingHAuto;
    case stdAc::swingh_t::kLeftMax:  return kMitsubishiHeavy88SwingHLeftMax;
    case stdAc::swingh_t::kLeft:     return kMitsubishiHeavy88SwingHLeft;
    case stdAc::swingh_t::kMiddle:   return kMitsubishiHeavy88SwingHMiddle;
    case stdAc::swingh_t::kRight:    return kMitsubishiHeavy88SwingHRight;
    case stdAc::swingh_t::kRightMax: return kMitsubishiHeavy88SwingHRightMax;
    default:                         return kMitsubishiHeavy88SwingHOff;
  }
}

/// Convert a native fan speed into its stdAc equivalent.
/// @param[in] speed The native setting to be converted.
/// @return The stdAc equivalent of the native setting.
stdAc::fanspeed_t IRMitsubishiHeavy88Ac::toCommonFanSpeed(const uint8_t speed) {
  switch (speed) {
    case kMitsubishiHeavy88FanTurbo: return stdAc::fanspeed_t::kMax;
    case kMitsubishiHeavy88FanHigh:  return stdAc::fanspeed_t::kHigh;
    case kMitsubishiHeavy88FanMed:   return stdAc::fanspeed_t::kMedium;
    case kMitsubishiHeavy88FanLow:   return stdAc::fanspeed_t::kLow;
    case kMitsubishiHeavy88FanEcono: return stdAc::fanspeed_t::kMin;
    default:                         return stdAc::fanspeed_t::kAuto;
  }
}

/// Convert a native horizontal swing postion to it's common equivalent.
/// @param[in] pos A native position to convert.
/// @return The common horizontal swing position.
stdAc::swingh_t IRMitsubishiHeavy88Ac::toCommonSwingH(const uint8_t pos) {
  switch (pos) {
    case kMitsubishiHeavy88SwingHLeftMax:  return stdAc::swingh_t::kLeftMax;
    case kMitsubishiHeavy88SwingHLeft:     return stdAc::swingh_t::kLeft;
    case kMitsubishiHeavy88SwingHMiddle:   return stdAc::swingh_t::kMiddle;
    case kMitsubishiHeavy88SwingHRight:    return stdAc::swingh_t::kRight;
    case kMitsubishiHeavy88SwingHRightMax: return stdAc::swingh_t::kRightMax;
    case kMitsubishiHeavy88SwingHOff:      return stdAc::swingh_t::kOff;
    default:                               return stdAc::swingh_t::kAuto;
  }
}

/// Convert a native vertical swing postion to it's common equivalent.
/// @param[in] pos A native position to convert.
/// @return The common vertical swing position.
stdAc::swingv_t IRMitsubishiHeavy88Ac::toCommonSwingV(const uint8_t pos) {
  switch (pos) {
    case kMitsubishiHeavy88SwingVHighest: return stdAc::swingv_t::kHighest;
    case kMitsubishiHeavy88SwingVHigh:    return stdAc::swingv_t::kHigh;
    case kMitsubishiHeavy88SwingVMiddle:  return stdAc::swingv_t::kMiddle;
    case kMitsubishiHeavy88SwingVLow:     return stdAc::swingv_t::kLow;
    case kMitsubishiHeavy88SwingVLowest:  return stdAc::swingv_t::kLowest;
    case kMitsubishiHeavy88SwingVOff:     return stdAc::swingv_t::kOff;
    default:                              return stdAc::swingv_t::kAuto;
  }
}

/// Convert the current internal state into its stdAc::state_t equivalent.
/// @return The stdAc equivalent of the native settings.
stdAc::state_t IRMitsubishiHeavy88Ac::toCommon(void) const {
  stdAc::state_t result{};
  result.protocol = decode_type_t::MITSUBISHI_HEAVY_88;
  result.model = -1;  // No models used.
  result.power = _.Power;
  result.mode = IRMitsubishiHeavy152Ac::toCommonMode(_.Mode);
  result.celsius = true;
  result.degrees = getTemp();
  result.fanspeed = toCommonFanSpeed(_.Fan);
  result.swingv = toCommonSwingV(getSwingVertical());
  result.swingh = toCommonSwingH(getSwingHorizontal());
  result.turbo = getTurbo();
  result.econo = getEcono();
  result.clean = _.Clean;
  // Not supported.
  result.quiet = false;
  result.filter = false;
  result.light = false;
  result.beep = false;
  result.sleep = -1;
  result.clock = -1;
  return result;
}

/// Convert the internal state into a human readable string.
/// @return A string containing the settings in human-readable form.
String IRMitsubishiHeavy88Ac::toString(void) const {
  String result = "";
  result.reserve(140);  // Reserve some heap for the string to reduce fragging.
  result += addBoolToString(_.Power, kPowerStr, false);
  result += addModeToString(_.Mode, kMitsubishiHeavyAuto,
                            kMitsubishiHeavyCool, kMitsubishiHeavyHeat,
                            kMitsubishiHeavyDry, kMitsubishiHeavyFan);
  result += addTempToString(getTemp());
  result += addIntToString(_.Fan, kFanStr);
  result += kSpaceLBraceStr;
  switch (_.Fan) {
    case kMitsubishiHeavy88FanAuto:
      result += kAutoStr;
      break;
    case kMitsubishiHeavy88FanHigh:
      result += kHighStr;
      break;
    case kMitsubishiHeavy88FanLow:
      result += kLowStr;
      break;
    case kMitsubishiHeavy88FanMed:
      result += kMedStr;
      break;
    case kMitsubishiHeavy88FanEcono:
      result += kEconoStr;
      break;
    case kMitsubishiHeavy88FanTurbo:
      result += kTurboStr;
      break;
    default:
      result += kUnknownStr;
  }
  result += ')';
  result += addSwingVToString(getSwingVertical(), kMitsubishiHeavy88SwingVAuto,
                              kMitsubishiHeavy88SwingVHighest,
                              kMitsubishiHeavy88SwingVHigh,
                              kMitsubishiHeavy88SwingVAuto,  // UpperMid unused
                              kMitsubishiHeavy88SwingVMiddle,
                              kMitsubishiHeavy88SwingVAuto,  // LowerMid unused
                              kMitsubishiHeavy88SwingVLow,
                              kMitsubishiHeavy88SwingVLowest,
                              kMitsubishiHeavy88SwingVOff,
                              // Below are unused.
                              kMitsubishiHeavy88SwingVAuto,
                              kMitsubishiHeavy88SwingVAuto,
                              kMitsubishiHeavy88SwingVAuto);
  result += addSwingHToString(getSwingHorizontal(),
                              kMitsubishiHeavy88SwingHAuto,
                              kMitsubishiHeavy88SwingHLeftMax,
                              kMitsubishiHeavy88SwingHLeft,
                              kMitsubishiHeavy88SwingHMiddle,
                              kMitsubishiHeavy88SwingHRight,
                              kMitsubishiHeavy88SwingHRightMax,
                              kMitsubishiHeavy88SwingHOff,
                              kMitsubishiHeavy88SwingHLeftRight,
                              kMitsubishiHeavy88SwingHRightLeft,
                              kMitsubishiHeavy88SwingH3D,
                              // Below are unused.
                              kMitsubishiHeavy88SwingHAuto);
  result += addBoolToString(getTurbo(), kTurboStr);
  result += addBoolToString(getEcono(), kEconoStr);
  result += addBoolToString(get3D(), k3DStr);
  result += addBoolToString(_.Clean, kCleanStr);
  return result;
}

#if DECODE_MITSUBISHIHEAVY
/// Decode the supplied Mitsubishi Heavy Industries A/C message.
/// Status: BETA / Appears to be working. Needs testing against a real device.
/// @param[in,out] results Ptr to the data to decode & where to store the result
/// @param[in] offset The starting index to use when attempting to decode the
///   raw data. Typically/Defaults to kStartOffset.
/// @param[in] nbits The number of data bits to expect.
///   Typically kMitsubishiHeavy88Bits or kMitsubishiHeavy152Bits (def).
/// @param[in] strict Flag indicating if we should perform strict matching.
/// @return True if it can decode it, false if it can't.
bool IRrecv::decodeMitsubishiHeavy(decode_results* results, uint16_t offset,
                                   const uint16_t nbits, const bool strict) {
  if (strict) {
    switch (nbits) {
      case kMitsubishiHeavy88Bits:
      case kMitsubishiHeavy152Bits:
        break;
      default:
        return false;  // Not what is expected
    }
  }

  uint16_t used;
  used = matchGeneric(results->rawbuf + offset, results->state,
                      results->rawlen - offset, nbits,
                      kMitsubishiHeavyHdrMark, kMitsubishiHeavyHdrSpace,
                      kMitsubishiHeavyBitMark, kMitsubishiHeavyOneSpace,
                      kMitsubishiHeavyBitMark, kMitsubishiHeavyZeroSpace,
                      kMitsubishiHeavyBitMark, kMitsubishiHeavyGap, true,
                      _tolerance, 0, false);
  if (used == 0) return false;
  offset += used;
  // Compliance
  switch (nbits) {
    case kMitsubishiHeavy88Bits:
      if (strict && !(IRMitsubishiHeavy88Ac::checkZjsSig(results->state) &&
                      IRMitsubishiHeavy88Ac::validChecksum(results->state)))
        return false;
      results->decode_type = MITSUBISHI_HEAVY_88;
      break;
    case kMitsubishiHeavy152Bits:
      if (strict && !(IRMitsubishiHeavy152Ac::checkZmsSig(results->state) &&
                      IRMitsubishiHeavy152Ac::validChecksum(results->state)))
        return false;
      results->decode_type = MITSUBISHI_HEAVY_152;
      break;
    default:
      return false;
  }

  // Success
  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_MITSUBISHIHEAVY
