

package gov.va.med.cds.client.functional.mbean;


import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * This class has constructors and methods for reading and processing options (command line
 * options, for example).  The options are presented as an array of String (like the {@code args} array
 * for a {@code main} method). Each option must begin with a minus (-).  Options can be either
 * binary (the option name and an associated value) or unary (an option with no value).
 * Binary options take two 'slots' in the {@code args} array - one for the option identifer
 * and one for the option's value.  Unary options require only one 'slot' and are boolean in
 * nature (they are either present or not present). Additionally, options can be defined as
 * required (i.e. they must be present or the calling application fails) or exclusive (only
 * one of a set of options can be present).
 * <p>Required options are a subset of all options and can be represented in one of two ways:
 * <ul>
 * <li>A simple required option.  This is simple the option identifier from either the {@code binaryOpts}
 * or {@code unaryOpts} list.</li>
 * <li>A required option with a default value.  The syntax is {@code "[opt=value]"}.</li>
 * </ul>
 * Simple and default required options can be mixed.  Example: "opt1:[opt2=val2]:opt3".
 * <p>If an option is defined as exclusive, it means that only one of a set of options can present.
 * The syntax for exclusive options is {@code "(opt1:opt2:opt3)"}.  If more than one of the options in
 * the set is present, an error occurs.
 * <p>Examples:<br>
 * &nbsp;&nbsp;
 * Department of Veterans Affairs
 * OI Field Office - Salt Lake City
 * Health Data Systems
 * 
 * @author DNS   schras
 *
 */
public class GetArgs
{
    /**
     * {@value}
     */
    public static final boolean UNARY_ONLY = true;
    /**
     * {@value}
     */
    public static final boolean BINARY_ONLY = false;

    private String mainClass;

    private Properties pairs = new Properties();
    private String[] binaryOpts = {};
    private ArrayList<String[]> exclusiveBinaryOpts = new ArrayList<String[]>();
    private String[] unaryOpts = {};
    private ArrayList<String[]> exclusiveUnaryOpts = new ArrayList<String[]>();
    private String[] requiredOpts = {};


    /**
     * Constructor that defines both binary and unary options.
     * @param parent the enclosing {@code Class}
     * @param args an array of options and values.
     * @param binary a colon separated list of options that require an argument.
     * @param unary a colon separated list of options that don't require an argument.
     */
    public GetArgs( Class parent, String[] args, String binary, String unary )
    {
        init( parent, args, binary, unary, null );
    }


    /**
     * Constructor that exclusivly defines either binary or unary options.
     * @param parent the enclosing {@code Class}
     * @param args an array of options and values.
     * @param argNames a colon separated list of options that require an argument.
     * @param unary {@code true} if {@code argNames} represents only unary options;
     * {@code false} if {@code argNames} represents only binary options.
     */
    public GetArgs( Class parent, String[] args, String argNames, boolean unary )
    {
        init( parent, args, ( unary ? null : argNames ), ( unary ? argNames : null ), null );
    }


    /**
     * Constructor that defines both binary and unary options and which of each are
     * required.
     * @param parent the enclosing {@code Class}
     * @param args an array of options and values.
     * @param binary a colon separated list of options that require an argument.
     * @param unary a colon separated list of options that don't require an argument.
     * @param required a colon separated list options from {@code binary} and/or
     * {@code unary} that are required.
     */
    public GetArgs( Class parent, String[] args, String binary, String unary, String required )
    {
        init( parent, args, binary, unary, required );
    }


    /**
     * Constructor that exclusivly defines either binary or unary options and which
     * of those options is required.
     * @param parent the enclosing {@code Class}
     * @param args an array of options and values.
     * @param argNames a colon separated list of options that require an argument.
     * @param required a colon separated list options from {@code argNames} that
     * are required.
     * @param unary {@code true} if {@code argNames} represents only unary options;
     * {@code false} if {@code argNames} represents only binary options.
     */
    public GetArgs( Class parent, String[] args, String argNames, String required, boolean unary )
    {
        init( parent, args, ( unary ? null : argNames ), ( unary ? argNames : null ), required );
    }


