/*
 * Copyright 2013-2014 Richard M. Hightower
 * 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.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.core.reflection;

import org.boon.*;
import org.boon.core.Conversions;
import org.boon.core.Typ;
import org.boon.core.TypeType;
import org.boon.core.Value;
import org.boon.core.reflection.fields.FieldAccess;
import org.boon.primitive.CharBuf;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.boon.Boon.className;
import static org.boon.Boon.sputs;
import static org.boon.Exceptions.*;
import static org.boon.Str.lines;
import static org.boon.StringScanner.isDigits;
import static org.boon.primitive.CharScanner.isDigit;

/**
 * Created by rick on 12/20/13.
 */
public class BeanUtils {

    /**
     * This method handles walking lists of lists.
     *
     * @param item
     * @param path
     * @return
     */
    public static Object getPropByPath( Object item, String... path ) {

        Object o = item;
        for ( int index = 0; index < path.length; index++ ) {
            String propName = path[ index ];
            if ( o == null ) {
                return null;
            } else if ( o.getClass().isArray() || o instanceof Collection ) {

                o = getCollectionProp(o, propName, index, path);
                break;
            } else {
                o = getProp( o, propName );
            }
        }

        return Conversions.unifyListOrArray(o);
    }

    /**
     * This returns getPropertyFieldFieldAccessMap(clazz, true, true);
     *
     * @param clazz gets the properties or fields of this class.
     * @return
     */
    private static Map<String, FieldAccess> getPropertyFieldAccessMap( Class<?> clazz ) {
        return Reflection.getPropertyFieldAccessMapFieldFirst( clazz );
    }

    public static  FieldAccess getField( Class clazz, String name ) {

        Map<String, FieldAccess> fields = getPropertyFieldAccessMap( clazz );
        if ( fields != null) {
            return fields.get(name);
        } else {
            return null;
        }
    }

    public static  FieldAccess getField( Object object, String name ) {

        Map<String, FieldAccess> fields = getFieldsFromObject( object );
        if ( fields != null) {
            return fields.get(name);
        } else {
            return null;
        }
    }

    public static Map<String, FieldAccess> getFieldsFromObject( Class<?> cls ) {
        return getPropertyFieldAccessMap( cls );
    }

    /**
     * Get fields from object or Map.
     * Allows maps to act like they have fields.
     *
     * @param object
     * @return
     */
    public static Map<String, FieldAccess> getFieldsFromObject( Object object ) {

        try {
           Map<String, FieldAccess> fields;

            if ( object instanceof Map ) {

                fields = getFieldsFromMap( ( Map<String, Object> ) object );
            } else {
                fields = getPropertyFieldAccessMap( object.getClass() );

            }

           return fields;
        } catch (Exception ex) {
           requireNonNull(object, "Item cannot be null" );
           return handle(Map.class, ex, "Unable to get fields from object", className(object));
        }

    }

    /**
     * Get fields from map.
     *
     * @param map
     * @return
     */
    private static Map<String, FieldAccess> getFieldsFromMap(final Map<String, Object> map ) {

        return new Map<String, FieldAccess>() {
            @Override
            public int size() {
                return map.size();
            }

            @Override
            public boolean isEmpty() {
                return map.isEmpty();
            }

            @Override
            public boolean containsKey(Object key) {
                return map.containsKey(key);
            }

            @Override
            public boolean containsValue(Object value) {
                return true;
            }

            @Override
            public FieldAccess get(final Object key) {
                return new FieldAccess() {
                    @Override
                    public boolean injectable() {
                        return false;
                    }

                    @Override
                    public boolean requiresInjection() {
                        return false;
                    }

                    @Override
                    public boolean isNamed() {
                        return false;
                    }

                    @Override
                    public boolean hasAlias() {
                        return false;
                    }

                    @Override
                    public String alias() {
                        return null;
                    }

                    @Override
                    public String named() {
                        return key.toString();
                    }

                    @Override
                    public String name() {
                        return key.toString();
                    }

                    @Override
                    public Object getValue(Object obj) {
                        return map.get(key);
                    }

                    @Override
                    public void setValue(Object obj, Object value) {
                        map.put(key.toString(), value);
                    }

                    @Override
                    public void setFromValue(Object obj, Value value) {
                        map.put(key.toString(), value.toValue());
                    }

                    @Override
                    public boolean getBoolean(Object obj) {
                        return Conversions.toBoolean(getValue(key));
                    }

                    @Override
                    public void setBoolean(Object obj, boolean value) {

                        setValue(map, value);

                    }

                    @Override
                    public int getInt(Object obj) {

                        return Conversions.toInt(getValue(key));
                    }

                    @Override
                    public void setInt(Object obj, int value) {

                        setValue(map, value);
                    }

                    @Override
                    public short getShort(Object obj) {

                        return Conversions.toShort(getValue(key));
                    }

                    @Override
                    public void setShort(Object obj, short value) {

                        setValue(map, value);

                    }

                    @Override
                    public char getChar(Object obj) {

                        return Conversions.toChar(getValue(key));
                    }

                    @Override
                    public void setChar(Object obj, char value) {

                        setValue(map, value);

                    }

                    @Override
                    public long getLong(Object obj) {

                        return Conversions.toChar(getValue(key));
                    }

                    @Override
                    public void setLong(Object obj, long value) {

                        setValue(map, value);
                    }

                    @Override
                    public double getDouble(Object obj) {

                        return Conversions.toDouble(getValue(key));
                    }

                    @Override
                    public void setDouble(Object obj, double value) {

                        setValue(map, value);

                    }

                    @Override
                    public float getFloat(Object obj) {

                        return Conversions.toFloat(getValue(key));
                    }

                    @Override
                    public void setFloat(Object obj, float value) {

                        setValue(map, value);

                    }

                    @Override
                    public byte getByte(Object obj) {

                        return Conversions.toByte(getValue(key));
                    }

                    @Override
                    public void setByte(Object obj, byte value) {

                        setValue(map, value);

                    }

                    @Override
                    public Object getObject(Object obj) {

                        return getValue(obj);
                    }

                    @Override
                    public void setObject(Object obj, Object value) {

                        this.setValue(obj, value);
                    }

                    @Override
                    public TypeType typeEnum() {
                        return TypeType.OBJECT;
                    }

                    @Override
                    public boolean isPrimitive() {
                        return false;
                    }

                    @Override
                    public boolean isFinal() {
                        return false;
                    }

                    @Override
                    public boolean isStatic() {
                        return false;
                    }

                    @Override
                    public boolean isVolatile() {
                        return false;
                    }

                    @Override
                    public boolean isQualified() {
                        return false;
                    }

                    @Override
                    public boolean isReadOnly() {
                        return false;
                    }

                    @Override
                    public boolean isWriteOnly() {
                        return false;
                    }

                    @Override
                    public Class<?> type() {
                        return Object.class;
                    }

                    @Override
                    public Class<?> declaringParent() {
                        return null;
                    }

                    @Override
                    public Object parent() {
                        return map;
                    }

                    @Override
                    public Field getField() {
                        return null;
                    }

                    @Override
                    public boolean include() {
                        return true;
                    }

                    @Override
                    public boolean ignore() {
                        return false;
                    }

                    @Override
                    public ParameterizedType getParameterizedType() {
                        return null;
                    }

                    @Override
                    public Class<?> getComponentClass() {
                        return null;
                    }

                    @Override
                    public boolean hasAnnotation(String annotationName) {
                        return false;
                    }

                    @Override
                    public Map<String, Object> getAnnotationData(String annotationName) {
                        return null;
                    }

                    @Override
                    public boolean isViewActive(String activeView) {
                        return false;
                    }

                    @Override
                    public void setStaticValue(Object newValue) {

                    }

                    @Override
                    public TypeType componentType() {
                        return null;
                    }
                };
            }

            @Override
            public FieldAccess put(String key, FieldAccess value) {
                return null;
            }

            @Override
            public FieldAccess remove(Object key) {
                return null;
            }

            @Override
            public void putAll(Map<? extends String, ? extends FieldAccess> m) {

            }

            @Override
            public void clear() {

            }

            @Override
            public Set<String> keySet() {
                return null;
            }

            @Override
            public Collection<FieldAccess> values() {
                return null;
            }

            @Override
            public Set<Entry<String, FieldAccess>> entrySet() {
                return null;
            }
        };

    }

