/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2011  Nokia Corporation
 *  Copyright (C) 2011  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <gdbus/gdbus.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <glib.h>

#include <bluetooth/bluetooth.h>

#include "lib/uuid.h"
#include "dbus-common.h"
#include "adapter.h"
#include "device.h"
#include "error.h"
#include "log.h"
#include "attrib/att.h"
#include "attrib/gattrib.h"
#include "attrib/gatt.h"
#include "attio.h"
#include "monitor.h"
#include "textfile.h"

#define PROXIMITY_INTERFACE "org.bluez.ProximityMonitor1"

#define ALERT_LEVEL_CHR_UUID 0x2A06
#define POWER_LEVEL_CHR_UUID 0x2A07

#define IMMEDIATE_TIMEOUT	5
#define TX_POWER_SIZE		1

enum {
	ALERT_NONE = 0,
	ALERT_MILD,
	ALERT_HIGH,
};

struct monitor {
	struct btd_device *device;
	GAttrib *attrib;
	struct att_range *linkloss;
	struct att_range *txpower;
	struct att_range *immediate;
	struct enabled enabled;
	char *linklosslevel;		/* Link Loss Alert Level */
	char *fallbacklevel;		/* Immediate fallback alert level */
	char *immediatelevel;		/* Immediate Alert Level */
	char *signallevel;		/* Path Loss RSSI level */
	uint16_t linklosshandle;	/* Link Loss Characteristic
					 * Value Handle */
	uint16_t txpowerhandle;		/* Tx Characteristic Value Handle */
	uint16_t immediatehandle;	/* Immediate Alert Value Handle */
	guint immediateto;		/* Reset Immediate Alert to "none" */
	guint attioid;
};

static GSList *monitors = NULL;

static struct monitor *find_monitor(struct btd_device *device)
{
	GSList *l;

	for (l = monitors; l; l = l->next) {
		struct monitor *monitor = l->data;

		if (monitor->device == device)
			return monitor;
	}

	return NULL;
}

static void write_proximity_config(struct btd_device *device, const char *alert,
					const char *level)
{
	char *filename;
	GKeyFile *key_file;
	char *data;
	gsize length = 0;

	filename = btd_device_get_storage_path(device, "proximity");
	if (!filename) {
		warn("Unable to get proximity storage path for device");
		return;
	}

	key_file = g_key_file_new();
	g_key_file_load_from_file(key_file, filename, 0, NULL);

	if (level)
		g_key_file_set_string(key_file, alert, "Level", level);
	else
		g_key_file_remove_group(key_file, alert, NULL);

	data = g_key_file_to_data(key_file, &length, NULL);
	if (length > 0) {
		create_file(filename, S_IRUSR | S_IWUSR);
		g_file_set_contents(filename, data, length, NULL);
	}

	g_free(data);
	g_free(filename);
	g_key_file_free(key_file);
}

static char *read_proximity_config(struct btd_device *device, const char *alert)
{
	char *filename;
	GKeyFile *key_file;
	char *str;

	filename = btd_device_get_storage_path(device, "proximity");
	if (!filename) {
		warn("Unable to get proximity storage path for device");
		return NULL;
	}

	key_file = g_key_file_new();
	g_key_file_load_from_file(key_file, filename, 0, NULL);

	str = g_key_file_get_string(key_file, alert, "Level", NULL);

	g_free(filename);
	g_key_file_free(key_file);

	return str;
}

static uint8_t str2level(const char *level)
{
	if (g_strcmp0("high", level) == 0)
		return ALERT_HIGH;
	else if (g_strcmp0("mild", level) == 0)
		return ALERT_MILD;

	return ALERT_NONE;
}

static void linkloss_written(guint8 status, const guint8 *pdu, guint16 plen,
							gpointer user_data)
{
	struct monitor *monitor = user_data;
	struct btd_device *device = monitor->device;
	const char *path = device_get_path(device);

	if (status != 0) {
		error("Link Loss Write Request failed: %s",
							att_ecode2str(status));
		return;
	}

	if (!dec_write_resp(pdu, plen)) {
		error("Link Loss Write Request: protocol error");
		return;
	}

	DBG("Link Loss Alert Level written");

	g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
				PROXIMITY_INTERFACE, "LinkLossAlertLevel");
}

