/*
 * Copyright (C) 2017 Konsulko Group
 * Author: Matt Ranostay <matt.ranostay@konsulko.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <gps.h>
#include <pthread.h>
#include <json-c/json.h>

#define AFB_BINDING_VERSION 2
#include <afb/afb-binding.h>

static struct gps_data_t data;
static struct afb_event location_event;

static pthread_t thread;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

#define MSECS_TO_USECS(x) (x * 1000)

// struct dop_t item order
static const char *dop_names[] = {
	"xdop",
	"ydop",
	"pdop",
	"hdop",
	"vdop",
	"tdop",
	"gdop",
	NULL
};

static json_object *populate_json_dop_data(json_object *jresp, struct dop_t *dop)
{
	char **names = (char **) dop_names;
	double *tmp = (double *) dop;
	json_object *value = NULL;

	while (*names) {
		double val = *tmp++;

		if (val != 0) {
			value = json_object_new_double(val);
			json_object_object_add(jresp, *names, value);
		}
		names++;
	}

	return jresp;
}

static json_object *populate_json_data(json_object *jresp)
{
	json_object *value = NULL;

	if (data.fix.mode != MODE_3D) {
		json_object_put(jresp);
		return NULL;
	}

	if (data.set & ALTITUDE_SET) {
		value = json_object_new_double(data.fix.altitude);
		json_object_object_add(jresp, "altitude", value);
	}

	if (data.set & LATLON_SET) {
		value = json_object_new_double(data.fix.latitude);
		json_object_object_add(jresp, "latitude", value);

		value = json_object_new_double(data.fix.longitude);
		json_object_object_add(jresp, "longitude", value);
	}

	if (data.set & SPEED_SET) {
		value = json_object_new_double(data.fix.speed);
		json_object_object_add(jresp, "speed", value);
	}

	if (data.set & TIME_SET) {
		char time[30];
		unix_to_iso8601(data.fix.time, (char *) &time, sizeof(time));
		value = json_object_new_string(time);
		json_object_object_add(jresp, "timestamp", value);
	}

	jresp = populate_json_dop_data(jresp, &data.dop);

	return jresp;
}

static void get_data(struct afb_req request)
{
	json_object *jresp = NULL;
	pthread_mutex_lock(&mutex);

	jresp = populate_json_data(json_object_new_object());
	if (jresp != NULL) {
		afb_req_success(request, jresp, "GNSS location data");
	} else {
		afb_req_fail(request, "failed", "No 3D GNSS fix");
	}

	pthread_mutex_unlock(&mutex);
}

static void subscribe(struct afb_req request)
{
	const char *value = afb_req_value(request, "value");

	if (value && !strcasecmp(value, "location")) {
		afb_req_subscribe(request, location_event);
		afb_req_success(request, NULL, NULL);
		return;
	}

	afb_req_fail(request, "failed", "Invalid event");
}

static void unsubscribe(struct afb_req request)
{
	const char *value = afb_req_value(request, "value");

	if (value && !strcasecmp(value, "location")) {
		afb_req_unsubscribe(request, location_event);
		afb_req_success(request, NULL, NULL);
		return;
	}

	afb_req_fail(request, "failed", "Invalid event");
}

static void *data_poll(void *ptr)
{
	/*
	 * keep reading till an error condition happens
	 */
	while (gps_waiting(&data, MSECS_TO_USECS(2000)) && !errno)
	{
		json_object *jresp = NULL;

		pthread_mutex_lock(&mutex);
		if (gps_read(&data) == -1) {
			AFB_ERROR("Cannot read from GPS daemon.\n");
			pthread_mutex_unlock(&mutex);
			break;
		}

		if (!(data.set & (TRACKERR_SET | SPEEDERR_SET| CLIMBERR_SET))) {
			jresp = populate_json_data(json_object_new_object());
			if (jresp != NULL)
				afb_event_push(location_event, jresp);
		}
		pthread_mutex_unlock(&mutex);
	}

	AFB_INFO("Closing GPS daemon connection.\n");
	gps_stream(&data, WATCH_DISABLE, NULL);
	gps_close(&data);

	_exit(0);

	return NULL;
}

static int init()
{
	const char *host, *port;
	int ret, tries = 5;

	location_event = afb_daemon_make_event("location");

	host = getenv("AFBGPS_HOST") ? : "localhost";
	port = getenv("AFBGPS_SERVICE") ? : "2947";

	ret = gps_open(host, port, &data);
	if (ret < 0)
		return ret;

	gps_stream(&data, WATCH_ENABLE | WATCH_JSON, NULL);

	// due to the gpsd.socket race condition need to loop till initial event
	do {
		gps_read(&data);
	} while (!gps_waiting(&data, MSECS_TO_USECS(2500)) && tries--);

	return pthread_create(&thread, NULL, &data_poll, NULL);
}

static const struct afb_verb_v2 binding_verbs[] = {
	{ .verb = "location",    .callback = get_data,     .info = "Get GNSS data" },
	{ .verb = "subscribe",   .callback = subscribe,    .info = "Subscribe to GNSS events" },
	{ .verb = "unsubscribe", .callback = unsubscribe,  .info = "Unsubscribe to GNSS events" },
	{ }
};

/*
 * binder API description
 */
const struct afb_binding_v2 afbBindingV2 = {
	.api = "gps",
	.specification = "GNSS/GPS API",
	.verbs = binding_verbs,
	.init = init,
};