package edu.stanford.nlp.sempre;

import java.util.*;
import fig.basic.*;

/**
 * Produces predicates but do it from the logical forms
 * in the context (inspects the ContextValue of the example).
 *
 * Takes depth, restrictType, and forbiddenTypes arguments allowing you
 * to specify the depth/size and type of (formula) subtrees that you want to
 * extract from the context.
 *
 * ONLY USE WITH TYPES!!
 *
 * E.g.,
 *
 * (rule $X (context) (ContextFn (depth 0) (type fb:type.any))
 * would extract any unary/entity.
 *
 * (rule $X (context) (ContextFn (depth 1) (type (-> fb:type.any
 * fb:type.any)) (forbidden (-> fb:type.any fb:type.something)) would
 * extract all binaries except those with arg1 of type fb:type.something
 *
 * @author William Hamilton
 */
// TODO(Will): Reintegrate useful functionality from old implementation.
public class ContextFn extends SemanticFn {
  // the depth/size of subtrees to extract
  private int depth;
  // the type that you want to extract
  private SemType restrictType = SemType.topType;

  // set of types to not extract (overrides restrictType).
  // For example, if restrict type is very general (e.g., (-> type.any type.any))
  // and you don't want some specific subtype  (e.g., (-> type.something type.any))
  // then you would say specify (forbidden (-> type.something type.any))
  // and all subtypes of (-> type.any type.any) would be permissible
  // except the forbidden one(s).
  private Set<SemType> forbiddenTypes = new HashSet<SemType>();

  public void init(LispTree tree) {
    super.init(tree);
    for (int i = 1; i < tree.children.size(); i++) {
      LispTree arg = tree.child(i);
      if ("type".equals(arg.child(0).value)) {
        restrictType = SemType.fromLispTree(arg.child(1));
      } else if ("depth".equals(arg.child(0).value)) {
        depth = Integer.parseInt(arg.child(1).value);
      } else if ("forbidden".equals(arg.child(0).value)) {
        forbiddenTypes.add(SemType.fromLispTree(arg.child(1)));
      } else {
        throw new RuntimeException("Unknown argument: " + arg);
      }
    }
  }

  public DerivationStream call(final Example ex, final Callable c) {
    return new MultipleDerivationStream() {
      int index = 0;
      List<Formula> formulas;

      public Derivation createDerivation() {
        if (ex.context == null) return null;

        if (formulas == null) {
          formulas = new ArrayList<Formula>();
          for (int i = ex.context.exchanges.size() - 1; i >= 0; i--) {
            ContextValue.Exchange e = ex.context.exchanges.get(i);
            extractFormulas(e.formula.toLispTree());
          }
        }
        if (index >= formulas.size()) return null;
        Formula formula = formulas.get(index++);
        for (SemType forbiddenType : forbiddenTypes) {
          if (TypeInference.inferType(formula).meet(forbiddenType).isValid())
            return null;
        }
        return new Derivation.Builder()
                .withCallable(c)
                .formula(formula)
                .type(TypeInference.inferType(formula))
                .createDerivation();
      }

      private void addFormula(Formula formula) {
        if (formulas.contains(formula))
          return;
        formulas.add(formula);
      }

      // Extract from the logical form.
      private void extractFormulas(LispTree formula) {
        if (correctDepth(formula, 0) && typeCheck(formula)) {
          addFormula(Formulas.fromLispTree(formula));
        }
        if (formula.isLeaf())
          return;
        for (LispTree child : formula.children)
          extractFormulas(child);
      }

      private boolean correctDepth(LispTree formula, int currentLevel) {
        if (formula.isLeaf()) {
          return currentLevel == depth;
        } else {
          boolean isCorrect = true;
          for (LispTree child : formula.children)
            isCorrect = isCorrect && correctDepth(child, currentLevel + 1);
          return isCorrect;
        }
      }

      private boolean typeCheck(LispTree treeFormula) {
        Formula formula = Formulas.fromLispTree(treeFormula);
        SemType type = TypeInference.inferType(formula);
        type = restrictType.meet(type);
        return type.isValid();
      }

    };
  }
}
