/*
 * Copyright (C) 2016,2017,2018 Kees Bakker, SODAQ
 * Copyright (C) 2017 George Psimenos
 * Copyright (C) 2018 Steffen Robertz
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     drivers_sht2x
 * @{
 *
 * @file
 * @brief       Device driver implementation for the SHT2x temperature and
 *              humidity sensor.
 *
 * @author      Kees Bakker <kees@sodaq.com>
 * @author      George Psimenos <gp7g14@soton.ac.uk>
 * @author      Steffen Robertz <steffen.robertz@rwth-aachen.de>
 *
 * @}
 */

#include <math.h>

#include "log.h"
#include "sht2x.h"
#include "sht2x_params.h"
#include "periph/i2c.h"
#include "xtimer.h"

#define ENABLE_DEBUG        0
#include "debug.h"

/**
 * @brief       The number of retries when doing a polled measurement
 */
#define MAX_RETRIES         20

/**
 * @brief       A few helper macros
 */
#define _BUS                (dev->params.i2c_dev)
#define _ADDR               (dev->params.i2c_addr)

typedef enum {
    temp_hold_cmd       = 0xE3,     /**< trigger temp measurement, hold master */
    hum_hold_cmd        = 0xE5,     /**< trigger humidity measurement, hold master */
    write_user_cmd      = 0xE6,     /**< write user register */
    read_user_cmd       = 0xE7,     /**< read user register */
    temp_no_hold_cmd    = 0xF3,     /**< trigger temp measurement, no hold master (poll) */
    hum_no_hold_cmd     = 0xF5,     /**< trigger humidity measurement, no hold master (poll) */
    soft_reset_cmd      = 0xFE,     /**< soft reset */
} cmd_t;

typedef enum {
    SHT2X_MEASURE_TEMP,
    SHT2X_MEASURE_RH,
} measure_type_t;

/**
 * @brief   Register addresses to read SHT2x Identification Code.
 */
static const uint16_t first_mem_addr = 0x0FFA;
static const uint16_t second_mem_addr = 0xC9FC;

static int read_sensor(const sht2x_t* dev, cmd_t command, uint16_t *val);
static int read_sensor_poll(const sht2x_t* dev, cmd_t command, uint16_t *val);
static uint8_t sht2x_checkcrc(uint8_t data[], uint8_t nbrOfBytes, uint8_t checksum);
static void sleep_during_temp_measurement(sht2x_res_t res);
static void sleep_during_hum_measurement(sht2x_res_t resolution);

/*---------------------------------------------------------------------------*
 *                          SHT2x Core API                                   *
 *---------------------------------------------------------------------------*/

int sht2x_init(sht2x_t* dev, const sht2x_params_t* params)
{
    int i2c_result;

    dev->params = *params;
    if (dev->params.resolution != SHT2X_RES_12_14BIT &&
        dev->params.resolution != SHT2X_RES_8_12BIT &&
        dev->params.resolution != SHT2X_RES_10_13BIT &&
        dev->params.resolution != SHT2X_RES_11_11BIT) {
        return SHT2X_ERR_RES;
    }

    i2c_result = sht2x_reset(dev);
    if (i2c_result != SHT2X_OK) {
        return SHT2X_ERR_I2C;
    }
    /* wait 15 ms for device to reset */
    xtimer_msleep(15);

    uint8_t userreg;
    uint8_t userreg2;
    i2c_result = sht2x_read_userreg(dev, &userreg);
    if (i2c_result != SHT2X_OK) {
        return i2c_result;
    }
    DEBUG("[SHT2x] User Register=%02x\n", userreg);

    if ((userreg & SHT2X_USER_RESOLUTION_MASK) != (uint8_t)dev->params.resolution) {
        userreg &= ~SHT2X_USER_RESOLUTION_MASK;
        userreg |= (uint8_t)dev->params.resolution;
        i2c_result = sht2x_write_userreg(dev, userreg);
        if (i2c_result != SHT2X_OK) {
            return i2c_result;
        }
        i2c_result = sht2x_read_userreg(dev, &userreg2);
        if (i2c_result != SHT2X_OK) {
            return i2c_result;
        }
        DEBUG("[SHT2x] New User Register=%02x\n", userreg2);
        if (userreg != userreg2) {
            return SHT2X_ERR_USERREG;
        }
    }

    return SHT2X_OK;
}

