/*
 * This file is part of telepathy-idle
 *
 * Copyright (C) 2011 Debarshi Ray <rishi@gnu.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "idle-contact-info.h"

#define IDLE_DEBUG_FLAG IDLE_DEBUG_CONNECTION
#include "idle-debug.h"
#include "idle-muc-channel.h"
#include "idle-parser.h"

typedef struct _ContactInfoRequest ContactInfoRequest;

struct _ContactInfoRequest {
	guint handle;
	const gchar *nick;
	gboolean is_away;
	gboolean is_operator;
	gboolean is_reg_nick;
	gboolean is_secure;
	GPtrArray *contact_info;
	DBusGMethodInvocation *context;
};

/*
 * _insert_contact_field:
 * @contact_info: an array of Contact_Info_Field structures
 * @field_name: a vCard field name in any case combination
 * @field_params: a list of vCard type-parameters, typically of the form
 *  type=xxx; must be in lower-case if case-insensitive
 * @field_values: for unstructured fields, an array containing one element;
 *  for structured fields, the elements of the field in order
 */
static void _insert_contact_field(GPtrArray *contact_info, const gchar *field_name, const gchar * const *field_params, const gchar * const *field_values) {
	g_ptr_array_add (contact_info, tp_value_array_build (3,
		G_TYPE_STRING, field_name,
		G_TYPE_STRV, field_params,
		G_TYPE_STRV, field_values,
		G_TYPE_INVALID));
}

static ContactInfoRequest * _get_matching_request(IdleConnection *conn, GValueArray *args) {
	ContactInfoRequest *request;
	TpHandle handle = g_value_get_uint(g_value_array_get_nth(args, 0));

	if (g_queue_is_empty(conn->contact_info_requests))
		return NULL;

	request = g_queue_peek_head(conn->contact_info_requests);
	if (request->handle != handle)
		return NULL;

	if (request->contact_info == NULL)
		request->contact_info = dbus_g_type_specialized_construct(TP_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST);

	return request;
}

static void _send_request_contact_info(IdleConnection *conn, ContactInfoRequest *request) {
	gchar cmd[IRC_MSG_MAXLEN + 1];

	g_snprintf(cmd, IRC_MSG_MAXLEN + 1, "WHOIS %s %s", request->nick, request->nick);
	idle_connection_send(conn, cmd);
}

static void _dequeue_request_contact_info(IdleConnection *conn) {
	ContactInfoRequest *request = g_queue_pop_head(conn->contact_info_requests);

	if (request->contact_info != NULL)
		g_boxed_free(TP_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST, request->contact_info);

	g_slice_free(ContactInfoRequest, request);

	if (g_queue_is_empty(conn->contact_info_requests))
		return;

	request = g_queue_peek_head(conn->contact_info_requests);
	_send_request_contact_info(conn, request);
}

static void _queue_request_contact_info(IdleConnection *conn, guint handle, const gchar *nick, DBusGMethodInvocation *context) {
	ContactInfoRequest *request;

	request = g_slice_new0(ContactInfoRequest);
	request->handle = handle;
	request->nick = nick;
	request->is_away = FALSE;
	request->is_operator = FALSE;
	request->is_reg_nick = FALSE;
	request->is_secure = FALSE;
	request->contact_info = NULL;
	request->context = context;

	if (g_queue_is_empty(conn->contact_info_requests))
		_send_request_contact_info(conn, request);

	g_queue_push_tail(conn->contact_info_requests, request);
}

static void _return_from_request_contact_info(IdleConnection *conn) {
	ContactInfoRequest *request = g_queue_peek_head(conn->contact_info_requests);

	tp_svc_connection_interface_contact_info_return_from_request_contact_info(request->context, request->contact_info);
	tp_svc_connection_interface_contact_info_emit_contact_info_changed(conn, request->handle, request->contact_info);
	_dequeue_request_contact_info(conn);
}

