/* dia-solver.c
 * Copyright (C) 2001  Arjan Molenaar
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "dia-solver.h"
#include "dia-canvas-i18n.h"

///#define D(msg) G_STMT_START { g_print (G_STRLOC": "); g_print msg; g_print ("\n"); } G_STMT_END
#define D(msg)

enum
{
	LAST_SIGNAL
};

static void dia_solver_init		(DiaSolver		*solver);
static void dia_solver_class_init	(DiaSolverClass		*klass);
static void dia_solver_finalize		(GObject		*object);
static void dia_solver_set_property	(GObject		*object,
					 guint			 property_id,
					 const GValue		*value,
					 GParamSpec		*pspec);
static void dia_solver_get_property	(GObject		*object,
					 guint			 property_id,
					 GValue			*value,
					 GParamSpec		*pspec);

static inline void mark_constraint	(DiaSolver		*solver,
					 DiaConstraint		*constraint);
static inline void unmark_constraint	(DiaSolver		*solver,
					 DiaConstraint		*constraint);
static void unmark_destroyed_constraint	(DiaSolver		*solver,
					 DiaConstraint		*constraint);
static inline void mark_variable	(DiaSolver		*solver,
					 DiaVariable		*var);
static inline void mark_variable_internal (DiaSolver		*solver,
					 DiaVariable		*var);
static inline void unmark_variable	(DiaSolver		*solver,
					 DiaVariable		*var);
static void unmark_destroyed_variable	(DiaSolver		*solver,
					 DiaVariable		*var);

static void need_resolve_cb		(DiaConstraint		*con,
					 DiaVariable		*var,
					 DiaSolver		*sol);

static GObjectClass *parent_class = NULL;
//static guint signals[LAST_SIGNAL] = { 0 };

GType
dia_solver_get_type (void)
{
	static GType object_type = 0;

	if (!object_type) {
		static const GTypeInfo object_info = {
			sizeof (DiaSolverClass),
			(GBaseInitFunc) NULL,
			(GBaseFinalizeFunc) NULL,
			(GClassInitFunc) dia_solver_class_init,
			(GClassFinalizeFunc) NULL,
			(gconstpointer) NULL, /* class_data */
			sizeof (DiaSolver),
			(guint16) 0, /* n_preallocs */
			(GInstanceInitFunc) dia_solver_init,
		};
		object_type = g_type_register_static (G_TYPE_OBJECT,
						      "DiaSolver",
						      &object_info,
						      0);
	}

	return object_type;
}


static void
dia_solver_class_init (DiaSolverClass *klass)
{
	GObjectClass *object_class;

	object_class = (GObjectClass*) klass;

	parent_class = g_type_class_peek_parent (klass);

	object_class->finalize = dia_solver_finalize;
	object_class->set_property = dia_solver_set_property;
	object_class->get_property = dia_solver_get_property;

	/*signals[CHANGED] = 
	  g_signal_new ("changed",
			G_TYPE_FROM_CLASS (klass),
			G_SIGNAL_RUN_LAST,
			G_STRUCT_OFFSET (DiaSolverClass, changed),
			NULL, NULL,
			dia_solver_marshal_VOID__VOID,
			G_TYPE_NONE, 0); */
}


static void
dia_solver_init (DiaSolver *solver)
{
	solver->constraints = NULL;

	solver->marked_cons = NULL;
	solver->marked_vars = NULL;
	solver->current_con = NULL;
}

static void
dia_solver_finalize (GObject *object)
{
	DiaSolver *solver = (DiaSolver*) object;

	while (solver->constraints)
		dia_solver_remove_constraint (solver, solver->constraints->data);

	while (solver->marked_cons)
		unmark_constraint (solver, solver->marked_cons->data);

	while (solver->marked_vars)
		unmark_variable (solver, solver->marked_vars->data);

	parent_class->finalize (object);
}

