#include "PMS.h"
#include "../Main/BoardDef.h"

/**
 * @brief Initializes the sensor and attempts to read data.
 *
 * @param stream UART stream
 * @return true Sucecss
 * @return false Failure
 */
bool PMSBase::begin(Stream *stream) {
  Serial.printf("initializing PM sensor\n");

  failCount = 0;
  _connected = false;

  // empty first
  int bytesCleared = 0;
  while (stream->read() != -1) {
    bytesCleared++;
  }
  Serial.printf("cleared %d byte(s)\n", bytesCleared);

  // explicitly put the sensor into active mode, this seems to be be needed for the Cubic PM2009X
  Serial.printf("setting active mode\n");
  uint8_t activeModeCommand[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 };
  size_t bytesWritten = stream->write(activeModeCommand, sizeof(activeModeCommand));
  Serial.printf("%d byte(s) written\n", bytesWritten);

  // Run and check sensor data for 4sec
  unsigned long lastInit = millis();
  while (true) {
    readPackage(stream);
    if (_connected) {
      break;
    }

    delay(1);
    unsigned long ms = (unsigned long)(millis() - lastInit);
    if (ms >= 4000) {
      break;
    }
  }
  return _connected;
}

/**
 * @brief Read PMS package send to device each 1sec
 *
 * @param serial
 */
void PMSBase::readPackage(Stream *serial) {
  /** If readPackage has process as period larger than READ_PACKAGE_TIMEOUT,
   * should be clear the lastPackage and readBufferIndex */
  if (lastReadPackage) {
    unsigned long ms = (unsigned long)(millis() - lastReadPackage);
    if (ms >= READ_PACKGE_TIMEOUT) {
      /** Clear buffer */
      readBufferIndex = 0;

      /** Disable check read package timeout */
      lastPackage = 0;

      Serial.println("Last process timeout, clear buffer and last handle package");
    }

    lastReadPackage = millis();
    if (!lastReadPackage) {
      lastReadPackage = 1;
    }
  } else {
    lastReadPackage = millis();
    if (!lastReadPackage) {
      lastReadPackage = 1;
    }
  }

  /** Count to call delay() to release the while loop MCU resource for avoid the
   * watchdog time reset */
  uint8_t delayCount = 0;
  while (serial->available()) {
    /** Get value */
    uint8_t value = (uint8_t)serial->read();

    /** Process receiving package... */
    switch (readBufferIndex) {
    case 0: /** Start byte 1 */
      if (value == 0x42) {
        readBuffer[readBufferIndex++] = value;
      }
      break;
    case 1: /** Start byte 2 */
      if (value == 0x4d) {
        readBuffer[readBufferIndex++] = value;
      } else {
        readBufferIndex = 0;
      }
      break;
    case 2: /** Frame length */
      if (value == 0x00) {
        readBuffer[readBufferIndex++] = value;
      } else {
        readBufferIndex = 0;
      }
      break;
    case 3: /** Frame length */
      if (value == 0x1C) {
        readBuffer[readBufferIndex++] = value;
      } else {
        readBufferIndex = 0;
      }
      break;
    default: /** Data */
    {
      readBuffer[readBufferIndex++] = value;

      /** Check that received full bufer */
      if (readBufferIndex >= sizeof(readBuffer)) {
        /** validata package */
        if (validate(readBuffer)) {
          _connected = true; /** Set connected status */

          /** Parse data */
          parse(readBuffer);

          /** Set last received package */
          lastPackage = millis();
          if (lastPackage == 0) {
            lastPackage = 1;
          }
        }

        /** Clear buffer index */
        readBufferIndex = 0;
      }
      break;
    }
    }

    /** Avoid task watchdog timer reset... */
    delayCount++;
    if (delayCount >= 32) {
      delayCount = 0;
      delay(1);
    }
  }

  /** Check that sensor removed */
  if (lastPackage) {
    unsigned long ms = (unsigned long)(millis() - lastPackage);
    if (ms >= READ_PACKGE_TIMEOUT) {
      lastPackage = 0;
      _connected = false;
      Serial.println("PMS disconnected");
    }
  }
}

/**
 * @brief Increate number of fail
 *
 */
void PMSBase::updateFailCount(void) {
  if (failCount < failCountMax) {
    failCount++;
  }
}

void PMSBase::resetFailCount(void) { failCount = 0; }

/**
 * @brief Get number of fail
 *
 * @return int
 */
int PMSBase::getFailCount(void) { return failCount; }

int PMSBase::getFailCountMax(void) { return failCountMax; }

