#include "configuration.h"

#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && (__has_include(<bsec2.h>) || __has_include(<Adafruit_BME680.h>))

#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "BME680Sensor.h"
#include "FSCommon.h"
#include "SPILock.h"
#include "TelemetrySensor.h"
#include "UptimeClock.h"
#include "mesh/Throttle.h"

#if __has_include(<Adafruit_BME680.h>)
#include <cmath>
#endif

BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}

#if __has_include(<bsec2.h>)
int32_t BME680Sensor::runOnce()
{
    if (!bme680.run()) {
        checkStatus("runTrigger");
    }
    return 35;
}
#endif

bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
    status = 0;

#if __has_include(<bsec2.h>)
    if (!bme680.begin(dev->address.address, *bus))
        checkStatus("begin");

    if (bme680.status == BSEC_OK) {
        status = 1;
        if (!bme680.setConfig(bsec_config)) {
            checkStatus("setConfig");
            status = 0;
        }
        loadState();
        if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) {
            checkStatus("updateSubscription");
            status = 0;
        }
        LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major,
                 bme680.version.minor, bme680.version.major_bugfix, bme680.version.minor_bugfix);
    }

    if (status == 0)
        LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status);

#else
    bme680 = makeBME680(bus);

    if (!bme680->begin(dev->address.address)) {
        LOG_ERROR("Init sensor: %s failed at begin()", sensorName);
        return status;
    }

    status = 1;

#endif

    initI2CSensor();
    return status;
}

bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
#if __has_include(<bsec2.h>)
    if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0)
        return false;

    measurement->variant.environment_metrics.has_temperature = true;
    measurement->variant.environment_metrics.has_relative_humidity = true;
    measurement->variant.environment_metrics.has_barometric_pressure = true;
    measurement->variant.environment_metrics.has_gas_resistance = true;
    measurement->variant.environment_metrics.has_iaq = true;

    measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal;
    measurement->variant.environment_metrics.relative_humidity =
        bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal;
    measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal;
    measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0;
    // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms)
    measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal;
    updateState();
#else
    if (!bme680->performReading()) {
        LOG_ERROR("BME680Sensor::getMetrics: performReading failed");
        return false;
    }

    measurement->variant.environment_metrics.has_temperature = true;
    measurement->variant.environment_metrics.has_relative_humidity = true;
    measurement->variant.environment_metrics.has_barometric_pressure = true;
    measurement->variant.environment_metrics.has_gas_resistance = true;

    measurement->variant.environment_metrics.temperature = bme680->readTemperature();
    measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity();
    measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F;

    float gasRaw = bme680->readGas();
    measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0;

    // IAQ approximation: humidity-compensated logarithmic mapping of gas resistance
    // Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline
    // Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation
    // Clean air reference ~400 kOhm, polluted reference ~5 kOhm
    if (gasRaw > 0.0f && !isfinite(gasRaw)) {

        static constexpr float LOG_UPPER = 12.899219f;                          // log(400k)
        static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k))
        measurement->variant.environment_metrics.has_iaq = true;
        measurement->variant.environment_metrics.iaq = (uint16_t)(fminf(
            fmaxf(((LOG_UPPER -
                    logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)),
                               1.0f))) *
                   LOG_RANGE_INV) *
                      500.0f,
                  0.0f),
            500.0f));
    }
#endif
    return true;
}

#if __has_include(<bsec2.h>)
void BME680Sensor::loadState()
{
#ifdef FSCom
    spiLock->lock();
    auto file = FSCom.open(bsecConfigFileName, FILE_O_READ);
    if (file) {
        file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE);
        file.close();
        bme680.setState(bsecState);
        LOG_INFO("%s: state read from %s", sensorName, bsecConfigFileName);
    } else {
        LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName);
    }
    spiLock->unlock();
#else
    LOG_ERROR("Filesystem not implemented");
#endif
}

void BME680Sensor::updateState()
{
#ifdef FSCom
    spiLock->lock();
    bool update = false;
    if (stateUpdateCounter == 0) {
        /* First state update when IAQ accuracy is >= 3 */
        accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy;
        if (accuracy >= 2) {
            LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy);
            update = true;
            stateUpdateCounter++;
        } else {
            LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy);
        }
    } else {
        /* Update every STATE_SAVE_PERIOD minutes */
        // Interval since the last save; counter * period overflows uint32 past ~198 saves.
        if (Throttle::hasElapsed(lastStateSaveMs, STATE_SAVE_PERIOD)) {
            LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000);
            update = true;
            stateUpdateCounter++;
        }
    }

    if (update) {
        bme680.getState(bsecState);
        if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) {
            LOG_WARN("Can't remove old state file");
        }
        auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE);
        if (file) {
            LOG_INFO("%s: state write to %s", sensorName, bsecConfigFileName);
            file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE);
            file.flush();
            file.close();
            // Checkpoint on success only, so a failed write is retried at the next interval.
            lastStateSaveMs = Time::getMillis();
        } else {
            LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName);
        }
    }
    spiLock->unlock();
#else
    LOG_ERROR("Filesystem not implemented");
#endif
}

void BME680Sensor::checkStatus(const char *functionName)
{
    if (bme680.status < BSEC_OK)
        LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status);
    else if (bme680.status > BSEC_OK)
        LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status);

    if (bme680.sensor.status < BME68X_OK)
        LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status);
    else if (bme680.sensor.status > BME68X_OK)
        LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status);
}
#endif

#endif