int sht2x_reset(sht2x_t* dev)
{
    int i2c_result;
    cmd_t command = soft_reset_cmd;

    /* Acquire exclusive access */
    i2c_acquire(_BUS);

    DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
    i2c_result = i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);

    i2c_release(_BUS);

    if (i2c_result != 0) {
        return SHT2X_ERR_I2C;
    }

    return SHT2X_OK;
}

/*
 * Returns temperature in centi DegC.
 */
int16_t sht2x_read_temperature(const sht2x_t* dev)
{
    uint16_t raw_value;
    int i2c_result;
    if (dev->params.measure_mode == SHT2X_MEASURE_MODE_NO_HOLD) {
        i2c_result = read_sensor_poll(dev, temp_no_hold_cmd, &raw_value);
    } else {
        i2c_result = read_sensor(dev, temp_hold_cmd, &raw_value);
    }
    if (i2c_result != SHT2X_OK) {
        return INT16_MIN;
    }
    return (-46.85 + 175.72 / 65536.0 * raw_value) * 100;
}

/*
 * Returns humidity in centi %RH (i.e. the percentage times 100).
 */
uint16_t sht2x_read_humidity(const sht2x_t *dev)
{
    uint16_t raw_value;
    int i2c_result;
    if (dev->params.measure_mode == SHT2X_MEASURE_MODE_NO_HOLD) {
        i2c_result = read_sensor_poll(dev, hum_no_hold_cmd, &raw_value);
    } else {
        i2c_result = read_sensor(dev, hum_hold_cmd, &raw_value);
    }
    if (i2c_result != SHT2X_OK) {
        return 0;
    }
    return 100 * (-6.0 + 125.0 / 65536.0 * raw_value);
}

static size_t _sht2x_add_ident_byte(uint8_t * buffer, size_t buflen, uint8_t b, size_t ix)
{
    if (ix < buflen) {
        buffer[ix++] = b;
    }
    return ix;
}

int sht2x_read_ident(const sht2x_t *dev, uint8_t * buffer, size_t buflen)
{
    uint8_t data1[8];        /* SNB_3, CRC, SNB_2, CRC, SNB_1, CRC, SNB_0, CRC */
    uint8_t data2[6];        /* SNC_1, SNC_0, CRC, SNA_1, SNA_0, CRC */
    size_t ix;
    int res;

    i2c_acquire(_BUS);
    res = i2c_read_regs(_BUS, _ADDR,
                        first_mem_addr, data1, sizeof(data1), I2C_REG16);
    i2c_release(_BUS);
    if (res < 0) {
        return res;
    }

    DEBUG("[SHT2x] ident (1): %02x %02x %02x %02x\n", data1[0], data1[1], data1[2], data1[3]);
    DEBUG("[SHT2x] ident (1): %02x %02x %02x %02x\n", data1[4], data1[5], data1[6], data1[7]);
    for (size_t ix = 0; ix < sizeof(data1); ix += 2) {
        if (sht2x_checkcrc(&data1[ix], 1, data1[ix + 1]) != 0) {
            DEBUG("[SHT2x] checksum error first (ix=%d)\n", ix);
            return SHT2X_ERR_CRC;
        }
    }

    i2c_acquire(_BUS);
    res = i2c_read_regs(_BUS, _ADDR,
                        second_mem_addr, data2, sizeof(data2), I2C_REG16);
    i2c_release(_BUS);
    if (res < 0) {
        return res;
    }
    DEBUG("[SHT2x] ident (2): %02x %02x %02x\n", data2[0], data2[1], data2[2]);
    DEBUG("[SHT2x] ident (2): %02x %02x %02x\n", data2[3], data2[4], data2[5]);
    for (size_t ix = 0; ix < sizeof(data2); ix += 3) {
        if (sht2x_checkcrc(&data2[ix], 2, data2[ix + 2]) != 0) {
            DEBUG("[SHT2x] checksum error, second (ix=%d)\n", ix);
            return SHT2X_ERR_CRC;
        }
    }

    /*
     * See Sensirion document Electronic_Identification_Code_SHT2x_V1-1_C2
     *
     * first memory address:
     *  SNB_3, CRC, SNB_2, CRC, SNB_1, CRC, SNB_0, CRC,
     * Second memory address:
     *  SNC_1, SNC_0, CRC, SNA_1, SNA_0, CRC
     *
     * To assemble the Identification code:
     *  SNA_1, SNA_0, SNB_3, SNB_2, SNB_1, SNB_0, SNC_1, SNC_0
     */
    if (buffer == NULL) {
        return 0;
    }
    ix = 0;
    ix = _sht2x_add_ident_byte(buffer, buflen, data2[3], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data2[4], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data1[0], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data1[2], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data1[4], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data1[6], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data2[0], ix);
    ix = _sht2x_add_ident_byte(buffer, buflen, data2[1], ix);
    return ix;
}

