/*
 * 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.
 */
package org.qi4j.api.composite;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.qi4j.api.Qi4j;
import org.qi4j.api.property.GenericPropertyInfo;
import org.qi4j.api.property.Property;
import org.qi4j.api.util.Classes;
import org.qi4j.api.util.Dates;
import org.qi4j.api.value.ValueComposite;

/**
 * Transfer java.util.Properties to Composite properties
 */
public final class PropertyMapper
{

    private final static Map<Type, MappingStrategy> STRATEGY;

    static
    {
        STRATEGY = new HashMap<>();
        STRATEGY.put( Integer.class, new IntegerMapper() );
        STRATEGY.put( Long.class, new LongMapper() );
        STRATEGY.put( Short.class, new ShortMapper() );
        STRATEGY.put( Byte.class, new ByteMapper() );
        STRATEGY.put( String.class, new StringMapper() );
        STRATEGY.put( Character.class, new CharMapper() );
        STRATEGY.put( Float.class, new FloatMapper() );
        STRATEGY.put( Double.class, new DoubleMapper() );
        STRATEGY.put( Date.class, new DateMapper() );
        STRATEGY.put( Boolean.class, new BooleanMapper() );
        STRATEGY.put( BigDecimal.class, new BigDecimalMapper() );
        STRATEGY.put( BigInteger.class, new BigIntegerMapper() );
        STRATEGY.put( Enum.class, new EnumMapper() );
        STRATEGY.put( Array.class, new ArrayMapper() );
        STRATEGY.put( Map.class, new MapMapper() );
        STRATEGY.put( List.class, new ListMapper() );
        STRATEGY.put( Set.class, new SetMapper() );
        STRATEGY.put( ValueComposite.class, new ValueCompositeMapper() );
    }

    /**
     * Populate the Composite with properties from the given properties object.
     *
     * @param props     properties object
     * @param composite the composite instance
     *
     * @throws IllegalArgumentException if properties could not be transferred to composite
     */
    public static void map( Properties props, Composite composite )
        throws IllegalArgumentException
    {
        for( Map.Entry<Object, Object> objectObjectEntry : props.entrySet() )
        {
            try
            {
                String methodName = objectObjectEntry.getKey().toString();
                Method propertyMethod = composite.getClass().getInterfaces()[ 0 ].getMethod( methodName );
                propertyMethod.setAccessible( true );
                Object value = objectObjectEntry.getValue();
                Type propertyType = GenericPropertyInfo.propertyTypeOf( propertyMethod );

                value = mapToType( composite, propertyType, value.toString() );

                @SuppressWarnings( "unchecked" )
                Property<Object> property = (Property<Object>) propertyMethod.invoke( composite );
                property.set( value );
            }
            catch( NoSuchMethodException e )
            {
                throw new IllegalArgumentException( "Could not find any property named " + objectObjectEntry.getKey() );
            }
            catch( IllegalAccessException e )
            {
                //noinspection ThrowableInstanceNeverThrown
                throw new IllegalArgumentException( "Could not populate property named " + objectObjectEntry.getKey(), e );
            }
            catch( InvocationTargetException e )
            {
                //noinspection ThrowableInstanceNeverThrown
                String message = "Could not populate property named " + objectObjectEntry.getKey();
                throw new IllegalArgumentException( message, e );
            }
        }
    }

