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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.testng.annotations.Test;
import org.testng.asserts.Assertion;
import org.testng.asserts.SoftAssert;
import org.testng.collections.Lists;

import com.google.common.collect.Sets;

import edu.stanford.nlp.sempre.Derivation;
import edu.stanford.nlp.sempre.ExactValueEvaluator;
import edu.stanford.nlp.sempre.Example;
import edu.stanford.nlp.sempre.FeatureExtractor;
import edu.stanford.nlp.sempre.FloatingParser;
import edu.stanford.nlp.sempre.Grammar;
import edu.stanford.nlp.sempre.Json;
import edu.stanford.nlp.sempre.LanguageAnalyzer;
import edu.stanford.nlp.sempre.Params;
import edu.stanford.nlp.sempre.Parser;
import edu.stanford.nlp.sempre.Parser.Spec;
import edu.stanford.nlp.sempre.ParserState;
import edu.stanford.nlp.sempre.Rule;
import edu.stanford.nlp.sempre.Session;
import edu.stanford.nlp.sempre.SimpleLexicon;
import edu.stanford.nlp.sempre.ValueEvaluator;
import edu.stanford.nlp.sempre.interactive.InteractiveBeamParser;
import edu.stanford.nlp.sempre.interactive.DALExecutor;
import edu.stanford.nlp.sempre.interactive.DefinitionAligner;
import edu.stanford.nlp.sempre.interactive.GrammarInducer;
import edu.stanford.nlp.sempre.interactive.InteractiveMaster;
import edu.stanford.nlp.sempre.interactive.InteractiveUtils;
import fig.basic.LogInfo;

/**
 * Test the grammar induction
 * 
 * @author sidaw
 */
public class GrammarInducerTest {
  Assertion hard = new Assertion();
  Assertion soft = new SoftAssert();

  private static Spec defaultSpec() {
    FloatingParser.opts.defaultIsFloating = true;
    DALExecutor.opts.convertNumberValues = true;
    DALExecutor.opts.printStackTrace = true;
    DALExecutor.opts.worldType = "VoxelWorld";

    Derivation.opts.showTypes = false;
    Derivation.opts.showRules = false;
    Derivation.opts.showCat = true;

    LanguageAnalyzer.opts.languageAnalyzer = "interactive.DALAnalyzer";
    Grammar.opts.inPaths = Lists.newArrayList("./interactive/voxelurn.grammar");
    Grammar.opts.useApplyFn = "interactive.ApplyFn";
    Grammar.opts.binarizeRules = false;
    
    SimpleLexicon.opts.inPaths = Lists.newArrayList("./interactive/csscolors.lexicon");

    FeatureExtractor.opts.featureComputers = Sets.newHashSet("interactive.DALFeatureComputer");
    FeatureExtractor.opts.featureDomains = Sets.newHashSet(":rule", ":stats", ":window");

    DefinitionAligner.opts.strategies = Sets.newHashSet(DefinitionAligner.Strategies.ExactExclusion);
    DefinitionAligner.opts.verbose = 2;
    GrammarInducer.opts.verbose = 2;
    InteractiveMaster.opts.useAligner = true;

    DALExecutor executor = new DALExecutor();
    DALExecutor simple_executor = new DALExecutor();

    FeatureExtractor extractor = new FeatureExtractor(executor);

    ValueEvaluator valueEvaluator = new ExactValueEvaluator();
    Grammar grammar = new Grammar();
    grammar.read();
    grammar.write();
    return new Parser.Spec(grammar, extractor, executor, simple_executor, valueEvaluator);
  }

  protected String d(String... defs) {
    List<List<String>> defList = new ArrayList<>();
    for (String def : defs) {
      defList.add(Lists.newArrayList(def, "?"));
    }
    return Json.writeValueAsStringHard(defList);
  }

  class ParseTester {
    Parser parser;
    Params params;
    List<Rule> allRules;

    public ParseTester() {
      parser = new InteractiveBeamParser(defaultSpec());
      params = new Params();
      allRules = new ArrayList<>();
    }

    public void def(String head, String def) {
      List<Rule> induced = InteractiveMaster.induceRulesHelper(":def", head, def, parser, params,
          new Session("testsession"), null);
      allRules.addAll(induced);
      LogInfo.logs("Defining %s := %s, added %s", head, def, induced);
      induced.forEach(r -> InteractiveUtils.addRuleInteractive(r, parser));
    }

