/*
 * Handle the Garmin simple text format supported by some Garmins.
 * Tested with the 'Garmin eTrex Legend' device working in 'Text Out' mode.
 *
 * Protocol info from:
 *       http://www8.garmin.com/support/text_out.html
 *       http://www.garmin.com/support/commProtocol.html
 *
 * Code by: Petr Slansky <slansky@usa.net>
 * all rights abandoned, a thank would be nice if you use this code.
 *
 * -D 3 = packet trace
 * -D 4 = packet details
 * -D 5 = more packet details
 * -D 6 = very excessive details
 *
 * limitations:
 *  very simple protocol, only very basic information
 * TODO
 * do not have from garmin:
 *      pdop
 *      vdop
 *      magnetic variation
 *      satellite information
 *
 * This file is Copyright 2010 by the GPSD project
 * SPDX-License-Identifier: BSD-2-clause
 *
 */

/***************************************************
Garmin Simple Text Output Format:

The simple text (ASCII) output contains time, position, and velocity data in
the fixed width fields (not delimited) defined in the following table:

    FIELD DESCRIPTION:      WIDTH:  NOTES:
    ----------------------- ------- ------------------------
    Sentence start          1       Always '@'
    ----------------------- ------- ------------------------
   /Year                    2       Last two digits of UTC year
  | ----------------------- ------- ------------------------
  | Month                   2       UTC month, "01".."12"
T | ----------------------- ------- ------------------------
i | Day                     2       UTC day of month, "01".."31"
m | ----------------------- ------- ------------------------
e | Hour                    2       UTC hour, "00".."23"
  | ----------------------- ------- ------------------------
  | Minute                  2       UTC minute, "00".."59"
  | ----------------------- ------- ------------------------
   \Second                  2       UTC second, "00".."59"
    ----------------------- ------- ------------------------
   /Latitude hemisphere     1       'N' or 'S'
  | ----------------------- ------- ------------------------
  | Latitude position       7       WGS84 ddmmmmm, with an implied
  |                                 decimal after the 4th digit
  | ----------------------- ------- ------------------------
  | Longitude hemisphere    1       'E' or 'W'
  | ----------------------- ------- ------------------------
  | Longitude position      8       WGS84 dddmmmmm with an implied
P |                                 decimal after the 5th digit
o | ----------------------- ------- ------------------------
s | Position status         1       'd' if current 2D differential GPS position
i |                                 'D' if current 3D differential GPS position
t |                                 'g' if current 2D GPS position
i |                                 'G' if current 3D GPS position
o |                                 'S' if simulated position
n |                                 '_' if invalid position
  | ----------------------- ------- ------------------------
  | Horizontal posn error   3       EPH in meters
  | ----------------------- ------- ------------------------
  | Altitude sign           1       '+' or '-'
  | ----------------------- ------- ------------------------
  | Altitude                5       Height above or below mean
   \                                sea level in meters
    ----------------------- ------- ------------------------
   /East/West velocity      1       'E' or 'W'
  |     direction
  | ----------------------- ------- ------------------------
  | East/West velocity      4       Meters per second in tenths,
  |     magnitude                   ("1234" = 123.4 m/s)
V | ----------------------- ------- ------------------------
e | North/South velocity    1       'N' or 'S'
l |     direction
o | ----------------------- ------- ------------------------
c | North/South velocity    4       Meters per second in tenths,
i |     magnitude                   ("1234" = 123.4 m/s)
t | ----------------------- ------- ------------------------
y | Vertical velocity       1       'U' or 'D' (up/down)
  |     direction
  | ----------------------- ------- ------------------------
  | Vertical velocity       4       Meters per second in hundredths,
   \    magnitude                   ("1234" = 12.34 m/s)
    ----------------------- ------- ------------------------
    Sentence end            2       Carriage return, '0x0D', and
                                    line feed, '0x0A'
    ----------------------- ------- ------------------------

If a numeric value does not fill its entire field width, the field is padded
with leading '0's (eg. an altitude of 50 meters above MSL will be output as
"+00050").

Any or all of the data in the text sentence (except for the sentence start
and sentence end fields) may be replaced with underscores to indicate
invalid data.

***************************************************/


#include "../include/gpsd_config.h"  // must be before all includes

#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include "../include/gpsd.h"

#ifdef GARMINTXT_ENABLE

// Simple text message is fixed length, 55 chars text data + 2 characters EOL
// buffer for text processing
#define TXT_BUFFER_SIZE 13