static void char_discovered_cb(GSList *characteristics, guint8 status,
							gpointer user_data)
{
	struct monitor *monitor = user_data;
	struct gatt_char *chr;
	uint8_t value = str2level(monitor->linklosslevel);

	if (status) {
		error("Discover Link Loss handle: %s", att_ecode2str(status));
		return;
	}

	DBG("Setting alert level \"%s\" on Reporter", monitor->linklosslevel);

	/* Assume there is a single Alert Level characteristic */
	chr = characteristics->data;
	monitor->linklosshandle = chr->value_handle;

	gatt_write_char(monitor->attrib, monitor->linklosshandle, &value, 1,
						linkloss_written, monitor);
}

static int write_alert_level(struct monitor *monitor)
{
	struct att_range *linkloss = monitor->linkloss;
	bt_uuid_t uuid;

	if (monitor->linklosshandle) {
		uint8_t value = str2level(monitor->linklosslevel);

		gatt_write_char(monitor->attrib, monitor->linklosshandle,
					&value, 1, linkloss_written, monitor);
		return 0;
	}

	bt_uuid16_create(&uuid, ALERT_LEVEL_CHR_UUID);

	/* FIXME: use cache (requires service changed support) ? */
	gatt_discover_char(monitor->attrib, linkloss->start, linkloss->end,
					&uuid, char_discovered_cb, monitor);

	return 0;
}

static void tx_power_read_cb(guint8 status, const guint8 *pdu, guint16 plen,
							gpointer user_data)
{
	uint8_t value[TX_POWER_SIZE];
	ssize_t vlen;

	if (status != 0) {
		DBG("Tx Power Level read failed: %s", att_ecode2str(status));
		return;
	}

	vlen = dec_read_resp(pdu, plen, value, sizeof(value));
	if (vlen < 0) {
		DBG("Protocol error");
		return;
	}

	if (vlen != 1) {
		DBG("Invalid length for TX Power value: %zd", vlen);
		return;
	}

	DBG("Tx Power Level: %02x", (int8_t) value[0]);
}

static void tx_power_handle_cb(GSList *characteristics, guint8 status,
							gpointer user_data)
{
	struct monitor *monitor = user_data;
	struct gatt_char *chr;

	if (status) {
		error("Discover Tx Power handle: %s", att_ecode2str(status));
		return;
	}

	chr = characteristics->data;
	monitor->txpowerhandle = chr->value_handle;

	DBG("Tx Power handle: 0x%04x", monitor->txpowerhandle);

	gatt_read_char(monitor->attrib, monitor->txpowerhandle,
							tx_power_read_cb, monitor);
}

static void read_tx_power(struct monitor *monitor)
{
	struct att_range *txpower = monitor->txpower;
	bt_uuid_t uuid;

	if (monitor->txpowerhandle != 0) {
		gatt_read_char(monitor->attrib, monitor->txpowerhandle,
						tx_power_read_cb, monitor);
		return;
	}

	bt_uuid16_create(&uuid, POWER_LEVEL_CHR_UUID);

	gatt_discover_char(monitor->attrib, txpower->start, txpower->end,
				&uuid, tx_power_handle_cb, monitor);
}

static gboolean immediate_timeout(gpointer user_data)
{
	struct monitor *monitor = user_data;
	const char *path = device_get_path(monitor->device);

	monitor->immediateto = 0;

	if (g_strcmp0(monitor->immediatelevel, "none") == 0)
		return FALSE;

	if (monitor->attrib) {
		uint8_t value = ALERT_NONE;
		gatt_write_cmd(monitor->attrib, monitor->immediatehandle,
				&value, 1, NULL, NULL);
	}

	g_free(monitor->immediatelevel);
	monitor->immediatelevel = g_strdup("none");


	g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
				PROXIMITY_INTERFACE, "ImmediateAlertLevel");

	return FALSE;
}

