/* bug-buddy bug submitting program
 *
 * Copyright (C) 2001 Jacob Berkman
 * Copyright 2001 Ximian, Inc.
 *
 * Author:  jacob berkman  <jacob@bug-buddy.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

#include <config.h>

#include "bug-buddy.h"
#include "distribution.h"

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <utime.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

#include <glib/gi18n.h>

#include <gmenu-tree.h>

#include <bonobo/bonobo-exception.h>
#include <bonobo-activation/bonobo-activation.h>

#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/xmlmemory.h>

#include <libsoup/soup.h>


#define APPLET_REQUIREMENTS \
	"has_all (repo_ids, ['IDL:Bonobo/Control:1.0'," \
	"		     'IDL:GNOME/Vertigo/PanelAppletShell:1.0']) && " \
	"defined (panel:icon)"

#define DESKTOP_ENTRY			"Desktop Entry"
#define DESKTOP_NAME			"Name"
#define DESKTOP_COMMENT			"Comment"
#define DESKTOP_ICON			"Icon"
#define DESKTOP_EXEC			"Exec"

#define BUGZILLA_BUGZILLA               "X-GNOME-Bugzilla-Bugzilla"
#define BUGZILLA_PRODUCT                "X-GNOME-Bugzilla-Product"
#define BUGZILLA_COMPONENT              "X-GNOME-Bugzilla-Component"
#define BUGZILLA_EMAIL                  "X-GNOME-Bugzilla-Email"
#define BUGZILLA_VERSION                "X-GNOME-Bugzilla-Version"
#define BUGZILLA_OTHER_BINARIES         "X-GNOME-Bugzilla-OtherBinaries"
#define BUGZILLA_EXTRA_INFO_SCRIPT      "X-GNOME-Bugzilla-ExtraInfoScript"

#define BA_BUGZILLA_BUGZILLA            "bugzilla:bugzilla"
#define BA_BUGZILLA_PRODUCT             "bugzilla:product"
#define BA_BUGZILLA_COMPONENT           "bugzilla:component"
#define BA_BUGZILLA_VERSION             "bugzilla:version"
#define BA_BUGZILLA_OTHER_BINARIES      "bugzilla:other_binaries"
#define BA_BUGZILLA_EXTRA_INFO_SCRIPT   "bugzilla:extra_info_script"

static void
add_bugzilla_application (GHashTable *hash, 
			  const char *name, 
			  const char *cname, 
			  const char *comment, 
			  const char *bugzilla, 
			  const char *product,
			  const char *component, 
			  const char *version, 
			  const char *icon,
			  const char *program,
			  const char *other_programs,
			  const char *extra_info_script)
{
	BugzillaApplication *app;
	char **programv;
	int i;

	app = g_new0 (BugzillaApplication, 1);
	
	app->name              = g_strdup (name);
	app->cname             = g_strdup (cname);
	app->comment           = g_strdup (comment);
	app->icon	       = g_strdup (icon);
	app->bugzilla          = g_strdup (bugzilla);
	app->product           = g_strdup (product);
	app->component         = g_strdup (component);
	app->version           = g_strdup (version);
	app->extra_info_script = g_strdup (extra_info_script);

	if (program) {
		g_shell_parse_argv (program, &i, &programv, NULL);
		if (programv[0]) {
			char *s;
			s = strrchr (programv[0], G_DIR_SEPARATOR);
			s = s ? s+1 : programv[0];
			app->ref_count += 1;
			g_hash_table_insert (hash, g_strdup (s), app);
		} else {
			g_free (app);
			return;
		}
		if (programv)
			g_strfreev (programv);
	}
		
	if (other_programs) {
		programv = g_strsplit (other_programs, ";", -1);
		for (i=0; programv[i]; i++) {
			app->ref_count += 1;
			g_hash_table_insert (hash, g_strdup (programv[i]), app);
		}
		g_strfreev (programv);
	}
}

static void
application_free (BugzillaApplication *app)
{
	app->ref_count -= 1;
	if (app->ref_count > 0)
		return;

	g_free (app->name);
	g_free (app->cname);
	g_free (app->comment);
	g_free (app->icon);
	g_free (app->bugzilla);
	g_free (app->product);
	g_free (app->component);
	g_free (app->version);
	g_free (app->extra_info_script);
	g_free (app);
}


	

static const GSList *
get_i18n_slist (void)
{
  const char * const *langs;
  guint i;
  static GSList *langs_gslist = NULL;

  if (langs_gslist)
	  return langs_gslist;

  langs = g_get_language_names ();
  for (i = 0; langs[i] != 0; ++i) {
	  langs_gslist = g_slist_append (langs_gslist, (gpointer) langs[i]);
  }

  return langs_gslist;
}

static void
load_applets (GHashTable *hash)
{
	Bonobo_ServerInfoList *info_list;
	Bonobo_ServerInfo *info;
	CORBA_Environment ev;
	GSList *langs;
	int i;
        gchar *name;

	CORBA_exception_init (&ev);
	info_list = bonobo_activation_query (APPLET_REQUIREMENTS, NULL, &ev);
	if (BONOBO_EX (&ev)) {
		g_warning ("Applet list query failed: %s", BONOBO_EX_REPOID (&ev));
		CORBA_exception_free (&ev);
		return;
	}
	CORBA_exception_free (&ev);

	langs = (GSList *)get_i18n_slist ();

	for (i = 0; i < info_list->_length; i++) {
		info = info_list->_buffer + i;
		if (!bonobo_server_info_prop_lookup (info,
						     BA_BUGZILLA_BUGZILLA,
NULL)) {
			continue;
		}

		name = g_strdup (bonobo_server_info_prop_lookup (info, "name", langs));
		/*FIXME:
		for (l = applications; l; l = l->next) {
			BugzillaApplication *app = l->data;
		
			if (strcmp (app->name, name) == 0) {
				g_free (name);
				name = g_strdup_printf (_("%s (Panel Applet)"), bonobo_server_info_prop_lookup (info, "name", langs));
	
				break;
			}
		}*/

		add_bugzilla_application (hash,
			name,
			bonobo_server_info_prop_lookup (info, "name", NULL),
			bonobo_server_info_prop_lookup (info, "description", langs),
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_BUGZILLA,  NULL),
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_PRODUCT,   NULL),
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_COMPONENT, NULL),
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_VERSION, NULL),
			bonobo_server_info_prop_lookup (info, "panel:icon", NULL),
			NULL,
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_OTHER_BINARIES, NULL),
			bonobo_server_info_prop_lookup (info, BA_BUGZILLA_EXTRA_INFO_SCRIPT, NULL));
		
		g_free (name);
	}

	CORBA_free (info_list);
}

