/*
 * Copyright 2012 astamuse company,Ltd.
 * 
 * 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.
 * 
 */

package com.astamuse.asta4d.data;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.astamuse.asta4d.Configuration;
import com.astamuse.asta4d.Context;
import com.astamuse.asta4d.data.annotation.ContextData;
import com.astamuse.asta4d.data.annotation.ContextDataSet;
import com.astamuse.asta4d.util.Java8Paranamer;
import com.astamuse.asta4d.util.Java8ParanamerBytecodeScanWrapper;
import com.astamuse.asta4d.util.annotation.AnnotatedPropertyInfo;
import com.astamuse.asta4d.util.annotation.AnnotatedPropertyUtil;
import com.astamuse.asta4d.util.annotation.ConvertableAnnotationRetriever;
import com.thoughtworks.paranamer.AdaptiveParanamer;
import com.thoughtworks.paranamer.AnnotationParanamer;
import com.thoughtworks.paranamer.DefaultParanamer;
import com.thoughtworks.paranamer.Paranamer;

/**
 * This class is a function holder to supply functionalities about data injection
 * 
 * @author e-ryu
 * 
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class InjectUtil {

    private final static Logger logger = LoggerFactory.getLogger(InjectUtil.class);

    private static final String ContextDataSetSingletonMapKey = "ContextDataSetSingletonMapKey#" + InjectUtil.class.getName();

    public static final String ContextDataNotFoundScope = "#ContextDataNotFoundScope";
    public static final String ContextDataTypeUnMatchScope = "#ContextDataTypeUnMatchScope";

    /**
     * A class that present the injectable target information
     * 
     * @author e-ryu
     * 
     */
    private static class TargetInfo {
        AnnotatedPropertyInfo propertyInfo;
        String name;
        String scope;
        TypeUnMacthPolicy typeUnMatch;
        ContextDataSetFactory contextDataSetFactory;
        boolean isContextDataSetSingletonInContext;
        Class<?> type;
        boolean isContextDataHolder;
        Object defaultValue;

        void fixForPrimitiveType() {
            TypeInfo typeInfo = new TypeInfo(type);
            type = typeInfo.getType();
            defaultValue = typeInfo.getDefaultValue();
        }

        ContextDataHolder createDataHolderInstance() throws InstantiationException, IllegalAccessException {
            if (isContextDataHolder) {
                return (ContextDataHolder) type.newInstance();
            } else {
                return new ContextDataHolder(type);
            }
        }

    }

    private static class MethodInfo extends TargetInfo {
        Method method;

    }

    private static class FieldInfo extends TargetInfo {
        Field field;

    }

    private static class InstanceWireTarget {
        List<FieldInfo> setFieldList = new ArrayList<>();
        List<FieldInfo> getFieldList = new ArrayList<>();
        List<MethodInfo> setMethodList = new ArrayList<>();
        List<MethodInfo> getMethodList = new ArrayList<>();
    }

    private final static ConcurrentHashMap<String, InstanceWireTarget> InstanceTargetCache = new ConcurrentHashMap<>();

    private final static ConcurrentHashMap<Method, List<TargetInfo>> MethodTargetCache = new ConcurrentHashMap<>();

    private final static Paranamer paranamer = new AdaptiveParanamer(new DefaultParanamer(), new Java8ParanamerBytecodeScanWrapper(),
            new AnnotationParanamer(), new Java8Paranamer());

    public final static Object retrieveContextDataSetInstance(Class cls, String searchName, String searchScope)
            throws DataOperationException {
        try {
            ContextDataSet cds = ConvertableAnnotationRetriever.retrieveAnnotation(ContextDataSet.class, cls.getAnnotations());
            if (cds == null) {
                throw new NullPointerException("Could not find any ContextDataSet convertable annotation on given class:" + cls.getName());
            }
            TargetInfo info = new TargetInfo();
            info.contextDataSetFactory = cds.factory().newInstance();
            info.isContextDataSetSingletonInContext = cds.singletonInContext();
            info.defaultValue = null;
            info.name = searchName;
            info.scope = searchScope;
            info.type = cls;
            info.typeUnMatch = TypeUnMacthPolicy.EXCEPTION;
            ContextDataHolder result = findValueForTarget(info, null);
            if (result == null) {
                return null;
            } else {
                return result.getValue();
            }
        } catch (IllegalAccessException | InstantiationException e) {
            throw new DataOperationException(e.getMessage(), e);
        }
    }

    /**
     * Set the value of all the fields marked by {@link ContextData} of the given instance.
     * 
     * @param instance
     * @throws DataOperationException
     */
    public final static void injectToInstance(Object instance) throws DataOperationException {

        InstanceWireTarget target = getInstanceTarget(instance);

        for (FieldInfo fi : target.setFieldList) {
            try {
                ContextDataHolder valueHolder = null;
                if (fi.isContextDataHolder) {
                    valueHolder = (ContextDataHolder) FieldUtils.readField(fi.field, instance);
                }
                if (valueHolder == null) {
                    valueHolder = fi.createDataHolderInstance();
                }
                Class searchType = valueHolder.getTypeCls();
                if (searchType == null) {
                    throw new DataOperationException(
                            fi.field.getName() +
                                    " should be initialized at first or we can not retrieve the type you want since it is a type of CotnextDataHolder. " +
                                    "You can also define an extended class to return the type class, in this case, you do not need to initialized it by your self");
                }
                ContextDataHolder foundData = findValueForTarget(fi, searchType);

                handleTypeUnMatch(instance, fi, foundData);

                if (fi.isContextDataHolder) {
                    transferDataHolder(foundData, valueHolder);
                    FieldUtils.writeField(fi.field, instance, valueHolder, true);
                } else {
                    FieldUtils.writeField(fi.field, instance, foundData.getValue(), true);
                }
            } catch (IllegalAccessException | IllegalArgumentException | InstantiationException e) {
                throw new DataOperationException("Exception when inject value to " + fi.field.toString(), e);
            }

        }

        for (MethodInfo mi : target.setMethodList) {
            try {
                ContextDataHolder valueHolder = mi.createDataHolderInstance();
                Class searchType = valueHolder.getTypeCls();
                if (searchType == null) {
                    throw new DataOperationException(mi.method.getName() + " cannot initialize an instance of " +
                            valueHolder.getClass().getName() + ". You should define an extended class to return the type class");
                }
                ContextDataHolder foundData = findValueForTarget(mi, searchType);
                handleTypeUnMatch(instance, mi, foundData);
                if (mi.isContextDataHolder) {
                    transferDataHolder(foundData, valueHolder);
                    mi.method.invoke(instance, valueHolder);
                } else {
                    mi.method.invoke(instance, foundData.getValue());
                }
            } catch (IllegalAccessException | IllegalArgumentException | InstantiationException | InvocationTargetException e) {
                throw new DataOperationException("Exception when inject value to " + mi.method.toString(), e);
            }
        }

    }

    private static void handleTypeUnMatch(Object instance, FieldInfo target, ContextDataHolder valueHolder) throws DataOperationException {
        handleTypeUnMatch(instance, target.field, null, null, -1, target, valueHolder);
    }

    private static void handleTypeUnMatch(Object instance, MethodInfo target, ContextDataHolder valueHolder) throws DataOperationException {
        handleTypeUnMatch(instance, null, target.method, null, -1, target, valueHolder);
    }

    private static void handleTypeUnMatch(Method method, int methodParameterIndex, TargetInfo target, ContextDataHolder valueHolder)
            throws DataOperationException {
        handleTypeUnMatch(null, null, null, method, methodParameterIndex, target, valueHolder);
    }

    private static void handleTypeUnMatch(Object instance, Field field, Method setter, Method method, int methodParameterIndex,
            TargetInfo target, ContextDataHolder valueHolder) throws DataOperationException {
        // type unmatched
        if (ContextDataTypeUnMatchScope.equals(valueHolder.getScope())) {
            switch (target.typeUnMatch) {
            case EXCEPTION:
                String msg = "Found data(%s) cannot be coverted from [%s] to [%s].";
                msg = String.format(msg, valueHolder.getFoundOriginalData(), valueHolder.getFoundOriginalData().getClass(), target.type);
                throw new DataOperationException(msg);
            case DEFAULT_VALUE:
                valueHolder.setData(valueHolder.getName(), valueHolder.getScope(), target.defaultValue);
                break;
            case DEFAULT_VALUE_AND_TRACE:
                ContextDataHolder traceHolder = new ContextDataHolder();
                transferDataHolder(valueHolder, traceHolder);
                if (field != null) {
                    InjectTrace.saveInstanceInjectionTraceInfo(instance, field, traceHolder);
                } else if (setter != null) {
                    InjectTrace.saveInstanceInjectionTraceInfo(instance, setter, traceHolder);
                } else if (method != null) {
                    InjectTrace.saveMethodInjectionTraceInfo(method, methodParameterIndex, traceHolder);
                }
                valueHolder.setData(valueHolder.getName(), valueHolder.getScope(), target.defaultValue);
                break;
            }
        }
    }

    private static void transferDataHolder(ContextDataHolder from, ContextDataHolder to) {
        to.setData(from.getName(), from.getScope(), from.getFoundOriginalData(), from.getValue());
    }

    private static ContextDataHolder findValueForTarget(TargetInfo targetInfo, Class overrideSearchType) throws DataOperationException {
        Context context = Context.getCurrentThreadContext();
        ContextDataFinder dataFinder = Configuration.getConfiguration().getContextDataFinder();

        Class searchType = overrideSearchType == null ? targetInfo.type : overrideSearchType;
        ContextDataHolder valueHolder = dataFinder.findDataInContext(context, targetInfo.scope, targetInfo.name, searchType);
        if (valueHolder == null && targetInfo.contextDataSetFactory != null) {
            Object value;
            if (targetInfo.isContextDataSetSingletonInContext) {
                // this map was initialized when the context was initialized
                HashMap<String, Object> cdSetSingletonMap = context.getData(ContextDataSetSingletonMapKey);
                // we must synchronize it to avoid concurrent access on the map
                synchronized (cdSetSingletonMap) {
                    String clsName = targetInfo.type.getName();
                    value = cdSetSingletonMap.get(clsName);
                    if (value == null) {
                        value = targetInfo.contextDataSetFactory.createInstance(targetInfo.type);
                        injectToInstance(value);
                        cdSetSingletonMap.put(clsName, value);
                    }
                }
            } else {
                value = targetInfo.contextDataSetFactory.createInstance(targetInfo.type);
                injectToInstance(value);
            }

            valueHolder = new ContextDataHolder(targetInfo.name, targetInfo.scope, value);
        } else if (valueHolder == null) {
            valueHolder = new ContextDataHolder(targetInfo.name, ContextDataNotFoundScope, targetInfo.defaultValue);
        }
        return valueHolder;

    }

    /**
     * Retrieve values from fields marked as reverse injectable of given instance.
     * 
     * There are only limited scopes can be marked as injectable. See {@link Configuration#setReverseInjectableScopes(List)}.
     * 
     * NOT SUPPORTED ANY MORE!!!
     * 
     * @param instance
     * @throws DataOperationException
     */
    @Deprecated
    public final static void setContextDataFromInstance(Object instance) throws DataOperationException {
        try {
            Context context = Context.getCurrentThreadContext();
            InstanceWireTarget target = getInstanceTarget(instance);
            Object value;
            for (FieldInfo fi : target.getFieldList) {
                value = FieldUtils.readField(fi.field, instance, true);
                context.setData(fi.scope, fi.name, value);
            }

            for (MethodInfo mi : target.getMethodList) {
                value = mi.method.invoke(instance);
                context.setData(mi.scope, mi.name, value);
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            String msg = String.format("Exception when inject value from instance of [%s] to Context.", instance.getClass().toString());
            throw new DataOperationException(msg, e);
        }
    }

    private final static InstanceWireTarget getInstanceTarget(Object instance) throws DataOperationException {
        boolean cacheEnable = Configuration.getConfiguration().isCacheEnable();
        InstanceWireTarget target = null;
        if (cacheEnable) {
            String key = instance.getClass().getName();
            target = InstanceTargetCache.get(key);
            if (target == null) {
                target = createInstanceTarget(instance);
                InstanceTargetCache.put(key, target);
            }
        } else {
            target = createInstanceTarget(instance);
        }
        return target;
    }

    private final static InstanceWireTarget createInstanceTarget(Object instance) throws DataOperationException {

        InstanceWireTarget target = new InstanceWireTarget();
        Class<?> cls = instance.getClass();

        List<AnnotatedPropertyInfo> propertyList = AnnotatedPropertyUtil.retrieveProperties(cls);

        try {
            for (AnnotatedPropertyInfo prop : propertyList) {
                ContextData cd = prop.getAnnotation(ContextData.class);

                if (prop.getField() != null) {
                    Field field = prop.getField();
                    FieldInfo fi = new FieldInfo();
                    fi.propertyInfo = prop;
                    fi.name = prop.getName();
                    fi.field = field;
                    fi.type = field.getType();
                    fi.isContextDataHolder = ContextDataHolder.class.isAssignableFrom(fi.type);

                    fi.scope = cd.scope();
                    fi.typeUnMatch = cd.typeUnMatch();
                    fi.fixForPrimitiveType();

                    ContextDataSet cdSet = ConvertableAnnotationRetriever
                            .retrieveAnnotation(ContextDataSet.class, fi.type.getAnnotations());
                    if (cdSet == null) {
                        fi.contextDataSetFactory = null;
                    } else {
                        fi.contextDataSetFactory = cdSet.factory().newInstance();
                        fi.isContextDataSetSingletonInContext = cdSet.singletonInContext();
                    }

                    target.setFieldList.add(fi);

                } else {// for method
                    MethodInfo mi = new MethodInfo();
                    mi.propertyInfo = prop;
                    mi.name = prop.getName();
                    mi.method = prop.getSetter();
                    if (mi.method == null) {
                        throw new DataOperationException("Could not find setter method for annotated property:" + prop.getName());
                    }
                    mi.type = mi.method.getParameterTypes()[0];
                    mi.isContextDataHolder = ContextDataHolder.class.isAssignableFrom(mi.type);

                    mi.scope = cd.scope();
                    mi.typeUnMatch = cd.typeUnMatch();
                    mi.fixForPrimitiveType();

                    ContextDataSet cdSet = ConvertableAnnotationRetriever
                            .retrieveAnnotation(ContextDataSet.class, mi.type.getAnnotations());
                    if (cdSet == null) {
                        mi.contextDataSetFactory = null;
                    } else {
                        mi.contextDataSetFactory = cdSet.factory().newInstance();
                        mi.isContextDataSetSingletonInContext = cdSet.singletonInContext();
                    }
                    target.setMethodList.add(mi);

                }

            }
        } catch (InstantiationException | IllegalAccessException e) {
            throw new DataOperationException("Exception occured on generating injection information of " + instance.getClass(), e);
        }

        return target;
    }

    /**
     * Retrieve value from {@link Context} for given Method by configured {@link ContextDataFinder}
     * 
     * @param method
     *            given method
     * @return Retrieved values
     * @throws DataOperationException
     */
    public final static Object[] getMethodInjectParams(Method method) throws DataOperationException {
        ContextDataFinder dataFinder = Configuration.getConfiguration().getContextDataFinder();
        return getMethodInjectParams(method, dataFinder);
    }

    /**
     * Retrieve value from {@link Context} for given Method by given {@link ContextDataFinder}
     * 
     * @param method
     *            given method
     * @param dataFinder
     *            given ContextDataFinder
     * @return Retrieved values
     * @throws DataOperationException
     */
    public final static Object[] getMethodInjectParams(Method method, ContextDataFinder dataFinder) throws DataOperationException {
        try {
            List<TargetInfo> targetList = getMethodTarget(method);
            Object[] params = new Object[targetList.size()];
            if (params.length == 0) {
                return params;
            }

            TargetInfo target;

            ContextDataHolder valueHolder, foundData;

            Class searchType;

            for (int i = 0; i < params.length; i++) {
                target = targetList.get(i);

                valueHolder = target.createDataHolderInstance();
                searchType = valueHolder.getTypeCls();
                if (searchType == null) {
                    throw new DataOperationException(method.getName() + " cannot initialize an instance of " +
                            valueHolder.getClass().getName() + ". You should define an extended class to return the type class");
                }
                foundData = findValueForTarget(target, searchType);
                handleTypeUnMatch(method, i, target, foundData);

                if (target.isContextDataHolder) {
                    transferDataHolder(foundData, valueHolder);
                    params[i] = valueHolder;
                } else {
                    params[i] = foundData.getValue();
                }

            }

            return params;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new DataOperationException("create instance failed.", e);
        }
    }

    private final static List<TargetInfo> getMethodTarget(Method method) throws InstantiationException, IllegalAccessException {
        boolean cacheEnable = Configuration.getConfiguration().isCacheEnable();
        List<TargetInfo> targetList = null;
        if (cacheEnable) {
            targetList = MethodTargetCache.get(method);
            if (targetList == null) {
                targetList = createMethodTarget(method);
                MethodTargetCache.put(method, targetList);
            }
        } else {
            targetList = createMethodTarget(method);
        }
        return targetList;
    }

    private final static List<TargetInfo> createMethodTarget(Method method) throws InstantiationException, IllegalAccessException {
        Class<?>[] types = method.getParameterTypes();
        List<TargetInfo> targetList = new ArrayList<>();
        if (types.length == 0) {
            return targetList;
        }
        Annotation[][] annotations = method.getParameterAnnotations();
        String[] parameterNames = paranamer.lookupParameterNames(method);
        TargetInfo target;
        ContextData cd;
        ContextDataSet cdSet;
        for (int i = 0; i < types.length; i++) {
            target = new TargetInfo();
            target.type = types[i];
            target.isContextDataHolder = ContextDataHolder.class.isAssignableFrom(target.type);

            cd = ConvertableAnnotationRetriever.retrieveAnnotation(ContextData.class, annotations[i]);
            cdSet = ConvertableAnnotationRetriever.retrieveAnnotation(ContextDataSet.class, target.type.getAnnotations());
            target.name = cd == null ? "" : cd.name();
            target.scope = cd == null ? "" : cd.scope();
            target.typeUnMatch = cd == null ? TypeUnMacthPolicy.EXCEPTION : cd.typeUnMatch();
            if (StringUtils.isEmpty(target.name)) {
                target.name = parameterNames[i];
            }

            if (cdSet == null) {
                target.contextDataSetFactory = null;
            } else {
                target.contextDataSetFactory = cdSet.factory().newInstance();
                target.isContextDataSetSingletonInContext = cdSet.singletonInContext();
            }

            target.fixForPrimitiveType();
            targetList.add(target);
        }
        return targetList;
    }

    public static final void initContext(Context context) {
        context.setData(ContextDataSetSingletonMapKey, new HashMap());
    }
}
