package yacs.solver.fdsolver.search;

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

import java.util.*;

import org.apache.log4j.*;

/**
 * <p>Einfacher MAC-Solver zur Lösungssuche mit "look-ahead" in einem
 * Constraint-Problem mit finiten Domänen nach Dechter und Frost (2002, S. 175
 * u. 178). Der Solver wurde dahingehend modifiziert, dass anstatt lediglich
 * einer Lösung alle möglichen Lösungen eines Problems gefunden werden.</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 MACSolver extends SearchSolver {

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

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

  /** Variablenordnungsheurstik: */
  private DomDegRatioVariableOrdering variableOrdering = new DomDegRatioVariableOrdering();

  /**
   * Konsistenz-Solver für "look-ahead":
   * @supplierCardinality 1
   * @clientCardinality 1
   * @link aggregation
   */
  private NCSolver ncsolver = new NCSolver();

  /**
   * Konsistenz-Solver für "look-ahead":
   * @supplierCardinality 1
   * @clientCardinality 1
   * @link aggregation
   */
  private ACSolver acsolver = new ACSolver();

  /**
   * <code>HashMap</code> mit den Namen der Variablen des Teilproblems als
   * "Keys" und Kopien der zugehörigen Domänen als "Values".
   */
  private HashMap clonedDomainsMap;

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

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

  /**
   * Sucht alle Lösungen eines Constraint-Problems mit finiten Domänen und trägt
   * sie in das übergebene Constraint-Netz ein. Liefert <code>true</code>, wenn
   * Lösungen gefunden werden konnten, <code>false</code> wenn keine konsistente
   * Lösung existiert.
   * @param constraintNet ConstraintNet
   * @return boolean
   */
  private boolean macSolve(ConstraintNet constraintNet) {
    int i = 0;
    Domain domain;
    String variableName;
    DomainElement element;
    boolean solutionFound = false;
    Expression expr = constraintNet.getSubproblem();
//    List variablesList = constraintNet.getVariables();
    // Reihenfolge für die zu belegenden Variablen ermitteln (statisch)
    List variablesList = this.variableOrdering.getOrdering(constraintNet);
    this.logger.debug("macSolve: variablesList: " + variablesList);
    // Nur wenn Variablen vorhanden sind:
    if (!variablesList.isEmpty()) {
      // Kopien der Domänen anlegen und in HashMap ablegen:
      this.clonedDomainsMap = this.generateClonedDomainsMap(constraintNet.getVariables(), constraintNet);
      // Erste Variable und Domäne (bzw. Kopie!) holen:
      variableName = (String)variablesList.get(i);
      domain = (Domain)this.clonedDomainsMap.get(variableName);
      // Solange sich der Zähler "innerhalb" der Variablenliste befindet:
      while ((0 <= i) && (i < variablesList.size())) {
        // Aktuelle Variable mit einem konsistenten Wert belegen:
        this.logger.debug("Gehe nach selectValue() mit: '" + variableName + "', Domäne: " + domain + "'");
        element = this.selectValue(variableName, domain, constraintNet);
        this.logger.debug("Ergebnis selectValue: '" + element + "' für '" + variableName + "'");
        this.logger.debug("Domäne von '" + variableName + "': " + domain);
        expr.set(variableName, element);
        this.logger.info(expr);
        // Wenn kein konsistenter Wert gefunden werden konnte:
        if (element == null) {
          // Backtrack:
          i = i - 1;
          this.logger.debug("Backtrack: i=" + i);
          // Falls noch nicht am Ende der Suche angelangt:
          if (i >= 0) {
            // Domäne der aktuellen Variable neu kopieren (wieder "befüllen"):
            this.clonedDomainsMap.put(variableName, expr.getDomain(variableName).cloneDeep());
            // Nächste Variable und Domäne (bzw. Kopie) holen:
            variableName = (String)variablesList.get(i);
            domain = (Domain)this.clonedDomainsMap.get(variableName);
          }
        } else {
          // Wenn aktuelle Belegung bereits eine Lösung:
          if (!expr.unsetVariablesLeft() && expr.satisfiable()) {
            this.logger.debug("Lösung: " + expr);
            // Lösung generieren und dem Constraint-Netz hinzufügen:
            Solution solution = this.generateSolution(constraintNet, expr);
            // Lösung dem Teilproblem bekannt machen:
            constraintNet.addSolution(solution);
            solutionFound = true;
            this.logger.debug("Lösung: " + solution);
          } else { // Ansonsten die nächste Variable auswählen ...
            // Step Forward:
            i = i + 1;
            this.logger.debug("Step Forward: i=" + i);
            if (i < variablesList.size()) {
              // Nächste Variable und Domäne (bzw. Kopie) holen:
              variableName = (String)variablesList.get(i);
              domain = (Domain)this.clonedDomainsMap.get(variableName);
              this.logger.debug("variableName: " + variableName);
              this.logger.debug("domain: " + domain);
            }
          }
        }
      }
    }
    return solutionFound;
  }

  /**
   * Liefert eine Wertebelegung für die übergebene Variable, die konsistent mit
   * der bisherigen Wertebelegung ist, <code>null</code> wenn keine konsistente
   * Belegung existiert.
   * @param domain Domain
   * @param constraintNet ConstraintNet
   * @return DomainElement
   */
  private DomainElement selectValue(String variableName, Domain domain, ConstraintNet constraintNet) {
    Expression expr;
    DomainElement element;
    this.logger.debug("selectValue(): constraintNet.getSubproblem(): " + constraintNet.getSubproblem());
    // Alle Elemente der übergebenen Domäne durchgehen:
    Iterator it = domain.getElements().iterator();
    this.logger.debug("selectValue(): domain.getElements(): " + domain.getElements());
    while (it.hasNext()) {
      // Übergebene Domäne kopieren:
      Domain clonedDomain = domain.cloneDeep();
      // Element holen:
      element = (DomainElement)it.next();
      // Expression kopieren (komplett):
      expr = constraintNet.getSubproblem().cloneDeep();
      this.logger.debug("selectValue(): variableName: " + variableName);
      this.logger.debug("selectValue(): variableValue: " + expr.get(variableName));
      this.logger.debug("selectValue(): domain: " + domain);
      // Alle Domänen auf die ggf. jeweils gesetzten Werte beschränken (betrifft
      // kopierten "Expression", ist daher unkritisch):
      expr.singletons();
      // Domäne der übergebene Variable setzen:
      expr.setDomain(variableName, domain);
      // Übergebene Variable mit Element belegen:
      expr.set(variableName, element);
      // ... und auch diese Domäne auf einen Wert beschränken:
      expr.getVariableReference(variableName).singletons();
      this.logger.debug("selectValue(): " + expr);
      try {
        // Look-ahead durch Konsistenzherstellung:
        this.ncsolver.nodeSolve(expr);
        this.acsolver.arcSolve(expr);
      } catch (InconsistencyException e) {
        this.logger.debug(e.toString());
      }
// Alle Variablen/Domänen des Expression ausgeben:
//vars = expr.getVariables().iterator();
//while (vars.hasNext()) {
//  varName = (String)vars.next();
//  this.logger.debug("varName: '"+varName+"', expr.get("+varName+"): "+expr.get(varName)+"', expr.getDomain("+varName+"): "+expr.getDomain(varName));
//}
//this.logger.debug("expr: " + expr);
      // Ergebnis der Konsistenzalgorithmen testen: Alle Domänen der vormals
      // unbelegten Variablen prüfen. Wenn eine Domäne leer ist, ist die
      // Belegung der aktuellen Variable inkonsistent. Wenn keine Domäne einer
      // der noch nicht belegten Variablen leer ist, ist die Belegung
      // (kanten-)konsistent und kann grundsätzlich zurückgegeben werden. Wenn
      // allerding eine "valuation domain" vorliegt, d.h. die Domänen aller
      // Variablen enthalten noch genau ein Element (immer der Fall, wenn das
      // letzte Element mit einem Wert belegt wurde), dann muss diese Belegung
      // erfüllbar sein:
      if (!expr.hasFalseDomain()) {
        // Prüft, ob "valuation domain" vorliegt und stellt sicher, dass
        // sämtliche Werte entsprechend gesetzt sind:
        if (expr.setValuationDomain()) {
          if (expr.satisfiable()) {
            this.logger.debug("selectValue(): satisfiable");
            // Ursprüngliche Domäne wieder herstellen, sonst lässt sich sich ein
            // nächster Wert nicht setzen (muss dafür in Domäne enthalten sein):
            this.copyDomainElements(clonedDomain, domain);
            // Oben gesetztes Element aus der Kopie der Domäne entfernen:
            domain.deleteElement(element);
            this.logger.debug("selectValue(): Gebe '" + element + "' zurück.");
            this.logger.debug("selectValue(): domain: " + domain);
            this.logger.debug("selectValue(): expr.getDomain(variableName): " + expr.getDomain(variableName));
            return element;
          }
        } else {
          // Ursprüngliche Domäne wieder herstellen, sonst lässt sich sich ein
          // nächster Wert nicht setzen (muss dafür in Domäne enthalten sein):
          this.copyDomainElements(clonedDomain, domain);
          // Oben gesetztes Element aus der Kopie der Domäne entfernen:
          domain.deleteElement(element);
          this.logger.debug("selectValue(): Gebe '" + element + "' zurück.");
          this.logger.debug("selectValue(): domain: " + domain);
          this.logger.debug("selectValue(): expr.getDomain(variableName): " + expr.getDomain(variableName));
          return element;
        }
/*this.logger.debug("selectValue: falseDomain: " + expr.hasFalseDomain());
this.logger.debug("selectValue: valuationDomain: " + expr.hasValuationDomain());
this.logger.debug("selectValue: satisfiable: " + expr.satisfiable());
// Alle Variablen/Domänen des Expression ausgeben:
vars = expr.getVariables().iterator();
while (vars.hasNext()) {
  varName = (String)vars.next();
  this.logger.debug("varName: '"+varName+"', expr.get("+varName+"): "+expr.get(varName)+"', expr.getDomain("+varName+"): "+expr.getDomain(varName));
}
this.logger.debug("expr: " + expr);*/
      }
      // Ursprüngliche Domäne wieder herstellen, sonst lässt sich sich ein
      // nächster Wert nicht setzen (muss dafür in Domäne enthalten sein):
      this.copyDomainElements(clonedDomain, domain);
      // Oben gesetztes Element aus der Kopie der Domäne entfernen:
      domain.deleteElement(element);
    }
    // ... ansonsten 'null' (keine konsistente Belegung gefunden):
    this.logger.debug("selectValue(): Gebe 'null' zurück.");
    return null;
  }

  /**
   * Kopiert sämtliche Elemente aus der Quelldomäne in die Zieldomäne (sofern
   * dort noch nicht vorhanden).
   * @param sourceDomain Domain
   * @param targetDomain Domain
   */
  private void copyDomainElements(Domain sourceDomain, Domain targetDomain) {
    DomainElement element;
    Iterator it = sourceDomain.getElements().iterator();
    while (it.hasNext()) {
      targetDomain.add((DomainElement)it.next());
    }
  }

  /**
   * Generiert für die übergebene Liste mit Variablennamen eine
   * <code>HashMap</code> mit den Namen der Variablen als "Keys" und Kopien der
   * zugehörigen Domänen entsprechend dem übergebenen Constraint-Netz als
   * "Values" und gibt diese zurück.
   * @param variableNames List
   * @param constraintNet ConstraintNet
   * @return HashMap
   */
  private HashMap generateClonedDomainsMap(List variableNames, ConstraintNet constraintNet) {
    String curVariableName;
    HashMap clonedDomainsMap = new HashMap();
    Iterator it = variableNames.iterator();
    // Alle Variablen aus der übergebenen Liste durchgehen:
    while (it.hasNext()) {
      curVariableName = (String)it.next();
      // ... und zusammen mit einer geclonten Domäne ablegen:
      clonedDomainsMap.put(curVariableName,
                           constraintNet.getDomain(curVariableName).cloneDeep());
    }
    return clonedDomainsMap;
  }

  /**
   * Generiert aus der aktuellen Belegung des übergebenen
   * <code>Expression</code> eine Lösung für das Constraint-Netz. Der
   * <code>Expression</code> muss identisch mit dem Subproblem des
   * Constraint-Netzes sein, kann allerdings eine davon abweichende
   * Wertebelegung der Constraint-Variablen beinhalten. Die generierte Lösung
   * wird nicht in das Constraint-Netz eingetragen sondern zurückgegeben.
   * @param constraintNet ConstraintNet
   * @param expr Expression
   * @return Solution
   */
  private Solution generateSolution(ConstraintNet constraintNet, Expression expr) {
    String actVariableName;
    DomainElement actVariableValue;
    Solution solution = new Solution();
    Iterator it = constraintNet.getVariables().iterator();
    // Sämtliche Variablen des Expression durchgehen:
    while (it.hasNext()) {
      actVariableName = (String)it.next();
      // Die Belegung auslesen:
      Domain actVariableDomain = constraintNet.getDomain(actVariableName);
      actVariableValue = (DomainElement)actVariableDomain.getElement(expr.get(actVariableName));
      // Anm.: Um zu verhindern, dass  Lösungen doppelt eingetragen werden
      // (z.B. bei wiederholten Iterationen des Lösungsprozesses durch den
      // YCM) muss in den Lösungs-Container die Originalreferenz des
      // ursprünglichen Domänenelements eingetragen werden! Dies ermöglicht
      // der "equals()"-Methode in "yacs.net.Solution" einen entsprechenden
      // Vergleich positiv zu beantworten (siehe auch "addSolution()" und
      // "containsSolution()" in "yacs.net.Solutions").
      //
      // Variable und Belegung dem Lösungsobjekt bekannt machen:
      solution.addVariable(actVariableName, actVariableValue);
    }
    return solution;
  }

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

  /**
   * 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
   */
  public boolean search(ConstraintNet constraintNet) throws InconsistencyException {
    if (!macSolve(constraintNet)) {
      throw new InconsistencyException();
    }
    // Dieser Solver ermittelt ausschließlich Lösungen und modifiziert das
    // Constraint-Netz ansonsten nicht:
    return false;
  }

}
