/*
 * Copyright The Sett Ltd, 2005 to 2014.
 *
 * Licensed 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 com.thesett.common.util;

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * CommandLineParser provides a utility for specifying the format of a command line and parsing command lines to ensure
 * that they fit their specified format. A command line is made up of flags and options, both may be refered to as
 * options. A flag is an option that does not take an argument (specifying it means it has the value 'true' and not
 * specifying it means it has the value 'false'). Options must take arguments but they can be set up with defaults so
 * that they take a default value when not set. Options may be mandatory in wich case it is an error not to specify them
 * on the command line. Flags are never mandatory because they are implicitly set to false when not specified.
 *
 *
Some examples command line are:
 *
 *

 *
This one has two options that expect arguments:
 *
 *

 * cruisecontrol -configfile cruisecontrol.xml -port 9000
 *

 *
This has one no-arg flag and two 'free' arguments:
 *
 *

 * zip -r project.zip project/*
 *

 *
This one concatenates multiple flags into a single block with only one '-':
 *
 *

 * jar -tvf mytar.tar
 *

 *
 *
The parsing rules are:
 *
 *

 *
Flags may be combined after a single '-' because they never take arguments. Normally such flags are single letter
 * flags but this is only a convention and not enforced. Flags of more than one letter are usually specified on their
 * own.
 *
Options expecting arguments must always be on their own.
 *
The argument to an option may be seperated from it by whitespace or appended directly onto the option.
 *
The argument to an option may never begin with a '-' character.
 *
All other arguments not beginning with a '-' character are free arguments that do not belong to any option.
 *
The second or later of a set of duplicate or repeated flags override earlier ones.
 *
Options are matched up to the shortest matching option. This is because of the possibility of having no space
 * between an option and its argument. This rules out the possibility of using two options where one is an opening
 * substring of the other. For example, the options "foo" and "foobar" cannot be used on the same command line because
 * it is not possible to distinguish the argument "-foobar" from being the "foobar" option or the "foo" option with the
 * "bar" argument.
 *

 *
 *
By default, unknown options are simply ignored if specified on the command line. This behaviour may be changed so
 * that the parser reports all unknowns as errors by using the {@link #setErrorsOnUnknowns} method.
 *
 *

CRC Card

 *
ResponsibilitiesCollaborations
 *
Accept a command line specification.
 *
Parse a command line into properties, validating it against its specification.
 *
Report all errors between a command line and its specification.
 *
Provide a formatted usage string for a command line.
 *
Provide a formatted options in force string for a command line.
 *
Allow errors on unknowns behaviour to be turned on or off.
 *

 *
 * @author Rupert Smith
 */
public class CommandLineParser
{
    /**
     * Holds a mapping from command line option names to detailed information about those options. Use of a tree map
     * ensures that the options are easy to print in alphabetical order as a usage string. An alternative might be to
     * use a LinkedHashMap to print them in the order they are specified.
     */
    private final Map<String, CommandLineOption> optionMap = new TreeMap<String, CommandLineOption>();

    /** Holds a list of parsing errors. */
    private Collection<String> parsingErrors = new ArrayList<String>();

    /** Holds the regular expression matcher to match command line options with. */
    private Matcher optionMatcher;

    /** Holds the parsed command line properties after parsing. */
    private Properties parsedProperties;

    /** Holds any trailing name=value pairs specified in the free arguments. */
    private Properties trailingProperties;

    /** Flag used to indicate that errors should be created for unknown options. False by default. */
    private boolean errorsOnUnknowns;

    /**
     * Creates a command line options parser from a command line specification. This is passed to this constructor as an
     * array of arrays of strings. Each array of strings specifies the command line for a single option. A static array
     * may therefore easily be used to configure the command line parser in a single method call with an easily readable
     * format.
     *
     *
Each array of strings must be 2, 3, 4 or 5 elements long. If any of the last three elements are missing they
     * are assumed to be null. The elements specify the following parameters:
     *
     *

     *
The name of the option without the leading '-'. For example, "file". To specify the format of the 'free'
     * arguments use the option names "1", "2", ... and so on.
     *
The option comment. A line of text describing the usage of the option. For example, "The file to be
     * processed."
     *
The options argument. This is a very short description of the argument to the option, often a single word or
     * a reminder as to the arguments format. When this element is null the option is a flag and does not accept any
     * arguments. For example, "filename" or "(unix | windows)" or null. The actual text specified is only used to print
     * in the usage message to remind the user of the usage of the option.
     *
The mandatory flag. When set to "true" an option must always be specified. Any other value, including null,
     * means that the option is mandatory. Flags are always mandatory (see class javadoc for explanation of why) so this
     * is ignored for flags.
     *
A regular expression describing the format that the argument must take. Ignored if null.
     *

     *
     *
An example call to this constructor is:
     *
     *

     * CommandLineParser commandLine = new CommandLineParser(
     *     new String[][] {{"file", "The file to be processed. ", "filename", "true"},
     *                     {"dir", "Directory to store results in. Current dir used if not set.", "out dir"},
     *                     {"os", "Operating system EOL format to use.", "(windows | unix)", null, "windows\|unix"},
     *                     {"v", "Verbose mode. Prints information about the processing as it goes."},
     *                     {"1", "The processing command to run.", "command", "true", "add\|remove\|list"}});
     *

     *
     * @param config The configuration as an array of arrays of strings.
     */
    public CommandLineParser(String[][] config)
    {
        // Loop through all the command line option specifications creating details for each in the options map.
        for (String[] nextOptionSpec : config)
        {
            addOption(nextOptionSpec[0], nextOptionSpec[1], (nextOptionSpec.length > 2) ? nextOptionSpec[2] : null,
                (nextOptionSpec.length > 3) && ("true".equals(nextOptionSpec[3])),
                (nextOptionSpec.length > 4) ? nextOptionSpec[4] : null);
        }
    }

