/*
 Copyright 2004-2008 Paul R. Holser, Jr.  All rights reserved.
 Licensed under the Academic Free License version 3.0
 */

package joptsimple;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.List;

import joptsimple.util.KeyValuePair;

/**
 * <p>Parses command line arguments according to GNU's conventions.</p>
 *
 * <p>This parser supports short options and long options.</p>
 *
 * <ul>
 *   <li><dfn>Short options</dfn> begin with a single hyphen ("<kbd>-</kbd>") followed
 *   by a single letter or digit, or question mark ("<kbd>?</kbd>"), or dot
 *   ("<kbd>.</kbd>").</li>
 *
 *   <li>Short options can accept single arguments.  The argument can be made required or
 *   optional.  The option's argument can occur:
 *     <ul>
 *       <li>in the slot after the option, as in <kbd>-d /tmp</kbd></li>
 *       <li>right up against the option, as in <kbd>-d/tmp</kbd></li>
 *       <li>right up against the option separated by an equals sign (<kbd>"="</kbd>),
 *       as in <kbd>-d=/tmp</kbd></li>
 *     </ul>
 *   To specify <var>n</var> arguments for an option, specify the option <var>n</var>
 *   times, once for each argument, as in <kbd>-d /tmp -d /var -d /opt</kbd>; or, when
 *   using the {@linkplain ArgumentAcceptingOptionSpec#withValuesSeparatedBy(char)
 *   "separated values"} clause of the "fluent interface" (see below), give multiple
 *   values separated by a given character as a single argument to the option.
 *   </li>
 *
 *   <li>Short options can be clustered -- so that <kbd>-abc</kbd> is treated as
 *   <kbd>-a -b -c</kbd> -- if none of those options can accept arguments.</li>
 *
 *   <li>An argument consisting only of two hyphens (<kbd>"--"</kbd>) signals that the
 *   remaining arguments are to be treated as non-options.</li>
 *
 *   <li>An argument consisting only of a single hyphen is considered a non-option
 *   argument (though it can be an argument of an option).  Many Unix programs treat
 *   single hyphens as stand-ins for the standard input or standard output streams.</li>
 *
 *   <li><dfn>Long options</dfn> begin with two hyphens (<kbd>"--"</kbd>), followed
 *   by multiple letters, digits, hyphens, question marks, or dots.  A hyphen cannot be
 *   the first character of a long option specification when configuring the parser.</li>
 *
 *   <li>You can abbreviate long options, so long as the abbreviation is unique.</li>
 *
 *   <li>Long options can accept single arguments.  The argument can be made required or
 *   optional.  The option's argument can occur:
 *     <ul>
 *       <li>in the slot after the option, as in <kbd>--directory /tmp</kbd></li>
 *       <li>right up against the option separated by an equals sign (<kbd>"="</kbd>),
 *       as in <kbd>--directory=/tmp</kbd>
 *     </ul>
 *   Specify multiple arguments for a long option in the same manner as for short options
 *   (see above).
 *   </li>
 *
 *   <li>You can use a single hyphen (<kbd>"-"</kbd>) instead of a double hyphen
 *   (<kbd>"--"</kbd>) for a long option.</li>
 *
 *   <li>The option <kbd>-W</kbd> is reserved.  If you tell the parser to {@linkplain
 *   #recognizeAlternativeLongOptions(boolean) recognize alternative long options}, then
 *   it will treat, for example, <kbd>-W foo=bar</kbd> as the long option
 *   <kbd>foo</kbd> with argument <kbd>bar</kbd>, as though you had written
 *   <kbd>--foo=bar</kbd>.</li>
 *
 *   <li>You can specify <kbd>-W</kbd> as a valid short option, or use it as an
 *   abbreviation for a long option, but {@linkplain
 *   #recognizeAlternativeLongOptions(boolean) recognizing alternative long options} will
 *   always supersede this behavior.</li>
 *
 *   <li>You can specify a given short or long option multiple times on a single command
 *   line.  The parser collects any arguments specified for those options as a list.</li>
 *
 *   <li>If the parser detects an option whose argument is optional, and the next argument
 *   "looks like" an option, that argument is not treated as the argument to the option,
 *   but as a potentially valid option.  If, on the other hand, the optional argument is
 *   typed as a derivative of {@link Number}, then that argument is treated as the
 *   negative number argument of the option, even if the parser recognizes the
 *   corresponding numeric option.  For example:
 *   <pre>
 *     <code>
 *     parser.accepts( "a" ).withOptionalArg().ofType( Integer.class );
 *     parser.accepts( "2" );
 *     OptionSet options = parser.parse( new String[] { "-a", "-2" } );
 *     </code>
 *   </pre>
 *   In this case, the option set contains <kbd>"a"</kbd> with argument <kbd>-2</kbd>,
 *   not both <kbd>"a"</kbd> and <kbd>"2"</kbd>.  Swapping the elements in the
 *   <var>args</var> array gives the latter.</li>
 * </ul>
 *
 * <p>There are two ways to tell the parser what options to recognize:</p>
 *
 * <ol>
 *   <li>A "fluent interface"-style API for specifying options, available since
 *   version 2.  Sentences in this internal domain-specific language begin with a call to
 *   {@link #accepts(String) accepts} or {@link #acceptsAll(List) acceptsAll} methods on
 *   the ensuing chain of objects describe whether the options can take an argument,
 *   whether the argument is required or optional, and to what type arguments of the
 *   options should be converted.</li>
 *
 *   <li>Since version 1, a more concise way of specifying short options has been to use
 *   the special {@linkplain #OptionParser(String) constructor}.  Arguments of options
 *   specified in this manner will be of type {@link String}.  Here are the rules for the
 *   format of the specification strings this constructor accepts:
 *
 *     <ul>
 *         <li>Any letter or digit is treated as an option character.</li>
 *
 *         <li>If an option character is followed by a single colon (<kbd>":"</kbd>),
 *         then the option requires an argument.</li>
 *
 *         <li>If an option character is followed by two colons (<kbd>"::"</kbd>), then
 *         the option accepts an optional argument.</li>
 *
 *         <li>Otherwise, the option character accepts no argument.</li>
 *
 *         <li>If the option specification string begins with a plus sign
 *         (<kbd>"+"</kbd>), the parser will behave "POSIX-ly correct".</li>
 *
 *         <li>If the option specification string contains the sequence <kbd>"W;"</kbd>
 *         (capital W followed by a semicolon), the parser will recognize the alternative
 *         form of long options.</li>
 *     </ul>
 *   </li>
 * </ol>
 *
 * <p>Each of the options in a list of options given to {@link #acceptsAll(List)
 * acceptsAll} is treated as a synonym of the others.  For example:
 *   <pre>
 *     <code>
 *     parser.acceptsAll(
 *         Arrays.asList( new String[] { "w", "interactive", "confirmation" } ) );
 *     OptionSet options = parser.parse( new String[] { "-w" } );
 *     </code>
 *   </pre>
 * In this case, <code>options.{@link OptionSet#has(String) has}</code> would answer
 * <code>true</code> when given arguments <kbd>"w"</kbd>, <kbd>"interactive"</kbd>, and
 * <kbd>"confirmation"</kbd>. The {@link OptionSet} would give the same responses to
 * these arguments for its other methods as well.</p>
 *
 * <p>By default, as with GNU <code>getopt()</code>, the parser allows intermixing of
 * options and non-options.  If, however, the parser has been created to be "POSIX-ly
 * correct", then the first argument that does not look lexically like an option, and
 * is not a required argument of a preceding option, signals the end of options.
 * You can still bind optional arguments to their options using the abutting (for short
 * options) or <kbd>=</kbd> syntax.</p>
 *
 * <p>Unlike GNU <code>getopt()</code>, this parser does not honor the environment
 * variable <code>POSIXLY_CORRECT</code>.  "POSIX-ly correct" parsers are configured by
 * either:</p>
 *
 * <ol>
 *   <li>using the method {@link #posixlyCorrect(boolean)}, or</li>
 *
 *   <li>using the {@linkplain #OptionParser(String) constructor} with an argument whose
 *   first character is a plus sign (<kbd>"+"</kbd>)</li>
 * </ol>
 *
 * @since 1.0
 * @author <a href="mailto:pholser@alumni.rice.edu">Paul Holser</a>
 * @version $Id: OptionParser.java,v 1.7 2008/05/01 15:38:45 pholser Exp $
 * @see <a href="http://www.gnu.org/software/libc/manual">The GNU C Library </a>
 */