    /**
     * Reads and processes options and stores them in a map ({@code pairs}).  The
     * options are presented as an array like the {@code args} array for a {@code main} method.
     * Each option must begin with a minus (-).
     * <p>Binary options take two 'slots' in the {@code args} array - one for the option identifer
     * and one for the option's value.  Unary options require only one 'slot' and are boolean in
     * nature (they are either present or not present).
     * <p>{@code requiredOpts) can be represented in one of two ways:
     * <ul>
     * <li>A simple option.  This is simple the option identifier from either the {@code binaryOpts}
     * or {@code unaryOpts} list.</li>
     * <li>A default option. This is a an option identifier from either the {@code binaryOpts}
     * or {@code unaryOpts} list with an included default value.  The syntax is {@code "[opt=value]"}.</li>
     * </ul>
     * Simple and default options can be mixed.  Example: "opt1:[opt2=val2]:opt3".
     * 
     * @param parent the {@code Class} using these options.
     * @param args an array of the options to be processed.
     * @param binaryOpts a colon separated list of binary options.
     * @param unaryOpts a colon separated list of unary options.
     * @param requiredOpts a colon separated list of required options.
     */
    private void init( Class parent, String[] args, String binaryOpts, String unaryOpts, String requiredOpts )
    {
        boolean hasRequired = ( null != requiredOpts && 0 != requiredOpts.length() );
        mainClass = parent.getName();
        if ( null != unaryOpts && 0 != unaryOpts.length() )
            this.unaryOpts = exclusiveOpts( unaryOpts.split( ":" ), true );
        if ( null != binaryOpts && 0 != binaryOpts.length() )
            this.binaryOpts = exclusiveOpts( binaryOpts.split( ":" ), false );
        if ( hasRequired )
        {
            this.requiredOpts = requiredOpts.split( ":" );
            addDefaults( this.requiredOpts );
        }

        for ( int i = 0; i < args.length; i++ )
        {
            boolean unary = false;
            String opt = args[i].substring( 1 );

            // Check that it's a valid argument.
            // If the process was started using the java's -jar option
            // the args[0] may be the name of the class invoked.
            if ( !( unary = isUnaryOpt( opt ) ) && !isBinaryOpt( opt ) && !( parent.getName().equals( args[i] ) ) )
                unknownOpt( args[i], parent );
            else
                pairs.put( opt, ( unary ? "true" : args[( ++i )] ) );
        }

        if ( ( hasRequired && missingRequired() ) || checkExclusive() )
            showUsage( parent );
    }

    private final Pattern defaultOptsPattern = Pattern.compile( "\\[(.+)=(.+)\\]" );


    /**
     * Finds the default options in a colon separated list of options and
     * puts them in {@code pairs} map.  These may later be
     * overridden by the option passed in on the command line.
     * @param opts a colon separated list of options
     */
    private void addDefaults( String[] opts )
    {
        Matcher match = null;
        for ( int i = 0; i < opts.length; i++ )
        {
            if ( ( match = defaultOptsPattern.matcher( opts[i] ) ).matches() )
            {
                opts[i] = match.group( 1 );
                String value = match.group( 2 );
                pairs.put( opts[i], value );

            }
        }
    }


    /**
     * Checks for options that are exclusive (i.e. only one of a set is legal).
     * @return returns true if more than one exclusive option is present, otherwise false.
     */
    private boolean checkExclusive( )
    {
        int excluded;
        for ( Iterator iter = exclusiveUnaryOpts.iterator(); iter.hasNext(); )
        {
            excluded = 0;
            String[] opts = ( String[] )iter.next();
            for ( int i = 0; i < opts.length; i++ )
                excluded += ( pairs.containsKey( opts[i] ) ? 1 : 0 );
            if ( 1 != excluded )
                return true;
        }
        for ( Iterator iter = exclusiveBinaryOpts.iterator(); iter.hasNext(); )
        {
            excluded = 0;
            String[] opts = ( String[] )iter.next();
            for ( int i = 0; i < opts.length; i++ )
                excluded += ( pairs.containsKey( opts[i] ) ? 1 : 0 );
            if ( 1 != excluded )
                return true;
        }
        return false;
    }


