
package de.ugoe.cs.quest.eventcore.guimodel;

import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;

import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * A GUI model is a tree of {@link IGUIElements} and represents a complete GUI of a software. It is
 * platform independent. It may have several root nodes, as some GUIs are made up of several Frames
 * being independent from each other. The GUI model is filled using the
 * {@link #integratePath(List, IGUIElementFactory)} method.
 * </p>
 * 
 * @version 1.0
 * @author Patrick Harms, Steffen Herbold
 */
public class GUIModel {

    /**
     * <p>
     * The root node of the tree not provided externally.
     * </p>
     */
    private TreeNode root = new TreeNode();

    /**
     * <p>
     * A list with all nodes currently known
     * </p>
     */
    private List<TreeNode> allNodes = new ArrayList<TreeNode>();

    /**
     * <p>
     * Integrates a path of GUI elements into the GUI model. The GUI model itself is a tree and
     * therefore a set of different paths through the tree that start with a root node and end with
     * a leaf node. Such a path can be added to the tree. The method checks, if any of the GUI
     * elements denoted by the path already exists. If so, it reuses it. It may therefore also
     * return an existing GUI element being the leaf node of the provided path. If a GUI element of
     * the path does not exist yet, it creates a new one using the provided GUI element factory.
     * </p>
     * <p>
     * If a GUI element specification describes an existing GUI element or not is determined through
     * comparing the GUI element specifications of the existing GUI elements with the ones provided
     * in the path. The comparison is done using the
     * {@link IGUIElementSpec#getSimilarity(IGUIElementSpec)} method. The comparison is only done on
     * the correct levels. I.e. the currently known root elements of the tree are only compared to
     * the first element in the path. If the correct one is found or created, its children are
     * compared only to the second specification in the path, and so on.
     * </p>
     * <p>
     * The returned GUI elements are singletons. I.e. it is tried to return always the identical
     * object for the same denoted element. However, while creating the GUI model, the similarity of
     * GUI elements may change. Therefore, the method might determine, that two formerly different
     * nodes are now similar. (This may happen, e.g. if GUI elements do not have initial names which
     * are set afterwards. Therefore, first they are handled differently and later they can be
     * identified as being the same.) In such a case, there are already several GUI element objects
     * instantiated for the same GUI element. The singleton paradigm gets broken. Therefore, such
     * GUI element objects are registered with each other, so that their equal method can determine
     * equality again correctly, although the objects are no singletons anymore.
     * </p>
     * 
     * @param guiElementPath
     *            the path to integrate into the model
     * @param guiElementFactory
     *            the GUI element factory to be used for instantiating GUI element objects
     * 
     * @return The GUI element object representing the GUI element denoted by the provided path
     * 
     * @throws GUIModelException
     *             thrown in cases such as the GUI element object could not be instantiated
     * @throws IllegalArgumentException
     *             if the provided path is invalid.
     */
    public IGUIElement integratePath(List<? extends IGUIElementSpec> guiElementPath,
                                     IGUIElementFactory guiElementFactory)
        throws GUIModelException, IllegalArgumentException
    {
        if ((guiElementPath == null) || (guiElementPath.size() <= 0)) {
            throw new IllegalArgumentException("GUI element path must contain at least one element");
        }

        List<IGUIElementSpec> remainingPath = new LinkedList<IGUIElementSpec>();

        for (IGUIElementSpec spec : guiElementPath) {
            remainingPath.add(spec);
        }

        return integratePath(root, remainingPath, guiElementFactory);
    }

    /**
     * <p>
     * Returns all children of the provided GUI element or null, if it does not have any or the node
     * is unknown.
     * </p>
     * 
     * @param guiElement
     *            the GUI element of which the children shall be returned
     * 
     * @return As described
     */
    public List<IGUIElement> getChildren(IGUIElement guiElement) {
        List<IGUIElement> result = null;
        for (TreeNode node : allNodes) {
            if (node.guiElement.equals(guiElement)) {
                if (result == null) {
                    result = new ArrayList<IGUIElement>();

                    if (node.children != null) {
                        for (TreeNode child : node.children) {
                            result.add(child.guiElement);
                        }
                    }
                }
                else {
                    Console
                        .traceln(Level.SEVERE,
                                 "Multiple nodes in the internal GUI model match the same GUI element. "
                                     + "This should not be the case and the GUI model is probably invalid.");
                }
            }
        }

        return result;
    }

    /**
     * <p>
     * Returns the parent GUI element of the provided GUI element or null, if it does not have a
     * parent (i.e. if it is a root node) or if the node is unknown.
     * </p>
     * 
     * @param guiElement
     *            the GUI element of which the parent shall be returned
     * 
     * @return As described
     */
    public IGUIElement getParent(IGUIElement guiElement) {
        IGUIElement parent = null;

        for (TreeNode node : allNodes) {
            for (TreeNode child : node.children) {
                if (child.guiElement.equals(guiElement)) {
                    if (parent != null) {
                        parent = node.guiElement;
                    }
                    else {
                        Console
                            .traceln(Level.SEVERE,
                                     "Multiple nodes in the internal GUI model match the same GUI element. "
                                         + "This should not be the case and the GUI model is probably invalid.");
                    }
                }
            }
        }

        return parent;
    }

    /**
     * <p>
     * Returns all root GUI elements of the model or an empty list, if the model is empty
     * </p>
     * 
     * @return As described
     */
    public List<IGUIElement> getRootElements() {
        List<IGUIElement> roots = new ArrayList<IGUIElement>();

        if (root.children != null) {
            for (TreeNode rootChild : root.children) {
                roots.add(rootChild.guiElement);
            }
        }

        return roots;
    }

    /**
     * <p>
     * dumps the GUI model to the provided stream. Each node is represented through its toString()
     * method. If a node has children, those are dumped indented and surrounded by braces.
     * </p>
     * 
     * @param out
     *            The stream to dump the textual representation of the model to
     * @param encoding
     *            The encoding to be used while dumping
     */
    public void dump(OutputStream out, String encoding) {
        PrintStream stream;

        if (out instanceof PrintStream) {
            stream = (PrintStream) out;
        }
        else {
            String enc = encoding == null ? "UTF-8" : encoding;
            try {
                stream = new PrintStream(out, true, enc);
            }
            catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("encodind " + enc + " not supported");
            }
        }

        for (IGUIElement root : getRootElements()) {
            dumpGUIElement(stream, root, "");
        }
    }

    /**
     * <p>
     * By calling this method, the GUIModel is traversed and similar nodes are merged.
     * </p>
     * 
     */
    public void condenseModel() {
        mergeSubTree(root);
    }
    
    /**
     * <p>
     * Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
     * </p>
     * 
     * @param guiElement1
     *            the first merge GUI element
     * @param guiElement2
     *            the second merge GUI element
     * @throws IllegalArgumentException
     *             thrown if the two GUI elements do not have the same parent
     */
    public void mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2)
        throws IllegalArgumentException
    {
        // check if both nodes have the same parent
        IGUIElement parentElement = guiElement1.getParent();
        if (parentElement != null && !parentElement.equals(guiElement2.getParent())) {
            throw new IllegalArgumentException("can only merge nodes with the same parent");
        }

        // get the TreeNode of the parent of the GUI elements
        TreeNode parent = findNode(parentElement);

        // get the TreeNodes for both GUI elements
        TreeNode node1 = findNode(guiElement1);
        TreeNode node2 = findNode(guiElement2);

        if (node1 == null || node2 == null) {
            throw new IllegalArgumentException(
                                               "Error while merging nodes: one element is not part of the GUI model!");
        }

        TreeNode replacement = mergeTreeNodes(node1, node2);

        if (parent != null) {
            // remove node1 and node2 from the parent's children and add the replacement instead
            // assumes that there are no duplicates of node1 and node2
            if (parent.children != null) {
                parent.children.set(parent.children.indexOf(node1), replacement);
                parent.children.remove(node2);
            }
        }

    }

    /**
     * <p>
     * internally integrates a path as the children of the provided parent node. This method is
     * recursive and calls itself, for the child of the parent node, that matches the first element
     * in the remaining path.
     * </p>
     * 
     * @param parentNode
     *            the parent node to add children for
     * @param guiElementPath
     *            the path of children to be created starting with the parent node
     * @param guiElementFactory
     *            the GUI element factory to be used for instantiating GUI element objects
     * 
     * @return The GUI element object representing the GUI element denoted by the provided path
     * 
     * @throws GUIModelException
     *             thrown in cases such as the GUI element object could not be instantiated
     */
    private IGUIElement integratePath(TreeNode parentNode,
                                      List<? extends IGUIElementSpec> remainingPath,
                                      IGUIElementFactory guiElementFactory)
        throws GUIModelException
    {
        IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0);

        TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor);
        if (child == null) {
            IGUIElement newElement =
                guiElementFactory.instantiateGUIElement(specToIntegrateElementFor,
                                                        parentNode.guiElement);

            child = parentNode.addChild(newElement);
        }

        if (remainingPath.size() > 0) {
            return integratePath(child, remainingPath, guiElementFactory);
        }
        else {
            return child.guiElement;
        }
    }

    /**
     * <p>
     * Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the
     * specification of the {@link TreeNode#guiElement} of the child. If a match is found, the child
     * is returned.
     * </p>
     * 
     * @param parentNode
     *            parent node whose children are searched
     * @param specToMatch
     *            specification that is searched for
     * @return matching child node or null if no child matches
     */
    private TreeNode findEqualChild(TreeNode parentNode, IGUIElementSpec specToMatch) {
        if (parentNode.children != null) {
            for (TreeNode child : parentNode.children) {
                if (specToMatch.equals(child.guiElement.getSpecification())) {
                    return child;
                }
            }
        }
        return null;
    }

    /**
     * <p>
     * Merges all similar nodes in the sub-tree of the GUI model defined by the subTreeRoot.
     * </p>
     * <p>
     * The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for
     * the grand children of the subTreeRoot, before we merge subTreeRoot.
     * </p>
     * <p>
     * The merging strategy is top-down. This means, that every time we merge two child nodes, we
     * call mergeSubTree recursively for all children of the merged nodes in order to check if we
     * can merge the children, too.
     * </p>
     * 
     * @param subTreeRoot
     *            root node of the sub-tree that is merged
     */
    private void mergeSubTree(TreeNode subTreeRoot) {
        if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) {
            return;
        }

        // lets first merge the grand children of the parentNode
        for (TreeNode child : subTreeRoot.children) {
            mergeSubTree(child);
        }

        boolean performedMerge;

        do {
            performedMerge = false;
            for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) {
                IGUIElementSpec elemSpec1 =
                    subTreeRoot.children.get(i).guiElement.getSpecification();
                for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) {
                    IGUIElementSpec elemSpec2 =
                        subTreeRoot.children.get(j).guiElement.getSpecification();
                    if (elemSpec1.getSimilarity(elemSpec2)) {
                        TreeNode replacement =
                            mergeTreeNodes(subTreeRoot.children.get(i), subTreeRoot.children.get(j));

                        subTreeRoot.children.set(i, replacement);
                        subTreeRoot.children.remove(j);
                        performedMerge = true;
                        i--;
                        break;
                    }
                }
            }
        }
        while (performedMerge);
    }

    /**
     * <p>
     * merges two nodes with each other. Merging means registering the GUI element objects with each
     * other for equality checks. Further it add all children of both nodes to a new replacing node.
     * Afterwards, all similar nodes of the replacement node are merged as well.
     * </p>
     * 
     * @param treeNode1
     *            the first of the two nodes to be merged
     * @param treeNode2
     *            the second of the two nodes to be merged
     * @return a tree node being the merge of the two provided nodes.
     */
    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
        // the following two lines are needed to preserve the references to the existing GUI
        // elements. If two elements are the same, one should be deleted to make the elements
        // singletons again. However, there may exist references to both objects. To preserve
        // these, we simply register the equal GUI elements with each other so that an equals
        // check can return true.
        treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement);
        treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement);

        // and now a replacement node that is the merge of treeNode1 and treeNode2 is created
        TreeNode replacement = new TreeNode();
        replacement.guiElement = treeNode1.guiElement;
        if (treeNode1.children != null) {
            for (TreeNode child : treeNode1.children) {
                replacement.addChildNode(child);
            }
        }
        if (treeNode2.children != null) {
            for (TreeNode child : treeNode2.children) {
                replacement.addChildNode(child);
            }
        }

        mergeSubTree(replacement);

        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());

        // finally, update the known nodes list
        // if you don't do this getChildren will return wrong things and very bad things happen!
        allNodes.remove(treeNode1);
        allNodes.remove(treeNode2);
        allNodes.add(replacement);

        return replacement;
    }

    /**
     * <p>
     * dumps a GUI element to the stream. A dump contains the toString() representation of the GUI
     * element as well as a indented list of its children surrounded by braces.
     * </p>
     * 
     * @param out
     *            {@link PrintStream} where the guiElement is dumped to
     * @param guiElement
     *            the guiElement whos string represenation is dumped
     * @param indent
     *            indent string of the dumping
     */
    private void dumpGUIElement(PrintStream out, IGUIElement guiElement, String indent) {
        out.print(indent);
        out.print(guiElement);

        List<IGUIElement> children = getChildren(guiElement);

        if ((children != null) && (children.size() > 0)) {
            out.println(" {");

            for (IGUIElement child : children) {
                dumpGUIElement(out, child, indent + "  ");
            }

            out.print(indent);
            out.print("}");
        }

        out.println();
    }
    
    /**
     * <p>
     * Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is
     * found.
     * </p>
     * 
     * @param element
     *            the GUI element
     * @return associated TreeNode; null if no such node exists
     */
    private TreeNode findNode(IGUIElement element) {
        if (element == null) {
            return null;
        }

        TreeNode result = null;
        for (TreeNode node : allNodes) {
            if (node.guiElement.equals(element)) {
                if (result == null) {
                    result = node;
                }
                else {
                    Console
                        .traceln(Level.SEVERE,
                                 "Multiple nodes in the internal GUI model match the same GUI element. "
                                     + "This should not be the case and the GUI model is probably invalid.");
                }
            }
        }
        return result;
    }

    /**
     * <p>
     * Used internally for building up the tree of GUI elements.
     * </p>
     * 
     * @version 1.0
     * @author Patrick Harms, Steffen Herbold
     */
    private class TreeNode {

        /**
         * <p>
         * GUI element associated with the TreeNode.
         * </p>
         */
        private IGUIElement guiElement;

        /**
         * <p>
         * Children of the TreeNode.
         * </p>
         */
        private List<TreeNode> children;

        /**
         * <p>
         * Adds a child to the current node while keeping all lists of nodes up to date
         * </p>
         * 
         * @param guiElement
         *            GUI element that will be associated with the new child
         * @return the added child
         */
        private TreeNode addChild(IGUIElement guiElement) {
            if (children == null) {
                children = new ArrayList<TreeNode>();
            }

            TreeNode child = new TreeNode();
            child.guiElement = guiElement;
            children.add(child);

            allNodes.add(child);

            return child;
        }

        /**
         * 
         * <p>
         * Adds a TreeNode as child to the current node. This way, the whole sub-tree is added.
         * </p>
         * 
         * @param node
         *            child node that is added
         * @return node that has been added
         */
        private TreeNode addChildNode(TreeNode node) {
            if (children == null) {
                children = new ArrayList<TreeNode>();
            }
            children.add(node);
            return node;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return guiElement.toString();
        }
    }
}
