/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.apache.jmeter.gui.tree;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.swing.tree.DefaultTreeModel;

import org.apache.jmeter.config.gui.AbstractConfigGui;
import org.apache.jmeter.control.gui.TestPlanGui;
import org.apache.jmeter.control.gui.WorkBenchGui;
import org.apache.jmeter.exceptions.IllegalUserActionException;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.gui.JMeterGUIComponent;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.testelement.WorkBench;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.collections.ListedHashTree;

public class JMeterTreeModel extends DefaultTreeModel {

    private static final long serialVersionUID = 240L;

    public JMeterTreeModel(TestElement tp, TestElement wb) {
        super(new JMeterTreeNode(wb, null));
        initTree(tp,wb);
    }

    public JMeterTreeModel() {
        this(new TestPlanGui().createTestElement(),new WorkBenchGui().createTestElement());
//        super(new JMeterTreeNode(new WorkBenchGui().createTestElement(), null));
//        TestElement tp = new TestPlanGui().createTestElement();
//        initTree(tp);
    }

    /**
     * Hack to allow TreeModel to be used in non-GUI and headless mode.
     *
     * @deprecated - only for use by JMeter class!
     * @param o - dummy
     */
    @Deprecated
    public JMeterTreeModel(Object o) {
        this(new TestPlan(),new WorkBench());
//      super(new JMeterTreeNode(new WorkBench(), null));
//      TestElement tp = new TestPlan();
//      initTree(tp, new WorkBench());
    }

    /**
     * Returns a list of tree nodes that hold objects of the given class type.
     * If none are found, an empty list is returned.
     * @param type The type of nodes, which are to be collected
     * @return a list of tree nodes of the given <code>type</code>, or an empty list
     */
    public List<JMeterTreeNode> getNodesOfType(Class<?> type) {
        List<JMeterTreeNode> nodeList = new LinkedList<JMeterTreeNode>();
        traverseAndFind(type, (JMeterTreeNode) this.getRoot(), nodeList);
        return nodeList;
    }

    /**
     * Get the node for a given TestElement object.
     * @param userObject The object to be found in this tree
     * @return the node corresponding to the <code>userObject</code>
     */
    public JMeterTreeNode getNodeOf(TestElement userObject) {
        return traverseAndFind(userObject, (JMeterTreeNode) getRoot());
    }

    /**
     * Adds the sub tree at the given node. Returns a boolean indicating whether
     * the added sub tree was a full test plan.
     * 
     * @param subTree
     *            The {@link HashTree} which is to be inserted into
     *            <code>current</code>
     * @param current
     *            The node in which the <code>subTree</code> is to be inserted.
     *            Will be overridden, when an instance of {@link TestPlan} or
     *            {@link WorkBench} is found in the subtree.
     * @return newly created sub tree now found at <code>current</code>
     * @throws IllegalUserActionException
     *             when <code>current</code> is not an instance of
     *             {@link AbstractConfigGui} and no instance of {@link TestPlan}
     *             or {@link WorkBench} could be found in the
     *             <code>subTree</code>
     */
    public HashTree addSubTree(HashTree subTree, JMeterTreeNode current) throws IllegalUserActionException {
        Iterator<Object> iter = subTree.list().iterator();
        while (iter.hasNext()) {
            TestElement item = (TestElement) iter.next();
            if (item instanceof TestPlan) {
                TestPlan tp = (TestPlan) item;
                current = (JMeterTreeNode) ((JMeterTreeNode) getRoot()).getChildAt(0);
                final TestPlan userObject = (TestPlan) current.getUserObject();
                userObject.addTestElement(item);
                userObject.setName(item.getName());
                userObject.setFunctionalMode(tp.isFunctionalMode());
                userObject.setSerialized(tp.isSerialized());
                addSubTree(subTree.getTree(item), current);
            } else if (item instanceof WorkBench) {
                current = (JMeterTreeNode) ((JMeterTreeNode) getRoot()).getChildAt(1);
                final TestElement testElement = ((TestElement) current.getUserObject());
                testElement.addTestElement(item);
                testElement.setName(item.getName());
                addSubTree(subTree.getTree(item), current);
            } else {
                addSubTree(subTree.getTree(item), addComponent(item, current));
            }
        }
        return getCurrentSubTree(current);
    }

