package yacs.net;

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

import java.util.*;

/**
 * <p>Klasse zur Repräsentation des Constraint-Netzes eines Teilproblems
 * innerhalb von YACS.</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 ConstraintNet {

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

  /**
   * Die zugehörige Constraint-Lösungsstrategie.
   * @supplierCardinality 1
   * @clientCardinality 1
   * @link aggregation
   */
  private Strategy strategy;

  /**
   * Constraint-Teilproblem (generiert aus einem <code>String</code> mit
   * Kommata-separierter Liste).
   */
  private Expression subproblem;

  /**
   * Enthält ggf. die Lösungen für das Constraint-Teilproblem.
   * @supplierCardinality 1
   * @clientCardinality 1
   * @link aggregationByValue
   */
  private Solutions solutions = new Solutions();

  /**
   * Liste aller primitiven Constraint-Ausdrücke (enthält Instanzen der Klasse
   * <code>yacs.parser.Expression</code>) des Teilproblems.
   */
  private List constraints = new LinkedList();

  /**
   * <code>HashMap</code> über die Variablen und Domänen des Teilproblems. Der
   * "Key" ist jeweils der Name einer Variable (als <code>String</code>), der
   * dazugehörige "Value" ist eine Instanz der Klasse
   * <code>yacs.domain.Domain</code>.
   */
  private HashMap variablesMap = new HashMap();

  /** Zeigt an, ob das Constraint-Netz inkonsistent ist oder nicht. */
  private boolean inconsistent;

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

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

  /**
   * Initialisierung mit dem aktuellen Teilproblem (<code>Expression</code>).
   * @param expr Expression
   */
  private void init(Expression expr) {
    // Teilproblem merken:
    this.subproblem = expr;
    // Nur wenn Constraint-Ausdruck vorhanden:
    if (this.subproblem != null) {
      // Liste aller Constraints dieses Teilproblems generieren und merken:
      this.constraints = expr.getPrimitiveExpressions();
      // HashMap mit den beteiligten Variablen des Teilproblems als "Keys" und den
      // zugehörigen Domänen als "Values" generieren und merken:
      String curVariableName;
      List variablesList = expr.getVariables();
      Iterator it = variablesList.iterator();
      // ... dafür alle Variablen aus dem Expression durchgehen:
      while (it.hasNext()) {
        curVariableName = (String)it.next();
        // ... und ablegen:
        this.variablesMap.put(curVariableName, expr.getDomain(curVariableName));
      }
    } else {
      // Wenn mit leerem Constraint-Ausdruck initialisert wurde:
      this.solutions = new Solutions();
      this.constraints = new LinkedList();
      this.variablesMap = new HashMap();
      this.inconsistent = false;
      // Vorsichtshalber den "Garbage Collector" manuell anwerfen:
      System.gc();
    }
  }

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

  /**
   * Konstruktor -- Das Constraint-Netz muss mit einer zugehörigen Strategie
   * instantiiert werden, die für die Verarbeitung der Constraints verwendet
   * wird.
   * @param strategy Strategy
   */
  public ConstraintNet(Strategy strategy) {
    this.strategy = strategy;
  }

  /**
   * Konstruktor -- Benötigt die zugehörige Strategie und ein oder mehrere
   * Constraints als <code>Expression</code> (ein Constraint-Teilproblem
   * generiert aus einem <code>String</code> mit Kommata-separierter Liste von
   * Constraint-Ausdrücken).
   * @param strategy Strategy
   * @param expr Expression
   */
  public ConstraintNet(Strategy strategy, Expression expr) {
    this(strategy);
    // Initialisieren des Constraints-Netzes mit dem übergebenen Teilproblem:
    this.init(expr);
  }

  /**
   * Fügt ein Constraint dem vorhandenen Constraint-<code>Expression</code>
   * hinzu. Damit ein einheitlicher Namensraum sichergestellt ist, muss durch
   * den Parser sichergestellt werden, dass neue Variablen mit den Namen bereits
   * existierender Variablen auf dassselbe Objekt verweisen.
   * @param constraint Expression
   */
  public void addConstraint(Expression constraint) {
    // Initialisieren des Constraints-Netzes mit dem übergebenen
    // Constraint-Ausdruck:
    if (this.subproblem == null) {
      // Falls noch kein Constraint-Ausdruck vorhanden:
      this.init(constraint);
    } else {
      // ... ansonsten den *bestehenden* Ausdruck unter die einzelnen, neuen
      // Ausdrücke "drunterklemmen":
      Expression primitiveExpr;
      List exprList = constraint.getPrimitiveExpressions();
      Iterator it = exprList.iterator();
      // Alle einzelnen, neuen Constraint-Ausdrücke durchgehen:
      while (it.hasNext()) {
        primitiveExpr = (Expression)it.next();
        // ... und jeweils den *bestehenden* Ausdruck drunterhängen:
        this.subproblem = new BinaryOperator(sym.SEMI, ";", this.subproblem, primitiveExpr);
      }
      this.init(this.subproblem);
    }
  }

  /**
   * Liefert den Namen der zu diesem Teilproblem gehörigen
   * Constraint-Lösungsstrategie.
   */
  public String getStrategyName() {
    return this.strategy.getStrategyName();
  }

  /**
   * Liefert das Constraint-Teilproblem bzw. den Constraint-Ausdruck.
   * @return Expression
   */
  public Expression getSubproblem() {
    return this.subproblem;
  }

  /**
   * Liefert eine Liste der in diesem Teilproblem enthaltenen (primitiven)
   * Constraints (enthält Instanzen der Klasse
   * <code>yacs.parser.Expression</code>).
   * @return List
   */
  public List getConstraints() {
    return this.constraints;
  }

  /**
   * Liefert eine "unmodifizierbare" Liste aller Variablen
   * (als <code>Strings</code>) des Teilproblems.
   * @return List
   */
  public List getVariables() {
    return Collections.unmodifiableList(new LinkedList(this.variablesMap.keySet()));
  }

  /**
   * Liefert die bereits gefundenen Lösungen für dieses Teilproblem.
   * @return Solutions
   */
  public Solutions getSolutions() {
    return this.solutions;
  }

  /**
   * Fügt diesem Teilproblem eine Lösungen hinzu. Liefert <code>true</code>,
   * wenn dies erfolgreich verlaufen ist (Lösung war noch nicht vorhanden),
   * <code>false</code> wenn nicht (Lösung existierte bereits)
   * @param solution Solution
   * @return boolean
   */
  public boolean addSolution(Solution solution) {
    this.inconsistent = false;
    return this.solutions.addSolution(solution);
  }

  /**
   * Erzeugt und liefert eine "unmodifizierbare" Liste aller Domänen (Instanzen
   * der der Klasse <code>yacs.domain.Domain</code>) des Teilproblems.
   * @return List
   */
  public List getDomains() {
    return Collections.unmodifiableList(new LinkedList(this.variablesMap.values()));
  }

  /**
   * Liefert die entsprechende Domäne zur übergebenen Variable. Gibt
   * <code>null</code> zurück, wenn keine Variable unter dem angegebenen Namen
   * existiert.
   * @param variableName String
   * @return Domain
   */
  public Domain getDomain(String variableName) {
    return (Domain)this.variablesMap.get(variableName);
  }

  /**
   * Belegt die angegebene Variable mit der übergebenen Domäne.
   * @param variableName String
   * @param variableDomain Domain
   */
  public boolean setDomain(String variableName, Domain variableDomain) {
    boolean success = false;
    // Nur wenn bereits Constraint-Ausdrücke im Constraint-Netz existieren:
    if (this.subproblem != null) {
      success = this.subproblem.setDomain(variableName, variableDomain);
      // Wenn erfolgreich, "variablesMap" ebenfalls aktualisieren:
      if (success) {
        this.variablesMap.put(variableName, variableDomain);
        this.inconsistent = false;
      }
    }
    return success;
  }

  /**
   * Liefert <code>true</code>, wenn die Domäne einer Variable des Teilproblems
   * leer ist. In diesem Fall ist keine Konsistenz bzw. Lösung herstellbar.
   * Ansonsten wird <code>false</code> zurückgegeben.
   * @return boolean
   */
  public boolean hasFalseDomain() {
    Iterator it = this.getDomains().iterator();
    while (it.hasNext()) {
      if (((Domain)it.next()).isEmpty()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Liefert <code>true</code>, wenn eine "valuation domain" vorliegt, d.h. in
   * den Domänen aller Variablen des Teilproblems ist jeweils genau ein Wert
   * enthalten. Ansonsten wird <code>false</code> zurückgegeben.
   * @return boolean
   */
   public boolean hasValuationDomain() {
    Iterator it = this.getDomains().iterator();
    while (it.hasNext()) {
      if (((Domain)it.next()).size() != 1) {
        return false;
      }
    }
    return true;
  }

  /**
   * Liefert <code>true</code>, wenn für das Teilproblem bereits Lösungen
   * gefunden wurden, <code>false</code> wenn nicht.
   * @return boolean
   */
  public boolean hasSolutions() {
    return this.solutions.hasSolutions();
  }

  /**
   * Gibt zurück, ob von einem Constraint-Solver eine Inkonsistenz gemeldet
   * wurde (durch Auslösen einer <code>InconsistencyException</code>).
   * @return boolean
   */
  public boolean isInconsistent() {
    return this.inconsistent;
  }

  /**
   * Phase 1 der Constraint-Verarbeitung: Wendet die in der Strategie
   * spezifizierten Preprozessing-Solver auf das Constraint-Netz an. Liefert
   * <code>true</code>, wenn Wertebereichseinschränkungen vorgenommen wurden,
   * <code>false</code> wenn nicht.
   */
  public boolean triggerPreprocessing() {
    boolean domainModification = false;
    // Alle Preprozessing-Solver durchgehen (nur wenn Constraints existieren):
    Iterator it = this.strategy.getPreprocessingSolvers().iterator();
    while ((it.hasNext()) && (this.subproblem != null)) {
      // ... und die jeweilige evaluate()-Methode aufrufen:
      try {
        domainModification |= ((PreprocessingSolver)it.next()).evaluate(this);
      } catch (InconsistencyException e) {
        this.inconsistent = true;
      }
      // Expression initialisieren:
      this.init(this.subproblem);
      // Anm.: Eine Initialisierung mit "init()" und dem neuen Expression ist
      // eigentlich nicht notwendig: Die "Domain"-Objekte, die der Solver
      // einschränkt, sind dieselben Objekte, die sich in der HashMap
      // befinden. Wenn allerdings neue Variablen und Constraints
      // hinzugekommen sind, z.B. durch Pfadkonsistenzalgorithmen oder
      // Constraint-Zerlegung, müssen diese berücksichtigt werden ...
      //
      // Aufräumen - Belegungen der Constraint-Variablen zurücksetzen:
      this.subproblem.init();
    }
    return domainModification;
  }

  /**
   * Phase 2 der Constraint-Verarbeitung: Wendet die in der Strategie
   * spezifizierten Solver zur Konsistenzherstellung auf das Constraint-Netz an.
   * Liefert <code>true</code>, wenn Wertebereichseinschränkungen vorgenommen
   * wurden, <code>false</code> wenn nicht.
   */
  public boolean triggerConsistency() {
    boolean domainModification = false;
    // Alle Preprozessing-Solver durchgehen (nur wenn Constraints existieren):
    Iterator it = this.strategy.getConsistencySolvers().iterator();
    while ((it.hasNext()) && (this.subproblem != null)) {
      // ... und die jeweilige evaluate()-Methode aufrufen:
      try {
        domainModification |= ((ConsistencySolver)it.next()).evaluate(this);
      } catch (InconsistencyException e) {
        this.inconsistent = true;
      }
      // Expression initialisieren:
      this.init(this.subproblem);
      // Anm.: Eine Initialisierung mit "init()" und dem neuen Expression ist
      // eigentlich nicht notwendig: Die "Domain"-Objekte, die der Solver
      // einschränkt, sind dieselben Objekte, die sich in der HashMap
      // befinden. Wenn allerdings neue Variablen und Constraints
      // hinzugekommen sind, z.B. durch Pfadkonsistenzalgorithmen oder
      // Constraint-Zerlegung, müssen diese berücksichtigt werden ...
      //
      // Aufräumen - Belegungen der Constraint-Variablen zurücksetzen:
      this.subproblem.init();
    }
    return domainModification;
  }

  /**
   * Phase 3 der Constraint-Verarbeitung: Wendet die in der Strategie
   * spezifizierten Solver zur Lösungssuche auf das Constraint-Netz an. Liefert
   * <code>true</code>, wenn Wertebereichseinschränkungen vorgenommen wurden,
   * <code>false</code> wenn nicht.
   */
  public boolean triggerSearch() {
    boolean domainModification = false;
    // ACHTUNG: Vorhandene Lösungen vor dem Durchlauf entfernen:
    this.solutions = new Solutions();
    // Alle Preprozessing-Solver durchgehen (nur wenn Constraints existieren):
    Iterator it = this.strategy.getSearchSolvers().iterator();
    while ((it.hasNext()) && (this.subproblem != null)) {
      // ... und die jeweilige evaluate()-Methode aufrufen:
      try {
        domainModification |= ((SearchSolver)it.next()).evaluate(this);
      } catch (InconsistencyException e) {
        this.inconsistent = true;
      }
      // Da hybride Suchverfahren auch die Domänen einschränken können(?),
      // muss auch hier das Constraint-Netz mit dem aktuellen Expression
      // initialisiert werden:
      this.init(this.subproblem);
      // Anm.: Eine Initialisierung mit "init()" und dem neuen Expression ist
      // eigentlich nicht notwendig: Die "Domain"-Objekte, die der Solver
      // einschränkt, sind dieselben Objekte, die sich in der HashMap
      // befinden. Wenn allerdings neue Variablen und Constraints
      // hinzugekommen sind, z.B. durch Pfadkonsistenzalgorithmen oder
      // Constraint-Zerlegung, müssen diese berücksichtigt werden ...
      //
      // Aufräumen: Belegungen der Constraint-Variablen zurücksetzen:
      this.subproblem.init();
    }
    return domainModification;
  }

  /**
   * Initialisiert das Constraint-Netz, d.h. es wird der <code>Expression</code>
   * für das Teilproblem (inkl. der Variablen und Domänen sowie der primitiven
   * Constraints), die bisher gefundenen Lösungen sowie die Informationen über
   * die Konsistenz des Constraint-Netzes gelöscht.
   */
  public void clear() {
    // init()-Methode mit "null" als Subproblem aufrufen:
    this.init(null);
  }

  /**
   * Rückgabe des Constraint-Netzes als <code>String</code> (für Debugging).
   * @return String
   */
  public String toString() {
    String returnString = new String();
    Iterator it;
    returnString += "Strategiename: '" + this.getStrategyName() + "', ";
    returnString += "Teilproblem: " + subproblem;
    return returnString;
  }

}
