/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/*
 *  Pix
 *
 *  Copyright (C) 2012 Free Software Foundation, Inc.
 *
 *  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, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>
#include <glib.h>
#include <glib/gi18n.h>
#include <pix.h>
#include "oauth-ask-authorization-dialog.h"
#include "oauth-consumer.h"
#include "oauth-service.h"


#define OAUTH_VERSION "1.0"
#define OAUTH_SIGNATURE_METHOD "HMAC-SHA1"
#define OAUTH_CALLBACK "http://localhost/"


G_DEFINE_TYPE (OAuthService, oauth_service, WEB_TYPE_SERVICE)


enum {
        PROP_0,
        PROP_CONSUMER
};


struct _OAuthServicePrivate {
	OAuthConsumer *consumer;
	char          *timestamp;
	char          *nonce;
	char          *signature;
	char          *token;
	char          *token_secret;
	char          *verifier;
};


static void
oauth_service_set_property (GObject      *object,
			    guint         property_id,
			    const GValue *value,
			    GParamSpec   *pspec)
{
	OAuthService *self;

        self = OAUTH_SERVICE (object);

	switch (property_id) {
	case PROP_CONSUMER:
		self->priv->consumer = g_value_get_pointer (value);
		break;
	default:
		break;
	}
}


static void
oauth_service_get_property (GObject    *object,
			    guint       property_id,
			    GValue     *value,
			    GParamSpec *pspec)
{
	OAuthService *self;

        self = OAUTH_SERVICE (object);

	switch (property_id) {
	case PROP_CONSUMER:
		g_value_set_pointer (value, self->priv->consumer);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}


static void
oauth_service_finalize (GObject *object)
{
	OAuthService *self;

	self = OAUTH_SERVICE (object);

	g_free (self->priv->verifier);
	g_free (self->priv->token);
	g_free (self->priv->token_secret);
	g_free (self->priv->signature);
	g_free (self->priv->nonce);
	g_free (self->priv->timestamp);

	G_OBJECT_CLASS (oauth_service_parent_class)->finalize (object);
}


/* -- oauth_service_get_request_token -- */


static void
_oauth_service_get_request_token_ready_cb (SoupSession *session,
					   SoupMessage *msg,
					   gpointer     user_data)
{
	OAuthService       *self = user_data;
	GSimpleAsyncResult *result;
	SoupBuffer         *body;
	GHashTable         *values;
	char               *token;
	char               *token_secret;

	result = _web_service_get_result (WEB_SERVICE (self));

	if (msg->status_code != 200) {
		g_simple_async_result_set_error (result,
						 SOUP_HTTP_ERROR,
						 msg->status_code,
						 "%s",
						 soup_status_get_phrase (msg->status_code));
		g_simple_async_result_complete_in_idle (result);
		return;
	}

	body = soup_message_body_flatten (msg->response_body);
	values = soup_form_decode (body->data);
	token = g_hash_table_lookup (values, "oauth_token");
	token_secret = g_hash_table_lookup (values, "oauth_token_secret");
	if ((token != NULL) && (token_secret != NULL)) {
		oauth_service_set_token (self, token);
		oauth_service_set_token_secret (self, token_secret);
		g_simple_async_result_set_op_res_gboolean (result, TRUE);
	}
	else {
		GError *error;

		error = g_error_new_literal (WEB_SERVICE_ERROR, WEB_SERVICE_ERROR_GENERIC, _("Unknown error"));
		g_simple_async_result_set_from_error (result, error);
	}

	g_simple_async_result_complete_in_idle (result);

	g_hash_table_destroy (values);
	soup_buffer_free (body);
}


static void
_oauth_service_get_request_token (OAuthService        *self,
				  GCancellable        *cancellable,
				  GAsyncReadyCallback  callback,
				  gpointer             user_data)
{
	GHashTable  *data_set;
	SoupMessage *msg;

	data_set = g_hash_table_new (g_str_hash, g_str_equal);
	g_hash_table_insert (data_set, "oauth_callback", OAUTH_CALLBACK);
	oauth_service_add_signature (self, "POST", self->priv->consumer->request_token_url, data_set);
	msg = soup_form_request_new_from_hash ("POST", self->priv->consumer->request_token_url, data_set);
	_web_service_send_message (WEB_SERVICE (self),
				   msg,
				   cancellable,
				   callback,
				   user_data,
				   _oauth_service_get_request_token,
				   _oauth_service_get_request_token_ready_cb,
				   self);

	g_hash_table_destroy (data_set);
}


gboolean
oauth_service_get_request_token_finish (OAuthService  *self,
					GAsyncResult  *result,
					GError       **error)
{
	if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
		return FALSE;
	else
		return TRUE;
}


/* -- _oauth_service_get_access_token -- */


