package de.ugoe.cs.quest.usageprofiles;

import java.io.Serializable;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;

import de.ugoe.cs.util.StringTools;

import edu.uci.ics.jung.graph.DelegateTree;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.Tree;

/**
 * <p>
 * This class implements a <it>trie</it>, i.e., a tree of sequences that the occurence of
 * subsequences up to a predefined length. This length is the trie order.
 * </p>
 * 
 * @author Steffen Herbold
 * 
 * @param <T>
 *            Type of the symbols that are stored in the trie.
 * 
 * @see TrieNode
 */
public class Trie<T> implements IDotCompatible, Serializable {

    /**
     * <p>
     * Id for object serialization.
     * </p>
     */
    private static final long serialVersionUID = 1L;

    /**
     * <p>
     * Collection of all symbols occuring in the trie.
     * </p>
     */
    private Collection<T> knownSymbols;

    /**
     * <p>
     * Reference to the root of the trie.
     * </p>
     */
    private final TrieNode<T> rootNode;

    /**
     * <p>
     * Contructor. Creates a new Trie.
     * </p>
     */
    public Trie() {
        rootNode = new TrieNode<T>();
        knownSymbols = new LinkedHashSet<T>();
    }

    /**
     * <p>
     * Copy-Constructor. Creates a new Trie as the copy of other. The other trie must noch be null.
     * </p>
     * 
     * @param other
     *            Trie that is copied
     */
    public Trie(Trie<T> other) {
        if (other == null) {
            throw new InvalidParameterException("other trie must not be null");
        }
        rootNode = new TrieNode<T>(other.rootNode);
        knownSymbols = new LinkedHashSet<T>(other.knownSymbols);
    }

    /**
     * <p>
     * Returns a collection of all symbols occuring in the trie.
     * </p>
     * 
     * @return symbols occuring in the trie
     */
    public Collection<T> getKnownSymbols() {
        return new LinkedHashSet<T>(knownSymbols);
    }

    /**
     * <p>
     * Trains the current trie using the given sequence and adds all subsequence of length
     * {@code maxOrder}.
     * </p>
     * 
     * @param sequence
     *            sequence whose subsequences are added to the trie
     * @param maxOrder
     *            maximum length of the subsequences added to the trie
     */
    public void train(List<T> sequence, int maxOrder) {
        if (maxOrder < 1) {
            return;
        }
        IncompleteMemory<T> latestActions = new IncompleteMemory<T>(maxOrder);
        int i = 0;
        for (T currentEvent : sequence) {
            latestActions.add(currentEvent);
            knownSymbols.add(currentEvent);
            i++;
            if (i >= maxOrder) {
                add(latestActions.getLast(maxOrder));
            }
        }
        int sequenceLength = sequence.size();
        for (int j = maxOrder - 1; j > 0; j--) {
            add(sequence.subList(sequenceLength - j, sequenceLength));
        }
    }

    /**
     * <p>
     * Adds a given subsequence to the trie and increases the counters accordingly.
     * </p>
     * 
     * @param subsequence
     *            subsequence whose counters are increased
     * @see TrieNode#add(List)
     */
    protected void add(List<T> subsequence) {
        if (subsequence != null && !subsequence.isEmpty()) {
            knownSymbols.addAll(subsequence);
            subsequence = new LinkedList<T>(subsequence); // defensive copy!
            T firstSymbol = subsequence.get(0);
            TrieNode<T> node = getChildCreate(firstSymbol);
            node.add(subsequence);
        }
    }

    /**
     * <p>
     * Returns the child of the root node associated with the given symbol or creates it if it does
     * not exist yet.
     * </p>
     * 
     * @param symbol
     *            symbol whose node is required
     * @return node associated with the symbol
     * @see TrieNode#getChildCreate(Object)
     */
    protected TrieNode<T> getChildCreate(T symbol) {
        return rootNode.getChildCreate(symbol);
    }

    /**
     * <p>
     * Returns the child of the root node associated with the given symbol or null if it does not
     * exist.
     * </p>
     * 
     * @param symbol
     *            symbol whose node is required
     * @return node associated with the symbol; null if no such node exists
     * @see TrieNode#getChild(Object)
     */
    protected TrieNode<T> getChild(T symbol) {
        return rootNode.getChild(symbol);
    }

    /**
     * <p>
     * Returns the number of occurences of the given sequence.
     * </p>
     * 
     * @param sequence
     *            sequence whose number of occurences is required
     * @return number of occurences of the sequence
     */
    public int getCount(List<T> sequence) {
        int count = 0;
        TrieNode<T> node = find(sequence);
        if (node != null) {
            count = node.getCount();
        }
        return count;
    }

    /**
     * <p>
     * Returns the number of occurences of the given prefix and a symbol that follows it.<br>
     * Convenience function to simplify usage of {@link #getCount(List)}.
     * </p>
     * 
     * @param sequence
     *            prefix of the sequence
     * @param follower
     *            suffix of the sequence
     * @return number of occurences of the sequence
     * @see #getCount(List)
     */
    public int getCount(List<T> sequence, T follower) {
        List<T> tmpSequence = new LinkedList<T>(sequence);
        tmpSequence.add(follower);
        return getCount(tmpSequence);

    }

