// Copyright 2000, CERN, Geneva, Switzerland.
package org.freehep.util.commandline;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.Map.Entry;


/**
 * CommandLine parses a command line for parameters and options.
 *
 * @author Mark Donszelmann (CommandLine)
 * @version $Id: CommandLine.java,v 1.11 2002/06/12 23:15:16 duns Exp $
 */
public class CommandLine {

    // requested
    private String name;
    private String description;
    private int numberOfParameters;
    private Hashtable options;          // (name or shortName, Option)
    private TreeMap   sortedOptions;    // (name, Option);
    private Vector parameters;
    private CommandLineException exception;
    private boolean multiLevel;
    private boolean check = true;

    // supplied
    private Vector arguments = new Vector();
    private Hashtable flags = new Hashtable();

    /**
     * Creates a CommandLine object to be used for parsing a set of arguments
     *
     * @param name name of the command
     * @param description description of the command
     * @param numberOfParameters minumum number of parameters
     * @param multiLevel forces the command line to be multi-level (i.e. does not check for
     *                   max number of parameters
     */
    public CommandLine(String name, String description, int numberOfParameters, boolean multiLevel) {
        this.name = name;
        this.description = description;
        this.numberOfParameters = numberOfParameters;
        this.multiLevel = multiLevel;
        this.parameters = new Vector();
        this.exception = null;
        this.options = new Hashtable();

        this.sortedOptions = new TreeMap(new Comparator() {
            public int compare(Object obj1, Object obj2) {
                return ((String)obj1).compareTo((String)obj2);
            }
        });
    }

    /**
     * Creates a CommandLine object to be used for parsing a set of arguments
     *
     * @param name name of the command
     * @param description description of the command
     * @param numberOfParameters minumum number of parameters
     */
    public CommandLine(String name, String description, int numberOfParameters) {
        this(name, description, numberOfParameters, false);
    }

    /**
     * enables checking of the options and parameters
     *
     * @param sets checking
     */
    public void enableChecking(boolean check) {
        this.check = check;
    }

	/**
	 * Adds an option to be recognized by parse.
	 *
     * @param longName the full name of the option
     * @param shortName the short name of the option (may be set to null)
     * @param comment description of the arguments to the option
	 */
	public void addOption(String longName, String shortName, String comment) {
	    addOption(new Option(longName, shortName, comment));
	}

	/**
	 * Adds an option to be recognized by parse.
	 *
     * @param longName the full name of the option
     * @param shortName the short name of the option (may be set to null)
     * @param qualifiers gives a list of qualifiers you can use
     * @param comment description of the arguments to the option
	 */
	public void addOption(String longName, String shortName, String[] qualifiers, String comment) {
	    addOption(new Option(longName, shortName, qualifiers, comment));
	}

	/**
	 * Adds an option to be recognized by parse.
	 *
     * @param longName the full name of the option
     * @param shortName the short name of the option (may be set to null)
     * @param valueDescription description of the value
     * @param comment description of the arguments to the option
	 */
	public void addOption(String longName, String shortName, String valueDescription, String comment) {
	    addOption(new Option(longName, shortName, valueDescription, comment));
	}

	/**
	 * Adds an option to be recognized by parse, without doing parameter or option checking.
	 * This is normally used for options such as -? -help and -version
	 *
     * @param longName the full name of the option
     * @param shortName the short name of the option (may be set to null)
     * @param comment description of the arguments to the option
	 */
	public void addBailOutOption(String longName, String shortName, String comment) {
	    addOption(new Option(longName, shortName, comment, true));
	}

	/**
	 * Adds an option to be recognized by parse. This option can be specified multiple times
	 * on the commandline, and is immediately followed by its value. Usage (-Iincludedir)
	 *
     * @param name the full name of the option (preferably one character)
     * @param valueDescription what the value after the option means
     * @param comment description of the argument to the option
	 */
	public void addMultiOption(String name, String valueDescription, String comment) {
	    addOption(new Option(name, name, valueDescription, comment, true));
	}

	private void addOption(Option option) {
        options.put(option.getName(), option);
        sortedOptions.put(option.getName(), option);
        if (option.getShortName() != null) options.put(option.getShortName(), option);
	}

    /**
     * Adds a parameter to be recognized by parse.
     *
     * @param name name of the parameter
     * @param comment comment describing the parameter
     */
    public void addParameter(String name, String comment) {
        Parameter par = new Parameter(name, comment, (parameters.size() >= numberOfParameters));
        parameters.addElement(par);
    }

    /**
     * @return name of the command
     */
    public String getName() {
        return name;
    }

