/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Copyright (C) 2000-2002 Ximian Inc.
 *
 *  Authors: Not Zed <notzed@lostzed.mmc.com.au>
 *           Jeffrey Stedfast <fejj@ximian.com>
 *
 * 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.
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#include <libgnomeui/gnome-dialog.h>

#include <gal/util/e-xml-utils.h>

#include "rule-context.h"
#include "filter-rule.h"

#define d(x) 

static int load (RuleContext *rc, const char *system, const char *user);
static int save (RuleContext *rc, const char *user);
static int revert (RuleContext *rc, const char *user);
static GList *rename_uri (RuleContext *rc, const char *olduri, const char *newuri, GCompareFunc cmp);
static GList *delete_uri (RuleContext *rc, const char *uri, GCompareFunc cmp);

static void rule_context_class_init (RuleContextClass *klass);
static void rule_context_init (RuleContext *rc);
static void rule_context_finalise (GObject *obj);

#define _PRIVATE(x) (((RuleContext *)(x))->priv)

struct _RuleContextPrivate {
	int frozen;
};

static GObjectClass *parent_class = NULL;

enum {
	RULE_ADDED,
	RULE_REMOVED,
	CHANGED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };


GType
rule_context_get_type (void)
{
	static GType type = 0;
	
	if (!type) {
		static const GTypeInfo info = {
			sizeof (RuleContextClass),
			NULL, /* base_class_init */
			NULL, /* base_class_finalize */
			(GClassInitFunc) rule_context_class_init,
			NULL, /* class_finalize */
			NULL, /* class_data */
			sizeof (RuleContext),
			0,    /* n_preallocs */
			(GInstanceInitFunc) rule_context_init,
		};
		
		type = g_type_register_static (G_TYPE_OBJECT, "RuleContext", &info, 0);
	}
	
	return type;
}

static void
rule_context_class_init (RuleContextClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	
	parent_class = g_type_class_ref (G_TYPE_OBJECT);
	
	object_class->finalize = rule_context_finalise;
	
	/* override methods */
	klass->load = load;
	klass->save = save;
	klass->revert = revert;
	klass->rename_uri = rename_uri;
	klass->delete_uri = delete_uri;
	
	/* signals */
	signals[RULE_ADDED] =
		g_signal_new ("rule_added",
			      RULE_TYPE_CONTEXT,
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (RuleContextClass, rule_added),
			      NULL,
			      NULL,
			      gtk_marshal_NONE__POINTER,
			      G_TYPE_NONE, 1, G_TYPE_POINTER);
	
	signals[RULE_REMOVED] =
		g_signal_new ("rule_removed",
			      RULE_TYPE_CONTEXT,
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (RuleContextClass, rule_removed),
			      gtk_marshal_NONE__POINTER,
			      G_TYPE_NONE, 1, G_TYPE_POINTER);
	
	signals[CHANGED] =
		g_signal_new ("changed",
			      RULE_TYPE_CONTEXT,
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (RuleContextClass, changed),
			      gtk_marshal_NONE__NONE,
			      G_TYPE_NONE, 0);
}

static void
rule_context_init (RuleContext *rc)
{
	rc->priv = g_malloc0 (sizeof (*rc->priv));
	
	rc->part_set_map = g_hash_table_new (g_str_hash, g_str_equal);
	rc->rule_set_map = g_hash_table_new (g_str_hash, g_str_equal);
}

static void
free_part_set (struct _part_set_map *map, void *data)
{
	g_free (map->name);
	g_free (map);
}

static void
free_rule_set (struct _rule_set_map *map, void *data)
{
	g_free (map->name);
	g_free (map);
}

static void
rule_context_finalise (GObject *obj)
{
	RuleContext *rc = (RuleContext *) obj;
	
	g_list_foreach (rc->rule_set_list, (GFunc)free_rule_set, NULL);
	g_list_free (rc->rule_set_list);
	g_hash_table_destroy (rc->rule_set_map);
	
	g_list_foreach (rc->part_set_list, (GFunc)free_part_set, NULL);
	g_list_free (rc->part_set_list);
	g_hash_table_destroy (rc->part_set_map);
	
	g_free (rc->error);
	
	g_list_foreach (rc->parts, (GFunc)g_object_unref, NULL);
	g_list_free (rc->parts);
	g_list_foreach (rc->rules, (GFunc)g_object_unref, NULL);
	g_list_free (rc->rules);
	
	if (rc->system)
		xmlFreeDoc (rc->system);
	if (rc->user)
		xmlFreeDoc (rc->user);
	
	g_free (rc->priv);
	
	G_OBJECT_CLASS (parent_class)->finalize (obj);
}

