/**
 * Copyright (c) 2021 Bosch Sensortec GmbH. All rights reserved.
 *
 * BSD-3-Clause
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @file	bsec2.cpp
 * @date	18 July 2024
 * @version	2.1.5
 *
 */

#include "bsec2.h"

static uint8_t workBuffer[BSEC_MAX_WORKBUFFER_SIZE];

/**
 * @brief Constructor of Bsec2 class
 */
Bsec2::Bsec2(void)
{
    ovfCounter = 0;
    lastMillis = 0;
    status = BSEC_OK;
	extTempOffset = 0.0f;
    opMode = BME68X_SLEEP_MODE;
    newDataCallback = nullptr;
    bsecInstance = nullptr;

    memset(&version, 0, sizeof(version));
    memset(&bmeConf, 0, sizeof(bmeConf));
    memset(&outputs, 0, sizeof(outputs));
}

/**
 * @brief Function to initialize the sensor based on custom callbacks
 */
bool Bsec2::begin(bme68xIntf intf, bme68x_read_fptr_t read, bme68x_write_fptr_t write,
        bme68x_delay_us_fptr_t idleTask, void *intfPtr, unsigned long (*millis)())
{
    bsecMillis = millis;
    sensor.begin(intf, read, write, idleTask, intfPtr);

    if (sensor.checkStatus() == BME68X_ERROR)
        return false;

    return beginCommon();
}

#ifdef ARDUINO
/**
 * @brief Function to initialize the sensor based on custom callbacks
 */
bool Bsec2::begin(bme68xIntf intf, bme68x_read_fptr_t read, bme68x_write_fptr_t write,
        bme68x_delay_us_fptr_t idleTask, void *intfPtr)
{
    bsecMillis = millis;
    sensor.begin(intf, read, write, idleTask, intfPtr);

    if (sensor.checkStatus() == BME68X_ERROR)
        return false;

    return beginCommon();
}

/**
 * @brief Function to initialize the sensor based on the Wire library
 */ 
bool Bsec2::begin(uint8_t i2cAddr, TwoWire &i2c, bme68x_delay_us_fptr_t idleTask)
{
    bsecMillis = millis;
    sensor.begin(i2cAddr, i2c, idleTask);

    if (sensor.checkStatus() == BME68X_ERROR)
        return false;

    return beginCommon();
}

/**
 * @brief Function to initialize the sensor based on the SPI library
 */
bool Bsec2::begin(uint8_t chipSelect, SPIClass &spi, bme68x_delay_us_fptr_t idleTask)
{
    bsecMillis = millis;
    sensor.begin(chipSelect, spi, idleTask);

    if (sensor.checkStatus() == BME68X_ERROR)
        return false;

    return beginCommon();
}
#endif

/**
 * @brief Function to request/subscribe for desired virtual outputs with the supported sample rates
 */
bool Bsec2::updateSubscription(bsecSensor sensorList[], uint8_t nSensors, float sampleRate)
{
    bsec_sensor_configuration_t virtualSensors[BSEC_NUMBER_OUTPUTS], sensorSettings[BSEC_MAX_PHYSICAL_SENSOR];
    uint8_t nSensorSettings = BSEC_MAX_PHYSICAL_SENSOR;

    for (uint8_t i = 0; i < nSensors; i++)
    {
        virtualSensors[i].sensor_id = sensorList[i];
        virtualSensors[i].sample_rate = sampleRate;
    }

    /* Subscribe to library virtual sensors outputs */
    status = bsec_update_subscription_m(bsecInstance, virtualSensors, nSensors, sensorSettings, &nSensorSettings);
    if (status != BSEC_OK)
        return false;

    return true;
}

/**
 * @brief Callback from the user to read data from the BME68X using parallel mode/forced mode, process and store outputs
 */
