/*
 * (C) 2010 by Andreas Eversberg <jolly@eversberg.eu>
 *
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <stdio.h>
#include <sys/file.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <stdbool.h>

#ifdef _HAVE_GPSD
#include <gps.h>
#endif

#include <osmocom/core/utils.h>
#include <osmocom/core/select.h>

#include <osmocom/bb/common/osmocom_data.h>
#include <osmocom/bb/common/logging.h>
#include <osmocom/bb/common/gps.h>

struct osmo_gps g = {
	0,
	GPS_TYPE_UNDEF,
#ifdef _HAVE_GPSD
    "localhost",
	"2947",
#endif
	"/dev/ttyACM0",
	0,
	0,
	0,
	0,0
};

static struct osmo_fd gps_bfd;

#ifdef _HAVE_GPSD

static struct gps_data_t* gdata = NULL;

#if GPSD_API_MAJOR_VERSION >= 5
static struct gps_data_t _gdata;
#endif

static inline int compat_gps_read(struct gps_data_t *data)
{
/* API break in gpsd 6bba8b329fc7687b15863d30471d5af402467802 */
#if GPSD_API_MAJOR_VERSION >= 7 && GPSD_API_MINOR_VERSION >= 0
	return gps_read(data, NULL, 0);
#elif GPSD_API_MAJOR_VERSION >= 5
	return gps_read(data);
#else
	return gps_poll(data);
#endif
}

int osmo_gpsd_cb(struct osmo_fd *bfd, unsigned int what)
{
	struct tm *tm;
	unsigned diff = 0;

	g.valid = 0;

	/* gps is offline */
#if GPSD_API_MAJOR_VERSION >= 9 && GPSD_API_MINOR_VERSION >= 0
	if (gdata->online.tv_sec || gdata->online.tv_nsec)
#else
	if (gdata->online)
#endif
	    goto gps_not_ready;

#if GPSD_API_MAJOR_VERSION >= 5
	/* gps has no data */
	if (gps_waiting(gdata, 500))
	    goto gps_not_ready;
#else
	/* gps has no data */
	if (gps_waiting(gdata))
	    goto gps_not_ready;
#endif

	/* polling returned an error */
	if (compat_gps_read(gdata))
	    goto gps_not_ready;

	/* data are valid */
	if (gdata->set & LATLON_SET) {
		g.valid = 1;
#if GPSD_API_MAJOR_VERSION >= 9 && GPSD_API_MINOR_VERSION >= 0
		g.gmt = gdata->fix.time.tv_sec;
#else
		g.gmt = gdata->fix.time;
#endif
		tm = localtime(&g.gmt);
		diff = time(NULL) - g.gmt;
		g.latitude = gdata->fix.latitude;
		g.longitude = gdata->fix.longitude;

		LOGP(DGPS, LOGL_INFO, " time=%02d:%02d:%02d %04d-%02d-%02d, "
			"diff-to-host=%d, latitude=%do%.4f, longitude=%do%.4f\n",
			tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_year + 1900,
			tm->tm_mday, tm->tm_mon + 1, diff,
			(int)g.latitude,
			(g.latitude - ((int)g.latitude)) * 60.0,
			(int)g.longitude,
			(g.longitude - ((int)g.longitude)) * 60.0);
	}

	return 0;

gps_not_ready:
	LOGP(DGPS, LOGL_DEBUG, "gps is offline");
	return -1;
}

int osmo_gpsd_open(void)
{
	LOGP(DGPS, LOGL_INFO, "Connecting to gpsd at '%s:%s'\n", g.gpsd_host, g.gpsd_port);

#if GPSD_API_MAJOR_VERSION >= 5
	if (gps_open(g.gpsd_host, g.gpsd_port, &_gdata) == -1)
		gdata = NULL;
	else
		gdata = &_gdata;
#else
	gdata = gps_open(g.gpsd_host, g.gpsd_port);
#endif
	if (gdata == NULL) {
		LOGP(DGPS, LOGL_ERROR, "Can't connect to gpsd\n");
		return -1;
	}
	if (gdata->gps_fd < 0)
		return gdata->gps_fd;

	if (gps_stream(gdata, WATCH_ENABLE, NULL) == -1) {
		LOGP(DGPS, LOGL_ERROR, "Error in gps_stream()\n");
		return -1;
	}

	osmo_fd_setup(&gps_bfd, gdata->gps_fd, OSMO_FD_READ, osmo_gpsd_cb, NULL, 0);
	osmo_fd_register(&gps_bfd);

	return 0;
}

