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 $Revision: $ $Date: 14.08.2012$
 * @author 2012, last modified by $Author: pharms$
 */
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) {
        for (TreeNode node : allNodes) {
            if (node.guiElement.equals(guiElement)) {
                List<IGUIElement> result = new ArrayList<IGUIElement>();
                
                if (node.children != null) {
                    for (TreeNode child : node.children) {
                      result.add(child.guiElement);
                    }
                }
                
                return result;
            }
        }
        
        return null;
    }

    /**
     * <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) {
        for (TreeNode node : allNodes) {
            for (TreeNode child : node.children) {
                if (child.guiElement.equals(guiElement)) {
                    return node.guiElement;
                }
            }
        }
        
        return null;
    }

    /**
     * <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>
     * 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);

        List<TreeNode> similarNodes =
            determineSimilarChildNodes(parentNode, specToIntegrateElementFor);
        
        if (similarNodes.size() > 1) {
            // this may happen, if the GUI elements changed over time (e.g. their name is usually
            // set later in the program execution) and if they now match because of the changes.
            // So perform a merge of all similar children of the current parent node to reduce the
            // model and then try the determination of matching children again.
            mergeSimilarChildren(parentNode);
            similarNodes = determineSimilarChildNodes(parentNode, specToIntegrateElementFor);
            
            if (similarNodes.size() > 1) {
                // this can happen, because the similarity check is not transitive. The new GUI
                // element may be similar to two or more existing ones, but the existing ones
                // may not be similar to each other. As an example, the new GUI element may
                // not yet provide sufficient information (such as all names it will have during
                // the execution of the program). Therefore the similarity check with GUI elements
                // that already contain more information may return true. But the similarity check
                // of two GUI elements that already carry a lot of information may return false,
                // although both are similar to the new GUI element. Therefore, we try a selection
                // based on the children of the existing and new GUI elements. The one for which
                // the existing children match best is selected to be the right one.
                similarNodes = considerSubChildren(similarNodes, remainingPath);

                if (similarNodes.size() > 1) {
                    Console.traceln(Level.WARNING, "TODO: implement handling to many similar" +
                                    "children: " + specToIntegrateElementFor);
                    for (TreeNode similarNode : similarNodes) {
                        Console.traceln(Level.WARNING, "    " + similarNode.guiElement);
                    }
                    Console.traceln(Level.WARNING, "");
                }
            }
        }
        else if (similarNodes.size() == 1) {
            similarNodes.get(0).guiElement.updateSpecification(specToIntegrateElementFor);
        }
        else if (similarNodes.size() == 0) {
            // if we get here, the corresponding path does not exist yet. So create it
            IGUIElement newElement = guiElementFactory.instantiateGUIElement
                (specToIntegrateElementFor, parentNode.guiElement);
            
            similarNodes.add(parentNode.addChild(newElement));
        }
        
        if (remainingPath.size() > 0) {
            return integratePath(similarNodes.get(0), remainingPath, guiElementFactory);
        }
        else {
            return similarNodes.get(0).guiElement;
        }
    }

    /**
     * <p>
     * Determines all children of the provided node, which are similar to the provided
     * specification.
     * </p>
     */
    private List<TreeNode> determineSimilarChildNodes(TreeNode        parentNode,
                                                      IGUIElementSpec specToMatch)
    {
        List<TreeNode> similarChildren = new ArrayList<TreeNode>();
        
        if (parentNode.children != null) {
            for (TreeNode child : parentNode.children) {
                if (specToMatch.getSimilarity(child.guiElement.getSpecification())) {
                    similarChildren.add(child);
                }
            }
        }
        
        return similarChildren;
    }

    /**
     * <p>
     * This method is called in the case, that several child nodes of a parent node are similar
     * to a node to be integrated into the model. This method tries to determine the similar child
     * nodes of which the sub children match best to the further path to be integrated. This method
     * does nothing, if the similar children do not have children yet of if the remaining path
     * does not denote further children. 
     * </p>
     *
     * @return a hopefully reduced list of similar nodes based on their children.
     */
    private List<TreeNode> considerSubChildren(List<TreeNode>                  similarNodes,
                                               List<? extends IGUIElementSpec> remainingPath)
    {
        List<TreeNode> reducedList = new ArrayList<TreeNode>();
        
        // check, if there are any children to consider and remove any similar node, that has
        // further children
        if (remainingPath.size() <= 0) {
            for (TreeNode similarNode : similarNodes) {
                if ((similarNode.children == null) || (similarNode.children.size() == 0)) {
                    reducedList.add(similarNode);
                }
            }
        }
        else {
            // if there are further children to consider, then check if there is already a child
            // node that has an appropriate child
            IGUIElementSpec subChildSpec = remainingPath.get(0);
            for (TreeNode similarNode : similarNodes) {
                if (similarNode.children != null) {
                    for (TreeNode subchild : similarNode.children) {
                        if (subchild.guiElement.getSpecification().getSimilarity(subChildSpec)) {
                            reducedList.add(similarNode);
                            break;
                        }
                    }
                }
            }
        }
        
        return reducedList;
    }

    /**
     * <p>
     * merges similar children of a parent node. The method compares all children of the parent
     * node with each other. If two of them are similar, it merges them, registers them with each
     * other for equality checks, and removes one of them from the list of children. 
     * </p>
     */
    private void mergeSimilarChildren(TreeNode parentNode) {
        boolean performedMerge;
        
        do {
            performedMerge = false;
            if (parentNode.children != null) {
                for (int i = 0; (!performedMerge) && (i < parentNode.children.size()); i++) {
                    for (int j = i + 1; j < parentNode.children.size(); j++) {
                        IGUIElement elem1 = parentNode.children.get(i).guiElement;
                        IGUIElement elem2 = parentNode.children.get(j).guiElement;
                        if (elem1.getSpecification().getSimilarity(elem2.getSpecification())) {
                            TreeNode replacement = mergeTreeNodes(parentNode.children.get(i),
                                                                  parentNode.children.get(j));
                            
                            parentNode.children.set(i, replacement);
                            parentNode.children.remove(j);
                            performedMerge = true;
                            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>
     *
     * @return a tree node being the merge of the two provided nodes.
     */
    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
        TreeNode replacement = new TreeNode();
        
        replacement.guiElement = treeNode1.guiElement;
        
        // 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.
        replacement.guiElement.addEqualGUIElement(treeNode2.guiElement);
        treeNode2.guiElement.addEqualGUIElement(replacement.guiElement);
        
        if (treeNode1.children != null) {
            for (TreeNode child : treeNode1.children) {
                replacement.addChild(child.guiElement);
            }
        }
        
        if (treeNode2.children != null) {
            for (TreeNode child : treeNode2.children) {
                replacement.addChild(child.guiElement);
            }
        }
        
        mergeSimilarChildren(replacement);
        
        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());
        
        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>
     */
    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>
     * used internally for building up the tree of GUI elements.
     * </p>
     */
    private class TreeNode
    {
        /** */
        private IGUIElement guiElement;
        
        /** */
        private List<TreeNode> children;
        
        /** */
        //private TreeNode parent;
        
        /**
         * <p>
         * adds a child to the current node while keeping all lists of nodes up to date
         * </p>
         */
        private TreeNode addChild(IGUIElement guiElement)
        {
            if (children == null)
            {
                children = new ArrayList<TreeNode>();
            }
            
            TreeNode child = new TreeNode();
            child.guiElement = guiElement;
            //child.parent = this;
            children.add(child);
            
            allNodes.add(child);
            
            return child;
        }

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