/*
 * Adds support for Bosch's BMP581 high accuracy pressure and temperature sensor
 *  - Component structure based on ESPHome's BMP3XX component (as of March, 2023)
 *    - Implementation is easier as the sensor itself automatically compensates pressure for the temperature
 *      - Temperature and pressure data is converted via simple divison operations in this component
 *    - IIR filter level can independently be applied to temperature and pressure measurements
 *  - Bosch's BMP5-Sensor-API was consulted to verify that sensor configuration is done correctly
 *    - Copyright (c) 2022 Bosch Sensortec Gmbh, SPDX-License-Identifier: BSD-3-Clause
 *  - This component uses forced power mode only so measurements are synchronized by the host
 *  - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
 */

#include "bmp581_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"

namespace esphome::bmp581_base {

static const char *const TAG = "bmp581";

// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");

static const LogString *oversampling_to_str(Oversampling oversampling) {
  return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
}

// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");

static const LogString *iir_filter_to_str(IIRFilter filter) {
  return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
}

void BMP581Component::dump_config() {
  ESP_LOGCONFIG(TAG, "BMP581:");

  switch (this->error_code_) {
    case NONE:
      break;
    case ERROR_COMMUNICATION_FAILED:
      ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
      break;
    case ERROR_WRONG_CHIP_ID:
      ESP_LOGE(TAG, "Unknown chip ID");
      break;
    case ERROR_SENSOR_RESET:
      ESP_LOGE(TAG, "Reset failed");
      break;
    case ERROR_SENSOR_STATUS:
      ESP_LOGE(TAG, "Get status failed");
      break;
    case ERROR_PRIME_IIR_FAILED:
      ESP_LOGE(TAG, "IIR Filter failed to prime with initial measurement");
      break;
    default:
      ESP_LOGE(TAG, "Error %d", (int) this->error_code_);
      break;
  }

  LOG_UPDATE_INTERVAL(this);

  ESP_LOGCONFIG(TAG, "  Measurement conversion time: %ums", this->conversion_time_);

  if (this->temperature_sensor_) {
    LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
    ESP_LOGCONFIG(TAG,
                  "    IIR Filter: %s\n"
                  "    Oversampling: %s",
                  LOG_STR_ARG(iir_filter_to_str(this->iir_temperature_level_)),
                  LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)));
  }

  if (this->pressure_sensor_) {
    LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
    ESP_LOGCONFIG(TAG,
                  "    IIR Filter: %s\n"
                  "    Oversampling: %s",
                  LOG_STR_ARG(iir_filter_to_str(this->iir_pressure_level_)),
                  LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
  }
}