    /**
     * Right pads a string with a given string to a given size. This method will repeat the padder string as many times
     * as is necessary until the exact specified size is reached. If the specified size is less than the size of the
     * original string then the original string is returned unchanged.
     *
     *

     * Example1 - original string "cat", padder string "white", size 8 gives "catwhite".
     * Example2 - original string "cat", padder string "white", size 15 gives "catwhitewhitewh".
     * Example3 - original string "cat", padder string "white", size 2 gives "cat".
     *

     *
     * @param  stringToPad The original string.
     * @param  padder      The string to pad onto the original string.
     * @param  size        The required size of the new string.
     *
     * @return The newly padded string.
     */
    public static String rightPad(String stringToPad, String padder, int size)
    {
        if (padder.length() == 0)
        {
            return stringToPad;
        }

        StringBuffer strb = new StringBuffer(stringToPad);
        CharacterIterator sci = new StringCharacterIterator(padder);

        while (strb.length() < size)
        {
            for (char ch = sci.first(); ch != CharacterIterator.DONE; ch = sci.next())
            {
                if (strb.length() < size)
                {
                    strb.append(String.valueOf(ch));
                }
            }
        }

        return strb.toString();
    }

    /**
     * Lists all the parsing errors from the most recent parsing in a string.
     *
     * @return All the parsing errors from the most recent parsing.
     */
    public String getErrors()
    {
        // Return the empty string if there are no errors.
        if (parsingErrors.isEmpty())
        {
            return "";
        }

        // Concatenate all the parsing errors together.
        String result = "";

        for (String s : parsingErrors)
        {
            result += s;
        }

        return result;
    }

    /**
     * Lists the properties set from the most recent parsing or an empty string if no parsing has been done yet.
     *
     * @return The properties set from the most recent parsing or an empty string if no parsing has been done yet.
     */
    public String getOptionsInForce()
    {
        // Check if there are no properties to report and return and empty string if so.
        if (parsedProperties == null)
        {
            return "";
        }

        // List all the properties.
        String result = "Options in force:\n";

        for (Map.Entry<Object, Object> property : parsedProperties.entrySet())
        {
            result += property.getKey() + " = " + property.getValue() + "\n";
        }

        return result;
    }

    /**
     * Generates a usage string consisting of the name of each option and each options argument description and comment.
     *
     * @return A usage string for all the options.
     */
    public String getUsage()
    {
        String result = "Options:\n";

        int optionWidth = 0;
        int argumentWidth = 0;

        // Calculate the column widths required for aligned layout.
        for (CommandLineOption optionInfo : optionMap.values())
        {
            int oWidth = optionInfo.option.length();
            int aWidth = (optionInfo.argument != null) ? (optionInfo.argument.length()) : 0;

            optionWidth = (oWidth > optionWidth) ? oWidth : optionWidth;
            argumentWidth = (aWidth > argumentWidth) ? aWidth : argumentWidth;
        }

        // Print usage on each of the command line options.
        for (CommandLineOption optionInfo : optionMap.values())
        {
            String argString = ((optionInfo.argument != null) ? (optionInfo.argument) : "");
            String optionString = optionInfo.option;

            argString = rightPad(argString, " ", argumentWidth);
            optionString = rightPad(optionString, " ", optionWidth);

            result += "-" + optionString + " " + argString + " " + optionInfo.comment + "\n";
        }

        return result;
    }

    /**
     * Control the behaviour of the errors on unkowns reporting. When turned on this reports all unkowns options as
     * errors. When turned off, all unknowns are simply ignored.
     *
     * @param errors The setting of the errors on unkown flag. True to turn it on.
     */
    public void setErrorsOnUnknowns(boolean errors)
    {
        errorsOnUnknowns = errors;
    }

