package yacs.solver.fdsolver.consistency;

import yacs.domain.*;
import yacs.exceptions.*;
import yacs.net.*;
import yacs.parser.*;
import yacs.solver.*;

import java.util.*;

import org.apache.log4j.*;

/**
 * <p>Einfacher Solver zum Herstellen von Kantenkonsistenz über finite Domänen
 * mittels AC-3 nach Mackworth (1977, S. 106).</p>
 *
 * <p>Copyright (C) 2005  Wolfgang Runte
 * <br><br>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <br><br>
 * 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.
 * <br><br>
 * 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</p>
 *
 * @author Wolfgang Runte (woru@tzi.org)
 * @version YACS 0.1.1
 */
public class AC3Solver extends ConsistencySolver {

// ********** private attributs **********

  /** Logger aktivieren: */
  private static Logger logger = Logger.getLogger("yacs.solver.fdsolver.consistency.AC3Solver");

// ********** public attributs **********

// ********** private methods **********

  /**
   * Löscht inkonsistente Werte aus der Domäne des Startknotens der übergebenen
   * Kante. Liefert <code>true</code>, wenn Werte entfernt wurden,
   * <code>false</code> wenn nicht.
   * @param arc BinaryArc
   * @return boolean
   */
  private boolean removeInconsistentValues(BinaryArc arc) {
    boolean satisfiable;
    boolean removed = false;
    DomainElement startValue, endValue;
    String startNode = arc.startNode();
    String endNode = arc.endNode();
    Domain startDomain = arc.startDomain();
    Domain endDomain = arc.endDomain();
    Iterator itStart = startDomain.getElements().iterator();
    // Für die Startknoten nacheinander jeden möglichen Wert der Domäne
    // als aktuelle Belegung setzen:
    while (itStart.hasNext()) {
      satisfiable = false;
      startValue = (DomainElement)itStart.next();
      (arc.expression()).set(startNode, startValue);
      Iterator itEnd = endDomain.getElements().iterator();
      // ... und für alle Endknoten entsprechend auch nacheinander alle
      // möglichen Wertebelegungen setzten:
      while ((itEnd.hasNext()) && (!satisfiable)) {
        endValue = (DomainElement)itEnd.next();
        (arc.expression()).set(endNode, endValue);
        // Ausdruck mit den gesetzten Variablen auf Erfüllbarkeit testen:
        this.logger.debug("removeInconsistentValues(): arc.expression() - " + arc.expression());
        if ((arc.expression()).satisfiable()) {
          this.logger.debug("removeInconsistentValues(): satisfiable!");
          satisfiable = true;
        }
      }
      // Wenn kein passender Wert gefunden wurde, den Wert des Startknotens
      // aus der Startdomäne entfernen:
      if (!satisfiable) {
        this.logger.debug("removeInconsistentValues(): NOT satisfiable!");
        this.logger.debug("removeInconsistentValues(): Entferne Wert '"+startValue+"' aus Domäne von '"+startNode+"' der Kante: "+arc+".");
        this.logger.info("Entferne Wert '"+startValue+"' aus Domäne von '"+startNode+"'.");
        startDomain.deleteElement(startValue);
        removed = true;
      }
    }
    this.logger.debug("removeInconsistentValues(): Ende!");
    return removed;
  }

  /**
   * Gibt alle Kanten zurück, über die Nachbarknoten zum übergebenen Knoten
   * verbunden sind. Die Nachbarknoten werden jeweils als Startknoten gesetzt.
   * @param variable String
   * @param expr Expression
   * @return List
   */
  private List getNeighbors(String variable, Expression expr) {
    Expression tmpExpr;
    List variablesList;
    List arcList = new Vector();
    // Liste generieren, die ausschließlich Ausdrücke mit der übergebenen
    // Variable enthält:
    List exprList = getNeighborExpressions(variable, expr);
//this.logger.debug("getNeighbors(): Liste, die ausschließlich Ausdrücke mit '" + variable + "' enthält (" + exprList.size() + ")");
//for (int i = 0; i < exprList.size(); i++) {
//  this.logger.debug(exprList.get(i));
//}
    // ... die entsprechenden Knoten-Objekte erzeugen - dazu wird der
    // Nachbarknoten jeweils als Startknoten gesetzt:
    for (int i=0; i<exprList.size(); i++) {
      tmpExpr = (Expression)exprList.get(i);
      variablesList = tmpExpr.getVariables();
      // Ausschließlich binäre Constraints berücksichtigen:
      if (variablesList.size() == 2) {
        // Den Nachbarknoten der übergebenen Variable jeweils als Startknoten
        // setzen und der Rückgabeliste hinzufügen:
        if (((String)variablesList.get(0)).equals(variable)) {
           arcList.add(new BinaryArc((String)variablesList.get(1), variable, tmpExpr));
        } else {
           arcList.add(new BinaryArc((String)variablesList.get(0), variable, tmpExpr));
        }
      }
    }
    return arcList;
  }