    /**
     * Get property value, loads nested properties
     *
     * @param root
     * @param properties
     * @return
     */
    public static void setPropertyValue( final Object root, final Object newValue, final String... properties ) {

        Object object = root;

        int index = 0;

        try {

            for ( String property : properties ) {

                Map<String, FieldAccess> fields = getFieldsFromObject( object );
                FieldAccess field = fields.get( property );

                if ( isDigits( property ) ) {
                    /* We can index numbers and names. */
                    object = idx ( object, StringScanner.parseInt ( property ) );

                } else {

                    if ( field == null ) {
                        die( sputs(
                                "We were unable to access property=", property,
                                "\nThe properties passed were=", properties,
                                "\nThe root object is =", root.getClass().getName(),
                                "\nThe current object is =", object.getClass().getName()
                        )
                        );
                    }

                    if ( index == properties.length - 1 ) {
                        field.setValue( object, newValue );
                    } else {
                        object = field.getObject( object );
                    }
                }

                index++;
            }
        } catch (Exception ex) {
            requireNonNull(root, "Root cannot be null");
            handle(ex, "Unable to set property for root object", className(root),
                    "for property path", properties, "with new value", newValue,
                    "last object in the tree was",
                    className(object), "current property index", index);
        }

    }

    /**
     * Get property value, loads nested properties
     *
     * @param root
     * @param properties
     * @return
     */
    public static void setPropertyValue( final Class<?> root, final Object newValue, final String... properties ) {

        Object object = root;

        int index = 0;

        Map<String, FieldAccess> fields = getFieldsFromObject( root );

        try {

            for ( String property : properties ) {

                FieldAccess field = fields.get( property );

                if ( isDigits( property ) ) {
                    /* We can index numbers and names. */
                    object = idx ( object, StringScanner.parseInt ( property ) );

                } else {

                    if ( field == null ) {
                        die( sputs(
                                "We were unable to access property=", property,
                                "\nThe properties passed were=", properties,
                                "\nThe root object is =", root.getClass().getName(),
                                "\nThe current object is =", object.getClass().getName()
                        )
                        );
                    }

                    if ( index == properties.length - 1 ) {
                        if (object instanceof Class) {
                            field.setStaticValue( newValue );
                        } else {
                            field.setValue( object, newValue );

                        }
                    } else {
                        object = field.getObject( object );
                        if (object != null) {
                            fields = getFieldsFromObject( root );
                        }
                    }
                }

                index++;
            }
        } catch (Exception ex) {
            requireNonNull(root, "Root cannot be null");
            handle(ex, "Unable to set property for root object", className(root),
                    "for property path", properties, "with new value", newValue,
                    "last object in the tree was",
                    className(object), "current property index", index);
        }

    }

        /**
         * Get property value, loads nested properties
         *
         * @param root
         * @param properties
         * @return
         */
    public static Object getPropertyValue( final Object root, final String... properties ) {

        Object object = root;

        for ( String property : properties ) {

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

            if (property.equals("this")) {
                if (!(object instanceof Map)) {
                    continue;
                } else {
                    Object aThis = ((Map) object).get("this");
                    if (aThis!=null) {
                        object = aThis;
                        continue;
                    } else {
                        continue;
                    }
                }
            }

            if (object instanceof Map) {
                object = ((Map) object).get(property);
                continue;
            }

            char c = property.charAt(0);
            if ( isDigit(c) ) {
                /* We can index numbers and names. */
                object = idx ( object, StringScanner.parseInt ( property ) );

            } else {

                if (object instanceof Collection) {

                    object = _getFieldValuesFromCollectionOrArray( object, property);
                    continue;
                } else if (Typ.isArray(object)) {

                    Iterator iter = Conversions.iterator(object);
                    List list = Lists.list(iter);
                    object = _getFieldValuesFromCollectionOrArray(list, property);

                    continue;
                }

                Map<String, FieldAccess> fields =
                         getPropertyFieldAccessMap( object.getClass() );

                FieldAccess field = fields.get( property );

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

                object = field.getValue( object );
            }
        }
        return object;
    }

    /**
     * Get property value, loads nested properties
     *
     * @param root
     * @param property
     * @return
     */
    public static Class<?> getPropertyType( final Object root, final String property ) {

        Map<String, FieldAccess> fields = getPropertyFieldAccessMap( root.getClass() );

        FieldAccess field = fields.get( property );
        return field.type();
    }

    @SuppressWarnings ( "unchecked" )
    public static <T> T idxGeneric( Class<T> t, Object object, final String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return ( T ) getPropertyValue( object, properties );

    }

    public static <T> List<T> idxList( Class<T> cls, Object items, String path ) {

        String[] properties = propertyPathAsStringArray(path);
        return ( List<T> ) getPropByPath( items, properties );
    }

    public static List idxList( Object items, String path ) {

        String[] properties = propertyPathAsStringArray(path);
        return ( List ) getPropByPath( items, properties );
    }

    public static <T> List<T> idxRecurse( Class<T> cls, Object items, String path ) {

        String[] properties = propertyPathAsStringArray(path);
        return ( List<T> ) getPropByPath( items, properties );
    }

    public static List idxRecurse( Object items, String path ) {

        String[] properties = propertyPathAsStringArray(path);
        return ( List ) getPropByPath( items, properties );
    }

    /**
     * Get property value
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static Object idx( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyValue( object, properties );
    }

    /**
     * Get property value
     *
     * @deprecated use atIndex or idx.
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static Object indexOf(Object object, String path) {
        return atIndex(object, path);
    }

    /**
     * Get property value
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static Object atIndex(Object object, String path) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyValue( object, properties );
    }

    static Map<String, String[]> splitsPathsCache = new ConcurrentHashMap<>();

    private static String[] propertyPathAsStringArray(String path) {
        String[] split = splitsPathsCache.get(path);
        if (split!=null) {
            return split;
        }

        split =  StringScanner.splitByCharsNoneEmpty(path, '.', '[', ']', '/');
        splitsPathsCache.put(path, split);
        return split;
    }

    public static Object findProperty(Object context, String propertyPath) {

        int index = propertyPath.indexOf('|');

        Object defaultValue;

        if (index!=-1) {

            String[] splitByPipe = Str.splitByPipe(propertyPath);
            defaultValue = splitByPipe[1];
            propertyPath = splitByPipe[0];

        } else {
            defaultValue = null;
        }

        Object object;
        Iterator iterator = Conversions.iterator(context);
        while (iterator.hasNext()) {
                Object ctx = iterator.next();
                object = idx(ctx, propertyPath);
                if (object != null) {

                    if (object instanceof List) {
                        List list = (List) object;
                        int nulls = 0;
                        for (Object o : list) {
                            if (o == null) {
                                nulls++;
                            }
                        }
                        if (nulls == list.size()) {
                            break;
                        }
                    }
                    return object;
                }
        }

        return defaultValue;

    }

    /**
     * Set property value to simulate dependency injection.
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static void injectIntoProperty( Object object, String path, Object value ) {

        String[] properties = propertyPathAsStringArray(path);

        setPropertyValue( object, value, properties );
    }

    /**
     * Set property value
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static void idx( Object object, String path, Object value ) {

        String[] properties = propertyPathAsStringArray(path);

        setPropertyValue( object, value, properties );
    }

    /**
     * Set a static value
     *
     * @param cls
     * @param path   in dotted notation
     * @return
     */
    public static void idx( Class<?> cls, String path, Object value ) {

        String[] properties = propertyPathAsStringArray(path);

        setPropertyValue( cls, value, properties );
    }

    /**
     * This is an amazing little recursive method. It walks a fanout of
     * nested collection to pull out the leaf nodes
     */
    private static Object getCollectionProp(Object o, String propName, int index, String[] path
                                           ) {
        o = _getFieldValuesFromCollectionOrArray(o, propName);

        if ( index + 1 == path.length ) {
            return o;
        } else {
            index++;
            return getCollectionProp(o, path[index], index, path);
        }
    }

    /**
     * This is one is forgiving of null paths.
     * This works with getters first, i.e., properties.
     *
     * @param object
     * @param property
     * @return
     */
    public static Object getProp( Object object, final String property ) {
        if ( object == null ) {
            return null;
        }

        if ( isDigits( property ) ) {
                /* We can index numbers and names. */
            object = idx(object, StringScanner.parseInt(property));

        }

        Class<?> cls = object.getClass();

        /** Tries the getters first. */
        Map<String, FieldAccess> fields = Reflection.getPropertyFieldAccessors( cls );

        if ( !fields.containsKey( property ) ) {
            fields = Reflection.getAllAccessorFields( cls );
        }

        if ( !fields.containsKey( property ) ) {
            return null;
        } else {
            return fields.get( property ).getValue( object );
        }

    }

