package yacs.solver.fdsolver.search;

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

import java.util.*;

import org.apache.log4j.*;

/**
 * <p>Einfacher Backtracking-Solver zur Lösungssuche in einem Constraint-Problem
 * mit finiten Domänen nach Dechter und Frost (1998, S. 13) bzw. Dechter und
 * Frost (2002, S. 152). 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 BacktrackingSolver extends SearchSolver {

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

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

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

  /**
   * <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
   */
  private boolean backSolve(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);
    // Nur wenn Variablen vorhanden sind:
    if (!variablesList.isEmpty()) {
      // Kopien der Domänen anlegen und in HashMap ablegen:
      this.clonedDomainsMap = this.generateClonedDomainsMap(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:
        element = this.selectValue(variableName, domain, constraintNet);
        this.logger.debug("selectValue: '" + element + "' für '" + variableName + "'");
        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()) {
            // 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;
          } 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 entsprechend ihrer
   * Domäne, die konsistent mit den bisherigen Wertebelegungen der Variablen des
   * Constraint-Netzes 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) {
    DomainElement element;
    Expression expr = constraintNet.getSubproblem();
    // Alle Elemente der Domäne durchgehen:
    Iterator it = domain.getElements().iterator();
    this.logger.debug("domain.getElements(): " + domain.getElements());
    while (it.hasNext()) {
      // Element holen:
      element = (DomainElement)it.next();
      // Variable mit Element belegen:
      expr.set(variableName, element);
      // ... und Element aus der Kopie der Domäne (s.o.) entfernen:
      domain.deleteElement(element);
      // Wenn Teilbelegung konsistent, Belegung zurückgeben:
      if (this.consistent(constraintNet)) {
        return element;
      }
    }
    // ... ansonsten 'null' (keine konsistente Belegung gefunden):
    return null;
  }

  /**
   * Liefert <code>true</code>, wenn alle primitiven Constraints (die keine
   * unbelegten Variablen mehr enthalten) erfüllbar sind, <code>false</code>
   * wenn eine inkonsistente Belegung existiert.
   * @param ConstraintNet constraintNet
   * @return boolean
   */
  private boolean consistent(ConstraintNet constraintNet) {
    boolean consistent = true;
    Expression constraint;
    // Primitive Constraints holen:
    List constraints = constraintNet.getConstraints();
    // Alle primitiven Constraints durchgehen (solange Ausdruck konsistent):
    Iterator it = constraints.iterator();
    while (it.hasNext() && consistent) {
      constraint = ((Expression)it.next());
      // Nur wenn keine unbelegten Variablen im aktuellen Ausdruck:
      if (!constraint.unsetVariablesLeft()) {
        // Auf Erfüllbarkeit testen und Ergebnisse "sammeln":
        consistent &= constraint.satisfiable();
      }
    }
    // Ergebnis ist positiv, wenn alle primitiven Constraints erfüllbar sind,
    // die keine unbelegten Variablen mehr enthalten:
    this.logger.debug("return consistent: " + consistent);
    return consistent;
  }

  /**
   * Generiert eine <code>HashMap</code> mit den Namen beteiligten Variablen des
   * übergebenen Constraint-Netzes als "Keys" und Kopien der zugehörigen Domänen
   * als "Values" und gibt diese zurück.
   * @param constraintNet ConstraintNet
   * @return HashMap
   */
  private HashMap generateClonedDomainsMap(ConstraintNet constraintNet) {
    String curVariableName;
    HashMap clonedDomainsMap = new HashMap();
    List variablesList = constraintNet.getVariables();
    Iterator it = variablesList.iterator();
    // Alle Variablen des Constraint-Netzes 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
   * @throws InconsistencyException
   */
  public boolean search(ConstraintNet constraintNet) throws InconsistencyException {
    if (!backSolve(constraintNet)) {
      throw new InconsistencyException();
    }
    // Dieser Solver ermittelt ausschließlich Lösungen und modifiziert das
    // Constraint-Netz ansonsten nicht:
    return false;
  }

}