  /**
   * Gibt alle (primitiven) Ausdrücke zurück, in denen die übergebene Variable
   * vorkommt.
   * @param variable String
   * @param expr Expression
   * @return List
   */
  private List getNeighborExpressions(String variable, Expression expr) {
    List exprList, variableList;
    List returnList = new Vector();
    Iterator it;
    Expression tmpExpr;
    // Liste der primitiven Ausdrücke generieren;
    exprList = expr.getPrimitiveExpressions();
//this.logger.debug("getNeighborExpressions(): Liste, die alle primitiven Ausdrücke enthält (" + exprList.size() + ")");
//for (int i = 0; i < exprList.size(); i++) {
//  this.logger.debug(exprList.get(i));
//}
    // Für alle primitiven Ausdrücke prüfen, ob sie die übergebene Variable
    // enthalten:
    it = exprList.iterator();
    while (it.hasNext()) {
      tmpExpr = (Expression)it.next();
      variableList = tmpExpr.getVariables();
      // Nur wenn Variable enthalten in die Rückgabeliste übernehmen:
      if (variableList.contains(variable)) {
        this.logger.debug("getNeighborExpressions(): '"+variable+"' IST in "+variableList+" enthalten!");
        this.logger.debug("getNeighborExpressions(): "+variableList+" wird übernommen!");
        returnList.add(tmpExpr);
      } else {
        this.logger.debug("getNeighborExpressions(): '"+variable+"' nicht in "+variableList+" enthalten.");
        this.logger.debug("getNeighborExpressions(): "+variableList+" wird NICHT übernommen!");
      }
    }
    // Rückgabeliste sollte nur primitive Ausdrücke enthalten, in denen die
    // übergebene Variable vorkommt:
    return returnList;
  }

// ********** public methods **********

  /**
   * Einfacher Constraint-Solver, der Kantenkonsistenz mittels AC-3 herstellt.
   * AC-3 kann ausschließlich binäre Constraints verarbeiten, d.h. es dürfen
   * maximal zwei Variablen in jedem primitiven Ausdruck vorhanden sein! Liefert
   * <code>true</code>, wenn Wertebereichseinschränkungen vorgenommen wurden,
   * <code>false</code> wenn nicht.
   * @param expr Expression
   * @return boolean
   * @throws OnlyBinaryConstraintsAllowedException
   * @throws InconsistencyException
   */
  public boolean arcSolve(Expression expr) throws OnlyBinaryConstraintsAllowedException, InconsistencyException {
    boolean domainModification = false;
    BinaryArc arc;
    Iterator it;
    Expression primitiveExpr;
    List variableList;
    List arcList = new Vector();
    List arcList2;
    // Liste der (primitiven) Constraints bzw. Constraint-Ausdrücke holen:
    List exprList = expr.getPrimitiveExpressions();
    // (Anm.: Anstatt an dieser Stelle die Methode "getConstraints()" aus der
    // Klasse "ConstraintNet" zu verwenden, hat der obige Aufruf den Vorteil
    // unabhängig von einem bestimmten Constraint-Netz zu sein. Bestehende
    // Konsistenzalgorithmen können dadurch z.B. auch von Suchalgorithmen
    // als "look-ahead"-Mechanismus für bestimmte Teilprobleme ("Expressions")
    // aufgerufen werden.
    //
    // Alle primitiven Ausdrücke durchgehen:
    it = exprList.iterator();
    while (it.hasNext()) {
      primitiveExpr = (Expression)it.next();
      variableList = primitiveExpr.getVariables();
      // Prüfen ob binäres Constraint:
      if (variableList.size() > 2) {
        throw new OnlyBinaryConstraintsAllowedException("AC3Solver.arcSolve(): Error -- n-ary Constraint! AC-3 can only solve binary Constraints.");
      } else if (variableList.size() == 2) {
        // Kante erzeugen:
        arc = new BinaryArc((String)variableList.get(0), (String)variableList.get(1), primitiveExpr);
        // ... und merken:
        arcList.add(arc);
        // Entgegengesetzte Kante ebenfalls erzeugen:
        arc = new BinaryArc((String)variableList.get(1), (String)variableList.get(0), primitiveExpr);
        // ... und ebenfalls merken:
        arcList.add(arc);
      } // Knotenkonsistenz wird nicht berücksichtigt.
    }
    // Alle gefundenen Kanten durchgehen:
    while (arcList.size() > 0) {
//this.logger.debug("arcSolve: Abzuarbeitende Kanten - (" + arcList.size() + ")");
//for (int i = 0; i < arcList.size(); i++) {
//  this.logger.debug(arcList.get(i));
//}
      // Nächstes Element holen:
      arc = (BinaryArc)arcList.get(0);
      this.logger.debug("arcSolve: nächste Kante: " + arc);
      // ... und aus der Liste entfernen:
      arcList.remove(0);
      // Wenn ein Element wg. Inkonsistenz gelöscht wird:
      if (removeInconsistentValues(arc)) {
        domainModification = true;
        // ... alle Kanten (Constraints), über die Nachbarn (Variablen) mit
        // dem ersten Knoten der aktuellen Kante verbunden sind, wieder in die
        // Liste aufnehmen:
        arcList2 = getNeighbors(arc.startNode(), expr);
//this.logger.debug("arcSolve: Neue Kanten: (" + arcList2.size() + ")");
//for (int i = 0; i < arcList2.size(); i++) {
//  this.logger.debug(arcList2.get(i));
//}
        arcList.addAll(arcList2);
      }
      // Prüfen, ob eine Domäne leer ist (keine Kantenkonsistenz herstellbar):
      if ((arc.startDomain().isEmpty()) || (arc.endDomain().isEmpty())) {
        this.logger.debug("Leere Domäne entdeckt, keine Kantenkonsistenz herstellbar.");
        throw new InconsistencyException();
      }
    }
    return domainModification;
  }

  /**
   * Benötigt als Eingabe ein Constraint-Netz. Liefert <code>true</code>, wenn
   * Wertebereichseinschränkungen vorgenommen wurden, <code>false</code> wenn
   * nicht.
   * @param constraintNet ConstraintNet
   * @return boolean
   * @throws InconsistencyException
   */
  public boolean propagate(ConstraintNet constraintNet) throws InconsistencyException {
    boolean domainModification = false;
    try {
      domainModification = arcSolve(constraintNet.getSubproblem());
    } catch (OnlyBinaryConstraintsAllowedException e) {
      e.printStackTrace();
    }
    return domainModification;
  }

}
