/*
 * Copyright 2001-2004 The Apache Software Foundation.
 * 
 * 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.
 */
// This file is pulled from package org.apache.avalon.excalibur.cli Excalibur
// version 4.1 (Jan 30, 2002).  Only the package name has been changed.
package org.apache.axis.utils;

import java.text.ParseException;
import java.util.Hashtable;
import java.util.Vector;

/**
 * Parser for command line arguments.
 *
 * This parses command lines according to the standard (?) of
 * GNU utilities.
 *
 * Note: This is still used in 1.1 libraries so do not add 1.2+ dependencies.
 *
 * @author <a href="mailto:peter@apache.org">Peter Donald</a>
 * @since 4.0
 */
public final class CLArgsParser
{
    private static final int                 STATE_NORMAL           = 0;
    private static final int                 STATE_REQUIRE_2ARGS    = 1;
    private static final int                 STATE_REQUIRE_ARG      = 2;
    private static final int                 STATE_OPTIONAL_ARG     = 3;
    private static final int                 STATE_NO_OPTIONS       = 4;
    private static final int                 STATE_OPTION_MODE      = 5;

    private static final int                 TOKEN_SEPARATOR        = 0;
    private static final int                 TOKEN_STRING           = 1;


    private static final char[]              ARG2_SEPARATORS        =
        new char[] { (char)0, '=', '-' };

    private static final char[]              ARG_SEPARATORS         =
        new char[] { (char)0, '=' };

    private static final char[]              NULL_SEPARATORS        =
        new char[] { (char)0 };

    private final CLOptionDescriptor[]       m_optionDescriptors;
    private final Vector                     m_options;
    private Hashtable m_optionIndex;
    private final ParserControl              m_control;

    private String                           m_errorMessage;
    private String[]                         m_unparsedArgs         = new String[] {};

    //variables used while parsing options.
    private char                           ch;
    private String[]                       args;
    private boolean                        isLong;
    private int                            argIndex;
    private int                            stringIndex;
    private int                            stringLength;

    //cached character == Integer.MAX_VALUE when invalid
    private static final int               INVALID         = Integer.MAX_VALUE;
    private int                            m_lastChar      = INVALID;

    private int                            m_lastOptionId;
    private CLOption                       m_option;
    private int                            m_state         = STATE_NORMAL;

    public final String[] getUnparsedArgs()
    {
        return m_unparsedArgs;
    }

    /**
     * Retrieve a list of options that were parsed from command list.
     *
     * @return the list of options
     */
    public final Vector getArguments()
    {
        //System.out.println( "Arguments: " + m_options );
        return m_options;
    }

    /**
     * Retrieve the {@link CLOption} with specified id, or
     * <code>null</code> if no command line option is found.
     *
     * @param id the command line option id
     * @return the {@link CLOption} with the specified id, or
     *    <code>null</code> if no CLOption is found.
     * @see CLOption
     */
    public final CLOption getArgumentById( final int id )
    {
        return (CLOption)m_optionIndex.get( new Integer( id ) );
    }

    /**
     * Retrieve the {@link CLOption} with specified name, or
     * <code>null</code> if no command line option is found.
     *
     * @param name the command line option name
     * @return the {@link CLOption} with the specified name, or
     *    <code>null</code> if no CLOption is found.
     * @see CLOption
     */
    public final CLOption getArgumentByName( final String name)
    {
        return (CLOption)m_optionIndex.get( name );
    }

    /**
     * Get Descriptor for option id.
     *
     * @param id the id
     * @return the descriptor
     */
    private final CLOptionDescriptor getDescriptorFor( final int id )
    {
        for( int i = 0; i < m_optionDescriptors.length; i++ )
        {
            if( m_optionDescriptors[i].getId() == id )
            {
                return m_optionDescriptors[i];
            }
        }

        return null;
    }

    /**
     * Retrieve a descriptor by name.
     *
     * @param name the name
     * @return the descriptor
     */
    private final CLOptionDescriptor getDescriptorFor( final String name )
    {
        for( int i = 0; i < m_optionDescriptors.length; i++ )
        {
            if( m_optionDescriptors[i].getName().equals( name ) )
            {
                return m_optionDescriptors[i];
            }
        }

        return null;
    }

    /**
     * Retrieve an error message that occured during parsing if one existed.
     *
     * @return the error string
     */
    public final String getErrorString()
    {
        //System.out.println( "ErrorString: " + m_errorMessage );
        return m_errorMessage;
    }

