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

#include <zephyr/kernel.h>
#include <stdio.h>
#include <math.h>
#include <date_time.h>
#include <app_event_manager.h>
#include <modem/location.h>
#include <modem/modem_info.h>
#include <nrf_modem_gnss.h>

#define MODULE location_module

#include "modules_common.h"
#include "events/app_module_event.h"
#include "events/location_module_event.h"
#include "events/data_module_event.h"
#include "events/util_module_event.h"
#include "events/modem_module_event.h"

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, CONFIG_LOCATION_MODULE_LOG_LEVEL);
#include "minilog.h"

BUILD_ASSERT(CONFIG_AT_MONITOR_HEAP_SIZE >= 1024,
	    "CONFIG_AT_MONITOR_HEAP_SIZE must be >= 1024 to fit neighbor cell measurements "
	    "and other notifications at the same time");

/* Use a timeout of 90 seconds for GNSS search. */
#define DATA_FETCH_TIMEOUT_GNSS_SEARCH 90

/* Use a timeout of 11 seconds to accommodate for neighbour cell measurements
 * that can take up to 10.24 seconds.
 */
#define DATA_FETCH_TIMEOUT_NEIGHBORHOOD_SEARCH 11

struct location_msg_data {
	union {
		struct app_module_event app;
		struct data_module_event data;
		struct util_module_event util;
		struct modem_module_event modem;
		struct location_module_event location;
	} module;
};

/* Location module super states. */
static enum state_type {
	STATE_INIT,
	STATE_RUNNING,
	STATE_SHUTDOWN
} state;

/* Location module sub states. */
static enum sub_state_type {
	SUB_STATE_IDLE,
	SUB_STATE_SEARCH
} sub_state;

static struct nrf_modem_gnss_pvt_data_frame pvt_data;

/* Local copy of the device configuration. */
static struct cloud_data_cfg copy_cfg;

static struct module_stats {
	/* Uptime set when location search is started. Used to calculate search time for
	 * location request and timeout.
	 */
	uint64_t start_uptime;
	/* Uptime set when GNSS search is started. Used to calculate search time for
	 * GNSS fix and timeout.
	 */
	uint32_t search_time;
	/* Number of satellites tracked at the time of a GNSS fix or a timeout. */
	uint8_t satellites_tracked;
} stats;

static struct module_data self = {
	.name = "location",
	.msg_q = NULL,
	.supports_shutdown = true,
};

/* Forward declarations. */
static void message_handler(struct location_msg_data *data);
static void search_start(void);
static void inactive_send(void);
static void time_set(void);
static void data_send_pvt(void);

/* Convenience functions used in internal state handling. */
static char *state2str(enum state_type new_state)
{
	switch (new_state) {
	case STATE_INIT:
		return "STATE_INIT";
	case STATE_RUNNING:
		return "STATE_RUNNING";
	case STATE_SHUTDOWN:
		return "STATE_SHUTDOWN";
	default:
		return "Unknown";
	}
}

static char *sub_state2str(enum sub_state_type new_state)
{
	switch (new_state) {
	case SUB_STATE_IDLE:
		return "SUB_STATE_IDLE";
	case SUB_STATE_SEARCH:
		return "SUB_STATE_SEARCH";
	default:
		return "Unknown";
	}
}

static void state_set(enum state_type new_state)
{
	if (new_state == state) {
		MLOG_DBG("State: %s", state2str(state));
		return;
	}

	MLOG_DBG("State transition %s --> %s",
		state2str(state),
		state2str(new_state));

	state = new_state;
}

static void sub_state_set(enum sub_state_type new_state)
{
	if (new_state == sub_state) {
		MLOG_DBG("Sub state: %s", sub_state2str(sub_state));
		return;
	}

	MLOG_DBG("Sub state transition %s --> %s",
		sub_state2str(sub_state),
		sub_state2str(new_state));

	sub_state = new_state;
}

