/*
 * Disambiguator.java
 *
 * Autor: Piotr Achinger <piotr.achinger at gmail.com>
 */
package dendrarium.trees.disamb;

import dendrarium.trees.AnswerType;
import dendrarium.trees.Forest;
import dendrarium.trees.Node;
import dendrarium.trees.NodeChildren;
import dendrarium.trees.NonterminalNode;
import dendrarium.utils.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import org.jboss.seam.log.Log;

/**
 * POEZJA
 *
 * @author Piotr Achinger <piotr.achinger at gmail.com>
 * @author duże zmiany poczyniła Karolina Sołtys
 */
public class Disambiguator {

    public Log log;

    protected Forest forest;

    protected NonterminalNode current;

    private List<NonterminalNode> accessibleNodes;

    private List<Variant> variants;
    private List<Variant> suggestedVariants;

    protected Variant chosenFilteringVariant;
    private List<Variant> filteringVariants;
    private List<Variant> suggestedFilteringVariants;
    private Comparator<Variant> filteringComparator
            = new ConstituentsSequenceComparator();

    private int hash;

    private boolean done;

    public Disambiguator(Forest forest) {
        this.forest = forest;
    }


    /* === G & S === */
    /** wszystkie wierzchołki w aktualnym drzewie rozbioru */
    public List<NonterminalNode> getAccessibleNodes() {
        return accessibleNodes;
    }

    /** wierzchołek aktualnie rozważany przez użytkownika */
    public Node getCurrent() {
        return current;
    }

    /** lista wszystkich wariantów dzieci danego wierzchołka */
    public List<Variant> getVariants() {
        return variants;
    }

    /** lista sugerowanych wariantów dzieci danego wierzchołka */
    public List<Variant> getSuggestedVariants() {
        return suggestedVariants;
    }

    public Forest getForest() {
        return forest;
    }

    /** jakaś liczba zależna od szczegółów aktualnego drzewa rozbioru,
     * potrzebna przy rysowaniu drzewek */
    public int getHash() {
        return hash;
    }

    /** czy zostały jeszcze jakieś wierzchołki, w których konieczna jest decyzja
     * użytkownika */
    public Boolean isDone() {
        return done;
    }

    /* === informacje potrzebne do sterowania procesem udzielania odpowiedzi
     * (np. w decyzji czy pokazać użytkownikowi wszystkie warianty, czy tylko
     * sugerowane === */
    public Boolean hasChoice() {
        return !(current == null);
    }

    public boolean hasSuggestedVariants() {
        return !suggestedVariants.isEmpty();
    }

    public boolean hasNotSuggestedVariants() {
        return variants.size() > suggestedVariants.size();
    }
    /**
     * @return czy w danym wariancie użytkownik wybrał wcześniej wierzchołek,
     * który nie był sugerowany
     */
    public boolean chosenNotSuggested() {
        if (current.getChosenChildren() == null) {
            return false;
        } else {
            return !isSuggested(current.getId(), current.getChosenChildrenNo());
        }
    }

    public boolean noTrees() {
        return forest.isEmpty();
    }


    /* === metody wykonujące polecenia użytkownika:
     * zmianę aktualnie rozważanego wierzchołka
     * lub dokonanie wyboru wariantu dla aktualnego wierzchołka === */
    public void setCurrent(NonterminalNode node) {
        current = node;
        chosenFilteringVariant = null;
        calcVariants();
        calcHash();
    }

    public void chooseVariant(Variant variant) {
        choose(current.getId(), variant.getChildrenNo());
    }

    public void chooseFilteringVariant(Variant variant) {
        chosenFilteringVariant = variant;
        calcVariants();
        calcHash();
    }