    /**
     * @return description of the command
     */
    public String getDescription() {
        return description;
    }

    /**
     * @return an enumeration of possible options (CommandLine.Option)
     */
    public Enumeration getOptions() {
        return options.keys();
    }

    /**
     * @return an enumeration of mandatory and optional parameters (CommandLine.Parameter)
     */
    public Enumeration getParameters() {
        return parameters.elements();
    }

    /**
     * @return true if the specified option is legal
     */
    public boolean isLegalOption(String name) {
        return options.containsKey(name);
    }

    /**
     * Parses the supplied arguments.
     *
     * @param args arguments for the command, including all options
     * @return true if the arguments were parsed correctly. False if a bailOut option was specified.
     * @throws CommandLineException in case the parsing failed.
     */
    public boolean parse(String[] args) throws CommandLineException {
        boolean bailOut = false;
        int i = 0;
        while (i < args.length) {
            if (args[i].charAt(0) == '-') {
                // option
                int j = (args[i].charAt(1) != '-') ? 1 : 2;
                StringBuffer flag = new StringBuffer();
                boolean qualified = false;
                while (j < args[i].length()) {
                    switch(args[i].charAt(j)) {
                        case '-':
                            // multiple options in one argument (never the last)
                            checkflag(flag.toString(), null, qualified);
                            bailOut |= ((Option)options.get(flag.toString())).isBailOut();
                            flag = new StringBuffer();
                            qualified = false;
                            break;
                        case ':':
                            qualified = true;
                            String qualifier = args[i].substring(j+1);
                            j = args[i].length() - 1;
                            checkflag(flag.toString(), qualifier, qualified);
                            flag = new StringBuffer();
                            qualified = false;
                            break;
                        case '=':
                            String value = args[i].substring(j+1);
                            j = args[i].length() - 1;
                            checkflag(flag.toString()+"="+value, value, qualified);
                            flag = new StringBuffer();
                            qualified = false;
                            break;
                        default:
                            flag.append(args[i].charAt(j));
                            break;
                    }
                    j++;
                }

                // (last) option could be set, but value may follow...
                if (flag.length() > 0) {
                    if (checkflag(flag.toString(), (i+1<args.length) ? args[i+1] : null, qualified)) {
                        i++;
                    }
                    Option option  = (Option)options.get(flag.toString());
                    bailOut |= (option == null) ? false : option.isBailOut();
                }

            } else {
                // arguments from here on
                while (i < args.length) {
                    arguments.addElement(args[i]);
                    i++;
                }
            }
            i++;
        }

        if (!bailOut) {
            // check for too few arguments here...
            if (arguments.size() < numberOfParameters) {
                exception = new MissingArgumentException("Too few arguments: "+arguments.size()+
                               ", while "+numberOfParameters+" "+
                                   ((parameters.size() == 1) ? "was" : "were")+" expected.");
                throw exception;
            }

            if ((check) && (!multiLevel)) {
                // check for too many arguments here ...
                if ((parameters.size() == 0) ||
                    !((Parameter)parameters.elementAt(parameters.size()-1)).getName().endsWith("...")) {
                    if (arguments.size() > parameters.size()) {
                        exception = new TooManyArgumentsException("Too many arguments: "+arguments.size()+
                                       ", while "+parameters.size()+" "+
                                       ((parameters.size() == 1) ? "was" : "were")+" expected.");
                        throw exception;
                    }
                }
            }
        }

        exception = null;
        return !bailOut;
    }

    private boolean checkflag(String flag, String value, boolean qualified) throws CommandLineException {
        if ((check) && (flag.length() <= 0)) {
            exception = new NoSuchOptionException("Option has zero length");
            throw exception;
        }

        Option option;
        if ((option = ((Option)options.get(flag)))!= null) {
            if (qualified) {
                // check qualifiers
                String[] qualifiers = option.getQualifiers();
                for (int j=0; j<qualifiers.length; j++) {
                    if (value.equals(qualifiers[j])) {
                        setFlag(option, value);
                        return false;
                    }
                }
                exception = new NoSuchQualifierException("Qualifier '"+value+"' for option '"+flag+"' does not exist.");
                throw exception;
            } else {
                // did we expect a value
                if (option.getValue() == null) {
                    setFlag(option, "true");
                    return false;
                } else {
                    if (value == null) {
                        exception = new MissingArgumentException("Missing argument <"+option.getValue()+"> for option '"+flag+"'.");
                        throw exception;
                    }
                    setFlag(option, value);
                    return true;
                }
            }
        }

        // check for special multi options
        for (Enumeration e = options.elements(); e.hasMoreElements(); ) {
            Option opt = (Option)e.nextElement();
            if (opt.isMulti() && flag.startsWith(opt.getName())) {
                if (opt.getName().length() < flag.length()) {
                    setFlag(opt, flag.substring(opt.getName().length()));
                    return false;
                } else {
                    exception = new MissingArgumentException("Missing argument <"+opt.getValue()+"> for option '"+opt.getName()+"'.");
                    throw exception;
                }
            }
        }

        // option does not exist
        if (check) {
            exception = new NoSuchOptionException("Option '"+flag+"' does not exist.");
            throw exception;
        }
        return false;
    }