    /**
     * Parses out exclusive options from an array of options adds them to the appropriate 
     * map to check against when options are processed.
     * @param opts an array of options to parse.
     * @param unary if true, the {@code opts} array represents unary options.  If false, the
     * {@code opts} array represents binary options. 
     * @return a list of all options found in the {@code opts} array.
     */
    private String[] exclusiveOpts( String[] opts, boolean unary )
    {
        boolean exclusiveGroup = false;
        List<String> optsL = Arrays.asList( opts );
        ArrayList<String> exclusives = new ArrayList<String>();
        for ( Iterator iter = optsL.iterator(); iter.hasNext(); )
        {
            String opt = ( String )iter.next();
            String optRef = opt;
            if ( opt.startsWith( "(" ) )
            {
                exclusiveGroup = true;
                opt = opt.substring( 1 );
            }
            else if ( exclusiveGroup && opt.endsWith( ")" ) )
            {
                exclusiveGroup = false;
                exclusives.add( ( opt = opt.substring( 0, opt.length() - 1 ) ) );
            }
            if ( exclusiveGroup )
                exclusives.add( opt );
            optsL.set( optsL.indexOf( optRef ), opt );
        }
        if ( 0 != exclusives.size() )
        {
            if ( unary )
                exclusiveUnaryOpts.add( exclusives.toArray( new String[exclusives.size()] ) );
            else
                exclusiveBinaryOpts.add( exclusives.toArray( new String[exclusives.size()] ) );
        }
        return ( String[] )optsL.toArray( opts );
    }


    private boolean missingRequired( )
    {
        for ( int i = 0; i < requiredOpts.length; i++ )
        {
            if ( !hasOpt( requiredOpts[i] ) )
                return true;
        }

        return false;
    }


    private boolean isRequired( String arg )
    {
        for ( int i = 0; i < requiredOpts.length; i++ )
        {
            if ( arg.equals( requiredOpts[i] ) )
                return true;
        }
        return false;
    }


    private boolean isUnaryOpt( String arg )
    {
        for ( int i = 0; i < unaryOpts.length; i++ )
        {
            if ( arg.equals( unaryOpts[i] ) )
                return true;
        }
        return false;
    }


    private boolean isBinaryOpt( String arg )
    {
        for ( int i = 0; i < binaryOpts.length; i++ )
        {
            if ( arg.equals( binaryOpts[i] ) )
                return true;
        }
        return false;
    }


    /**
     * Returns an option's argument as an integer value.
     * @param arg the name of the options.
     * @return the option's argument as an integer value.
     */
    public int getIntArg( String arg )
    {
        String value = ( String )pairs.get( arg );
        if ( null == value )
            value = "0";
        return Integer.parseInt( value );
    }


    /**
     * Returns an option's argument as a String.
     * @param arg the name of the options.
     * @return the option's argument as a String.
     */
    public String getArg( String arg )
    {
        return ( String )pairs.get( arg );
    }


    /**
     * Returns whether or not a unary options is present.
     * @param arg the name of the options.
     * @return {@code true} if the option is present, otherwise false.
     */
    public boolean getUnaryOpt( String arg )
    {
        return pairs.containsKey( arg );
    }


    /**
     * Returns an option's argument as an integer value.  If the option
     * isn't present, the {@code defaultValue} is returned.
     * @param arg the name of the options.
     * @return the option's argument as an integer value.
     */
    public int getIntArg( String arg, int defaultValue )
    {
        return ( pairs.containsKey( arg ) ? Integer.parseInt( ( String )pairs.get( arg ) ) : defaultValue );
    }


