/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.plexus;

import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.inject.Inject;

import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.codehaus.plexus.util.xml.pull.MXParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.sisu.bean.BeanProperties;
import org.eclipse.sisu.bean.BeanProperty;
import org.eclipse.sisu.inject.Logs;
import org.eclipse.sisu.inject.TypeArguments;

import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.TypeConverter;
import com.google.inject.spi.TypeConverterBinding;

/**
 * {@link PlexusBeanConverter} {@link Module} that converts Plexus XML configuration into beans.
 */
@Singleton
public final class PlexusXmlBeanConverter
    implements PlexusBeanConverter
{
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final String CONVERSION_ERROR = "Cannot convert: \"%s\" to: %s";

    // ----------------------------------------------------------------------
    // Implementation fields
    // ----------------------------------------------------------------------

    private final Collection<TypeConverterBinding> typeConverterBindings;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    @Inject
    PlexusXmlBeanConverter( final Injector injector )
    {
        typeConverterBindings = injector.getTypeConverterBindings();
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    @SuppressWarnings( { "unchecked", "rawtypes" } )
    public Object convert( final TypeLiteral role, final String value )
    {
        if ( value.trim().startsWith( "<" ) )
        {
            try
            {
                final MXParser parser = new MXParser();
                parser.setInput( new StringReader( value ) );
                parser.nextTag();

                return parse( parser, role );
            }
            catch ( final Exception e )
            {
                throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, role ), e );
            }
        }

        return convertText( value, role );
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    /**
     * Parses a sequence of XML elements and converts them to the given target type.
     * 
     * @param parser The XML parser
     * @param toType The target type
     * @return Converted instance of the target type
     */
    private Object parse( final MXParser parser, final TypeLiteral<?> toType )
        throws Exception
    {
        parser.require( XmlPullParser.START_TAG, null, null );

        final Class<?> rawType = toType.getRawType();
        if ( Xpp3Dom.class.isAssignableFrom( rawType ) )
        {
            return parseXpp3Dom( parser );
        }
        if ( Properties.class.isAssignableFrom( rawType ) )
        {
            return parseProperties( parser );
        }
        if ( Map.class.isAssignableFrom( rawType ) )
        {
            return parseMap( parser, TypeArguments.get( toType.getSupertype( Map.class ), 1 ) );
        }
        if ( Collection.class.isAssignableFrom( rawType ) )
        {
            return parseCollection( parser, TypeArguments.get( toType.getSupertype( Collection.class ), 0 ) );
        }
        if ( rawType.isArray() )
        {
            return parseArray( parser, TypeArguments.get( toType, 0 ) );
        }
        return parseBean( parser, toType, rawType );
    }

    /**
     * Parses an XML subtree and converts it to the {@link Xpp3Dom} type.
     * 
     * @param parser The XML parser
     * @return Converted Xpp3Dom instance
     */
    private static Xpp3Dom parseXpp3Dom( final XmlPullParser parser )
        throws Exception
    {
        return Xpp3DomBuilder.build( parser );
    }

    /**
     * Parses a sequence of XML elements and converts them to the appropriate {@link Properties} type.
     * 
     * @param parser The XML parser
     * @return Converted Properties instance
     */
    private static Properties parseProperties( final XmlPullParser parser )
        throws Exception
    {
        final Properties properties = newImplementation( parser, Properties.class );
        while ( parser.nextTag() == XmlPullParser.START_TAG )
        {
            parser.nextTag();
            // 'name-then-value' or 'value-then-name'
            if ( "name".equals( parser.getName() ) )
            {
                final String name = parser.nextText();
                parser.nextTag();
                properties.put( name, parser.nextText() );
            }
            else
            {
                final String value = parser.nextText();
                parser.nextTag();
                properties.put( parser.nextText(), value );
            }
            parser.nextTag();
        }
        return properties;
    }

    /**
     * Parses a sequence of XML elements and converts them to the appropriate {@link Map} type.
     * 
     * @param parser The XML parser
     * @return Converted Map instance
     */
    private Map<String, Object> parseMap( final MXParser parser, final TypeLiteral<?> toType )
        throws Exception
    {
        @SuppressWarnings( "unchecked" )
        final Map<String, Object> map = newImplementation( parser, HashMap.class );
        while ( parser.nextTag() == XmlPullParser.START_TAG )
        {
            map.put( parser.getName(), parse( parser, toType ) );
        }
        return map;
    }

    /**
     * Parses a sequence of XML elements and converts them to the appropriate {@link Collection} type.
     * 
     * @param parser The XML parser
     * @return Converted Collection instance
     */
    private Collection<Object> parseCollection( final MXParser parser, final TypeLiteral<?> toType )
        throws Exception
    {
        @SuppressWarnings( "unchecked" )
        final Collection<Object> collection = newImplementation( parser, ArrayList.class );
        while ( parser.nextTag() == XmlPullParser.START_TAG )
        {
            collection.add( parse( parser, toType ) );
        }
        return collection;
    }

    /**
     * Parses a sequence of XML elements and converts them to the appropriate array type.
     * 
     * @param parser The XML parser
     * @return Converted array instance
     */
    private Object parseArray( final MXParser parser, final TypeLiteral<?> toType )
        throws Exception
    {
        // convert to a collection first then convert that into an array
        final Collection<?> collection = parseCollection( parser, toType );
        final Object array = Array.newInstance( toType.getRawType(), collection.size() );

        int i = 0;
        for ( final Object element : collection )
        {
            Array.set( array, i++, element );
        }

        return array;
    }

    /**
     * Parses a sequence of XML elements and converts them to the appropriate bean type.
     * 
     * @param parser The XML parser
     * @return Converted bean instance
     */
    private Object parseBean( final MXParser parser, final TypeLiteral<?> toType, final Class<?> rawType )
        throws Exception
    {
        final Class<?> clazz = loadImplementation( parseImplementation( parser ), rawType );

        // simple bean? assumes string constructor
        if ( parser.next() == XmlPullParser.TEXT )
        {
            final String text = parser.getText();

            // confirm element doesn't contain nested XML
            if ( parser.next() != XmlPullParser.START_TAG )
            {
                return convertText( text, clazz == rawType ? toType : TypeLiteral.get( clazz ) );
            }
        }

        if ( String.class == clazz )
        {
            // mimic plexus: discard any strings containing nested XML
            while ( parser.getEventType() == XmlPullParser.START_TAG )
            {
                final String pos = parser.getPositionDescription();
                Logs.warn( "Expected TEXT, not XML: {}", pos, new Throwable() );
                parser.skipSubTree();
                parser.nextTag();
            }
            return "";
        }

        final Object bean = newImplementation( clazz );

        // build map of all known bean properties belonging to the chosen implementation
        final Map<String, BeanProperty<Object>> propertyMap = new HashMap<String, BeanProperty<Object>>();
        for ( final BeanProperty<Object> property : new BeanProperties( clazz ) )
        {
            final String name = property.getName();
            if ( !propertyMap.containsKey( name ) )
            {
                propertyMap.put( name, property );
            }
        }

        while ( parser.getEventType() == XmlPullParser.START_TAG )
        {
            // update properties inside the bean, guided by the cached property map
            final BeanProperty<Object> property = propertyMap.get( Roles.camelizeName( parser.getName() ) );
            if ( property != null )
            {
                property.set( bean, parse( parser, property.getType() ) );
                parser.nextTag();
            }
            else
            {
                throw new XmlPullParserException( "Unknown bean property: " + parser.getName(), parser, null );
            }
        }

        return bean;
    }

    /**
     * Parses an XML element looking for the name of a custom implementation.
     * 
     * @param parser The XML parser
     * @return Name of the custom implementation; otherwise {@code null}
     */
    private static String parseImplementation( final XmlPullParser parser )
    {
        return parser.getAttributeValue( null, "implementation" );
    }

    /**
     * Attempts to load the named implementation, uses default implementation if no name is given.
     * 
     * @param name The optional implementation name
     * @param defaultClazz The default implementation type
     * @return Custom implementation type if one was given; otherwise default implementation type
     */
    private static Class<?> loadImplementation( final String name, final Class<?> defaultClazz )
    {
        if ( null == name )
        {
            return defaultClazz; // just use the default type
        }

        // TCCL allows surrounding container to influence class loading policy
        final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        if ( tccl != null )
        {
            try
            {
                return tccl.loadClass( name );
            }
            catch ( final Exception e )
            {
                // drop through...
            }
            catch ( final LinkageError e )
            {
                // drop through...
            }
        }

        // assume custom type is in same class space as default
        final ClassLoader peer = defaultClazz.getClassLoader();
        if ( peer != null )
        {
            try
            {
                return peer.loadClass( name );
            }
            catch ( final Exception e )
            {
                // drop through...
            }
            catch ( final LinkageError e )
            {
                // drop through...
            }
        }

        try
        {
            // last chance - classic model
            return Class.forName( name );
        }
        catch ( final Exception e )
        {
            throw new TypeNotPresentException( name, e );
        }
        catch ( final LinkageError e )
        {
            throw new TypeNotPresentException( name, e );
        }
    }

    /**
     * Creates an instance of the given implementation using the default constructor.
     * 
     * @param clazz The implementation type
     * @return Instance of given implementation
     */
    private static <T> T newImplementation( final Class<T> clazz )
    {
        try
        {
            return clazz.newInstance();
        }
        catch ( final Exception e )
        {
            throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
        }
        catch ( final LinkageError e )
        {
            throw new IllegalArgumentException( "Cannot create instance of: " + clazz, e );
        }
    }

    /**
     * Creates an instance of the given implementation using the given string, assumes a public string constructor.
     * 
     * @param clazz The implementation type
     * @param value The string argument
     * @return Instance of given implementation, constructed using the the given string
     */
    private static <T> T newImplementation( final Class<T> clazz, final String value )
    {
        try
        {
            return clazz.getConstructor( String.class ).newInstance( value );
        }
        catch ( final Exception e )
        {
            final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e;
            throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), cause );
        }
        catch ( final LinkageError e )
        {
            throw new IllegalArgumentException( String.format( CONVERSION_ERROR, value, clazz ), e );
        }
    }

    /**
     * Creates an instance of the implementation named in the current XML element, or the default if no name is given.
     * 
     * @param parser The XML parser
     * @param defaultClazz The default implementation type
     * @return Instance of custom implementation if one was given; otherwise instance of default type
     */
    @SuppressWarnings( "unchecked" )
    private static <T> T newImplementation( final XmlPullParser parser, final Class<T> defaultClazz )
    {
        return (T) newImplementation( loadImplementation( parseImplementation( parser ), defaultClazz ) );
    }

    /**
     * Converts the given string to the target type, using {@link TypeConverter}s registered with the {@link Injector}.
     * 
     * @param value The string value
     * @param toType The target type
     * @return Converted instance of the target type
     */
    private Object convertText( final String value, final TypeLiteral<?> toType )
    {
        final String text = value.trim();

        final Class<?> rawType = toType.getRawType();
        if ( rawType.isAssignableFrom( String.class ) )
        {
            return text; // compatible type => no conversion needed
        }

        // use temporary Key as quick way to auto-box primitive types into their equivalent object types
        final TypeLiteral<?> boxedType = rawType.isPrimitive() ? Key.get( rawType ).getTypeLiteral() : toType;

        for ( final TypeConverterBinding b : typeConverterBindings )
        {
            if ( b.getTypeMatcher().matches( boxedType ) )
            {
                return b.getTypeConverter().convert( text, toType );
            }
        }

        // last chance => attempt to create an instance of the expected type: use the string if non-empty
        return text.length() == 0 ? newImplementation( rawType ) : newImplementation( rawType, text );
    }
}
