/**
 * Copyright 2013 International Business Machines Corp.
 *
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership. 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 org.apache.batchee.container.util;

import org.apache.batchee.container.exception.BatchContainerRuntimeException;
import org.apache.batchee.container.exception.IllegalBatchPropertyException;
import org.apache.batchee.container.proxy.InjectionReferences;
import org.apache.batchee.jaxb.Property;
import org.apache.xbean.propertyeditor.PropertyEditors;

import javax.batch.api.BatchProperty;
import javax.batch.runtime.context.JobContext;
import javax.batch.runtime.context.StepContext;
import javax.inject.Inject;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class DependencyInjections {

    private DependencyInjections() {
        // private utility class ct
    }

    public static void injectReferences(final Object artifact, final InjectionReferences injectionRefs) {
        final Map<String, Field> propertyMap = findPropertyFields(artifact);
        if (injectionRefs.getProps() != null) {
            injectProperties(artifact, injectionRefs.getProps(), propertyMap);
        }
        injectBatchContextFields(artifact, injectionRefs.getJobContext(), injectionRefs.getStepContext());
    }


    /**
     * @param props The properties directly associated with this batch artifact.
     */
    private static void injectProperties(final Object artifact, final List<Property> props, final Map<String, Field> propertyFieldMap) {

        //check if jsl properties are null or if 
        //the propertyMap is null. this means there are no annotated fields with @BatchProperty

        if (props == null || propertyFieldMap == null) {
            return;
        }

        // go through each field marked with @BatchProperty
        for (final Entry<String, Field> batchProperty : propertyFieldMap.entrySet()) {
            String propValue = getPropertyValue(props, batchProperty.getKey());

            // if a property is supplied in the job xml inject the given value
            // into
            // the field otherwise the default value will remain
            try {
                if (!(propValue == null)) {
                    final Field field = batchProperty.getValue();
                    final Type genericType = field.getGenericType();
                    final Class<?> type = field.getType();
                    field.set(artifact, convertTo(propValue, genericType, type));
                }
            } catch (IllegalArgumentException e) {
                throw new IllegalBatchPropertyException("The given property value is not an instance of the declared field.", e);
            } catch (IllegalAccessException e) {
                throw new BatchContainerRuntimeException(e);
            }

        }

    }

    public static <T> T convertTo(final String propValue, final Type genericType, final Class<T> type) {
        if (String.class.equals(type)) {
            return (T) propValue;
        } else if (type == null ||PropertyEditors.canConvert(type)) {
            return (T) PropertyEditors.getValue(genericType, propValue);
        }
        throw new IllegalArgumentException("Can't find a converter for type " + type);
    }


    /**
     * @param props list of properties from job xml
     * @param name  name of the property
     * @return null if no matching property found
     */
    public static String getPropertyValue(final List<Property> props, final String name) {
        if (props == null) {
            return null;
        }

        for (final Property prop : props) {
            if (name.equals(prop.getName())) {

                String propValue = prop.getValue();
                if ("".equals(propValue)) {
                    return null;
                } else {
                    return propValue;
                }

            }
        }


        return null;
    }

    /**
     * @param artifact An instance of the batch artifact
     * @return an ArrayList<Field> of fields annotated with @JobContext
     */
    private static void injectBatchContextFields(final Object artifact, final JobContext jobCtx, final StepContext stepCtx) {
        // Go through declared field annotations
        for (final Field field : artifact.getClass().getDeclaredFields()) {
            setAccessible(field);

            final Inject injectAnnotation = field.getAnnotation(Inject.class);
            if (injectAnnotation != null) {
                try {
                    // check the field for the context type
                    if (JobContext.class.isAssignableFrom(field.getType()) && field.get(artifact) == null) {
                        field.set(artifact, jobCtx);
                    } else if (StepContext.class.isAssignableFrom(field.getType()) && field.get(artifact) == null) {
                        field.set(artifact, stepCtx);
                    }
                } catch (final IllegalArgumentException e) {
                    throw new BatchContainerRuntimeException(e);
                } catch (final IllegalAccessException e) {
                    throw new BatchContainerRuntimeException(e);
                }
            }
        }
    }

    /**
     * @param delegate An instance of the batch artifact
     * @return A map of Fields annotated with @BatchProperty.
     */
    private static Map<String, Field> findPropertyFields(final Object delegate) {
        Map<String, Field> propertyMap = null;

        Class<?> current = delegate.getClass();
        while (current.getName().contains("$$")) { // remove common proxies
            current = current.getSuperclass();
        }

        while (current != null && current != Object.class) {
            for (final Field field : current.getDeclaredFields()) {
                setAccessible(field);

                final BatchProperty batchPropertyAnnotation = field.getAnnotation(BatchProperty.class);
                if (batchPropertyAnnotation != null) {
                    if (propertyMap == null) {
                        propertyMap = new HashMap<String, Field>();
                    }
                    // If a name is not supplied the batch property name defaults to
                    // the field name
                    String batchPropName = null;
                    if (batchPropertyAnnotation.name().equals("")) {
                        batchPropName = field.getName();
                    } else {
                        batchPropName = batchPropertyAnnotation.name();
                    }

                    // Check if we have already used this name for a property.
                    if (propertyMap.containsKey(batchPropName)) {
                        throw new IllegalBatchPropertyException("There is already a batch property with this name: " + batchPropName);
                    }

                    propertyMap.put(batchPropName, field);
                }

            }
            current = current.getSuperclass();
        }
        return propertyMap;
    }

    private static void setAccessible(final Field field) {
        if (System.getSecurityManager() == null) {
            field.setAccessible(true);
        } else {
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    field.setAccessible(true); // ignore java accessibility
                    return null;
                }
            });
        }
    }

}
