/**
 * Licensed to the Austrian Association for Software Tool Integration (AASTI)
 * under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership. The AASTI licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openengsb.core.edbi.jdbc.util;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.openengsb.core.api.model.OpenEngSBModel;
import org.openengsb.core.api.model.annotation.Model;
import org.openengsb.core.api.model.annotation.OpenEngSBModelId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Introspector
 */
public final class Introspector {

    private static final Logger LOG = LoggerFactory.getLogger(Introspector.class);

    private Introspector() {
        // static class
    }

    /**
     * Returns a create of all property names and their respective type.
     * 
     * @param clazz the class to introspect
     * @return a mapping between property names and their type.
     */
    public static Map<String, Class<?>> getPropertyTypeMap(Class<?> clazz) {
        return getPropertyTypeMap(clazz, "class");
    }

    /**
     * Returns a create of all property names and their respective type. Any property passed via exclude are removed
     * from the create before hand.
     * 
     * @param clazz the class to introspect
     * @param exclude the properties to exclude
     * @return a mapping between property names and their type.
     */
    public static Map<String, Class<?>> getPropertyTypeMap(Class<?> clazz, String... exclude) {
        PropertyDescriptor[] descriptors;

        try {
            descriptors = getPropertyDescriptors(clazz);
        } catch (IntrospectionException e) {
            LOG.error("Failed to introspect " + clazz, e);
            return Collections.emptyMap();
        }

        HashMap<String, Class<?>> map = new HashMap<>(descriptors.length);

        for (PropertyDescriptor pd : descriptors) {
            map.put(pd.getName(), pd.getPropertyType());
        }

        for (String property : exclude) {
            map.remove(property);
        }

        return map;
    }

    /**
     * Returns a create of all property names and their values contained in the given object.
     * 
     * @param object the object to visit out of
     * @return the extracted value create
     */
    public static Map<String, Object> read(Object object) {
        PropertyDescriptor[] descriptors;
        Class<?> clazz = object.getClass();

        try {
            descriptors = getPropertyDescriptors(clazz);
        } catch (IntrospectionException e) {
            LOG.error("Failed to introspect " + clazz, e);
            return Collections.emptyMap();
        }

        HashMap<String, Object> map = new HashMap<>(descriptors.length);

        for (PropertyDescriptor pd : descriptors) {
            try {
                Object value = pd.getReadMethod().invoke(object);
                map.put(pd.getName(), value);
            } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) {
                LOG.error("Failed to visit property " + pd.getName(), e);
            }
        }

        map.remove("class");

        return map;
    }

    /**
     * Returns all {@link java.lang.reflect.Field}s declared in the given {@link Class} that are annotated with the
     * given {@link java.lang.annotation.Annotation}.
     * 
     * @param clazz the class to introspect
     * @param annotation the annotation to look for
     * @return a collection of {@link java.lang.reflect.Field}s that are annotated with the given annotation
     */
    public static List<Field> getAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotation) {
        Field[] fields = clazz.getDeclaredFields();
        List<Field> annotatedFields = new ArrayList<>(fields.length);

        for (Field f : fields) {
            if (f.isAnnotationPresent(annotation)) {
                annotatedFields.add(f);
            }
        }

        return annotatedFields;
    }

    /**
     * Uses {@link #getAnnotatedFields(Class, Class)} to look for a field annotated with the {@code OpenEngSBModelId}
     * annotation, and if present, returns that Field instance.
     * 
     * @param modelClass the class to introspect
     * @return the {@link java.lang.reflect.Field} annotated with {@code OpenEngSBModelId}, or null if none exists
     */
    public static Field getOpenEngSBModelIdField(Class<?> modelClass) {
        List<Field> fields = getAnnotatedFields(modelClass, OpenEngSBModelId.class);

        if (fields.isEmpty()) {
            return null;
        }

        return fields.get(0);
    }

    /**
     * Uses {@link #getAnnotatedFields(Class, Class)} to look for a field annotated with the {@code OpenEngSBModelId}
     * annotation, and if present, returns its name.
     * 
     * @param modelClass the class to introspect
     * @return the name of the {@link java.lang.reflect.Field} annotated with {@code OpenEngSBModelId}, or null
     */
    public static String getOpenEngSBModelIdProperty(Class<?> modelClass) {
        Field field = getOpenEngSBModelIdField(modelClass);

        return (field == null) ? null : field.getName();
    }

    /**
     * Checks whether the given object is a OpenEngSBModel (i.e. is either assignable by OpenEngSBModel or has the Model
     * annotation.
     * 
     * @param object the object to check
     * @return true if the object is a model
     */
    public static boolean isModel(Object object) {
        return isModelClass(object.getClass());
    }

    /**
     * Checks whether the given class is a OpenEngSBModel class (i.e. is either assignable by OpenEngSBModel or has the
     * Model annotation.
     * 
     * @param clazz the class to check
     * @return true if the class is a model
     */
    public static boolean isModelClass(Class<?> clazz) {
        return OpenEngSBModel.class.isAssignableFrom(clazz) || clazz.isAnnotationPresent(Model.class);
    }

    /**
     * Returns descriptors for all properties of the bean. May return {@code null} if the information should be obtained
     * by automatic analysis.
     * 
     * @param beanClass the class to introspect
     * @return an array of {@code PropertyDescriptor}s describing all properties supported by the bean or {@code null}
     */
    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> beanClass) throws IntrospectionException {
        BeanInfo beanInfo = java.beans.Introspector.getBeanInfo(beanClass);

        return beanInfo.getPropertyDescriptors();
    }

}