/**
 * rule_context_new:
 *
 * Create a new RuleContext object.
 * 
 * Return value: A new #RuleContext object.
 **/
RuleContext *
rule_context_new (void)
{
	return (RuleContext *) g_object_new (RULE_TYPE_CONTEXT, NULL, NULL);
}

void
rule_context_add_part_set (RuleContext *rc, const char *setname, int part_type, RCPartFunc append, RCNextPartFunc next)
{
	struct _part_set_map *map;
	
	g_assert(g_hash_table_lookup (rc->part_set_map, setname) == NULL);
	
	map = g_malloc0 (sizeof (*map));
	map->type = part_type;
	map->append = append;
	map->next = next;
	map->name = g_strdup (setname);
	g_hash_table_insert (rc->part_set_map, map->name, map);
	rc->part_set_list = g_list_append (rc->part_set_list, map);
	d(printf("adding part set '%s'\n", setname));
}

void
rule_context_add_rule_set (RuleContext *rc, const char *setname, int rule_type, RCRuleFunc append, RCNextRuleFunc next)
{
	struct _rule_set_map *map;
	
	g_assert(g_hash_table_lookup (rc->rule_set_map, setname) == NULL);
	
	map = g_malloc0 (sizeof (*map));
	map->type = rule_type;
	map->append = append;
	map->next = next;
	map->name = g_strdup (setname);
	g_hash_table_insert (rc->rule_set_map, map->name, map);
	rc->rule_set_list = g_list_append (rc->rule_set_list, map);
	d(printf("adding rule set '%s'\n", setname));
}

/**
 * rule_context_set_error:
 * @f: 
 * @error: 
 * 
 * Set the text error for the context, or NULL to clear it.
 **/
static void
rule_context_set_error (RuleContext *rc, char *error)
{
	g_assert(rc);
	
	g_free (rc->error);
	rc->error = error;
}

/**
 * rule_context_load:
 * @f: 
 * @system: 
 * @user: 
 *
 * Load a rule context from a system and user description file.
 * 
 * Return value: 
 **/
int
rule_context_load (RuleContext *rc, const char *system, const char *user)
{
	int res;
	
	g_assert(rc);
	
	d(printf("rule_context: loading %s %s\n", system, user));
	
	rc->priv->frozen++;
	res = RULE_CONTEXT_GET_CLASS (rc)->load (rc, system, user);
	rc->priv->frozen--;
	
	return res;
}

static int
load (RuleContext *rc, const char *system, const char *user)
{
	xmlNodePtr set, rule;
	struct _part_set_map *part_map;
	struct _rule_set_map *rule_map;
	
	rule_context_set_error (rc, NULL);
	
	d(printf("loading rules %s %s\n", system, user));
	
	rc->system = xmlParseFile (system);
	if (rc->system == NULL) {
		rule_context_set_error (rc, g_strdup_printf ("Unable to load system rules '%s': %s",
							     system, g_strerror (errno)));
		return -1;
	}
	
	if (strcmp (rc->system->name, "filterdescription")) {
		rule_context_set_error (rc, g_strdup_printf ("Unable to load system rules '%s': Invalid format", system));
		xmlFreeDoc (rc->system);
		rc->system = NULL;
		return -1;
	}
	/* doesn't matter if this doens't exist */
	rc->user = xmlParseFile (user);
	
	/* now parse structure */
	/* get rule parts */
	set = rc->system->children;
	while (set) {
		d(printf("set name = %s\n", set->name));
		part_map = g_hash_table_lookup (rc->part_set_map, set->name);
		if (part_map) {
			d(printf("loading parts ...\n"));
			rule = set->children;
			while (rule) {
				if (!strcmp (rule->name, "part")) {
					FilterPart *part = FILTER_PART (g_object_new (part_map->type, NULL, NULL));
					
					if (filter_part_xml_create (part, rule) == 0) {
						part_map->append (rc, part);
					} else {
						g_object_unref (part);
						g_warning ("Cannot load filter part");
					}
				}
				rule = rule->next;
			}
		}
		set = set->next;
	}
	
	/* now load actual rules */
	if (rc->user) {
		set = rc->user->children;
		while (set) {
			d(printf("set name = %s\n", set->name));
			rule_map = g_hash_table_lookup (rc->rule_set_map, set->name);
			if (rule_map) {
				d(printf("loading rules ...\n"));
				rule = set->children;
				while (rule) {
					d(printf("checking node: %s\n", rule->name));
					if (!strcmp (rule->name, "rule")) {
						FilterRule *part = FILTER_RULE (g_object_new (rule_map->type, NULL, NULL));
						
						if (filter_rule_xml_decode (part, rule, rc) == 0) {
							rule_map->append (rc, part);
						} else {
							g_object_unref (part);
							g_warning ("Cannot load filter part");
						}
					}
					rule = rule->next;
				}
			}
			set = set->next;
		}
	}
	
	return 0;
}