void osmo_gpsd_close(void)
{
	if (gps_bfd.fd <= 0)
		return;

	LOGP(DGPS, LOGL_INFO, "Disconnecting from gpsd\n");

	osmo_fd_unregister(&gps_bfd);

#if GPSD_API_MAJOR_VERSION >= 5
	gps_stream(gdata, WATCH_DISABLE, NULL);
#endif
	gps_close(gdata);
	gps_bfd.fd = -1; /* -1 or 0 indicates: 'close' */
}

#endif

static struct termios gps_termios, gps_old_termios;

static int osmo_serialgps_line(char *line)
{
	time_t gps_now, host_now;
	struct tm *tm;
	int32_t diff;
	double latitude, longitude;

	if (!!strncmp(line, "$GPGLL", 6))
		return 0;
	line += 7;
	if (strlen(line) < 37)
		return 0;
	line[37] = '\0';
	/* ddmm.mmmm,N,dddmm.mmmm,E,hhmmss.mmm,A */

	/* valid position */
	if (line[36] != 'A') {
		LOGP(DGPS, LOGL_INFO, "%s (invalid)\n", line);
		g.valid = 0;
		return 0;
	}
	g.valid = 1;

	/* time stamp */
	gps_now = line[30] - '0';
	gps_now += (line[29] - '0') * 10;
	gps_now += (line[28] - '0') * 60;
	gps_now += (line[27] - '0') * 600;
	gps_now += (line[26] - '0') * 3600;
	gps_now += (line[25] - '0') * 36000;
	time(&host_now);
	/* calculate the number of seconds the host differs from GPS */
	diff = host_now % 86400 - gps_now;
	if (diff < 0)
		diff += 86400;
	if (diff >= 43200)
		diff -= 86400;
	/* apply the "date" part to the GPS time */
	gps_now = host_now - diff;
	g.gmt = gps_now;
	tm = localtime(&gps_now);

	/* position */
	latitude = (double)(line[0] - '0') * 10.0;
	latitude += (double)(line[1] - '0');
	latitude += (double)(line[2] - '0') / 6.0;
	latitude += (double)(line[3] - '0') / 60.0;
	latitude += (double)(line[5] - '0') / 600.0;
	latitude += (double)(line[6] - '0') / 6000.0;
	latitude += (double)(line[7] - '0') / 60000.0;
	latitude += (double)(line[8] - '0') / 600000.0;
	if (line[10] == 'S')
		latitude = 0.0 - latitude;
	g.latitude = latitude;
	longitude = (double)(line[12] - '0') * 100.0;
	longitude += (double)(line[13] - '0') * 10.0;
	longitude += (double)(line[14] - '0');
	longitude += (double)(line[15] - '0') / 6.0;
	longitude += (double)(line[16] - '0') / 60.0;
	longitude += (double)(line[18] - '0') / 600.0;
	longitude += (double)(line[19] - '0') / 6000.0;
	longitude += (double)(line[20] - '0') / 60000.0;
	longitude += (double)(line[21] - '0') / 600000.0;
	if (line[23] == 'W')
		longitude = 360.0 - longitude;
	g.longitude = longitude;

	LOGP(DGPS, LOGL_DEBUG, "%s\n", line);
	LOGP(DGPS, LOGL_INFO, " time=%02d:%02d:%02d %04d-%02d-%02d, "
		"diff-to-host=%d, latitude=%do%.4f, longitude=%do%.4f\n",
		tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_year + 1900,
		tm->tm_mday, tm->tm_mon + 1, diff,
		(int)g.latitude,
		(g.latitude - ((int)g.latitude)) * 60.0,
		(int)g.longitude,
		(g.longitude - ((int)g.longitude)) * 60.0);
	return 0;
}

