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

package joptsimple;

import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.List;
import java.util.StringTokenizer;

import joptsimple.internal.Reflection;
import joptsimple.internal.ReflectionException;
import joptsimple.internal.Strings;

/**
 * <p>Specification of an option that accepts an argument.</p>
 *
 * <p>Instances are returned from {@link OptionSpecBuilder} methods to allow the formation
 * of parser directives as sentences in a domain-specific language.  For example:</p>
 *
 * <pre>
 *   <code>
 *   OptionParser parser = new OptionParser();
 *   parser.accepts( "c" ).withRequiredArg().<strong>ofType( Integer.class )</strong>;
 *   </code>
 * </pre>
 *
 * <p>If no methods are invoked on an instance of this class, then that instance's option
 * will treat its argument as a {@link String}.</p>
 *
 * @since 1.0
 * @author <a href="mailto:pholser@alumni.rice.edu">Paul Holser</a>
 * @version $Id: ArgumentAcceptingOptionSpec.java,v 1.7 2008/05/01 15:38:45 pholser Exp $
 */
public abstract class ArgumentAcceptingOptionSpec extends OptionSpec {
    private static final char NIL_VALUE_SEPARATOR = '\u0000';
    private final boolean argumentRequired;
    private Member converter;
    private String argumentDescription = "";
    private String separator = String.valueOf( NIL_VALUE_SEPARATOR );

    ArgumentAcceptingOptionSpec( String option, boolean argumentRequired ) {
        super( option );

        this.argumentRequired = argumentRequired;
    }

    ArgumentAcceptingOptionSpec( List options, boolean argumentRequired,
        String description ) {

        super( options, description );

        this.argumentRequired = argumentRequired;
    }

    /**
     * <p>Specifies a type to which arguments of this spec's option are to be
     * converted.</p>
     *
     * <p>JOpt Simple accepts types that have either:</p>
     *
     * <ol>
     *   <li>a public static method called <code>valueOf</code> which accepts a single
     *   argument of type {@link String} and whose return type is the same as the class
     *   on which the method is declared.  The <code>java.lang</code> primitive wrapper
     *   classes have such methods.</li>
     *
     *   <li>a public constructor which accepts a single argument of type
     *   {@link String}.</li>
     * </ol>
     *
     * <p>This class converts arguments using those methods in that order; that is,
     * <code>valueOf</code> would be invoked before a one-{@link String}-arg constructor
     * would.</p>
     *
     * @param argumentType desired type of arguments to this spec's option
     * @return self, so that the caller can add clauses to the domain-specific sentence
     * @throws NullPointerException if the type is <code>null</code>
     * @throws IllegalArgumentException if the type does not have the standard conversion
     * methods
     */
    public ArgumentAcceptingOptionSpec ofType( Class argumentType ) {
        converter = Reflection.findConverter( argumentType );
        return this;
    }

    /**
     * <p>Specifies a description for the argument of the option that this spec
     * represents.  This description is used when generating help information about
     * the parser.</p>
     *
     * @param description describes the nature of the argument of this spec's
     * option
     * @return self, so that the caller can add clauses to the domain-specific sentence
     */
    public final ArgumentAcceptingOptionSpec describedAs( String description ) {
        argumentDescription = description;

        return this;
    }

    /**
     * <p>Specifies a value separator for the argument of the option that this spec
     * represents.  This allows a single option argument to represent multiple values
     * for the option.  For example:</p>
     *
     * <pre>
     *   <code>
     *   parser.accepts( "z" ).withRequiredArg()
     *       .<strong>withValuesSeparatedBy( ',' )</strong>;
     *   OptionSet options = parser.parse( new String[] { "-z", "foo,bar,baz", "-z",
     *       "fizz", "buzz" } );
     *   </code>
     * </pre>
     *
     * <p>Then <code>options.valuesOf( "z" )</code> would yield the list <code>[foo, bar,
     * baz, fizz, buzz]</code>.</p>
     *
     * <p>You cannot use Unicode U+0000 as the separator.</p>
     *
     * @param separator a character separator
     * @return self, so that the caller can add clauses to the domain-specific sentence
     * @throws IllegalArgumentException if the separator is Unicode U+0000
     */
    public final ArgumentAcceptingOptionSpec withValuesSeparatedBy( char separator ) {
        if ( separator == NIL_VALUE_SEPARATOR )
            throw new IllegalArgumentException( "cannot use U+0000 as separator" );

        this.separator = String.valueOf( separator );
        return this;
    }

    final void handleOption( OptionParser parser, ArgumentList arguments,
        OptionSet detectedOptions, String detectedArgument ) {

        if ( Strings.isNullOrEmpty( detectedArgument ) )
            detectOptionArgument( parser, arguments, detectedOptions );
        else {
            addArguments( detectedOptions, detectedArgument );
        }
    }

    protected void addArguments( OptionSet detectedOptions, String detectedArgument ) {
        StringTokenizer lexer = new StringTokenizer( detectedArgument, separator );
        while ( lexer.hasMoreTokens() )
            detectedOptions.addAllWithArgument( options(), convert( lexer.nextToken() ) );
    }

    protected abstract void detectOptionArgument( OptionParser parser,
        ArgumentList arguments, OptionSet detectedOptions );

    protected final Object convert( String argument ) {
        if ( converter == null )
            return argument;

        Object[] args = new Object[] { argument };

        try {
            if ( converter instanceof Constructor )
                return Reflection.invokeQuietly( (Constructor) converter, args );

            return Reflection.invokeStaticQuietly( (Method) converter, args );
        }
        catch ( ReflectionException ignored ) {
            throw new OptionArgumentConversionException(
                options(), argument, converter.getDeclaringClass() );
        }
    }

    protected boolean canConvertArgument( String argument ) {
        StringTokenizer lexer = new StringTokenizer( argument, separator );

        try {
            while ( lexer.hasMoreTokens() )
                convert( lexer.nextToken() );
            return true;
        }
        catch ( OptionException ignored ) {
            return false;
        }
    }

    protected boolean isArgumentOfNumberType() {
        return converter != null
            && Number.class.isAssignableFrom( converter.getDeclaringClass() );
    }

    boolean acceptsArguments() {
        return true;
    }

    boolean requiresArgument() {
        return argumentRequired;
    }

    String argumentDescription() {
        return argumentDescription;
    }

    Class argumentType() {
        return converter == null ? String.class : converter.getDeclaringClass();
    }

    /**
     * {@inheritDoc}
     */
    public boolean equals( Object that ) {
        if ( !super.equals( that ) )
            return false;

        ArgumentAcceptingOptionSpec other = (ArgumentAcceptingOptionSpec) that;
        return requiresArgument() == other.requiresArgument();
    }

    /**
     * {@inheritDoc}
     */
    public int hashCode() {
        return super.hashCode() ^ ( argumentRequired ? 0 : 1 );
    }
}