int sht2x_read_userreg(const sht2x_t *dev, uint8_t * userreg)
{
    cmd_t command = read_user_cmd;

    if (userreg) {
        int i2c_result;
        DEBUG("[SHT2x] read command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
        i2c_acquire(_BUS);
        i2c_result = i2c_read_reg(_BUS, _ADDR, (uint8_t)command, userreg, 0);
        i2c_release(_BUS);
        if (i2c_result != 0) {
            return SHT2X_ERR_I2C_READ;
        }
    } else {
        return SHT2X_ERR_OTHER;
    }

    return SHT2X_OK;
}

int sht2x_write_userreg(const sht2x_t *dev, uint8_t userreg)
{
    cmd_t command = write_user_cmd;
    int i2c_result;
    DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);

    i2c_acquire(_BUS);
    i2c_result = i2c_write_reg(_BUS, _ADDR, (uint8_t)command, userreg, 0);
    i2c_release(_BUS);
    if (i2c_result != 0) {
        return SHT2X_ERR_I2C;
    }

    return SHT2X_OK;
}

/******************************************************************************
 * Local Functions
 ******************************************************************************/

/**
 * @brief       Read a sensor value from the given SHT2X device
 *
 * @param[in]  dev          Device descriptor of SHT2X device to read from
 * @param[in]  cmd          The SHT2x command (hold mode only)
 * @param[out] val          The raw sensor value (only valid if no error)
 *
 * @return                  SHT2X_OK value is returned in @p val
 * @return                  SHT2X_NODEV if sensor communication failed
 * @return                  SHT2X_ERR_OTHER if parameters are invalid
 * @return                  SHT2X_ERR_TIMEDOUT if sensor times out
 * @return                  SHT2X_ERR_CRC if the checksum is wrong
 */
static int read_sensor(const sht2x_t* dev, cmd_t command, uint16_t *val)
{
    uint8_t buffer[3];
    int i2c_result;

    /* Acquire exclusive access */
    i2c_acquire(_BUS);

    DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
    (void)i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);
    i2c_result = i2c_read_bytes(_BUS, _ADDR, buffer, sizeof(buffer), 0);
    i2c_release(_BUS);
    if (i2c_result != 0) {
        DEBUG("[Error] Cannot read SHT2x sensor data.\n");
        return SHT2X_ERR_I2C_READ;
    }

    DEBUG("[SHT2x] read: %02x %02x %02x\n", buffer[0], buffer[1], buffer[2]);
    if (val) {
        *val = (buffer[0] << 8) | buffer[1];
        *val &= ~0x0003;            /* clear two low bits (status bits) */
    }

    if (dev->params.is_crc_enabled) {
        /* byte #3 is the checksum */
        if (sht2x_checkcrc(buffer, 2, buffer[2]) != 0) {
            return SHT2X_ERR_CRC;
        }
    }

    return SHT2X_OK;
}

