package eu.gity.jmeter.ddos.common;

import org.apache.jmeter.exceptions.IllegalUserActionException;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.gui.action.*;
import org.apache.jmeter.gui.tree.JMeterTreeListener;
import org.apache.jmeter.gui.tree.JMeterTreeModel;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.tree.TreePath;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

import static eu.gity.jmeter.ddos.properties.strings.DdosPropertiesStrings.DDOS_ERROR;
import static eu.gity.jmeter.ddos.properties.strings.DdosPropertiesStrings.DDOS_THREAD_GROUP_ERROR_INSERTION;

/**
 * This is a singleton class used to guard copy/cut-paste and drag&drop user operations to disable insertion of
 * DDoS sampler anywhere else to DDoS Thread Group and any non-DDoS component to the DDoS Thread Group
 * <p>
 * It's made final to improve performance because subclassing is not welcomed anyway as someone might try to create another instance (which
 * is defeated second time by a private ctor).
 *
 * @author Gity a.s.
 */
public final class DDoSTreeGuard {
    private static final DDoSTreeGuard INSTANCE = new DDoSTreeGuard();

    private static final Logger log = LoggerFactory.getLogger(DDoSTreeGuard.class);
    private final ArrayList<TeAndParent> cutNodesParents = new ArrayList<TeAndParent>();
    private final ArrayList<TeAndParent> editedNodesParents = new ArrayList<TeAndParent>();
    private boolean listenersRegistered = false;

    /**
     * Exists to defeat external instantiation (double-guarded together with declaring the class final)
     */
    private DDoSTreeGuard() {
    }

    /**
     * @return the singleton instance of this class
     */
    public static DDoSTreeGuard getInstance() {
        return INSTANCE;
    }

    /**
     * Register all neccessary JMeter's ActionRouter listeners to guard the insertion events if not done yet.
     * The purpose of the listeners is to prevent the user from following actions:
     * - adding DDoS components (except HTTP flood) elsewhere but to Trafgen Thread Group
     * - adding any non DDoS sampler component to the Trafgen Thread Group
     */
    public void registerListeners() {
        if (!listenersRegistered) {
            registerDragAndDropListeners();
            registerClipboardListeners();
            listenersRegistered = true;
        }
    }