    /**
     * Parses a set of command line arguments into a set of properties, keyed by the argument flag. The free arguments
     * are keyed by integers as strings starting at "1" and then "2", ... and so on.
     *
     *
See the class level comment for a description of the parsing rules.
     *
     * @param  args The command line arguments.
     *
     * @return The arguments as a set of properties.
     *
     * @throws IllegalArgumentException If the command line cannot be parsed against its specification. If this
     *                                  exception is thrown a call to {@link #getErrors} will provide a diagnostic of
     *                                  the command line errors.
     */
    public Properties parseCommandLine(String[] args) throws IllegalArgumentException
    {
        Properties options = new Properties();

        // Used to keep count of the current 'free' argument.
        int free = 1;

        // Used to indicate that the most recently parsed option is expecting arguments.
        boolean expectingArgs = false;

        // The option that is expecting arguments from the next element of the command line.
        String optionExpectingArgs = null;

        // Used to indicate that the most recently parsed option is a duplicate and should be ignored.
        // boolean ignore = false;

        // Create the regular expression matcher for the command line options.
        String regexp = "^(";
        int optionsAdded = 0;

        for (Iterator<String> i = optionMap.keySet().iterator(); i.hasNext();)
        {
            String nextOption = i.next();

            // Check that the option is not a free argument definition.
            boolean notFree = false;

            try
            {
                Integer.parseInt(nextOption);
            }
            catch (NumberFormatException e)
            {
                notFree = true;
            }

            // Add the option to the regular expression matcher if it is not a free argument definition.
            if (notFree)
            {
                regexp += nextOption + (i.hasNext() ? "|" : "");
                optionsAdded++;
            }
        }

        // There has to be more that one option in the regular expression or else the compiler complains that the close
        // cannot be nullable if the '?' token is used to make the matched option string optional.
        regexp += ")" + ((optionsAdded > 0) ? "?" : "") + "(.*)";

        Pattern pattern = Pattern.compile(regexp);

        // Loop through all the command line arguments.
        for (String arg1 : args)
        {
            // Check if the next command line argument begins with a '-' character and is therefore the start of
            // an option.
            if (arg1.startsWith("-"))
            {
                // Extract the value of the option without the leading '-'.
                String arg = arg1.substring(1);

                // Match up to the longest matching option.
                optionMatcher = pattern.matcher(arg);
                optionMatcher.matches();

                String matchedOption = optionMatcher.group(1);

                // Match any argument directly appended onto the longest matching option.
                String matchedArg = optionMatcher.group(2);

                // Check that a known option was matched.
                if ((matchedOption != null) && !"".equals(matchedOption))
                {
                    // Get the command line option information for the matched option.
                    CommandLineOption optionInfo = optionMap.get(matchedOption);

                    // Check if this option is expecting arguments.
                    if (optionInfo.expectsArgs)
                    {
                        // The option is expecting arguments so swallow the next command line argument as an
                        // argument to this option.
                        expectingArgs = true;
                        optionExpectingArgs = matchedOption;

                        // In the mean time set this options argument to the empty string in case no argument is ever
                        // supplied.
                        // options.put(matchedOption, "");
                    }

                    // Check if the option was matched on its own and is a flag in which case set that flag.
                    if ("".equals(matchedArg) && !optionInfo.expectsArgs)
                    {
                        options.put(matchedOption, "true");
                    }

                    // The option was matched as a substring with its argument appended to it or is a flag that is
                    // condensed together with other flags.
                    else if (!"".equals(matchedArg))
                    {
                        // Check if the option is a flag and therefore is allowed to be condensed together
                        // with other flags.
                        if (!optionInfo.expectsArgs)
                        {
                            // Set the first matched flag.
                            options.put(matchedOption, "true");

                            // Repeat the longest matching process on the remainder but ensure that the remainder
                            // consists only of flags as only flags may be condensed together in this fashion.
                            do
                            {
                                // Match the remainder against the options.
                                optionMatcher = pattern.matcher(matchedArg);
                                optionMatcher.matches();

                                matchedOption = optionMatcher.group(1);
                                matchedArg = optionMatcher.group(2);

                                // Check that an option was matched.
                                if (matchedOption != null)
                                {
                                    // Get the command line option information for the next matched option.
                                    optionInfo = optionMap.get(matchedOption);

                                    // Ensure that the next option is a flag or raise an error if not.
                                    if (optionInfo.expectsArgs)
                                    {
                                        parsingErrors.add("Option " + matchedOption +
                                            " cannot be combined with flags.\n");
                                    }

                                    options.put(matchedOption, "true");
                                }

                                // The remainder could not be matched against a flag it is either an unknown flag
                                // or an illegal argument to a flag.
                                else
                                {
                                    parsingErrors.add("Illegal argument to a flag in the option " + arg + "\n");

                                    break;
                                }
                            }
                            // Continue until the remainder of the argument has all been matched with flags.
                            while (!"".equals(matchedArg));
                        }

                        // The option is expecting an argument, so store the unmatched portion against it
                        // as its argument.
                        else
                        {
                            // Check the arguments format is correct against any specified format.
                            checkArgumentFormat(optionInfo, matchedArg);

                            // Store the argument against its option (regardless of its format).
                            options.put(matchedOption, matchedArg);

                            // The argument to this flag has already been supplied to it. Do not swallow the
                            // next command line argument as an argument to this flag.
                            expectingArgs = false;
                        }
                    }
                }
                else // No matching option was found.
                {
                    // Add this to the list of parsing errors if errors on unkowns is being used.
                    if (errorsOnUnknowns)
                    {
                        parsingErrors.add("Option " + matchedOption + " is not a recognized option.\n");
                    }
                }
            }

            // The command line argument did not being with a '-' so it is an argument to the previous flag or it
            // is a free argument.
            else
            {
                // Check if a previous flag is expecting to swallow this next argument as its argument.
                if (expectingArgs)
                {
                    // Get the option info for the option waiting for arguments.
                    CommandLineOption optionInfo = optionMap.get(optionExpectingArgs);

                    // Check the arguments format is correct against any specified format.
                    checkArgumentFormat(optionInfo, arg1);

                    // Store the argument against its option (regardless of its format).
                    options.put(optionExpectingArgs, arg1);

                    // Clear the expecting args flag now that the argument has been swallowed.
                    expectingArgs = false;
                    optionExpectingArgs = null;
                }

                // This command line option is not an argument to any option. Add it to the set of 'free' options.
                else
                {
                    // Get the option info for the free option, if there is any.
                    CommandLineOption optionInfo = optionMap.get(Integer.toString(free));

                    if (optionInfo != null)
                    {
                        // Check the arguments format is correct against any specified format.
                        checkArgumentFormat(optionInfo, arg1);
                    }

                    // Add to the list of free options.
                    options.put(Integer.toString(free), arg1);

                    // Move on to the next free argument.
                    free++;
                }
            }
        }

        // Scan through all the specified options to check that all mandatory options have been set and that all flags
        // that were not set are set to false in the set of properties.
        for (CommandLineOption optionInfo : optionMap.values())
        {
            // Check if this is a flag.
            if (!optionInfo.expectsArgs)
            {
                // Check if the flag is not set in the properties and set it to false if so.
                if (!options.containsKey(optionInfo.option))
                {
                    options.put(optionInfo.option, "false");
                }
            }

            // Check if this is a mandatory option and was not set.
            else if (optionInfo.mandatory && !options.containsKey(optionInfo.option))
            {
                // Create an error for the missing option.
                parsingErrors.add("Option " + optionInfo.option + " is mandatory but not was not specified.\n");
            }
        }

        // Check if there were any errors.
        if (!parsingErrors.isEmpty())
        {
            // Throw an illegal argument exception to signify that there were parsing errors.
            throw new IllegalArgumentException();
        }

        // Convert any name/value pairs in the free arguments into properties in the parsed options.
        trailingProperties = takeFreeArgsAsProperties(options, 1);

        parsedProperties = options;

        return options;
    }

