package pipe.gui;

import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import pipe.constants.GUIConstants;
import pipe.controllers.application.PipeApplicationController;
import pipe.gui.plugin.GuiModule;
import uk.ac.imperial.pipe.models.petrinet.PetriNet;

import javax.swing.*;
import javax.swing.tree.*;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * The ModuleManager class contains methods to create swing components to allow
 * the user to load modules and execute methods within them. To use, instantiate
 * a ModuleManager object and use the methods to return the required components.
 *
 * @author Camilla Clifford
 * @author David Patterson -- minor changes 24 Nov 2006
 * @author Matthew Worthington -- changed the ModuleManger to dynamically load
 *         all class files in the module directory without the need to update the cfg
 *         files and provide path properties. Modules can now be dropped into the module
 *         folder and automatically loaded on all subsequent executions of pipe.
 *         Also refactored to reduce number of methods loaded with reflection into the
 *         Jtree which were subsequently never used. Now only loading the run method of
 *         each of the modules. (Jan,2007)
 * @author Pere Bonet - JAR May 2007
 */
public class ModuleManager {

    /**
     * Load text
     */
    private static final String LOAD_NODE_STRING = "Find IModule";

    /**
     * Class logger
     */
    private static final Logger LOGGER = Logger.getLogger(ModuleManager.class.getName());

    /**
     * PIPE Gui concrete models package
     */
    public static final String PIPE_GUI_PLUGIN_CONCRETE_PACKAGE = "pipe.gui.plugin.concrete";

    /**
     * All modules that have been found in the PIPE_GUI_PLUGIN_CONCRETE_PACKAGE
     */
    private final Set<Class<?>> installedModules;

    /**
     * Main PIPE application controller
     */
    private final PipeApplicationController controller;

    /**
     * Parent of the module loader
     */
    private final Component parent;

    /**
     * Module tree
     */
    private JTree moduleTree;

    /**
     * Tree model
     */
    private DefaultTreeModel treeModel;

    /**
     * Loaded modules
     */
    private DefaultMutableTreeNode loadModules;


    /**
     * Constructor
     * @param view view on which the modules should be displayed
     * @param controller main PIPE appliaction controller
     */
    public ModuleManager(Component view, PipeApplicationController controller) {
        this.controller = controller;

        parent = view;
        installedModules = new HashSet<>();
    }

    public JTree getModuleTree() {

        Collection<Class<? extends GuiModule>> classes = new ArrayList<>();

        // get the names of all the classes that are confirmed to be modules
        Collection<Class<? extends GuiModule>> names = getModuleClasses();
        classes.addAll(names);

        // create the root node
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("Analysis Module Manager");

        // create root children
        loadModules = new DefaultMutableTreeNode("Available Modules");

        MutableTreeNode add_modules = new DefaultMutableTreeNode(LOAD_NODE_STRING);

        // iterate over the class names and create a node for each
        for (Class<? extends GuiModule> clazz : classes) {
            // create each ModuleClass node using an instantiation of the
            // ModuleClass
            addClassToTree(clazz);
        }

        root.add(loadModules);
        root.add(add_modules);

        treeModel = new DefaultTreeModel(root);

        moduleTree = new JTree(treeModel);
        moduleTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

        moduleTree.addMouseListener(new TreeHandler());

        moduleTree.setFocusable(false);

        // expand the modules path
        moduleTree.expandPath(moduleTree.getPathForRow(1));
        return moduleTree;
    }

