/*
 * Copyright (c) 2019 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/socket.h>
#include <supl_os_client.h>
#include <supl_session.h>
#include <nrf_modem_gnss.h>
#include <zephyr/logging/log.h>

#include "assistance.h"

//#include "modules/ui_buzzer.h"
LOG_MODULE_REGISTER(MODULE, CONFIG_LOCATION_MODULE_LOG_LEVEL);
#include "minilog.h"

#define SUPL_SERVER        "supl.google.com"
//#define SUPL_SERVER        "srv.aeroluminate.com.au"
#define SUPL_SERVER_PORT   7276

//BUILD_ASSERT(sizeof(CONFIG_GNSS_SAMPLE_SUPL_HOSTNAME) > 1, "Server hostname must be configured");

static int supl_fd = -1;
static volatile bool assistance_active;

static ssize_t supl_read(void *p_buff, size_t nbytes, void *user_data)
{
	ARG_UNUSED(user_data);

	ssize_t rc = recv(supl_fd, p_buff, nbytes, 0);

	if (rc < 0 && (errno == EAGAIN)) {
		return 0;
	}

	return rc;
}

static ssize_t supl_write(const void *p_buff, size_t nbytes, void *user_data)
{
	ARG_UNUSED(user_data);

	return send(supl_fd, p_buff, nbytes, 0);
}

static void get_agps_data_type_string(char *type_string, uint16_t type)
{
	switch (type) {
	case NRF_MODEM_GNSS_AGPS_UTC_PARAMETERS:
		(void)strcpy(type_string, "utc");
		break;
	case NRF_MODEM_GNSS_AGPS_EPHEMERIDES:
		(void)strcpy(type_string, "ephe");
		break;
	case NRF_MODEM_GNSS_AGPS_ALMANAC:
		(void)strcpy(type_string, "alm");
		break;
	case NRF_MODEM_GNSS_AGPS_KLOBUCHAR_IONOSPHERIC_CORRECTION:
		(void)strcpy(type_string, "klob");
		break;
	case NRF_MODEM_GNSS_AGPS_NEQUICK_IONOSPHERIC_CORRECTION:
		(void)strcpy(type_string, "neq");
		break;
	case NRF_MODEM_GNSS_AGPS_GPS_SYSTEM_CLOCK_AND_TOWS:
		(void)strcpy(type_string, "time");
		break;
	case NRF_MODEM_GNSS_AGPS_LOCATION:
		(void)strcpy(type_string, "pos");
		break;
	case NRF_MODEM_GNSS_AGPS_INTEGRITY:
		(void)strcpy(type_string, "int");
		break;
	default:
		(void)strcpy(type_string, "UNKNOWN");
		break;
	}
}

static void print_supl_data(void *agps, size_t agps_size, uint16_t type, char *type_string)
{
	union {
		struct nrf_modem_gnss_agps_data_utc utc_data;
		struct nrf_modem_gnss_agps_data_ephemeris ephemeris;
		struct nrf_modem_gnss_agps_data_almanac almanac;
		struct nrf_modem_gnss_agps_data_klobuchar klobuchar;
		struct nrf_modem_gnss_agps_data_nequick nequick;
		struct nrf_modem_gnss_agps_data_system_time_and_sv_tow tows;
		struct nrf_modem_gnss_agps_data_location agps;
		struct nrf_modem_gnss_agps_data_integrity integrity;
	} data;
	
	memcpy(&data, agps, agps_size);

	MLOG_INF("Injected A-GPS data, type: %s, size: %d", type_string, agps_size);
	
	switch (type) {
	case NRF_MODEM_GNSS_AGPS_UTC_PARAMETERS: {
		MLOG_INF("a1=%d,a0=%d,tot=%d,wn_t=%d,delta_tls=%d,wn_lsf=%d,dn=%d,delta_tlsf=%d",
			 data.utc_data.a1, data.utc_data.a0, data.utc_data.tot, data.utc_data.wn_t,
			 data.utc_data.delta_tls, data.utc_data.wn_lsf, data.utc_data.dn,
			 data.utc_data.delta_tlsf);
		break;
	}
	case NRF_MODEM_GNSS_AGPS_EPHEMERIDES:
		MLOG_INF(
			"sv_id=%d,health=%d,iodc=%d,toc=%d,af2=%d,af1=%d,af0=%d,tgd=%d,ura=%d,fit_int=%d,toe=%d,w=%d,delta_n=%d,m0=%d,omega_dot=%d,e=%d,idot=%d,sqrt_a=%d,i0=%d,omega=%d,crs=%d,cis=%d,cus=%d,crc=%d,cic=%d,cuc=%d",
			data.ephemeris.sv_id, data.ephemeris.health, data.ephemeris.iodc,
			data.ephemeris.toc, data.ephemeris.af2, data.ephemeris.af1,
			data.ephemeris.af0, data.ephemeris.tgd, data.ephemeris.ura,
			data.ephemeris.fit_int, data.ephemeris.toe, data.ephemeris.w,
			data.ephemeris.delta_n, data.ephemeris.m0, data.ephemeris.omega_dot,
			data.ephemeris.e, data.ephemeris.idot, data.ephemeris.sqrt_a,
			data.ephemeris.i0, data.ephemeris.omega0, data.ephemeris.crs,
			data.ephemeris.cis, data.ephemeris.cus, data.ephemeris.crc,
			data.ephemeris.cic, data.ephemeris.cuc);
		break;
	case NRF_MODEM_GNSS_AGPS_ALMANAC:
		MLOG_INF(
			"sv_id=%d,wn=%d,toa=%d,ioda=%d,e=%d,delta_i=%d,omega_dot=%d,sv_health=%d,sqrt_a=%d,omega0=%d,w=%d,m0=%d,af0=%d,af1=%d",
			data.almanac.sv_id, data.almanac.wn, data.almanac.toa, data.almanac.ioda,
			data.almanac.e, data.almanac.delta_i, data.almanac.omega_dot,
			data.almanac.sv_health, data.almanac.sqrt_a, data.almanac.omega0,
			data.almanac.w, data.almanac.m0, data.almanac.af0, data.almanac.af1);
		break;
	case NRF_MODEM_GNSS_AGPS_KLOBUCHAR_IONOSPHERIC_CORRECTION:
		MLOG_INF(
			"alpha0=%d,alpha1=%d,alpha2=%d,alpha3=%d,beta0=%d,beta1=%d,beta2=%d,beta3=%d,",
			data.klobuchar.alpha0, data.klobuchar.alpha1, data.klobuchar.alpha2,
			data.klobuchar.alpha3, data.klobuchar.beta0, data.klobuchar.beta1,
			data.klobuchar.beta2, data.klobuchar.beta3);
		break;
	case NRF_MODEM_GNSS_AGPS_NEQUICK_IONOSPHERIC_CORRECTION:
		MLOG_INF("ai0=%d,ai1=%d,ai2=%d,storm_cond=%d,storm_valid=%d", data.nequick.ai0,
			 data.nequick.ai1, data.nequick.ai2, data.nequick.storm_cond,
			 data.nequick.storm_valid);
		break;
	case NRF_MODEM_GNSS_AGPS_GPS_SYSTEM_CLOCK_AND_TOWS:
		MLOG_INF("date_day=%d,time_full_s=%d,time_frac_ms=%d,sv_mask=%d",
			 data.tows.date_day, data.tows.time_full_s, data.tows.time_frac_ms,
			 data.tows.sv_mask);
		for (int i = 0; i < 32; i++)
			MLOG_INF("\ttlm=%x,flags=%x", data.tows.sv_tow[i].tlm,
				 data.tows.sv_tow[i].flags);
		break;
	case NRF_MODEM_GNSS_AGPS_LOCATION:
		MLOG_INF(
			"latitude=%d,longitude=%d,altitude=%d,semimajor=%d,semiminor=%d,orientation_major=%d,unc_altitude=%d,confidence=%d",
			data.agps.latitude, data.agps.longitude, data.agps.altitude,
			data.agps.unc_semimajor, data.agps.unc_semiminor,
			data.agps.orientation_major, data.agps.unc_altitude, data.agps.confidence);
		break;
	case NRF_MODEM_GNSS_AGPS_INTEGRITY:
		MLOG_INF("integrity=%d", data.integrity.integrity_mask);
		break;
	default:
		break;
	}

}

static int inject_agps_type(void *agps, size_t agps_size, uint16_t type, void *user_data)
{
	ARG_UNUSED(user_data);

	int retval = nrf_modem_gnss_agps_write(agps, agps_size, type);
	char type_string[16];
	get_agps_data_type_string(type_string, type);
	if (retval != 0) {
		MLOG_ERR("Failed to write A-GPS data, type: %s (errno: %d)", type_string, errno);
		return -1;
	}

	print_supl_data(agps, agps_size, type, type_string);

	return 0;
}

static int supl_logger(int level, const char *fmt, ...)
{
	char buffer[256] = { 0 };
	va_list args;

	va_start(args, fmt);
	int ret = vsnprintk(buffer, sizeof(buffer), fmt, args);

	va_end(args);

	if (ret < 0) {
		MLOG_ERR("%s: encoding error", __func__);
		return ret;
	} else if ((size_t)ret >= sizeof(buffer)) {
		MLOG_WRN("%s: too long message,"
		       "it will be cut short", __func__);
	}

	MLOG_INF("%s", buffer);

	return ret;
}

static int open_supl_socket(void)
{
	int err;
	char port[6];
	struct addrinfo *info;

	struct addrinfo hints = {
		.ai_flags = AI_NUMERICSERV,
		.ai_family = AF_UNSPEC, /* Both IPv4 and IPv6 addresses accepted. */
		.ai_socktype = SOCK_STREAM
	};

	snprintf(port, sizeof(port), "%d", SUPL_SERVER_PORT);

	err = getaddrinfo(SUPL_SERVER, port, &hints, &info);
	if (err) {
		MLOG_ERR("Failed to resolve hostname %s, error: %d", SUPL_SERVER, err);

		return -1;
	}

	/* Not connected. */
	err = -1;

	for (struct addrinfo *addr = info; addr != NULL; addr = addr->ai_next) {
		char ip[INET6_ADDRSTRLEN] = { 0 };
		struct sockaddr *const sa = addr->ai_addr;

		supl_fd = socket(sa->sa_family, SOCK_STREAM, IPPROTO_TCP);
		if (supl_fd < 0) {
			MLOG_ERR("Failed to create socket, errno %d", errno);
			goto cleanup;
		}

		/* The SUPL library expects a 1 second timeout for the read function. */
		struct timeval timeout = {
			.tv_sec = 1,
			.tv_usec = 0,
		};

		err = setsockopt(supl_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
		if (err) {
			MLOG_ERR("Failed to set socket timeout, errno %d", errno);
			goto cleanup;
		}

		inet_ntop(sa->sa_family,
			  (void *)&((struct sockaddr_in *)sa)->sin_addr,
			  ip,
			  INET6_ADDRSTRLEN);
		MLOG_INF("Connecting to %s port %d", ip, SUPL_SERVER_PORT);

		err = connect(supl_fd, sa, addr->ai_addrlen);
		if (err) {
			close(supl_fd);
			supl_fd = -1;

			/* Try the next address. */
			MLOG_ERR("Connecting to server failed, errno %d", errno);
		} else {
			/* Connected. */
			break;
		}
	}

cleanup:
	freeaddrinfo(info);

	if (err) {
		/* Unable to connect, close socket. */
		MLOG_ERR("Could not connect to SUPL server");
		if (supl_fd > -1) {
			close(supl_fd);
			supl_fd = -1;
		}
		return -1;
	}

	return 0;
}

static void close_supl_socket(void)
{
	if (close(supl_fd) < 0) {
		MLOG_ERR("Failed to close SUPL socket");
	}
}

int assistance_init()
{
	static struct supl_api supl_api = {
		.read       = supl_read,
		.write      = supl_write,
		.handler    = inject_agps_type,
		.logger     = supl_logger,
		.counter_ms = k_uptime_get
	};

	if (supl_init(&supl_api) != 0) {
		MLOG_ERR("Failed to initialize SUPL library");
		return -1;
	}

//	ui_buzzer_buzz(500, 500, 100);
	return 0;
}

int assistance_request(struct nrf_modem_gnss_agps_data_frame *agps_request)
{
	int err;

	assistance_active = true;

	err = open_supl_socket();
	if (err) {
		goto exit;
	}

	MLOG_INF("Starting SUPL session");
	err = supl_session(agps_request);
	close_supl_socket();

exit:
	assistance_active = false;
//	ui_buzzer_buzz(1000, 200, 100);

	return err;
}

bool assistance_is_active(void)
{
	return assistance_active;
}