/**
 * rule_context_save:
 * @f: 
 * @user: 
 * 
 * Save a rule context to disk.
 * 
 * Return value: 
 **/
int
rule_context_save (RuleContext *rc, const char *user)
{
	g_assert(rc);
	g_assert(user);
	
	return RULE_CONTEXT_CLASS (rc)->save (rc, user);
}

static int
save (RuleContext *rc, const char *user)
{
	xmlDocPtr doc;
	xmlNodePtr root, rules, work;
	GList *l;
	FilterRule *rule;
	struct _rule_set_map *map;
	int ret;
	
	doc = xmlNewDoc ("1.0");
	/* FIXME: set character encoding to UTF-8? */
	root = xmlNewDocNode (doc, NULL, "filteroptions", NULL);
	xmlDocSetRootElement (doc, root);
	l = rc->rule_set_list;
	while (l) {
		map = l->data;
		rules = xmlNewDocNode (doc, NULL, map->name, NULL);
		xmlAddChild (root, rules);
		rule = NULL;
		while ((rule = map->next (rc, rule, NULL))) {
			d(printf("processing rule %s\n", rule->name));
			work = filter_rule_xml_encode (rule);
			xmlAddChild (rules, work);
		}
		l = g_list_next (l);
	}
	
	ret = e_xml_save_file (user, doc);
	
	xmlFreeDoc (doc);
	
	return ret;
}

/**
 * rule_context_revert:
 * @f: 
 * @user: 
 *
 * Reverts a rule context from a user description file.  Assumes the
 * system description file is unchanged from when it was loaded.
 * 
 * Return value: 
 **/
int
rule_context_revert(RuleContext *rc, const char *user)
{
	g_assert(rc);
	
	d(printf("rule_context: restoring %s %s\n", user));
	
	return RULE_CONTEXT_CLASS (rc)->revert (rc, user);
}

struct _revert_data {
	GHashTable *rules;
	int rank;
};

static void
revert_rule_remove (void *key, FilterRule *frule, RuleContext *rc)
{
	rule_context_remove_rule (rc, frule);
	g_object_unref (frule);
}

static void
revert_source_remove(void *key, struct _revert_data *rest_data, RuleContext *rc)
{
	g_hash_table_foreach (rest_data->rules, (GHFunc)revert_rule_remove, rc);
	g_hash_table_destroy (rest_data->rules);
	g_free (rest_data);
}

static guint
source_hashf (const char *a)
{
	if (a)
		return g_str_hash (a);
	return 0;
}

static int
source_eqf (const char *a, const char *b)
{
	return (a && b && strcmp (a, b) == 0)
		|| (a == NULL && b == NULL);
}