static void
_oauth_service_get_access_token_ready_cb (SoupSession *session,
					  SoupMessage *msg,
					  gpointer     user_data)
{
	OAuthService       *self = user_data;
	GSimpleAsyncResult *result;
	SoupBuffer         *body;

	result = _web_service_get_result (WEB_SERVICE (self));

	if (msg->status_code != 200) {
		g_simple_async_result_set_error (result,
						 SOUP_HTTP_ERROR,
						 msg->status_code,
						 "%s",
						 soup_status_get_phrase (msg->status_code));
		g_simple_async_result_complete_in_idle (result);
		return;
	}

	body = soup_message_body_flatten (msg->response_body);
	self->priv->consumer->access_token_response (self, msg, body, result);
	g_simple_async_result_complete_in_idle (result);

	soup_buffer_free (body);
}


static void
_oauth_service_get_access_token (OAuthService        *self,
				 const char          *verifier,
				 GCancellable        *cancellable,
				 GAsyncReadyCallback  callback,
				 gpointer             user_data)
{
	GHashTable  *data_set;
	SoupMessage *msg;

	data_set = g_hash_table_new (g_str_hash, g_str_equal);
	if (verifier != NULL)
		g_hash_table_insert (data_set, "oauth_verifier", (gpointer) verifier);
	oauth_service_add_signature (self, "POST", self->priv->consumer->access_token_url, data_set);
	msg = soup_form_request_new_from_hash ("POST", self->priv->consumer->access_token_url, data_set);
	_web_service_send_message (WEB_SERVICE (self),
				   msg,
				   cancellable,
				   callback,
				   user_data,
				   _oauth_service_get_access_token,
				   _oauth_service_get_access_token_ready_cb,
				   self);

	g_hash_table_destroy (data_set);
}


static OAuthAccount *
_oauth_service_get_access_token_finish (OAuthService  *self,
					GAsyncResult  *result,
					GError       **error)
{
	if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
		return FALSE;
	else
		return g_object_ref (g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result)));
}


/* -- oauth_service_ask_authorization  -- */


static void
get_access_token_ready_cb (GObject      *source_object,
			   GAsyncResult *result,
			   gpointer      user_data)
{
	OAuthService *self = user_data;
	GError       *error = NULL;
	GtkWidget    *dialog;
	OAuthAccount *account;

	dialog = _web_service_get_auth_dialog (WEB_SERVICE (self));
	account = _oauth_service_get_access_token_finish (self, result, &error);
	if (account == NULL) {
		gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
		gth_task_completed (GTH_TASK (self), error);
		return;
	}

	web_service_set_current_account (WEB_SERVICE (self), account);
	gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);

	g_object_unref (account);
}


static void
ask_authorization_dialog_load_request_cb (OAuthAskAuthorizationDialog *dialog,
					  gpointer                     user_data)
{
	OAuthService *self = user_data;
	const char   *uri;

	uri = oauth_ask_authorization_dialog_get_uri (dialog);
	if (uri == NULL)
		return;

	if (g_str_has_prefix (uri, OAUTH_CALLBACK)) {
		const char *uri_data;
		GHashTable *data;
		gboolean    success = FALSE;

		uri_data = uri + strlen (OAUTH_CALLBACK "?");

		data = soup_form_decode (uri_data);
		_g_strset (&self->priv->token, g_hash_table_lookup (data, "oauth_token"));

		if (self->priv->token != NULL) {
			gtk_widget_hide (GTK_WIDGET (dialog));
			gth_task_dialog (GTH_TASK (self), FALSE, NULL);

			success = TRUE;
			_oauth_service_get_access_token (self,
							 g_hash_table_lookup (data, "oauth_verifier"),
							 gth_task_get_cancellable (GTH_TASK (self)),
							 get_access_token_ready_cb,
							 self);
		}

		if (! success)
			gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);

		g_hash_table_destroy (data);
	}
}


static void
get_request_token_ready_cb (GObject      *source_object,
	        	    GAsyncResult *result,
	        	    gpointer      user_data)
{
	OAuthService *self = user_data;
	GError       *error = NULL;
	char         *url;
	GtkWidget    *dialog;

	if (! oauth_service_get_request_token_finish (self, result, &error)) {
		gth_task_completed (GTH_TASK (self), error);
		return;
	}

	url = self->priv->consumer->get_authorization_url (self);
	dialog = oauth_ask_authorization_dialog_new (url);
	_gtk_window_resize_to_fit_screen_height (dialog, 1024);
	_web_service_set_auth_dialog (WEB_SERVICE (self), GTK_DIALOG (dialog));
	g_signal_connect (OAUTH_ASK_AUTHORIZATION_DIALOG (dialog),
			  "load-request",
			  G_CALLBACK (ask_authorization_dialog_load_request_cb),
			  self);
	gtk_widget_show (dialog);

	g_free (url);
}


static void
oauth_service_ask_authorization (WebService *base)
{
	OAuthService *self = OAUTH_SERVICE (base);

	oauth_service_set_token (self, NULL);
	oauth_service_set_token_secret (self, NULL);
	_oauth_service_get_request_token (self,
					  gth_task_get_cancellable (GTH_TASK (self)),
					  get_request_token_ready_cb,
					  self);
}


