package yacs.parser;

import yacs.domain.*;

import java.util.*;

/**
 * <p>Klasse zur Repräsentation eines binären Operators innerhalb einer
 * rekursiven Struktur zur Repräsentation eines algebraischen
 * Constraint-Problems (Dokumentation siehe <code>Expression</code>).</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 BinaryOperator extends Expression {

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

  /** Bezeichnet den Typ des Operators (vgl. <code>yacs.parser.sym</code>). */
  private int type;

  /** Der "Name" des Operators (für die Ausgabe mit <code>toString()</code>). */
  private String name;

  /**
   * Der erste Constraint-Ausdruck, auf den der Operator angewendet wird.
   * @supplierCardinality 2
   * @clientCardinality 1
   * @link aggregation
   */
  private Expression expr1;

  /**
   * Der zweite Constraint-Ausdruck, auf den der Operator angewendet wird.
   * @supplierCardinality 1
   * @clientCardinality 1
   * @link aggregation
   */
  private Expression expr2;

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

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

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

  /**
   * Konstruktor -- Instantiiert den Operator mit seinem Typ, seinem "Namen" und
   * den beiden zugehörigen Constraint-Ausdrücken.
   * @param type int
   * @param name String
   * @param expr1 Expression
   * @param expr2 Expression
   */
  public BinaryOperator(int type, String name, Expression expr1, Expression expr2) {
    this.type = type;
    this.name = name;
    this.expr1 = expr1;
    this.expr2 = expr2;
  }

  /**
   * Liefert den Namen der Variable, deren Wertebereich eingeschränkt wurde.
   * Gibt <code>null</code> zurück, wenn keine Änderung vorgenommen wurde.
   * ACHTUNG: Verarbeitet zurzeit ausschließlich Constraints bzw.
   * <code>Expressions</code>, die eine "solution function" darstellen.
   * @return String
   */
  public String refine() {
    DomainElement element;
    // Wenn eine "valuation domain" vorliegt, alle Element mit Wert belegen:
    if (this.setValuationDomain()) {
      // Prüfen, ob expr1 eine Variable ist:
      if (this.expr1 instanceof Variable) {
        // Wenn ja, expr2 berechnen:
        element = this.expr2.evaluate();
        // ... und checken, ob die Domäne von expr1 eingeschränkt werden kann:
        if (((Variable)expr1).narrow(this.type, element)) {
          return ((Variable)expr1).getName();
        }
      // Analog expr2, wenn der Expression eine Variable ist:
      } else if (this.expr2 instanceof Variable) {
        // Wenn ja, expr1 berechnen:
        element = this.expr1.evaluate();
        // ... und checken, ob die Domäne von expr2 eingeschränkt werden kann:
        if (((Variable)expr2).narrow(this.type, element)) {
          return ((Variable)expr2).getName();
        }
      }
    }
    // Wenn keine "valuation domain" oder keine "solution function" vorliegt:
    return null;
  }

  public boolean setDomain(String variableName, Domain domain) {
    boolean result;
    result = this.expr1.setDomain(variableName, domain);
    // Wird nicht benötigt, wenn sichergestellt ist, dass dieselben Variablen
    // dieselbe Objektreferenz aufweisen (z.B. durch den Parser):
    // result = result | expr2.setDomain(variableName, domain);
    // ... stattdessen:
    if (result == false) {
      result = this.expr2.setDomain(variableName, domain);
    }
    return result;
  }

  public Domain getDomain(String variableName) {
    Domain result;
    result = this.expr1.getDomain(variableName);
    if (result == null) {
      result = this.expr2.getDomain(variableName);
    }
    return result;
  }

  public List getVariables() {
    List variableList = new LinkedList();
    // Variablen aus beiden "Zweigen" holen:
    variableList.addAll(this.expr1.getVariables());
    variableList.addAll(this.expr2.getVariables());
    // ... doppelte dabei löschen: (Verbesserungsvorschlag: Klasse "Set" nutzen)
    if (variableList.size() > 1) {
      for (int i = 0; i < variableList.size()-1; i++) {
        for (int j = i+1; j < variableList.size(); j++) {
          if (((String)variableList.get(i)).equals((String)variableList.get(j))) {
            variableList.remove(j);
            j--;
          }
        }
      }
    }
    return variableList;
  }

  public List getUnsetVariables() {
    List variableList = new LinkedList();
    // Variablen aus beiden "Zweigen" holen:
    variableList.addAll(this.expr1.getUnsetVariables());
    variableList.addAll(this.expr2.getUnsetVariables());
    // ... doppelte dabei löschen: (Verbesserungsvorschlag: Klasse "Set" nutzen)
    if (variableList.size() > 1) {
      for (int i = 0; i < variableList.size()-1; i++) {
        for (int j = i+1; j < variableList.size(); j++) {
          if (((String)variableList.get(i)).equals((String)variableList.get(j))) {
            variableList.remove(j);
            j--;
          }
        }
      }
    }
    return variableList;
  }

  public boolean unsetVariablesLeft() {
    if (this.getUnsetVariables().isEmpty()) {
      return false;
    }
    return true;
  }

  public boolean set(String variableName, DomainElement element) {
    boolean result;
    result = this.expr1.set(variableName, element);
    result = result | this.expr2.set(variableName, element);
    return result;
  }

  public DomainElement get(String variableName) {
    DomainElement result;
    result = this.expr1.get(variableName);
    if (result == null) {
      result = this.expr2.get(variableName);
    }
    return result;
  }

  public boolean satisfiable() {
    switch (this.type) {
      case sym.EQUAL : return this.expr1.evaluate().equals(this.expr2.evaluate());
      case sym.NOT_EQUAL : return this.expr1.evaluate().notEquals(this.expr2.evaluate());
      case sym.GREATER : return this.expr1.evaluate().greaterAs(this.expr2.evaluate());
      case sym.LOWER : return this.expr1.evaluate().lowerAs(this.expr2.evaluate());
      case sym.GREATER_EQUAL : return this.expr1.evaluate().greaterEqualAs(this.expr2.evaluate());
      case sym.LOWER_EQUAL : return this.expr1.evaluate().lowerEqualAs(this.expr2.evaluate());
      case sym.SEMI : return this.expr1.satisfiable() & this.expr2.satisfiable();
      default:
        System.err.println("Error in BinaryOperator.satisfiable(): unknown operator with type " + this.type);
        return false;
    }
  }

  public DomainElement evaluate() {
    switch (this.type) {
      case sym.PLUS : return (DomainElement)this.expr1.evaluate().plus(this.expr2.evaluate());
      case sym.MINUS : return (DomainElement)this.expr1.evaluate().minus(this.expr2.evaluate());
      case sym.TIMES : return (DomainElement)this.expr1.evaluate().times(this.expr2.evaluate());
      case sym.DIVIDE : return (DomainElement)this.expr1.evaluate().divide(this.expr2.evaluate());
      default:
        System.err.println("Error in BinaryOperator.evaluate(): unknown operator with type " + this.type);
        return null;
    }
  }

  public List getPrimitiveExpressions() {
    List exprList = new LinkedList();
    if ((this.type == sym.EQUAL) || (this.type == sym.NOT_EQUAL) || (this.type == sym.GREATER) ||
        (this.type == sym.LOWER) || (this.type == sym.GREATER_EQUAL) || (this.type == sym.LOWER_EQUAL)) {
      exprList.add(this);
    }
    exprList.addAll(this.expr1.getPrimitiveExpressions());
    exprList.addAll(this.expr2.getPrimitiveExpressions());
    return exprList;
  }

  public Variable getVariableReference(String variableName) {
    Variable result;
    result = this.expr1.getVariableReference(variableName);
    if (result == null) {
      result = this.expr2.getVariableReference(variableName);
    }
    return result;
  }

  public boolean containsVariable(String variableName) {
    boolean result;
    result = this.expr1.containsVariable(variableName);
    if (!result) {
      result = this.expr2.containsVariable(variableName);
    }
    return result;
  }

  public boolean hasFalseDomain() {
    boolean result;
    result = this.expr1.hasFalseDomain();
    result = result | this.expr2.hasFalseDomain();
    return result;
  }

  public boolean hasValuationDomain() {
    boolean result;
    result = this.expr1.hasValuationDomain();
    result = result & this.expr2.hasValuationDomain();
    return result;
  }

  public boolean setValuationDomain() {
    boolean result;
    result = this.expr1.setValuationDomain();
    result = result & this.expr2.setValuationDomain();
    return result;
  }

  public void singletons() {
    this.expr1.singletons();
    this.expr2.singletons();
  }

  public int count(String variableName) {
    int count;
    count = expr1.count(variableName);
    count += expr2.count(variableName);
    return count;
  }

  public void init() {
    this.expr1.init();
    this.expr2.init();
  }

  public String toString() {
    if (this.name == null || name == "") {
      return ("op_"+this.type+"("+this.expr1+","+this.expr2+")");
    } else if (this.name == ";") {
      return (this.expr1+"; "+this.expr2);
    }
    return ("("+this.expr1+this.name+this.expr2+")");
  }

}