    /**
     * Get an int property.
     */
    public static int getPropertyInt( final Object root, final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toInt(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.intgr ) {
            return field.getInt( object );
        } else {
            return Conversions.toInt( field.getValue( object ) );
        }

    }

    /**
     * @param root
     * @param properties
     * @return
     */
    private static Object baseForGetProperty( Object root, String[] properties ) {
        Object object = root;

        Map<String, FieldAccess> fields = null;

        for ( int index = 0; index < properties.length - 1; index++ ) {
            if (object == null) {
                return null;
            }

            String property = properties[ index ];

            if (property.equals("this")) {
                continue;
            }

            if ( isDigits( property ) ) {
                /* We can index numbers and names. */
                object = idx ( object, StringScanner.parseInt ( property ) );

            } else {

                if (object instanceof Collection) {
                    object = _getFieldValuesFromCollectionOrArray( object, property);

                    continue;
                } else if (Typ.isArray(object)) {

                    Iterator iter = Conversions.iterator(object);
                    List list = Lists.list(iter);
                    object = _getFieldValuesFromCollectionOrArray(list, property);

                    continue;
                }

                fields = getPropertyFieldAccessMap( object.getClass() );

                FieldAccess field = fields.get( property );

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

                object = field.getObject( object );
            }
        }
        return object;
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    private static Class<?> baseForGetProperty( Class<?> root, String[] properties ) {
        Class cls = root;

        Map<String, FieldAccess> fields = null;

        for ( int index = 0; index < properties.length - 1; index++ ) {
            fields = getPropertyFieldAccessMap( cls );

            String property = properties[ index ];
            FieldAccess field = fields.get( property );
            cls = field.type();
        }
        return cls;
    }
    /**
     * Get property value
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static int idxInt( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyInt( object, properties );
    }

    /**
     * Get property value
     *
     * @param object
     * @param path   in dotted notation
     * @return
     */
    public static String idxStr( Object object, String path ) {

        final Object val = idx(object, path);
        return Conversions.toString(val);
    }

    private static String getPropertyString(Object root, String[] properties) {

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        final String lastProperty = properties[ properties.length - 1 ];
        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.string ) {
            return (String) field.getObject( object );
        } else {
            return Conversions.toString(field.getValue(object));
        }

    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static byte getPropertyByte( final Object root, final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toByte(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.bt ) {
            return field.getByte( object );
        } else {
            return Conversions.toByte( field.getValue( object ) );
        }
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static byte idxByte( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyByte( object, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static float getPropertyFloat( final Object root, final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toFloat(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.flt ) {
            return field.getFloat( object );
        } else {
            return Conversions.toFloat( field.getValue( object ) );
        }
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static float idxFloat( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyFloat( object, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static short getPropertyShort( final Object root,
                                          final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toShort(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );
        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.shrt ) {
            return field.getShort( object );
        } else {
            return Conversions.toShort( field.getValue( object ) );
        }
    }

    /**
     * Get Property Path TypeType
     * @param root
     * @param properties
     * @return
     */
    public static Class<?> getPropertyPathType( final Object root,
                                          final String... properties ) {

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );
        final String lastProperty = properties[ properties.length - 1 ];
        FieldAccess field = fields.get( lastProperty );

        return field.type();
    }

    /**
     * Get Property Path TypeType
     * @param root
     * @param properties
     * @return
     */
    public static FieldAccess getPropertyPathField( final Object root,
                                                final String... properties ) {

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );
        final String lastProperty = properties[ properties.length - 1 ];
        FieldAccess field = fields.get( lastProperty );

        return field;
    }

    /**
     * Get Property Path TypeType
     * @param root
     * @param properties
     * @return
     */
    public static FieldAccess getPropertyPathField( final Class root,
                                                    final String... properties ) {

        Class cls = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( cls );
        final String lastProperty = properties[ properties.length - 1 ];
        FieldAccess field = fields.get( lastProperty );

        return field;
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static short idxShort( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyShort( object, properties );
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static Class idxType( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyPathType( object, properties );
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static FieldAccess idxField( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyPathField( object, properties );
    }

    /**
     * @param cls
     * @param path
     * @return
     */
    public static FieldAccess idxField( Class<?> cls, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyPathField( cls, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static char getPropertyChar( final Object root,
                                        final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toChar(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.chr ) {
            return field.getChar( object );
        } else {
            return Conversions.toChar( field.getValue( object ) );
        }
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static char idxChar( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyChar( object, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static double getPropertyDouble( final Object root,
                                            final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toDouble(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.dbl ) {
            return field.getDouble( object );
        } else {
            return Conversions.toDouble( field.getValue( object ) );
        }
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static double idxDouble( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyDouble( object, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static long getPropertyLong( final Object root,
                                        final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toLong(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.lng ) {
            return field.getLong( object );
        } else {
            return Conversions.toLong( field.getValue( object ) );
        }
    }

    /**
     * @param object
     * @param path
     * @return
     */
    public static long idxLong( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyLong( object, properties );
    }

    /**
     * @param root
     * @param properties
     * @return
     */
    public static boolean getPropertyBoolean( final Object root,
                                              final String... properties ) {

        final String lastProperty = properties[ properties.length - 1 ];

        if ( isDigits( lastProperty ) ) {

            return Conversions.toBoolean(getPropertyValue(root, properties));

        }

        Object object = baseForGetProperty( root, properties );

        Map<String, FieldAccess> fields = getFieldsFromObject( object );

        FieldAccess field = fields.get( lastProperty );

        if ( field.type() == Typ.bln ) {
            return field.getBoolean( object );
        } else {
            return Conversions.toBoolean( field.getValue( object ) );
        }
    }

    public static boolean idxBoolean( Object object, String path ) {

        String[] properties = propertyPathAsStringArray(path);

        return getPropertyBoolean( object, properties );
    }

    public static <V> Map<String, V> collectionToMap( String propertyKey, Collection<V> values ) {
        LinkedHashMap<String, V> map = new LinkedHashMap<>( values.size() );
        for ( V v : values ) {
            String key = idxGeneric( Typ.string, v, propertyKey );
            map.put( key, v );
        }
        return map;
    }

    public static void copyPropertiesFromMap( final Object dest, final Map<String, Object> src ) {

        Set<Map.Entry<String, Object>> props = src.entrySet();
        for ( Map.Entry<String, Object> entry : props ) {

            if (entry.getValue() instanceof Map) {

                Object newDest;
                if (dest instanceof Map) {
                    newDest = ((Map) dest).get(entry.getKey());
                } else {
                    newDest = BeanUtils.getPropByPath(dest, entry.getKey());
                }

                if (newDest == null) {
                    if (dest instanceof Map) {
                        newDest = new LinkedHashMap<>();
                        ((Map) dest).put(entry.getKey(), newDest);
                    }
                }
                final Map<String, Object> newSrc = ((Map) entry.getValue());
                copyPropertiesFromMap(newDest, newSrc);
            } else {
                setPropertyValue(dest, entry.getValue(), entry.getKey());
            }
        }
    }

    public static void copyProperties( Object object, Map<String, Object> properties ) {

        Set<Map.Entry<String, Object>> props = properties.entrySet();
        for ( Map.Entry<String, Object> entry : props ) {
            setPropertyValue( object, entry.getValue(), entry.getKey() );
        }
    }

    private static Object _getFieldValuesFromCollectionOrArray(Object object,
                                                               final String key) {
        if ( object == null ) {
            return null;
        }

        if (object instanceof Collection ) {
            Collection collection = (Collection) object;

            if (collection.size() == 0) {
                return Collections.EMPTY_LIST;
            }
            List list = new ArrayList( collection.size() );

            Class lastClass=null;
            Map<String, FieldAccess> fields=null;
            FieldAccess field=null;

            for (Object item : collection) {

                if (item instanceof Map) {
                    Map map = (Map) item;
                    list.add(map.get(key));
                }else {

                    Class currentClass = Boon.cls(item);

                    if (lastClass==null || currentClass != lastClass) {

                        fields = getPropertyFieldAccessMap(currentClass);
                        field = fields.get(key);
                        lastClass = currentClass;

                    }

                    if (field == null) {
                        list.add(idx(item, key));
                    } else {

                        list.add(field.getValue(item));
                    }
                }
            }
            return list;

        } else if ( object.getClass().isArray() ) {
            int len = Array.getLength(object);
            List list = new ArrayList( len );

            Map<String, FieldAccess> fields =
                    getPropertyFieldAccessMap( object.getClass().getComponentType());

            for (int index = 0; index < len; index++) {
                list.add(  fields.get(key).getValue(Array.get(object, index)));
            }
            return list;
        }
        else {
            return atIndex(object, key);
        }

    }

    public static <T> T copy( T item ) {
        if ( item instanceof Cloneable ) {
             return (T) ClassMeta.classMeta(item.getClass()).invokeUntyped(item, "clone", ( Class[] ) null);
        } else {
            return fieldByFieldCopy( item );
        }
    }

    public static <T> T fieldByFieldCopy( T item ) {

        final Class<T> aClass = (Class<T>) item.getClass();
        Map<String, FieldAccess> fields = Reflection.getAllAccessorFields( aClass );

        T clone = Reflection.newInstance( aClass );

        for ( FieldAccess field : fields.values() ) {

            try {

                /* If the field is static or write only continue. */
                if (field.isStatic() || field.isWriteOnly()) {
                    continue;
                }

                   /* if the field is primitive then just inject it
                    and allow for conversion if needed. */
                if (field.isPrimitive()) {
                    field.setValue(clone, field.getValue(item));
                    continue;
                }

                Object value = field.getObject(item);

                if (value == null ) {

                    field.setObject(clone, null);
                    continue;
                }

               /* If the field is not a basic type and not a primitive then
                   then recursively copy a version into the field field.
                 */
                if (!field.isPrimitive() && !Typ.isBasicType(field.type())) {

                     field.setObject(clone, copy(value));

                    continue;
                }

                    /* It was a basic type so just copy a reference to it. */
                /* It is a basic type. */
                if (Typ.isBasicType(field.type())) {

                     field.setObject(clone, value);

                    continue;
                }

                if (Typ.isCollection(field.type())) {

                    Collection<Object> src = ( Collection<Object> ) value;
                    Class<?> collectionType = field.type();
                    Collection<Object> dst = Conversions.createCollection(collectionType, src.size());

                    for (Object o : src) {
                        dst.add( copy ( o ) );
                    }

                    field.setObject(clone, dst);

                    continue;
                }

                /** We don't handle maps yet. */
                if (Typ.isMap(field.type())) {

                    continue;
                }

                if (field.type().isArray()) {

                    int length = Array.getLength(value);
                    Object dst = Array.newInstance(field.getComponentClass(), length);

                    for (int index =0; index < length; index++) {
                        Object o = Array.get(value, index);
                        Array.set(dst, index, copy (o));
                    }

                    field.setObject(clone, dst);

                    continue;
                }

            } catch (Exception ex) {

                return (T) Exceptions.handle(Object.class, "" + field,  ex );
            }
        }
        return clone;
    }

    public static void copyProperties( Object src, Object dest ) {
         fieldByFieldCopy( src, dest );
    }

    public static <T> T createFromSrc( Object src, Class<T> dest ) {
        T instance = Reflection.newInstance(dest);
        fieldByFieldCopy( src, instance );
        return instance;
    }

    public static void copyProperties( Object src, Object dest, String... ignore) {
        fieldByFieldCopy( src, dest, Sets.set ( ignore ) );
    }

    public static void copyProperties( Object src, Object dest, Set<String> ignore) {
        fieldByFieldCopy( src, dest, ignore );
    }

    private static void fieldByFieldCopy( Object src, Object dst, Set<String> ignore ) {

        final Class<?> srcClass = src.getClass();
        Map<String, FieldAccess> srcFields = Reflection.getAllAccessorFields( srcClass );

        final Class<?> dstClass =  dst.getClass();
        Map<String, FieldAccess> dstFields = Reflection.getAllAccessorFields ( dstClass );

        for ( FieldAccess srcField : srcFields.values() ) {

            if (ignore.contains ( srcField.name() )) {
                continue;
            }

            FieldAccess dstField = dstFields.get ( srcField.name() );
            try {

                copySrcFieldToDestField ( src, dst, dstField, srcField, ignore );

            }catch (Exception ex) {
                Exceptions.handle( sputs("copying field", srcField.name(), srcClass, " to ", dstField.name(), dstClass), ex );
            }
        }
    }

    private static void fieldByFieldCopy( Object src, Object dst ) {

        final Class<?> srcClass = src.getClass();
        Map<String, FieldAccess> srcFields = Reflection.getAllAccessorFields( srcClass );

        final Class<?> dstClass =  dst.getClass();
        Map<String, FieldAccess> dstFields = Reflection.getAllAccessorFields ( dstClass );

        for ( FieldAccess srcField : srcFields.values() ) {

            FieldAccess dstField = dstFields.get ( srcField.name() );
            try {

                copySrcFieldToDestField ( src, dst, dstField, srcField, null );

            }catch (Exception ex) {
                 Exceptions.handle( sputs("copying field", srcField.name(), srcClass, " to ", dstField.name(), dstClass), ex );
            }
        }
    }

    private static void copySrcFieldToDestField( Object src, Object dst, FieldAccess dstField, FieldAccess srcField, Set<String> ignore ) {
        if ( srcField.isStatic() ) {
            return ;
        }

        if (dstField == null ) {
            return ;
        }

                /* If its primitive handle it. */
        if ( srcField.isPrimitive() ) {
            dstField.setValue( dst, srcField.getValue( src ) );
            return ;
        }

        Object srcValue = srcField.getObject( src );

                /* if value is null then handle it unless it is primitive.*/
        if (srcValue == null) {
            if ( !dstField.isPrimitive () ) {
                dstField.setObject(dst, null);
            }
            return ;
        }

                /* Basic type. */
        if ( Typ.isBasicType( srcField.type() ) ) {
            /* Handle non primitive copy. */
            Object value = srcField.getObject( src );
            dstField.setValue( dst,  value  );
            return ;
        }

                /* Types match and not a collection so just copy. */
        if (    !(srcValue instanceof Collection ) && dstField.type() == srcValue.getClass() ||
                Typ.isSuperType ( dstField.type(), srcValue.getClass () ) ) {

            dstField.setObject(dst, copy( srcField.getObject ( src ) ));
            return ;
        }

                /* Collection field copy. */
        if ( srcValue instanceof Collection && dstField.getComponentClass() != null
                            && Typ.isCollection ( dstField.type() )
                            ) {

            handleCollectionFieldCopy ( dst, dstField, ( Collection ) srcValue );
            return ;

        }

                      /* Non identical object copy. */
        if (dstField.typeEnum () == TypeType.ABSTRACT || dstField.typeEnum () == TypeType.INTERFACE) {
                            //no op
        } else {
                Object newInstance = Reflection.newInstance ( dstField.type() );
                if (ignore == null) {
                    fieldByFieldCopy( srcField.getObject( src ), newInstance );
                } else {
                    fieldByFieldCopy( srcField.getObject( src ), newInstance, ignore );
                }
                dstField.setObject ( dst, newInstance );
        }
    }

    private static void handleCollectionFieldCopy( Object dst, FieldAccess dstField, Collection srcValue ) {
        if ( dstField.getComponentClass () != Typ.string )  {

            Collection dstCollection = Conversions.createCollection( dstField.type(), srcValue.size() );
            for ( Object srcComponentValue : srcValue ) {

                Object newInstance = Reflection.newInstance( dstField.getComponentClass() );
                fieldByFieldCopy( srcComponentValue, newInstance );
                dstCollection.add ( newInstance );
            }

            dstField.setObject ( dst, dstCollection );
        } else {

            Collection dstCollection = Conversions.createCollection( dstField.type(), srcValue.size() );
            for ( Object srcComponentValue : srcValue ) {

                if (srcComponentValue!=null) {
                    dstCollection.add ( srcComponentValue.toString () );
                }
            }

            dstField.setObject ( dst, dstCollection );

        }
    }

    public static Object idx( Object object, int index ) {
        if ( Boon.isArray( object ) ) {
            object = Array.get ( object, index );
        } else if ( object instanceof List ) {
            object = Lists.idx ( ( List ) object, index );
        }
        return object;
    }

    public static void idx( Object object, int index, Object value ) {
        try {
            if ( Boon.isArray( object ) ) {
                Array.set( object, index, value );
            } else if ( object instanceof List ) {
                Lists.idx( ( List ) object, index, value );
            }
        } catch ( Exception notExpected ) {
            String msg = lines( "An unexpected error has occurred",
                    "This is likely a programming error!",
                    String.format( "Object is %s, index is %s, and set is %s", object, index, value ),
                    String.format( "The object is an array? %s", object == null ? "null" : object.getClass().isArray() ),
                    String.format( "The object is of type %s", object == null ? "null" : object.getClass().getName() ),
                    String.format( "The set is of type %s", value == null ? "null" : value.getClass().getName() ),

                    ""

            );
            Exceptions.handle( msg, notExpected );
        }
    }

    public static <T> T idx( Class<T> type, Object object, String property ) {
        return (T) idx(object, property);
    }

    public static <T> T atIndex( Class<T> type, Object object, String property ) {
        return (T) idx(object, property);
    }

    /**
     * Is this a property path?
     * @param prop property
     * @return true or false
     */
    public static boolean isPropPath(String prop) {
        if (prop.contains(".")) return true;
        if (prop.equals("this")) return true;
        if (prop.contains("[")) return true;

        return false;
    }

    public static void setCollectionProperty(Collection<?> list, String propertyName, Object value) {
        for (Object object : list) {
            BeanUtils.idx(object, propertyName, value);
        }
    }

    public static void setIterableProperty(Iterable<?> list, String propertyName, Object value) {
        for (Object object : list) {
            BeanUtils.idx(object, propertyName, value);
        }
    }

    public static String asPrettyJsonString(Object bean) {
        CharBuf buf = CharBuf.createCharBuf();
        return buf.prettyPrintBean(bean).toString();
    }

    public static String asPrettyJsonString(Mapper mapper, Object bean) {
        CharBuf buf = CharBuf.createCharBuf();
        return buf.prettyPrintBean(mapper, bean).toString();
    }

}


/*
 * Copyright 2013-2014 Richard M. Hightower
 * 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.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.core.reflection;

import org.boon.Exceptions;
import org.boon.Lists;
import org.boon.core.Function;
import org.boon.Pair;
import org.boon.core.Sys;
import org.boon.core.Typ;
import org.boon.core.reflection.fields.FieldAccess;
import org.boon.core.reflection.fields.PropertyField;
import org.boon.core.reflection.fields.ReflectField;
import org.boon.core.reflection.fields.UnsafeField;
import sun.misc.Unsafe;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import static org.boon.Boon.sputs;
import static org.boon.Exceptions.die;
import static org.boon.Str.lower;
import static org.boon.Str.slc;

public class Reflection {

    public static List<Field> getFields( Class<? extends Object> theClass ) {

        List<Field> fields = context().__fields.get(theClass);
        if (fields!=null) {
            return fields;
        }

        fields = Lists.list( theClass.getDeclaredFields() );
        boolean foundCrap = false;
        for ( Field field : fields ) {
            if (field.getName().indexOf('$')!=-1) {
                foundCrap = true;
                continue;
            }
            field.setAccessible( true );
        }

        if (foundCrap) {
            List<Field> copy = Lists.copy(fields);

            for (Field field : copy) {
                if (field.getName().indexOf('$')!=-1) {
                   fields.remove(field);
                }

            }
        }

        return fields;
    }

    private static final Logger log = Logger.getLogger( Reflection.class.getName() );

    private static boolean _useUnsafe;

    static {
        try {
            Class.forName( "sun.misc.Unsafe" );
            _useUnsafe = true;
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace();
            _useUnsafe = false;
        }

        _useUnsafe = _useUnsafe && !Boolean.getBoolean( "org.boon.noUnsafe" );
    }

    private static final boolean useUnsafe = _useUnsafe;

    private final static Context _context;
    private static WeakReference<Context> weakContext = new WeakReference<>( null );

    static {

        boolean noStatics = Boolean.getBoolean( "org.boon.noStatics" );
        if ( noStatics || Sys.inContainer() ) {

            _context = null;
            weakContext = new WeakReference<>( new Context() );

        } else {
            ;
            _context = new Context();
        }
    }

    public static Unsafe getUnsafe() {
        if ( context().control == null ) {
            try {
                Field f = Unsafe.class.getDeclaredField( "theUnsafe" );
                f.setAccessible( true );
                context().control = ( Unsafe ) f.get( null );
                return context().control;
            } catch ( Exception e ) {
                return null;
            }
        } else {
            return context().control;
        }
    }

    public static Object contextToHold() {
        return context();
    }

    /* Manages weak references. */
    static Context context() {

        if ( _context != null ) {
            return _context;
        } else {
            Context context = weakContext.get();
            if ( context == null ) {
                context = new Context();
                weakContext = new WeakReference<>( context );
            }
            return context;
        }
    }

    static class Context {

        Map<Class<?>, List<Field>> __fields = new ConcurrentHashMap<>( 200 );

        Unsafe control;
        Map<String, String> _sortableFields = new ConcurrentHashMap<>();

        Map<Class<?>, ClassMeta<?>> _classMetaMap = new ConcurrentHashMap<>( 200 );

        Map<Class<?>, Map<String, FieldAccess>> _allAccessorReflectionFieldsCache = new ConcurrentHashMap<>( 200 );
        Map<Class<?>, Map<String, FieldAccess>> _allAccessorPropertyFieldsCache = new ConcurrentHashMap<>( 200 );
        Map<Class<?>, Map<String, FieldAccess>> _allAccessorUnsafeFieldsCache = new ConcurrentHashMap<>( 200 );

        Map<Class<?>, Map<String, FieldAccess>> _combinedFieldsFieldsFirst = new ConcurrentHashMap<>( 200 );
        Map<Class<?>, Map<String, FieldAccess>> _combinedFieldsFieldsFirstForSerializer = new ConcurrentHashMap<>( 200 );

        Map<Class<?>, Map<String, FieldAccess>> _combinedFieldsPropertyFirst = new ConcurrentHashMap<>( 200 );
        Map<Class<?>, Map<String, FieldAccess>> _combinedFieldsPropertyFirstForSerializer = new ConcurrentHashMap<>( 200 );

    }

    private static Map<String, FieldAccess> getCombinedFieldsPropertyFirst(Class<? extends Object> theClass) {
        return context()._combinedFieldsPropertyFirst.get(theClass);
    }

    private static Map<String, FieldAccess> getCombinedFieldsPropertyFirstForSerializer(Class<? extends Object> theClass) {
        return context()._combinedFieldsPropertyFirstForSerializer.get(theClass);
    }

    private static Map<String, FieldAccess> getCombinedFieldsFieldFirst(Class<? extends Object> theClass) {
        return context()._combinedFieldsFieldsFirst.get(theClass);
    }

    private static Map<String, FieldAccess> getCombinedFieldsFieldFirstForSerializer(Class<? extends Object> theClass) {
        return context()._combinedFieldsFieldsFirstForSerializer.get(theClass);
    }

    private static void  putCombinedFieldsPropertyFirst(Class<?> theClass, Map<String, FieldAccess> map) {
        context()._combinedFieldsPropertyFirst.put(theClass, map);
    }

    private static void  putCombinedFieldsPropertyFirstForSerializer(Class<?> theClass, Map<String, FieldAccess> map) {
        context()._combinedFieldsPropertyFirst.put(theClass, map);
    }

    private static void putCombinedFieldsFieldFirst(Class<?> theClass,  Map<String, FieldAccess> map) {
        context()._combinedFieldsFieldsFirst.put(theClass, map);
    }

    private static void putCombinedFieldsFieldFirstForSerializer(Class<?> theClass,  Map<String, FieldAccess> map) {
        context()._combinedFieldsFieldsFirstForSerializer.put(theClass, map);
    }

    static {
        try {
            if ( _useUnsafe ) {
                Field field = String.class.getDeclaredField( "value" );
            }
        } catch ( Exception ex ) {
            Exceptions.handle( ex );
        }
    }

    private static void setAccessorFieldInCache( Class<? extends Object> theClass, boolean useUnsafe, Map<String, FieldAccess> map ) {
        if ( useUnsafe ) {
            context()._allAccessorUnsafeFieldsCache.put( theClass, map );
        } else {
            context()._allAccessorReflectionFieldsCache.put( theClass, map );

        }
    }

    private static void setPropertyAccessorFieldsInCache( Class<? extends Object> theClass, Map<String, FieldAccess> map ) {
        context()._allAccessorPropertyFieldsCache.put( theClass, map );
    }

    private static Map<String, FieldAccess> getPropertyAccessorFieldsFromCache( Class<? extends Object> theClass ) {
        return context()._allAccessorPropertyFieldsCache.get( theClass );
    }

    private static Map<String, FieldAccess> getAccessorFieldsFromCache(Class<? extends Object> theClass, boolean useUnsafe) {

        if ( useUnsafe ) {
            return context()._allAccessorUnsafeFieldsCache.get( theClass );
        } else {
            return context()._allAccessorReflectionFieldsCache.get( theClass );

        }
    }

    /**
     * Gets a list of fields merges with properties if field is not found.
     *
     * @param clazz get the properties or fields
     * @return
     */
    public static Map<String, FieldAccess> getPropertyFieldAccessMapFieldFirst( Class<?> clazz ) {
        Map<String, FieldAccess> combinedFieldsFieldFirst = getCombinedFieldsFieldFirst(clazz);

        if (combinedFieldsFieldFirst!=null) {
            return combinedFieldsFieldFirst;
        } else {

            /* Fallback map. */
            Map<String, FieldAccess> fieldsFallbacks = null;

            /* Primary merge into this one. */
            Map<String, FieldAccess> fieldsPrimary = null;

             /* Try to find the fields first if this is set. */
            fieldsPrimary = Reflection.getAllAccessorFields(clazz, true);

            fieldsFallbacks = Reflection.getPropertyFieldAccessors(clazz);

            combineFieldMaps(fieldsFallbacks, fieldsPrimary);

            combinedFieldsFieldFirst = fieldsPrimary;

            putCombinedFieldsFieldFirst(clazz, combinedFieldsFieldFirst);
            return combinedFieldsFieldFirst;

        }

    }

    /**
     * Gets a list of fields merges with properties if field is not found.
     *
     * @param clazz get the properties or fields
     * @return
     */
    public static Map<String, FieldAccess> getPropertyFieldAccessMapFieldFirstForSerializer( Class<?> clazz ) {
        Map<String, FieldAccess> combinedFieldsFieldFirst = getCombinedFieldsFieldFirstForSerializer(clazz);

        if (combinedFieldsFieldFirst!=null) {
            return combinedFieldsFieldFirst;
        } else {

            /* Fallback map. */
            Map<String, FieldAccess> fieldsFallbacks = null;

            /* Primary merge into this one. */
            Map<String, FieldAccess> fieldsPrimary = null;

             /* Try to find the fields first if this is set. */
            fieldsPrimary = Reflection.getAllAccessorFields(clazz, true);
            fieldsFallbacks = Reflection.getPropertyFieldAccessors(clazz);

            fieldsPrimary = removeNonSerializable(fieldsPrimary);
            fieldsFallbacks = removeNonSerializable(fieldsFallbacks);

            combineFieldMaps(fieldsFallbacks, fieldsPrimary);

            combinedFieldsFieldFirst = fieldsPrimary;

            putCombinedFieldsFieldFirstForSerializer(clazz, combinedFieldsFieldFirst);
            return combinedFieldsFieldFirst;

        }

    }

    private static void combineFieldMaps( Map<String, FieldAccess> fieldsFallbacks, Map<String, FieldAccess> fieldsPrimary ) {
    /* Add missing fields */
        for ( Map.Entry<String, FieldAccess> field : fieldsFallbacks.entrySet() ) {
            if ( !fieldsPrimary.containsKey( field.getKey() ) ) {
                fieldsPrimary.put( field.getKey(), field.getValue() );
            }
        }
    }

    public static Map<String, FieldAccess> getPropertyFieldAccessMapPropertyFirst( Class<?> clazz ) {

        Map<String, FieldAccess> combinedFields = getCombinedFieldsPropertyFirst(clazz);

        if (combinedFields!=null) {
            return combinedFields;
        } else {
             /* Fallback map. */
            Map<String, FieldAccess> fieldsFallbacks = null;

            /* Primary merge into this one. */
            Map<String, FieldAccess> fieldsPrimary = null;

             /* Try to find the properties first if this is set. */
            fieldsFallbacks = Reflection.getAllAccessorFields(clazz, true);
            fieldsPrimary = Reflection.getPropertyFieldAccessors(clazz);

            /* Add missing fields */
            combineFieldMaps(fieldsFallbacks, fieldsPrimary);

            combinedFields = fieldsPrimary;
            putCombinedFieldsPropertyFirst(clazz, combinedFields);
            return combinedFields;
        }
    }

    public static Map<String, FieldAccess> getPropertyFieldAccessMapPropertyFirstForSerializer( Class<?> clazz ) {

        Map<String, FieldAccess> combinedFields = getCombinedFieldsPropertyFirstForSerializer(clazz);

        if (combinedFields!=null) {
            return combinedFields;
        } else {
             /* Fallback map. */
            Map<String, FieldAccess> fieldsFallbacks = null;

            /* Primary merge into this one. */
            Map<String, FieldAccess> fieldsPrimary = null;

             /* Try to find the properties first if this is set. */
            fieldsFallbacks = Reflection.getAllAccessorFields(clazz, true);
            fieldsFallbacks = removeNonSerializable(fieldsFallbacks);

            fieldsPrimary = Reflection.getPropertyFieldAccessors(clazz);
            fieldsPrimary = removeNonSerializable(fieldsPrimary);

            /* Add missing fields */
            combineFieldMaps(fieldsFallbacks, fieldsPrimary);

            combinedFields = fieldsPrimary;
            putCombinedFieldsPropertyFirstForSerializer(clazz, combinedFields);
            return combinedFields;
        }
    }

    private static Map<String, FieldAccess> removeNonSerializable(Map<String, FieldAccess> fieldAccessMap) {

        LinkedHashMap<String, FieldAccess> map = new LinkedHashMap<>(fieldAccessMap);
        final List<String> set = new ArrayList(fieldAccessMap.keySet());
        for (String key : set) {
            final FieldAccess fieldAccess = fieldAccessMap.get(key);
            if (fieldAccess.isStatic() || fieldAccess.ignore() ) {
                map.remove(key);
            }
        }
        return map;
    }

    @SuppressWarnings ( "serial" )
    public static class ReflectionException extends RuntimeException {

        public ReflectionException() {
            super();
        }

        public ReflectionException( String message, Throwable cause ) {
            super( message, cause );
        }

        public ReflectionException( String message ) {
            super( message );
        }

        public ReflectionException( Throwable cause ) {
            super( cause );
        }
    }

    private static void handle( Exception ex ) {
        throw new ReflectionException( ex );
    }

    public static Class<?> loadClass( String className ) {

        try {
            Class<?> clazz = Class.forName( className );

            return clazz;

        } catch ( Exception ex ) {
            log.info( String.format( "Unable to create load class %s", className ) );
            return null;
        }
    }
    public static Object newInstance( String className ) {

        try {
            Class<?> clazz = Class.forName( className );

            return newInstance( clazz );

        } catch ( Exception ex ) {
            log.info( String.format( "Unable to create this class %s", className ) );
            return null;
        }
    }

    public static <T> T newInstance( Class<T> clazz ) {
        T newInstance = null;
        ClassMeta <T> cls = ClassMeta.classMeta(clazz);

        try {
            /* See if there is a no arg constructor. */
            ConstructorAccess<T> declaredConstructor = cls.noArgConstructor();
            if (declaredConstructor !=null ) {
                /* If there was a no argument constructor, then use it. */
                newInstance = declaredConstructor.create();
            } else {
                if ( _useUnsafe ) {
                    newInstance = ( T ) getUnsafe().allocateInstance( clazz );
                } else {
                    die ( sputs( clazz.getName (), "does not have a no arg constructor and unsafe is not turned on" ) );
                }

            }
        } catch ( Exception ex ) {
            try {
                if ( _useUnsafe ) {
                    newInstance = ( T ) getUnsafe().allocateInstance( clazz );
                    return newInstance; //we handled it.
                }
            } catch ( Exception ex2 ) {
                handle( ex2 );
            }

            handle( ex );
        }

        return newInstance;

    }

    public static <T> T newInstance( Class<T> clazz, Object arg ) {
        T newInstance = null;

        ClassMeta <T> cls = ClassMeta.classMeta(clazz);
         try {
            /* See if there is a no arg constructor. */
            ConstructorAccess<T> declaredConstructor = cls.declaredConstructor(arg.getClass());
            if (declaredConstructor !=null ) {
                /* If there was a no argument constructor, then use it. */
                newInstance = declaredConstructor.create(arg);
            }
        } catch ( Exception ex ) {
            handle( ex );
        }
        return newInstance;
    }

    public static Class<?> getComponentType( Collection<?> collection, FieldAccess fieldAccess ) {
        Class<?> clz = fieldAccess.getComponentClass();
        if ( clz == null ) {
            clz = getComponentType( collection );
        }
        return clz;

    }

    public static Class<?> getComponentType( Collection<?> value ) {
        if ( value.size() > 0 ) {
            Object next = value.iterator().next();
            return next.getClass();
        } else {
            return Typ.object;
        }
    }

    private static class FieldConverter implements Function<Field, FieldAccess> {

        boolean thisUseUnsafe;

        FieldConverter( boolean useUnsafe ) {
            this.thisUseUnsafe = useUnsafe;
        }

        @Override
        public FieldAccess apply( Field from ) {
            if ( useUnsafe && thisUseUnsafe ) {
                return UnsafeField.createUnsafeField( from );
            } else {
                return new ReflectField( from );
            }
        }
    }

    public static Map<String, FieldAccess> getAllAccessorFields(
            Class<? extends Object> theClass ) {
        return getAllAccessorFields( theClass, true );
    }

    public static Map<String, FieldAccess> getAllAccessorFields(
            Class<? extends Object> theClass, boolean useUnsafe ) {
        Map<String, FieldAccess> map = getAccessorFieldsFromCache(theClass, useUnsafe);
        if ( map == null ) {
            List<FieldAccess> list = Lists.mapBy( getAllFields( theClass ), new FieldConverter( useUnsafe ) );
            map = new LinkedHashMap<>( list.size() );
            for ( FieldAccess fieldAccess : list ) {
                map.put( fieldAccess.name(), fieldAccess );
            }
            setAccessorFieldInCache( theClass, useUnsafe, map );
        }
        return map;
    }

    public static List<Field> getAllFields( Class<? extends Object> theClass ) {

        try {
            List<Field> list = getFields( theClass );
            while ( theClass != Typ.object ) {

                theClass = theClass.getSuperclass();
                getFields( theClass, list );
            }
            return list;
        } catch (Exception ex) {
            return  Exceptions.handle(List.class, ex, "getAllFields the class", theClass);
        }
    }

    public static Map<String, FieldAccess> getPropertyFieldAccessors(
            Class<? extends Object> theClass ) {

        Map<String, FieldAccess> fields = getPropertyAccessorFieldsFromCache( theClass );
        if ( fields == null ) {
            Map<String, Pair<Method, Method>> methods = getPropertySetterGetterMethods( theClass );

            fields = new LinkedHashMap<>();

            for ( Map.Entry<String, Pair<Method, Method>> entry :
                    methods.entrySet() ) {

                final Pair<Method, Method> methodPair = entry.getValue();
                final String key = entry.getKey();

                PropertyField pf = new PropertyField( key, methodPair.getFirst(), methodPair.getSecond() );

                fields.put( pf.alias(), pf );

            }

            setPropertyAccessorFieldsInCache( theClass, fields );
        }

        return fields;
    }

    public static Map<String, Pair<Method, Method>> getPropertySetterGetterMethods(
            Class<? extends Object> theClass ) {

        try {
            Method[] methods = theClass.getMethods();

            Map<String, Pair<Method, Method>> methodMap = new LinkedHashMap<>( methods.length );
            List<Method> getterMethodList = new ArrayList<>( methods.length );

            for ( int index = 0; index < methods.length; index++ ) {
                Method method = methods[ index ];
                if (extractPropertyInfoFromMethodPair(methodMap, getterMethodList, method)) continue;
            }

            for ( Method method : getterMethodList ) {
                extractProperty(methodMap, method);

            }
            return methodMap;
        } catch (Exception ex) {
            ex.printStackTrace();
            return Exceptions.handle(Map.class, ex, theClass);
        }
    }

    private static boolean extractPropertyInfoFromMethodPair(Map<String, Pair<Method, Method>> methodMap,
                                                             List<Method> getterMethodList,
                                                             Method method) {
        String name = method.getName();

        try {

            if ( method.getParameterTypes().length == 1
                    && name.startsWith( "set" ) ) {
                Pair<Method, Method> pair = new Pair<>();
                pair.setFirst( method );
                String propertyName = slc( name, 3 );

                propertyName = lower( slc( propertyName, 0, 1 ) ) + slc( propertyName, 1 );
                methodMap.put( propertyName, pair );
            }

            if ( method.getParameterTypes().length > 0
                    || method.getReturnType() == void.class
                    || !( name.startsWith( "get" ) || name.startsWith( "is" ) )
                    || name.equals( "getClass" ) || name.equals("get") || name.equals("is") ) {
                return true;
            }
            getterMethodList.add( method );
            return false;

        } catch (Exception ex) {
            return Exceptions.handle(Boolean.class, ex, name, method);
        }
    }

    private static void extractProperty(Map<String, Pair<Method, Method>> methodMap,
                                        Method method) {
        try {
            String name = method.getName();
            String propertyName = null;
            if ( name.startsWith( "is" ) ) {
                propertyName = name.substring( 2 );
            } else if ( name.startsWith( "get" ) ) {
                propertyName = name.substring( 3 );
            }

            propertyName = lower( propertyName.substring( 0, 1 ) ) + propertyName.substring( 1 );

            Pair<Method, Method> pair = methodMap.get( propertyName );
            if ( pair == null ) {
                pair = new Pair<>();
                methodMap.put( propertyName, pair );
            }
            pair.setSecond(method);

        } catch (Exception ex) {
            Exceptions.handle(ex, "extractProperty property extract of getPropertySetterGetterMethods", method);
        }
    }

    public static void getFields( Class<? extends Object> theClass,
                                  List<Field> list ) {

        try {
            List<Field> more = getFields( theClass );
            list.addAll( more );

        }catch (Exception ex) {
            Exceptions.handle(ex, "getFields", theClass, list);
        }
    }

    public static boolean respondsTo( Class<?> type, String methodName) {
        return ClassMeta.classMeta(type).respondsTo(methodName);
    }

    public static boolean respondsTo( Class<?> type, String methodName, Class<?>... params) {
        return ClassMeta.classMeta(type).respondsTo(methodName, params);
    }

    public static boolean respondsTo( Class<?> type, String methodName, Object... params) {
        return ClassMeta.classMeta(type).respondsTo(methodName, params);
    }

    public static boolean respondsTo( Class<?> type, String methodName, List<?> params) {
        return ClassMeta.classMeta(type).respondsTo(methodName, params);
    }

    public static boolean respondsTo( Object object, String methodName) {
        if (object == null || methodName == null) {
            return false;
        }
        return ClassMeta.classMeta(object.getClass()).respondsTo(methodName);
    }

    public static boolean respondsTo( Object object, String methodName, Class<?>... params) {
        return ClassMeta.classMeta(object.getClass()).respondsTo(methodName, params);
    }

    public static boolean respondsTo( Object object, String methodName, Object... params) {
        return ClassMeta.classMeta(object.getClass()).respondsTo(methodName, params);
    }

    public static boolean respondsTo( Object object, String methodName, List<?> params) {
        return ClassMeta.classMeta(object.getClass()).respondsTo(methodName, params);
    }

    public static boolean handles( Object object, Class<?> interfaceCls) {
        return ClassMeta.classMeta(object.getClass()).handles(interfaceCls);
    }

    public static boolean handles( Class cls, Class<?> interfaceCls) {
        return ClassMeta.classMeta(cls).handles(interfaceCls);
    }

    public static Object invoke (Object object, String name, Object... args){
        return ClassMeta.classMeta( object.getClass() ).invokeUntyped(object, name, args );
    }

    public static Object invoke (Object object, String name, List<?> args){
        return ClassMeta.classMeta( object.getClass() ).invokeUntyped(object, name, args.toArray(new Object[args.size()]));
    }
}


/*
 * Copyright 2013-2014 Richard M. Hightower
 * 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.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.messages;

import org.boon.validation.ValidationContext;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * Contains information about how to generate a message.
 * This class knows how to create a message.
 * It will look up the message in the resource bundle if it
 * starts with a "{".
 * <p/>
 * Future: It will look up the message in the
 * EL context if it starts with a "#{"
 */
public class MessageSpecification implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * The detailMessage part of the message.
     */
    private String detailMessage = "detailMessage";
    /**
     * The summaryMessage part of the message.
     */
    private String summaryMessage = "summaryMessage";
    /**
     * Arguments that get passed to the detailMessage message.
     */
    private List<String> detailArgs;
    /**
     * Arguments that get passed to the summaryMessage message.
     */
    private List<String> summaryArgs;

    /**
     * The name of this message specification, used to look up information
     * in the resource bundle if needed.
     */
    private String name;
    /**
     * Used to create messages that inherit properties from their parents.
     */
    private String parent;

    /**
     * Who is this message about? For example for field validation
     * the subject is the name of the field.
     */
    private String subject = "";

    private boolean noSummary;

    private static final String SUMMARY_KEY = ".summary";
    private static final String DETAIL_KEY = ".detail";

    /**
     * The init method tries to generate the message keys.
     * You should only call the init method if you don't inject
     * values into the detailMessage and summaryMessage.
     */
    public void init() {
        /* If the parent and name are equal to null,
         * use the classname to load listFromClassLoader.
    	 * */
        if ( name == null && parent == null ) {
            this.setDetailMessage( "{" + this.getClass().getName() + DETAIL_KEY + "}" );
            this.setSummaryMessage( "{" + this.getClass().getName() + SUMMARY_KEY + "}" );
        /* If the parent is null and the name is not,
         * use the name to load listFromClassLoader.
         */
        } else if ( name != null && parent == null ) {
            this.setDetailMessage( "{" + "message." + getName() + DETAIL_KEY + "}" );
            this.setSummaryMessage( "{" + "message." + getName() + SUMMARY_KEY + "}" );
        /* If the parent is present, initialize the message keys
         * with the parent name.
         */
        } else if ( parent != null ) {
            this.setDetailMessage( "{" + "message." + parent + DETAIL_KEY + "}" );
            this.setSummaryMessage( "{" + "message." + parent + SUMMARY_KEY + "}" );
        }
    }

    public boolean isNoSummary() {
        return noSummary;
    }

    public void setNoSummary( boolean noSummary ) {
        this.noSummary = noSummary;
    }

    /**
     * Create the summaryMessage message.
     */
    public String createSummaryMessage( Object... args ) {
        return createMessage( summaryMessage, summaryArgs, args );
    }

    /**
     * Create the detailMessage message.
     */
    public String createDetailMessage( Object... args ) {
        return createMessage( detailMessage, detailArgs, args );
    }

    /**
     * Creates a message.
     *
     * @param argKeys arguments to lookupWithDefault that were configured.
     * @param args    Arguments that were passed via the object that wants to
     *                generate the message
     */
    public String createMessage( String key, List<String> argKeys, Object... args ) {
        /* Look up the message. */
        String message = getMessage( key );

    	/* Holds the actual arguments. */
        Object[] actualArgs;

    	/* If they passed arguments,
         * then use this as the actual arguments. */
        if ( args.length > 0 ) {
            actualArgs = args;
        /* If they did not pass arguments, use the configured ones. */
        } else if ( argKeys != null ) {
    		/* Convert the keys to values. */
            actualArgs = keysToValues( argKeys );
        } else {
            actualArgs = new Object[]{ };
        }

        return doCreateMessage( message, actualArgs );

    }

    /**
     * Actually creates the message.
     *
     * @param message    The message that was looked up.
     * @param actualArgs Arguments to the message.
     * @return
     */
    @SuppressWarnings ( "unchecked" )
    private String doCreateMessage( String message, Object[] actualArgs ) {

        return ValidationContext.get().createMessage( message, getSubject(), actualArgs );
    }

    private String getMessage( String key ) {

        return ValidationContext.get().getMessage( key );
    }

    /**
     * Convert the keys to values.
     */
    private Object[] keysToValues( List<String> argKeys ) {
        List<String> values = new ArrayList<>();
        for ( String key : argKeys ) {
            values.add( getMessage( key ) );
        }
        return values.toArray();
    }

    /**
     * Allows client objects to set the subject for the current thread
     * per instance of the MessageSpecification.
     */
    public void setCurrentSubject( String subject ) {
        ValidationContext.get().setCurrentSubject( subject );
    }

    /**
     * Gets the current subject or the configured subject if the
     * current subject is not found.
     */
    public String getSubject() {
        return ValidationContext.get().getCurrentSubject() == null ? this.subject :
                ValidationContext.get().getCurrentSubject();
    }

    protected String getDetailMessage() {
        return this.detailMessage;
    }

    public void setDetailMessage( String detailKey ) {
        this.detailMessage = detailKey;
    }

    protected String getSummaryMessage() {
        return this.summaryMessage;
    }

    public void setSummaryMessage( String summaryKey ) {
        this.summaryMessage = summaryKey;
    }

    protected List<String> getDetailArgs() {
        return this.detailArgs;
    }

    public void setDetailArgs( List<String> argKeys ) {
        this.detailArgs = argKeys;
    }

    protected List<String> getSummaryArgs() {
        return this.summaryArgs;
    }

    public void setSummaryArgs( List<String> summaryArgKeys ) {
        this.summaryArgs = summaryArgKeys;
    }

    public void setName( String aName ) {
        this.name = aName;
    }

    public String getName() {
        return this.name;
    }

    public void setParent( String parent ) {
        this.parent = parent;
    }

    public void setSubject( String subject ) {
        this.subject = subject;
    }

}


/*
 * Copyright 2013-2014 Richard M. Hightower
 * 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.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */
package org.boon.json;

import org.boon.IO;
import org.boon.HTTP;
import org.boon.json.implementation.JsonParserUsingCharacterSource;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;

/**
 * JSON slurper which parses text or reader content into a data structure of lists and maps.
 *

 * Example usage:
 *

 * def slurper = new JsonSlurper()
 * def result = slurper.parseText('{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}')
 *
 * assert result.person.name == "Guillaume"
 * assert result.person.age == 33
 * assert result.person.pets.size() == 2
 * assert result.person.pets[0] == "dog"
 * assert result.person.pets[1] == "cat"
 *

 *
 * @author Guillaume Laforge
 * @author Rick Hightower
 * @since 1.8.0
 */
public class JsonSlurper {

    /**
     * Parse a text representation of a JSON data structure
     *
     * @param text JSON text to parse
     * @return a data structure of lists and maps
     */
    public Object parseText(String text) {
        if (text == null || text.length() == 0) {
            throw new IllegalArgumentException("The JSON input text should neither be null nor empty.");
        }

        return JsonFactory.create().fromJson ( text );
    }

    /**
     * Parse a JSON data structure from content from a reader
     *
     * @param reader reader over a JSON content
     * @return a data structure of lists and maps
     */
    public Object parse(Reader reader) {
         return new JsonParserUsingCharacterSource (  ).parse ( reader );
    }

    /**
     * Parse a JSON data structure from content within a given File.
     *
     * @param file File containing JSON content
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(File file) {
        return parseFile(file, null);
    }

    /**
     * Parse a JSON data structure from content within a given File.
     *
     * @param file File containing JSON content
     * @param charset the charset for this File
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(File file, String charset) {
        return parseFile(file, charset);
    }

    /** Slight changes to remove groovy dependencies .*/
    private Object parseFile(File file, String scharset) {
        Charset charset = scharset==null || scharset.length ()==0 ? StandardCharsets.UTF_8 : Charset.forName ( scharset );

        if (file.length() > 2_000_000) {
            try (Reader reader = Files.newBufferedReader( IO.path ( file.toString () ), charset )) {
                return parse(reader);
            } catch(IOException ioe) {
                throw new JsonException("Unable to process file: " + file.getPath(), ioe);
            }
        } else {
            try {
                return JsonFactory.create().fromJson ( Files.newBufferedReader( IO.path ( file.toString () ), charset ) );
            } catch ( IOException e ) {
                throw new JsonException("Unable to process file: " + file.getPath(), e);
            }
        }
    }

    /**
     * Parse a JSON data structure from content at a given URL.
     *
     * @param url URL containing JSON content
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(URL url) {
        return parseURL(url, null);
    }

    /**
     * Parse a JSON data structure from content at a given URL.
     *
     * @param url URL containing JSON content
     * @param params connection parameters
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(URL url, Map params) {
        return parseURL(url, params);
    }

    /**
     * Parse a JSON data structure from content at a given URL. Convenience variant when using Groovy named parameters for the connection params.
     *
     * @param params connection parameters
     * @param url URL containing JSON content
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(Map params, URL url) {
        return parseURL(url, params);
    }

    private Object parseURL(URL url, Map params) {
            return parse(new StringReader ( IO.read ( url.toString () )));
    }

    /**
     * Parse a JSON data structure from content at a given URL.
     *
     * @param url URL containing JSON content
     * @param charset the charset for this File
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(URL url, String charset) {
        return parseURL(url, null, charset);
    }

    /**
     * Parse a JSON data structure from content at a given URL.
     *
     * @param url URL containing JSON content
     * @param params connection parameters
     * @param charset the charset for this File
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(URL url, Map params, String charset) {
        return parseURL(url, params, charset);
    }

    /**
     * Parse a JSON data structure from content at a given URL. Convenience variant when using Groovy named parameters for the connection params.
     *
     * @param params connection parameters
     * @param url URL containing JSON content
     * @param charset the charset for this File
     * @return a data structure of lists and maps
     * @since 2.2.0
     */
    public Object parse(Map params, URL url, String charset) {
        return parseURL(url, params, charset);
    }

    private Object parseURL(URL url, Map params, String charset) {
            return parse(new StringReader( HTTP.getJSONWithParams ( url.toString (), null, params )));
    }
}