static void
oauth_service_class_init (OAuthServiceClass *klass)
{
	GObjectClass    *object_class;
	WebServiceClass *service_class;

	g_type_class_add_private (klass, sizeof (OAuthServicePrivate));

	object_class = (GObjectClass*) klass;
	object_class->set_property = oauth_service_set_property;
	object_class->get_property = oauth_service_get_property;
	object_class->finalize = oauth_service_finalize;

	service_class = (WebServiceClass*) klass;
	service_class->ask_authorization = oauth_service_ask_authorization;

	/* properties */

	g_object_class_install_property (object_class,
					 PROP_CONSUMER,
					 g_param_spec_pointer ("consumer",
                                                               "Consumer",
                                                               "",
                                                               G_PARAM_READWRITE));
}


static void
oauth_service_init (OAuthService *self)
{
	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, OAUTH_TYPE_SERVICE, OAuthServicePrivate);
	self->priv->consumer = NULL;
	self->priv->timestamp = NULL;
	self->priv->nonce = NULL;
	self->priv->signature = NULL;
	self->priv->token = NULL;
	self->priv->token_secret = NULL;
	self->priv->verifier = NULL;
}


/* -- oauth_service_add_signature -- */


static char *
oauth_create_timestamp (GTimeVal *t)
{
	return g_strdup_printf ("%ld", t->tv_sec);
}


static char *
oauth_create_nonce (GTimeVal *t)
{
	char *s;
	char *v;

	s = g_strdup_printf ("%ld%u", t->tv_usec, g_random_int ());
	v = g_compute_checksum_for_string (G_CHECKSUM_MD5, s, -1);

	g_free (s);

	return v;
}


void
oauth_service_add_signature (OAuthService *self,
			     const char   *method,
			     const char   *url,
			     GHashTable   *parameters)
{
	GTimeVal  t;
	GString  *param_string;
	GList    *keys;
	GList    *scan;
	GString  *base_string;
	GString  *signature_key;

	/* Add the OAuth specific parameters */

	g_get_current_time (&t);

	g_free (self->priv->timestamp);
	self->priv->timestamp = oauth_create_timestamp (&t);
	g_hash_table_insert (parameters, "oauth_timestamp", self->priv->timestamp);

	g_free (self->priv->nonce);
	self->priv->nonce = oauth_create_nonce (&t);
	g_hash_table_insert (parameters, "oauth_nonce", self->priv->nonce);
	g_hash_table_insert (parameters, "oauth_version", OAUTH_VERSION);
	g_hash_table_insert (parameters, "oauth_signature_method", OAUTH_SIGNATURE_METHOD);
	g_hash_table_insert (parameters, "oauth_consumer_key", (gpointer) self->priv->consumer->consumer_key);
	if (self->priv->token != NULL)
		g_hash_table_insert (parameters, "oauth_token", self->priv->token);

	/* Create the parameter string */

	param_string = g_string_new ("");
	keys = g_hash_table_get_keys (parameters);
	keys = g_list_sort (keys, (GCompareFunc) strcmp);
	for (scan = keys; scan; scan = scan->next) {
		char *key = scan->data;
		char *value = g_hash_table_lookup (parameters, key);

		g_string_append_uri_escaped (param_string, key, NULL, FALSE);
		g_string_append (param_string, "=");
		g_string_append_uri_escaped (param_string, value, NULL, FALSE);
		if (scan->next != NULL)
			g_string_append (param_string, "&");
	}

	/* Create the Base String */

	base_string = g_string_new ("");
	g_string_append_uri_escaped (base_string, method, NULL, FALSE);
	g_string_append (base_string, "&");
	g_string_append_uri_escaped (base_string, url, NULL, FALSE);
	g_string_append (base_string, "&");
	g_string_append_uri_escaped (base_string, param_string->str, NULL, FALSE);

	/* Calculate the signature value */

	signature_key = g_string_new ("");
	g_string_append_uri_escaped (signature_key, self->priv->consumer->consumer_secret, NULL, FALSE);
	g_string_append (signature_key, "&");
	if (self->priv->token_secret != NULL)
		g_string_append_uri_escaped (signature_key, self->priv->token_secret, NULL, FALSE);
	g_free (self->priv->signature);
	self->priv->signature = g_compute_signature_for_string (G_CHECKSUM_SHA1,
								G_SIGNATURE_ENC_BASE64,
							        signature_key->str,
							        signature_key->len,
							        base_string->str,
							        base_string->len);
	g_hash_table_insert (parameters, "oauth_signature", self->priv->signature);

	g_string_free (signature_key, TRUE);
	g_string_free (base_string, TRUE);
	g_list_free (keys);
	g_string_free (param_string, TRUE);
}


void
oauth_service_set_token (OAuthService *self,
			 const char   *token)
{
	_g_strset (&self->priv->token, token);
}


const char *
oauth_service_get_token (OAuthService *self)
{
	return self->priv->token;
}


void
oauth_service_set_token_secret (OAuthService *self,
				const char   *token_secret)
{
	_g_strset (&self->priv->token_secret, token_secret);
}


const char *
oauth_service_get_token_secret (OAuthService *self)
{
	return self->priv->token_secret;
}