/**************************************************************************
 * decode text string to double number, translate prefix to sign
 * return 0: OK
 *       -1: data error
 *       -2: data not valid
 *
 * examples with context->errout.debug == 0:
 *
 *  gar_decode(context, cbuf, 9, "EW", 100000.0, &result);
 *  E01412345 -> +14.12345
 *
 *  gar_decode(context, cbuf, 9, "EW", 100000.0, &result);
 *  W01412345 -> -14.12345
 *
 *  gar_decode(context, cbuf, 3, "", 10.0, &result);
 *  123 -> +12.3
 *
**************************************************************************/
static int gar_decode(const struct gps_context_t *context,
                      const char *data, const size_t length,
                      const char *prefix, const double divisor,
                      double *result)
{
    char buf[10];
    float sign = 1.0;
    int offset = 1;      // assume one character prefix (E,W,S,N,U,D, etc)
    long int intresult;

    if (1 > length) {
        GPSD_LOG(LOG_ERROR, &context->errout, "GTXT: field too short %zu\n",
                 length);
        return -1;
    }

    if (sizeof(buf) <= length) {
        GPSD_LOG(LOG_ERROR, &context->errout,
                 "GTXT: internal buffer too small\n");
        return -1;
    }

    memset(buf, 0, sizeof(buf));
    (void)strlcpy(buf, data, length);
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);

    if (NULL != strchr(buf, '_')) {
        // value is not valid, ignore it
        return -2;
    }

    // parse prefix
    do {
        if ('\0' == prefix[0]) {
            offset = 0;         // only number, no prefix
            break;
        }
        // second character in prefix is flag for negative number
        if ('\0' != prefix[1]) {
            if (buf[0] == prefix[1]) {
                sign = -1.0;
                break;
            }
            // 2nd prefix char not match
        }
        // first character in prefix is flag for positive number
        if (buf[0] == prefix[0]) {
            sign = 1.0;
            break;
        }
        GPSD_LOG(LOG_WARN, &context->errout,
                 "GTXT: Unexpected char \"%c\" in data \"%s\"\n",
                 buf[0], buf);
        return -1;
    } while (0);

    if (strspn(buf + offset, "0123456789") != length - offset) {
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
        return -1;
    }

    intresult = atol(buf + offset);
    if (0L == intresult) {
        sign = 0.0;             //  don't create negative zero
    }

    *result = (double)intresult / divisor * sign;

    return 0;                   // SUCCESS
}

/**************************************************************************
 * decode integer from string, check if the result is in expected range
 * return 0: OK
 *       -1: data error
 *       -2: data not valid
**************************************************************************/
static int gar_int_decode(const struct gps_context_t *context,
                          const char *data, const size_t length,
                          const unsigned int min, const unsigned int max,
                          unsigned int *result)
{
    char buf[10];
    unsigned int res;

    if (sizeof(buf) <= length ) {
        GPSD_LOG(LOG_ERROR, &context->errout,
                 "GTXT: internal buffer too small\n");
        return -1;
    }

    memset(buf, 0, sizeof(buf));
    (void)strlcpy(buf, data, length);
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);

    if (NULL != strchr(buf, '_')) {
        // value is not valid, ignore it
        return -2;
    }

    if (strspn(buf, "0123456789") != length) {
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
        return -1;
    }

    res = (unsigned)atoi(buf);
    if (IN(min, res, max)) {
        *result = res;
        return 0;               // SUCCESS
    }
    GPSD_LOG(LOG_WARN, &context->errout,
             "GTXT: Value %u out of range <%u, %u>\n", res, min,
             max);
    return -1;
}


/**************************************************************************
 *
 * Entry points begin here
 *
 **************************************************************************/