    @SuppressWarnings( "raw" )
    private static Object mapToType( Composite composite, Type propertyType, Object value )
    {
        final String stringValue = value.toString();
        MappingStrategy strategy;
        if( propertyType instanceof Class )
        {
            Class type = (Class) propertyType;
            if( type.isArray() )
            {
                strategy = STRATEGY.get( Array.class );
            }
            else if( Enum.class.isAssignableFrom( Classes.RAW_CLASS.map( propertyType ) ) )
            {
                strategy = STRATEGY.get( Enum.class );
            }
            else
            {
                strategy = STRATEGY.get( type );
            }
            if( strategy == null  ) // If null, try with the ValueComposite Mapper...
            {
                strategy = STRATEGY.get( ValueComposite.class );
            }
        }
        else if( propertyType instanceof ParameterizedType )
        {
            ParameterizedType type = ( (ParameterizedType) propertyType );

            if( type.getRawType() instanceof Class )
            {
                Class clazz = (Class) type.getRawType();
                if( List.class.isAssignableFrom( clazz ) )
                {
                    strategy = STRATEGY.get( List.class );
                }
                else if( Set.class.isAssignableFrom( clazz ) )
                {
                    strategy = STRATEGY.get( Set.class );
                }
                else if( Map.class.isAssignableFrom( clazz ) )
                {
                    strategy = STRATEGY.get( Map.class );
                }
                else
                {
                    throw new IllegalArgumentException( propertyType + " is not supported." );
                }
            }
            else
            {
                throw new IllegalArgumentException( propertyType + " is not supported." );
            }
        }
        else
        {
            throw new IllegalArgumentException( propertyType + " is not supported." );
        }

        if( strategy == null )
        {
            throw new IllegalArgumentException( propertyType + " is not supported." );
        }

        return strategy.map( composite, propertyType, stringValue );
    }

    /**
     * Load a Properties object from the given stream, close it, and then populate
     * the Composite with the properties.
     *
     * @param propertyInputStream properties input stream
     * @param composite           the instance
     *
     * @throws IOException if the stream could not be read
     */

    public static void map( InputStream propertyInputStream, Composite composite )
        throws IOException
    {
        if( propertyInputStream != null )
        {
            Properties configProps = new Properties();
            try
            {
                configProps.load( propertyInputStream );
            }
            finally
            {
                propertyInputStream.close();
            }
            map( configProps, composite );
        }
    }

    /**
     * Create Properties object which is backed by the given Composite.
     *
     * @param composite the instance
     *
     * @return properties instance
     */
    public static Properties toJavaProperties( final Composite composite )
    {
        return new Properties()
        {
            private static final long serialVersionUID = 3550125427530538865L;

            @Override
            public Object get( Object o )
            {
                try
                {
                    Method propertyMethod = composite.getClass().getMethod( o.toString() );
                    Property<?> property = (Property<?>) propertyMethod.invoke( composite );
                    return property.get();
                }
                catch( NoSuchMethodException | IllegalAccessException | InvocationTargetException e )
                {
                    return null;
                }
            }

            @Override
            public Object put( Object o, Object o1 )
            {
                Object oldValue = get( o );

                try
                {
                    Method propertyMethod = composite.getClass().getMethod( o.toString(), Object.class );
                    propertyMethod.invoke( composite, o1 );
                }
                catch( NoSuchMethodException | IllegalAccessException | InvocationTargetException e )
                {
                    e.printStackTrace();
                }

                return oldValue;
            }
        };
    }

    private static void tokenize( String valueString, boolean mapSyntax, TokenizerCallback callback )
    {
        char[] data = valueString.toCharArray();

        int oldPos = 0;
        for( int pos = 0; pos < data.length; pos++ )
        {
            char ch = data[ pos ];
            if( ch == '\"' )
            {
                pos = resolveQuotes( valueString, callback, data, pos, '\"' );
                oldPos = pos;
            }
            if( ch == '\'' )
            {
                pos = resolveQuotes( valueString, callback, data, pos, '\'' );
                oldPos = pos;
            }
            if( ch == ',' || ( mapSyntax && ch == ':' ) )
            {
                String token = new String( data, oldPos, pos - oldPos );
                callback.token( token );
                oldPos = pos + 1;
            }
        }
        String token = new String( data, oldPos, data.length - oldPos );
        callback.token( token );
    }

    private static int resolveQuotes( String valueString,
                                      TokenizerCallback callback,
                                      char[] data,
                                      int pos, char quote
    )
    {
        boolean found = false;
        for( int j = pos + 1; j < data.length; j++ )
        {
            if( !found )
            {
                if( data[ j ] == quote )
                {
                    String token = new String( data, pos + 1, j - pos - 1 );
                    callback.token( token );
                    found = true;
                }
            }
            else
            {
                if( data[ j ] == ',' )
                {
                    return j + 1;
                }
            }
        }
        if( !found )
        {
            throw new IllegalArgumentException( "String is not quoted correctly: " + valueString );
        }
        return data.length;
    }

