/*
 * SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
 *
 * SPDX-License-Identifier: MIT
 */
/*!
  @file unit_SCD40.hpp
  @brief SCD40 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SCD40_HPP
#define M5_UNIT_ENV_UNIT_SCD40_HPP

#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits>  // NaN

namespace m5 {
namespace unit {

/*!
  @namespace scd4x
  @brief For SCD40/41
 */
namespace scd4x {
/*!
  @enum Mode
  @brief Mode of periodic measurement
 */
enum class Mode : uint8_t {
    Normal,    //!< Normal (Receive data every 5 seconds)
    LowPower,  //!< Low power (Receive data every 30 seconds)
};

/*!
  @struct Data
  @brief Measurement data group
 */
struct Data {
    std::array<uint8_t, 9> raw{};  //!< @brief RAW data
    uint16_t co2() const;          //!< @brief CO2 concentration (ppm)
    //! @brief temperature (Celsius)
    inline float temperature() const
    {
        return celsius();
    }
    float celsius() const;     //!< @brief temperature (Celsius)
    float fahrenheit() const;  //!< @brief temperature (Fahrenheit)
    float humidity() const;    //!< @brief humidity (RH)
};

///@cond
// Max command duration(ms)
// For SCD40/41
constexpr uint16_t READ_MEASUREMENT_DURATION{1};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT_DURATION{500};
constexpr uint16_t SET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t GET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t SET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t GET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t SET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t GET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t PERFORM_FORCED_CALIBRATION_DURATION{400};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_DATA_READY_STATUS_DURATION{1};
constexpr uint16_t PERSIST_SETTINGS_DURATION{800};
constexpr uint16_t GET_SERIAL_NUMBER_DURATION{1};
constexpr uint16_t PERFORM_SELF_TEST_DURATION{10000};
constexpr uint16_t PERFORM_FACTORY_RESET_DURATION{1200};
constexpr uint16_t REINIT_DURATION{20};
/// @endcond

}  // namespace scd4x

/*!
  @class m5::unit::UnitSCD40
  @brief SCD40 unit component
*/
class UnitSCD40 : public Component, public PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data> {
    M5_UNIT_COMPONENT_HPP_BUILDER(UnitSCD40, 0x62);

public:
    /*!
      @struct config_t
      @brief Settings for begin
     */
    struct config_t {
        //! Start periodic measurement on begin?
        bool start_periodic{true};
        //! Mode of periodic measurement if start on begin?
        scd4x::Mode mode{scd4x::Mode::Normal};
        //! Enable ASC on begin?
        bool calibration{true};
    };

    explicit UnitSCD40(const uint8_t addr = DEFAULT_ADDRESS)
        : Component(addr), _data{new m5::container::CircularBuffer<scd4x::Data>(1)}
    {
        auto ccfg  = component_config();
        ccfg.clock = 400 * 1000U;
        component_config(ccfg);
    }
    virtual ~UnitSCD40()
    {
    }

    virtual bool begin() override;
    virtual void update(const bool force = false) override;

    ///@name Settings for begin
    ///@{
    /*! @brief Gets the configration */
    inline config_t config() const
    {
        return _cfg;
    }
    //! @brief Set the configration
    inline void config(const config_t &cfg)
    {
        _cfg = cfg;
    }
    ///@}

    ///@name Measurement data by periodic
    ///@{
    //! @brief Oldest measured CO2 concentration (ppm)
    inline uint16_t co2() const
    {
        return !empty() ? oldest().co2() : 0;
    }
    //! @brief Oldest measured temperature (Celsius)
    inline float temperature() const
    {
        return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
    }
    //! @brief Oldest measured temperature (Celsius)
    inline float celsius() const
    {
        return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
    }
    //! @brief Oldest measured temperature (Fahrenheit)
    inline float fahrenheit() const
    {
        return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
    }
    //! @brief Oldest measured humidity (RH)
    inline float humidity() const
    {
        return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
    }
    ///@}