    /**
     * Add a {@link TestElement} to a {@link JMeterTreeNode}
     * @param component The {@link TestElement} to be used as data for the newly created note
     * @param node The {@link JMeterTreeNode} into which the newly created node is to be inserted
     * @return new {@link JMeterTreeNode} for the given <code>component</code>
     * @throws IllegalUserActionException
     *             when the user object for the <code>node</code> is not an instance
     *             of {@link AbstractConfigGui}
     */
    public JMeterTreeNode addComponent(TestElement component, JMeterTreeNode node) throws IllegalUserActionException {
        if (node.getUserObject() instanceof AbstractConfigGui) {
            throw new IllegalUserActionException("This node cannot hold sub-elements");
        }

        GuiPackage guiPackage = GuiPackage.getInstance();
        if (guiPackage != null) {
            // The node can be added in non GUI mode at startup
            guiPackage.updateCurrentNode();
            JMeterGUIComponent guicomp = guiPackage.getGui(component);
            guicomp.configure(component);
            guicomp.modifyTestElement(component);
            guiPackage.getCurrentGui(); // put the gui object back
                                        // to the way it was.
        }
        JMeterTreeNode newNode = new JMeterTreeNode(component, this);

        // This check the state of the TestElement and if returns false it
        // disable the loaded node
        try {
            newNode.setEnabled(component.isEnabled());
        } catch (Exception e) { // TODO - can this ever happen?
            newNode.setEnabled(true);
        }

        this.insertNodeInto(newNode, node, node.getChildCount());
        return newNode;
    }

    public void removeNodeFromParent(JMeterTreeNode node) {
        if (!(node.getUserObject() instanceof TestPlan) && !(node.getUserObject() instanceof WorkBench)) {
            super.removeNodeFromParent(node);
        }
    }

    private void traverseAndFind(Class<?> type, JMeterTreeNode node, List<JMeterTreeNode> nodeList) {
        if (type.isInstance(node.getUserObject())) {
            nodeList.add(node);
        }
        Enumeration<JMeterTreeNode> enumNode = node.children();
        while (enumNode.hasMoreElements()) {
            JMeterTreeNode child = enumNode.nextElement();
            traverseAndFind(type, child, nodeList);
        }
    }

    private JMeterTreeNode traverseAndFind(TestElement userObject, JMeterTreeNode node) {
        if (userObject == node.getUserObject()) {
            return node;
        }
        Enumeration<JMeterTreeNode> enumNode = node.children();
        while (enumNode.hasMoreElements()) {
            JMeterTreeNode child = enumNode.nextElement();
            JMeterTreeNode result = traverseAndFind(userObject, child);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    /**
     * Get the current sub tree for a {@link JMeterTreeNode}
     * @param node The {@link JMeterTreeNode} from which the sub tree is to be taken 
     * @return newly copied sub tree
     */
    public HashTree getCurrentSubTree(JMeterTreeNode node) {
        ListedHashTree hashTree = new ListedHashTree(node);
        Enumeration<JMeterTreeNode> enumNode = node.children();
        while (enumNode.hasMoreElements()) {
            JMeterTreeNode child = enumNode.nextElement();
            hashTree.add(node, getCurrentSubTree(child));
        }
        return hashTree;
    }

    /**
     * Get the {@link TestPlan} from the root of this tree
     * @return The {@link TestPlan} found at the root of this tree
     */
    public HashTree getTestPlan() {
        return getCurrentSubTree((JMeterTreeNode) ((JMeterTreeNode) this.getRoot()).getChildAt(0));
    }

    /**
     * Get the {@link WorkBench} from the root of this tree
     * @return The {@link WorkBench} found at the root of this tree
     */
    public HashTree getWorkBench() {
        return getCurrentSubTree((JMeterTreeNode) ((JMeterTreeNode) this.getRoot()).getChildAt(1));
    }

    /**
     * Clear the test plan, and use default node for test plan and workbench.
     *
     * N.B. Should only be called by {@link GuiPackage#clearTestPlan()}
     */
    public void clearTestPlan() {
        TestElement tp = new TestPlanGui().createTestElement();
        clearTestPlan(tp);
    }

    /**
     * Clear the test plan, and use specified node for test plan and default node for workbench
     *
     * N.B. Should only be called by {@link GuiPackage#clearTestPlan(TestElement)}
     *
     * @param testPlan the node to use as the testplan top node
     */
    public void clearTestPlan(TestElement testPlan) {
        // Remove the workbench and testplan nodes
        int children = getChildCount(getRoot());
        while (children > 0) {
            JMeterTreeNode child = (JMeterTreeNode)getChild(getRoot(), 0);
            super.removeNodeFromParent(child);
            children = getChildCount(getRoot());
        }
        // Init the tree
        initTree(testPlan,new WorkBenchGui().createTestElement()); // Assumes this is only called from GUI mode
    }

    /**
     * Initialize the model with nodes for testplan and workbench.
     *
     * @param tp the element to use as testplan
     * @param wb the element to use as workbench
     */
    private void initTree(TestElement tp, TestElement wb) {
        // Insert the test plan node
        insertNodeInto(new JMeterTreeNode(tp, this), (JMeterTreeNode) getRoot(), 0);
        // Insert the workbench node
        insertNodeInto(new JMeterTreeNode(wb, this), (JMeterTreeNode) getRoot(), 1);
        // Let others know that the tree content has changed.
        // This should not be necessary, but without it, nodes are not shown when the user
        // uses the Close menu item
        nodeStructureChanged((JMeterTreeNode)getRoot());
    }
}