static int
revert (RuleContext *rc, const char *user)
{
	xmlNodePtr set, rule;
	/*struct _part_set_map *part_map;*/
	struct _rule_set_map *rule_map;
	struct _revert_data *rest_data;
	GHashTable *source_hash;
	xmlDocPtr userdoc;
	FilterRule *frule;
	
	rule_context_set_error (rc, NULL);
	
	d(printf("restoring rules %s %s\n", user));
	
	userdoc = xmlParseFile (user);
	if (userdoc == NULL)
		/* clear out anythign we have? */
		return 0;
	
	source_hash = g_hash_table_new ((GHashFunc)source_hashf, (GCompareFunc)source_eqf);
	
	/* setup stuff we have now */
	/* Note that we assume there is only 1 set of rules in a given rule context,
	   although other parts of the code dont assume this */
	frule = NULL;
	while ((frule = rule_context_next_rule (rc, frule, NULL))) {
		rest_data = g_hash_table_lookup (source_hash, frule->source);
		if (rest_data == NULL) {
			rest_data = g_malloc0 (sizeof (*rest_data));
			rest_data->rules = g_hash_table_new (g_str_hash, g_str_equal);
			g_hash_table_insert (source_hash, frule->source, rest_data);
		}
		g_hash_table_insert (rest_data->rules, frule->name, frule);
	}
	
	/* make what we have, match what we load */
	set = userdoc->children;
	while (set) {
		d(printf("set name = %s\n", set->name));
		rule_map = g_hash_table_lookup (rc->rule_set_map, set->name);
		if (rule_map) {
			d(printf("loading rules ...\n"));
			rule = set->children;
			while (rule) {
				d(printf("checking node: %s\n", rule->name));
				if (!strcmp (rule->name, "rule")) {
					FilterRule *part = FILTER_RULE (g_object_new (rule_map->type, NULL, NULL));
					
					if (filter_rule_xml_decode (part, rule, rc) == 0) {
						/* use the revert data to keep track of the right rank of this rule part */
						rest_data = g_hash_table_lookup (source_hash, part->source);
						if (rest_data == NULL) {
							rest_data = g_malloc0 (sizeof (*rest_data));
							rest_data->rules = g_hash_table_new (g_str_hash, g_str_equal);
							g_hash_table_insert (source_hash, part->source, rest_data);
						}
						frule = g_hash_table_lookup (rest_data->rules, part->name);
						if (frule) {
							if (rc->priv->frozen == 0 && !filter_rule_eq (frule, part))
								filter_rule_copy (frule, part);
							
							g_object_unref (part);
							rule_context_rank_rule (rc, frule, rest_data->rank);
							g_hash_table_remove (rest_data->rules, frule->name);
						} else {
							rule_context_add_rule (rc, part);
							rule_context_rank_rule (rc, part, rest_data->rank);
						}
						rest_data->rank++;
					} else {
						g_object_unref (part);
						g_warning ("Cannot load filter part");
					}
				}
				rule = rule->next;
			}
		}
		set = set->next;
	}
	
	xmlFreeDoc (userdoc);
	
	/* remove any we still have that weren't in the file */
	g_hash_table_foreach (source_hash, (GHFunc)revert_source_remove, rc);
	g_hash_table_destroy (source_hash);
	
	return 0;
}

FilterPart *
rule_context_find_part (RuleContext *rc, const char *name)
{
	g_assert(rc);
	g_assert(name);
	
	d(printf("find part : "));
	return filter_part_find_list (rc->parts, name);
}

FilterPart *
rule_context_create_part (RuleContext *rc, const char *name)
{
	FilterPart *part;
	
	g_assert(rc);
	g_assert(name);
	
	if ((part = rule_context_find_part (rc, name)))
		return filter_part_clone (part);
	
	return NULL;
}

FilterPart *
rule_context_next_part (RuleContext *rc, FilterPart *last)
{
	g_assert(rc);
	
	return filter_part_next_list (rc->parts, last);
}

FilterRule *
rule_context_next_rule (RuleContext *rc, FilterRule *last, const char *source)
{
	g_assert(rc);
	
	return filter_rule_next_list (rc->rules, last, source);
}

FilterRule *
rule_context_find_rule (RuleContext *rc, const char *name, const char *source)
{
	g_assert(name);
	g_assert(rc);
	
	return filter_rule_find_list (rc->rules, name, source);
}

void
rule_context_add_part (RuleContext *rc, FilterPart *part)
{
	g_assert(rc);
	g_assert(part);
	
	rc->parts = g_list_append (rc->parts, part);
}

void
rule_context_add_rule (RuleContext *rc, FilterRule *new)
{
	g_assert(rc);
	g_assert(new);
	
	d(printf("add rule '%s'\n", new->name));
	
	rc->rules = g_list_append (rc->rules, new);
	
	if (rc->priv->frozen == 0) {
		g_signal_emit (rc, signals[RULE_ADDED], 0, new);
		g_signal_emit (rc, signals[CHANGED], 0);
	}
}

static void
new_rule_clicked (GtkWidget *dialog, int button, RuleContext *context)
{
	if (button == 0) {
		FilterRule *rule = g_object_get_data ((GObject *) dialog, "rule");
		char *user = g_object_get_data ((GObject *) dialog, "path");
		
		if (!filter_rule_validate (rule)) {
			/* no need to popup a dialog because the validate code does that. */
			return;
		}
		
		g_object_ref (rule);
		rule_context_add_rule (context, rule);
		if (user)
			rule_context_save (context, user);
	}
	
	if (button != -1)
		gnome_dialog_close (GNOME_DIALOG (dialog));
}

