
package de.ugoe.cs.util.console;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Level;

/**
 * <p>
 * Executes commands. The commands have to implement the {@link Command} interface and be in
 * packages registered using addCommandPackage(). Additionally, default commands are implemented in
 * the de.ugoe.cs.util.console.defaultcommands package.
 * </p>
 * <p>
 * This class is implemented as a <i>Singleton</i>.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class CommandExecuter {

    /**
     * <p>
     * Handle of the CommandExecuter instance.
     * </p>
     */
    private final static CommandExecuter theInstance = new CommandExecuter();

    /**
     * <p>
     * Prefix of all command classes.
     * </p>
     */
    private static final String cmdPrefix = "CMD";

    /**
     * <p>
     * Name of the package for default commands.
     * </p>
     */
    private static final String defaultPackage = "de.ugoe.cs.util.console.defaultcommands";

    /**
     * <p>
     * List of packages in which commands may be defined. The exec methods trys to load command from
     * these packages in the order they have been added.
     * </p>
     * <p>
     * The de.ugoe.cs.util.console.defaultcommands package has always lowest priority, unless it is
     * specifically added.
     * </p>
     */
    private List<String> commandPackageList;

    /**
     * <p>
     * Returns the instance of CommandExecuter. If no instances exists yet, a new one is created.
     * </p>
     * 
     * @return the instance of CommandExecuter
     */
    public static synchronized CommandExecuter getInstance() {
        return theInstance;
    }

    /**
     * <p>
     * Creates a new CommandExecuter. Private to prevent multiple instances (Singleton).
     * </p>
     */
    private CommandExecuter() {
        commandPackageList = new ArrayList<String>();
    }

    /**
     * <p>
     * Adds a package that will be used by {@link #exec(String)} to load command from.
     * </p>
     * 
     * @param pkg
     *            package where commands are located
     * @throws InvalidParameterException
     *             thrown if the package name is null or empty string
     */
    public void addCommandPackage(String pkg) {
        if ("".equals(pkg) || pkg == null) {
            throw new InvalidParameterException("package name must not be null or empty string");
        }
        commandPackageList.add(pkg);
    }

    /**
     * <p>
     * Executes the command defined by string. A command has the following form (mix of EBNF and
     * natural language):
     * </p>
     * <code>
     * &lt;command&gt; := &lt;commandname&gt;&lt;whitespace&gt;{&lt;parameter&gt;}<br>
     * &lt;commandname&gt; := String without whitespaces. Has to be a valid Java class name<br>
     * &lt;parameter&gt; := &lt;string&gt;|&lt;stringarray&gt;<br>
     * &lt;string&gt; := &lt;stringwithoutwhitespaces&gt;|&lt;stringwithwhitespaces&gt;
     * &lt;stringwithoutwhitespaces&gt; := a string without whitespaces<br>
     * &lt;stringwithoutwhitespaces&gt; := a string, that can have whitespaces, but must be in double quotes<br>
     * &lt;stringarray&gt; := "["&lt;string&gt;{&lt;whitespace&gt;&lt;string&gt;"]"
     * </code>
     * 
     * @param command
     *            the command as a string
     */
    public void exec(String command) {
        Console.commandNotification(command);
        Command cmd = null;
        CommandParser parser = new CommandParser();
        parser.parse(command);
        for (int i = 0; cmd == null && i < commandPackageList.size(); i++) {
            cmd = loadCMD(commandPackageList.get(i) + "." + cmdPrefix + parser.getCommandName());
        }
        if (cmd == null) { // check if command is available as default command
            cmd = loadCMD(defaultPackage + "." + cmdPrefix + parser.getCommandName());
        }
        if (cmd == null) {
            Console.println("Unknown command");
        }
        else {
            try {
                cmd.run(parser.getParameters());
            }
            catch (InvalidParameterException e) {
                Console.println("Usage: " + cmd.help());
            }
        }
    }

    /**
     * <p>
     * Helper method that loads a class and tries to cast it to {@link Command}.
     * </p>
     * 
     * @param className
     *            qualified name of the class (including package name)
     * @return if class is available and implement {@link Command} and instance of the class, null
     *         otherwise
     */
    private Command loadCMD(String className) {
        Command cmd = null;
        try {
            Class<?> cmdClass = Class.forName(className);
            cmd = (Command) cmdClass.newInstance();
        }
        catch (NoClassDefFoundError e) {
            String[] splitResult = e.getMessage().split("CMD");
            String correctName = splitResult[splitResult.length - 1].replace(")", "");
            Console.println("Did you mean " + correctName + "?");
        }
        catch (ClassNotFoundException e) {}
        catch (IllegalAccessException e) {}
        catch (InstantiationException e) {}
        catch (ClassCastException e) {
            Console.traceln(Level.WARNING, className + "found, but does not implement Command");
        }
        return cmd;
    }

    /**
     * <p>
     * reads all available commands from the registered command packages and returns a list of their
     * names
     * </p>
     * 
     * @return an array containing the names of the available commands.
     */
    public Command[] getAvailableCommands() {
        List<Command> commands = new ArrayList<Command>();
        List<String> packages = new ArrayList<String>();
        packages.addAll(commandPackageList);
        packages.add(defaultPackage);

        FilenameFilter filter = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return (name != null) && (name.startsWith(cmdPrefix)) && (name.endsWith(".class"));
            }
        };

        SortedSet<String> classNames = new TreeSet<String>(new Comparator<String>() {
            @Override
            public int compare(String arg1, String arg2) {
                String str1 = arg1.substring(arg1.lastIndexOf('.') + cmdPrefix.length() + 1);
                String str2 = arg2.substring(arg2.lastIndexOf('.') + cmdPrefix.length() + 1);
                return str1.compareTo(str2);
            }

        });

        for (String packageName : packages) {
            String path = packageName.replace('.', '/');
            try {
                Enumeration<URL> resources = ClassLoader.getSystemResources(path);

                while (resources.hasMoreElements()) {
                    URL resource = resources.nextElement();
                    File packageDir = new File(resource.getFile());

                    if (packageDir.isDirectory()) {
                        for (File classFile : packageDir.listFiles(filter)) {
                            String className =
                                classFile.getName().substring(0,
                                                              classFile.getName().lastIndexOf('.'));
                            classNames.add(packageName + "." + className);
                        }
                    }
                    else {
                        resource.getFile().startsWith("file:");
                        int index = resource.getFile().lastIndexOf('!');
                        if ((index > 0) && (resource.getFile().startsWith("file:")) &&
                            (resource.getFile().endsWith("!/" + path)))
                        {
                            String jarFile = resource.getFile().substring("file:".length(), index);

                            // we have to read the package content from a jar file
                            JarInputStream jarInputStream = null;
                            try {
                                jarInputStream = new JarInputStream(new FileInputStream(jarFile));
                            }
                            catch (Exception e) {
                                e.printStackTrace();
                                Console.traceln(Level.WARNING, "could not read contents of jar " +
                                    jarFile);
                            }

                            JarEntry entry = null;
                            do {
                                entry = jarInputStream.getNextJarEntry();
                                if ((entry != null) && (!entry.isDirectory()) &&
                                    (entry.getName().startsWith(path)))
                                {
                                    String className =
                                        entry.getName().substring(path.length(),
                                                                  entry.getName().lastIndexOf('.'));
                                    classNames.add(packageName + "." + className);
                                }
                            }
                            while (entry != null);
                        }
                    }
                }
            }
            catch (IOException e) {
                Console.traceln(Level.WARNING, "could not read commands of package " + packageName);
            }
        }

        for (String className : classNames) {
            // class may still be inner classes. Therefore load the command, to
            // see if it is really available and a command.
            Command cmd = loadCMD(className);
            if (cmd != null) {
                commands.add(cmd);
            }
        }

        Command[] commandArray = commands.toArray(new Command[commands.size()]);
        return commandArray;
    }
}
