/*
 *
 *  oFono - Open Source Telephony
 *
 *  Copyright (C) 2008-2011  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#include <glib.h>
#include <ell/ell.h>

#include <ofono/log.h>
#include <ofono/modem.h>
#include <ofono/phonebook.h>
#include "util.h"

#include "gatchat.h"
#include "gatresult.h"

#include "atmodem.h"
#include "vendor.h"

#define INDEX_INVALID -1

#define CHARSET_UTF8 1
#define CHARSET_UCS2 2
#define CHARSET_IRA  4
#define CHARSET_SUPPORT (CHARSET_UTF8 | CHARSET_UCS2)

static const char *none_prefix[] = { NULL };
static const char *cpbr_prefix[] = { "+CPBR:", NULL };
static const char *cscs_prefix[] = { "+CSCS:", NULL };
static const char *cpbs_prefix[] = { "+CPBS:", NULL };

struct pb_data {
	int index_min, index_max;
	char *old_charset;
	int supported;
	GAtChat *chat;
	unsigned int vendor;
	guint poll_source;
	guint poll_count;
	guint ready_id;
};

static void warn_bad(void)
{
	ofono_warn("Name field conversion to UTF8 failed, this can indicate a"
			" problem with modem integration, as this field"
			" is required by 27.007.");
}

static gboolean parse_text(GAtResultIter *iter, char **str, int encoding)
{
	const char *string;
	const guint8 *hex;
	int len;
	char *utf8;
	/* charset_current is CHARSET_UCS2, CHARSET_IRA or CHARSET_UTF8 */
	if (encoding == CHARSET_UCS2) {
		/*
		 * Some devices omit the quotes, so use next_hexstring,
		 * which handles quoted or unquoted hex strings
		 */
		if (g_at_result_iter_next_hexstring(iter, &hex, &len) == FALSE)
			return FALSE;

		utf8 = l_utf8_from_ucs2be(hex, len);
		if (!utf8)
			return FALSE;

		*str = utf8;
		return TRUE;
	}

	/*
	 * In the case of IRA charset, assume these are Latin1
	 * characters, same as in UTF8
	 */
	if (g_at_result_iter_next_string(iter, &string)) {
		*str = l_strdup(string);
		return TRUE;
	}

	return FALSE;
}

static const char *best_charset(int supported)
{
	const char *charset = "Invalid";

	if (supported & CHARSET_IRA)
		charset = "IRA";

	if (supported & CHARSET_UCS2)
		charset = "UCS2";

	if (supported & CHARSET_UTF8)
		charset = "UTF-8";

	return charset;
}

static void at_cpbr_notify(GAtResult *result, gpointer user_data)
{
	struct cb_data *cbd = user_data;
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	GAtResultIter iter;
	int current;

	if (pbd->supported & CHARSET_IRA)
		current = CHARSET_IRA;

	if (pbd->supported & CHARSET_UCS2)
		current = CHARSET_UCS2;

	if (pbd->supported & CHARSET_UTF8)
		current = CHARSET_UTF8;

	g_at_result_iter_init(&iter, result);

	while (g_at_result_iter_next(&iter, "+CPBR:")) {
		int index;
		const char *number;
		int type;
		char *text;
		int hidden = -1;
		char *group = NULL;
		const char *adnumber = NULL;
		int adtype = -1;
		char *secondtext = NULL;
		char *email = NULL;
		char *sip_uri = NULL;
		char *tel_uri = NULL;

		if (!g_at_result_iter_next_number(&iter, &index))
			continue;

		if (!g_at_result_iter_next_string(&iter, &number))
			continue;

		if (!g_at_result_iter_next_number(&iter, &type))
			continue;

		if (!parse_text(&iter, &text, current)) {
			warn_bad();
			continue;
		}

		g_at_result_iter_next_number_default(&iter, 0, &hidden);
		parse_text(&iter, &group, current);
		g_at_result_iter_next_string(&iter, &adnumber);
		g_at_result_iter_next_number_default(&iter, 0, &adtype);
		parse_text(&iter, &secondtext, current);
		parse_text(&iter, &email, current);
		parse_text(&iter, &sip_uri, current);
		parse_text(&iter, &tel_uri, current);

		ofono_phonebook_entry(pb, index, number, type,
			text, hidden, group, adnumber,
			adtype, secondtext, email,
			sip_uri, tel_uri);

		l_free(text);
		l_free(group);
		l_free(secondtext);
		l_free(email);
		l_free(sip_uri);
		l_free(tel_uri);
	}
}

static void export_failed(struct cb_data *cbd)
{
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	ofono_phonebook_cb_t cb = cbd->cb;

	CALLBACK_WITH_FAILURE(cb, cbd->data);

	g_free(cbd);

	if (pbd->old_charset) {
		g_free(pbd->old_charset);
		pbd->old_charset = NULL;
	}
}