    ///@name Periodic measurement
    ///@{
    /*!
      @brief Start periodic measurement
      @param mode Measurement mode
      @return True if successful
    */
    inline bool startPeriodicMeasurement(const scd4x::Mode mode = scd4x::Mode::Normal)
    {
        return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::startPeriodicMeasurement(mode);
    }
    /*!
      @brief Start low power periodic measurement
      @return True if successful
    */
    inline bool startLowPowerPeriodicMeasurement()
    {
        return startPeriodicMeasurement(scd4x::Mode::LowPower);
    }
    /*!
      @brief Stop periodic measurement
      @param duration Max command duration(ms)
      @return True if successful
    */
    inline bool stopPeriodicMeasurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION)
    {
        return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::stopPeriodicMeasurement(duration);
    }
    ///@}

    ///@name On-Chip Output Signal Compensation
    ///@{
    /*!
      @brief Write the temperature offset
      @details Define how warm the sensor is compared to ambient, so RH and T
      are temperature compensated. Has no effect on the CO2 reading Default offsetis 4C
      @param offset (0 <= offset < 175)
      @param duration Max command duration(ms)
      @return True if successful
      @note Recommended temperature offset values are between 0 and 20
      @warning During periodic detection runs, an error is returned
    */
    bool writeTemperatureOffset(const float offset, const uint32_t duration = scd4x::SET_TEMPERATURE_OFFSET_DURATION);
    /*!
      @brief Read the temperature offset
      @param[out] offset Offset value
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool readTemperatureOffset(float &offset);
    /*!
      @brief Write the sensor altitude
      @details Define the sensor altitude in metres above sea level, so RH and CO2 arecompensated for atmospheric
      pressure Default altitude is 0m
      @param altitude Sensor altitude [m]
      @param duration Max command duration(ms)
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool writeSensorAltitude(const uint16_t altitude, const uint32_t duration = scd4x::SET_SENSOR_ALTITUDE_DURATION);
    /*!
      @brief Read the sensor altitude
      @param[out] altitude Sensor altitude [m]
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool readSensorAltitude(uint16_t &altitude);
    /*!
      @brief Write the ambient pressure
      @details Define the ambient pressure in Pascals, so RH and CO2 are compensated for atmospheric pressure
      setAmbientPressure overrides setSensorAltitude
      @param pressure Ambient pressure [hPa]
      @param duration Max command duration(ms)
      @return True if successful
      @warning Valid Valid input values are between 700 – 1200 hPa
    */
    bool writeAmbientPressure(const uint16_t pressure, const uint32_t duration = scd4x::SET_AMBIENT_PRESSURE_DURATION);
    /*!
      @brief Read the ambient pressure
      @param[out]  presure Ambient pressure [hPa]
      @return True if successful
    */
    bool readAmbientPressure(uint16_t &pressure);
    ///@}

    ///@name Field Calibration
    ///@{
    /*!
      @brief Perform forced recalibration
      @param concentration Unit:ppm
      @param[out] correction The FRC correction value
      @return True if successful
      @warning During periodic detection runs, an error is returned
      @warning  Blocked until the process is completed (about 400ms)
    */
    bool performForcedRecalibration(const uint16_t concentration, int16_t &correction);
    /*!
      @brief Enable/disable automatic self calibration
      @param enabled Enable automatic self calibration if true
      @param duration Max command duration(ms)
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool writeAutomaticSelfCalibrationEnabled(
        const bool enabled = true, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION);
    /*!
      @brief Check if automatic self calibration is enabled
      @param[out] enabled  True if automatic self calibration is enabled
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool readAutomaticSelfCalibrationEnabled(bool &enabled);

    /*!
      @brief Write the value of the ASC baseline target
      @param ppm ASC target ppm
      @param duration Max command duration(ms)
      @return True if successful
      @warning During periodic detection runs, an error is returned
     */
    bool writeAutomaticSelfCalibrationTarget(
        const uint16_t ppm, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION);
    /*!
      @brief Read the value of the ASC baseline target
      @param[out] ppm ASC target ppm
      @return True if successful
      @warning During periodic detection runs, an error is returned
     */
    bool readAutomaticSelfCalibrationTarget(uint16_t &ppm);
    ///@}

    ///@name Advanced Features
    ///@{
    /*!
      @brief Write sensor settings from RAM to EEPROM
      @return True if successful
      @warning During periodic detection runs, an error is returned
    */
    bool writePersistSettings(const uint32_t duration = scd4x::PERSIST_SETTINGS_DURATION);
    /*!
      @brief Read the serial number string
      @param[out] serialNumber Output buffer
      @return True if successful
      @warning Size must be at least 13 bytes
      @warning During periodic detection runs, an error is returned
    */
    bool readSerialNumber(char *serialNumber);
    /*!
      @brief Read the serial number value
      @param[out] serialNumber serial number value
      @return True if successful
      @note The serial number is 48 bit
      @warning During periodic detection runs, an error is returned
    */
    bool readSerialNumber(uint64_t &serialNumber);
    /*!
      @brief Perform self test
      @param[out] True if malfunction detected
      @return True if successful
      @note Takes 10 seconds to complete
      @warning During periodic detection runs, an error is returned
    */
    bool performSelfTest(bool &malfunction);
    /*!
      @brief Peform factory reset
      @details Reset all settings to the factory values
      @return True if successful
      @warning During periodic detection runs, an error is returned
      @warning Measurement duration max 1200 ms
    */
    bool performFactoryReset(const uint32_t duration = scd4x::PERFORM_FACTORY_RESET_DURATION);
    /*!
      @brief Re-initialize the sensor, load settings from EEPROM
      @return True if successful
      @warning During periodic detection runs, an error is returned
      @warning Measurement duration max 20 ms
    */
    bool reInit(const uint32_t duration = scd4x::REINIT_DURATION);
    ///@}