/* add a rule, with a gui, asking for confirmation first ... optionally save to path */
void
rule_context_add_rule_gui (RuleContext *rc, FilterRule *rule, const char *title, const char *path)
{
	GtkWidget *dialog, *w;
	
	d(printf("add rule gui '%s'\n", rule->name));
	
	g_assert (rc);
	g_assert (rule);
	
	w = filter_rule_get_widget (rule, rc);
	dialog = gnome_dialog_new (title, GNOME_STOCK_BUTTON_OK, GNOME_STOCK_BUTTON_CANCEL, NULL);
	gtk_window_set_policy (GTK_WINDOW (dialog), FALSE, TRUE, FALSE);
	gtk_box_pack_start (GTK_BOX (GNOME_DIALOG (dialog)->vbox), w, TRUE, TRUE, 0);
	gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 400);
	gtk_widget_show (w);
	
	g_object_set_data_full ((GObject *) dialog, "rule", rule, g_object_unref);
	if (path)
		g_object_set_data_full ((GObject *) dialog, "path", g_strdup (path), g_free);
	
	g_signal_connect (dialog, "clicked", GTK_SIGNAL_FUNC (new_rule_clicked), rc);
	
	g_object_ref (rc);
	
	g_object_set_data_full ((GObject *) dialog, "context", rc, g_object_unref);
	
	gtk_widget_show (dialog);
}

void
rule_context_remove_rule (RuleContext *rc, FilterRule *rule)
{
	g_assert (rc);
	g_assert (rule);
	
	d(printf("remove rule '%s'\n", rule->name));
	
	rc->rules = g_list_remove (rc->rules, rule);
	
	if (rc->priv->frozen == 0) {
		g_signal_emit (rc, signals[RULE_REMOVED], 0, rule);
		g_signal_emit (rc, signals[CHANGED], 0);
	}
}

void
rule_context_rank_rule (RuleContext *rc, FilterRule *rule, int rank)
{
	GList *node;
	int i = 0, index = 0;
	
	g_assert(rc);
	g_assert(rule);
	
	if (rule_context_get_rank_rule (rc, rule, rule->source) == rank)
		return;
	
	rc->rules = g_list_remove (rc->rules, rule);
	node = rc->rules;
	while (node) {
		FilterRule *r = node->data;
		
		if (i == rank) {
			rc->rules = g_list_insert (rc->rules, rule, index);
			if (rc->priv->frozen == 0)
				g_signal_emit (rc, signals[CHANGED], 0);
			
			return;
		}
		
		index++;		
		if (rule->source == NULL || (r->source && strcmp (r->source, rule->source) == 0))
			i++;
		
		node = node->next;
	}
	
	rc->rules = g_list_append (rc->rules, rule);
	if (rc->priv->frozen == 0)
		g_signal_emit (rc, signals[CHANGED], 0);
}

int
rule_context_get_rank_rule (RuleContext *rc, FilterRule *rule, const char *source)
{
	GList *node;
	int i = 0;
	
	g_assert(rc);
	g_assert(rule);
	
	d(printf("getting rank of rule '%s'\n", rule->name));
	
	node = rc->rules;
	while (node) {
		FilterRule *r = node->data;
		
		d(printf(" checking against rule '%s' rank '%d'\n", r->name, i));
		
		if (r == rule)
			return i;
		
		if (source == NULL || (r->source && strcmp (r->source, source) == 0))
			i++;
		
		node = node->next;
	}
	
	return -1;
}

FilterRule *
rule_context_find_rank_rule (RuleContext *rc, int rank, const char *source)
{
	GList *node;
	int i = 0;
	
	g_assert(rc);
	
	d(printf("getting rule at rank %d source '%s'\n", rank, source?source:"<any>"));
	
	node = rc->rules;
	while (node) {
		FilterRule *r = node->data;
		
		d(printf(" checking against rule '%s' rank '%d'\n", r->name, i));
		
		if (source == NULL || (r->source && strcmp (r->source, source) == 0)) {
			if (rank == i)
				return r;
			i++;
		}
		
		node = node->next;
	}
	
	return NULL;
}

static GList *
delete_uri (RuleContext *rc, const char *uri, GCompareFunc cmp)
{
	return NULL;
}

GList *
rule_context_delete_uri (RuleContext *rc, const char *uri, GCompareFunc cmp)
{
	return RULE_CONTEXT_GET_CLASS (rc)->delete_uri (rc, uri, cmp);
}

static GList *
rename_uri (RuleContext *rc, const char *olduri, const char *newuri, GCompareFunc cmp)
{
	return NULL;
}

GList *
rule_context_rename_uri (RuleContext *rc, const char *olduri, const char *newuri, GCompareFunc cmp)
{
	return RULE_CONTEXT_GET_CLASS (rc)->rename_uri (rc, olduri, newuri, cmp);
}

void
rule_context_free_uri_list (RuleContext *rc, GList *uris)
{
	GList *l = uris, *n;
	
	/* TODO: should be virtual */
	
	while (l) {
		n = l->next;
		g_free (l->data);
		g_list_free_1 (l);
		l = n;
	}
}