static int
compare_applications (GMenuTreeEntry *a,
		      GMenuTreeEntry *b)
{
	return g_utf8_collate (gmenu_tree_entry_get_name (a),
			       gmenu_tree_entry_get_name (b));
}

static GSList *get_all_applications_from_dir (GMenuTreeDirectory *directory,
					      GSList             *list);

static GSList *
get_all_applications_from_alias (GMenuTreeAlias *alias,
				 GSList         *list)
{
	GMenuTreeItem *aliased_item;

	aliased_item = gmenu_tree_alias_get_item (alias);

	switch (gmenu_tree_item_get_type (aliased_item)) {
	case GMENU_TREE_ITEM_DIRECTORY:
		list = get_all_applications_from_dir (GMENU_TREE_DIRECTORY (aliased_item), list);
		break;

	case GMENU_TREE_ITEM_ENTRY:
		list = g_slist_append (list, gmenu_tree_item_ref (aliased_item));
		break;

	default:
		break;
	}

	gmenu_tree_item_unref (aliased_item);

	return list;
}

static GSList *
get_all_applications_from_dir (GMenuTreeDirectory *directory,
			       GSList             *list)
{
	GSList *items;
	GSList *l;

	if (g_main_context_pending (NULL)) {
		g_main_context_iteration (NULL, FALSE);
	}

	items = gmenu_tree_directory_get_contents (directory);
	for (l = items; l; l = l->next) {
		GMenuTreeItem *item = l->data;

		switch (gmenu_tree_item_get_type (item)) {
		case GMENU_TREE_ITEM_DIRECTORY:
			list = get_all_applications_from_dir (GMENU_TREE_DIRECTORY (item), list);
			break;

		case GMENU_TREE_ITEM_ENTRY:
			list = g_slist_append (list, gmenu_tree_item_ref (item));
			break;

		case GMENU_TREE_ITEM_ALIAS:
			list = get_all_applications_from_alias (GMENU_TREE_ALIAS (item), list);
			break;

		default:
			break;
		}

		gmenu_tree_item_unref (item);
	}

	g_slist_free (items);

	return list;
}