/**
 * @brief Read PMS 0.1 ug/m3 with CF = 1 PM estimates
 *
 * @return uint16_t
 */
uint16_t PMSBase::getRaw0_1(void) { return pms_raw0_1; }

/**
 * @brief Read PMS 2.5 ug/m3 with CF = 1 PM estimates
 *
 * @return uint16_t
 */
uint16_t PMSBase::getRaw2_5(void) { return pms_raw2_5; }

/**
 * @brief Read PMS 10 ug/m3 with CF = 1 PM estimates
 *
 * @return uint16_t
 */
uint16_t PMSBase::getRaw10(void) { return pms_raw10; }

/**
 * @brief Read PMS 0.1 ug/m3
 *
 * @return uint16_t
 */
uint16_t PMSBase::getPM0_1(void) { return pms_pm0_1; }

/**
 * @brief Read PMS 2.5 ug/m3
 *
 * @return uint16_t
 */
uint16_t PMSBase::getPM2_5(void) { return pms_pm2_5; }

/**
 * @brief Read PMS 10 ug/m3
 *
 * @return uint16_t
 */
uint16_t PMSBase::getPM10(void) { return pms_pm10; }

/**
 * @brief Get numnber concentrations over 0.3 um/0.1L
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount0_3(void) { return pms_count0_3; }

/**
 * @brief Get numnber concentrations over 0.5 um/0.1L
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount0_5(void) { return pms_count0_5; }

/**
 * @brief Get numnber concentrations over 1.0 um/0.1L
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount1_0(void) { return pms_count1_0; }

/**
 * @brief Get numnber concentrations over 2.5 um/0.1L
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount2_5(void) { return pms_count2_5; }

bool PMSBase::connected(void) { return _connected; }

/**
 * @brief Get numnber concentrations over 5.0 um/0.1L (only PMS5003)
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount5_0(void) { return pms_count5_0; }

/**
 * @brief Get numnber concentrations over 10.0 um/0.1L (only PMS5003)
 *
 * @return uint16_t
 */
uint16_t PMSBase::getCount10(void) { return pms_count10; }

/**
 * @brief Get temperature (only PMS5003T)
 *
 * @return uint16_t
 */
int16_t PMSBase::getTemp(void) { return pms_temp; }

/**
 * @brief Get humidity (only PMS5003T)
 *
 * @return uint16_t
 */
uint16_t PMSBase::getHum(void) { return pms_hum; }

/**
 * @brief Get firmware version code
 *
 * @return uint8_t
 */
uint8_t PMSBase::getFirmwareVersion(void) { return pms_firmwareVersion; }

/**
 * @brief Ge PMS5003 error code
 *
 * @return uint8_t
 */
uint8_t PMSBase::getErrorCode(void) { return pms_errorCode; }

/**
 * @brief Convert PMS2.5 to US AQI unit
 *
 * @param pm02
 * @return int
 */
int PMSBase::pm25ToAQI(int pm02) {
  if (pm02 <= 9.0)
    return ((50 - 0) / (9.0 - .0) * (pm02 - .0) + 0);
  else if (pm02 <= 35.4)
    return ((100 - 51) / (35.4 - 9.1) * (pm02 - 9.0) + 51);
  else if (pm02 <= 55.4)
    return ((150 - 101) / (55.4 - 35.5) * (pm02 - 35.5) + 101);
  else if (pm02 <= 125.4)
    return ((200 - 151) / (125.4 - 55.5) * (pm02 - 55.5) + 151);
  else if (pm02 <= 225.4)
    return ((300 - 201) / (225.4 - 125.5) * (pm02 - 125.5) + 201);
  else if (pm02 <= 325.4)
    return ((500 - 301) / (325.4 - 225.5) * (pm02 - 225.5) + 301);
  else
    return 500;
}


/**
 * @brief SLR correction for PM2.5
 *
 * Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/
 *
 * @param pm25 PM2.5 raw value
 * @param pm003Count PM0.3 count
 * @param scalingFactor Scaling factor
 * @param intercept Intercept
 * @return float Calibrated PM2.5 value
 */
float PMSBase::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) {
  float calibrated;

  float lowCalibrated = (scalingFactor * pm003Count) + intercept;
  if (lowCalibrated < 31) {
    calibrated = lowCalibrated;
  } else {
    calibrated = pm25;
  }

  // No negative value for pm2.5
  if (calibrated < 0) {
    return 0.0;
  }

  return calibrated;
}