    /**
     * If a command line has been parsed, calling this method sets all of its free arguments that were name=value pairs
     * on the specified properties.
     *
     * @param properties The property set to add the name=value pairs to.
     */
    public void addTrailingPairsToProperties(Properties properties)
    {
        if (trailingProperties != null)
        {
            for (Object propKey : trailingProperties.keySet())
            {
                String name = (String) propKey;
                String value = trailingProperties.getProperty(name);

                properties.setProperty(name, value);
            }
        }
    }

    /**
     * If a command line has been parsed, calling this method sets all of its options that were set to the specified
     * properties.
     *
     * @param properties The property set to the options to.
     */
    public void addOptionsToProperties(Properties properties)
    {
        if (parsedProperties != null)
        {
            for (Object propKey : parsedProperties.keySet())
            {
                String name = (String) propKey;
                String value = parsedProperties.getProperty(name);

                // This filters out all trailing items.
                if (!name.matches("^[0-9]+$"))
                {
                    properties.setProperty(name, value);
                }
            }
        }
    }

    /**
     * Resets this command line parser after it has been used to parse a command line. This method will only need to be
     * called to use this parser a second time which is not likely seeing as a command line is usually only specified
     * once. However, it is exposed as a public method for the rare case where this may be done.
     *
     *
Cleans the internal state of this parser, removing all stored errors and information about the options in
     * force.
     */
    public void reset()
    {
        parsingErrors = new ArrayList<String>();
        parsedProperties = null;
    }

    /**
     * Adds the option to list of available command line options.
     *
     * @param option       The option to add as an available command line option.
     * @param comment      A comment for the option.
     * @param argument     The text that appears after the option in the usage string.
     * @param mandatory    When true, indicates that this option is mandatory.
     * @param formatRegexp The format that the argument must take, defined as a regular expression.
     */
    protected void addOption(String option, String comment, String argument, boolean mandatory, String formatRegexp)
    {
        // Check if usage text has been set in which case this option is expecting arguments.
        boolean expectsArgs = (!((argument == null) || "".equals(argument)));

        // Add the option to the map of command line options.
        CommandLineOption opt = new CommandLineOption(option, expectsArgs, comment, argument, mandatory, formatRegexp);
        optionMap.put(option, opt);
    }

    /**
     * Converts the free arguments into property declarations. After parsing the command line the free arguments are
     * numbered from 1, such that the parsed properties contain values for the keys "1", "2", ... This method converts
     * any free arguments declared using the 'name=value' syntax into properties with key 'name', value 'value'.
     *
     *
For example the comand line:
     *
     *

     * ... debug=true
     *

     *
     *
After parsing has properties:
     *
     *
[[1, debug=true]]

     *
     *
After applying this method the properties are:
     *
     *
[[1, debug=true], [debug, true]]

     *
     * @param  properties The parsed command line properties.
     * @param  from       The free argument index to convert to properties from.
     *
     * @return The parsed command line properties, with free argument name value pairs too.
     */
    private Properties takeFreeArgsAsProperties(Properties properties, int from)
    {
        Properties result = new Properties();

        for (int i = from; true; i++)
        {
            String nextFreeArg = properties.getProperty(Integer.toString(i));

            // Terminate the loop once all free arguments have been consumed.
            if (nextFreeArg == null)
            {
                break;
            }

            // Split it on the =, strip any whitespace and set it as a system property.
            String[] nameValuePair = nextFreeArg.split("=");

            if (nameValuePair.length == 2)
            {
                result.setProperty(nameValuePair[0], nameValuePair[1]);
            }
        }

        return result;
    }

    /**
     * Checks the format of an argument to an option against its specified regular expression format if one has been
     * set. Any errors are added to the list of parsing errors.
     *
     * @param optionInfo The command line option information for the option which is havings its argument checked.
     * @param matchedArg The string argument to the option.
     */
    private void checkArgumentFormat(CommandLineOption optionInfo, CharSequence matchedArg)
    {
        // Check if this option enforces a format for its argument.
        if (optionInfo.argumentFormatRegexp != null)
        {
            Pattern pattern = Pattern.compile(optionInfo.argumentFormatRegexp);
            Matcher argumentMatcher = pattern.matcher(matchedArg);

            // Check if the argument does not meet its required format.
            if (!argumentMatcher.matches())
            {
                // Create an error for this badly formed argument.
                parsingErrors.add("The argument to option " + optionInfo.option +
                    " does not meet its required format.\n");
            }
        }
    }

    /**
     * Holds information about a command line options. This includes what its name is, whether or not it is a flag,
     * whether or not it is mandatory, what its user comment is, what its argument reminder text is and what its regular
     * expression format is.
     *
     *

CRC Card

     *
ResponsibilitiesCollaborations
     *
Hold details of a command line option.
     *

     *
     * @author Rupert Smith
     */
    protected static class CommandLineOption
    {
        /** Holds the text for the flag to match this argument with. */
        public String option;

        /** Holds a string describing how to use this command line argument. */
        public String argument;

        /** Flag that determines whether or not this command line argument can take arguments. */
        public boolean expectsArgs;

        /** Holds a short comment describing what this command line argument is for. */
        public String comment;

        /** Flag that determines whether or not this is an mandatory command line argument. */
        public boolean mandatory;

        /** A regular expression describing what format the argument to this option muist have. */
        public String argumentFormatRegexp;