static void at_read_entries_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct cb_data *cbd = user_data;
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	ofono_phonebook_cb_t cb = cbd->cb;
	const char *charset;
	struct ofono_error error;
	char buf[32];

	decode_at_error(&error, g_at_result_final_response(result));
	cb(&error, cbd->data);
	g_free(cbd);

	charset = best_charset(pbd->supported);

	if (strcmp(pbd->old_charset, charset)) {
		snprintf(buf, sizeof(buf), "AT+CSCS=\"%s\"", pbd->old_charset);
		g_at_chat_send(pbd->chat, buf, none_prefix, NULL, NULL, NULL);
	}

	g_free(pbd->old_charset);
	pbd->old_charset = NULL;
}

static void at_read_entries(struct cb_data *cbd)
{
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	char buf[32];

	snprintf(buf, sizeof(buf), "AT+CPBR=%d,%d",
			pbd->index_min, pbd->index_max);
	if (g_at_chat_send_listing(pbd->chat, buf, cpbr_prefix,
					at_cpbr_notify, at_read_entries_cb,
					cbd, NULL) > 0)
		return;

	/* If we get here, then most likely connection to the modem dropped
	 * and we can't really restore the charset anyway
	 */
	export_failed(cbd);
}

static void at_set_charset_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct cb_data *cbd = user_data;

	if (!ok) {
		export_failed(cbd);
		return;
	}

	at_read_entries(cbd);
}

static void at_read_charset_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct cb_data *cbd = user_data;
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	GAtResultIter iter;
	const char *charset;
	char buf[32];

	if (!ok)
		goto error;

	g_at_result_iter_init(&iter, result);

	if (!g_at_result_iter_next(&iter, "+CSCS:"))
		goto error;

	g_at_result_iter_next_string(&iter, &charset);

	pbd->old_charset = g_strdup(charset);

	charset = best_charset(pbd->supported);

	if (!strcmp(pbd->old_charset, charset)) {
		at_read_entries(cbd);
		return;
	}

	snprintf(buf, sizeof(buf), "AT+CSCS=\"%s\"", charset);
	if (g_at_chat_send(pbd->chat, buf, none_prefix,
				at_set_charset_cb, cbd, NULL) > 0)
		return;

error:
	export_failed(cbd);
}

static void at_list_indices_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct cb_data *cbd = user_data;
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	GAtResultIter iter;

	if (!ok)
		goto error;

	g_at_result_iter_init(&iter, result);
	if (!g_at_result_iter_next(&iter, "+CPBR:"))
		goto error;

	if (!g_at_result_iter_open_list(&iter))
		goto error;

	/* Retrieve index_min and index_max from indices
	 * which seems like "(1-150),32,16"
	 */
	if (!g_at_result_iter_next_range(&iter, &pbd->index_min,
						&pbd->index_max))
		goto error;

	if (!g_at_result_iter_close_list(&iter))
		goto error;

	if (g_at_chat_send(pbd->chat, "AT+CSCS?", cscs_prefix,
				at_read_charset_cb, cbd, NULL) > 0)
		return;

error:
	export_failed(cbd);
}

static void at_select_storage_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct cb_data *cbd = user_data;
	struct ofono_phonebook *pb = cbd->user;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);

	if (!ok)
		goto error;

	if (g_at_chat_send(pbd->chat, "AT+CPBR=?", cpbr_prefix,
				at_list_indices_cb, cbd, NULL) > 0)
		return;

error:
	export_failed(cbd);
}

static void at_export_entries(struct ofono_phonebook *pb, const char *storage,
				ofono_phonebook_cb_t cb, void *data)
{
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	struct cb_data *cbd = cb_data_new(cb, data);
	char buf[32];

	cbd->user = pb;

	snprintf(buf, sizeof(buf), "AT+CPBS=\"%s\"", storage);
	if (g_at_chat_send(pbd->chat, buf, none_prefix,
				at_select_storage_cb, cbd, NULL) > 0)
		return;

	g_free(cbd);

	CALLBACK_WITH_FAILURE(cb, data);
}

static void phonebook_not_supported(struct ofono_phonebook *pb)
{
	ofono_error("Phonebook not supported by this modem.  If this is in "
			"error please submit patches to support this hardware");

	ofono_phonebook_remove(pb);
}

static void at_list_storages_cb(gboolean ok, GAtResult *result,
						gpointer user_data);

static gboolean cpbs_support_check(gpointer user_data)
{
	struct ofono_phonebook *pb = user_data;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);

	pbd->poll_source = 0;

	if (g_at_chat_send(pbd->chat, "AT+CPBS=?", cpbs_prefix,
				at_list_storages_cb, pb, NULL) > 0)
		return FALSE;

	phonebook_not_supported(pb);

	return FALSE;
}

static void ifx_pbready_notify(GAtResult *result, gpointer user_data)
{
	struct ofono_phonebook *pb = user_data;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);

	g_at_chat_unregister(pbd->chat, pbd->ready_id);
	pbd->ready_id = 0;

	cpbs_support_check(pb);
}