/* Handlers */
static bool app_event_handler(const struct app_event_header *aeh)
{
	if (is_app_module_event(aeh)) {
		struct app_module_event *event = cast_app_module_event(aeh);
		struct location_msg_data msg = {
			.module.app = *event
		};

		message_handler(&msg);
	}

	if (is_data_module_event(aeh)) {
		struct data_module_event *event = cast_data_module_event(aeh);
		struct location_msg_data msg = {
			.module.data = *event
		};

		message_handler(&msg);
	}

	if (is_util_module_event(aeh)) {
		struct util_module_event *event = cast_util_module_event(aeh);
		struct location_msg_data msg = {
			.module.util = *event
		};

		message_handler(&msg);
	}

	if (is_location_module_event(aeh)) {
		struct location_module_event *event = cast_location_module_event(aeh);
		struct location_msg_data msg = {
			.module.location = *event
		};

		message_handler(&msg);
	}

	if (is_modem_module_event(aeh)) {
		struct modem_module_event *event = cast_modem_module_event(aeh);
		struct location_msg_data msg = {
			.module.modem = *event
		};

		message_handler(&msg);
	}
	return false;
}

static void timeout_send(void)
{
	struct location_module_event *location_module_event = new_location_module_event();

	location_module_event->data.location.search_time = stats.search_time;
	location_module_event->data.location.satellites_tracked = stats.satellites_tracked;
	location_module_event->type = LOCATION_MODULE_EVT_TIMEOUT;

	APP_EVENT_SUBMIT(location_module_event);
}

static void data_send_pvt(void)
{
	struct location_module_event *location_module_event = new_location_module_event();

	location_module_event->data.location.pvt.longitude = pvt_data.longitude;
	location_module_event->data.location.pvt.latitude = pvt_data.latitude;
	location_module_event->data.location.pvt.altitude = pvt_data.altitude;
	location_module_event->data.location.pvt.accuracy = pvt_data.accuracy;
	location_module_event->data.location.pvt.speed = pvt_data.speed;
	location_module_event->data.location.pvt.heading = pvt_data.heading;
	location_module_event->data.location.timestamp = k_uptime_get();
	location_module_event->type = LOCATION_MODULE_EVT_GNSS_DATA_READY;

	location_module_event->data.location.satellites_tracked = stats.satellites_tracked;
	location_module_event->data.location.search_time =
		(uint32_t)(location_module_event->data.location.timestamp - stats.start_uptime);

	APP_EVENT_SUBMIT(location_module_event);
}

static void search_start(void)
{
	int err;
	struct location_config config;
	enum location_method methods[2];
	int methods_count = 0;
	int methods_index_gnss = -1;
	int methods_index_cellular = -1;

	if (copy_cfg.no_data.neighbor_cell && copy_cfg.no_data.gnss) {
		SEND_EVENT(location, LOCATION_MODULE_EVT_DATA_NOT_READY);
		MLOG_ERR("Both GNSS and cellular are configured off");
		return;
	}

	if (!copy_cfg.no_data.gnss) {
		methods[methods_count] = LOCATION_METHOD_GNSS;
		methods_index_gnss = methods_count;
		methods_count++;
	}

	if (!copy_cfg.no_data.neighbor_cell) {
		methods[methods_count] = LOCATION_METHOD_CELLULAR;
		methods_index_cellular = methods_count;
		methods_count++;
	}

	location_config_defaults_set(&config, methods_count, methods);

	config.timeout = copy_cfg.location_timeout * MSEC_PER_SEC;

	if (methods_index_gnss != -1) {
		config.methods[methods_index_gnss].gnss.timeout =
			DATA_FETCH_TIMEOUT_GNSS_SEARCH * MSEC_PER_SEC;
	}

	if (methods_index_cellular != -1) {
		config.methods[methods_index_cellular].cellular.timeout =
			DATA_FETCH_TIMEOUT_NEIGHBORHOOD_SEARCH * MSEC_PER_SEC;
	}

	MLOG_INF("Requesting location...");

	err = location_request(&config);
	if (err) {
		SEND_EVENT(location, LOCATION_MODULE_EVT_DATA_NOT_READY);
		MLOG_ERR("Location request failed: %d", err);
		return;
	}

	SEND_EVENT(location, LOCATION_MODULE_EVT_ACTIVE);
	stats.start_uptime = k_uptime_get();
}

static void inactive_send(void)
{
	SEND_EVENT(location, LOCATION_MODULE_EVT_INACTIVE);
}