void BMP581Component::setup() {
  /*
   * Setup goes through several stages, which follows the post-power-up procedure (page 18 of datasheet) and then sets
   * configured options
   *  1) Soft reboot
   *  2) Verify ASIC chip ID matches BMP581
   *  3) Verify sensor status (check if NVM is okay)
   *  4) Enable data ready interrupt
   *  5) Write oversampling settings and set internal configuration values
   *  6) Configure and prime IIR Filter(s), if enabled
   */

  this->error_code_ = NONE;
  ////////////////////
  // 1) Soft reboot //
  ////////////////////

  // Power-On-Reboot bit is asserted if sensor successfully reset
  if (!this->reset_()) {
    ESP_LOGE(TAG, "Reset failed");

    this->error_code_ = ERROR_SENSOR_RESET;
    this->mark_failed();

    return;
  }

  ///////////////////////////////////////////
  // 2) Verify ASIC chip ID matches BMP581 //
  ///////////////////////////////////////////

  uint8_t chip_id;

  // read chip id from sensor
  if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
    ESP_LOGE(TAG, "Read chip ID failed");

    this->error_code_ = ERROR_COMMUNICATION_FAILED;
    this->mark_failed();

    return;
  }

  // verify id
  if (chip_id != BMP581_ASIC_ID && chip_id != BMP585_ASIC_ID) {
    ESP_LOGE(TAG, "Unknown chip ID");

    this->error_code_ = ERROR_WRONG_CHIP_ID;
    this->mark_failed();

    return;
  }

  ////////////////////////////////////////////////////
  // 3) Verify sensor status (check if NVM is okay) //
  ////////////////////////////////////////////////////

  if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
    ESP_LOGE(TAG, "Failed to read status register");

    this->error_code_ = ERROR_COMMUNICATION_FAILED;
    this->mark_failed();

    return;
  }

  // verify status_nvm_rdy bit (it is asserted if boot was successful)
  if (!(this->status_.bit.status_nvm_rdy)) {
    ESP_LOGE(TAG, "NVM not ready");

    this->error_code_ = ERROR_SENSOR_STATUS;
    this->mark_failed();

    return;
  }

  // verify status_nvm_err bit (it is asserted if an error is detected)
  if (this->status_.bit.status_nvm_err) {
    ESP_LOGE(TAG, "NVM error detected");

    this->error_code_ = ERROR_SENSOR_STATUS;
    this->mark_failed();

    return;
  }

  ////////////////////////////////////
  // 4) Enable data ready interrupt //
  ////////////////////////////////////

  // enable the data ready interrupt source
  if (!this->write_interrupt_source_settings_(true)) {
    ESP_LOGE(TAG, "Failed to write interrupt source register");

    this->error_code_ = ERROR_COMMUNICATION_FAILED;
    this->mark_failed();

    return;
  }

  //////////////////////////////////////////////////////////////////////////
  // 5) Write oversampling settings and set internal configuration values //
  //////////////////////////////////////////////////////////////////////////

  // configure pressure readings, if sensor is defined
  // otherwise, disable pressure oversampling
  if (this->pressure_sensor_) {
    this->osr_config_.bit.press_en = true;
  } else {
    this->pressure_oversampling_ = OVERSAMPLING_NONE;
  }

  // write oversampling settings
  if (!this->write_oversampling_settings_(this->temperature_oversampling_, this->pressure_oversampling_)) {
    ESP_LOGE(TAG, "Failed to write oversampling register");

    this->error_code_ = ERROR_COMMUNICATION_FAILED;
    this->mark_failed();

    return;
  }

  // set output data rate to 4 Hz=0x19 (page 65 of datasheet)
  //  - ?shouldn't? matter as this component only uses FORCED_MODE - datasheet is ambiguous
  //  - If in NORMAL_MODE or NONSTOP_MODE, then this would still allow deep standby to save power
  //  - will be written to BMP581 at next requested measurement
  this->odr_config_.bit.odr = 0x19;

  ///////////////////////////////////////////////////////
  /// 6) Configure and prime IIR Filter(s), if enabled //
  ///////////////////////////////////////////////////////

  if ((this->iir_temperature_level_ != IIR_FILTER_OFF) || (this->iir_pressure_level_ != IIR_FILTER_OFF)) {
    if (!this->write_iir_settings_(this->iir_temperature_level_, this->iir_pressure_level_)) {
      ESP_LOGE(TAG, "Failed to write IIR configuration registers");

      this->error_code_ = ERROR_COMMUNICATION_FAILED;
      this->mark_failed();

      return;
    }

    if (!this->prime_iir_filter_()) {
      ESP_LOGE(TAG, "Failed to prime the IIR filter with an initial measurement");

      this->error_code_ = ERROR_PRIME_IIR_FAILED;
      this->mark_failed();

      return;
    }
  }
}

void BMP581Component::update() {
  /*
   * Each update goes through several stages
   *  0) Verify either a temperature or pressure sensor is defined before proceeding
   *  1) Request a measurement
   *  2) Wait for measurement to finish (based on oversampling rates)
   *  3) Read data registers for temperature and pressure, if applicable
   *  4) Publish measurements to sensor(s), if applicable
   */

  ////////////////////////////////////////////////////////////////////////////////////
  // 0) Verify either a temperature or pressure sensor is defined before proceeding //
  ////////////////////////////////////////////////////////////////////////////////////

  if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) {
    return;
  }

  //////////////////////////////
  // 1) Request a measurement //
  //////////////////////////////

  ESP_LOGVV(TAG, "Requesting measurement");

  if (!this->start_measurement_()) {
    ESP_LOGW(TAG, "Requesting forced measurement failed");
    this->status_set_warning();

    return;
  }

  //////////////////////////////////////////////////////////////////////
  // 2) Wait for measurement to finish (based on oversampling rates) //
  //////////////////////////////////////////////////////////////////////

  ESP_LOGVV(TAG, "Measurement should take %d ms", this->conversion_time_);

  this->set_timeout("measurement", this->conversion_time_, [this]() {
    float temperature = 0.0;
    float pressure = 0.0;

    ////////////////////////////////////////////////////////////////////////
    // 3) Read data registers for temperature and pressure, if applicable //
    ////////////////////////////////////////////////////////////////////////

    if (this->pressure_sensor_) {
      if (!this->read_temperature_and_pressure_(temperature, pressure)) {
        ESP_LOGW(TAG, "Failed to read temperature and pressure; skipping update");
        this->status_set_warning();

        return;
      }
    } else {
      if (!this->read_temperature_(temperature)) {
        ESP_LOGW(TAG, "Failed to read temperature; skipping update");
        this->status_set_warning();

        return;
      }
    }

    /////////////////////////////////////////////////////////
    // 4) Publish measurements to sensor(s), if applicable //
    /////////////////////////////////////////////////////////

    if (this->temperature_sensor_) {
      this->temperature_sensor_->publish_state(temperature);
    }

    if (this->pressure_sensor_) {
      this->pressure_sensor_->publish_state(pressure);
    }

    this->status_clear_warning();
  });
}