    private interface TokenizerCallback
    {
        void token( String token );
    }

    private interface MappingStrategy
    {
        Object map( Composite composite, Type type, String value );
    }

    private static class StringMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return value;
        }
    }

    private static class IntegerMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Integer( value.trim() );
        }
    }

    private static class FloatMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Float( value.trim() );
        }
    }

    private static class DoubleMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Double( value.trim() );
        }
    }

    private static class LongMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Long( value.trim() );
        }
    }

    private static class ShortMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Short( value.trim() );
        }
    }

    private static class ByteMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new Byte( value.trim() );
        }
    }

    private static class CharMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return value.trim().charAt( 0 );
        }
    }

    private static class BigDecimalMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new BigDecimal( value.trim() );
        }
    }

    private static class BigIntegerMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return new BigInteger( value.trim() );
        }
    }

    private static class EnumMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( "unchecked" )
        public Object map( Composite composite, Type type, String value )
        {
            return Enum.valueOf( (Class<Enum>) type, value );
        }
    }

    private static class DateMapper
        implements MappingStrategy
    {
        @Override
        public Object map( Composite composite, Type type, String value )
        {
            return Dates.fromString( value.trim() );
        }
    }

    private static class ValueCompositeMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( "unchecked" )
        public Object map( Composite composite, Type type, String value )
        {
            return Qi4j.FUNCTION_COMPOSITE_INSTANCE_OF.map( composite ).module().newValueFromSerializedState( (Class<Object>) type, value );
        }
    }

    private static class ArrayMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( {"raw", "unchecked"} )
        public Object map( final Composite composite, Type type, String value )
        {
            final Class arrayType = ( (Class) type ).getComponentType();
            final ArrayList result = new ArrayList();
            tokenize( value, false, new TokenizerCallback()
            {
                @Override
                public void token( String token )
                {
                    result.add( mapToType( composite, arrayType, token ) );
                }
            } );
            return result.toArray( (Object[]) Array.newInstance( arrayType, result.size() ) );
        }
    }

    private static class BooleanMapper
        implements MappingStrategy
    {
        @Override
        public Object map( final Composite composite, Type type, String value )
        {
            return Boolean.valueOf( value.trim() );
        }
    }

    private static class ListMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( {"raw", "unchecked"} )
        public Object map( final Composite composite, Type type, String value )
        {
            final Type dataType = ( (ParameterizedType) type ).getActualTypeArguments()[ 0 ];
            final Collection result = new ArrayList();
            tokenize( value, false, new TokenizerCallback()
            {
                @Override
                public void token( String token )
                {
                    result.add( mapToType( composite, dataType, token ) );
                }
            } );
            return result;
        }
    }

    private static class SetMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( {"raw", "unchecked"} )
        public Object map( final Composite composite, Type type, String value )
        {
            final Type dataType = ( (ParameterizedType) type ).getActualTypeArguments()[ 0 ];
            final Collection result = new HashSet();
            tokenize( value, false, new TokenizerCallback()
            {
                @Override
                public void token( String token )
                {
                    result.add( mapToType( composite, dataType, token ) );
                }
            } );
            return result;
        }
    }

    private static class MapMapper
        implements MappingStrategy
    {
        @Override
        @SuppressWarnings( {"raw", "unchecked"} )
        public Object map( final Composite composite, Type generictype, String value )
        {
            ParameterizedType type = (ParameterizedType) generictype;
            final Type keyType = type.getActualTypeArguments()[ 0 ];
            final Type valueType = type.getActualTypeArguments()[ 0 ];
            final Map result = new HashMap();
            tokenize( value, true, new TokenizerCallback()
            {
                boolean keyArrivingNext = true;
                String key;

                @Override
                public void token( String token )
                {
                    if( keyArrivingNext )
                    {
                        key = token;
                        keyArrivingNext = false;
                    }
                    else
                    {
                        result.put( mapToType( composite, keyType, key ), mapToType( composite, valueType, token ) );
                        keyArrivingNext = true;
                    }
                }
            } );
            return result;
        }
    }

    private PropertyMapper()
    {
    }
}