static GSList *
get_all_applications (void)
{
	GMenuTree          *tree;
	GMenuTreeDirectory *root;
	GSList             *retval;
	const char         *menufile = BUDDY_DATADIR "/bug-buddy.menu";

	if (g_file_test (menufile, G_FILE_TEST_IS_REGULAR)) {
		/* use a custom menu file to scan desktop entry files so we aren't limited
		 * to reporting bugs only present in the applications menu*/
		tree = gmenu_tree_lookup (menufile, GMENU_TREE_FLAGS_INCLUDE_NODISPLAY);
	} else {
		/* fallback to using the applications menu */
		tree = gmenu_tree_lookup ("applications.menu", GMENU_TREE_FLAGS_INCLUDE_NODISPLAY);
	}

	root = gmenu_tree_get_root_directory (tree);

	retval = get_all_applications_from_dir (root, NULL);

	gmenu_tree_item_unref (root);
	gmenu_tree_unref (tree);

	retval = g_slist_sort (retval, (GCompareFunc) compare_applications);

	return retval;
}

GQuark
bugzilla_error_quark (void)
{
	  return g_quark_from_static_string ("bugzilla_error");
}

GHashTable *
load_applications (void)
{
	GSList         *all_applications;
	GSList         *l;
	char           *prev_name = NULL;
	GError	       *error = NULL;

	GHashTable *program_to_application = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) application_free);

	all_applications = get_all_applications ();
	for (l = all_applications; l; l = l->next) {
		GKeyFile *key_file;
		char *name;
		char *cname;
		char *comment;
		char *bugzilla;
		char *product;
		char *component;
		char *version;
		char *icon;
		char *exec;
		char *other_binaries;
		char *extra_info_script;
		GMenuTreeEntry *entry = l->data;

		if (g_main_context_pending (NULL)) {
			g_main_context_iteration (NULL, FALSE);
		}

		if (prev_name && strcmp (gmenu_tree_entry_get_name (entry), prev_name) == 0) {
			gmenu_tree_item_unref (entry);
			continue;
		}
		key_file = g_key_file_new ();
		g_key_file_load_from_file (key_file, gmenu_tree_entry_get_desktop_file_path (entry), 
					   G_KEY_FILE_NONE, &error);
		if (error) {
			g_warning ("Couldn't load %s: %s", gmenu_tree_entry_get_desktop_file_path (entry),
				   error->message);
			g_error_free (error);
			error = NULL;
			gmenu_tree_item_unref (entry);
			continue;
		}
    
		if (!g_key_file_has_group (key_file, DESKTOP_ENTRY) || !g_key_file_has_key (key_file, DESKTOP_ENTRY, BUGZILLA_BUGZILLA, &error)) {
			g_key_file_free (key_file);	
			gmenu_tree_item_unref (entry);
			if (error)
				g_error_free (error);
			continue;
		}

		name = g_key_file_get_locale_string (key_file, DESKTOP_ENTRY, DESKTOP_NAME, NULL, NULL);
		cname = g_key_file_get_string (key_file, DESKTOP_ENTRY, DESKTOP_NAME, NULL);
		comment = g_key_file_get_locale_string (key_file, DESKTOP_ENTRY, DESKTOP_COMMENT, NULL, NULL);
		bugzilla = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_BUGZILLA, NULL);
		product = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_PRODUCT, NULL);
		component = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_COMPONENT, NULL);
		version = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_VERSION, NULL);
		icon = g_key_file_get_string (key_file, DESKTOP_ENTRY, DESKTOP_ICON, NULL);
		exec = g_key_file_get_string (key_file, DESKTOP_ENTRY, DESKTOP_EXEC, NULL);
		other_binaries = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_OTHER_BINARIES, NULL);
		extra_info_script = g_key_file_get_string (key_file, DESKTOP_ENTRY, BUGZILLA_EXTRA_INFO_SCRIPT, NULL);

		add_bugzilla_application (program_to_application,
					  name,
					  cname,
					  comment,
					  bugzilla,
					  product,
					  component,
					  version,
					  icon,
					  exec,
					  other_binaries,
					  extra_info_script);
		g_free (name);
		g_free (cname);
		g_free (comment);
		g_free (bugzilla);
		g_free (product);
		g_free (component);
		g_free (version);
		g_free (icon);
		g_free (exec);
		g_free (other_binaries);
		g_free (extra_info_script);
		g_free (prev_name);
		prev_name = g_strdup (gmenu_tree_entry_get_name (entry));
		g_key_file_free (key_file);	
		gmenu_tree_item_unref (entry);
	}
	g_slist_free (all_applications);

	load_applets (program_to_application);

	return program_to_application;
}