    /**
     * Require state to be placed in for option.
     *
     * @param descriptor the Option Descriptor
     * @return the state
     */
    private final int getStateFor( final CLOptionDescriptor descriptor )
    {
        int flags = descriptor.getFlags();
        if( ( flags & CLOptionDescriptor.ARGUMENTS_REQUIRED_2 ) ==
            CLOptionDescriptor.ARGUMENTS_REQUIRED_2 )
        {
            return STATE_REQUIRE_2ARGS;
        }
        else if( ( flags & CLOptionDescriptor.ARGUMENT_REQUIRED ) ==
                 CLOptionDescriptor.ARGUMENT_REQUIRED )
        {
            return STATE_REQUIRE_ARG;
        }
        else if( ( flags & CLOptionDescriptor.ARGUMENT_OPTIONAL ) ==
                 CLOptionDescriptor.ARGUMENT_OPTIONAL )
        {
            return STATE_OPTIONAL_ARG;
        }
        else
        {
            return STATE_NORMAL;
        }
    }

    /**
     * Create a parser that can deal with options and parses certain args.
     *
     * @param args the args, typically that passed to the
     * <code>public static void main(String[] args)</code> method.
     * @param optionDescriptors the option descriptors
     */
    public CLArgsParser( final String[] args,
                         final CLOptionDescriptor[] optionDescriptors,
                         final ParserControl control )
    {
        m_optionDescriptors = optionDescriptors;
        m_control = control;
        m_options = new Vector();
        this.args = args;

        try
        {
            parse();
            checkIncompatibilities( m_options );
            buildOptionIndex();
        }
        catch( final ParseException pe )
        {
            m_errorMessage = pe.getMessage();
        }

        //System.out.println( "Built : " + m_options );
        //System.out.println( "From : " + Arrays.asList( args ) );
    }

    /**
     * Check for duplicates of an option.
     * It is an error to have duplicates unless appropriate flags is set in descriptor.
     *
     * @param arguments the arguments
     */
    private final void checkIncompatibilities( final Vector arguments )
        throws ParseException
    {
        final int size = arguments.size();

        for( int i = 0; i < size; i++ )
        {
            final CLOption option = (CLOption)arguments.elementAt( i );
            final int id = option.getId();
            final CLOptionDescriptor descriptor = getDescriptorFor( id );

            //this occurs when id == 0 and user has not supplied a descriptor
            //for arguments
            if( null == descriptor )
            {
                continue;
            }

            final int[] incompatible = descriptor.getIncompatible();

            checkIncompatible( arguments, incompatible, i );
        }
    }

    private final void checkIncompatible( final Vector arguments,
                                    final int[] incompatible,
                                    final int original )
        throws ParseException
    {
        final int size = arguments.size();

        for( int i = 0; i < size; i++ )
        {
            if( original == i )
            {
                continue;
            }

            final CLOption option = (CLOption)arguments.elementAt( i );
            final int id = option.getId();
//            final CLOptionDescriptor descriptor = getDescriptorFor( id );

            for( int j = 0; j < incompatible.length; j++ )
            {
                if( id == incompatible[ j ] )
                {
                    final CLOption originalOption = (CLOption)arguments.elementAt( original );
                    final int originalId = originalOption.getId();

                    String message = null;

                    if( id == originalId )
                    {
                        message =
                            "Duplicate options for " + describeDualOption( originalId ) +
                            " found.";
                    }
                    else
                    {
                        message = "Incompatible options -" +
                            describeDualOption( id ) + " and " +
                            describeDualOption( originalId ) + " found.";
                    }
                    throw new ParseException( message, 0 );
                }
            }
        }
    }

    private final String describeDualOption( final int id )
    {
        final CLOptionDescriptor descriptor = getDescriptorFor( id );
        if( null == descriptor )
        {
            return "<parameter>";
        }
        else
        {
            final StringBuffer sb = new StringBuffer();
            boolean hasCharOption = false;

            if( Character.isLetter( (char)id ) )
            {
                sb.append( '-' );
                sb.append( (char)id );
                hasCharOption = true;
            }

            final String longOption = descriptor.getName();
            if( null != longOption )
            {
                if( hasCharOption )
                {
                    sb.append( '/' );
                }
                sb.append( "--" );
                sb.append( longOption );
            }

            return sb.toString();
        }
    }

    /**
     * Create a parser that deals with options and parses certain args.
     *
     * @param args the args
     * @param optionDescriptors the option descriptors
     */
    public CLArgsParser( final String[] args,
                         final CLOptionDescriptor[] optionDescriptors )
    {
        this( args, optionDescriptors, null );
    }