static void time_set(void)
{
	/* Change datetime.year and datetime.month to accommodate the
	 * correct input format.
	 */
	struct tm gnss_time = {
		.tm_year = pvt_data.datetime.year - 1900,
		.tm_mon = pvt_data.datetime.month - 1,
		.tm_mday = pvt_data.datetime.day,
		.tm_hour = pvt_data.datetime.hour,
		.tm_min = pvt_data.datetime.minute,
		.tm_sec = pvt_data.datetime.seconds,
	};

	date_time_set(&gnss_time);
}

static inline int adjust_rsrp(int input)
{
	if (IS_ENABLED(CONFIG_LOCATION_MODULE_NEIGHBOR_CELLS_DATA_CONVERT_RSRP_TO_DBM)) {
		return RSRP_IDX_TO_DBM(input);
	}

	return input;
}

static inline int adjust_rsrq(int input)
{
	if (IS_ENABLED(CONFIG_LOCATION_MODULE_NEIGHBOR_CELLS_DATA_CONVERT_RSRQ_TO_DB)) {
		return round(RSRQ_IDX_TO_DB(input));
	}

	return input;
}

static void send_neighbor_cell_update(struct lte_lc_cells_info *cell_info)
{
	struct location_module_event *evt = new_location_module_event();

	BUILD_ASSERT(sizeof(evt->data.neighbor_cells.cell_data) ==
		     sizeof(struct lte_lc_cells_info));
	BUILD_ASSERT(sizeof(evt->data.neighbor_cells.neighbor_cells) >=
		     sizeof(struct lte_lc_ncell) * CONFIG_LTE_NEIGHBOR_CELLS_MAX);

	memcpy(&evt->data.neighbor_cells.cell_data, cell_info, sizeof(struct lte_lc_cells_info));
	memcpy(&evt->data.neighbor_cells.neighbor_cells, cell_info->neighbor_cells,
	       sizeof(struct lte_lc_ncell) * evt->data.neighbor_cells.cell_data.ncells_count);

	/* Convert RSRP to dBm and RSRQ to dB per "nRF91 AT Commands" v1.7. */
	evt->data.neighbor_cells.cell_data.current_cell.rsrp =
			adjust_rsrp(evt->data.neighbor_cells.cell_data.current_cell.rsrp);
	evt->data.neighbor_cells.cell_data.current_cell.rsrq =
			adjust_rsrq(evt->data.neighbor_cells.cell_data.current_cell.rsrq);

	for (size_t i = 0; i < evt->data.neighbor_cells.cell_data.ncells_count; i++) {
		evt->data.neighbor_cells.neighbor_cells[i].rsrp =
			adjust_rsrp(evt->data.neighbor_cells.neighbor_cells[i].rsrp);
		evt->data.neighbor_cells.neighbor_cells[i].rsrq =
			adjust_rsrq(evt->data.neighbor_cells.neighbor_cells[i].rsrq);
	}

	evt->type = LOCATION_MODULE_EVT_NEIGHBOR_CELLS_DATA_READY;
	evt->data.neighbor_cells.timestamp = k_uptime_get();

	APP_EVENT_SUBMIT(evt);
}

