/*
 * Author: Jon Trulson <jtrulson@ics.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 <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>

#include <upm_utilities.h>

#include "ecezo.h"

// "Typical" command completion delay in ms
#define CMD_DELAY (350)

// uncomment for dubugging
//#define ECEZO_DEBUG (1)

// I2C read helper
static int readBytes(const ecezo_context dev, uint8_t *buffer, int len)
{
    assert(dev != NULL);
    assert(dev->i2c != NULL);

    bool done = false;
    int rv;
    int retries = 10;

    while (!done && (retries-- > 0))
    {
        if ((rv = mraa_i2c_read(dev->i2c, buffer, len)) < 0)
        {
            printf("%s: mraa_i2c_read(code) failed.\n", __FUNCTION__);
            return rv;
        }

#if defined(ECEZO_DEBUG)
        printf("CODE: %02x\n", buffer[0]);
#endif

        if (buffer[0] == 0xff || buffer[0] == 0x02)
        {
            // no data available, or error
            return -1;
        }
        else if (buffer[0] == 0x01)
        {
            // data is ready
            done = true;

            // now we need to move the data one byte down so the rest
            // of this driver can work as-is.
            memmove(buffer, (buffer + 1), len - 1);
        }
        else
        {
            // buffer[0] 0xfe - data is pending. wait and loop again.
            upm_delay_ms(CMD_DELAY);
        }
    }

    if (retries <= 0)
    {
        printf("%s: timed out waiting for correct response.\n", __FUNCTION__);
        return -1;
    }

#if defined(ECEZO_DEBUG)
    printf("%s: Got %d bytes\n", __FUNCTION__, rv);

    for (int i=0; i<rv; i++)
    {
        printf("%02x (%c) ", buffer[i],
               isprint(buffer[i]) ? buffer[i] : '@');
    }
    printf("\n");
#endif // ECEZO_DEBUG

    return rv;
}

static upm_result_t generic_init(const ecezo_context dev)
{
    assert(dev != NULL);

    // do some generic initialization
    bool error = false;

    // turn off response (*OK) codes (UART only)
    if (dev->uart)
    {
        if (ecezo_send_command(dev, "Response,0", NULL, 0) < 0)
            error = true;
    }

    // turn off continuous sampling
    if (ecezo_set_continuous(dev, false))
        error = true;

    // make sure all parameters are enabled
    if (ecezo_send_command(dev, "O,EC,1", NULL, 0) < 0)
        error = true;
    if (ecezo_send_command(dev, "O,TDS,1", NULL, 0) < 0)
        error = true;
    if (ecezo_send_command(dev, "O,S,1", NULL, 0) < 0)
        error = true;
    if (ecezo_send_command(dev, "O,SG,1", NULL, 0) < 0)
        error = true;

    if (error)
        return UPM_ERROR_OPERATION_FAILED;
    else
        return UPM_SUCCESS;
}

static upm_result_t decode_report(const ecezo_context dev, char *data)
{
    assert(dev != NULL);

    char *startptr = data;
    char *endptr = NULL;
    float val;

    // the format of the data string should be: ec,tds,s,sg

    // ec
    val = strtof(startptr, &endptr);

    if (startptr == endptr)
    {
        // error
        return UPM_ERROR_OPERATION_FAILED;
    }

    dev->ec = val;
    startptr = endptr + 1;

    // tds
    val = strtof(startptr, &endptr);

    // error
    if (startptr == endptr)
        return UPM_ERROR_OPERATION_FAILED;

    dev->tds = val;
    startptr = endptr + 1;

    // salinity
    val = strtof(startptr, &endptr);

    // error
    if (startptr == endptr)
        return UPM_ERROR_OPERATION_FAILED;

    dev->salinity = val;
    startptr = endptr + 1;

    // sg
    val = strtof(startptr, &endptr);

    if (startptr == endptr)
        return UPM_ERROR_OPERATION_FAILED;

    dev->sg = val;

    return UPM_SUCCESS;
}

static bool ecezo_data_available(const ecezo_context dev, unsigned int millis)
{
    assert(dev != NULL);

    // i2c, we don't support this
    if (dev->i2c)
    {
        return false;
    }

    // uart
    if (mraa_uart_data_available(dev->uart, millis))
        return true;
    else
        return false;
}

// uart init
ecezo_context ecezo_uart_init(unsigned int uart, unsigned int baudrate)
{
    // make sure MRAA is initialized
    int mraa_rv;
    if ((mraa_rv = mraa_init()) != MRAA_SUCCESS)
    {
        printf("%s: mraa_init() failed (%d).\n", __FUNCTION__, mraa_rv);
        return NULL;
    }

    ecezo_context dev =
        (ecezo_context)malloc(sizeof(struct _ecezo_context));

    if (!dev)
        return NULL;

    // zero out context
    memset((void *)dev, 0, sizeof(struct _ecezo_context));

    // initialize the MRAA contexts

    // uart, default should be 8N1
    if (!(dev->uart = mraa_uart_init(uart)))
    {
        printf("%s: mraa_uart_init() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    if (mraa_uart_set_baudrate(dev->uart, baudrate))
    {
        printf("%s: mraa_uart_set_baudrate() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    mraa_uart_set_flowcontrol(dev->uart, false, false);

    if (generic_init(dev))
    {
        printf("%s: generic_init() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    return dev;
}

// i2c ublox init
ecezo_context ecezo_i2c_init(unsigned int bus, uint8_t addr)
{
    // make sure MRAA is initialized
    int mraa_rv;
    if ((mraa_rv = mraa_init()) != MRAA_SUCCESS)
    {
        printf("%s: mraa_init() failed (%d).\n", __FUNCTION__, mraa_rv);
        return NULL;
    }

    ecezo_context dev =
        (ecezo_context)malloc(sizeof(struct _ecezo_context));

    if (!dev)
        return NULL;

    // zero out context
    memset((void *)dev, 0, sizeof(struct _ecezo_context));

    // initialize the MRAA contexts

    if (!(dev->i2c = mraa_i2c_init(bus)))
    {
        printf("%s: mraa_i2c_init() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    if (mraa_i2c_address(dev->i2c, addr))
    {
        printf("%s: mraa_i2c_address() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    if (generic_init(dev))
    {
        printf("%s: generic_init() failed.\n", __FUNCTION__);
        ecezo_close(dev);
        return NULL;
    }

    return dev;
}

void ecezo_close(ecezo_context dev)
{
    assert(dev != NULL);

    if (dev->uart)
        mraa_uart_stop(dev->uart);
    if (dev->i2c)
        mraa_i2c_stop(dev->i2c);

    free(dev);
}

upm_result_t ecezo_set_continuous(const ecezo_context dev, bool enable)
{
    int rv;

    if (enable)
        rv = ecezo_send_command(dev, "C,1", NULL, 0);
    else
        rv = ecezo_send_command(dev, "C,0", NULL, 0);

    return ((rv < 0) ? UPM_ERROR_OPERATION_FAILED : UPM_SUCCESS);
}

upm_result_t ecezo_set_temperature(const ecezo_context dev, float temp)
{
    char buffer[ECEZO_MAX_BUFFER_LEN];

    snprintf(buffer, ECEZO_MAX_BUFFER_LEN, "T,%f", temp);

    int rv = ecezo_send_command(dev, buffer, NULL, 0);

    return ((rv < 0) ? UPM_ERROR_OPERATION_FAILED : UPM_SUCCESS);
}

upm_result_t ecezo_set_led(const ecezo_context dev, bool enable)
{
    int rv;

    if (enable)
        rv = ecezo_send_command(dev, "L,1", NULL, 0);
    else
        rv = ecezo_send_command(dev, "L,0", NULL, 0);

    return ((rv < 0) ? UPM_ERROR_OPERATION_FAILED : UPM_SUCCESS);
}

upm_result_t ecezo_set_k_value(const ecezo_context dev, float k)
{
    char buffer[ECEZO_MAX_BUFFER_LEN];

    // the K value must be between 0.1 and 10.0
    if (k < 0.1 || k > 10.0)
    {
        printf("%s: K value must be between 0.1 and 10.0\n", __FUNCTION__);
        return UPM_ERROR_OUT_OF_RANGE;
    }

    snprintf(buffer, ECEZO_MAX_BUFFER_LEN, "K,%f", k);

    int rv = ecezo_send_command(dev, buffer, NULL, 0);

    return ((rv < 0) ? UPM_ERROR_OPERATION_FAILED : UPM_SUCCESS);
}

upm_result_t ecezo_set_sleep(const ecezo_context dev, bool enable)
{
    int rv = 0;

    if (enable)
        rv =  ecezo_send_command(dev, "SLEEP", NULL, 0);
    else
    {
        // "WAKE" isn't a real command, but should wake the device up.
        // We ignore the return value, as it will likely be an error
        // anyway.
        ecezo_send_command(dev, "WAKE", NULL, 0);
    }

    return ((rv < 0) ? UPM_ERROR_OPERATION_FAILED : UPM_SUCCESS);
}

int ecezo_read(const ecezo_context dev, char *buffer, size_t len)
{
    assert(dev != NULL);

    upm_delay_ms(CMD_DELAY); // delay CMD_DELAY ms to make sure cmd completed

    // i2c
    if (dev->i2c)
    {
        return readBytes(dev, (uint8_t *)buffer, len);
    }
    else
    {
        // UART
        int bytesRead = 0;

        while(bytesRead < len)
        {
            // we read one byte at a time, exiting when either len is
            // reached, or a '\r' is found indicating the end of a
            // sentence. Most commands (except 'R') require a minimum
            // of 300ms to execute, so we wait up to CMD_DELAY ms after all
            // data (if any) is read.
            if (ecezo_data_available(dev, CMD_DELAY))
            {
                int br = mraa_uart_read(dev->uart, &buffer[bytesRead], 1);

                if (br <= 0)
                    return br;

                if (buffer[bytesRead] == '\r')
                {
                    // if we found a CR, replace it with a 0 byte
                    buffer[bytesRead++] = 0;
                    return bytesRead;
                }
                bytesRead++;
            }
            else
            {
                // timed out - ok with responses disabled
                return 0;
            }
        }
    }

    // anything else is an error
    return -1;
}

upm_result_t ecezo_write(const ecezo_context dev, char *buffer, size_t len)
{
    assert(dev != NULL);

    if (dev->uart)
    {
        if (mraa_uart_write(dev->uart, buffer, len) != len)
        {
            printf("%s: mraa_uart_write() failed.\n", __FUNCTION__);
            return UPM_ERROR_OPERATION_FAILED;
        }

    }
    else
    {
        // I2C
        if (mraa_i2c_write(dev->i2c, (uint8_t *)buffer, len))
        {
            printf("%s: mraa_i2c_write() failed.\n", __FUNCTION__);
            return UPM_ERROR_OPERATION_FAILED;
        }
    }

    return UPM_SUCCESS;
}

int ecezo_send_command(const ecezo_context dev, char *cmd, char *buffer,
                       int len)
{
    assert(dev != NULL);

    if (!cmd)
        return -1;

    // Our local buffer in case one isn't supplied
    char localBuffer[ECEZO_MAX_BUFFER_LEN];

    // our read buffer ptr
    char *readBuffer = NULL;

    if (!buffer || !len)
    {
        readBuffer = localBuffer;
        len = ECEZO_MAX_BUFFER_LEN;
    }
    else
    {
        readBuffer = buffer;
    }

#if defined(ECEZO_DEBUG)
    printf("Command: %s\n", cmd);
#endif // ECEZO_DEBUG

    // our write buffer
    char writeBuffer[ECEZO_MAX_BUFFER_LEN];

    strncpy(writeBuffer, cmd, ECEZO_MAX_BUFFER_LEN);
    writeBuffer[ECEZO_MAX_BUFFER_LEN - 1] = 0;

    int writelen = strlen(writeBuffer);

    if (dev->uart)
    {
        if (strlen(writeBuffer) >= ECEZO_MAX_BUFFER_LEN - 2)
        {
            // too big.  Should never happen in real life.
            printf("%s: cmd writeBuffer too big.\n", __FUNCTION__);
            return -1;
        }

        strcat(writeBuffer, "\r");
    }

    // for the uart this will now include the added CR, for I2C, this
    // will now include the already existing \0 terminator.
    writelen++;

    // Let the games begin...
    int retries = 10;

    while (retries-- > 0)
    {
        if (ecezo_write(dev, writeBuffer, writelen))
        {
            printf("%s: ecezo_write() failed\n", __FUNCTION__);
            return -1;
        }

        // we wait up to CMD_DELAY ms for an error response, which should be
        // more than enough time.  No response is also ok, since we
        // disable the "*OK" response in the init.

        memset((void *)readBuffer, 0, len);
        int bytesRead = 0;
        if ((bytesRead = ecezo_read(dev, readBuffer, len)) < 0)
        {
            return -1;
        }

        // for I2C, we are done at this point
        if (dev->i2c)
            break;

        // for UART, we need some more checks
        if (bytesRead && strstr(readBuffer, "*ER"))
        {
            // need to retry the command
#if defined(ECEZO_DEBUG)
            printf("%s: *ER DETECTED, retry\n", __FUNCTION__);
#endif // ECEZO_DEBUG
            continue;
        }
        else if (bytesRead && strchr(readBuffer, '*'))
        {
            // Some other diagnostic code, output it.
#if defined(ECEZO_DEBUG)
            printf("%s: * diagnostic code detected (%s), retry\n",
                   __FUNCTION__, buffer);
#endif // ECEZO_DEBUG
            continue;
        }
        else
        {
            // we are done here
#if defined(ECEZO_DEBUG)
            printf("%s: bytesRead = %d\n", __FUNCTION__, bytesRead);
#endif // ECEZO_DEBUG

            break;
        }
    }

    if (retries <= 0)
    {
        printf("%s: read timed out and/or and retries exhausted\n",
               __FUNCTION__);
        return 0;
    }

    return len;
}

upm_result_t ecezo_update(const ecezo_context dev)
{
    assert(dev != NULL);

    // first we send a 'R' command to get a reading (takes a minimum
    // of 1 second), then we parse out the string values into the
    // context variables.

    char buffer[ECEZO_MAX_BUFFER_LEN];

    // first issue the report command
    int rv = ecezo_send_command(dev, "R", buffer, ECEZO_MAX_BUFFER_LEN);
    if (rv == 0)
    {
        printf("%s: timed out waiting for data\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }
    else if (rv < 0)
    {
        printf("%s: error retrieving data\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    // decode
    if (decode_report(dev, buffer))
    {
        printf("%s: decode_report() failed\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    return UPM_SUCCESS;
}

float ecezo_get_ec(const ecezo_context dev)
{
    assert(dev != NULL);

    return dev->ec;
}

float ecezo_get_tds(const ecezo_context dev)
{
    assert(dev != NULL);

    return dev->tds;
}

float ecezo_get_salinity(const ecezo_context dev)
{
    assert(dev != NULL);

    return dev->salinity;
}

float ecezo_get_sg(const ecezo_context dev)
{
    assert(dev != NULL);

    return dev->sg;
}

upm_result_t ecezo_calibrate(const ecezo_context dev, ECEZO_CALIBRATION_T cal,
                             float ec)
{
    assert(dev != NULL);

    char cmdBuffer[ECEZO_MAX_BUFFER_LEN];

    switch(cal)
    {
    case ECEZO_CALIBRATE_CLEAR:
        snprintf(cmdBuffer, ECEZO_MAX_BUFFER_LEN, "cal,clear");
        break;

    case ECEZO_CALIBRATE_DRY:
        snprintf(cmdBuffer, ECEZO_MAX_BUFFER_LEN, "cal,dry");
        break;

    case ECEZO_CALIBRATE_ONE:
        snprintf(cmdBuffer, ECEZO_MAX_BUFFER_LEN, "cal,one,%f", ec);
        break;

    case ECEZO_CALIBRATE_LOW:
        snprintf(cmdBuffer, ECEZO_MAX_BUFFER_LEN, "cal,low,%f", ec);
        break;

    case ECEZO_CALIBRATE_HIGH:
        snprintf(cmdBuffer, ECEZO_MAX_BUFFER_LEN, "cal,high,%f", ec);
        break;

    default:
        // should be able to happen
        printf("%s: invalid cal parameter\n", __FUNCTION__);
        return UPM_ERROR_INVALID_PARAMETER;
    }

    return ecezo_send_command(dev, cmdBuffer, NULL, 0);
}