    /**
     * Reload tree after moving nodes programmatically and shows user an error message
     */
    private void reloadTreeAndShowError() {
        TreePath[] pathsToUnresolved = null;
        if (!editedNodesParents.isEmpty()) { //Preserves selection for a case other Gity components needs to
            //do their action check on rest of the unhandled components from the current selection
            JMeterTreeNode[] selectedNodes = GuiPackage.getInstance().getTreeListener().getSelectedNodes();
            pathsToUnresolved = new TreePath[selectedNodes.length];
            for (int i = 0; i < selectedNodes.length; i++) {
                pathsToUnresolved[i] = new TreePath(selectedNodes[i].getPath());
            }
        }

        JTree jmeterTree = GuiPackage.getInstance().getMainFrame().getTree();
        ((JMeterTreeModel) jmeterTree.getModel()).reload();
        for (int i = 0; i < jmeterTree.getRowCount(); i++) {
            jmeterTree.expandRow(i);
        }

        if (pathsToUnresolved != null) {
            GuiPackage.getInstance().getTreeListener().getJTree().setSelectionPaths(pathsToUnresolved);
        }
        JOptionPane.showMessageDialog(null, JMeterUtils.getLocaleString(DDOS_THREAD_GROUP_ERROR_INSERTION),
                JMeterUtils.getLocaleString(DDOS_ERROR), JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Creates a new node from the provided node object and inserts it to the specified parent.
     * This method ensures that also treeModel is set to the new node opposed to insert() method of JMeterTreeNode
     *
     * @param parent node which will be direct ancestor of the new node
     * @param node   original node to get user object from
     */
    private void addNode(JMeterTreeNode parent, JMeterTreeNode node) {
        try {
            // Add this node
            JMeterTreeNode newNode = GuiPackage.getInstance().getTreeModel().addComponent(node.getTestElement(), parent);
            // Add all the child nodes of the node we are adding
            for (int i = 0; i < node.getChildCount(); i++) {
                addNode(newNode, (JMeterTreeNode) node.getChildAt(i));
            }
        } catch (IllegalUserActionException iuae) {
            log.error("Illegal user action while adding a tree node.", iuae);
        }
    }

    /**
     * Registration of listeners to manage drag&drop user action.
     * <p>
     * Pre-action listener of "Edit" event saves reference to parents of the edited nodes which may be needed
     * by following post-action of "Edit" event listener to revert changes if they are not approved
     */
    private void registerDragAndDropListeners() {
        ActionRouter.getInstance().addPreActionListener(EditCommand.class, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (e.getSource() instanceof JMeterTreeListener) {
                    /**
                     * Node must not be added again if it's already in the list otherwise it would be overwritten by
                     * a new parent during following invocation of "Edit" event just before component is moved.
                     *
                     * Nodes which are not selected must be removed from the list one by one.
                     *
                     * Comparison of nodes must be done by content of their user object because they are cloned during drag&drop
                     */

                    //Creates lists of nodes to be added/removed which contains only different TEs
                    JMeterTreeNode[] toBeAdded = ((JMeterTreeListener) e.getSource()).getSelectedNodes();
                    ArrayList<TeAndParent> toBeRemoved = new ArrayList<TeAndParent>(editedNodesParents);
                    for (int i = 0; i < toBeAdded.length; i++) {
                        for (TeAndParent rem : toBeRemoved) {
                            if (rem.te.equals(toBeAdded[i].getTestElement())) {
                                toBeAdded[i] = null;
                                toBeRemoved.remove(rem);
                                break;
                            }
                        }
                    }
                    for (int i = 0; i < toBeAdded.length; i++) {
                        if (toBeAdded[i] != null && toBeAdded[i].getParent() != null) {
                            editedNodesParents.add(new TeAndParent(toBeAdded[i].getTestElement(), (JMeterTreeNode) toBeAdded[i].getParent()));
                        }
                    }
                    editedNodesParents.removeAll(toBeRemoved);
                }
            }
        });

        ActionRouter.getInstance().addPostActionListener(EditCommand.class, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (e.getSource() instanceof JMeterTreeListener) {
                    JMeterTreeNode currentNode = ((JMeterTreeListener) e.getSource()).getCurrentNode();
                    JMeterTreeModel jmeterTreeModel = GuiPackage.getInstance().getTreeModel();
                    JMeterTreeNode[] selectedNodes = ((JMeterTreeListener) e.getSource()).getSelectedNodes();
                    boolean correction = false;

                    List<JMeterTreeNode> ddosThreadGroupNodes = jmeterTreeModel.getNodesOfType(DDoSThreadGroup.class);
                    if (ddosThreadGroupNodes.isEmpty()) {
                        return;
                    }
                    //Excludes TestPlan and WorkBench as the event is also triggered for them even if they don't have these nodes as children
                    if (currentNode.getParent() == jmeterTreeModel.getRoot()) {
                        return;
                    }
                    for (JMeterTreeNode selectedNode : selectedNodes) {

                        TestElement selectedNodeTE = selectedNode.getTestElement();
                        String guiClass = selectedNodeTE.getPropertyAsString(TestElement.GUI_CLASS);
                        if (!guiClass.contains("NetEmulGui") && !guiClass.contains("NetIntGui") && !guiClass.contains("NetLineGui")
                                && !guiClass.contains("NetAnalyzerGui")) { //Guarded by NetEmulInit/NetAnalyzerGui classes

                            JMeterTreeNode parent = (JMeterTreeNode) selectedNode.getParent();
                            if (parent != null) {
                                if (selectedNodeTE instanceof Trafgen) {
                                    //Next section prevents adding DDoS components elsewhere outside of Trafgen Thread Group
                                    if (!(parent.getTestElement() instanceof DDoSThreadGroup)) {
                                        //Finds original parent of the selectedNode
                                        for (TeAndParent pair : editedNodesParents) {
                                            if (pair.te.equals(selectedNodeTE)) {
                                                addNode(pair.parentNode, selectedNode);
                                                selectedNode.removeFromParent();
                                                editedNodesParents.remove(pair);
                                                break;
                                            }
                                        }
                                        correction = true;
                                    }
                                } else {
                                    //Next section prevents adding any non DDoS sampler component to the Trafgen Thread Group
                                    if (parent.getTestElement() instanceof DDoSThreadGroup) {
                                        //Finds original parent of the selectedNode
                                        for (TeAndParent pair : editedNodesParents) {
                                            if (pair.te.equals(selectedNodeTE)) {
                                                addNode(pair.parentNode, selectedNode);
                                                selectedNode.removeFromParent();
                                                editedNodesParents.remove(pair); //Just for case of more exactly same components from different TGs
                                                break;
                                            }
                                        }
                                        correction = true;
                                    }
                                }
                            }
                        }
                    }
                    if (correction) {
                        reloadTreeAndShowError();
                    }
                }
            }
        });
    }

    /**
     * Registration of listeners to manage copy/cut&paste user action.
     * <p>
     * Pre-action listener of "Cut" event saves reference to parent of the cut node which may be needed
     * by following post-action listener of "Paste" event to revert the changes if they are not approved
     */
    private void registerClipboardListeners() {
        ActionRouter.getInstance().addPreActionListener(Cut.class, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                JMeterTreeNode[] selectedNodes = GuiPackage.getInstance().getTreeListener().getSelectedNodes();
                cutNodesParents.clear();
                for (JMeterTreeNode selectedNode : selectedNodes) {
                    JMeterTreeNode parent = (JMeterTreeNode) selectedNode.getParent();
                    if (parent != null) {
                        cutNodesParents.add(new TeAndParent(selectedNode.getTestElement(), parent));
                    }
                }
            }
        });

        ActionRouter.getInstance().addPostActionListener(Paste.class, new ActionListener() {

            /**
             * Can not directly remove movedNode from it's new parent parent because it doesn't have any parent but
             * new node is created from the original node TE so it can be found by comparing of original TE with TEs of children.
             *
             * @param parent node to which the pasted node was incorrectly copied or moved
             * @param nodeTE
             */
            private void removeNode(JMeterTreeNode parent, TestElement nodeTE) {
                for (int i = 0; i < parent.getChildCount(); i++) {
                    if (((JMeterTreeNode) parent.getChildAt(i)).getTestElement().equals(nodeTE)) {
                        parent.remove(i);
                        break;
                    }
                }
            }

            @Override
            public void actionPerformed(ActionEvent e) {
                JMeterTreeNode currentNode = GuiPackage.getInstance().getCurrentNode();
                JMeterTreeNode[] movedNodes = Copy.getCopiedNodes();
                JMeterTreeModel jmeterTreeModel = GuiPackage.getInstance().getTreeModel();
                boolean correction = false;

                List<JMeterTreeNode> ddosThreadGroupNodes = jmeterTreeModel.getNodesOfType(DDoSThreadGroup.class);
                if (ddosThreadGroupNodes.isEmpty()) {
                    return;
                }
                //Excludes TestPlan and WorkBench as the event is also triggered for them even if they don't have these nodes as children
                if (currentNode.getParent() == jmeterTreeModel.getRoot()) {
                    return;
                }
                for (JMeterTreeNode movedNode : movedNodes) {

                    TestElement movedNodeTE = movedNode.getTestElement();
                    String guiClass = movedNodeTE.getPropertyAsString(TestElement.GUI_CLASS);
                    if (!guiClass.contains("NetEmulGui") && !guiClass.contains("NetIntGui") && !guiClass.contains("NetLineGui")
                            && !guiClass.contains("NetAnalyzerGui")) { //Guarded by NetEmulInit/NetAnalyzerGui classes
                        if (movedNodeTE instanceof Trafgen) {
                            //Next section prevents adding DDoS components elsewhere outside of Trafgen Thread Group
                            if (!(currentNode.getTestElement() instanceof DDoSThreadGroup)) {
                                //Finds original parent of the selectedNode for case of "Cut" command
                                for (TeAndParent pair : cutNodesParents) {
                                    if (pair.te.equals(movedNodeTE)) {
                                        if (pair.parentNode != null) { //Means "Cut" was performed, not "Copy"
                                            addNode(pair.parentNode, movedNode);
                                            cutNodesParents.remove(pair);
                                            break;
                                        }
                                    }
                                }
                                removeNode(currentNode, movedNodeTE);
                                correction = true;
                            }
                        } else {
                            //Next section prevents adding any nono DDoS sampler component to the Trafgen Thread Group
                            if (currentNode.getTestElement() instanceof DDoSThreadGroup) {
                                //Finds original parent of the selectedNode for case of "Cut" command
                                for (TeAndParent pair : cutNodesParents) {
                                    if (pair.te.equals(movedNodeTE)) {
                                        if (pair.parentNode != null) { //Means "Cut" was performed, not "Copy"
                                            addNode(pair.parentNode, movedNode);
                                            cutNodesParents.remove(pair);
                                            break;
                                        }
                                    }
                                }
                                removeNode(currentNode, movedNodeTE);
                                correction = true;
                            }
                        }
                    }
                }
                if (correction) {
                    reloadTreeAndShowError();
                }
                cutNodesParents.clear();
            }
        });
    }

    /**
     * Single purpose class which defines a pair of node's TestElement and it's parent node
     *
     * @author Gity a.s.
     */
    private class TeAndParent {
        private final TestElement te;
        private final JMeterTreeNode parentNode;

        public TeAndParent(TestElement testElement, JMeterTreeNode parent) {
            this.te = testElement;
            this.parentNode = parent;
        }
    }
}