gboolean
bugzilla_search_for_package (gpointer key, gpointer value, const char *package)
{
	BugzillaApplication *app = (BugzillaApplication*) value;

	if (!strcmp (app->product, package))
		return TRUE;
	
	return FALSE;
}


char *
bugzilla_parse_response (SoupMessage *msg, GError **err)
{
	GValue value;
	int bugid = 0;
	char *url = NULL;

	g_return_val_if_fail ((err == NULL || *err == NULL), NULL);

	if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
		g_set_error (err, BUGZILLA_ERROR, BUGZILLA_ERROR_RECV_BAD_STATUS, 
		             _("HTTP Response returned bad status code %d"), msg->status_code);
		return NULL;
	}

	if (!soup_xmlrpc_parse_method_response (msg->response_body->data,
						msg->response_body->length,
						&value, err))
		return NULL;

	if (G_VALUE_HOLDS_INT (&value))
		bugid = g_value_get_int (&value);
	else if (G_VALUE_HOLDS_STRING (&value))
		url = g_value_dup_string (&value);
	else {
		g_value_unset (&value);
		g_set_error (err, BUGZILLA_ERROR, BUGZILLA_ERROR_RECV_PARSE_FAILED, 
			     _("Unable to parse XML-RPC Response\n\n%s"),
			     msg->response_body->data);
		return NULL;
	}
	g_value_unset (&value);

	return bugid ? g_strdup_printf ("%d", bugid) : url;
}