/* Non-static so that this can be used in tests to mock location library API. */
void location_event_handler(const struct location_event_data *event_data)
{
	switch (event_data->id) {
	case LOCATION_EVT_LOCATION:
		MLOG_DBG("Got location:");
		MLOG_DBG("  method: %s", location_method_str(event_data->method));
		MLOG_DBG("  latitude: %.06f", event_data->location.latitude);
		MLOG_DBG("  longitude: %.06f", event_data->location.longitude);
		MLOG_DBG("  accuracy: %.01f m", event_data->location.accuracy);
		MLOG_DBG("  altitude: %.01f m", event_data->location.details.gnss.pvt_data.altitude);
		MLOG_DBG("  speed: %.01f m", event_data->location.details.gnss.pvt_data.speed);
		MLOG_DBG("  heading: %.01f deg", event_data->location.details.gnss.pvt_data.heading);

		if (event_data->location.datetime.valid) {
			MLOG_DBG("  date: %04d-%02d-%02d",
				event_data->location.datetime.year,
				event_data->location.datetime.month,
				event_data->location.datetime.day);
			MLOG_DBG("  time: %02d:%02d:%02d.%03d UTC",
				event_data->location.datetime.hour,
				event_data->location.datetime.minute,
				event_data->location.datetime.second,
				event_data->location.datetime.ms);
		}

		stats.search_time = (uint32_t)(k_uptime_get() - stats.start_uptime);
		MLOG_DBG("  search time: %d ms", stats.search_time);

		/* Only GNSS result is handled as cellular is handled
		 * as part of LOCATION_EVT_CELLULAR_EXT_REQUEST
		 */
		stats.satellites_tracked = 0;
		if (event_data->method == LOCATION_METHOD_GNSS) {
			pvt_data = event_data->location.details.gnss.pvt_data;
			stats.satellites_tracked =
				event_data->location.details.gnss.satellites_tracked;
			MLOG_DBG("  satellites tracked: %d", stats.satellites_tracked);

			if (event_data->location.datetime.valid) {
				/* Date and time is in pvt_data that is set above */
				time_set();
			}
			data_send_pvt();
		}
		MLOG_DBG("  Google maps URL: https://maps.google.com/?q=%.06f,%.06f",
			event_data->location.latitude, event_data->location.longitude);

		inactive_send();
		break;

	case LOCATION_EVT_RESULT_UNKNOWN:
		MLOG_DBG("Getting location completed with undefined result");
		stats.search_time = (uint32_t)(k_uptime_get() - stats.start_uptime);
		MLOG_DBG("  search time: %d", stats.search_time);
		inactive_send();
		/* No events are sent because LOCATION_MODULE_EVT_NEIGHBOR_CELLS_DATA_READY
		 * has already been sent earlier and hence APP_LOCATION has been set already.
		 */
		break;

	case LOCATION_EVT_TIMEOUT:
		MLOG_DBG("Getting location timed out");
		stats.search_time = (uint32_t)(k_uptime_get() - stats.start_uptime);
		MLOG_DBG("  search time: %d", stats.search_time);

		stats.satellites_tracked = 0;
		if (event_data->method == LOCATION_METHOD_GNSS) {
			stats.satellites_tracked =
				event_data->error.details.gnss.satellites_tracked;
			MLOG_DBG("  satellites tracked: %d", stats.satellites_tracked);
		}

		timeout_send();
		inactive_send();
		break;

	case LOCATION_EVT_ERROR:
		MLOG_WRN("Getting location failed");
		SEND_EVENT(location, LOCATION_MODULE_EVT_DATA_NOT_READY);
		inactive_send();
		break;

	case LOCATION_EVT_GNSS_ASSISTANCE_REQUEST: {
		MLOG_DBG("Requested A-GPS data");
#if defined(CONFIG_LOCATION_METHOD_GNSS_AGPS_EXTERNAL)
		struct location_module_event *location_module_event = new_location_module_event();

		location_module_event->data.agps_request = event_data->agps_request;
		location_module_event->type = LOCATION_MODULE_EVT_AGPS_NEEDED;
		APP_EVENT_SUBMIT(location_module_event);
#endif
		break;
	}

	case LOCATION_EVT_GNSS_PREDICTION_REQUEST: {
		MLOG_DBG("Requested P-GPS data");
#if defined(CONFIG_LOCATION_METHOD_GNSS_PGPS_EXTERNAL)
		struct location_module_event *location_module_event = new_location_module_event();

		location_module_event->data.pgps_request = event_data->pgps_request;
		location_module_event->type = LOCATION_MODULE_EVT_PGPS_NEEDED;
		APP_EVENT_SUBMIT(location_module_event);
#endif
		break;
	}

	case LOCATION_EVT_CELLULAR_EXT_REQUEST:
		MLOG_DBG("Getting cellular request");
		send_neighbor_cell_update(
			(struct lte_lc_cells_info *)&event_data->cellular_request);
		location_cellular_ext_result_set(LOCATION_CELLULAR_EXT_RESULT_UNKNOWN, NULL);
		break;

	default:
		MLOG_DBG("Getting location: Unknown event %d", event_data->id);
		break;
	}
}