bool BMP581Component::check_data_readiness_() {
  //   - verifies component is not internally in standby mode
  //   - reads interrupt status register
  //   - checks if data ready bit is asserted
  //      - If true, then internally sets component to standby mode if in forced mode
  //   - returns data readiness state

  if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) {
    ESP_LOGD(TAG, "Data not ready, sensor is in standby mode");
    return false;
  }

  uint8_t status;

  if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
    ESP_LOGE(TAG, "Failed to read interrupt status register");
    return false;
  }

  this->int_status_.reg = status;

  if (this->int_status_.bit.drdy_data_reg) {
    // If in forced mode, then set internal record of the power mode to STANDBY_MODE
    //  - sensor automatically returns to standby mode after completing a forced measurement
    if (this->odr_config_.bit.pwr_mode == FORCED_MODE) {
      this->odr_config_.bit.pwr_mode = STANDBY_MODE;
    }

    return true;
  }

  return false;
}

bool BMP581Component::prime_iir_filter_() {
  // - temporarily disables oversampling for a fast initial measurement; avoids slowing down ESPHome's startup process
  // - enables IIR filter flushing with forced measurements
  // - forces a measurement; flushing the IIR filter and priming it with a current value
  // - disables IIR filter flushing with forced measurements
  // - reverts to internally configured oversampling rates
  // - returns success of all register writes/priming

  // store current internal oversampling settings to revert to after priming
  Oversampling current_temperature_oversampling = (Oversampling) this->osr_config_.bit.osr_t;
  Oversampling current_pressure_oversampling = (Oversampling) this->osr_config_.bit.osr_p;

  // temporarily disables oversampling for temperature and pressure for a fast priming measurement
  if (!this->write_oversampling_settings_(OVERSAMPLING_NONE, OVERSAMPLING_NONE)) {
    ESP_LOGE(TAG, "Failed to write oversampling register");

    return false;
  }

  // flush the IIR filter with forced measurements (we will only flush once)
  this->dsp_config_.bit.iir_flush_forced_en = true;
  if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
    ESP_LOGE(TAG, "Failed to write IIR source register");

    return false;
  }

  // forces an intial measurement
  //  - this measurements flushes the IIR filter reflecting written DSP settings
  //  - flushing with this initial reading avoids having the internal previous data aquisition being 0, which
  //    (I)nfinitely affects future values
  if (!this->start_measurement_()) {
    ESP_LOGE(TAG, "Failed to request a forced measurement");

    return false;
  }

  // wait for priming measurement to complete
  //  - with oversampling disabled, the conversion time for a single measurement for pressure and temperature is
  //    ceilf(1.05*(1.0+1.0)) = 3ms
  //  - see page 12 of datasheet for details
  delay(3);

  if (!this->check_data_readiness_()) {
    ESP_LOGE(TAG, "IIR priming measurement was not ready");

    return false;
  }

  // disable IIR filter flushings on future forced measurements
  this->dsp_config_.bit.iir_flush_forced_en = false;
  if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
    ESP_LOGE(TAG, "Failed to write IIR source register");

    return false;
  }

  // revert oversampling rates to original settings
  return this->write_oversampling_settings_(current_temperature_oversampling, current_pressure_oversampling);
}

bool BMP581Component::read_temperature_(float &temperature) {
  // - verifies data is ready to be read
  // - reads in 3 bytes of temperature data
  // - returns whether successful, where the the variable parameter contains
  //    - the measured temperature (in degrees Celsius)

  if (!this->check_data_readiness_()) {
    ESP_LOGW(TAG, "Data not ready, skipping this update");
    this->status_set_warning();

    return false;
  }

  uint8_t data[3];
  if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
    ESP_LOGW(TAG, "Failed to read measurement");
    this->status_set_warning();

    return false;
  }

  // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0]
  int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8;
  temperature = (float) (raw_temp / 65536.0);  // convert measurement to degrees Celsius (page 22 of datasheet)

  return true;
}

bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &pressure) {
  // - verifies data is ready to be read
  // - reads in 6 bytes of temperature data (3 for temeperature, 3 for pressure)
  // - returns whether successful, where the variable parameters contain
  //    - the measured temperature (in degrees Celsius)
  //    - the measured pressure (in Pa)

  if (!this->check_data_readiness_()) {
    ESP_LOGW(TAG, "Data not ready, skipping this update");
    this->status_set_warning();

    return false;
  }

  uint8_t data[6];
  if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
    ESP_LOGW(TAG, "Failed to read measurement");
    this->status_set_warning();

    return false;
  }

  // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0]
  int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8;
  temperature = (float) (raw_temp / 65536.0);  // convert measurement to degrees Celsius (page 22 of datasheet)

  // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3]
  int32_t raw_press = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3];
  pressure = (float) (raw_press / 64.0);  // Divide by 2^6=64 for Pa (page 22 of datasheet)

  return true;
}

bool BMP581Component::reset_() {
  // - activates interface (only relevant for SPI mode)
  // - writes reset command to the command register
  // - waits for sensor to complete reset
  // - activates interface (only relevant for SPI mode)
  // - returns the Power-On-Reboot interrupt status, which is asserted if successful

  // activates communication interface (SPI only)
  this->activate_interface();

  // writes reset command to BMP's command register
  if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
    ESP_LOGE(TAG, "Failed to write reset command");
    return false;
  }

  // t_{soft_res} = 2ms (page 11 of datasheet); time it takes to enter standby mode
  //  - round up to 3 ms
  delay(3);

  // reactivates communication interface after reset (SPI only)
  this->activate_interface();

  // read interrupt status register
  if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
    ESP_LOGE(TAG, "Failed to read interrupt status register");

    return false;
  }

  // power-On-Reboot bit is asserted if sensor successfully reset
  return this->int_status_.bit.por;
}

bool BMP581Component::start_measurement_() {
  // - only pushes the sensor into FORCED_MODE for a reading if already in STANDBY_MODE
  // - returns whether a measurement is in progress or has been initiated

  if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) {
    return this->write_power_mode_(FORCED_MODE);
  } else {
    return true;
  }
}

bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter pressure_iir) {
  // - ensures data registers store filtered values
  // - sets IIR filter levels on sensor
  // - matches other default settings on sensor
  // - writes configuration to the two relevant registers
  // - returns success or failure of write to the registers

  // If the temperature/pressure IIR filter is configured, then ensure data registers store the filtered measurement
  this->dsp_config_.bit.shdw_sel_iir_t = (temperature_iir != IIR_FILTER_OFF);
  this->dsp_config_.bit.shdw_sel_iir_p = (pressure_iir != IIR_FILTER_OFF);

  // set temperature and pressure IIR filter level to configured values
  this->iir_config_.bit.set_iir_t = temperature_iir;
  this->iir_config_.bit.set_iir_p = pressure_iir;

  // enable pressure and temperature compensation (page 61 of datasheet)
  //  - ?only relevant if IIR filter is applied?; the datasheet is ambiguous
  //  - matches BMP's default setting
  this->dsp_config_.bit.comp_pt_en = 0x3;

  // BMP581_DSP register and BMP581_DSP_IIR registers are successive
  //  - allows us to write the IIR configuration with one command to both registers
  uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
  return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
}

bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
  // - updates component's internal setting
  // - returns success or failure of write to interrupt source register

  this->int_source_.bit.drdy_data_reg_en = data_ready_enable;

  // write interrupt source register
  return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
}

bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
                                                   Oversampling pressure_oversampling) {
  // - updates component's internal setting
  // - returns success or failure of write to Over-Sampling Rate register

  this->osr_config_.bit.osr_t = temperature_oversampling;
  this->osr_config_.bit.osr_p = pressure_oversampling;

  return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
}

bool BMP581Component::write_power_mode_(OperationMode mode) {
  // - updates the component's internal power mode
  // - returns success or failure of write to Output Data Rate register

  this->odr_config_.bit.pwr_mode = mode;

  // write odr register
  return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
}

}  // namespace esphome::bmp581_base