static void immediate_written(gpointer user_data)
{
	struct monitor *monitor = user_data;
	const char *path = device_get_path(monitor->device);

	g_free(monitor->fallbacklevel);
	monitor->fallbacklevel = NULL;


	g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
				PROXIMITY_INTERFACE, "ImmediateAlertLevel");

	monitor->immediateto = g_timeout_add_seconds(IMMEDIATE_TIMEOUT,
						immediate_timeout, monitor);
}

static void write_immediate_alert(struct monitor *monitor)
{
	uint8_t value = str2level(monitor->immediatelevel);

	gatt_write_cmd(monitor->attrib, monitor->immediatehandle, &value, 1,
						immediate_written, monitor);
}

static void immediate_handle_cb(GSList *characteristics, guint8 status,
							gpointer user_data)
{
	struct monitor *monitor = user_data;
	struct gatt_char *chr;

	if (status) {
		error("Discover Immediate Alert handle: %s",
						att_ecode2str(status));
		return;
	}

	chr = characteristics->data;
	monitor->immediatehandle = chr->value_handle;

	DBG("Immediate Alert handle: 0x%04x", monitor->immediatehandle);

	if (monitor->fallbacklevel)
		write_immediate_alert(monitor);
}

static void discover_immediate_handle(struct monitor *monitor)
{
	struct att_range *immediate = monitor->immediate;
	bt_uuid_t uuid;

	bt_uuid16_create(&uuid, ALERT_LEVEL_CHR_UUID);

	gatt_discover_char(monitor->attrib, immediate->start, immediate->end,
					&uuid, immediate_handle_cb, monitor);
}

static void attio_connected_cb(GAttrib *attrib, gpointer user_data)
{
	struct monitor *monitor = user_data;

	monitor->attrib = g_attrib_ref(attrib);

	if (monitor->enabled.linkloss)
		write_alert_level(monitor);

	if (monitor->enabled.pathloss)
		read_tx_power(monitor);

	if (monitor->immediatehandle == 0) {
		if(monitor->enabled.pathloss || monitor->enabled.findme)
			discover_immediate_handle(monitor);
	} else if (monitor->fallbacklevel)
		write_immediate_alert(monitor);
}

static void attio_disconnected_cb(gpointer user_data)
{
	struct monitor *monitor = user_data;
	const char *path = device_get_path(monitor->device);

	g_attrib_unref(monitor->attrib);
	monitor->attrib = NULL;

	if (monitor->immediateto == 0)
		return;

	g_source_remove(monitor->immediateto);
	monitor->immediateto = 0;

	if (g_strcmp0(monitor->immediatelevel, "none") == 0)
		return;

	g_free(monitor->immediatelevel);
	monitor->immediatelevel = g_strdup("none");

	g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
				PROXIMITY_INTERFACE, "ImmediateAlertLevel");
}

static gboolean level_is_valid(const char *level)
{
	return (g_str_equal("none", level) ||
			g_str_equal("mild", level) ||
			g_str_equal("high", level));
}

static gboolean property_get_link_loss_level(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct monitor *monitor = data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
						&monitor->linklosslevel);

	return TRUE;
}

static void property_set_link_loss_level(const GDBusPropertyTable *property,
		DBusMessageIter *iter, GDBusPendingPropertySet id, void *data)
{
	struct monitor *monitor = data;
	const char *level;

	if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	dbus_message_iter_get_basic(iter, &level);

	if (!level_is_valid(level)) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	if (g_strcmp0(monitor->linklosslevel, level) == 0)
		goto done;

	g_free(monitor->linklosslevel);
	monitor->linklosslevel = g_strdup(level);

	write_proximity_config(monitor->device, "LinkLossAlertLevel", level);

	if (monitor->attrib)
		write_alert_level(monitor);

done:
	g_dbus_pending_property_success(id);
}

static gboolean property_exists_link_loss_level(
				const GDBusPropertyTable *property, void *data)
{
	struct monitor *monitor = data;

	if (!monitor->enabled.linkloss)
		return FALSE;

	return TRUE;
}

static gboolean property_get_immediate_alert_level(
					const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct monitor *monitor = data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
						&monitor->immediatelevel);

	return TRUE;
}