    /**
     * Returns an option's argument as a String.  If the option
     * isn't present, the {@code defaultValue} is returned.
     * @param arg the name of the options.
     * @return the option's argument as a String.
     */
    public String getArg( String arg, String defaultValue )
    {
        return ( pairs.containsKey( arg ) ? ( String )pairs.get( arg ) : defaultValue );
    }


    /**
     * Returns whether or not a unary options is present.  If the option
     * isn't present, the {@code defaultValue} is returned.
     * @param arg the name of the options.
     * @return {@code true} if the option is present, otherwise false.
     */
    public boolean getUnaryOpt( String arg, boolean defaultValue )
    {
        return ( pairs.containsKey( arg ) ? true : defaultValue );
    }


    /**
     * Returns whether at least one of a set of one or more options are present.
     * @param arg a colon separated list of option names.
     * @return {@code true} if at least one of the options in the list is present, otherwise {@code false}.
     */
    public boolean hasOpt( String arg )
    {
        String[] opts = arg.split( ":" );
        for ( String opt : opts )
        {
            if ( pairs.containsKey( opt.trim() ) )
            {
                return true;
            }
        }
        return false;
    }


    /**
     * Generates a generic usage message based on the option definitions <i>and</i> terminates
     * the application (i.e. calls {@code System.exit()} with a value of {@code exitValue}.
     * @param exitValue the exit value with which to terminate the application.
     * @see #usage()
     */
    public void usage( int exitValue )
    {
        usage();
        System.exit( exitValue );
    }


    /**
     * Uses reflection to call the 'usage' method defined by the containing class.  The signiture
     * of the 'usage' method is {@code void usage()} or {@code void usage(int)}.  If the latter
     * signiture is used, the parameter will always be the value 1.
     * <p>If the method is not defined, a generic usage message will be generated and written to
     * {@code System.err}.
     * @param parent the containing {@code Class}.
     * @see #usage(int)
     * @see #usage()
     */
    private void showUsage( Class parent )
    {
        Method usageMethod = null;

        try
        {
            usageMethod = parent.getDeclaredMethod( "usage", new Class[] { int.class } );
            usageMethod.invoke( null, new Object[] { new Integer( 1 ) } );
        }
        catch ( Exception e )
        {
            try
            {
                usageMethod = parent.getDeclaredMethod( "usage", new Class[] {} );
                usageMethod.invoke( null, ( Object[] )null );
            }
            catch ( Exception e1 )
            {
                usage( 1 );
            }
        }
    }


    /**
     * Generates a generic usage message based on the option definitions.
     * @see #usage(int)
     * @see #showUsage(Class)
     */
    public void usage( )
    {
        StringBuffer usageStr = new StringBuffer( "java " );
        usageStr.append( mainClass );
        boolean optional = true;
        for ( int i = 0; i < unaryOpts.length; i++ )
        {
            optional = !isRequired( unaryOpts[i] );
            usageStr.append( ( optional ? " [-" : " -" ) + unaryOpts[i] + ( optional ? "]" : "" ) );
        }
        for ( int i = 0; i < binaryOpts.length; i++ )
        {
            optional = !isRequired( binaryOpts[i] );
            usageStr.append( ( optional ? " [-" : " -" ) + binaryOpts[i] + " " + binaryOpts[i] + ".value" + ( optional ? "]" : "" ) );
        }
        System.err.println( usageStr.toString() + "\n" );
    }


    /**
     * Writes a message to {@code System.err} indicating that an unidentified option
     * has been encountered.  If the option is either "-?" or "-help", {@linkplain #showUsage(Class)}
     * will be called.
     * @param opt the unidentifed option.
     * @param parent the enclosing {@code Class}.
     * @see #showUsage(Class)
     * @see #usage()
     * @see #usage(int)
     */
    public void unknownOpt( String opt, Class parent )
    {
        if ( "-?".equals( opt ) || "-help".equals( opt ) )
            showUsage( parent );
        System.err.println( "Ignoring unknown option '" + opt + "' for " + mainClass + ". Continuing..." );
    }

}
