/*
 * Author: Henry Bruce <henry.bruce@intel.com>
 * Copyright (c) 2016 Intel Corporation.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include "ms5611.hpp"


// Used to prevent silent failure in mraa_i2c_write()
// Value come from I2C_SMBUS_I2C_BLOCK_MAX in i2c driver
#define MAX_I2C_WRITE_SIZE 32

#define MS5611_CMD_ADC_READ           (0x00)
#define MS5611_CMD_RESET              (0x1E)
#define MS5611_CMD_CONV_D1            (0x40)
#define MS5611_CMD_CONV_D2            (0x50)
#define MS5611_CMD_READ_PROM          (0xA0)

#define MS5611_OSR_ULTRA_LOW_POWER    0x00
#define MS5611_OSR_LOW_POWER          0x02
#define MS5611_OSR_STANDARD           0x04
#define MS5611_OSR_HIGH_RES           0x06
#define MS5611_OSR_ULTRA_HIGH_RES     0x08

#define MS5611_PROM_SIZE              8


using namespace upm;


MS5611::MS5611(int i2cBus, int address)
{
    status = mraa::ERROR_INVALID_RESOURCE;
    i2c = new mraa::I2c(2);
    this->address = address;
    i2c->address(address);
    prom = new uint16_t[MS5611_PROM_SIZE];
    status = i2c->writeByte(MS5611_CMD_RESET);
    if (status != mraa::SUCCESS)
        UPM_THROW("Reset failed.");
    delayms(5);
    for (int i = 0; i < MS5611_PROM_SIZE; ++i) {
        uint8_t buf[2];
        int bytesRead = i2c->readBytesReg(MS5611_CMD_READ_PROM + 2*i, buf, 2);
        if (bytesRead != 2)
           UPM_THROW("PROM address failed.");
        prom[i] = buf[0] << 8;
        prom[i] |= buf[1];
        // printf("Read PROM entry %d = %04x\n", i, prom[i]);
    }

    // printf("CRC = %X\n", promCrc4());
    if (promCrc4() != (prom[7] & 0x000F))
        UPM_THROW("PROM checksum error.");
    setOverSampling(ULTRA_HIGH_RES);
}


MS5611::~MS5611()
{
    delete[] prom;
    delete i2c;
}


int MS5611::promCrc4() {
    unsigned int n_rem;
    unsigned int crc_read;
    unsigned char n_bit;
    n_rem = 0x00;
    crc_read = prom[7];
    prom[7]=(0xFF00 & (prom[7]));
    for (int cnt = 0; cnt < 16; cnt++) {
            if (cnt%2 == 1) {
                n_rem ^= (unsigned short) ((prom[cnt>>1]) & 0x00FF);
            } else {
                n_rem ^= (unsigned short) (prom[cnt>>1]>>8);
            }
            for (n_bit = 8; n_bit > 0; n_bit--) {
                if (n_rem & (0x8000)) {
                    n_rem = (n_rem << 1) ^ 0x3000;
                } else {
                    n_rem = (n_rem << 1);
                }
            }
        }
    n_rem= (0x000F & (n_rem >> 12));
    prom[7]=crc_read;
    return (n_rem ^ 0x0);
}


// Set oversampling value
void MS5611::setOverSampling(OsrMode osrMode)
{
    switch (osrMode)
    {
    case ULTRA_LOW_POWER:
        osr = MS5611_OSR_ULTRA_LOW_POWER;
        samplePeriod = 1;
        break;
    case LOW_POWER:
        osr = MS5611_OSR_LOW_POWER;
        samplePeriod = 2;
        break;
    case STANDARD:
        osr = MS5611_OSR_STANDARD;
        samplePeriod = 3;
        break;
    case HIGH_RES:
        osr = MS5611_OSR_HIGH_RES;
        samplePeriod = 5;
        break;
    case ULTRA_HIGH_RES:
        osr = MS5611_OSR_ULTRA_HIGH_RES;
        samplePeriod = 10;
        break;
     }
}


void MS5611::delayms(int millisecs)
{
   struct timespec sleepTime;
   sleepTime.tv_sec = millisecs / 1000;
   sleepTime.tv_nsec = (millisecs % 1000) * 1000000;
   while ((nanosleep( &sleepTime, &sleepTime ) != 0 ) && ( errno == EINTR ) );
}

uint32_t MS5611::readADC(int adcReg)
{
    uint32_t value;
    uint8_t buf[3];
    i2c->address(address);
    status = i2c->writeByte(adcReg + osr);
    if (status != mraa::SUCCESS)
       UPM_THROW("Convert D2 failed");
    delayms(100);
    int bytesRead = i2c->readBytesReg(MS5611_CMD_ADC_READ, buf, 3);
    if (bytesRead != 3)
       UPM_THROW("ADC read failed");
    // printf("%02X%02X%02X\n", buf[0], buf[1], buf[2]);
    value = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2];
    return value;
}

uint32_t MS5611::readRawTemperature()
{
    return readADC(MS5611_CMD_CONV_D2);
}

uint32_t MS5611::readRawPressure()
{
    return readADC(MS5611_CMD_CONV_D1);
}

int MS5611::getTemperatureCelsius()
{
    uint32_t D2 = readRawTemperature();
    int64_t dT = D2 - ((uint64_t)prom[5] << 8);
    int32_t temp = 2000 + ((int64_t)dT * (int64_t)prom[6]) / (int64_t)(1 << 23);
    return (temp + 50) / 100;
}


int MS5611::getPressurePa()
{
    int32_t rawTemp = readRawTemperature();
    int32_t rawPressure = readRawPressure();
    int64_t dT = rawTemp - ((uint64_t)prom[5] << 8);
    int64_t offset  = ((uint32_t)prom[2] << 16) + ((dT * (prom[4]) >> 7));     //was  OFF  = (C[2] << 17) + dT * C[4] / (1 << 6);
    int64_t scaler = ((uint32_t)prom[1] << 15) + ((dT * (prom[3]) >> 8));     //was  SENS = (C[1] << 16) + dT * C[3] / (1 << 7);
    int32_t temp = 2000 + (int64_t)dT * (int64_t)prom[6] / (int64_t)(1 << 23);

    if(temp < 2000) {
        float T1 = (temp - 2000) * (temp - 2000);
        int64_t offsetDelta  = (5 * T1) / 2;
        int64_t scalerDelta = (5 * T1) / 4;

        if(temp < -1500) {
            T1 = (temp + 1500) * (temp + 1500);
            offsetDelta  += 7 * T1;
            scalerDelta += 11 * T1 / 2;
        }
        offset -= offsetDelta;
        scaler -= scalerDelta;
    }
    int pressure = ((((int64_t)rawPressure * scaler ) >> 21) - offset) / (double) (1 << 15);
    return pressure;
}