static void property_set_immediate_alert_level(
		const GDBusPropertyTable *property, DBusMessageIter *iter,
		GDBusPendingPropertySet id, void *data)
{
	struct monitor *monitor = data;
	struct btd_device *device = monitor->device;
	const char *level;

	if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	dbus_message_iter_get_basic(iter, &level);

	if (!level_is_valid(level)) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	if (g_strcmp0(monitor->immediatelevel, level) == 0)
		goto done;

	if (monitor->immediateto) {
		g_source_remove(monitor->immediateto);
		monitor->immediateto = 0;
	}

	/* Previous Immediate Alert level if connection/write fails */
	g_free(monitor->fallbacklevel);
	monitor->fallbacklevel = monitor->immediatelevel;

	monitor->immediatelevel = g_strdup(level);

	/*
	 * Means that Link/Path Loss are disabled or there is a pending
	 * writting for Find Me(Immediate Alert characteristic value).
	 * If enabled, Path Loss always registers a connection callback
	 * when the Proximity Monitor starts.
	 */
	if (monitor->attioid == 0)
		monitor->attioid = btd_device_add_attio_callback(device,
							attio_connected_cb,
							attio_disconnected_cb,
							monitor);
	else if (monitor->attrib)
		write_immediate_alert(monitor);

done:
	g_dbus_pending_property_success(id);
}

static gboolean property_exists_immediate_alert_level(
				const GDBusPropertyTable *property, void *data)
{
	struct monitor *monitor = data;

	if (!(monitor->enabled.findme || monitor->enabled.pathloss))
		return FALSE;

	return TRUE;
}

static gboolean property_get_signal_level(
					const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct monitor *monitor = data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
						&monitor->signallevel);

	return TRUE;
}

static gboolean property_exists_signal_level(const GDBusPropertyTable *property,
								void *data)
{
	struct monitor *monitor = data;

	if (!monitor->enabled.pathloss)
		return FALSE;

	return TRUE;
}

static const GDBusPropertyTable monitor_device_properties[] = {
	{ "LinkLossAlertLevel", "s", property_get_link_loss_level,
					property_set_link_loss_level,
					property_exists_link_loss_level },
	{ "ImmediateAlertLevel", "s", property_get_immediate_alert_level,
					property_set_immediate_alert_level,
					property_exists_immediate_alert_level },
	{ "SignalLevel", "s", property_get_signal_level, NULL,
					property_exists_signal_level },
	{ }
};

static void monitor_destroy(gpointer user_data)
{
	struct monitor *monitor = user_data;

	btd_device_unref(monitor->device);
	g_free(monitor->linklosslevel);
	g_free(monitor->immediatelevel);
	g_free(monitor->signallevel);
	g_free(monitor);

	monitors = g_slist_remove(monitors, monitor);
}

static struct monitor *register_monitor(struct btd_device *device)
{
	const char *path = device_get_path(device);
	struct monitor *monitor;
	char *level;

	monitor = find_monitor(device);
	if (monitor != NULL)
		return monitor;

	level = read_proximity_config(device, "LinkLossAlertLevel");

	monitor = g_new0(struct monitor, 1);
	monitor->device = btd_device_ref(device);
	monitor->linklosslevel = (level ? : g_strdup("high"));
	monitor->signallevel = g_strdup("unknown");
	monitor->immediatelevel = g_strdup("none");

	monitors = g_slist_append(monitors, monitor);

	if (g_dbus_register_interface(btd_get_dbus_connection(), path,
				PROXIMITY_INTERFACE,
				NULL, NULL, monitor_device_properties,
				monitor, monitor_destroy) == FALSE) {
		error("D-Bus failed to register %s interface",
						PROXIMITY_INTERFACE);
		monitor_destroy(monitor);
		return NULL;
	}

	DBG("Registered interface %s on path %s", PROXIMITY_INTERFACE, path);

	return monitor;
}