    /* === logika dezambiguatora - głółnie mechanizm dokonywania automatycznych
     * wyborów, zawarty w metodzie saturate === */
    /** mechanizm nasycania - stara się jak najbardziej rozwinąć aktualne
     * drzewo rozbioru, dokonując automatycznych wyborów dzieci; dla danego
     * wierzchołka może automatycznie wybrać dziecko, jeśli jest ono:
     * - wcześniej wybrane przez użytkownika
     * - jedynym dzieckiem
     * - jedynym sugerowanym dzieckiem (sugerowanie implementowane jest przez
     *      podklasy - np. ze względu na zgodność z NKJP)
     *
     * Jeśli nie może automatycznie wybrać dziecka dla pewnego osiągalnego w
     * tworzonym drzewie wierzchołka, zaznacza, że trzeba o ten wierzchołek
     * zapytać użytkownika.
     *
     * Przy każdym dokonywanym wyborze zapisuje w wierzchołku, jaki był rodzaj
     * wyboru (NonterminalNode.ChoiceType) dokonany w tym wierzchołku (tzn. który z powyższych
     * przypadków zaszedł), co jest widoczne później w interfejsie.
     */
    public void saturate() {
        /* wierzchołki, do których możemy dojść w aktualnym drzewie rozbioru, dla
         * których jeszcze w danym wywołaniu saturate nie wybraliśmy dzieci
         */
        Stack<NonterminalNode> unhappyNodes = new Stack<NonterminalNode>();
        accessibleNodes = new LinkedList<NonterminalNode>();
        /* wierzchołek, o który zapytamy użytkownika po zakończeniu pogłębiania */
        NonterminalNode nextCurrent = null;

        /* na początku jedynym nienasyconym wierzchołkiem jest korzeń */
        unhappyNodes.add(forest.getRoot());

        /* nieciekawy przypadek szczególny, kiedy nie ma żadnych drzew rozbioru,
         * po prostu pytamy użytkownika o sztuczny korzeń, żeby wybrał jakąś
         * odpowiedź specjalną
         */
        if (forest.isEmpty()) {
            setCurrent(forest.getRoot());
            return;
        }

        /* dopoki sa niezaspokojone wierzcholki próbujemy automatycznie
         * zaspokoić (wybrać dziecko - chosenChildren) aktualny wierzcholek (active) */
        while (!unhappyNodes.empty()) {
            NonterminalNode active = unhappyNodes.pop();
            accessibleNodes.add(active);
            NodeChildren chosenChildren = null;

            List<NodeChildren> children = active.getChildren();
            List<NodeChildren> suggestedChildren = new LinkedList<NodeChildren>();

            /* próbujemy dokonać automatycznego wyboru */
            if (children.size() == 1) {
                /* jeśli wierzchołek ma tylko jedno dziecko, wybieramy je, dbając
                 * o prawidłowe określenie rodzaju wyboru w tym wierzchołku
                 * (czyli czy dziecko było sugerowane, czy nie)
                 */
                chosenChildren = children.get(0);
                if (isSuggested(active.getId(), 0)) {
                    active.setChoiceType(NonterminalNode.ChoiceType.ONE_SUGGESTED_CHILD);
                } else {
                    active.setChoiceType(NonterminalNode.ChoiceType.ONE_NOT_SUGGESTED_CHILD);
                }
            } else {
                /* liczymy, ile jest sugerowanych dzieci dla danego wierzchołka */
                for (int i = 0 ; i < children.size() ; i++) {
                    if (isSuggested(active.getId(), i)) {
                        suggestedChildren.add(children.get(i));
                    }
                }
                /* jeśli jest tylko jedno sugerowane dziecko, wybieramy je - o ile
                 użytkownik nie dokonał wcześniej innego wyboru */
                if (suggestedChildren.size() == 1 && (active.getChosenChildren() == null ||
                        active.getChosenChildren() == suggestedChildren.get(0))) {
                    chosenChildren = suggestedChildren.get(0);
                    active.setChoiceType(NonterminalNode.ChoiceType.MANY_CHILDREN_ONE_SUGGESTED);
                }
            }
            if (/* pta ks 19022010 <3 */ chosenChildren == null && active.getChosenChildren() != null) {
                /* jeśli wierzchołek ma już wcześniej wybrane (bezpośrednio przez użytkownika) dziecko, wybieramy je ponownie */
                chosenChildren = active.getChosenChildren();
                if (isSuggested(active.getId(), active.getChosenChildrenNo())) {
                    active.setChoiceType(NonterminalNode.ChoiceType.USER_SUGGESTED);
                } else {
                    active.setChoiceType(NonterminalNode.ChoiceType.USER_NOT_SUGGESTED);
                }
            }
            if (chosenChildren == null) {
                /* w danym wierzchołku nie mogliśmy dokonać automatycznego wyboru */
                /* !!! interwencja (rev 80): usuniecie ChoiceType.TERMINAL */
                if (children.isEmpty()) {
                    /* jesteśmy w nieterminalu ktory nie ma dzieci */
                    active.setChoiceType(NonterminalNode.ChoiceType.TERMINAL);
                } else {
                    /* o ten wierzchołek musimy zapytać użytkownika */
                    active.setChoiceType(NonterminalNode.ChoiceType.NONE);
                    if (nextCurrent == null) {
                        nextCurrent = active;
                    }
                }
            } else {
                /* wybralismy dziecko, wierzchołki wchodzące w jego skład dodajemy
                 * do listy wierzchołków wymagających nasycenia */
                chooseAuto(active.getId(), active.getChildren().indexOf(chosenChildren));
                for (Node node : chosenChildren.getChildren()) {
                    if (node instanceof NonterminalNode) {
                        unhappyNodes.push((NonterminalNode) node);
                    }
                }
            }
        }

        /* jeśli po zakończeniu pogłębiania żaden wierzchołek nie został oznaczony
         * jako wymagający interwencji użytkownika, oznacza to, że proces odpowiedzi
         * zakończył się */
        if (nextCurrent != null) {
            done = false;
        } else {
            done = true;
        }
        setCurrent(nextCurrent);
    }