    private void setFlag(Option option, String value) {
        if (option.isMulti()) {
            if (flags.get(option.getName()) == null) {
                flags.put(option.getName(), new Vector());
            }
            Vector values = (Vector)flags.get(option.getName());
            values.addElement(value);
        } else {
            flags.put(option.getName(), value);
        }
        if (option.getShortName() != null) flags.put(option.getShortName(), value);
    }

    /**
     * @return the last thrown exception
     */
    public CommandLineException getException() {
        return exception;
    }

    /**
     * @return a textual representation of the parsed arguments
     */
    public String toString() {
        StringBuffer s = new StringBuffer("CommandLine [\n");
        // line 1
        s.append("    ");
        s.append(name);
        s.append(" - ");
        s.append(description);
        s.append("\n");

        // line 2
        for (Enumeration e = flags.keys(); e.hasMoreElements(); ) {
            String key = (String)e.nextElement();
            s.append("    option[");
            s.append(key);
            s.append("] = ");
            Option option = (Option)options.get(key);
            if (option.isMulti()) {
                Vector values = (Vector)flags.get(key);
                for (int i=0; i<values.size(); i++) {
                    s.append((String)values.elementAt(i)+", ");
                }
            } else {
                s.append(flags.get(key));
            }
            s.append("\n");
        }

        // line 3
        for (Enumeration e = arguments.elements(); e.hasMoreElements(); ) {
            String arg = (String)e.nextElement();
            s.append("    arg = ");
            s.append(arg);
            s.append("\n");
        }

        // line 4
        if (exception != null) {
            s.append(exception.getMessage());
            s.append("\n");
        }

        s.append("\n]");
        return s.toString();
    }

    /**
     * @param name name of the option
     * @return the value of a specified option. null is returned in case the option was not specified.
     * true if this option was a flag.
     */
    public String getOption(String name) {
        return getOption(name, null);
    }

    /**
     * @param name name of the option
     * @return the value of a specified option. An empty vector is returned in case the option was not specified.
     */
    public Vector getMultiOption(String name) {
        Object flag = flags.get(name);
        return (flag == null) ? new Vector() : (Vector)flag;
    }

    /**
     * @param name name of the option
     * @param defaultValue default value if option was not set
     * @return the value of a specified option. null is returned in case the option was not specified.
     * true if this option was a flag.
     */
    public String getOption(String name, String defaultValue) {
        Object flag = flags.get(name);
        return (flag == null) ? defaultValue : flag.toString();
    }

    /**
     * @param name name of the option
     * @return true if the option was specified.
     */
    public boolean hasOption(String name) {
        return flags.containsKey(name);
    }

    /**
     * @param name name of the argument
     * @return a named argument. null in case the named argument was not supplied.
     */
    public String getArgument(String name) {
        for (int i=0; i<parameters.size(); i++) {
            Parameter parameter = (Parameter)parameters.elementAt(i);
            if (name.equals(parameter.getName())) {
                return (String)arguments.elementAt(i);
            }
        }
        return null;
    }

    /**
     * @return string array of parameters
     */
    public String[] getArguments() {
        String[] s = new String[arguments.size()];
        arguments.copyInto(s);
        return s;
    }

    /**
     * @return straing array of parameters which were not used by the command line
     */
    public String[] getUnparsedArguments() {
        String[] s = new String[arguments.size() - parameters.size()];
        int j = 0;
        for (int i=parameters.size(); i<arguments.size(); i++) {
            s[j] = (String)arguments.elementAt(i);
        }
        return s;
    }