public class OptionParser {
    private final AbbreviationMap recognizedOptions = new AbbreviationMap();
    private OptionParserState state = OptionParserState.moreOptions( false );
    private boolean posixlyCorrect;

    /**
     * <p>Creates an option parser that initially recognizes no options, and does not
     * exhibit "POSIX-ly correct" behavior.</p>
     *
     * @since 1.0
     */
    public OptionParser() {
    }

    /**
     * <p>Creates an option parser and configures it to recognize the short options
     * specified in the given string.</p>
     *
     * <p>Arguments of options specified this way will be of type {@link String}.</p>
     *
     * @since 1.0
     * @param optionSpecification an option specification
     * @throws NullPointerException if <var>optionSpecification</var> is
     * <code>null</code>
     * @throws OptionException if the option specification contains illegal characters
     * or otherwise cannot be recognized
     */
    public OptionParser( String optionSpecification ) {
        new OptionSpecTokenizer( optionSpecification ).configure( this );
    }

    /**
     * <p>Tells the parser to recognize the given option.</p>
     *
     * <p>This method returns an instance of {@link OptionSpecBuilder} to allow the
     * formation of parser directives as sentences in an internal domain-specific
     * language.  For example:</p>
     *
     * <pre><code>
     *   OptionParser parser = new OptionParser();
     *   parser.<strong>accepts( "c" )</strong>.withRequiredArg().ofType( Integer.class );
     * </code></pre>
     *
     * <p>If no methods are invoked on the returned {@link OptionSpecBuilder}, then the
     * parser treats the option as accepting no argument.</p>
     *
     * @since 2.0
     * @param option the option to recognize
     * @return an object that can be used to flesh out more detail about the option
     * @throws OptionException if the option contains illegal characters
     * @throws NullPointerException if the option is <code>null</code>
     */
    public OptionSpecBuilder accepts( String option ) {
        return acceptsAll( Collections.singletonList( option ) );
    }