float PMSBase::slrCorrectionBy003Count(float pm003Count, float scalingFactor, float intercept) {
  float calibrated = (scalingFactor * pm003Count) + intercept;

  // No negative value for pm2.5
  if (calibrated < 0) {
    return 0.0;
  }

  return calibrated;
}

float PMSBase::slrCorrectionByPm25Raw(float pm25, float scalingFactor,
                                      float intercept) {
  float calibrated;
  if (pm25 == 0.0) {
    calibrated = 0.0;
  } else {
    calibrated = (scalingFactor * pm25) + intercept;
  }

  // No negative value for pm2.5
  if (calibrated < 0) {
    return 0.0;
  }

  return calibrated;
}

/**
 * @brief Correction PM2.5
 *
 * Formula: https://www.airgradient.com/documentation/correction-algorithms/
 *
 * @param pm25 Raw PM2.5 value
 * @param humidity Humidity value (%)
 * @return compensated pm25 value
 */
float PMSBase::compensate(float pm25, float humidity) {
  float value;

  // Correct invalid humidity value
  if (humidity < 0) {
    humidity = 0;
  }
  if (humidity > 100) {
    humidity = 100.0f;
  }

  // If its already 0, do not proceed
  if (pm25 == 0) {
    return 0.0;
  }

  if (pm25 < 30) { /** pm2.5 < 30 */
    value = (pm25 * 0.524f) - (humidity * 0.0862f) + 5.75f;
  } else if (pm25 < 50) { /** 30 <= pm2.5 < 50 */
    value = (0.786f * (pm25 * 0.05f - 1.5f) + 0.524f * (1.0f - (pm25 * 0.05f - 1.5f))) * pm25 -
            (0.0862f * humidity) + 5.75f;
  } else if (pm25 < 210) { /** 50 <= pm2.5 < 210 */
    value = (0.786f * pm25) - (0.0862f * humidity) + 5.75f;
  } else if (pm25 < 260) { /** 210 <= pm2.5 < 260 */
    value = (0.69f * (pm25 * 0.02f - 4.2f) + 0.786f * (1.0f - (pm25 * 0.02f - 4.2f))) * pm25 -
            (0.0862f * humidity * (1.0f - (pm25 * 0.02f - 4.2f))) +
            (2.966f * (pm25 * 0.02f - 4.2f)) + (5.75f * (1.0f - (pm25 * 0.02f - 4.2f))) +
            (8.84f * (1.e-4) * pm25 * pm25 * (pm25 * 0.02f - 4.2f));
  } else { /** 260 <= pm2.5 */
    value = 2.966f + (0.69f * pm25) + (8.84f * (1.e-4) * pm25 * pm25);
  }

  // No negative value for pm2.5
  if (value < 0) {
    return 0.0;
  }

  return value;
}

/**
 * @brief   Convert two byte value to uint16_t value
 *
 * @param buf bytes array (must be >= 2)
 * @return int16_t
 */
int16_t PMSBase::toI16(const uint8_t *buf) {
  int16_t value = buf[0];
  value = (value << 8) | buf[1];
  return value;
}

uint16_t PMSBase::toU16(const uint8_t *buf) {
  uint16_t value = buf[0];
  value = (value << 8) | buf[1];
  return value;
}

/**
 * @brief Validate package data
 *
 * @param buf Package buffer
 * @return true Success
 * @return false Failed
 */
bool PMSBase::validate(const uint8_t *buf) {
  uint16_t sum = 0;
  for (int i = 0; i < 30; i++) {
    sum += buf[i];
  }
  if (sum == toU16(&buf[30])) {
    return true;
  }
  return false;
}

void PMSBase::parse(const uint8_t *buf) {
  // Standard particle
  pms_raw0_1 = toU16(&buf[4]);
  pms_raw2_5 = toU16(&buf[6]);
  pms_raw10 = toU16(&buf[8]);
  // atmospheric
  pms_pm0_1 = toU16(&buf[10]);
  pms_pm2_5 = toU16(&buf[12]);
  pms_pm10 = toU16(&buf[14]);

  // particle count
  pms_count0_3 = toU16(&buf[16]);
  pms_count0_5 = toU16(&buf[18]);
  pms_count1_0 = toU16(&buf[20]);
  pms_count2_5 = toU16(&buf[22]);
  pms_count5_0 = toU16(&buf[24]); // PMS5003 only
  pms_count10 = toU16(&buf[26]);  // PMS5003 only

  // Others
  pms_temp = toU16(&buf[24]); // PMS5003T only
  pms_hum = toU16(&buf[26]);  // PMS5003T only
  pms_firmwareVersion = buf[28];
  pms_errorCode = buf[29];
}
