/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2009  Bastien Nocera <hadess@hadess.net>
 *
 *
 *  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 <stdlib.h>
#include <string.h>
#include <glib.h>
#include <libudev.h>
#include <bluetooth-enums.h>
#include <bluetooth-utils.h>

#include "pin.h"

#define PIN_CODE_DB "pin-code-database.xml"
#define MAX_DIGITS_PIN_PREFIX "max:"

char *
oui_to_vendor (const char *oui)
{
	struct udev *udev = NULL;
	struct udev_hwdb *hwdb = NULL;
	struct udev_list_entry *list, *l;
	char *modalias = NULL;
	char *vendor = NULL;

	if (oui == NULL ||
	    strlen (oui) < 8)
		return NULL;

	udev = udev_new ();
	if (udev == NULL)
		goto bail;

	hwdb = udev_hwdb_new (udev);
	if (hwdb == NULL)
		goto bail;

	modalias = g_strdup_printf ("OUI:%c%c%c%c%c%c",
				    g_ascii_toupper (oui[0]),
				    g_ascii_toupper (oui[1]),
				    g_ascii_toupper (oui[3]),
				    g_ascii_toupper (oui[4]),
				    g_ascii_toupper (oui[6]),
				    g_ascii_toupper (oui[7]));

	list = udev_hwdb_get_properties_list_entry (hwdb, modalias, 0);

	udev_list_entry_foreach (l, list) {
		const char *name = udev_list_entry_get_name (l);

		if (g_strcmp0 (name, "ID_OUI_FROM_DATABASE") == 0) {
			vendor = g_strdup (udev_list_entry_get_value (l));
			break;
		}
	}

bail:
	g_clear_pointer (&modalias, g_free);
	g_clear_pointer (&hwdb, udev_hwdb_unref);
	g_clear_pointer (&udev, udev_unref);

	return vendor;
}

#define TYPE_IS(x, r) {				\
	if (g_str_equal(type, x)) return r;	\
}

static guint string_to_type(const char *type)
{
	TYPE_IS ("any", BLUETOOTH_TYPE_ANY);
	TYPE_IS ("mouse", BLUETOOTH_TYPE_MOUSE);
	TYPE_IS ("tablet", BLUETOOTH_TYPE_TABLET);
	TYPE_IS ("keyboard", BLUETOOTH_TYPE_KEYBOARD);
	TYPE_IS ("headset", BLUETOOTH_TYPE_HEADSET);
	TYPE_IS ("headphones", BLUETOOTH_TYPE_HEADPHONES);
	TYPE_IS ("audio", BLUETOOTH_TYPE_OTHER_AUDIO);
	TYPE_IS ("printer", BLUETOOTH_TYPE_PRINTER);
	TYPE_IS ("network", BLUETOOTH_TYPE_NETWORK);
	TYPE_IS ("joypad", BLUETOOTH_TYPE_JOYPAD);

	g_warning ("unhandled type '%s'", type);
	return BLUETOOTH_TYPE_ANY;
}

typedef struct {
	char *ret_pin;
	guint max_digits;
	guint type;
	const char *address;
	const char *name;
	char *vendor;
	gboolean confirm;
} PinParseData;

static void
pin_db_parse_start_tag (GMarkupParseContext *ctx,
			const gchar         *element_name,
			const gchar        **attr_names,
			const gchar        **attr_values,
			gpointer             data,
			GError             **error)
{
	PinParseData *pdata = (PinParseData *) data;

	if (pdata->ret_pin != NULL || pdata->max_digits != 0)
		return;
	if (g_str_equal (element_name, "device") == FALSE)
		return;

	while (*attr_names && *attr_values) {
		if (g_str_equal (*attr_names, "type")) {
			guint type;

			type = string_to_type (*attr_values);
			if (type != BLUETOOTH_TYPE_ANY && type != pdata->type)
				return;
		} else if (g_str_equal (*attr_names, "oui")) {
			if (g_str_has_prefix (pdata->address, *attr_values) == FALSE)
				return;
		} else if (g_str_equal (*attr_names, "vendor")) {
			if (*attr_values == NULL || pdata->vendor == NULL)
				return;
			if (strstr (pdata->vendor, *attr_values) == NULL)
				return;
		} else if (g_str_equal (*attr_names, "name")) {
			if (*attr_values == NULL || pdata->name == NULL)
				return;
			if (strstr (pdata->name, *attr_values) == NULL)
				return;
			pdata->confirm = FALSE;
		} else if (g_str_equal (*attr_names, "pin")) {
			if (g_str_has_prefix (*attr_values, MAX_DIGITS_PIN_PREFIX) != FALSE) {
				pdata->max_digits = strtoul (*attr_values + strlen (MAX_DIGITS_PIN_PREFIX), NULL, 0);
				g_assert (pdata->max_digits > 0 && pdata->max_digits < PIN_NUM_DIGITS);
			} else {
				pdata->ret_pin = g_strdup (*attr_values);
			}
			return;
		}

		++attr_names;
		++attr_values;
	}
}

char *
get_pincode_for_device (guint       type,
			const char *address,
			const char *name,
			guint      *max_digits,
			gboolean   *confirm)
{
	GMarkupParseContext *ctx;
	GMarkupParser parser = { pin_db_parse_start_tag, NULL, NULL, NULL, NULL };
	PinParseData *data;
	char *buf;
	gsize buf_len;
	GError *err = NULL;
	char *tmp_vendor, *ret_pin;

	g_return_val_if_fail (address != NULL, NULL);

	g_debug ("Getting pincode for device '%s' (type: %s address: %s)",
		 name ? name : "", bluetooth_type_to_string (type), address);

	/* Load the PIN database and split it in lines */
	if (!g_file_get_contents(PIN_CODE_DB, &buf, &buf_len, NULL)) {
		char *filename;

		filename = g_build_filename(PKGDATADIR, PIN_CODE_DB, NULL);
		if (!g_file_get_contents(filename, &buf, &buf_len, NULL)) {
			g_warning("Could not load "PIN_CODE_DB);
			g_free (filename);
			return NULL;
		}
		g_free (filename);
	}

	data = g_new0 (PinParseData, 1);
	data->type = type;
	data->address = address;
	data->name = name;
	data->confirm = TRUE;

	tmp_vendor = oui_to_vendor (address);
	if (tmp_vendor)
		data->vendor = g_ascii_strdown (tmp_vendor, -1);
	g_free (tmp_vendor);

	ctx = g_markup_parse_context_new (&parser, 0, data, NULL);

	if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err)) {
		g_warning ("Failed to parse '%s': %s\n", PIN_CODE_DB, err->message);
		g_error_free (err);
	}

	g_markup_parse_context_free (ctx);
	g_free (buf);

	if (max_digits != NULL)
		*max_digits = data->max_digits;
	if (confirm != NULL)
		*confirm = data->confirm;

	g_debug ("Got pin '%s' (max digits: %d, confirm: %d) for device '%s' (type: %s address: %s, vendor: %s)",
		 data->ret_pin, data->max_digits, data->confirm,
		 name ? name : "", bluetooth_type_to_string (type), address, data->vendor);

	g_free (data->vendor);
	ret_pin = data->ret_pin;
	g_free (data);

	return ret_pin;
}