    /** usuwa wybory dokonane w wierzchołkach, które przestały być dostępne (tzn.
     * nie należą już do wybranego drzewa rozbioru), bo użytkownik zmienił swój
     * wybór dokonany w wyższej części drzewa. Metoda wywoływana po zaakceptowaniu
     * odpowiedzi; dzięki temu dwie merytorycznie równoważne odpowiedzi są fizycznie
     * (w przeciwnym wypadku mogłyby je rozróżniać błędne wybory, z których jeden
     * z użytkowników się wycofał)
     */
    public void clearNotAccessible() {
        saturate();
        for (int i = 0 ; i < forest.getNodes() ; ++i) {
            Node node = forest.nodeById(i);
            if (!accessibleNodes.contains(node) && node instanceof NonterminalNode) {
                ((NonterminalNode) node).setChosenChildren(null);
            }
        }
    }

    /** automatyczny wybór wierzchołka, nie wywołuje saturate (bo to właśnie
     * saturate wywołuje tę metodę)
     */
    protected void chooseAuto(int nodeId, int childrenNo) {
        NonterminalNode node = (NonterminalNode) forest.nodeById(nodeId);
        node.setChosenChildren(node.getChildren().get(childrenNo));
    }

    /** wykonuje czynności związane z ręcznym (dokonanym przez użytkownika)
     * wyborem wierzchołka oraz wywołuje metodę saturate (która spowoduje
     * dokonanie dalszych, automatycznych wyborów)
     */
    private void choose(int nodeId, int childrenNo) {
        NonterminalNode node = (NonterminalNode) forest.nodeById(nodeId);
        node.setChosenChildren(node.getChildren().get(childrenNo));

        if (isSuggested(nodeId, childrenNo)) {
            node.setChoiceType(NonterminalNode.ChoiceType.USER_SUGGESTED);
        } else {
            node.setChoiceType(NonterminalNode.ChoiceType.USER_NOT_SUGGESTED);
        }

        saturate();
    }

