package yacs;

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

import java.io.*;
import java.util.*;

import org.apache.log4j.*;

/**
 * <p>Implementierung des YACS Constraint-Managers.</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 YacsConstraintManagerImpl implements YacsConstraintManager {

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

  /** Logger aktivieren: */
  private static Logger logger = Logger.getLogger("yacs.YacsConstraintManagerImpl");

  /** Liste der verfügbaren Constraint-Netze. Der "Key" ist jeweils der Name der
   * zugehörigen Strategie, der "Value" ist eine Instanz der Klasse
   * <code>yacs.net.ConstraintNet</code>.*/
  private HashMap constraintNetsMap = new HashMap();

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

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

  /**
   * Generiert eine <code>HashMap</code> mit den Variablen aller vorhandenen
   * Constraint-Netze. Der "Key" ist jeweils der Name der Variable,
   * der "Value" ist die Variable selbst (Instanz der Klasse
   * <code>yacs.parser.Variable</code>). Diese Methode wird benötigt, um dem
   * Constraint-Parser eine Map mit den bereits vorhandenen Variablen übergeben
   * zu können. Die Map benötigt der Parser, damit Variablen mit demselben Namen
   * nicht doppelt instantiiert werden.
   * @return HashMap
   */
  private HashMap generateVariablesMap() {
    HashMap variablesMap = new HashMap();
    Variable curVariable;
    ConstraintNet constraintNet;
    Iterator varIt;
    Iterator it = this.getConstraintNets().iterator();
    // Alle Constraint-Netze durchgehen:
    while (it.hasNext()) {
      constraintNet = (ConstraintNet)it.next();
      // Für jedes Constraint-Netz einen Iterator über die Variablen holen:
      varIt = constraintNet.getVariables().iterator();
      // Alle Variablen durchgehen:
      while (varIt.hasNext()) {
        // Referenzen der Variablen aus den Expressions holen ...
        curVariable = constraintNet.getSubproblem().getVariableReference((String)varIt.next());
        // ... und falls sie noch nicht erfasst sind in der HashMap ablegen:
        if (!variablesMap.containsKey(curVariable.getName())) {
          variablesMap.put(curVariable.getName(), curVariable);
        }
      }
    }
    return variablesMap;
  }

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

  /**
   * Konstruktor -- Wird dieser Konstruktor ohne Angabe des Pfades zu der
   * XML-Datei mit den Constraint-Lösungsstrategien aufgerufen, wird diese im
   * aktuellen Verzeichnis unter dem Namen <code>yacs_strategies.xml</code>
   * gesucht.
   */
  public YacsConstraintManagerImpl() {
    this("yacs_strategies.xml");
  }

  /**
   * Konstruktor -- Der Parameter <code>strategyPath</code> ist der Pfad zur der
   * XML-Datei, in der die Constraint-Lösungsstrategien spezifiziert sind.
   * @param strategyPath String
   */
  public YacsConstraintManagerImpl(String strategyPath) {
    // Log4J-Ausgabe konfigurieren (Default):
    BasicConfigurator.configure();
    this.logger.setLevel(Level.OFF);
    // Log-Messages:
    //this.logger.debug("debug - fine-grained informational events that are most useful to debug an application");
    //this.logger.info("info - informational messages that highlight the progress of the application at coarse-grained level");
    //this.logger.warn("warn - potentially harmful situations");
    //this.logger.error("error - error events that might still allow the application to continue running");
    //this.logger.fatal("fatal - very severe error events that will presumably lead the application to abort");*/
    //
    // Den StrategyReader instantiieren:
    StrategyReader strategyReader = new StrategyReader(strategyPath);
    // Instantiierte Strategien holen:
    Strategy strategy;
    Iterator it = strategyReader.getStrategies().iterator();
    // ... und, eingebettet in ein Objekt für ein Constraint-Netz, in der dafür
    // vorgesehenen HashMap ablegen:
    while (it.hasNext()) {
      strategy = (Strategy)it.next();
      this.constraintNetsMap.put(strategy.getStrategyName(), new ConstraintNet(strategy));
    }
  }

  /**
   * Liefert eine "unmodifizierbare" Liste mit den Namen (<code>Strings</code>)
   * der existierenden Constraint-Lösungsstrategien und entsprechend der
   * verfügbaren Constraint-Netze.
   * @return List
   */
  public List getStrategyNames() {
    return Collections.unmodifiableList(new LinkedList(this.constraintNetsMap.keySet()));
  }

  /**
   * Liefert eine "unmodifizierbare" Liste mit verfügbaren Constraint-Netzen
   * (enthält Instanzen der Klasse <code>yacs.net.ConstraintNet</code>).
   * @return List
   */
  public List getConstraintNets() {
    return Collections.unmodifiableList(new LinkedList(this.constraintNetsMap.values()));
  }

  /**
   * Liefert das entsprechende Constraint-Netz zu dem übergebenen
   * Strategienamen, <code>null</code> wenn kein Constraint-Netz mit dieser
   * Strategie existiert.
   * @param strategyName String
   * @return ConstraintNet
   * @throws StrategyNotFoundException
   */
  public ConstraintNet getConstraintNet(String strategyName) throws StrategyNotFoundException {
    if (this.constraintNetsMap.containsKey(strategyName)) {
      return (ConstraintNet)this.constraintNetsMap.get(strategyName);
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method getConstraintNet(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

//  /**
//   * Liefert eine "unmodifizierbare" Map mit den verfügbaren Constraint-Netzen.
//   * Der "Key" ist jeweils der Name der zuständigen Strategie, der "Value" das
//   * Constraint-Netz selbst (Instanz der Klasse
//   * <code>yacs.net.ConstraintNet</code>).
//   * @return Map
//   */
//  public Map getConstraintNetsMap() {
//    return Collections.unmodifiableMap(this.constraintNetsMap);
//  }

  /**
    * Mit dieser Methoden kann ein Constraint inkrementell dem Lösungsprozess
    * zugeführt werden. Neben dem eigentlichen Constraint-Ausdruck als
    * <code>String</code> wird der Name der Strategie benötigt, mit der das
    * Constraint verarbeitet werden soll.
    * @param constraint String
    * @param strategyName String
    * @throws ConstraintParserException
    * @throws StrategyNotFoundException
    */
   public void addConstraint(String constraint, String strategyName) throws ConstraintParserException, StrategyNotFoundException {
     this.addConstraints(constraint, strategyName);
   }

  /**
   * Mit dieser Methoden können mehrere Constraints gleichzeitig bzw. ein
   * Teilproblem inkrementell dem Lösungsprozess zugeführt werden. Neben dem
   * eigentlichen Constraint-Ausdruck als <code>String</code> (die
   * Constraint-Ausdrücke müssen darin jeweils durch ein Semikolon voneinander
   * getrennt sein) wird der Name der Strategie benötigt, mit der die
   * Constraints verarbeitet werden sollen.
   * @param constraint String
   * @param strategyName String
   * @throws ConstraintParserException
   * @throws StrategyNotFoundException
   */
  public void addConstraints(String constraint, String strategyName) throws ConstraintParserException, StrategyNotFoundException {
    // Nur wenn es eine Strategie mit dem übergebenen Namen gibt:
    if (this.constraintNetsMap.containsKey(strategyName)) {
      // Parser mit dem Constraint-Ausdruck initialisieren:
      Yylex ylex = new Yylex(new StringReader(constraint));
      parser parser = new parser(ylex);
      // Dem Parser die Liste der bereits vorhandenen Variablen übergeben, weil
      // diese, bedingt durch den inkrementellen Aufbau des Constraint-Netzes,
      // dem aktuellen Parser ansonsten nicht bekannt sind. Neu hinzukommende
      // Ausdrücke können dadurch innerhalb des Parsers auf bereits vorhandene
      // Variablen geprüft und ggf. mit einer Referenz auf das vorhandene Objekt
      // instantiiert werden. Das Ganze dient dazu, auch bei einem inkrementell
      // anwachsenden Constraint-Netz, dieselben Variablen durch dieselben
      // Objekte repräsentieren zu können. Drei Möglichkeiten zur Umsetzung
      // boten sich an, die letztere wurde realisiert:
      // 1. HashMap static im YCM
      // 2. HashMap static im parser() -> von action_obj drauf zugreifen
      // 3. HashMap public im parser() -> von action_obj drauf zugreifen
      // ACHTUNG: Resultiert in einem einheitlichen Namensraum für Variablen in
      // unterschiedlichen Constraint-Lösungsstrategien!
      // "It's not a bug, it's a feature." ... :-)
      HashMap variablesMap = this.generateVariablesMap();
      this.logger.debug("addConstraints(): parser: " + parser);
      this.logger.debug("addConstraints(): variablesMap: " + variablesMap);
      parser.addVariablesMap(variablesMap);
      // Parsing-Prozess starten:
      try {
System.out.print("Eingabe:"); //Die nachfolgende Ausgabe kommt aus parser.cup!
        parser.parse();
      } catch (Exception e) {
        throw new ConstraintParserException("JLex/CUP parser error: " + e.toString());
      }
      // Den geparsten Expression des Constraint-Ausdrucks holen und dem
      // jeweiligen Constraint-Netz übergeben:
      Expression expr = parser.expression();
      this.logger.debug("addConstraints(): Expression: " + expr.toString());
      this.logger.debug("addConstraints(): Variablen: " + expr.getVariables());
      ((ConstraintNet)this.constraintNetsMap.get(strategyName)).addConstraint(expr);
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method addConstraints(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

  /**
   * Setzt den Wertebereich für eine Variable.
   * @param variableName String
   * @param variableDomain Domain
   * @throws VariableNotFoundException
   */
  public void setVariableDomain(String variableName, Domain variableDomain) throws VariableNotFoundException {
    boolean success = false;
    ConstraintNet constraintNet;
    // Alle vorhandenen Constraint-Netze durchgehen:
    Iterator it = this.constraintNetsMap.values().iterator();
    while (it.hasNext()) {
      // Bei jedem Constraint-Netz versuchen die Domäne zur übergebenen Variable
      // zu setzen (muss wg. einheitlichem Namensraum für alle Constraint-Netze
      // passieren, um in jedem Fall eine konsistente Belegung zu garantieren).
      // Für die Belegung reicht der erste Treffer, da die Domäne direkt im
      // Variablen-Objekt gesetzt wird. Dies ist, falls eine Variable mehrfach
      // in unterschiedliche Constraint-Netzen vorkommt, immer dasselbe Objekt.
      // Allerdings muss in jedem Constraint-Netz die "variablesMap"
      // aktualisiert werden, wenn es die Variable enthält. Daher muss auch die
      // Methode "setDomain()" für jedes Constraint-Netz aufgerufen werden ...
      constraintNet = (ConstraintNet)it.next();
      success = success | constraintNet.setDomain(variableName, variableDomain);
    }
    // Wenn kein Constraint-Netz die übergebene Variable enthält, Exception auslösen:
    if (!success) {
      throw new VariableNotFoundException("YACS constraint manager error in method setVariableDomain(): the specified variable '" + variableName + "' does not exist.");
    }
  }

  /**
   * Liefert die entsprechende Domäne zur übergebenen Variable.
   * @param variableName String
   * @return Domain
   * @throws VariableNotFoundException
   */
  public Domain getVariableDomain(String variableName) throws VariableNotFoundException {
    Domain domain;
    ConstraintNet constraintNet;
    // Alle vorhandenen Constraint-Netze durchgehen:
    Iterator it = this.constraintNetsMap.values().iterator();
    while (it.hasNext()) {
      // Von jedem Constraint-Netz versuchen die entsprechende Domäne zur
      // übergebenen Variable holen:
      constraintNet = (ConstraintNet)it.next();
      domain = constraintNet.getDomain(variableName);
      // Erste Fundstelle reicht, weil einheitlicher Namensraum für alle
      // Variablen existiert:
      if (domain != null) {
        return domain;
      }
    }
    // Wenn keine Domäne gefunden wurden, Exception auslösen:
    throw new VariableNotFoundException("YACS constraint manager error in method getVariableDomain(): the specified variable '" + variableName + "' does not exist.");
  }

  /**
   * Liefert eine Liste mit den Namen aller Variablen sämtlicher vorhandenen
   * Constraint-Netze (enthält Instanzen der Klasse <code>String</code>).
   * @return List
   */
  public List getVariables() {
    List variablesList = new LinkedList();
    String curVariableName;
    ConstraintNet constraintNet;
    Iterator varIt;
    Iterator it = this.getConstraintNets().iterator();
    // Alle Constraint-Netze durchgehen:
    while (it.hasNext()) {
      constraintNet = (ConstraintNet)it.next();
      // Für jedes Constraint-Netz einen Iterator über die Variablen holen:
      varIt = constraintNet.getVariables().iterator();
      // Alle Variablen durchgehen:
      while (varIt.hasNext()) {
        curVariableName = (String)varIt.next();
        // ... und falls sie noch nicht erfasst sind zur Liste hinzufügen:
        if (!variablesList.contains(curVariableName)) {
          variablesList.add(curVariableName);
        }
      }
    }
    return variablesList;
  }

  /**
   * Liefert eine Liste mit den Variablen (Instanzen der Klasse
   * <code>yacs.parser.Variable</code>), die sich in dem der übergebenen
   * Strategie zugehörigen Constraint-Netz befinden.
   * @param strategyName String
   * @return List
   * @throws StrategyNotFoundException
   */
  public List getVariables(String strategyName) throws StrategyNotFoundException {
    return this.getConstraintNet(strategyName).getVariables();
  }

  /**
   * Liefert die ggf. bereits gefundenen Lösungen für das der übergebenen
   * Strategie zugehörige Teilproblem.
   * @param strategyName String
   * @return Solutions
   * @throws StrategyNotFoundException
   */
  public Solutions getSolutions(String strategyName) throws StrategyNotFoundException {
    if (this.constraintNetsMap.containsKey(strategyName)) {
      return ((ConstraintNet)this.constraintNetsMap.get(strategyName)).getSolutions();
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method getSolutions(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

  /**
   * Liefert <code>true</code>, wenn für das der übergebenen Strategie
   * zugehörige Teilproblem bereits Lösungen gefunden wurden,
   * <code>false</code> wenn nicht.
   * @param strategyName String
   * @return boolean
   * @throws StrategyNotFoundException
   */
  public boolean hasSolutions(String strategyName) throws StrategyNotFoundException {
    if (this.constraintNetsMap.containsKey(strategyName)) {
      return ((ConstraintNet)this.constraintNetsMap.get(strategyName)).hasSolutions();
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method hasSolutions(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

  /**
   * Liefert <code>true</code>, wenn die Domäne einer Variablen des der
   * übergebenen Strategie zugehörigen Teilproblems leer ist. In diesem Fall ist
   * für dieses Teilproblem keine Konsistenz bzw. Lösung herstellbar. Ansonsten
   * wird <code>false</code> zurückgegeben.
   * @param strategyName String
   * @return boolean
   * @throws StrategyNotFoundException
   */
  public boolean hasFalseDomain(String strategyName) throws StrategyNotFoundException  {
    if (this.constraintNetsMap.containsKey(strategyName)) {
      return ((ConstraintNet)this.constraintNetsMap.get(strategyName)).hasFalseDomain();
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method hasFalseDomain(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

  /**
   * Liefert <code>true</code>, wenn für das der übergebenen Strategie
   * zugehörige Teilproblem 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.
   * @param strategyName String
   * @return boolean
   * @throws StrategyNotFoundException
   */
  public boolean hasValuationDomain(String strategyName) throws StrategyNotFoundException  {
     if (this.constraintNetsMap.containsKey(strategyName)) {
       return ((ConstraintNet)this.constraintNetsMap.get(strategyName)).hasValuationDomain();
     } else {
       throw new StrategyNotFoundException("YACS constraint manager error in method hasValuationDomain(): the specified strategy '" + strategyName + "' does not exist.");
     }
  }

  /**
   * Gibt zurück, ob ein Constraint-Solver für das der übergebenen Strategie
   * zugehörige Teilproblem eine Inkonsistenz gemeldet hat oder nicht (wird
   * zurückgesetzt durch die benutzerinitiierte Modifikation der Wertebereiche
   * der Constraint-Variablen und durch Hinzufügen einer Lösung zum
   * Teilproblem).
   * @param strategyName String
   * @return boolean
   * @throws StrategyNotFoundException
   */
  public boolean isInconsistent(String strategyName) throws StrategyNotFoundException   {
    if (this.constraintNetsMap.containsKey(strategyName)) {
      return ((ConstraintNet)this.constraintNetsMap.get(strategyName)).isInconsistent();
    } else {
      throw new StrategyNotFoundException("YACS constraint manager error in method hasValuationDomain(): the specified strategy '" + strategyName + "' does not exist.");
    }
  }

  /**
   * Startet den Constraint-Lösungsvorgang. Ruft jeweils solange die
   * Auswertemethoden der vorhandenen Constraint-Netze auf
   * (<code>triggerPreprocess()</code>, <code>triggerConsistency</code> und
   * <code>triggerSearch</code>), bis keine Wertebereichseinschränkungen mehr
   * auftreten.
   */
  public void evaluate() {
    boolean domainModification;
    Iterator it;
    // Phase 1: Preprozessing
    // ======================
    do {
      this.logger.info("Neue Iteration in Phase 1 in YacsConstraintManagerImpl.evaluate().");
      domainModification = false;
      it = this.getConstraintNets().iterator();
      // Alle Constraint-Netze durchgehen:
      while (it.hasNext()) {
        domainModification |= ((ConstraintNet)it.next()).triggerPreprocessing();
      }
    } while (domainModification);
    // Phase 2: Konsistenzherstellung
    // ==============================
    do {
      this.logger.info("Neue Iteration in Phase 2 in YacsConstraintManagerImpl.evaluate().");
      domainModification = false;
      it = this.getConstraintNets().iterator();
      // Alle Constraint-Netze durchgehen:
      while (it.hasNext()) {
        domainModification |= ((ConstraintNet)it.next()).triggerConsistency();
      }
    } while (domainModification);
    // Phase 3: Lösungssuche
    // =====================
    do {
      this.logger.info("Neue Iteration in Phase 3 in YacsConstraintManagerImpl.evaluate().");
      domainModification = false;
      it = this.getConstraintNets().iterator();
      // Alle Constraint-Netze durchgehen:
      while (it.hasNext()) {
        domainModification |= ((ConstraintNet)it.next()).triggerSearch();
      }
    } while (domainModification);
  }

  /**
   * Setzt sämtliche Constraint-Netze wieder in den Ausgangszustand zurück, d.h.
   * es werden alle Informationen über die zugehörigen Teilprobleme der
   * Strategie gelöscht (Constraints, Variablen, Domänen, etc.).
   */
  public void tabularasa() {
    // Alle Constraint-Netze durchgehen:
    Iterator it = this.getConstraintNets().iterator();
    while (it.hasNext()) {
      ((ConstraintNet)it.next()).clear();
    }
  }

}
