/*
 * Author: William Penner <william.penner@intel.com>
 * Copyright (c) 2014 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 <string>
#include <stdexcept>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <time.h>

#include "am2315.hpp"

using namespace upm;

char g_name[] = AM2315_NAME;

AM2315::AM2315(int bus, int devAddr) {
    m_temperature = 0;
    m_humidity    = 0;
    m_last_time = 0;

    m_name = g_name;

    m_controlAddr = devAddr;
    m_bus = bus;

    m_base_priority = sched_getscheduler(0);

    if ( !(m_i2ControlCtx = mraa_i2c_init(m_bus)) ) 
      {
        throw std::invalid_argument(std::string(__FUNCTION__) +
                                    ": mraa_i2c_init() failed");
        return;
      }

    mraa_result_t ret = mraa_i2c_address(m_i2ControlCtx, m_controlAddr);
    if (ret != MRAA_SUCCESS) {
        throw std::invalid_argument(std::string(__FUNCTION__) +
                                    ": mraa_i2c_address() failed");
        return;
    }
    m_model = i2cReadReg_16(AM2315_MODEL);
    m_version = i2cReadReg_8(AM2315_VERSION);
    m_id = i2cReadReg_32(AM2315_ID);

    fprintf(stdout,"%s: Model: 0x%04x Version: 0x%02x ID: 0x%08x\n",
            m_name, m_model, m_version, m_id );
}

AM2315::~AM2315() {
    mraa_i2c_stop(m_i2ControlCtx);
}

void
AM2315::update_values(void)
{
    time_t ctime = time(NULL);
    if ((ctime - m_last_time) >= AM2315_SAMPLE) {
        uint32_t uival = i2cReadReg_32(AM2315_HUMIDITY);
        m_humidity = uival >> 16;
        m_temperature = uival & 0xffff;
        m_last_time = ctime;
    }
    else {
        // In case the time is changed - backwards
        if (ctime < m_last_time)
            m_last_time = ctime;
    }
}

float
AM2315::getTemperature(void)
{
    update_values();
    return (float)m_temperature / 10;
}

float
AM2315::getTemperatureF(void)
{
    return getTemperature() * 9 / 5 + 32;
}

float
AM2315::getHumidity(void)
{
    update_values();
    return (float)m_humidity / 10;
}

/*
 * Test function: when reading the AM2315 many times rapidly should
 * result in a temperature increase.  This test will verify that the
 * value is changing from read to read
 */

int
AM2315::testSensor(void)
{
    int i;
    int iError = 0;
    float fTemp, fHum;
    float fTempMax, fTempMin;
    float fHumMax, fHumMin;

    fprintf(stdout, "%s: Executing Sensor Test\n", m_name );

    fHum  = getHumidity();
    fTemp = getTemperature();
    fTempMax = fTempMin = fTemp;
    fHumMax  = fHumMin  = fHum;

    // Then sample the sensor a few times
    for (i=0; i < 10; i++) {
        fHum  = getHumidity();
        fTemp = getTemperature();
        if (fHum  < fHumMin)  fHumMin  = fHum;
        if (fHum  > fHumMax)  fHumMax  = fHum;
        if (fTemp < fTempMin) fTempMin = fTemp;
        if (fTemp > fTempMax) fTempMax = fTemp;
        usleep(50000);
    }

    // Now check the results
    if (fHumMin == fHumMax && fTempMin == fTempMax) {
        fprintf(stdout, "%s:  Humidity/Temp reading was unchanged - warning\n",
                m_name );
        iError++;
    }
    if (iError == 0) {
        fprintf(stdout, "%s:  Device appears functional\n", m_name );
    }

    fprintf(stdout, "%s: Test complete\n", m_name );

    return iError;
}