    /**
     * <p>
     * Searches the trie for a given sequence and returns the node associated with the sequence or
     * null if no such node is found.
     * </p>
     * 
     * @param sequence
     *            sequence that is searched for
     * @return node associated with the sequence
     * @see TrieNode#find(List)
     */
    public TrieNode<T> find(List<T> sequence) {
        if (sequence == null || sequence.isEmpty()) {
            return rootNode;
        }
        List<T> sequenceCopy = new LinkedList<T>(sequence);
        TrieNode<T> result = null;
        TrieNode<T> node = getChild(sequenceCopy.get(0));
        if (node != null) {
            sequenceCopy.remove(0);
            result = node.find(sequenceCopy);
        }
        return result;
    }

    /**
     * <p>
     * Returns a collection of all symbols that follow a given sequence in the trie. In case the
     * sequence is not found or no symbols follow the sequence the result will be empty.
     * </p>
     * 
     * @param sequence
     *            sequence whose followers are returned
     * @return symbols following the given sequence
     * @see TrieNode#getFollowingSymbols()
     */
    public Collection<T> getFollowingSymbols(List<T> sequence) {
        Collection<T> result = new LinkedList<T>();
        TrieNode<T> node = find(sequence);
        if (node != null) {
            result = node.getFollowingSymbols();
        }
        return result;
    }

    /**
     * <p>
     * Returns the longest suffix of the given context that is contained in the tree and whose
     * children are leaves.
     * </p>
     * 
     * @param context
     *            context whose suffix is searched for
     * @return longest suffix of the context
     */
    public List<T> getContextSuffix(List<T> context) {
        List<T> contextSuffix;
        if (context != null) {
            contextSuffix = new LinkedList<T>(context); // defensive copy
        }
        else {
            contextSuffix = new LinkedList<T>();
        }
        boolean suffixFound = false;

        while (!suffixFound) {
            if (contextSuffix.isEmpty()) {
                suffixFound = true; // suffix is the empty word
            }
            else {
                TrieNode<T> node = find(contextSuffix);
                if (node != null) {
                    if (!node.getFollowingSymbols().isEmpty()) {
                        suffixFound = true;
                    }
                }
                if (!suffixFound) {
                    contextSuffix.remove(0);
                }
            }
        }

        return contextSuffix;
    }

    /**
     * <p>
     * Helper class for graph visualization of a trie.
     * </p>
     * 
     * @author Steffen Herbold
     * @version 1.0
     */
    static public class Edge {}

    /**
     * <p>
     * Helper class for graph visualization of a trie.
     * </p>
     * 
     * @author Steffen Herbold
     * @version 1.0
     */
    static public class TrieVertex {

        /**
         * <p>
         * Id of the vertex.
         * </p>
         */
        private String id;

        /**
         * <p>
         * Contructor. Creates a new TrieVertex.
         * </p>
         * 
         * @param id
         *            id of the vertex
         */
        protected TrieVertex(String id) {
            this.id = id;
        }

        /**
         * <p>
         * Returns the id of the vertex.
         * </p>
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return id;
        }
    }

    /**
     * <p>
     * Returns a {@link Graph} representation of the trie.
     * </p>
     * 
     * @return {@link Graph} representation of the trie
     */
    protected Tree<TrieVertex, Edge> getGraph() {
        DelegateTree<TrieVertex, Edge> graph = new DelegateTree<TrieVertex, Edge>();
        rootNode.getGraph(null, graph);
        return graph;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.quest.usageprofiles.IDotCompatible#getDotRepresentation()
     */
    public String getDotRepresentation() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("digraph model {" + StringTools.ENDLINE);
        rootNode.appendDotRepresentation(stringBuilder);
        stringBuilder.append('}' + StringTools.ENDLINE);
        return stringBuilder.toString();
    }

    /**
     * <p>
     * Returns the string representation of the root node.
     * </p>
     * 
     * @see TrieNode#toString()
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return rootNode.toString();
    }

    /**
     * <p>
     * Returns the number of symbols contained in the trie.
     * </p>
     * 
     * @return number of symbols contained in the trie
     */
    public int getNumSymbols() {
        return knownSymbols.size();
    }

    /**
     * <p>
     * Returns the number of trie nodes that are ancestors of a leaf. This is the equivalent to the
     * number of states a first-order markov model would have.
     * <p>
     * 
     * @return number of trie nodes that are ancestors of leafs.
     */
    public int getNumLeafAncestors() {
        List<TrieNode<T>> ancestors = new LinkedList<TrieNode<T>>();
        rootNode.getLeafAncestors(ancestors);
        return ancestors.size();
    }

    /**
     * <p>
     * Returns the number of trie nodes that are leafs.
     * </p>
     * 
     * @return number of leafs in the trie
     */
    public int getNumLeafs() {
        return rootNode.getNumLeafs();
    }

    /**
     * <p>
     * Updates the list of known symbols by replacing it with all symbols that are found in the
     * child nodes of the root node. This should be the same as all symbols that are contained in
     * the trie.
     * </p>
     */
    public void updateKnownSymbols() {
        knownSymbols = new HashSet<T>();
        for (TrieNode<T> node : rootNode.getChildren()) {
            knownSymbols.add(node.getSymbol());
        }
    }

    /**
     * <p>
     * Two Tries are defined as equal, if their {@link #rootNode} are equal.
     * </p>
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @SuppressWarnings("rawtypes")
    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if (other instanceof Trie) {
            return rootNode.equals(((Trie) other).rootNode);
        }
        return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        int multiplier = 17;
        int hash = 42;
        if (rootNode != null) {
            hash = multiplier * hash + rootNode.hashCode();
        }
        return hash;
    }
}