        /**
         * Create a command line option object that holds specific information about a command line option.
         *
         * @param option       The text that matches the option.
         * @param expectsArgs  Whether or not the option expects arguments. It is a flag if this is false.
         * @param comment      A comment explaining how to use this option.
         * @param argument     A short reminder of the format of the argument to this option/
         * @param mandatory    Set to true if this option is mandatory.
         * @param formatRegexp The regular expression that the argument to this option must meet to be valid.
         */
        public CommandLineOption(String option, boolean expectsArgs, String comment, String argument, boolean mandatory,
            String formatRegexp)
        {
            this.option = option;
            this.expectsArgs = expectsArgs;
            this.comment = comment;
            this.argument = argument;
            this.mandatory = mandatory;
            this.argumentFormatRegexp = formatRegexp;
        }
    }
}


/*
 * Copyright The Sett Ltd, 2005 to 2014.
 *
 * Licensed 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 com.thesett.common.util;

import java.util.Iterator;

/**
 * Comparisons provides helper methods to compare collections of things. Intended for writing tests, hence the use of
 * error messages as return values, rather than booleans or lists or iterators of differences.
 *
 *

CRC Card

 *
ResponsibilitiesCollaborations
 *

 *
 * @author Rupert Smith
 * @todo   Replace error messages with lists of differences, and provide a seperate test utility for converting these
 *         into error messages. Probably involves using a Pair implementation to return pairs of differences.
 */
public class Comparisons
{
    /**
     * Walks down two iterators, comparing them element by element, using the equals method.
     *
     * @param  iterator         The first iterator to compare.
     * @param  expectedIterator The second iterator to compare.
     * @param  mapping          A mapping between the types of the iterators.
     * @param                The type of the left iterator.
     * @param  <T>              The type of the right iterator.
     *
     * @return Any mismatches encountered in a string, or the empty string if none found.
     */
    public static <T, U> String compareIterators(Iterator iterator, Iterator<T> expectedIterator,
        Function mapping)
    {
        String errorMessage = "";

        while (iterator.hasNext())
        {
            U next = iterator.next();
            T nextMapped = mapping.apply(next);
            T nextExpected = expectedIterator.next();

            if (!nextMapped.equals(nextExpected))
            {
                errorMessage += "Expecting " + nextExpected + " but got " + nextMapped;
            }
        }

        return errorMessage;
    }
}


/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * PTStemmer.java
 * Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.stemmers;

import java.io.File;
import java.util.Enumeration;
import java.util.Vector;

import ptstemmer.exceptions.PTStemmerException;
import ptstemmer.implementations.OrengoStemmer;
import ptstemmer.implementations.PorterStemmer;
import ptstemmer.implementations.SavoyStemmer;
import ptstemmer.support.PTStemmerUtilities;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.SelectedTag;
import weka.core.Tag;
import weka.core.Utils;

/**

 * A wrapper for PTStemmer (developed by Pedro Oliveira):

 * http://code.google.com/p/ptstemmer/
 *

 *

 * Valid options are:

 *
 *
 -S <ORENGO|PORTER|SAVOY>
 *  The type of stemmer algorithm to use:
 *  ORENGO = Orengo
 *  PORTER = Porter
 *  SAVOY = Savoy
 *  (default: ORENGO)

 *
 *
 -N <file>
 *  The file with the named entities to ignore (optional).
 *  File format: simple text file with one entity per line.
 *  (default: none)
 *

 *
 *
 -W <file>
 *  The file with the stopwords (optional).
 *  File format: simple text file with one stopword per line.
 *  (default: none)
 *

 *
 *
 -C <int>
 *  The size of the cache. Disable with 0.
 *  (default: 1000)
 *

 *

 *
 * @author  FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 1179 $
 */
