package edu.stanford.nlp.sempre.interactive.test;

import static org.testng.AssertJUnit.assertEquals;

import java.util.*;
import java.util.function.Predicate;

import fig.basic.*;
import edu.stanford.nlp.sempre.*;
import edu.stanford.nlp.sempre.Parser.Spec;
import edu.stanford.nlp.sempre.interactive.InteractiveBeamParser;
import edu.stanford.nlp.sempre.interactive.DALExecutor;

import org.testng.Assert;
import org.testng.annotations.Test;
import org.testng.collections.Lists;

/**
 * Test the parser, and some floating examples
 * @author sidaw
 */
@Test(groups = { "InteractiveLearning" })
public class FloatingParsingTest {
  Predicate<Example> contains(String formula) {
    Formula answer = Formulas.fromLispTree(LispTree.proto.parseFromString(formula));
    return e -> e.predDerivations.stream().anyMatch(d -> d.formula.equals(answer));
  }
  Predicate<Example> moreThan(int count) {
    return e -> e.predDerivations.size() > count;
  }
  Predicate<Example> hasAll(String...substrings) {
    return new Predicate<Example>() {
      List<String> required = Lists.newArrayList(substrings);

      @Override
      public boolean test(Example e) {
        int match = 0;
        for (Derivation deriv : e.predDerivations) {
          String formula = deriv.formula.toString();
          if (required.stream().anyMatch(s -> formula.indexOf(s)!=-1)) {
            match ++;
            LogInfo.log("Got a match: " + formula);
          }
        }
        if (match == 0)
          throw new RuntimeException("Failed to match " + required.toString() + " : " + e.utterance);
        return true;
      }
    };
  }

  private static Spec defaultSpec() {
    FloatingParser.opts.defaultIsFloating = true;
    DALExecutor.opts.convertNumberValues  = true;
    DALExecutor.opts.printStackTrace = true;
    DALExecutor.opts.worldType = "VoxelWorld";
    Grammar.opts.inPaths = Lists.newArrayList("./interactive/voxelurn.grammar");
    Grammar.opts.useApplyFn = "interactive.ApplyFn";
    Grammar.opts.binarizeRules = false;

    DALExecutor executor = new DALExecutor();
    DALExecutor simple_executor = new DALExecutor();
    DALExecutor.opts.worldType = "BlocksWorld";
    FeatureExtractor extractor = new FeatureExtractor(executor);
    FeatureExtractor.opts.featureDomains.add("rule");
    ValueEvaluator valueEvaluator = new ExactValueEvaluator();
    Grammar grammar = new Grammar();
    grammar.read();
    grammar.write();
    return new Parser.Spec(grammar, extractor, executor, simple_executor, valueEvaluator);
  }

  protected static void parse(String beamUtt, String floatUtt, ContextValue context, Predicate<Example> checker) {
    LogInfo.begin_track("Cannonical: %s\t Float: %s", beamUtt, floatUtt);

    Example.Builder b = new Example.Builder();
    b.setId("session:test");
    b.setUtterance(floatUtt);
    b.setContext(context);
    Example ex = b.createExample();
    ex.preprocess();

    Spec defSpec = defaultSpec();
    Parser parser = new InteractiveBeamParser(defSpec);
    ParserState state = parser.parse(new Params(), ex, false);
    LogInfo.end_track();

    // Add the floating parser and check?
    if (checker != null) {
      if (!checker.test(ex)) {
        Assert.fail(floatUtt);
      }
    }
  }

  private static ContextValue getContext(String blocks) {
    // a hack to pass in the world state without much change to the code
    String strigify2 = Json.writeValueAsStringHard(blocks); // some parsing issue inside lisptree parser
    return ContextValue.fromString(String.format("(context (graph NaiveKnowledgeGraph ((string \"%s\") (name b) (name c))))", strigify2));
  }

  public void basicTest() {
    String defaultBlocks = "[[1,1,1,\"Green\",[]],[1,2,1,\"Blue\",[]],[2,2,1,\"Red\",[]],[3,2,2,\"Yellow\",[]]]";
    ContextValue context = getContext(defaultBlocks);
    LogInfo.begin_track("testJoin");

    parse("select all", "select all", context, contains("(: select *)"));
    // parse("select has color red", "red blocks", context, contains("(:for (color red) (: select))"));
    parse("select has color red", "red blocks", context, hasAll("(color red)", "(: select)", ":foreach"));
    parse("add red top", "add some to top of red blocks", context, contains("(: add red top)"));
    parse("for has color red [ remove ]", "remove red blocks", context, contains("(:foreach (color red) (: remove))"));
    parse("repeat 3 [add red]", "add red 3 times", context, contains("(:loop (number 3) (: add red))"));
    parse("for has color red [ add yellow top ]", "add red to top of yellow", context, moreThan(0));
    // parse("select has row 3", "select row 3", context, moreThan(0));
    // parse("select has color red or has color green", "select red and green", context, contains("(:for (or (color red) (color green)) (: select))"));
    // parse("select has color red or has color green", "select red or green", context, contains("(:for (or (color red) (color green)) (: select))"));
    parse("remove has color red ; remove has color blue", "remove red then remove blue", context, moreThan(0));
    LogInfo.end_track();
  }

  public void advanced() {
    String defaultBlocks = "[[1,1,1,\"Green\",[]],[1,2,1,\"Blue\",[]],[2,2,1,\"Red\",[]],[3,2,2,\"Yellow\",[]]]";
    ContextValue context = getContext(defaultBlocks);
    LogInfo.begin_track("testJoin");

    parse("repeat 4 [add yellow]", "add 4 yellow blocks", context, hasAll("(:loop", "(number 4)", "(color yellow)"));
    parse("repeat 4 [for has color red [ add yellow left ] ]", "put 4 yellow left of red", context, hasAll(":for", "red", "left"));
    parse("", "put 4 yellow to the left of red", context, hasAll(":foreach", "red", "left"));
    parse("", "select has color red or has color green",  context, hasAll("(color red)", "(color green)", "(: select)"));
    parse("", "add red then add green and then add yellow",  context, hasAll("(color red)", "(color green)", "(color yellow)"));
    parse("", "add red then add green and then add yellow",  context, hasAll("(: add", "(color red)", "(color green)", "(color yellow)"));
    parse("", "remove the very left yellow block", context, moreThan(0));
    parse("", "add red top then add yellow then add green", context, moreThan(0));
    parse("", "add 4 yellow to red or green", context, moreThan(0));
    // might be manageable with projectivity
    parse("", "add 4 yellow to the left of red or green", context, moreThan(0));
    parse("", "repeat 3 [delete top of all]", context, moreThan(0));
    parse("", "repeat 3 [delete top of all]", context, moreThan(0));
    // parse("", "add 3 red to left", context, hasAll("(:loop (number 3) (: add red left))"));
    // parse("repeat 5 [ add red left ]", "add 5 red left", context, hasAll("(:loop (number 5) (: add red left))"));
    LogInfo.end_track();
  }
  // things we won't handle
  public void outOfScope() {
    String defaultBlocks = "[[1,1,1,\"Green\",[]],[1,2,1,\"Blue\",[]],[2,2,1,\"Red\",[]],[3,2,2,\"Yellow\",[]]]";
    ContextValue context = getContext(defaultBlocks);
    LogInfo.begin_track("testJoin");

    parse("", "repeat 3 [ repeat 3 [delete very top of all] ]", context, moreThan(0));
    LogInfo.end_track();
  }
}