    /* === metody tworzące listę wariantów === */
    /**
     * Plytkie liczenie roznic
     * 
     * @author pta
     */
    private void calcVariants() {
        variants = new LinkedList<Variant>();
        TreeMap <Integer, LinkedList<Variant>> variantsUnsorted = new TreeMap <Integer, LinkedList<Variant>>(); //AX
        suggestedVariants = new LinkedList<Variant>();
        if (current == null) {
            return;
        }
        log.info("current.children " + current.getChildren().size() + " current.id " + current.getId());

        /* 1. grupowanie wzgledem reguly (produkcji) */

        /* mapa (nazwa reguly) -> lista wariantow z ta regula */
        Map<String, List<NodeChildren>> byRule = new HashMap<String, List<NodeChildren>>();
        for (NodeChildren nc : current.getChildren()) {
            if (!byRule.containsKey(nc.getRule())) {
                byRule.put(nc.getRule(), new LinkedList<NodeChildren>());
            }
            byRule.get(nc.getRule()).add(nc);
        }

        /* 2. laczenie w GRUPY : w obrebie grupy jest ta sama produkcja, takie same zestawy dzieci
         *     (zakladamy ze to drugie wynika z pierwszego) oraz takie same podzdania dzieci (zakresy),
         *     grupy sa numerowane kolejno przez algorytm
         *
         * W ramach tej fazy liczymy jednoczesnie roznice atrybutow
         */

        /* mapa (id grupy) -> lista wariantow z tej grupy */
        Map<Integer, List<NodeChildren>> byGroupId = new HashMap<Integer, List<NodeChildren>>();
        /* mapa (id grupy) -> zbior par (numer dziecka, nazwa atrybutu) rozniacych sie atrybutow */
        Map<Integer, Set<Pair<Integer, String>>> differingAttributes =
                                                 new HashMap<Integer, Set<Pair<Integer, String>>>();


        int group = 0; // numer kolejnej grupy
        /* przeprowadzamy grupowanie (zapelniamy byGroupId oraz differingAttributes
         * dla kolejnych regul (produkcji)
         */
        for (List<NodeChildren> ruleGroup : byRule.values()) {
            /* mapa (id grupy) -> pierwszy dodany do niej wariant, w obrebie ruleGroup */
            Map<Integer, NodeChildren> byGroupIdFirst = new HashMap<Integer, NodeChildren>();

            /* dla kazdego wariantu w obrebie reguly sprawdzamy, czy mozna go dodac do istniejacej grupy */
            for (NodeChildren nc : ruleGroup) {
                /* czy trzeba stworzyc nowa grupe */
                boolean isNew = true;

                /* dla kazdej grupy juz utworzonej w obrebie reguly (produkcji) */
                for (Integer groupId : byGroupIdFirst.keySet()) {
                    /* reprezentant tej grupy -- pierwszy do niej dodany */
                    NodeChildren nc2 = byGroupIdFirst.get(groupId);

                    /* sprawdzenie, czy mozna dodac nc do grupy nc2 */
                    if (equalRanges(nc, nc2)) {
                        /* dodajemy nc do grupy nc2 */
                        byGroupId.get(groupId).add(nc);

                        /* aktualizujemy zbior roznic atrybutow w obrebie grupy */
                        for (int i = 0 ; i < nc.getChildren().size() ; ++i) {
                            Node child = nc.getChildren().get(i);
                            Node child2 = nc2.getChildren().get(i);

                            for (String key : child.getAttributesMap().keySet()) {
                                if (!child.getAttribute(key).equals(
                                        child2.getAttribute(key))) {

                                    /* atrybut key i-tego dziecka nc != atrybut key i-tego dziecka nc2,
                                     * zatem dodajemy (i, key) do zbioru roznic
                                     */
                                    differingAttributes.get(groupId).add(new Pair<Integer, String>(i, key));
                                }
                            }
                        }

                        /* nc zalatwione */
                        isNew = false;
                        break;
                    }
                }

                /* utworzenie nowej grupy */
                if (isNew) {
                    byGroupIdFirst.put(group, nc);
                    byGroupId.put(group, new LinkedList<NodeChildren>());
                    byGroupId.get(group).add(nc);
                    differingAttributes.put(group, new HashSet<Pair<Integer, String>>());
                    ++group;
                }
            }
        }

        /* 3. tworzenie listy wariantow */
        /* rozpatrujemy warianty w kolejnosci grup */
        for (Integer groupId : byGroupId.keySet()) {
            for (NodeChildren nc : byGroupId.get(groupId)) {
                List<ChildInfo> children = new LinkedList<ChildInfo>();
                String tempDesc = "* " + nc.getRule() + "\n";

                /* konstruujemy liste dzieci (ChildInfo) z opisami) */
                for (int i = 0 ; i < nc.getChildren().size() ; ++i) {
                    Node child = nc.getChildren().get(i);

                    /* lista (tylko istotnych) atrybutow */
                    List<String> diffAttr = new LinkedList<String>();
                    for (Pair<Integer, String> diff : differingAttributes.get(groupId)) {
                        int childNo = diff.getF1();
                        String attrName = diff.getF2();

                        if (childNo == i) {
                            diffAttr.add(attrName);
                        }
                    }

                    ChildInfo childInfo = new ChildInfo(child.getSymbol(), child.getAttributes(), diffAttr,
                            forest.wordRangeAsList(child.getFrom(), child.getTo()));

                    children.add(childInfo);
                    tempDesc += " |- " + child.getSymbol() + " (" + diffAttr + ")  ... \"" + forest.wordRange(child.getFrom(), child.getTo()) + "\"\n";
                }

                Variant variant = new Variant(tempDesc, nc.getIndex(), groupId, nc.getRule(), children, nc.getSubtrees());
                variant.setComment(decorate(current.getId(), variant.getChildrenNo()));
                variant.setChosen(isChosen(current.getId(), variant.getChildrenNo()));
                if (chosenFilteringVariant != null &&
                        filteringComparator.compare(chosenFilteringVariant, variant) != 0)
                    continue;
                
                //variants.add(variant);  //AX wykomentowuje to
                
                //AX begin //
                LinkedList<Variant> variantsForGroup = variantsUnsorted.remove(groupId);
                if (variantsForGroup == null) variantsForGroup = new LinkedList<Variant>();
                variantsForGroup.add(variant);
                variantsUnsorted.put(groupId, variantsForGroup);
                //AX end //
            }
        }

        // AX begin //
        VariantComparator gcmp = new VariantComparator();
        Set<Variant> groupIDsSorted = new TreeSet(gcmp);
        // przepisujemy na variants:
        for (Integer groupID : variantsUnsorted.keySet()) {
            List<Variant> variants4GroupID = variantsUnsorted.get(groupID);
            groupIDsSorted.addAll(variants4GroupID);
        }
        Iterator<Variant> itr = groupIDsSorted.iterator();
        while(itr.hasNext())
            variants.add(itr.next());
        // AX end //

        
        for (Variant v : variants) {
            boolean suggested = isSuggested(current.getId(), v.getChildrenNo());
            v.setSuggested(suggested);
            if (suggested)
                suggestedVariants.add(v);
        }
    }