// parse GARMIN Simple Text sentence, unpack it into a session structure
gps_mask_t garmintxt_parse(struct gps_device_t * session)
{

    gps_mask_t mask = 0;

    GPSD_LOG(LOG_PROG, &session->context->errout,
             "GTXT: Garmin Simple Text packet, len %zd: %s\n",
             session->lexer.outbuflen, (char*)session->lexer.outbuffer);

    if (54 > session->lexer.outbuflen) {
        /* trailing CR and LF can be ignored; ('@' + 54x 'DATA' + '\r\n')
         * has length 57 */
        GPSD_LOG(LOG_WARN, &session->context->errout,
                 "GTXT: Message is too short, rejected.\n");
        return ONLINE_SET;
    }

    session->lexer.type = GARMINTXT_PACKET;

    // only one message, set cycle start
    session->cycle_end_reliable = true;
    do {
        struct tm gdate = {0};            // date part of last sentence time
        unsigned int result;
        char *buf = (char *)session->lexer.outbuffer + 1;

        GPSD_LOG(LOG_PROG, &session->context->errout,
                 "GTXT: Timestamp: %.12s\n", buf);

        // year
        if (0 != gar_int_decode(session->context,
                                buf + 0, 2, 0, 99, &result)) {
            break;
        }
        gdate.tm_year = (session->context->century + (int)result) - 1900;
        // month
        if (0 != gar_int_decode(session->context,
                                buf + 2, 2, 1, 12, &result)) {
            break;
        }
        gdate.tm_mon = (int)result - 1;
        // day
        if (0 != gar_int_decode(session->context,
                                buf + 4, 2, 1, 31, &result)) {
            break;
        }
        gdate.tm_mday = (int)result;
        // hour
        if (0 != gar_int_decode(session->context,
                                buf + 6, 2, 0, 23, &result)) {
            break;
        }
        // mday update??
        gdate.tm_hour = (int)result;
        // minute
        if (0 != gar_int_decode(session->context,
                                buf + 8, 2, 0, 59, &result)) {
            break;
        }
        gdate.tm_min = (int)result;
        // second
        // second value can be even 60, occasional leap second
        if (0 != gar_int_decode(session->context,
                                buf + 10, 2, 0, 60, &result)) {
            break;
        }
        gdate.tm_sec = (int)result;
        session->newdata.time.tv_sec = mkgmtime(&gdate);
        session->newdata.time.tv_nsec = 0;
        mask |= TIME_SET;
    } while (0);

    /* assume that position is unknown; if the position is known we
     * will fix status information later */
    session->newdata.mode = MODE_NO_FIX;
    session->newdata.status = STATUS_UNK;
    mask |= MODE_SET | STATUS_SET | CLEAR_IS | REPORT_IS;

    // process position

    do {
        double lat, lon;
        unsigned int degfrag;
        char status;

        // Latitude, [NS]ddmmmmm
        // decode degrees of Latitude
        if (0 !=
            gar_decode(session->context,
                (char *)session->lexer.outbuffer + 13, 3, "NS", 1.0,
                &lat)) {
            break;
        }
        // decode minutes of Latitude
        if (0 !=
            gar_int_decode(session->context,
                           (char *)session->lexer.outbuffer + 16, 5, 0,
                           99999, &degfrag)) {
            break;
        }
        lat += degfrag * 100.0 / 60.0 / 100000.0;
        session->newdata.latitude = lat;

        // Longitude, [EW]dddmmmmm
        // decode degrees of Longitude
        if (0 !=
            gar_decode(session->context,
                       (char *)session->lexer.outbuffer + 21, 4, "EW", 1.0,
                       &lon)) {
            break;
        }
        // decode minutes of Longitude
        if (0 !=
            gar_int_decode(session->context,
                           (char *)session->lexer.outbuffer + 25, 5, 0,
                           99999, &degfrag)) {
            break;
        }
        lon += degfrag * 100.0 / 60.0 / 100000.0;
        session->newdata.longitude = lon;
        session->newdata.geoid_sep = wgs84_separation(lat, lon);

        // fix mode, GPS status, [gGdDS_]
        status = (char)session->lexer.outbuffer[30];

        switch (status) {
        case 'D':
            session->newdata.mode = MODE_3D;
            session->newdata.status = STATUS_DGPS;
            break;
        case 'G':
            session->newdata.mode = MODE_3D;
            session->newdata.status = STATUS_GPS;
            break;
        case 'S':
            session->newdata.mode = MODE_3D;
            session->newdata.status = STATUS_SIM;
            break;
        case 'd':
            session->newdata.mode = MODE_2D;
            session->newdata.status = STATUS_DGPS;
            break;
        case 'g':
            session->newdata.mode = MODE_2D;
            session->newdata.status = STATUS_GPS;
            break;
        default:
            session->newdata.mode = MODE_NO_FIX;
            session->newdata.status = STATUS_UNK;
        }
        mask |= MODE_SET | STATUS_SET | LATLON_SET;
    } while (0);

    // EPH
    do {
        double eph;
        if (0 !=
            gar_decode(session->context,
                       (char *)session->lexer.outbuffer + 31, 3, "", 1.0,
                       &eph)) {
            break;
        }
        // this conversion looks dodgy...
        session->newdata.eph = eph * (GPSD_CONFIDENCE / CEP50_SIGMA);
        mask |= HERR_SET;
    } while (0);

    // Altitude
    do {
        double alt;
        if (0 !=
            gar_decode(session->context,
                       (char *)session->lexer.outbuffer + 34, 6, "+-", 1.0,
                       &alt)) {
            break;
        }
        // alt is MSL
        session->newdata.altMSL = alt;
        // Let gpsd_error_model() deal with altHAE
        mask |= ALTITUDE_SET;
    } while (0);

    // Velocities, meters per second
    do {
        double ewvel, nsvel;
        double climb;

        if (0 != gar_decode(session->context,
                            (char *)session->lexer.outbuffer + 40, 5,
                            "EW", 10.0, &ewvel)) {
            break;
        }
        if (0 != gar_decode(session->context,
                            (char *)session->lexer.outbuffer + 45, 5,
                            "NS", 10.0, &nsvel)) {
            break;
        }
        if (0 != gar_decode(session->context,
                            (char *)session->lexer.outbuffer + 50, 5,
                            "UD", 100.0, &climb)) {
            break;
        }

        session->newdata.NED.velN = ewvel;
        session->newdata.NED.velE = nsvel;
        session->newdata.NED.velD = -climb;
        mask |= VNED_SET;
    } while (0);

    GPSD_LOG(LOG_DATA, &session->context->errout,
             "GTXT: time=%lld, lat=%.2f lon=%.2f altMSL=%.2f "
             "climb=%.2f eph=%.2f mode=%d status=%d\n",
             (long long)session->newdata.time.tv_sec,
             session->newdata.latitude,
             session->newdata.longitude, session->newdata.altMSL,
             session->newdata.climb, session->newdata.eph,
             session->newdata.mode,
             session->newdata.status);
    return mask;
}

#endif  // GARMINTXT_ENABLE
// vim: set expandtab shiftwidth=4
