/**
 * @file nmea.c
 * @note Copyright (C) 2020 Richard Cochran <richardcochran@gmail.com>
 * @note SPDX-License-Identifier: GPL-2.0+
 */
#include <malloc.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "nmea.h"
#include "print.h"

#define NMEA_CHAR_MIN	' '
#define NMEA_CHAR_MAX	'~'
#define NMEA_MAX_LENGTH	256

enum nmea_state {
	NMEA_IDLE,
	NMEA_HAVE_DOLLAR,
	NMEA_HAVE_STARTG,
	NMEA_HAVE_STARTX,
	NMEA_HAVE_BODY,
	NMEA_HAVE_CSUMA,
	NMEA_HAVE_CSUM_MSB,
	NMEA_HAVE_CSUM_LSB,
	NMEA_HAVE_PENULTIMATE,
};

struct nmea_parser {
	char sentence[NMEA_MAX_LENGTH + 1];
	char payload_checksum[3];
	enum nmea_state state;
	uint8_t checksum;
	int offset;
};

static void nmea_reset(struct nmea_parser *np);

static void nmea_accumulate(struct nmea_parser *np, char c)
{
	if (c < NMEA_CHAR_MIN || c > NMEA_CHAR_MAX) {
		nmea_reset(np);
		return;
	}
	if (np->offset == NMEA_MAX_LENGTH) {
		nmea_reset(np);
	}
	np->sentence[np->offset++] = c;
	np->checksum ^= c;
}

static int nmea_parse_symbol(struct nmea_parser *np, char c)
{
	switch (np->state) {
	case NMEA_IDLE:
		if (c == '$') {
			np->state = NMEA_HAVE_DOLLAR;
		}
		break;
	case NMEA_HAVE_DOLLAR:
		if (c == 'G') {
			np->state = NMEA_HAVE_STARTG;
			nmea_accumulate(np, c);
		} else {
			nmea_reset(np);
		}
		break;
	case NMEA_HAVE_STARTG:
		np->state = NMEA_HAVE_STARTX;
		nmea_accumulate(np, c);
		break;
	case NMEA_HAVE_STARTX:
		np->state = NMEA_HAVE_BODY;
		nmea_accumulate(np, c);
		break;
	case NMEA_HAVE_BODY:
		if (c == '*') {
			np->state = NMEA_HAVE_CSUMA;
		} else {
			nmea_accumulate(np, c);
		}
		break;
	case NMEA_HAVE_CSUMA:
		np->state = NMEA_HAVE_CSUM_MSB;
		np->payload_checksum[0] = c;
		break;
	case NMEA_HAVE_CSUM_MSB:
		np->state = NMEA_HAVE_CSUM_LSB;
		np->payload_checksum[1] = c;
		break;
	case NMEA_HAVE_CSUM_LSB:
		if (c == '\n') {
			/*skip the CR*/
			return 0;
		}
		if (c == '\r') {
			np->state = NMEA_HAVE_PENULTIMATE;
		} else {
			nmea_reset(np);
		}
		break;
	case NMEA_HAVE_PENULTIMATE:
		if (c == '\n') {
			return 0;
		}
		nmea_reset(np);
		break;
	}
	return -1;
}

static void nmea_reset(struct nmea_parser *np)
{
	memset(np, 0, sizeof(*np));
}

static int nmea_scan_rmc(struct nmea_parser *np, struct nmea_rmc *result)
{
	int cnt, i, msec = 0;
	char *ptr, status;
	uint8_t checksum;
	struct tm tm = {0};

	pr_debug("nmea sentence: %s", np->sentence);
	cnt = sscanf(np->payload_checksum, "%02hhx", &checksum);
	if (cnt != 1) {
		return -1;
	}
	if (checksum != np->checksum) {
		pr_err("checksum mismatch 0x%02hhx != 0x%02hhx on %s",
		       checksum, np->checksum, np->sentence);
		return -1;
	}
	cnt = sscanf(np->sentence,
		     "G%*cRMC,%2d%2d%2d.%d,%c",
		     &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &msec, &status);
	if (cnt != 5) {
		cnt = sscanf(np->sentence,
			     "G%*cRMC,%2d%2d%2d,%c",
			     &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &status);
		if (cnt != 4) {
			return -1;
		}
	}
	ptr = np->sentence;
	for (i = 0; i < 9; i++) {
		ptr = strchr(ptr, ',');
		if (!ptr) {
			return -1;
		}
		ptr++;
	}
	cnt = sscanf(ptr, "%2d%2d%2d", &tm.tm_mday, &tm.tm_mon, &tm.tm_year);
	if (cnt != 3) {
		return -1;
	}
	/* Convert an inserted leap second to ambiguous 23:59:59 */
	if (tm.tm_sec == 60)
		tm.tm_sec = 59;
	tm.tm_year += 100;
	tm.tm_mon--;
	tm.tm_isdst = 0;
	result->ts.tv_sec = mktime(&tm);
	result->ts.tv_nsec = msec * 1000000UL;
	result->fix_valid = status == 'A' ? true : false;
	return 0;
}

int nmea_parse(struct nmea_parser *np, const char *ptr, int buflen,
	       struct nmea_rmc *result, int *parsed)
{
	int count = 0;
	while (buflen) {
		if (!nmea_parse_symbol(np, *ptr)) {
			if (!nmea_scan_rmc(np, result)) {
				nmea_reset(np);
				*parsed = count + 1;
				return 0;
			}
			nmea_reset(np);
		}
		buflen--;
		count++;
		ptr++;
	}
	*parsed = count;
	return -1;
}

struct nmea_parser *nmea_parser_create(void)
{
	struct nmea_parser *np;
	np = malloc(sizeof(*np));
	if (!np) {
		return NULL;
	}
	nmea_reset(np);
	/* Ensure that mktime(3) returns a value in the UTC time scale. */
	setenv("TZ", "UTC", 1);
	return np;
}

void nmea_parser_destroy(struct nmea_parser *np)
{
	free(np);
}