static void idle_connection_request_contact_info(TpSvcConnectionInterfaceContactInfo *iface, guint contact, DBusGMethodInvocation *context) {
	IdleConnection *self = IDLE_CONNECTION(iface);
	TpBaseConnection *base = TP_BASE_CONNECTION(self);
	TpHandleRepoIface *contact_handles = tp_base_connection_get_handles(base, TP_HANDLE_TYPE_CONTACT);
	const gchar *nick;
	GError *error = NULL;

	TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED(base, context);

	if (!tp_handle_is_valid(contact_handles, contact, &error)) {
		dbus_g_method_return_error(context, error);
		g_error_free(error);
		return;
	}

	nick = tp_handle_inspect(contact_handles, contact);

	IDLE_DEBUG ("Queued contact info request for handle: %u (%s)", contact, nick);
	_queue_request_contact_info(self, contact, nick, context);
}

static IdleParserHandlerResult _away_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	const gchar *msg;
	const gchar *field_values[2] = {NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	field_values[0] = g_strdup_printf("%d", TP_CONNECTION_PRESENCE_TYPE_AWAY);
	_insert_contact_field(request->contact_info, "x-presence-type", NULL, field_values);
	g_free((gpointer) field_values[0]);

	field_values[0] = "away";
	_insert_contact_field(request->contact_info, "x-presence-status-identifier", NULL, field_values);

	msg = g_value_get_string(g_value_array_get_nth(args, 1));
	field_values[0] = msg;
	_insert_contact_field(request->contact_info, "x-presence-status-message", NULL, field_values);

	request->is_away = TRUE;

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _end_of_whois_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	const gchar *field_values[2] = {NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	if (request->is_away == FALSE) {
		field_values[0] = g_strdup_printf("%d", TP_CONNECTION_PRESENCE_TYPE_AVAILABLE);
		_insert_contact_field(request->contact_info, "x-presence-type", NULL, field_values);
		g_free((gpointer) field_values[0]);

		field_values[0] = "available";
		_insert_contact_field(request->contact_info, "x-presence-status-identifier", NULL, field_values);

		field_values[0] = "";
		_insert_contact_field(request->contact_info, "x-presence-status-message", NULL, field_values);
	}

	field_values[0] = (request->is_operator) ? "true" : "false";
	_insert_contact_field(request->contact_info, "x-irc-operator", NULL, field_values);

	field_values[0] = (request->is_reg_nick) ? "true" : "false";
	_insert_contact_field(request->contact_info, "x-irc-registered-nick", NULL, field_values);

	field_values[0] = (request->is_secure) ? "true" : "false";
	_insert_contact_field(request->contact_info, "x-irc-secure-connection", NULL, field_values);

	_return_from_request_contact_info(conn);
	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _no_such_server_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	TpBaseConnection *base = TP_BASE_CONNECTION(conn);
	TpHandleRepoIface *contact_handles = tp_base_connection_get_handles(base, TP_HANDLE_TYPE_CONTACT);
	TpHandle handle;
	ContactInfoRequest *request;
	GValueArray *norm_args = g_value_array_copy(args);
	GValue value = {0};
	const gchar *server;
	GError *error = NULL;

	/* We issue our requests as WHOIS <nick> <nick>, where the first <nick> is actually a shorthand for the server to which it is connected to.
	 * Therefore, if this error was caused by us then the value of <server name> in the error message will be <nick>. Otherwise, this error was not
	 * caused by us, and we do not want to handle it.
	 *
	 * To check this we map the value of the <server name> to a handle and see if it matches the handle for which we had made the request.
	 */

	server = g_value_get_string(g_value_array_get_nth(args, 0));
	handle = tp_handle_ensure(contact_handles, server, NULL, NULL);

	g_value_array_remove(norm_args, 0);

	g_value_init(&value, G_TYPE_UINT);
	g_value_set_uint(&value, handle);
	g_value_array_prepend(norm_args, &value);

	request = _get_matching_request(conn, norm_args);
	if (request == NULL)
		goto cleanup;

	error = g_error_new(TP_ERROR, TP_ERROR_DOES_NOT_EXIST, "User '%s' unknown; they may have disconnected", server);
	dbus_g_method_return_error(request->context, error);
	g_error_free(error);

	_dequeue_request_contact_info(conn);

cleanup:
	g_value_array_free(norm_args);
	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _try_again_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request;
	const gchar *command;
	const gchar *msg;
	GError *error = NULL;

	/* The RPL_TRYAGAIN message does not contain the nick for which the request was issued, but only the type of the message, which in this case is
	 * WHOIS. So we assume that the last WHOIS request that we had issued has resulted in this RPL_TRYAGAIN. This is fine as long nobody else is
	 * issuing a WHOIS because we issue a new request only after the earlier one was successfully completed or resulted in an error.
	 */

	if (g_queue_is_empty(conn->contact_info_requests))
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	command = g_value_get_string(g_value_array_get_nth(args, 0));
	if (g_ascii_strcasecmp(command, "WHOIS"))
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	request = g_queue_peek_head(conn->contact_info_requests);

	msg = g_value_get_string(g_value_array_get_nth(args, 1));

	error = g_error_new_literal(TP_ERROR, TP_ERROR_SERVICE_BUSY, msg);
	dbus_g_method_return_error(request->context, error);
	g_error_free(error);

	_dequeue_request_contact_info(conn);

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_channels_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	gchar *channels;
	gchar **channelsv;
	const gchar *field_values[2] = {NULL, NULL};
	guint i;

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	if (args->n_values != 2)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	channels = g_value_dup_string(g_value_array_get_nth(args, 1));
	g_strchomp(channels);
	channelsv = g_strsplit(channels, " ", -1);

	for (i = 0; channelsv[i] != NULL; i++) {
		const gchar *channel = channelsv[i];
		gchar *field_params[2] = {NULL, NULL};

		if (idle_muc_channel_is_modechar(channel[0]) && idle_muc_channel_is_typechar(channel[1])) {
			field_params[0] = g_strdup_printf("role=%c", channel[0]);
			channel++;
		}

		field_values[0] = channel;
		_insert_contact_field(request->contact_info, "x-irc-channel", (const gchar **) field_params, field_values);
		g_free(field_params[0]);
	}

	g_strfreev(channelsv);
	g_free(channels);

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_host_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	gchar *msg;
	gchar **msgv;

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	msg = g_value_dup_string(g_value_array_get_nth(args, 1));
	g_strchomp(msg);

	if (!g_str_has_prefix(msg, "is connecting from "))
		goto cleanup;

	msgv = g_strsplit(msg, " ", -1);

	/* msg == "is connecting from *@<hostname> <IP>" */
	_insert_contact_field(request->contact_info, "x-host", NULL, (const gchar **) (msgv + 3));

	g_strfreev(msgv);

cleanup:
	g_free(msg);
	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_idle_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	guint sec;
	const gchar *field_values[2] = {NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	sec = g_value_get_uint(g_value_array_get_nth(args, 1));

	field_values[0] = g_strdup_printf("%u", sec);
	_insert_contact_field(request->contact_info, "x-idle-time", NULL, field_values);
	g_free((gpointer) field_values[0]);

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_logged_in_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	const gchar *msg;
	const gchar *nick;
	const gchar *field_values[2] = {NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	msg = g_value_get_string(g_value_array_get_nth(args, 2));
	if (g_strcmp0(msg, "is logged in as"))
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	nick = g_value_get_string(g_value_array_get_nth(args, 1));
	field_values[0] = nick;
	_insert_contact_field(request->contact_info, "nickname", NULL, field_values);

	request->is_reg_nick = TRUE;

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_operator_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	request->is_operator = TRUE;

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_reg_nick_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	request->is_reg_nick = TRUE;

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_secure_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	request->is_secure = TRUE;

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_server_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	const gchar *server;
	const gchar *server_info;
	const gchar *field_values[3] = {NULL, NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	server = g_value_get_string(g_value_array_get_nth(args, 1));
	server_info = g_value_get_string(g_value_array_get_nth(args, 2));
	field_values[0] = server;
	field_values[1] = server_info;
	_insert_contact_field(request->contact_info, "x-irc-server", NULL, field_values);

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static IdleParserHandlerResult _whois_user_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
	IdleConnection *conn = IDLE_CONNECTION(user_data);
	ContactInfoRequest *request = _get_matching_request(conn, args);
	const gchar *name;
	const gchar *field_values[2] = {NULL, NULL};

	if (request == NULL)
		return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;

	name = g_value_get_string(g_value_array_get_nth(args, 3));
	field_values[0] = name;
	_insert_contact_field(request->contact_info, "fn", NULL, field_values);

	return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}

static void idle_contact_info_properties_getter(GObject *object, GQuark interface, GQuark name, GValue *value, gpointer getter_data) {
	GQuark q_supported_fields = g_quark_from_static_string("SupportedFields");

	if (name == q_supported_fields) {
		GPtrArray *fields = dbus_g_type_specialized_construct(TP_ARRAY_TYPE_FIELD_SPECS);

		g_value_set_boxed(value, fields);
		g_boxed_free(TP_ARRAY_TYPE_FIELD_SPECS, fields);
	}
	else /* ContactInfoFlags */
		g_value_set_uint(value, 0);
}

static void _contact_info_requests_foreach_free(gpointer data, gpointer user_data) {
	g_slice_free(ContactInfoRequest, data);
}

void idle_contact_info_finalize (GObject *object) {
	IdleConnection *conn = IDLE_CONNECTION(object);

	g_queue_foreach(conn->contact_info_requests, _contact_info_requests_foreach_free, NULL);
	g_queue_free(conn->contact_info_requests);
}

void idle_contact_info_class_init (IdleConnectionClass *klass) {
	GObjectClass *object_class = G_OBJECT_CLASS(klass);

	static TpDBusPropertiesMixinPropImpl props[] = {
		{"ContactInfoFlags", NULL, NULL},
		{"SupportedFields", NULL, NULL},
		{NULL}
	};

	tp_dbus_properties_mixin_implement_interface(object_class,
		TP_IFACE_QUARK_CONNECTION_INTERFACE_CONTACT_INFO,
		idle_contact_info_properties_getter,
		NULL,
		props);
}

static void
idle_contact_info_fill_contact_attributes (
    GObject *obj,
    const GArray *contacts,
    GHashTable *attributes_hash)
{
  /* We don't cache contact info, so we just never put /info into the
   * attributes hash. This is spec-compliant: we don't implement
   * GetContactInfo, and the spec says the attribute should be the same as the
   * value returned by that method (or omitted if unknown). This function
   * exists at all to make ContactInfo show up in ContactAttributeInterfaces
   * (otherwise tp-glib might be justified in falling back to
   * GetContactInfo(), which we know will fail).
   */
}

void idle_contact_info_init (IdleConnection *conn) {
	conn->contact_info_requests = g_queue_new();

	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISUSER, _whois_user_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISCHANNELS, _whois_channels_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISSERVER, _whois_server_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISOPERATOR, _whois_operator_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_AWAY, _away_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISHOST, _whois_host_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISREGNICK, _whois_reg_nick_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISSECURE, _whois_secure_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISIDLE, _whois_idle_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISLOGGEDIN, _whois_logged_in_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_ENDOFWHOIS, _end_of_whois_handler, conn);

	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_NOSUCHSERVER, _no_such_server_handler, conn);
	idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_TRYAGAIN, _try_again_handler, conn);

	tp_contacts_mixin_add_contact_attributes_iface ((GObject *) conn,
		TP_IFACE_CONNECTION_INTERFACE_CONTACT_INFO,
		idle_contact_info_fill_contact_attributes);
}

void idle_contact_info_iface_init(gpointer g_iface, gpointer iface_data) {
	TpSvcConnectionInterfaceContactInfoClass *klass = (TpSvcConnectionInterfaceContactInfoClass *) g_iface;

#define IMPLEMENT(x) tp_svc_connection_interface_contact_info_implement_##x (\
		klass, idle_connection_##x)
	IMPLEMENT(request_contact_info);
#undef IMPLEMENT
}