static bool location_data_requested(enum app_module_data_type *data_list, size_t count)
{
	for (size_t i = 0; i < count; i++) {
		if (data_list[i] == APP_DATA_LOCATION) {
			return true;
		}
	}

	return false;
}

static int setup(void)
{
	int err;

	err = location_init(location_event_handler);
	if (err) {
		MLOG_ERR("Initializing the Location library failed, error: %d", err);
		return -1;
	}

	return 0;
}

/* Message handler for STATE_INIT. */
static void on_state_init(struct location_msg_data *msg)
{
	if (IS_EVENT(msg, modem, MODEM_EVT_INITIALIZED)) {
		int err;

		err = setup();
		if (err) {
			MLOG_ERR("setup, error: %d", err);
			SEND_ERROR(location, LOCATION_MODULE_EVT_ERROR_CODE, err);
		}

		state_set(STATE_RUNNING);
	}
}

/* Message handler for SUB_STATE_SEARCH. */
static void on_state_running_location_search(struct location_msg_data *msg)
{
	if (IS_EVENT(msg, location, LOCATION_MODULE_EVT_INACTIVE)) {
		sub_state_set(SUB_STATE_IDLE);
	}

	if (IS_EVENT(msg, app, APP_EVT_DATA_GET)) {
		if (!location_data_requested(msg->module.app.data_list, msg->module.app.count)) {
			return;
		}

		MLOG_DBG("Location request already active and will not be restarted");
		MLOG_DBG("Seeing this message sometimes is normal, especially, "
			"when trying to acquire the first GNSS fix.");
		MLOG_DBG("If you see this often for other than the first location search, "
			"try setting the sample/publication interval a bit greater.");
	}
}

/* Message handler for SUB_STATE_IDLE. */
static void on_state_running_location_idle(struct location_msg_data *msg)
{
	if (IS_EVENT(msg, location, LOCATION_MODULE_EVT_ACTIVE)) {
		sub_state_set(SUB_STATE_SEARCH);
	}

	if (IS_EVENT(msg, app, APP_EVT_DATA_GET)) {
		if (!location_data_requested(msg->module.app.data_list, msg->module.app.count)) {
			return;
		}

		search_start();
	}
}

/* Message handler for all states. */
static void on_all_states(struct location_msg_data *msg)
{
	if (IS_EVENT(msg, app, APP_EVT_START)) {
		int err;

		err = module_start(&self);
		if (err) {
			MLOG_ERR("Failed starting module, error: %d", err);
			SEND_ERROR(location, LOCATION_MODULE_EVT_ERROR_CODE, err);
		}

		state_set(STATE_INIT);
	}

	if (IS_EVENT(msg, util, UTIL_EVT_SHUTDOWN_REQUEST)) {
		/* The module doesn't have anything to shut down and can
		 * report back immediately.
		 */
		SEND_SHUTDOWN_ACK(location, LOCATION_MODULE_EVT_SHUTDOWN_READY, self.id);
		state_set(STATE_SHUTDOWN);
	}

	if ((IS_EVENT(msg, data, DATA_EVT_CONFIG_INIT)) ||
	    (IS_EVENT(msg, data, DATA_EVT_CONFIG_READY))) {
		copy_cfg = msg->module.data.data.cfg;
	}
}

static void message_handler(struct location_msg_data *msg)
{
	switch (state) {
	case STATE_INIT:
		on_state_init(msg);
		break;
	case STATE_RUNNING:
		switch (sub_state) {
		case SUB_STATE_SEARCH:
			on_state_running_location_search(msg);
			break;
		case SUB_STATE_IDLE:
			on_state_running_location_idle(msg);
			break;
		default:
			MLOG_ERR("Unknown sub state.");
			break;
		}
		break;
	case STATE_SHUTDOWN:
		/* The shutdown state has no transition. */
		break;
	default:
		MLOG_ERR("Unknown state.");
		break;
	}

	on_all_states(msg);
}

APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, app_module_event);
APP_EVENT_SUBSCRIBE(MODULE, data_module_event);
APP_EVENT_SUBSCRIBE(MODULE, util_module_event);
APP_EVENT_SUBSCRIBE(MODULE, modem_module_event);
APP_EVENT_SUBSCRIBE(MODULE, location_module_event);