public class PTStemmer
  implements Stemmer, OptionHandler {

  /** for serialization. */
  static final long serialVersionUID = -6113024782588197L;

  /** orengo stemmer. */
  public static final int STEMMER_ORENGO = 0;

  /** porter stemmer. */
  public static final int STEMMER_PORTER = 1;

  /** savoy stemmer. */
  public static final int STEMMER_SAVOY = 2;

  /** stemmers. */
  public static final Tag[] TAGS_STEMMERS = {
    new Tag(STEMMER_ORENGO, "orengo", "Orengo"),
    new Tag(STEMMER_PORTER, "porter", "Porter"),
    new Tag(STEMMER_SAVOY, "savoy", "Savoy")
  };

  /** the type of stemmer to use. */
  protected int m_Stemmer = STEMMER_ORENGO;

  /** the named entities. */
  protected File m_NamedEntities = new File(".");

  /** the stopwords. */
  protected File m_Stopwords = new File(".");

  /** the cache size. */
  protected int m_Cache = 1000;

  /** the actual stemmer. */
  protected ptstemmer.Stemmer m_ActualStemmer;

  /**
   * Returns a string describing the stemmer.
   *
   * @return 		a description suitable for displaying in the
   * 			explorer/experimenter gui
   */
  public String globalInfo() {
    return
        "A wrapper for PTStemmer (developed by Pedro Oliveira):\n"
      + "http://code.google.com/p/ptstemmer/";
  }

  /**
   * Returns an enumeration describing the available options.
   *
   * @return an enumeration of all the available options.
   */
  public Enumeration listOptions() {
    Vector<Option>	result;
    String		desc;
    SelectedTag		tag;
    int			i;

    result = new Vector<Option>();
    desc  = "";
    for (i = 0; i < TAGS_STEMMERS.length; i++) {
      tag = new SelectedTag(TAGS_STEMMERS[i].getID(), TAGS_STEMMERS);
      desc  +=   "\t" + tag.getSelectedTag().getIDStr()
      	       + " = " + tag.getSelectedTag().getReadable()
      	       + "\n";
    }
    result.addElement(new Option(
	"\tThe type of stemmer algorithm to use:\n"
	+ desc
	+ "\t(default: " + new SelectedTag(STEMMER_ORENGO, TAGS_STEMMERS) + ")",
	"S", 1, "-S " + Tag.toOptionList(TAGS_STEMMERS)));

    result.addElement(new Option(
        "\tThe file with the named entities to ignore (optional).\n"
        + "\tFile format: simple text file with one entity per line.\n"
        + "\t(default: none)\n",
        "N", 1, "-N <file>"));

    result.addElement(new Option(
        "\tThe file with the stopwords (optional).\n"
        + "\tFile format: simple text file with one stopword per line.\n"
        + "\t(default: none)\n",
        "W", 1, "-W <file>"));

    result.addElement(new Option(
        "\tThe size of the cache. Disable with 0.\n"
        + "\t(default: 1000)\n",
        "C", 1, "-C <int>"));

    return result.elements();
  }

  /**
   * Parses the options.

   *

   * Valid options are:

   *
   *
 -S <ORENGO|PORTER|SAVOY>
   *  The type of stemmer algorithm to use:
   *  ORENGO = Orengo
   *  PORTER = Porter
   *  SAVOY = Savoy
   *  (default: ORENGO)

   *
   *
 -N <file>
   *  The file with the named entities to ignore (optional).
   *  File format: simple text file with one entity per line.
   *  (default: none)
   *

   *
   *
 -W <file>
   *  The file with the stopwords (optional).
   *  File format: simple text file with one stopword per line.
   *  (default: none)
   *

   *
   *
 -C <int>
   *  The size of the cache. Disable with 0.
   *  (default: 1000)
   *

   *

   *
   * @param options	the options to parse
   * @throws Exception 	if parsing fails
   */
  public void setOptions(String[] options) throws Exception {
    String	tmpStr;

    tmpStr = Utils.getOption('S', options);
    if (tmpStr.length() != 0)
      setStemmer(new SelectedTag(tmpStr, TAGS_STEMMERS));
    else
      setStemmer(new SelectedTag(STEMMER_ORENGO, TAGS_STEMMERS));

    tmpStr = Utils.getOption('N', options);
    if (tmpStr.length() != 0)
      setNamedEntities(new File(tmpStr));
    else
      setNamedEntities(new File("."));

    tmpStr = Utils.getOption('W', options);
    if (tmpStr.length() != 0)
      setStopwords(new File(tmpStr));
    else
      setStopwords(new File("."));

    tmpStr = Utils.getOption('C', options);
    if (tmpStr.length() != 0)
      setCache(Integer.parseInt(tmpStr));
    else
      setCache(1000);
  }

  /**
   * Gets the current settings of the classifier.
   *
   * @return an array of strings suitable for passing to setOptions
   */
  public String[] getOptions() {
    Vector<String>	result;

    result = new Vector<String>();

    result.add("-S");
    result.add("" + getStemmer());

    result.add("-N");
    result.add("" + getNamedEntities());

    result.add("-W");
    result.add("" + getStopwords());

    result.add("-C");
    result.add("" + getCache());

    return result.toArray(new String[result.size()]);
  }

  /**
   * Sets the stemmer type to use
   *
   * @param value 	the type
   */
  public void setStemmer(SelectedTag value) {
    if (value.getTags() == TAGS_STEMMERS) {
      m_Stemmer = value.getSelectedTag().getID();
      invalidate();
    }
  }

  /**
   * Gets the stemmer type to use.
   *
   * @return 		the type
   */
  public SelectedTag getStemmer() {
    return new SelectedTag(m_Stemmer, TAGS_STEMMERS);
  }

  /**
   * Returns the tip text for this property
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the explorer/experimenter gui
   */
  public String stemmerTipText() {
    return "Sets the type of stemmer to use.";
  }

  /**
   * Sets the file for the named entities.
   *
   * @param value	the file.
   */
  public void setNamedEntities(File value) {
    m_NamedEntities = value;
    invalidate();
  }

  /**
   * Gets the file for the named entities.
   *
   * @return 		the file.
   */
  public File getNamedEntities() {
    return m_NamedEntities;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the explorer/experimenter gui
   */
  public String namedEntitiesTipText() {
    return
        "The file with the named entities to ignore (format: text file with "
      + "one entity per line); gets ignored if pointing to a directory.";
  }

  /**
   * Sets the file for the stopwords.
   *
   * @param value	the file.
   */
  public void setStopwords(File value) {
    m_Stopwords = value;
    invalidate();
  }

  /**
   * Gets the file for the named entities.
   *
   * @return 		the file.
   */
  public File getStopwords() {
    return m_Stopwords;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the explorer/experimenter gui
   */
  public String stopwordsTipText() {
    return
        "The file with the stopwords to ignore (format: text file with "
      + "one stopword per line); gets ignored if pointing to a directory.";
  }

  /**
   * Sets the size of the cache.
   *
   * @param value	the size.
   */
  public void setCache(int value) {
    m_Cache = value;
    invalidate();
  }

  /**
   * Gets the size of the cache.
   *
   * @return 		the size.
   */
  public int getCache() {
    return m_Cache;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the explorer/experimenter gui
   */
  public String cacheTipText() {
    return "The size of the cache (use 0 to disable caching).";
  }

  /**
   * Invalidates the stemmer object.
   */
  protected void invalidate() {
    m_ActualStemmer = null;
  }

  /**
   * Returns the stemmer to use.
   *
   * @return		the stemmer to use
   */
  protected synchronized ptstemmer.Stemmer getActualStemmer() throws PTStemmerException {
    if (m_ActualStemmer == null) {
      // stemmer algorithm
      if (m_Stemmer == STEMMER_ORENGO)
	m_ActualStemmer = new OrengoStemmer();
      else if (m_Stemmer == STEMMER_PORTER)
	m_ActualStemmer = new PorterStemmer();
      else if (m_Stemmer == STEMMER_SAVOY)
	m_ActualStemmer = new SavoyStemmer();
      else
	throw new IllegalStateException("Unhandled stemmer type: " + m_Stemmer);

      // named entities
      if (!m_NamedEntities.isDirectory())
	m_ActualStemmer.ignore(PTStemmerUtilities.fileToSet(m_NamedEntities.getAbsolutePath()));

      // stopwords
      if (!m_Stopwords.isDirectory())
	m_ActualStemmer.ignore(PTStemmerUtilities.fileToSet(m_Stopwords.getAbsolutePath()));

      // cache
      if (m_Cache > 0)
	m_ActualStemmer.enableCaching(m_Cache);
      else
	m_ActualStemmer.disableCaching();
    }

    return m_ActualStemmer;
  }

  /**
   * Returns the stemmed version of the given word.
   * Word is converted to lower case before stemming.
   *
   * @param word 	a string consisting of a single word
   * @return 		the stemmed word
   */
  public String stem(String word) {
	String ret = null;
    try {
	  ret = getActualStemmer().getWordStem(word);
	}
    catch (PTStemmerException e) {
      e.printStackTrace();
	}
    return ret;
  }

  /**
   * returns a string representation of the stemmer.
   *
   * @return a string representation of the stemmer
   */
  public String toString() {
    return getClass().getName();
  }

  /**
   * Returns the revision string.
   *
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 1179 $");
  }

  /**
   * Runs the stemmer with the given options.
   *
   * @param args      the options
   */
  public static void main(String[] args) {
    try {
      Stemming.useStemmer(new PTStemmer(), args);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}


/*
 * Copyright The Sett Ltd, 2005 to 2014.
 *
 * Licensed 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 com.thesett.aima.logic.fol.prolog.builtins;

import com.thesett.aima.logic.fol.NumericType;
import com.thesett.aima.logic.fol.Term;

/**
 * BuiltInUtils provides some helper methods for evaluating built-in operators and predicates.
 *
 *

CRC Card

 *
 Responsibilities  Collaborations
 *
 Evaluate terms and check that they are valid numbers.
 *

 *
 * @author Rupert Smith
 */
public class BuiltInUtils
{
    /**
     * Evaluates a term by invoking its {@link Term#getValue()} method (which may cause a recursive evaluation, for
     * example, in the case of arithmetic expressions), and checks that the result is a fully instantiated numeric
     * value.
     *
     * @param  numeric The term to evaluate as a number.
     *
     * @return The term as a number.
     */
    static NumericType evaluateAsNumeric(Term numeric)
    {
        // Evaluate the expression.
        Term expressionValue = numeric.getValue();

        // Ensure that the result of evaluating the expression is a number.
        if (expressionValue.isVar())
        {
            throw new IllegalStateException("instantiation_error, 'is' expects a fully instantiated term to unify against.");
        }

        if (!expressionValue.isNumber())
        {
            throw new IllegalStateException("arithmetic_error, 'is' expectes a numeric expression to unify against.");
        }

        return (NumericType) expressionValue;
    }
}


/*
 * Copyright The Sett Ltd, 2005 to 2014.
 *
 * Licensed 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 com.thesett.aima.attribute.impl;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.thesett.aima.state.BaseType;
import com.thesett.aima.state.InfiniteValuesException;
import com.thesett.aima.state.RandomInstanceFactory;
import com.thesett.aima.state.Type;

/**
 * Implements a float range type. This allows an interval of floats to be defined as a named type.
 *
 *

CRC Card

 *
 Responsibilities  Collaborations
 *
 Maintain a set of named float range types.
 *
 Provide a default value that lies in the range.
 *
 Check a float value against the range type.
 *

 *
 * @author Rupert Smith
 */
public class FloatRangeType extends BaseType<Float> implements Type<Float>, RandomInstanceFactory<Float>, Serializable
{
    /** Holds all of the named float range types that have been defined. */
    private static final Map<String, FloatRangeType> FLOAT_RANGE_TYPES = new HashMap<String, FloatRangeType>();

    /** Holds the minimum value an instance of this type can take. */
    private final float minValue;

    /** Holds the maximum value an instance of this type can take. */
    private final float maxValue;

    /** Used to hold the name of this float range type. */
    private final String typeName;

    /**
     * Creates an instance of a float range type with the specified name and min and max values.
     *
     * @param name The name of the type.
     * @param min  The minimum value an instance of the type can take.
     * @param max  The maximum value an instance of the type can take.
     */
    private FloatRangeType(String name, float min, float max)
    {
        typeName = name;
        minValue = min;
        maxValue = max;
    }

    /**
     * Gets the named float range type, if one exists.
     *
     * @param  name The name of the float range type to get.
     *
     * @return The named float range type, or <tt>null</tt> if none matching the name exists.
     */
    public static FloatRangeType getInstance(String name)
    {
        synchronized (FLOAT_RANGE_TYPES)
        {
            return FLOAT_RANGE_TYPES.get(name);
        }
    }

    /**
     * Creates a new float range type with the specified name, if it does not already exist.
     *
     * @param  name The name of the float range type to create.
     * @param  min  The minimum value that an instance of the type can take.
     * @param  max  The maximum value that an instance of teh type can take.
     *
     * @return A new float range type.
     *
     * @throws IllegalArgumentException If min is not less than or equal to max, or the named type already exists.
     */
    public static Type createInstance(String name, float min, float max)
    {
        // Ensure that min is less than or equal to max.
        if (min > max)
        {
            throw new IllegalArgumentException("'min' must be less than or equal to 'max'.");
        }

        synchronized (FLOAT_RANGE_TYPES)
        {
            // Add the newly created type to the map of all types.
            FloatRangeType newType = new FloatRangeType(name, min, max);

            // Ensure that the named type does not already exist, unless it has an identical definition already, in which
            // case the old definition can be re-used and the new one discarded.
            FloatRangeType oldType = FLOAT_RANGE_TYPES.get(name);

            if ((oldType != null) && !oldType.equals(newType))
            {
                throw new IllegalArgumentException("The type '" + name + "' already exists and cannot be redefined.");
            }
            else if ((oldType != null) && oldType.equals(newType))
            {
                return oldType;
            }
            else
            {
                FLOAT_RANGE_TYPES.put(name, newType);

                return newType;
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     *
The default value of a float range type is zero when zero lies between 'min' and 'max', otherwise it is
     * 'min'.
     */
    public Float getDefaultInstance()
    {
        return ((0 >= minValue) && (0 <= maxValue)) ? 0 : minValue;
    }

    /**
     * {@inheritDoc}
     *
     *
A random value of this type lies between 'min' and 'max'.
     */
    public Float createRandomInstance()
    {
        return random.nextFloat() + (minValue * (maxValue - minValue));
    }

    /** {@inheritDoc} */
    public String getName()
    {
        return typeName;
    }

    /** {@inheritDoc} */
    public Class<Float> getBaseClass()
    {
        return Float.class;
    }

    /** {@inheritDoc} */
    public String getBaseClassName()
    {
        return getBaseClass().getName();
    }

    /** {@inheritDoc} */
    public int getNumPossibleValues()
    {
        return -1;
    }

    /**
     * Checks a float value against this type, to determine if it is a valid instance of the type.
     *
     * @param  value The value to check.
     *
     * @return <tt>true</tt> if the value lies in the types range, <tt>false</tt> if it does not.
     */
    public boolean isInstance(float value)
    {
        return (value >= minValue) && (value <= maxValue);
    }

    /** {@inheritDoc} */
    public Set<Float> getAllPossibleValuesSet() throws InfiniteValuesException
    {
        throw new InfiniteValuesException("FloatRangeType has too many values to enumerate.", null);
    }

    /** {@inheritDoc} */
    public Iterator<Float> getAllPossibleValuesIterator() throws InfiniteValuesException
    {
        throw new InfiniteValuesException("FloatRangeType has too many values to enumerate.", null);
    }

    /**
     * Checks two float range types for equality, based on their name and float range.
     *
     * @param  o The object to compare to.
     *
     * @return <tt>true</tt> if the comparator is an float range type with the same name and range as this one.
     */
    public boolean equals(Object o)
    {
        if (this == o)
        {
            return true;
        }

        if (!(o instanceof FloatRangeType))
        {
            return false;
        }

        FloatRangeType that = (FloatRangeType) o;

        return (Float.compare(that.maxValue, maxValue) == 0) && (Float.compare(that.minValue, minValue) == 0) &&
            !((typeName != null) ? (!typeName.equals(that.typeName)) : (that.typeName != null));
    }

    /**
     * Provides a hashcode based on the name, and float range of this type.
     *
     * @return A hashcode based on the name, and float range of this type.
     */
    public int hashCode()
    {
        int result;
        result = ((minValue != +0.0f) ? Float.floatToIntBits(minValue) : 0);
        result = (31 * result) + ((maxValue != +0.0f) ? Float.floatToIntBits(maxValue) : 0);
        result = (31 * result) + ((typeName != null) ? typeName.hashCode() : 0);

        return result;
    }
}


/*
 * Copyright The Sett Ltd, 2005 to 2014.
 *
 * Licensed 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 com.thesett.common.swing.workpanel;

import java.beans.PropertyChangeEvent;

import com.thesett.common.state.BaseState;

/**
 * WorkPanelState models the possible states of a work panel.
 *
 *
A work panel can have three states, not intialized, ready and not saved. A newly created work panel that has not
 * loaded its data or built its user interface will be in the not initialized state. A work panel that has loaded its
 * data and built its user interface will be in the ready state. A work panel that has modified its data but not yet
 * commited the changes to the server will be in the not intialized state.
 *
 *

CRC Card

 *
ResponsibilitiesCollaborations
 *
Maintain work panel state
 *
Notify listners of changes to state
 *

 *
 * @author Rupert Smith
 */
public class WorkPanelState extends BaseState
{
    /** Used for logging. */
    /* private static final Logger log = Logger.getLogger(WorkPanelState.class.getName()); */

    /** Constant representing the initial state of the work panel before it is ready to be used. */
    public static final String NOT_INITIALIZED = "Not Initialized";

    /** Constant representing the state of the work panel when its data is loaded and it is ready to be edited. */
    public static final String READY = "Ready";

    /**
     * Constant representing the state of the work panel when its data has been changed but the changes have not yet
     * been commited or canceled.
     */
    public static final String NOT_SAVED = "Not Saved";

    /** Represents the initial state of a work panel before it is ready to use. */
    private String state = NOT_INITIALIZED;

    /**
     * Sets the state of the work panel. State must be one of the defined constants: NOT_INITIALIZED, READY or
     * NOT_SAVED.
     *
     *
Listneres are notified of the state change if it is a change.
     *
     * @param state The new state to set.
     */
    public void setState(String state)
    {
        /*log.fine("void setState(String): called");*/
        /*log.fine("state is " + state);*/

        // Check if the state has changed
        if (!this.state.equals(state))
        {
            String oldState = this.state;

            // Keep the new state
            this.state = state;

            // Notify any listeners of the change in state
            firePropertyChange(new PropertyChangeEvent(this, "state", oldState, state));
        }
    }

    /**
     * Returns the state of the work panel.
     *
     * @return The current state of the work panel.
     */
    public String getState()
    {
        return state;
    }
}