bool Bsec2::run(void)
{
    uint8_t nFieldsLeft = 0;
    bme68xData data;
    int64_t currTimeNs = getTimeMs() * INT64_C(1000000);
    opMode = bmeConf.op_mode;

    if (currTimeNs >= bmeConf.next_call)
    {
        /* Provides the information about the current sensor configuration that is
           necessary to fulfill the input requirements, eg: operation mode, timestamp
           at which the sensor data shall be fetched etc */
        status = bsec_sensor_control_m(bsecInstance ,currTimeNs, &bmeConf);
        if (status != BSEC_OK)
            return false;

        switch (bmeConf.op_mode)
        {
        case BME68X_FORCED_MODE:
            setBme68xConfigForced();
            break;
        case BME68X_PARALLEL_MODE:
            if (opMode != bmeConf.op_mode)
            {
                setBme68xConfigParallel();
            }
            break;

        case BME68X_SLEEP_MODE:
            if (opMode != bmeConf.op_mode)
            {
                sensor.setOpMode(BME68X_SLEEP_MODE);
                opMode = BME68X_SLEEP_MODE;
            }
            break;
        }

        if (sensor.checkStatus() == BME68X_ERROR)
            return false;

        if (bmeConf.trigger_measurement && bmeConf.op_mode != BME68X_SLEEP_MODE)
        {
            if (sensor.fetchData())
            {
                do
                {
                    nFieldsLeft = sensor.getData(data);
                    /* check for valid gas data */
                    if (data.status & BME68X_GASM_VALID_MSK)
                    {
						/* Convert sensor raw pressure unit from pascal to hecto pascal */
						data.pressure *= 0.01f;

                        if (!processData(currTimeNs, data))
                        {
                            return false;
                        }
                    }
                } while (nFieldsLeft);
            }

        }

    }
    return true;
}

/**
 * @brief Function to get the state of the algorithm to save to non-volatile memory
 */
bool Bsec2::getState(uint8_t *state)
{
    uint32_t n_serialized_state = BSEC_MAX_STATE_BLOB_SIZE;

    status = bsec_get_state_m(bsecInstance, 0, state, BSEC_MAX_STATE_BLOB_SIZE, workBuffer, BSEC_MAX_WORKBUFFER_SIZE,
            &n_serialized_state);
    if (status != BSEC_OK)
        return false;
    return true;
}

/**
 * @brief Function to set the state of the algorithm from non-volatile memory
 */
bool Bsec2::setState(uint8_t *state)
{
    status = bsec_set_state_m(bsecInstance, state, BSEC_MAX_STATE_BLOB_SIZE, workBuffer, BSEC_MAX_WORKBUFFER_SIZE);
    if (status != BSEC_OK)
        return false;

    memset(&bmeConf, 0, sizeof(bmeConf));

    return true;
}

/**
 * @brief Function to retrieve the current library configuration
 */
bool Bsec2::getConfig(uint8_t *config)
{
    uint32_t n_serialized_settings = 0;
    
    status = bsec_get_configuration_m(bsecInstance, 0, config, BSEC_MAX_PROPERTY_BLOB_SIZE, workBuffer, BSEC_MAX_WORKBUFFER_SIZE, &n_serialized_settings);
    if (status != BSEC_OK)
        return false;

    return true;
}

/**
 * @brief Function to set the configuration of the algorithm from memory
 */
bool Bsec2::setConfig(const uint8_t *config)
{
    status = bsec_set_configuration_m(bsecInstance, config, BSEC_MAX_PROPERTY_BLOB_SIZE, workBuffer, BSEC_MAX_WORKBUFFER_SIZE);
    if (status != BSEC_OK)
        return false;

    memset(&bmeConf, 0, sizeof(bmeConf));

    return true;
}

/**
 * @brief Function to calculate an int64_t timestamp in milliseconds
 */
int64_t Bsec2::getTimeMs(void)
{
    int64_t timeMs = bsecMillis();

    if (lastMillis > timeMs) /* An overflow occurred */
    { 
        ovfCounter++;
    }

    lastMillis = timeMs;

    return timeMs + (ovfCounter * INT64_C(0xFFFFFFFF));
}

/**
 * @brief Function to assign the memory block to the bsec instance
 */
void Bsec2::allocateMemory(uint8_t (&memBlock)[BSEC_INSTANCE_SIZE])
{
    /* allocating memory for the bsec instance */
    bsecInstance = memBlock;
}

/**
 * @brief Function to de-allocate the dynamically allocated memory
 */
void Bsec2::clearMemory(void)
{
    delete[] bsecInstance;
}

/* Private functions */

/**
 * @brief Reads data from the BME68X sensor and process it
 */