static void
dia_solver_set_property (GObject *object, guint property_id,
			 const GValue *value, GParamSpec *pspec)
{
	switch (property_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}

static void
dia_solver_get_property (GObject *object, guint property_id,
			 GValue *value, GParamSpec *pspec)
{
	switch (property_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
	}
}

/**
 * mark_constraint:
 * @solver: 
 * @con: 
 *
 * Add a constraint to the marked_cons list. It will be resolved.
 **/
static inline void
mark_constraint (DiaSolver *solver, DiaConstraint *con)
{
	if (g_slist_find (solver->marked_cons, con))
		return;

	solver->marked_cons = g_slist_append (solver->marked_cons, con);
	g_object_weak_ref (G_OBJECT (con),
			   (GWeakNotify) unmark_destroyed_constraint,
			   solver);
}

static inline void
unmark_constraint (DiaSolver *solver, DiaConstraint *con)
{
	solver->marked_cons = g_slist_remove (solver->marked_cons, con);
	g_object_weak_unref (G_OBJECT (con),
			     (GWeakNotify) unmark_destroyed_constraint,
			     solver);
}

static void
unmark_destroyed_constraint (DiaSolver *solver, DiaConstraint *con)
{
	solver->marked_cons = g_slist_remove (solver->marked_cons, con);
}

/**
 * mark_variable:
 * @solver: 
 * @var: 
 *
 * All variables that are edited (by the user and by the solver) are added to
 * a list, do they can fire the %changed signal when the solver is done.
 **/
static inline void
mark_variable (DiaSolver *solver, DiaVariable *var)
{
	if (g_slist_find (solver->marked_vars, var))
		return;

	solver->marked_vars = g_slist_append (solver->marked_vars, var);
	g_object_weak_ref (G_OBJECT (var),
			   (GWeakNotify) unmark_destroyed_variable,
			   solver);
}

static inline void
mark_variable_internal (DiaSolver *solver, DiaVariable *var)
{
	if (g_slist_find (solver->marked_vars, var))
		return;

	solver->marked_vars = g_slist_prepend (solver->marked_vars, var);
	g_object_weak_ref (G_OBJECT (var),
			   (GWeakNotify) unmark_destroyed_variable,
			   solver);
}

static inline void
unmark_variable (DiaSolver *solver, DiaVariable *var)
{
	solver->marked_vars = g_slist_remove (solver->marked_vars, var);
	g_object_weak_unref (G_OBJECT (var),
			     (GWeakNotify) unmark_destroyed_variable,
			     solver);
}

static void
unmark_destroyed_variable (DiaSolver *solver, DiaVariable *var)
{
	solver->marked_vars = g_slist_remove (solver->marked_vars, var);
}

static void
need_resolve_cb (DiaConstraint *con, DiaVariable *var, DiaSolver *sol)
{
	if (sol->current_con != con) {
		mark_constraint (sol, con);
		mark_variable (sol, var);
	}
}


/**
 * dia_solver_new:
 *
 * Create a new, empty constaint solver.
 *
 * Return value: A new constraint solver.
 **/
DiaSolver*
dia_solver_new (void)
{
	return g_object_new (DIA_TYPE_SOLVER, NULL);
}


static void
add_variable_to_marked_vars (DiaConstraint *con, DiaVariable *var, gdouble c,
			     gpointer solver)
{
	if (var)
		mark_variable (solver, var);
}

/**
 * dia_solver_add_constraint:
 * @solver: 
 * @constraint: 
 *
 * Add @constraint to @solver. The constraint is marked immutable.
 **/
void
dia_solver_add_constraint (DiaSolver *solver, DiaConstraint *constraint)
{
	g_return_if_fail (DIA_IS_SOLVER (solver));
	g_return_if_fail (DIA_IS_CONSTRAINT (constraint));
	g_return_if_fail (dia_constraint_has_variables (constraint));

	if (solver->constraints
	    && (g_list_find (solver->constraints, constraint) != NULL)) {
		g_warning (_("Tried to add a constraint multiple times to the solver."));
		return;
	}

	g_object_ref (constraint);
	dia_constraint_optimize (constraint);
	dia_constraint_freeze (constraint);
	
	solver->constraints = g_list_append (solver->constraints, constraint);
	mark_constraint (solver, constraint);
	
	dia_constraint_foreach (constraint, add_variable_to_marked_vars,
				solver);

	g_signal_connect (constraint, "need_resolve",
			  G_CALLBACK (need_resolve_cb), solver);
}

/**
 * dia_solver_remove_constraint:
 * @solver: 
 * @constraint: 
 *
 * Remove @constraint from the solver.
 **/
void
dia_solver_remove_constraint (DiaSolver *solver, DiaConstraint *constraint)
{
	g_return_if_fail (DIA_IS_SOLVER (solver));
	g_return_if_fail (DIA_IS_CONSTRAINT (constraint));
	g_return_if_fail (g_list_find (solver->constraints, constraint));
	
	solver->constraints = g_list_remove (solver->constraints, constraint);
	dia_constraint_thaw (constraint);

	if (g_slist_find (solver->marked_cons, constraint))
		unmark_constraint (solver, constraint);

	g_signal_handlers_disconnect_by_func (constraint,
					      G_CALLBACK (need_resolve_cb),
					      solver);
	g_object_unref (constraint);
}

/**
 * find_weakest_variable:
 * @solver: 
 * @con: 
 * @wvar: [OUT] weakest variable
 *
 * Find the weakest variable in a constraint. The variable with the weakest
 * priority will be put in @wvar. Variables that are also edited will have
 * a slightly increated strength over variables who have not been edited.
 * If more edited variables have been found with the same strength, the variable
 * that was first edited will be the weakest.
 * 
 * Note that only one variable is returned. This could lead to not so
 * nice solutions.  Maybe we should create a list of weakest variables.
 * 
 * Return value: TRUE if the weakest variable is in the marked_vars list,
 * 		 FALSE otherwise.
 **/
static gboolean
find_weakest_variable (DiaSolver *solver, DiaConstraint *con, DiaVariable **wvar)
{
	guint i;
	gint eindex = G_MAXINT;
	DiaVariable *nevar = NULL; /* Not edited variable */
	DiaVariable *evar = NULL; /* Edited variable */

	for (i = 0; i < con->expr->len; i++) {
		DiaVariable *v = con->expr->elem[i].variable;
		register gint index = g_slist_index (solver->marked_vars, v);

		if (!v)
			continue;

		/* Find the weakest non-edit variable. The weakest edited
		 * variable depends on its strength and the index in the
		 * solver->marked_vars list. */

		if ((index == -1) && ((nevar == NULL)
				      || (nevar->strength > v->strength))) {
			nevar = v;
		} else if ((evar == NULL)
			   || (evar->strength > v->strength)
			   || ((evar->strength == v->strength)
				&& (index < eindex))) {
			evar = v;
			eindex = index;
		}
	}

	if (nevar) {
		if (evar) {
			if (evar->strength < nevar->strength)
				*wvar = evar;
			else
				*wvar = nevar;
		} else
			*wvar = nevar;
	} else
		*wvar = evar;
	
	if (*wvar == NULL) {
		g_warning ("No editable variable found in constraint.");
		return FALSE;
	}

	if (*wvar == evar)
		return TRUE;

	return FALSE;
}

/**
 * dia_solver_resolve:
 * @solver: 
 *
 * Try to make all constraints true. 
 **/
void
dia_solver_resolve (DiaSolver *solver)
{
	static guint changed_signal = 0;
	DiaVariable *wvar = NULL;
	DiaConstraint *con;
	GSList *cyclic = NULL;
	gdouble new_val;

	if (!changed_signal)
		changed_signal = g_signal_lookup ("changed", DIA_TYPE_VARIABLE);

	/* foreach (infeasable constraint) {
	 * 1	remove the constraint from the marked_cons list
	 * 2	find  the weakest variable that is not in the
	 * 	solver->marked_vars list. If no such variable can be
	 * 	found, return the overall weakest and add the constraint to the
	 * 	cyclic list (so it will not be solved again).
	 * 	wvar = find_weakest_var (solver, constraint)
	 * 3	make constraint = 0, by changing wvar. signals
	 *	to constraint are blocked by using current_con.
	 *	solve_constraint (constraint, wvar);
	 * }
	 */

	while (solver->marked_cons) {
		con = solver->marked_cons->data;
		solver->current_con = con;

		/* remove from list */
		unmark_constraint (solver, con);

		/* Skip the constraint if it is already solved using an edited
		 * variable. */
		if (g_slist_find (cyclic, con))
			continue;

		if (find_weakest_variable (solver, con, &wvar))
			cyclic = g_slist_prepend (cyclic, con);

		if (wvar) {
			new_val = dia_constraint_solve (con, wvar);
			if (dia_variable_get_value (wvar) != new_val) {
				dia_variable_set_value (wvar, new_val);
				mark_variable_internal (solver, wvar);
			}
		}
	}
	
	/* Cleanup: */
	g_slist_free (cyclic);

	while (solver->marked_cons)
		unmark_constraint (solver, solver->marked_cons->data);

	while (solver->marked_vars) {
		g_signal_emit (solver->marked_vars->data, changed_signal, 0);
		unmark_variable (solver, solver->marked_vars->data);
	}

	solver->current_con = NULL;
}


