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

import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * <p>
 * The goal of a GUI model is to correctly l
 * </p>
 * 
 * @version $Revision: $ $Date: 14.08.2012$
 * @author 2012, last modified by $Author: pharms$
 */
public class GUIModel {
    
    /**
     * 
     */
    private TreeNode root = new TreeNode();
    
    /**
     * 
     */
    private List<TreeNode> allNodes = new ArrayList<TreeNode>();

    /**
     * TODO: comment
     *
     * @param currentGUIElementPath
     * @return
     * @throws GUIModelException 
     */
    public IGUIElement integratePath(List<? extends IGUIElementSpec> guiElementPath,
                                     IGUIElementFactory              guiElementFactory)
        throws GUIModelException
    {
        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);
    }

    /**
     * TODO: comment
     *
     * @param root
     * @return
     */
    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>
     * TODO: comment
     * </p>
     *
     * @param guiElement
     * @return
     */
    public IGUIElement getParent(IGUIElement guiElement) {
        for (TreeNode node : allNodes) {
            for (TreeNode child : node.children) {
                if (child.guiElement.equals(guiElement)) {
                    return node.guiElement;
                }
            }
        }
        
        return null;
    }

    /**
     * TODO: comment
     *
     * @return
     */
    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;
    }

    /**
     * TODO: comment
     *
     * @return
     */
    public void dump(OutputStream out) {
        PrintStream stream;
        
        if (out instanceof PrintStream) {
            stream = (PrintStream) out;
        }
        else {
            stream = new PrintStream(out);
        }
        
        for (IGUIElement root : getRootElements()) {
            dumpGUIElement(stream, root, "");
        }
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param root2
     * @param guiElementPath
     * @param guiElementFactory
     * @return
     * @throws GUIModelException 
     */
    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, specToIntegrateElementFor, remainingPath);

                if (similarNodes.size() > 1) {
                    System.out.println("TODO: implement handling to many similar children: " +
                                       specToIntegrateElementFor);
                    for (TreeNode similarNode : similarNodes) {
                        System.out.println("    " + similarNode.guiElement);
                    }
                    System.out.println();
                }
            }
        }
        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>
     * TODO: comment
     * </p>
     *
     * @param parentNode
     * @param specToIntegrateElementFor
     * @return
     */
    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>
     * TODO: comment
     * </p>
     *
     * @param similarChildren
     * @param remove
     * @return
     */
    private List<TreeNode> considerSubChildren(List<TreeNode>                  similarNodes,
                                               IGUIElementSpec                 specToMatch,
                                               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>
     * TODO: comment
     * </p>
     *
     * @param parentNode
     */
    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>
     * TODO: comment
     * </p>
     *
     * @param treeNode
     * @param treeNode2
     * @return
     */
    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;
    }

    /**
     * TODO: comment
     * 
     * @param root
     * @param root2
     */
    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>
     * TODO comment
     * </p>
     * 
     * @version $Revision: $ $Date: 17.08.2012$
     * @author 2012, last modified by $Author: pharms$
     */
    private class TreeNode
    {
        /** */
        private IGUIElement guiElement;
        
        /** */
        private List<TreeNode> children;
        
        /** */
        //private TreeNode parent;
        
        /**
         * <p>
         * TODO: comment
         * </p>
         *
         * @param guiElement
         * @return
         */
        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();
        }
    }
}