    private String pad(String str, int length) {
        String padding = "                                                             ";
        int l = str.length();
        if (l == length) return str;
        else if (l < length) return str + padding.substring(0,length-l);
        else return str.substring(length);
    }
    /**
     * Get the help message, suitable for printing in response to a -help option
     */
    public String getHelp() {
        StringBuffer s = new StringBuffer();

        // line 1
        s.append(name);
        s.append(" - ");
        s.append(description);
        s.append("\n");

        // line 2
        s.append("Usage: ");
        s.append(name);
        if (options.size() > 0) {
            s.append(" [-options]");
        }
        int len = 0;
        for (int i=0; i<parameters.size(); i++) {
            Parameter parameter = (Parameter)parameters.elementAt(i);
            len = Math.max(len, parameter.getName().length());
            if (i<numberOfParameters) {
                s.append(" <");
                s.append(parameter.getName());
                s.append(">");
            } else {
                s.append(" [");
                s.append(parameter.getName());
            }
        }
        for (int i=numberOfParameters; i<parameters.size(); i++) {
            s.append("]");
        }
        s.append("\n\n");

        if (!parameters.isEmpty())
        {
            // line 3
            s.append("where:\n");

            // line 4
            for (int i=0; i<parameters.size(); i++) {
                Parameter parameter = (Parameter)parameters.elementAt(i);
                s.append("    ");
                s.append(pad(parameter.getName(), len));
                s.append("  ");
                s.append(parameter.getDescription());
                s.append("\n");
            }
            s.append("\n");
        }

        if (!sortedOptions.isEmpty())
        {
            // line 5
            s.append("where options include:\n");

            // line 6
            len = 0;
            int slen = 0;
            for(Iterator iterator = sortedOptions.entrySet().iterator(); iterator.hasNext(); ) {
                Option option = (Option)((Entry)iterator.next()).getValue();
                len = Math.max(len, option.getString().length());
                slen = Math.max(slen, (option.getShortName() == null) ? 0 : option.getShortName().length());
            }

            boolean multi = false;
            for(Iterator iterator = sortedOptions.keySet().iterator(); iterator.hasNext(); ) {
                String key = (String)iterator.next();
                Option option = (Option)sortedOptions.get(key);
                s.append("    ");
                if (slen > 0) {
                    if (option.getShortName() == null) {
                        s.append(option.isMulti() ? "*" : " ");
                        multi |= option.isMulti();
                        s.append(pad("", slen));
                    } else {
                        s.append("-");
                        s.append(pad(option.getShortName(), slen));
                    }
                    s.append(" ");
                }
                s.append(pad(option.getString(), len));
                s.append("  ");
                s.append(option.getDescription());
                s.append("\n");
            }

            s.append("\n");
            if (multi) {
                s.append("(* option can be specified multiple times).\n");
                s.append("\n");
            }
        }
        return s.toString();
    }

    /**
     * Keeps mandatory and optional parameter names.
     */
    public static class Parameter {
        private String name;
        private String description;
        private boolean optional;

        public Parameter(String name, String description, boolean optional) {
            this.name = name;
            this.description = description;
            this.optional = optional;
        }

        public String getName() {
            return name;
        }

        public String getDescription() {
            return description;
        }

        public boolean isOptional() {
            return optional;
        }
    }

    /**
     * Keeps option names and types.
     */
    public static class Option {
        private String name;
        private String shortName;
        private String description;
        private String[] qualifiers = null;
        private String value = null;
        private boolean multi = false;
        private boolean bailOut;

        public Option(String name, String shortName, String description) {
            this(name, shortName, description, false);
        }

        public Option(String name, String shortName, String description, boolean bailOut) {
            this.name = name;
            this.shortName = shortName;
            this.description = description;
            this.bailOut = bailOut;
        }

        public Option(String name, String shortName, String[] qualifiers, String description) {
            this(name, shortName, description);
            this.qualifiers = qualifiers;
        }

        public Option(String name, String shortName, String value, String description) {
            this(name, shortName, description);
            this.value = value;
        }

        public Option(String name, String shortName, String value, String description, boolean multi) {
            this(name, (multi) ? null : shortName, description);
            this.value = value;
            this.multi = multi;
        }

        public String getName() {
            return name;
        }

        public String getShortName() {
            return shortName;
        }

        public String getDescription() {
            return description;
        }

        public boolean isBailOut() {
            return bailOut;
        }

        public String[] getQualifiers() {
            return qualifiers;
        }

        public String getValue() {
            return value;
        }

        public boolean isMulti() {
            return multi;
        }

        public String getString() {
            StringBuffer s = new StringBuffer("-");
            s.append(name);
            if (value != null) {
                if (!isMulti()) s.append(" ");
                s.append("<");
                s.append(value);
                s.append(">");
            } else if (qualifiers != null) {
                s.append(":{");
                for (int i=0; i<qualifiers.length; i++) {
                    if (i > 0) {
                        s.append("|");
                    }
                    s.append(qualifiers[i]);
                }
                s.append("}");
            }
            return s.toString();
        }
    }
}
