package org.apache.maven.shared.utils.introspection;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.introspection.MethodMap.AmbiguousException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;


/**
 * <p>Using simple dotted expressions to extract the values from an Object instance,
 * For example we might want to extract a value like: <code>project.build.sourceDirectory</code></p>
 * <p>The implementation supports indexed, nested and mapped properties similar to the JSP way.</p>
 *
 * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 *
 * @see <a href="http://struts.apache.org/1.x/struts-taglib/indexedprops.html">
 * http://struts.apache.org/1.x/struts-taglib/indexedprops.html</a>
 */
public class ReflectionValueExtractor
{
    private static final Class<?>[] CLASS_ARGS = new Class[0];

    private static final Object[] OBJECT_ARGS = new Object[0];

    /**
     * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
     * This approach prevents permgen space overflows due to retention of discarded
     * classloaders.
     */
    private static final Map<Class<?>, ClassMap> CLASS_MAPS = new WeakHashMap<Class<?>, ClassMap>();

    static final int EOF = -1;

    static final char PROPERTY_START = '.';

    static final char INDEXED_START = '[';

    static final char INDEXED_END = ']';

    static final char MAPPED_START = '(';

    static final char MAPPED_END = ')';

    static class Tokenizer
    {
        final String expression;

        int idx;

        Tokenizer( String expression )
        {
            this.expression = expression;
        }

        public int peekChar()
        {
            return idx < expression.length() ? expression.charAt( idx ) : EOF;
        }

        public int skipChar()
        {
            return idx < expression.length() ? expression.charAt( idx++ ) : EOF;
        }

        public String nextToken( char delimiter )
        {
            int start = idx;

            while ( idx < expression.length() && delimiter != expression.charAt( idx ) )
            {
                idx++;
            }

            // delimiter MUST be present
            if ( idx <= start || idx >= expression.length() )
            {
                return null;
            }

            return expression.substring( start, idx++ );
        }

        public String nextPropertyName()
        {
            final int start = idx;

            while ( idx < expression.length() && Character.isJavaIdentifierPart( expression.charAt( idx ) ) )
            {
                idx++;
            }

            // property name does not require delimiter
            if ( idx <= start || idx > expression.length() )
            {
                return null;
            }

            return expression.substring( start, idx );
        }

        public int getPosition()
        {
            return idx < expression.length() ? idx : EOF;
        }

        // to make tokenizer look pretty in debugger
        @Override
        public String toString()
        {
            return idx < expression.length() ? expression.substring( idx ) : "<EOF>";
        }
    }

    private ReflectionValueExtractor()
    {
    }

    /**
     * <p>The implementation supports indexed, nested and mapped properties.</p>
     * <ul>
     * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
     * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
     * pattern, i.e. "user.addresses[1].street"</li>
     * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern,
     *  i.e. "user.addresses(myAddress).street"</li>
     * </ul>
     *
     * @param expression not null expression
     * @param root       not null object
     * @return the object defined by the expression
     * @throws IntrospectionException if any
     */
    public static Object evaluate( @Nonnull String expression, @Nullable Object root )
        throws IntrospectionException
    {
        return evaluate( expression, root, true );
    }