    /**
     * Finds all the fully qualified (ie: full package names) module classnames
     * by recursively searching the rootDirectories
     *
     * @return
     */
    //only load attempt to add .class files
    private Collection<Class<? extends GuiModule>> getModuleClasses() {
        Collection<Class<? extends GuiModule>> results = new ArrayList<>();
        try {
            ClassPath classPath = ClassPath.from(this.getClass().getClassLoader());
            ImmutableSet<ClassPath.ClassInfo> set = classPath.getTopLevelClasses(PIPE_GUI_PLUGIN_CONCRETE_PACKAGE);
            for (ClassPath.ClassInfo classInfo : set) {
                Class<?> clazz = classInfo.load();
                if (GuiModule.class.isAssignableFrom(clazz)) {
                    results.add((Class<? extends GuiModule>) clazz);
                }
            }
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage());
        }
        return results;
    }

    /**
     * Method creates and returns a IModule management tree.
     * This consists of two nodes, one resposible for listing all the available
     * modules from the module directory, and another for admin options such as
     * list refreshing.
     * Each node of the tree has it's own user object, for class nodes this will
     * be ModuleClass, for method nodes ModuleMethod, and another one yet to be
     * implemented for other options.
     * When the user clicks on a method node the method is invoked.
     * <p/>
     * Matthew - modified to reduce unnecessary reflection, now only loading
     * the run method of each module class into the tree
     *
     * @param moduleClass
     */
    private void addClassToTree(Class<? extends GuiModule> moduleClass) {
        if (installedModules.add(moduleClass)) {
            DefaultMutableTreeNode modNode = new DefaultMutableTreeNode(new ModuleClassContainer(moduleClass));

            try {
                Method tempMethod = moduleClass.getMethod("start", PetriNet.class);
                ModuleMethod m = new ModuleMethod(moduleClass, tempMethod);
                m.setName(modNode.getUserObject().toString());
                modNode.add(new DefaultMutableTreeNode(m));
            } catch (SecurityException | NoSuchMethodException e) {
                LOGGER.log(Level.SEVERE, e.getMessage());
            }

            if (modNode.getChildCount() == 1) {
                Object m = ((DefaultMutableTreeNode) modNode.getFirstChild()).
                        getUserObject();
                loadModules.add(new DefaultMutableTreeNode(m));
            } else {
                loadModules.add(modNode);
            }
        }
    }

    /**
     * Removes a node from the IModule subtree
     *
     * @param newNode The node to be removed.
     */
    private void removeModuleFromTree(MutableTreeNode newNode) {
        treeModel.removeNodeFromParent(newNode);
        treeModel.reload();
    }


    /**
     * Action object that can be used to remove a module from the ModuleTree
     */
    class RemoveModuleAction extends AbstractAction {
        private final DefaultMutableTreeNode removeNode;

        RemoveModuleAction(TreePath path) {
            removeNode = (DefaultMutableTreeNode) path.getLastPathComponent();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            Object o = removeNode.getUserObject();

            if (o instanceof ModuleMethod) {
                installedModules.remove(((ModuleMethod) o).getModClass());
            } else if (o instanceof ModuleClassContainer) {
                installedModules.remove(((ModuleClassContainer) o).returnClass());
            } else {
                LOGGER.log(Level.INFO, "Don't know how to delete class for " + o.getClass());
            }
            removeModuleFromTree(removeNode);
            moduleTree.expandPath(moduleTree.getPathForRow(1));
        }
    }


    // now add in the action listener to enable module method loading.
    public class TreeHandler extends MouseAdapter {

        @Override
        public void mouseClicked(MouseEvent e) {
            int selRow = moduleTree.getRowForLocation(e.getX(), e.getY());
            TreePath selPath = moduleTree.getPathForLocation(e.getX(), e.getY());

            if (selRow != -1) {
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
                Object nodeObj = node.getUserObject();

                if (e.getClickCount() == 2) {
                    if (nodeObj instanceof ModuleMethod) {

                        PetriNet petriNet = controller.getActivePetriNetController().getPetriNet();
                        ((ModuleMethod) nodeObj).execute(petriNet);
                    } else if (nodeObj.equals(LOAD_NODE_STRING)) {

                        //Create a file chooser
                        JFileChooser fc = new JFileChooser();
                        fc.setFileFilter(new ExtensionFilter(GUIConstants.PROPERTY_FILE_EXTENSION,
                                        GUIConstants.PROPERTY_FILE_DESC)
                        );
                        //In response to a button click:
                        int returnVal = fc.showOpenDialog(parent);
                        if (returnVal == JFileChooser.APPROVE_OPTION) {
                            File moduleProp = fc.getSelectedFile();
                            Class<?> newModuleClass = ModuleLoader.importModule(moduleProp);

                            if (newModuleClass != null) {
                                //TODO
                                //                                addClassToTree(newModuleClass);
                                treeModel.reload();
                                moduleTree.expandPath(moduleTree.getPathForRow(1));
                            } else {
                                JOptionPane.showMessageDialog(parent, "Invalid file selected.\n Please ensure the "
                                                + "class implements the IModule interface and is"
                                                + " on the CLASSPATH.", "File Selection Error",
                                        JOptionPane.ERROR_MESSAGE
                                );
                            }
                        }
                    }
                }
            }
        }
        /**
         * Show the menu popup to run the module
         * @param e mouse event 
         */
        @Override
        public void mousePressed(MouseEvent e) {
            if (e.isPopupTrigger()) {
                showPopupMenu(e);
            }
        }

        /**
         * Show the menu popup to run the module
         * @param e mouse event 
         */
        @Override
        public void mouseReleased(MouseEvent e) {
            if (e.isPopupTrigger()) {
                showPopupMenu(e);
            }
        }

        /**
         * Show the menu for the modules
         * @param e mouse event 
         */
        private void showPopupMenu(MouseEvent e) {
            TreePath selPath = moduleTree.getPathForLocation(e.getX(), e.getY());

            if (selPath != null) {
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
                Object nodeObj = node.getUserObject();

                if ((nodeObj instanceof ModuleClassContainer) || (nodeObj instanceof ModuleMethod)) {
                    JPopupMenu popup = new JPopupMenu();
                    TreePath removePath = moduleTree.getPathForLocation(e.getX(), e.getY());
                    JMenuItem menuItem = new JMenuItem(new RemoveModuleAction(removePath));
                    menuItem.setText("Remove Module");
                    popup.add(menuItem);
                    popup.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        }
    }

}