static void at_list_storages_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct ofono_phonebook *pb = user_data;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	struct ofono_error error;
	gboolean sm_supported = FALSE;
	gboolean me_supported = FALSE;
	gboolean in_list = FALSE;
	GAtResultIter iter;
	const char *storage;

	decode_at_error(&error, g_at_result_final_response(result));

	switch (error.type) {
	case OFONO_ERROR_TYPE_NO_ERROR:
		break;
	case OFONO_ERROR_TYPE_CME:
		/* Check for SIM busy - try again later */
		if (error.error == 14) {
			if (pbd->poll_count++ < 12) {
				pbd->poll_source = g_timeout_add_seconds(5,
						cpbs_support_check, pb);
				return;
			}
		}
		/* fall through */
	default:
		goto error;
	}

	g_at_result_iter_init(&iter, result);
	if (!g_at_result_iter_next(&iter, "+CPBS:"))
		goto error;

	/* Some modems don't report CPBS in a proper list */
	if (g_at_result_iter_open_list(&iter))
		in_list = TRUE;

	while (g_at_result_iter_next_string(&iter, &storage)) {
		if (!strcmp(storage, "ME"))
			me_supported = TRUE;
		else if (!strcmp(storage, "SM"))
			sm_supported = TRUE;
	}

	if (in_list && !g_at_result_iter_close_list(&iter))
		goto vendor;

	if (!me_supported && !sm_supported)
		goto vendor;

	ofono_phonebook_register(pb);
	return;

vendor:
	switch (pbd->vendor) {
	case OFONO_VENDOR_IFX:
		pbd->ready_id = g_at_chat_register(pbd->chat, "+PBREADY",
					ifx_pbready_notify, FALSE, pb, NULL);
		return;
	}

error:
	phonebook_not_supported(pb);
}

static void at_list_charsets_cb(gboolean ok, GAtResult *result,
						gpointer user_data)
{
	struct ofono_phonebook *pb = user_data;
	struct pb_data *pbd = ofono_phonebook_get_data(pb);
	gboolean in_list = FALSE;
	GAtResultIter iter;
	const char *charset;

	if (!ok)
		goto error;

	g_at_result_iter_init(&iter, result);
	if (!g_at_result_iter_next(&iter, "+CSCS:"))
		goto error;

	/* Some modems don't report CSCS in a proper list */
	if (g_at_result_iter_open_list(&iter))
		in_list = TRUE;

	while (g_at_result_iter_next_string(&iter, &charset)) {
		if (!strcmp(charset, "UTF-8"))
			pbd->supported |= CHARSET_UTF8;
		else if (!strcmp(charset, "UCS2"))
			pbd->supported |= CHARSET_UCS2;
		else if (!strcmp(charset, "IRA"))
			pbd->supported |= CHARSET_IRA;
	}

	if (in_list && !g_at_result_iter_close_list(&iter))
		goto error;

	if (!(pbd->supported & CHARSET_SUPPORT)) {
		/* Some modems, like the Google G1, do not support UCS2 or UTF8
		 * Such modems are effectively junk, but we can still get some
		 * useful information out of them by using IRA charset, which
		 * is essentially Latin1.  Still, all bets are off if a SIM
		 * with UCS2 encoded entries is present.
		 */
		if (pbd->supported & CHARSET_IRA) {
			ofono_error("This modem does not support UCS2 or UTF8 "
					"character sets.  This means no i18n "
					"phonebook is possible on this modem,"
					" if this is in error, submit patches "
					"to properly support this hardware");
		} else {
			goto error;
		}
	}

	pbd->poll_count = 0;

	cpbs_support_check(pb);
	return;

error:
	phonebook_not_supported(pb);
}

static void at_list_charsets(struct ofono_phonebook *pb)
{
	struct pb_data *pbd = ofono_phonebook_get_data(pb);

	if (g_at_chat_send(pbd->chat, "AT+CSCS=?", cscs_prefix,
				at_list_charsets_cb, pb, NULL) > 0)
		return;

	phonebook_not_supported(pb);
}

static int at_phonebook_probe(struct ofono_phonebook *pb, unsigned int vendor,
				void *data)
{
	GAtChat *chat = data;
	struct pb_data *pbd;

	pbd = g_try_new0(struct pb_data, 1);
	if (pbd == NULL)
		return -ENOMEM;

	pbd->chat = g_at_chat_clone(chat);
	pbd->vendor = vendor;

	ofono_phonebook_set_data(pb, pbd);

	at_list_charsets(pb);

	return 0;
}

static void at_phonebook_remove(struct ofono_phonebook *pb)
{
	struct pb_data *pbd = ofono_phonebook_get_data(pb);

	if (pbd->poll_source > 0)
		g_source_remove(pbd->poll_source);

	if (pbd->old_charset)
		g_free(pbd->old_charset);

	ofono_phonebook_set_data(pb, NULL);

	g_at_chat_unref(pbd->chat);
	g_free(pbd);
}

static const struct ofono_phonebook_driver driver = {
	.name		= "atmodem",
	.probe		= at_phonebook_probe,
	.remove		= at_phonebook_remove,
	.export_entries	= at_export_entries
};

void at_phonebook_init(void)
{
	ofono_phonebook_driver_register(&driver);
}

void at_phonebook_exit(void)
{
	ofono_phonebook_driver_unregister(&driver);
}