bool Bsec2::processData(int64_t currTimeNs, const bme68xData &data)
{
    bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; /* Temp, Pres, Hum & Gas */
    uint8_t nInputs = 0;
    /* Checks all the required sensor inputs, required for the BSEC library for the requested outputs */
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_HEATSOURCE))
    {
        inputs[nInputs].sensor_id = BSEC_INPUT_HEATSOURCE;
        inputs[nInputs].signal = extTempOffset;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_TEMPERATURE))
    {
#ifdef BME68X_USE_FPU
        inputs[nInputs].signal = data.temperature;
#else
        inputs[nInputs].signal = data.temperature / 100.0f;
#endif
        inputs[nInputs].sensor_id = BSEC_INPUT_TEMPERATURE;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_HUMIDITY))
    {
#ifdef BME68X_USE_FPU
        inputs[nInputs].signal = data.humidity;
#else
        inputs[nInputs].signal = data.humidity / 1000.0f;
#endif
        inputs[nInputs].sensor_id = BSEC_INPUT_HUMIDITY;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_PRESSURE))
    {
        inputs[nInputs].sensor_id = BSEC_INPUT_PRESSURE;
        inputs[nInputs].signal = data.pressure;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_GASRESISTOR) &&
            (data.status & BME68X_GASM_VALID_MSK))
    {
        inputs[nInputs].sensor_id = BSEC_INPUT_GASRESISTOR;
        inputs[nInputs].signal = data.gas_resistance;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }
    if (BSEC_CHECK_INPUT(bmeConf.process_data, BSEC_INPUT_PROFILE_PART) &&
            (data.status & BME68X_GASM_VALID_MSK))
    {
        inputs[nInputs].sensor_id = BSEC_INPUT_PROFILE_PART;
        inputs[nInputs].signal = (opMode == BME68X_FORCED_MODE) ? 0 : data.gas_index;
        inputs[nInputs].time_stamp = currTimeNs;
        nInputs++;
    }

    if (nInputs > 0)
    {

        outputs.nOutputs = BSEC_NUMBER_OUTPUTS;
        memset(outputs.output, 0, sizeof(outputs.output));

        /* Processing of the input signals and returning of output samples is performed by bsec_do_steps() */
        status = bsec_do_steps_m(bsecInstance, inputs, nInputs, outputs.output, &outputs.nOutputs);

        if (status != BSEC_OK)
            return false;

        if(newDataCallback)
            newDataCallback(data, outputs, *this);
    }
    return true;
}

/**
 * @brief Common code for the begin function
 */
bool Bsec2::beginCommon()
{
    if (!bsecInstance)
    {
        /* allocate memory for the instance if not allocated */
        bsecInstance = new uint8_t[bsec_get_instance_size_m()];
    }

    if (BSEC_INSTANCE_SIZE < bsec_get_instance_size_m())
    {
        status = BSEC_E_INSUFFICIENT_INSTANCE_SIZE;
        return false;
    }
    status = bsec_init_m(bsecInstance);
    if (status != BSEC_OK)
        return false;

    status = bsec_get_version_m(bsecInstance, &version);
    if (status != BSEC_OK)
        return false;

    memset(&bmeConf, 0, sizeof(bmeConf));
    memset(&outputs, 0, sizeof(outputs));

    return true;
}

/**
 * @brief Set the BME68X sensor configuration to forced mode
 */
void Bsec2::setBme68xConfigForced(void)
{
    /* Set the filter, odr, temperature, pressure and humidity settings */
    sensor.setTPH(bmeConf.temperature_oversampling, bmeConf.pressure_oversampling, bmeConf.humidity_oversampling);

    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    sensor.setHeaterProf(bmeConf.heater_temperature, bmeConf.heater_duration);

    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    sensor.setOpMode(BME68X_FORCED_MODE);
    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    opMode = BME68X_FORCED_MODE;
}

/**
 * @brief Set the BME68X sensor configuration to parallel mode
 */
void Bsec2::setBme68xConfigParallel(void)
{
    uint16_t sharedHeaterDur = 0;

    /* Set the filter, odr, temperature, pressure and humidity settings */
    sensor.setTPH(bmeConf.temperature_oversampling, bmeConf.pressure_oversampling, bmeConf.humidity_oversampling);

    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    sharedHeaterDur = BSEC_TOTAL_HEAT_DUR - (sensor.getMeasDur(BME68X_PARALLEL_MODE) / INT64_C(1000));

    sensor.setHeaterProf(bmeConf.heater_temperature_profile, bmeConf.heater_duration_profile, sharedHeaterDur,
            bmeConf.heater_profile_len);

    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    sensor.setOpMode(BME68X_PARALLEL_MODE);

    if (sensor.checkStatus() == BME68X_ERROR)
        return;

    opMode = BME68X_PARALLEL_MODE;
}