protected:
    bool read_register(const uint16_t reg, uint8_t *rbuf, const uint32_t rlen, const uint32_t duration = 1);
    bool write_register(const uint16_t reg, uint8_t *wbuf, const uint32_t wlen);

    bool start_periodic_measurement(const scd4x::Mode mode = scd4x::Mode::Normal);
    bool stop_periodic_measurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION);
    bool read_data_ready_status();
    bool read_measurement(scd4x::Data &d, const bool all = true);

    virtual bool is_valid_chip();
    bool delay_true(const uint32_t duration);

    M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSCD40, scd4x::Data);

protected:
    std::unique_ptr<m5::container::CircularBuffer<scd4x::Data>> _data{};
    config_t _cfg{};
};

///@cond
namespace scd4x {
namespace command {
// Basic Commands
constexpr uint16_t START_PERIODIC_MEASUREMENT{0x21b1};
constexpr uint16_t READ_MEASUREMENT{0xec05};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT{0x3f86};
// On-chip output signal compensation
constexpr uint16_t SET_TEMPERATURE_OFFSET{0x241d};
constexpr uint16_t GET_TEMPERATURE_OFFSET{0x2318};
constexpr uint16_t SET_SENSOR_ALTITUDE{0x2427};
constexpr uint16_t GET_SENSOR_ALTITUDE{0x2322};
constexpr uint16_t AMBIENT_PRESSURE{0xe000};
// Field calibration
constexpr uint16_t PERFORM_FORCED_CALIBRATION{0x362f};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2416};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2313};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x243a};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x233f};
// Low power
constexpr uint16_t START_LOW_POWER_PERIODIC_MEASUREMENT{0x21ac};
constexpr uint16_t GET_DATA_READY_STATUS{0xe4b8};
// Advanced features
constexpr uint16_t PERSIST_SETTINGS{0x3615};
constexpr uint16_t GET_SERIAL_NUMBER{0x3682};
constexpr uint16_t PERFORM_SELF_TEST{0x3639};
constexpr uint16_t PERFORM_FACTORY_RESET{0x3632};
constexpr uint16_t REINIT{0x3646};
//
constexpr uint16_t GET_SENSOR_VARIANT{0x202f};
}  // namespace command
}  // namespace scd4x
///@endcond

}  // namespace unit
}  // namespace m5
#endif