static void update_monitor(struct monitor *monitor)
{
	if (monitor->txpower != NULL && monitor->immediate != NULL)
		monitor->enabled.pathloss = TRUE;
	else
		monitor->enabled.pathloss = FALSE;

	DBG("Link Loss: %s, Path Loss: %s, FindMe: %s",
				monitor->enabled.linkloss ? "TRUE" : "FALSE",
				monitor->enabled.pathloss ? "TRUE" : "FALSE",
				monitor->enabled.findme ? "TRUE" : "FALSE");

	if (!monitor->enabled.linkloss && !monitor->enabled.pathloss)
		return;

	if (monitor->attioid != 0)
		return;

	monitor->attioid = btd_device_add_attio_callback(monitor->device,
							attio_connected_cb,
							attio_disconnected_cb,
							monitor);
}

int monitor_register_linkloss(struct btd_device *device,
						struct enabled *enabled,
						struct gatt_primary *linkloss)
{
	struct monitor *monitor;

	if (!enabled->linkloss)
		return 0;

	monitor = register_monitor(device);
	if (monitor == NULL)
		return -1;

	monitor->linkloss = g_new0(struct att_range, 1);
	monitor->linkloss->start = linkloss->range.start;
	monitor->linkloss->end = linkloss->range.end;
	monitor->enabled.linkloss = TRUE;

	update_monitor(monitor);

	return 0;
}

int monitor_register_txpower(struct btd_device *device,
						struct enabled *enabled,
						struct gatt_primary *txpower)
{
	struct monitor *monitor;

	if (!enabled->pathloss)
		return 0;

	monitor = register_monitor(device);
	if (monitor == NULL)
		return -1;

	monitor->txpower = g_new0(struct att_range, 1);
	monitor->txpower->start = txpower->range.start;
	monitor->txpower->end = txpower->range.end;

	update_monitor(monitor);

	return 0;
}

int monitor_register_immediate(struct btd_device *device,
						struct enabled *enabled,
						struct gatt_primary *immediate)
{
	struct monitor *monitor;

	if (!enabled->pathloss && !enabled->findme)
		return 0;

	monitor = register_monitor(device);
	if (monitor == NULL)
		return -1;

	monitor->immediate = g_new0(struct att_range, 1);
	monitor->immediate->start = immediate->range.start;
	monitor->immediate->end = immediate->range.end;
	monitor->enabled.findme = enabled->findme;

	update_monitor(monitor);

	return 0;
}

static void cleanup_monitor(struct monitor *monitor)
{
	struct btd_device *device = monitor->device;
	const char *path = device_get_path(device);

	if (monitor->immediate != NULL || monitor->txpower != NULL)
		return;

	if (monitor->immediateto != 0) {
		g_source_remove(monitor->immediateto);
		monitor->immediateto = 0;
	}

	if (monitor->linkloss != NULL)
		return;

	if (monitor->attioid != 0) {
		btd_device_remove_attio_callback(device, monitor->attioid);
		monitor->attioid = 0;
	}

	if (monitor->attrib != NULL) {
		g_attrib_unref(monitor->attrib);
		monitor->attrib = NULL;
	}

	g_dbus_unregister_interface(btd_get_dbus_connection(), path,
							PROXIMITY_INTERFACE);
}

void monitor_unregister_linkloss(struct btd_device *device)
{
	struct monitor *monitor;

	monitor = find_monitor(device);
	if (monitor == NULL)
		return;

	g_free(monitor->linkloss);
	monitor->linkloss = NULL;
	monitor->enabled.linkloss = FALSE;

	cleanup_monitor(monitor);
}

void monitor_unregister_txpower(struct btd_device *device)
{
	struct monitor *monitor;

	monitor = find_monitor(device);
	if (monitor == NULL)
		return;

	g_free(monitor->txpower);
	monitor->txpower = NULL;
	monitor->enabled.pathloss = FALSE;

	cleanup_monitor(monitor);
}

void monitor_unregister_immediate(struct btd_device *device)
{
	struct monitor *monitor;

	monitor = find_monitor(device);
	if (monitor == NULL)
		return;

	g_free(monitor->immediate);
	monitor->immediate = NULL;
	monitor->enabled.findme = FALSE;
	monitor->enabled.pathloss = FALSE;

	cleanup_monitor(monitor);
}