static int nmea_checksum(char *line)
{
	uint8_t checksum = 0;

	while (*line) {
		if (*line == '$') {
			line++;
			continue;
		}
		if (*line == '*')
			break;
		checksum ^= *line++;
	}
	return (strtoul(line+1, NULL, 16) == checksum);
}

int osmo_serialgps_cb(struct osmo_fd *bfd, unsigned int what)
{
	char buff[128];
	static char line[128];
	static int lpos = 0;
	int i = 0, len;

	len = read(bfd->fd, buff, sizeof(buff));
	if (len <= 0) {
		fprintf(stderr, "error reading GPS device (errno=%d)\n", errno);
		return len;
	}
	while(i < len) {
		if (buff[i] == 13) {
			i++;
			continue;
		}
		if (buff[i] == 10) {
			line[lpos] = '\0';
			lpos = 0;
			i++;
			if (!nmea_checksum(line))
				fprintf(stderr, "NMEA checksum error\n");
			else
				osmo_serialgps_line(line);
			continue;
		}
		line[lpos++] = buff[i++];
		if (lpos == sizeof(line))
			lpos--;
	}

	return 0;
}

int osmo_serialgps_open(void)
{
	int baud = 0;
	int fd;

	if (gps_bfd.fd > 0)
		return 0;

	LOGP(DGPS, LOGL_INFO, "Open GPS device '%s'\n", g.device);

	fd = open(g.device, O_RDONLY);
	if (fd < 0)
		return fd;
	osmo_fd_setup(&gps_bfd, fd, OSMO_FD_READ, osmo_serialgps_cb, NULL, 0);

	switch (g.baud) {
	case   4800:
		baud = B4800;      break;
	case   9600:
		baud = B9600;      break;
	case  19200:
		baud = B19200;     break;
	case  38400:
		baud = B38400;     break;
	case  57600:
		baud = B57600;     break;
	case 115200:
		baud = B115200;    break;
	}

	if (isatty(gps_bfd.fd))
	{
		/* get termios */
		tcgetattr(gps_bfd.fd, &gps_old_termios);
		tcgetattr(gps_bfd.fd, &gps_termios);
		/* set baud */
		if (baud) {
			gps_termios.c_cflag |= baud;
			cfsetispeed(&gps_termios, baud);
			cfsetospeed(&gps_termios, baud);
		}
		if (tcsetattr(gps_bfd.fd, TCSANOW, &gps_termios))
			printf("Failed to set termios for GPS\n");
	}

	osmo_fd_register(&gps_bfd);

	return 0;
}

void osmo_serialgps_close(void)
{
	if (gps_bfd.fd <= 0)
		return;

	LOGP(DGPS, LOGL_INFO, "Close GPS device\n");

	osmo_fd_unregister(&gps_bfd);

	if (isatty(gps_bfd.fd))
		tcsetattr(gps_bfd.fd, TCSANOW, &gps_old_termios);

	close(gps_bfd.fd);
	gps_bfd.fd = -1; /* -1 or 0 indicates: 'close' */
}

void osmo_gps_init(void)
{
	memset(&gps_bfd, 0, sizeof(gps_bfd));
}

int osmo_gps_open(void)
{
	switch (g.gps_type) {
#ifdef _HAVE_GPSD
		case GPS_TYPE_GPSD:
			return osmo_gpsd_open();
#endif
		case GPS_TYPE_SERIAL:
			return osmo_serialgps_open();

		default:
			return 0;
	}
}

void osmo_gps_close(void)
{
	switch (g.gps_type) {
#ifdef _HAVE_GPSD
		case GPS_TYPE_GPSD:
			return osmo_gpsd_close();
#endif
		case GPS_TYPE_SERIAL:
			return osmo_serialgps_close();

		default:
			return;
	}
}