    /**
     * <p>Tells the parser to recognize the given option.</p>
     *
     * @since 2.1
     * @see #accepts(String)
     * @param option the option to recognize
     * @param description a string that describes the purpose of the option.  This is
     * used when generating help information about the parser.
     * @return an object that can be used to flesh out more detail about the option
     * @throws OptionException if the option contains illegal characters
     * @throws NullPointerException if the option is <code>null</code>
     */
    public OptionSpecBuilder accepts( String option, String description ) {
        return acceptsAll( Collections.singletonList( option ), description );
    }

    /**
     * <p>Tells the parser to recognize the given options, and treat them as
     * synonymous.</p>
     *
     * @since 2.4
     * @see #accepts(String)
     * @param options the options to recognize and treat as synonymous
     * @return an object that can be used to flesh out more detail about the options
     * @throws OptionException if any of the options contain illegal characters
     * @throws NullPointerException if the option list or any of its elements are
     * <code>null</code>
     * @throws ClassCastException if any of the items in the option list is not a
     * {@link String}
     */
    public OptionSpecBuilder acceptsAll( List options ) {
        return acceptsAll( options, "" );
    }

    /**
     * <p>Tells the parser to recognize the given options, and treat them as
     * synonymous.</p>
     *
     * @since 2.4
     * @see #acceptsAll(List)
     * @param options the options to recognize and treat as synonymous
     * @param description a string that describes the purpose of the option.  This is
     * used when generating help information about the parser.
     * @return an object that can be used to flesh out more detail about the options
     * @throws OptionException if any of the options contain illegal characters
     * @throws NullPointerException if the option list or any of its elements are
     * <code>null</code>
     * @throws IllegalArgumentException if the option list is empty
     * @throws ClassCastException if any of the items in the option list is not a
     * {@link String}
     */
    public OptionSpecBuilder acceptsAll( List options, String description ) {
        if ( options.isEmpty() )
            throw new IllegalArgumentException( "acceptsAll needs at least one option" );

        ParserRules.checkLegalOptions( options );

        return new OptionSpecBuilder( this, options, description );
    }

    /**
     * <p>Tells the parser whether or not to behave "POSIX-ly correct"-ly.</p>
     *
     * @since 1.0
     * @param setting <code>true</code> if the parser should behave "POSIX-ly
     * correct"-ly
     * @deprecated Use {@link #posixlyCorrect(boolean)} instead.
     */
    public void setPosixlyCorrect( boolean setting ) {
        this.posixlyCorrect = setting;
        state = OptionParserState.moreOptions( setting );
    }

    /**
     * <p>Tells the parser whether or not to behave "POSIX-ly correct"-ly.</p>
     *
     * @since 2.4
     * @param setting <code>true</code> if the parser should behave "POSIX-ly
     * correct"-ly
     */
    public void posixlyCorrect( boolean setting ) {
        setPosixlyCorrect( setting );
    }

    boolean posixlyCorrect() {
        return posixlyCorrect;
    }

    /**
     * <p>Tells the parser either to recognize or ignore <kbd>"-W"</kbd>-style long
     * options.</p>
     *
     * @since 1.0
     * @param recognize <code>true</code> if the parser is to recognize the special style
     * of long options
     */
    public void recognizeAlternativeLongOptions( boolean recognize ) {
        if ( recognize )
            recognize( new AlternativeLongOptionSpec() );
        else
            recognizedOptions.remove(
                String.valueOf( ParserRules.RESERVED_FOR_EXTENSIONS ) );
    }

    void recognize( OptionSpec spec ) {
        recognizedOptions.putAll( spec.options(), spec );
    }