/**
 * @brief       Read a sensor value from the given SHT2X device, polling mode
 *
 * @param[in]  dev          Device descriptor of SHT2X device to read from
 * @param[in]  cmd          The SHT2x command (hold mode only)
 * @param[out] val          The raw sensor value (only valid if no error)
 *
 * @return                  SHT2X_OK value is returned in @p val
 * @return                  SHT2X_NODEV if sensor communication failed
 * @return                  SHT2X_ERR_OTHER if parameters are invalid
 * @return                  SHT2X_ERR_TIMEDOUT if sensor times out
 * @return                  SHT2X_ERR_CRC if the checksum is wrong
 */
static int read_sensor_poll(const sht2x_t* dev, cmd_t command, uint16_t *val)
{
    uint8_t buffer[3];
    int i2c_result;

    /* Acquire exclusive access */
    i2c_acquire(_BUS);

    DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
    (void)i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);
    if (command == temp_no_hold_cmd) {
        sleep_during_temp_measurement(dev->params.resolution);
    } else {
        sleep_during_hum_measurement(dev->params.resolution);
    }

    uint8_t ix = 0;
    for (; ix < MAX_RETRIES; ix++) {
        i2c_result = i2c_read_bytes(_BUS, _ADDR, buffer, sizeof(buffer), 0);
        if (i2c_result == 0) {
            break;
        }
    }

    i2c_release(_BUS);
    if (i2c_result != 0) {
        DEBUG("[Error] Cannot read SHT2x sensor data.\n");
        return SHT2X_ERR_I2C_READ;
    }

    DEBUG("[SHT2x] read: %02x %02x %02x\n", buffer[0], buffer[1], buffer[2]);
    if (val) {
        *val = (buffer[0] << 8) | buffer[1];
        *val &= ~0x0003;            /* clear two low bits (status bits) */
    }

    /* byte #3 is the checksum */
    if (dev->params.is_crc_enabled) {
        if (sht2x_checkcrc(buffer, 2, buffer[2]) != 0) {
            return SHT2X_ERR_CRC;
        }
    }

    return SHT2X_OK;
}

static const uint16_t POLYNOMIAL = 0x131;       /* P(x)=x^8+x^5+x^4+1 = 100110001 */
/**
 * @brief       Calculate 8-Bit checksum with given polynomial
 */
static uint8_t sht2x_checkcrc(uint8_t data[], uint8_t nbrOfBytes, uint8_t checksum)
{
    uint8_t crc = 0;
    uint8_t byteCtr;
    for (byteCtr = 0; byteCtr < nbrOfBytes; ++byteCtr)
    {
        crc ^= (data[byteCtr]);
        for (uint8_t bit = 8; bit > 0; --bit)
        {
            if ((crc & 0x80) != 0)
                crc = (crc << 1) ^ POLYNOMIAL;
            else
                crc = (crc << 1);
        }
    }
    if (crc != checksum)
        return 1;
    else
        return 0;
}

/**
 * @brief       Initialize the given SHT2X device
 *
 * @param[in]   res         The resolution bits in the User Register
 *
 * @details     Sleep for the typical time it takes to complete the measurement
 *              this depends on the resolution and is taken from the datasheet.
 *              Measurement time differs for temperature and humidity.
 */
static void sleep_during_temp_measurement(sht2x_res_t res)
{
    uint32_t amount_ms = 0;

    switch (res) {
        case SHT2X_RES_12_14BIT:
            amount_ms = 66;
            break;
        case SHT2X_RES_8_12BIT:
            amount_ms = 17;
            break;
        case SHT2X_RES_10_13BIT:
            amount_ms = 33;
            break;
        case SHT2X_RES_11_11BIT:
            amount_ms = 9;
            break;
    }
    xtimer_msleep(amount_ms);
}

static void sleep_during_hum_measurement(sht2x_res_t resolution)
{
    uint32_t amount_ms = 0;

    switch (resolution) {
        case SHT2X_RES_12_14BIT:
            amount_ms = 22;
            break;
        case SHT2X_RES_8_12BIT:
            amount_ms = 3;
            break;
        case SHT2X_RES_10_13BIT:
            amount_ms = 7;
            break;
        case SHT2X_RES_11_11BIT:
            amount_ms = 12;
            break;
    }
    xtimer_msleep(amount_ms);
}