    /**
     * Create a string array that is subset of input array.
     * The sub-array should start at array entry indicated by index. That array element
     * should only include characters from charIndex onwards.
     *
     * @param array[] the original array
     * @param index the cut-point in array
     * @param charIndex the cut-point in element of array
     * @return the result array
     */
    private final String[] subArray( final String[] array,
                               final int index,
                               final int charIndex )
    {
        final int remaining = array.length - index;
        final String[] result = new String[ remaining ];

        if( remaining > 1 )
        {
            System.arraycopy( array, index + 1, result, 1, remaining - 1 );
        }

        result[0] = array[ index ].substring( charIndex - 1 );

        return result;
    }

    /**
     * Actually parse arguments
     *
     * @param args[] arguments
     */
    private final void parse()
        throws ParseException
    {
        if( 0 == args.length )
        {
            return;
        }

        stringLength = args[ argIndex ].length();

        //ch = peekAtChar();

        while( true )
        {
            ch = peekAtChar();

            //System.out.println( "Pre State=" + m_state );
            //System.out.println( "Pre Char=" + (char)ch + "/" + (int)ch );

            if( argIndex >= args.length )
            {
                break;
            }

            if( null != m_control && m_control.isFinished( m_lastOptionId ) )
            {
                //this may need mangling due to peeks
                m_unparsedArgs = subArray( args, argIndex, stringIndex );
                return;
            }

            //System.out.println( "State=" + m_state );
            //System.out.println( "Char=" + (char)ch + "/" + (int)ch );

            if( STATE_OPTION_MODE == m_state )
            {
                //if get to an arg barrier then return to normal mode
                //else continue accumulating options
                if( 0 == ch )
                {
                    getChar(); //strip the null
                    m_state = STATE_NORMAL;
                }
                else
                {
                    parseShortOption();
                }
            }
            else if( STATE_NORMAL == m_state )
            {
                parseNormal();
            }
            else if( STATE_NO_OPTIONS == m_state )
            {
                //should never get to here when stringIndex != 0
                addOption( new CLOption( args[ argIndex++ ] ) );
            }
            else if( STATE_OPTIONAL_ARG == m_state && '-' == ch )
            {
                m_state = STATE_NORMAL;
                addOption( m_option );
            }
            else
            {
                parseArguments();
            }
        }

        if( m_option != null )
        {
            if( STATE_OPTIONAL_ARG == m_state )
            {
                m_options.addElement( m_option );
            }
            else if( STATE_REQUIRE_ARG == m_state )
            {
                final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() );
                final String message =
                    "Missing argument to option " + getOptionDescription( descriptor );
                throw new ParseException( message, 0 );
            }
            else if( STATE_REQUIRE_2ARGS == m_state )
            {
                if( 1 == m_option.getArgumentCount() )
                {
                    m_option.addArgument( "" );
                    m_options.addElement( m_option );
                }
                else
                {
                    final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() );
                    final String message =
                        "Missing argument to option " + getOptionDescription( descriptor );
                    throw new ParseException( message, 0 );
                }
            }
            else
            {
                throw new ParseException( "IllegalState " + m_state + ": " + m_option, 0 );
            }
        }
    }

    private final String getOptionDescription( final CLOptionDescriptor descriptor )
    {
        if( isLong )
        {
            return "--" + descriptor.getName();
        }
        else
        {
            return "-" + (char)descriptor.getId();
        }
    }

    private final char peekAtChar()
    {
        if( INVALID == m_lastChar )
        {
            m_lastChar = readChar();
        }
        return (char)m_lastChar;
    }

    private final char getChar()
    {
        if( INVALID != m_lastChar )
        {
            final char result = (char)m_lastChar;
            m_lastChar = INVALID;
            return result;
        }
        else
        {
            return readChar();
        }
    }

    private final char readChar()
    {
        if( stringIndex >= stringLength )
        {
            argIndex++;
            stringIndex = 0;

            if( argIndex < args.length )
            {
                stringLength = args[ argIndex ].length();
            }
            else
            {
                stringLength = 0;
            }

            return 0;
        }

        if( argIndex >= args.length )
            return 0;

        return args[ argIndex ].charAt( stringIndex++ );
    }

    private final Token nextToken( final char[] separators )
    {
        ch = getChar();

        if( isSeparator( ch, separators ) )
        {
            ch = getChar();
            return new Token( TOKEN_SEPARATOR, null );
        }

        final StringBuffer sb = new StringBuffer();

        do
        {
            sb.append( ch );
            ch = getChar();
        }
        while( !isSeparator( ch, separators ) );

        return new Token( TOKEN_STRING, sb.toString() );
    }

    private final boolean isSeparator( final char ch, final char[] separators )
    {
        for( int i = 0; i < separators.length; i++ )
        {
            if( ch == separators[ i ] )
            {
                return true;
            }
        }

        return false;
    }

    private final void addOption( final CLOption option )
    {
        m_options.addElement( option );
        m_lastOptionId = option.getId();
        m_option = null;
    }

    private final void parseOption( final CLOptionDescriptor descriptor,
                              final String optionString )
        throws ParseException
    {
        if( null == descriptor )
        {
            throw new ParseException( "Unknown option " + optionString, 0 );
        }

        m_state = getStateFor( descriptor );
        m_option = new CLOption( descriptor.getId() );

        if( STATE_NORMAL == m_state )
        {
            addOption( m_option );
        }
    }

    private final void parseShortOption()
        throws ParseException
    {
        ch = getChar();
        final CLOptionDescriptor descriptor = getDescriptorFor( ch );
        isLong = false;
        parseOption( descriptor, "-" + ch );

        if( STATE_NORMAL == m_state )
        {
            m_state = STATE_OPTION_MODE;
        }
    }

    private final void parseArguments()
        throws ParseException
    {
        if( STATE_REQUIRE_ARG == m_state )
        {
            if( '=' == ch || 0 == ch )
            {
                getChar();
            }

            final Token token = nextToken( NULL_SEPARATORS );
            m_option.addArgument( token.getValue() );

            addOption( m_option );
            m_state = STATE_NORMAL;
        }
        else if( STATE_OPTIONAL_ARG == m_state )
        {
            if( '-' == ch || 0 == ch )
            {
                getChar(); //consume stray character
                addOption( m_option );
                m_state = STATE_NORMAL;
                return;
            }

            if( '=' == ch )
            {
                getChar();
            }

            final Token token = nextToken( NULL_SEPARATORS );
            m_option.addArgument( token.getValue() );

            addOption( m_option );
            m_state = STATE_NORMAL;
        }
        else if( STATE_REQUIRE_2ARGS == m_state )
        {
            if( 0 == m_option.getArgumentCount() )
            {
                final Token token = nextToken( ARG_SEPARATORS );

                if( TOKEN_SEPARATOR == token.getType() )
                {
                    final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() );
                    final String message =
                        "Unable to parse first argument for option " +
                        getOptionDescription( descriptor );
                    throw new ParseException( message, 0 );
                }
                else
                {
                    m_option.addArgument( token.getValue() );
                }
            }
            else //2nd argument
            {
                final StringBuffer sb = new StringBuffer();

                ch = getChar();
                if( '-' == ch )
                {
                    m_lastChar = ch;
                }

                while( !isSeparator( ch, ARG2_SEPARATORS ) )
                {
                    sb.append( ch );
                    ch = getChar();
                }

                final String argument = sb.toString();

                //System.out.println( "Arguement:" + argument );

                m_option.addArgument( argument );
                addOption( m_option );
                m_option = null;
                m_state = STATE_NORMAL;
            }
        }
    }

    /**
     * Parse Options from Normal mode.
     */
    private final void parseNormal()
        throws ParseException
    {
        if( '-' != ch )
        {
            //Parse the arguments that are not options
            final String argument = nextToken( NULL_SEPARATORS ).getValue();
            addOption( new CLOption( argument ) );
            m_state = STATE_NORMAL;
        }
        else
        {
            getChar(); // strip the -

            if( 0 == peekAtChar() )
            {
                throw new ParseException( "Malformed option -", 0 );
            }
            else
            {
                ch = peekAtChar();

                //if it is a short option then parse it else ...
                if( '-' != ch )
                {
                    parseShortOption();
                }
                else
                {
                    getChar(); // strip the -
                    //-- sequence .. it can either mean a change of state
                    //to STATE_NO_OPTIONS or else a long option

                    if( 0 == peekAtChar() )
                    {
                        getChar();
                        m_state = STATE_NO_OPTIONS;
                    }
                    else
                    {
                        //its a long option
                        final String optionName = nextToken( ARG_SEPARATORS ).getValue();
                        final CLOptionDescriptor descriptor = getDescriptorFor( optionName );
                        isLong = true;
                        parseOption( descriptor, "--" + optionName );
                    }
                }
            }
        }
    }

    /**
     * Build the m_optionIndex lookup map for the parsed options.
     */
    private final void buildOptionIndex()
    {
        m_optionIndex = new Hashtable( m_options.size() * 2 );

        for( int i = 0; i < m_options.size(); i++ )
        {
            final CLOption option = (CLOption)m_options.get( i );
            final CLOptionDescriptor optionDescriptor =
                getDescriptorFor( option.getId() );

            m_optionIndex.put( new Integer( option.getId() ), option );

            if( null != optionDescriptor )
            {
                m_optionIndex.put( optionDescriptor.getName(), option );
            }
        }
    }
}
