package org.ggp.base.util.gdl.transforms;

import java.util.List;
import java.util.Set;

import org.ggp.base.util.gdl.GdlUtils;
import org.ggp.base.util.gdl.grammar.Gdl;
import org.ggp.base.util.gdl.grammar.GdlDistinct;
import org.ggp.base.util.gdl.grammar.GdlLiteral;
import org.ggp.base.util.gdl.grammar.GdlNot;
import org.ggp.base.util.gdl.grammar.GdlPool;
import org.ggp.base.util.gdl.grammar.GdlRule;
import org.ggp.base.util.gdl.grammar.GdlSentence;
import org.ggp.base.util.gdl.grammar.GdlVariable;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * As a GDL transformer, this class takes in a GDL description of a game,
 * transforms it in some way, and outputs a new GDL descriptions of a game
 * which is functionally equivalent to the original game.
 *
 * The AimaProver does not correctly apply "distinct" literals in rules if
 * they have not yet been bound. (See test_distinct_beginning_rule.kif for
 * an example where this comes up.) The same is true for "not" literals.
 * This transformation moves "distinct" and "not" literals later in the
 * rule, so they always appear after sentence literals have defined those
 * variables.
 *
 * This should be applied to the input to the ProverStateMachine until this
 * bug is fixed some other way.
 */
public class DistinctAndNotMover {
    public static List<Gdl> run(List<Gdl> oldRules) {
        oldRules = DeORer.run(oldRules);

        List<Gdl> newRules = Lists.newArrayListWithCapacity(oldRules.size());
        for (Gdl gdl : oldRules) {
            if (gdl instanceof GdlRule) {
                GdlRule rule = (GdlRule) gdl;
                newRules.add(reorderRule(rule));
            } else {
                newRules.add(gdl);
            }
        }
        return newRules;
    }

    private static GdlRule reorderRule(GdlRule oldRule) {
        List<GdlLiteral> newBody = Lists.newArrayList(oldRule.getBody());
        rearrangeDistinctsAndNots(newBody);
        return GdlPool.getRule(oldRule.getHead(), newBody);
    }

    private static void rearrangeDistinctsAndNots(List<GdlLiteral> ruleBody) {
        Integer oldIndex = findDistinctOrNotToMoveIndex(ruleBody);
        while (oldIndex != null) {
            GdlLiteral literalToMove = ruleBody.get(oldIndex);
            ruleBody.remove((int) oldIndex);
            reinsertLiteralInRightPlace(ruleBody, literalToMove);

            oldIndex = findDistinctOrNotToMoveIndex(ruleBody);
        }
    }

    //Returns null if no distincts have to be moved.
    private static Integer findDistinctOrNotToMoveIndex(List<GdlLiteral> ruleBody) {
        Set<GdlVariable> setVars = Sets.newHashSet();
        for (int i = 0; i < ruleBody.size(); i++) {
            GdlLiteral literal = ruleBody.get(i);
            if (literal instanceof GdlSentence) {
                setVars.addAll(GdlUtils.getVariables(literal));
            } else if (literal instanceof GdlDistinct
                    || literal instanceof GdlNot) {
                if (!allVarsInLiteralAlreadySet(literal, setVars)) {
                    return i;
                }
            }
        }
        return null;
    }

    private static void reinsertLiteralInRightPlace(List<GdlLiteral> ruleBody,
            GdlLiteral literalToReinsert) {
        Set<GdlVariable> setVars = Sets.newHashSet();
        for (int i = 0; i < ruleBody.size(); i++) {
            GdlLiteral literal = ruleBody.get(i);
            if (literal instanceof GdlSentence) {
                setVars.addAll(GdlUtils.getVariables(literal));

                if (allVarsInLiteralAlreadySet(literalToReinsert, setVars)) {
                    ruleBody.add(i + 1, literalToReinsert);
                    return;
                }
            }
        }
    }

    private static boolean allVarsInLiteralAlreadySet(GdlLiteral literal,
            Set<GdlVariable> setVars) {
        List<GdlVariable> varsInLiteral = GdlUtils.getVariables(literal);
        for (GdlVariable varInLiteral : varsInLiteral) {
            if (!setVars.contains(varInLiteral)) {
                return false;
            }
        }
        return true;
    }
}