    /**
     * <p>Writes information about the options this parser recognizes to the given output
     * sink.</p>
     *
     * @since 2.1
     * @param sink the sink to write information to
     * @throws IOException if there is a problem writing to the sink
     * @throws NullPointerException if <var>sink</var> is <code>null</code>
     */
    public void printHelpOn( OutputStream sink ) throws IOException {
        OutputStreamWriter writer = null;

        try {
            writer = new OutputStreamWriter( sink );
            printHelpOn( writer );
        }
        finally {
            if ( writer != null )
                writer.close();
        }
    }

    /**
     * <p>Writes information about the options this parser recognizes to the given output
     * sink.</p>
     *
     * @since 2.1
     * @param sink the sink to write information to
     * @throws IOException if there is a problem writing to the sink
     * @throws NullPointerException if <var>sink</var> is <code>null</code>
     */
    public void printHelpOn( Writer sink ) throws IOException {
        sink.write(
            new OptionParserHelpFormatter().format( recognizedOptions.toJavaUtilMap() ) );
    }

    /**
     * <p>Parses the given command line arguments according to the option specifications
     * given to the parser.</p>
     *
     * @since 1.0
     * @param arguments arguments to parse
     * @return an {@link OptionSet} describing the parsed options, their arguments, and
     * any non-option arguments found
     * @throws OptionException if problems are detected while parsing
     * @throws NullPointerException if the argument list is <code>null</code>
     */
    public OptionSet parse( String[] arguments ) {
        ArgumentList argumentList = new ArgumentList( arguments );
        OptionSet detected = new OptionSet();

        while ( argumentList.hasMore() )
            state.handleArgument( this, argumentList, detected );

        reset();
        return detected;
    }

    void handleLongOptionToken( String candidate, ArgumentList arguments,
        OptionSet detected ) {

        KeyValuePair optionAndArgument = parseLongOptionWithArgument( candidate );

        if ( !isRecognized( optionAndArgument.key ) )
            throw OptionException.unrecognizedOption( optionAndArgument.key );

        OptionSpec optionSpec = specFor( optionAndArgument.key );
        optionSpec.handleOption( this, arguments, detected, optionAndArgument.value );
    }

    void handleShortOptionToken( String candidate, ArgumentList arguments,
        OptionSet detected ) {

        KeyValuePair optionAndArgument = parseShortOptionWithArgument( candidate );

        if ( isRecognized( optionAndArgument.key ) ) {
            specFor( optionAndArgument.key ).handleOption(
                this, arguments, detected, optionAndArgument.value );
        }
        else
            handleShortOptionCluster( candidate, arguments, detected );
    }

    private void handleShortOptionCluster( String candidate, ArgumentList arguments,
        OptionSet detected ) {

        char[] options = extractShortOptionsFrom( candidate );
        validateOptionCharacters( options );

        OptionSpec optionSpec = specFor( options[ 0 ] );

        if ( optionSpec.acceptsArguments() && options.length > 1 ) {
            String detectedArgument = String.valueOf( options, 1, options.length - 1 );
            optionSpec.handleOption( this, arguments, detected, detectedArgument );
        }
        else {
            for ( int i = 0; i < options.length; ++i )
                specFor( options[ i ] ).handleOption( this, arguments, detected, null );
        }
    }

    void noMoreOptions() {
        state = OptionParserState.noMoreOptions();
    }

    boolean looksLikeAnOption( String argument ) {
        return ParserRules.isShortOptionToken( argument )
            || ParserRules.isLongOptionToken( argument );
    }

    private boolean isRecognized( String option ) {
        return recognizedOptions.contains( option );
    }

    private OptionSpec specFor( char option ) {
        return specFor( String.valueOf( option ) );
    }

    private OptionSpec specFor( String option ) {
        return (OptionSpec) recognizedOptions.get( option );
    }

    private void reset() {
        state = OptionParserState.moreOptions( posixlyCorrect );
    }

    private static char[] extractShortOptionsFrom( String argument ) {
        char[] options = new char[ argument.length() - 1 ];
        argument.getChars( 1, argument.length(), options, 0 );

        return options;
    }

    private void validateOptionCharacters( char[] options ) {
        for ( int i = 0; i < options.length; ++i ) {
            String option = String.valueOf( options[ i ] );

            if ( !isRecognized( option ) )
                throw OptionException.unrecognizedOption( option );

            if ( specFor( option ).acceptsArguments() ) {
                if ( i > 0 )
                    throw OptionException.illegalOptionCluster( option );

                // remainder of chars are the argument to the option at char 0
                return;
            }
        }
    }

    private static KeyValuePair parseLongOptionWithArgument( String argument ) {
        return KeyValuePair.valueOf( argument.substring( 2 ) );
    }

    private static KeyValuePair parseShortOptionWithArgument( String argument ) {
        return KeyValuePair.valueOf( argument.substring( 1 ) );
    }
}