    /** metoda używana przez calcVariant, sprawdzająca, czy dwoje dzieci
     * wierzchołka należy do tej samej grupy */
    private boolean equalRanges(NodeChildren nc1, NodeChildren nc2) {
        if (nc1.getChildren().size() != nc2.getChildren().size()) {
            return false;
        } else {
            for (int i = 0 ; i < nc1.getChildren().size() ; ++i) {
                Node n1 = nc1.getChildren().get(i);
                Node n2 = nc2.getChildren().get(i);
                //log.info("n1.getSymbol()="+n1.getSymbol()+" n2.getSymbol()="+n2.getSymbol());
                if (n1.getFrom() != n2.getFrom() || n1.getTo() != n2.getTo() || !(n1.getSymbol().contentEquals(n2.getSymbol())) ) {
                    return false;
                }
            }

            return true;
        }
    }

    private void calcHash() {
        hash = this.hashCode();

        for (int i = 0 ; i < forest.getNodes() ; ++i) {
            Node node = forest.nodeById(i);

            if (node instanceof NonterminalNode) {
                NonterminalNode nt = (NonterminalNode) node;
                if (nt.getChosenChildren() != null) {
                    hash = 1738 * hash + 1231 * nt.getChildren().indexOf(nt.getChosenChildren()) + 23 * i;
                }
            }
        }
    }