SoupMessage*
bugzilla_create_report (BugzillaApplication *app, int type, GnomeVersionInfo *gnome_version,
		        const char *username, const char *title, const char *text,
		        GtkBuilder *ui, const char *minidump_file, GError **err)
{
	SoupMessage *message;
	const char *uri;
	GHashTable *report;
	char *user_agent;
	char *os_version;
	const gchar *crt;
    	GString *rv;

	g_return_val_if_fail (app != NULL, NULL);
	g_return_val_if_fail (gnome_version != NULL, NULL);
	g_return_val_if_fail (username != NULL, NULL);
	g_return_val_if_fail (text != NULL, NULL);
	g_return_val_if_fail (ui != NULL, NULL);
	g_return_val_if_fail (err == NULL || *err == NULL, NULL);

	/* FIXME: Hardcoded right now */
	if (app->bugzilla == NULL || strcmp (app->bugzilla, "GNOME") != 0) {
		g_set_error (err, BUGZILLA_ERROR, BUGZILLA_ERROR_SEND_NOTSUPPORTED_APP,
		             _("Application does not track its bugs in the GNOME Bugzilla."));
		return NULL;
	}

	if (!app->product || !app->component) {
		g_set_error (err, BUGZILLA_ERROR, BUGZILLA_ERROR_SEND_NOTSUPPORTED_APP,
		             _("Product or component not specified."));
		return NULL;
	}

	report = soup_value_hash_new ();

	soup_value_hash_insert (report, "version", G_TYPE_STRING,
				app->version ? app->version : "unspecified");
	soup_value_hash_insert (report, "product", G_TYPE_STRING, app->product);
	soup_value_hash_insert (report, "component", G_TYPE_STRING, app->component);
	soup_value_hash_insert (report, "gnome_version", G_TYPE_STRING,
				gnome_version->gnome_platform);
	soup_value_hash_insert (report, "reporter", G_TYPE_STRING, username);

	os_version = get_distro_name ();
	soup_value_hash_insert (report, "os_version", G_TYPE_STRING, os_version);
	g_free (os_version);

	if (type == BUG_TYPE_CRASH) {
		soup_value_hash_insert (report, "priority", G_TYPE_STRING, "High");
		soup_value_hash_insert (report, "bug_severity", G_TYPE_STRING, "critical");
	}

	soup_value_hash_insert (report, "short_desc", G_TYPE_STRING, title);

	/* Skip UTF-8 control chars that are not valid in XML.*/
    	rv = g_string_new (NULL);
    	crt = text;
    	while (*crt) {
		gchar *next = g_utf8_next_char (crt);
		gunichar uni = g_utf8_get_char (crt);

		if (!g_unichar_iscntrl (uni) || (uni == '\n') || (uni == '\t'))
	    		g_string_append_len (rv, crt, next - crt);

		crt = next;
    	}
	
	soup_value_hash_insert (report, "comment", G_TYPE_STRING, rv->str);
    	g_string_free (rv, TRUE);

	if (minidump_file) {
		gchar *minidumpbuf;
		gsize length;
		GError *error = NULL;
		gchar *base64 = NULL;

		if (g_file_get_contents (minidump_file, &minidumpbuf, &length, &error)) {
			base64 = g_base64_encode ((guchar*)minidumpbuf, length);
			g_free (minidumpbuf);
		} else {
			g_error_free (error);
		}

		if (base64) {	
			gchar *basename;
			gchar *id;
			GByteArray *ba;

			basename = g_path_get_basename (minidump_file);
			id = g_strndup (basename, strlen (basename) - strlen (".dmp"));
			
			soup_value_hash_insert (report, "minidump-id",
						G_TYPE_STRING, id);

			/* FIXME: This is broken; for some reason
			 * bug-buddy is base64-encoding the minidump
			 * *twice*. (Once itself, and once by asking
			 * libsoup to do it.) Fix this after verifying
			 * that the server side can deal with the
			 * fix...
			 */
			ba = g_byte_array_sized_new (strlen (base64));
			g_byte_array_append (ba, (guchar *)base64, strlen (base64));
			soup_value_hash_insert (report, "minidump",
						SOUP_TYPE_BYTE_ARRAY, ba);
			g_byte_array_free (ba, TRUE);
			g_free (base64);
			g_free (basename);
			g_free (id);
		}
	}

	if (minidump_file)
		//uri = "http://localhost/breakpad/xmlrpc.py";
		uri = "http://socorro.gnome.org/collect.py";
	else 
		uri = "http://bugzilla.gnome.org/bugbuddy.cgi";

	message = soup_xmlrpc_request_new (uri, "BugBuddy.createBug",
					   G_TYPE_HASH_TABLE, report,
					   G_TYPE_INVALID);
	g_hash_table_destroy (report);
	if (message == NULL) {
		g_set_error (err, BUGZILLA_ERROR, BUGZILLA_ERROR_SEND_ERROR,
		             _("Unable to create XML-RPC message."));
		return NULL;
	}

	/* FIXME: wrong User-Agent syntax. Should be "Bug-Buddy (VERSION)" */
	user_agent = g_strdup_printf ("Bug-Buddy: %s", VERSION);
	soup_message_headers_append (message->request_headers,
				     "User-Agent", user_agent);
	g_free (user_agent);
	
	return message;
}
	