    public boolean match(String head, String def) {
      Example.Builder b = new Example.Builder();
      b.setUtterance(head);
      Example exHead = b.createExample();
      exHead.preprocess();

      // LogInfo.logs("Parsing definition: %s", ex.utterance);
      parser.parse(params, exHead, true);

      Derivation defDeriv = InteractiveUtils.combine(InteractiveUtils.derivsfromJson(def, parser, params, null));

      boolean found = false;
      int ind = 0;
      for (Derivation d : exHead.predDerivations) {
        // LogInfo.logs("considering: %s", d.formula.toString());
        LogInfo.logs("Comparing %s vs %s", InteractiveUtils.stripBlock(d).formula.toString(),
            InteractiveUtils.stripBlock(defDeriv).formula.toString());
        if (InteractiveUtils.stripBlock(d).formula.toString()
            .equals(InteractiveUtils.stripBlock(defDeriv).formula.toString())) {
          found = true;
          LogInfo.logs("found %s at %d", d.formula, ind);
        }
        ind++;
      }
      printAllRules();
      if (!found) {
        LogInfo.logs("Did not find %s among \n %s", defDeriv.formula, exHead.predDerivations);
      }
      return found;
    }

    public void accept(String head, String def) {
      LogInfo.begin_track("Accepting");
      Example.Builder b = new Example.Builder();
      b.setUtterance(head);
      Example exHead = b.createExample();
      exHead.preprocess();

      Derivation defDeriv = InteractiveUtils.combine(InteractiveUtils.derivsfromJson(def, parser, params, null));

      // LogInfo.logs("Parsing definition: %s", ex.utterance);
      parser.parse(params, exHead, true);

      for (Derivation deriv : exHead.predDerivations) {
        deriv.compatibility = defDeriv.formula.equals(deriv.formula) ? 1 : 0;
      }
      exHead.predDerivations.forEach(d -> LogInfo.logs("Compatibility %s : %f", d.formula, d.compatibility));

      HashMap<String, Double> counts = new HashMap<>();
      ParserState.computeExpectedCounts(exHead.predDerivations, counts);
      LogInfo.logs("Gradients: %s", counts);
      // LogInfo.logs("paramsbefore: %s", params.getWeights());
      params.update(counts);
      // LogInfo.logs("paramsafter: %s", params.getWeights());
      LogInfo.end_track();
    }

    public void printAllRules() {
      LogInfo.begin_track("Rules induced");
      allRules.forEach(r -> LogInfo.log(r));
      LogInfo.end_track();
    }
  }