    public void calcFilteringVariants(List<Variant> allVariants) {
        assert chosenFilteringVariant == null;
        assert variants != null;

        if (allVariants == null)
            allVariants = new ArrayList<Variant>(variants);
        Collections.sort(allVariants, filteringComparator);

        filteringVariants = new ArrayList<Variant>();
        Variant firstInGroup = new Variant();
        for (Variant v: allVariants) {
            if (filteringComparator.compare(v, firstInGroup) != 0) {
                Variant groupVariant = v.clone();
                groupVariant.setSubtrees(0);
                firstInGroup = groupVariant;
                filteringVariants.add(groupVariant);
            }

            // Comments choosing rules:
            //  1. If any of the variants has no comment, the group also has
            //     no comment.
            //  2. If all variants have the same comment, the group has it too.
            //  3. Otherwise, the text "(Wiele komentarzy)" is used.
            if (v.getComment() == null || v.getComment().isEmpty()) {
                firstInGroup.setComment(null);
            } else if (firstInGroup.getComment() != null) {
                if (!firstInGroup.getComment().equals(v.getComment())) {
                    firstInGroup.setComment("(Wiele komentarzy)");
                }
            }

            if (v.getSuggested()) {
                firstInGroup.setSuggested(true);
            }

            if (v.getChosen()) {
                firstInGroup.setChosen(true);
            }

            firstInGroup.setSubtrees(firstInGroup.getSubtrees() + v.getSubtrees());
        }
    }

    private void calcFilteringVariants() {
        calcFilteringVariants(null);
    }


    /* === pewne własności wariantu === */
    public boolean isChosen(int nodeId, int childrenNo) {
        NonterminalNode parent = (NonterminalNode) forest.nodeById(nodeId);
        return parent.getChosenChildren() == parent.getChildren().get(childrenNo);
    }

    /** metoda przesłaniana w podklasach, których głównym zadaniem jest sugerowanie
     * pewnych wariantów wyboru (np. zgodnych z NKJP). Główny mechanizm dezambiguatora,
     * zawarty w metodzie saturate, będzie wybierał takie warianty automatycznie,
     * w przypadkach, kiedy to jest sensowne (tzn. kiedy do wyboru jest jeden
     * wariant sugerowany i kilka nie sugerowanych)
     */
    public boolean isSuggested(int nodeId, int childrenNo) {
        return false;
    }

    /** metody przesłaniane w podklasach, zazwyczaj jest to słowny opis tego,
     * dlaczego dany wariant (lub odpowiedź specjalna) jest sugerowana (bądź dlaczego nie jest), widoczny
     * w interfejsie użytkownika
     */
    String decorate(int nodeId, int childrenNo) {
        return "";
    }

    public String decorate(AnswerType type) {
        return "";
    }

    public boolean isFiltered() {
        return chosenFilteringVariant != null;
    }

    public List<Variant> getFilteringVariants() {
        return filteringVariants;
    }


}