    /**
     * <p>
     * The implementation supports indexed, nested and mapped properties.
     * </p>
     * <ul>
     * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
     * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
     * pattern, i.e. "user.addresses[1].street"</li>
     * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
     * "user.addresses(myAddress).street"</li>
     * </ul>
     *
     * @param expression not null expression
     * @param root not null object
     * @param trimRootToken trim root token yes/no.
     * @return the object defined by the expression
     * @throws IntrospectionException if any
     */
    public static Object evaluate( @Nonnull String expression, @Nullable Object root, boolean trimRootToken )
        throws IntrospectionException
    {
        Object value = root;

        // ----------------------------------------------------------------------
        // Walk the dots and retrieve the ultimate value desired from the
        // MavenProject instance.
        // ----------------------------------------------------------------------

        if ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
        {
            return null;
        }

        boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;

        final Tokenizer tokenizer;
        if ( trimRootToken && hasDots )
        {
            tokenizer = new Tokenizer( expression );
            tokenizer.nextPropertyName();
            if ( tokenizer.getPosition() == EOF )
            {
                return null;
            }
        }
        else
        {
            tokenizer = new Tokenizer( "." + expression );
        }

        int propertyPosition = tokenizer.getPosition();
        while ( value != null && tokenizer.peekChar() != EOF )
        {
            switch ( tokenizer.skipChar() )
            {
                case INDEXED_START:
                    value =
                        getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
                                         tokenizer.nextToken( INDEXED_END ) );
                    break;
                case MAPPED_START:
                    value =
                        getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
                                        tokenizer.nextToken( MAPPED_END ) );
                    break;
                case PROPERTY_START:
                    propertyPosition = tokenizer.getPosition();
                    value = getPropertyValue( value, tokenizer.nextPropertyName() );
                    break;
                default:
                    // could not parse expression
                    return null;
            }
        }

        return value;
    }

    private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
                                          final String key )
        throws IntrospectionException
    {
        if ( value == null || key == null )
        {
            return null;
        }

        if ( value instanceof Map )
        {
            Object[] localParams = new Object[] { key };
            ClassMap classMap = getClassMap( value.getClass() );
            try
            {
                Method method = classMap.findMethod( "get", localParams );
                return method.invoke( value, localParams );
            }
            catch ( AmbiguousException e )
            {
                throw new IntrospectionException( e );
            }
            catch ( IllegalAccessException e )
            {
                throw new IntrospectionException( e );
            }
            catch ( InvocationTargetException e )
            {
                throw new IntrospectionException( e.getTargetException() );
            }

        }

        final String message =
            String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value "
                + "seems is an instance of '%s'", expression.subSequence( from, to ), from, value.getClass() );

        throw new IntrospectionException( message );
    }

    private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
                                           final String indexStr )
        throws IntrospectionException
    {
        try
        {
            int index = Integer.parseInt( indexStr );

            if ( value.getClass().isArray() )
            {
                return Array.get( value, index );
            }

            if ( value instanceof List )
            {
                ClassMap classMap = getClassMap( value.getClass() );
                // use get method on List interface
                Object[] localParams = new Object[] { index };
                Method method = null;
                try
                {
                    method = classMap.findMethod( "get", localParams );
                    return method.invoke( value, localParams );
                }
                catch ( AmbiguousException e )
                {
                    throw new IntrospectionException( e );
                }
                catch ( IllegalAccessException e )
                {
                    throw new IntrospectionException( e );
                }
            }
        }
        catch ( NumberFormatException e )
        {
            return null;
        }
        catch ( InvocationTargetException e )
        {
            // catch array index issues gracefully, otherwise release
            if ( e.getCause() instanceof IndexOutOfBoundsException )
            {
                return null;
            }

            throw new IntrospectionException( e.getTargetException() );
        }

        final String message =
            String.format( "The token '%s' at position '%d' refers to a java.util.List or an array, but the value "
                + "seems is an instance of '%s'", expression.subSequence( from, to ), from, value.getClass() );

        throw new IntrospectionException( message );
    }

    private static Object getPropertyValue( Object value, String property )
        throws IntrospectionException
    {
        if ( value == null || property == null )
        {
            return null;
        }

        ClassMap classMap = getClassMap( value.getClass() );
        String methodBase = StringUtils.capitalizeFirstLetter( property );
        String methodName = "get" + methodBase;
        try
        {
            Method method = classMap.findMethod( methodName, CLASS_ARGS );

            if ( method == null )
            {
                // perhaps this is a boolean property??
                methodName = "is" + methodBase;

                method = classMap.findMethod( methodName, CLASS_ARGS );
            }

            if ( method == null )
            {
                return null;
            }

            return method.invoke( value, OBJECT_ARGS );
        }
        catch ( InvocationTargetException e )
        {
            throw new IntrospectionException( e.getTargetException() );
        }
        catch ( AmbiguousException e )
        {
            throw new IntrospectionException( e );
        }
        catch ( IllegalAccessException e )
        {
            throw new IntrospectionException( e );
        }
    }

    private static ClassMap getClassMap( Class<?> clazz )
    {
        ClassMap classMap = CLASS_MAPS.get( clazz );

        if ( classMap == null )
        {
            classMap = new ClassMap( clazz );

            CLASS_MAPS.put( clazz, classMap );
        }

        return classMap;
    }
}