  // tests simple substitutions
  @Test(groups = { "Interactive" })
  public void simpleTest() {
    LogInfo.begin_track("simpleTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("add red twice", d("add red top", "add red top"));
    A.assertTrue(T.match("add blue twice", d("add blue top", "add blue top")));

    T.def("add red 3 times", d("repeat 3 [add red]"));
    A.assertTrue(T.match("add yellow 5 times", d("repeat 5 [add yellow]")));

    T.def("add red 3 ^ 2 times", d("repeat 3 {[repeat 3 [add red]]}"));
    A.assertTrue(T.match("add blue 2 ^ 2 times", d("repeat 2 {[repeat 2 [add blue]]}")));

    T.def("add a red block to the left", d("add red left"));
    A.assertTrue(T.match("add a yellow block to the right", d("add yellow right")));

    T.def("remove the leftmost red block", d("remove very left of has color red"));
    A.assertTrue(T.match("remove the leftmost yellow block", d("remove very left of has color yellow")));

    T.def("add red to both sides", d("add red left", "add red right"));
    T.def("add red to both sides", d("add red back", "add red front"));
    A.assertTrue(T.match("add green to both sides", d("add green left", "add green right")));
    A.assertTrue(T.match("add green to both sides", d("add green back", "add green front")));
    A.assertFalse(T.match("add green to both sides", d("add green left", "add green back")));

    T.def("select all but yellow", d("select not has color yellow"));
    A.assertTrue(T.match("select all but blue", d("select not has color blue")));

    T.def("add a yellow block on top of red blocks", d("select has color red", "add yellow top"));
    A.assertTrue(T.match("add a green block on top of blue blocks", d("select has color blue", "add green top")));

    T.def("add a yellow block on top of red blocks", d("select has color red; add yellow top"));
    A.assertTrue(T.match("add a green block on top of blue blocks", d("select has color blue ; add green top")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void actionTest() {
    LogInfo.begin_track("actionTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    // by default, we prefer higher level abstractions
    T.def("add red twice", d("repeat 2 [add red]"));
    // T.def("select has color red twice", d("repeat 2 [select has color
    // red]"));
    A.assertTrue(T.match("add blue top twice", d("repeat 2 [add blue top]")));

    T.def("add red 3 times", d("repeat 3 [add red]"));
    A.assertTrue(T.match("add yellow top 5 times", d("repeat 5 [add yellow top]")));

    T.def("add red then add yellow left", d("add red; add yellow left"));
    A.assertTrue(T.match("remove has color red then add brown", d("remove has color red; add brown")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void notActionTest() {
    LogInfo.begin_track("notActionTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("add red top twice", d("add red; add red top"));
    T.accept("add yellow top twice", d("add yellow; add yellow top"));
    // T.accept("add yellow bot twice", d("add yellow; add yellow bot"));
    // T.accept("add brown left twice", d("add brown; add brown left"));
    T.def("add red top twice", d("add red; add red top"));
    A.assertTrue(T.match("add blue right twice", d("add blue; add blue right")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void learnCatTest() {
    LogInfo.begin_track("test the learning via alignment");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("add cardinal", d("add red"));
    A.assertTrue(T.match("select has color cardinal", d("select has color red")));

    T.def("move up", d("move top"));
    A.assertTrue(T.match("select up", d("select top")));
    A.assertTrue(T.match("add red up", d("add red top")));

    T.def("move it up", d("move top"));
    A.assertTrue(T.match("select up", d("select top")));
    A.assertTrue(T.match("add red up", d("add red top")));

    // do not redefine core test
    T.def("move it top", d("move it bot"));
    A.assertFalse(T.match("select top", d("select bot")));
    A.assertFalse(T.match("add red top", d("add red bot")));

    T.def("select the highest of has color red", d("select very top of has color red"));
    A.assertTrue(T.match("remove the highest of all", d("remove very top of all")));

    T.def("select the top most of has color red", d("select very top of has color red"));
    A.assertTrue(T.match("remove the bot most of all", d("remove very bot of all")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void cubeTest() {
    LogInfo.begin_track("cubeTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("red stick of size 3", d("{repeat 3 [add red; select top]}"));
    T.def("red plate of size 3", d("{repeat 3 [red stick of size 3; select left]}"));
    T.def("red cube of size 3", d("{repeat 3 [red plate of size 3; select back]}"));

    A.assertTrue(T.match("blue stick of size 4", d("{repeat 4 [add blue; select top]}")));
    A.assertTrue(T.match("blue plate of size 4", d("{repeat 4 [blue stick of size 4; select left]}")));
    A.assertTrue(T.match("blue cube of size 5", d("{repeat 5 [blue plate of size 5; select back]}")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void rectTest() {
    LogInfo.begin_track("rectTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("red stick of size 4", d("{repeat 4 [add red; select top]}"));
    T.def("red plate of size 3 by 4", d("{repeat 3 [red stick of size 4; select left]}"));
    T.def("red cube of size 2 by 3 by 4", d("{repeat 2 [red plate of size 3 by 4; select back]}"));

    A.assertTrue(T.match("red plate of size 1 by 2", d("{repeat 1 [red stick of size 2; select left]}")));
    A.assertTrue(T.match("red cube of size 1 by 2 by 3", d("{repeat 1 [red plate of size 2 by 3; select back]}")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

  @Test(groups = { "Interactive" })
  public void setsTest() {
    LogInfo.begin_track("setsTest");
    ParseTester T = new ParseTester();
    Assertion A = hard;

    T.def("remove those red blocks", d("remove has color red"));
    A.assertTrue(T.match("select those red blocks", d("select has color red")));

    T.def("remove the leftmost red block", d("remove very left of has color red"));
    A.assertTrue(T.match("select the leftmost red block", d("select very left of has color red")));

    // T.printAllRules();
    // A.assertAll();

    LogInfo.end_track();
  }

}