uint16_t
AM2315::crc16(uint8_t* ptr, uint8_t len)
{
    uint16_t crc = 0xffff;
    uint8_t i;

    while(len--) {
        crc ^= *ptr++;
        for (i=0; i < 8; i++) {
            if (crc & 0x01) {
                crc >>= 1;
                crc ^= 0xA001;
            }
            else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

/*
 * Functions to read and write data to the i2c device in the
 * special format used by the device.  This is using i2c to
 * interface to a controller that the AOSONG AM2315 uses to
 * perform the measurements and manage other registers.
 */
int
AM2315::i2cWriteReg(uint8_t reg, uint8_t* data, uint8_t ilen)
{
    uint8_t tdata[16] = { AM2315_WRITE, reg, ilen };
    mraa_result_t error;

    for (int i=0; i < ilen; i++) {
        tdata[i+3] = data[i];
    }
    uint16_t crc = crc16(tdata, ilen+3);
    // CRC is sent out backwards from other registers (low, high)
    tdata[ilen+3] = crc;
    tdata[ilen+4] = (crc >> 8);

    int iLoops = 5;
    mraa_set_priority(HIGH_PRIORITY);
    do {
        error = mraa_i2c_write(m_i2ControlCtx, tdata, ilen+5);
        usleep(800);
    } while(error != MRAA_SUCCESS && --iLoops);
    mraa_set_priority(m_base_priority);

    if (error != MRAA_SUCCESS) {
        fprintf(stdout, "%s: Error, timeout writing sensor.\n", m_name);
        return -1;
    }
    crc = crc16(tdata,3);
    mraa_i2c_read(m_i2ControlCtx, tdata, 5);
    if ((tdata[0] != AM2315_WRITE) ||
        (tdata[1] != reg)          ||
        (tdata[2] != ilen)         ||
        (tdata[3] != (crc & 0xff)) ||
        (tdata[4] != (crc >> 8))) {
        fprintf(stdout, "%s: CRC error during write verification\n", m_name);
        return -1;
    }
    return 0;
}


// TODO: Need to patch up function to return only the data that
// is needed and not require the various functions that call this
// to send it enough buffer to cover the function

uint8_t
AM2315::i2cReadReg(int reg, uint8_t* data, int ilen)
{
  uint8_t tdata[16] = { AM2315_READ, static_cast<uint8_t>(reg),
                        static_cast<uint8_t>(ilen) };

    mraa_result_t ret;
    int iLoops = 5;
    mraa_set_priority(HIGH_PRIORITY);
    do {
        ret = mraa_i2c_write(m_i2ControlCtx, tdata, 3);
        usleep(800);
    } while(ret != MRAA_SUCCESS && --iLoops);
    if (ret != MRAA_SUCCESS) {
        fprintf(stdout, "%s: Error, timeout reading sensor.\n", m_name);
        mraa_set_priority(m_base_priority);
        return -1;
    }
    usleep(5000);
    mraa_i2c_read(m_i2ControlCtx, tdata, ilen+4);
    mraa_set_priority(m_base_priority);

    uint16_t crc = crc16(tdata, ilen+2);
    if ((tdata[0] != AM2315_READ)  ||
        (tdata[1] != ilen)         ||
        (tdata[ilen+2] != (crc & 0xff)) ||
        (tdata[ilen+3] != (crc >> 8))) {
        fprintf(stdout, "%s: Read crc failed.\n", m_name);
    }
    for (int i=0; i < ilen; i++)
        data[i] = tdata[i+2];

    return 0;
}

/*
 * Functions to set up the reads and writes to simplify the process of
 * formatting data as needed by the microcontroller
 */

int
AM2315::i2cWriteReg_32(int reg, uint32_t ival) {
    uint8_t data[4];
    data[0] = ival >> 24;
    data[1] = ival >> 16;
    data[1] = ival >>  8;
    data[1] = ival & 0xff;
    return i2cWriteReg(reg, data, 4);
}

int
AM2315::i2cWriteReg_16(int reg, uint16_t ival) {
    uint8_t data[2];
    data[0] = ival & 0xff;
    data[1] = ival >> 8;
    return i2cWriteReg(reg, data, 2);
}

int
AM2315::i2cWriteReg_8(int reg, uint8_t ival) {
    uint8_t data[2];
    data[0] = ival & 0xff;
    data[1] = ival >> 8;
    return i2cWriteReg(reg, data, 2);
}

uint32_t
AM2315::i2cReadReg_32 (int reg) {
    uint8_t data[4];
    i2cReadReg(reg, data, 4);
    return ((((((uint32_t)data[0] << 8) | data[1]) << 8) |
            data[2]) << 8) | data[3];
}

uint16_t
AM2315::i2cReadReg_16 (int reg) {
    uint8_t data[2];
    i2cReadReg(reg, data, 2);
    return ((int16_t)data[0] << 8) | (uint16_t)data[1];
}

uint8_t
AM2315::i2cReadReg_8 (int reg) {
    uint8_t data[1];
    i2cReadReg(reg, data, 1);
    return data[0];
}

