/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.util;

import org.kuali.kfs.krad.service.KualiModuleService;
import org.kuali.kfs.krad.service.ModuleService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.krad.util.ExternalizableBusinessObjectUtils;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.web.format.FormatException;
import org.kuali.rice.krad.bo.BusinessObject;
import org.kuali.rice.krad.bo.ExternalizableBusinessObject;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ObjectPopulationUtils {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ObjectPopulationUtils.class);

    /**
     * Populates a class using a base class to determine fields
     *
     * @param base                   the class to determine what fields to copy
     * @param src                    the source class
     * @param target                 the target class
     * @param supplementalUncopyable a list of fields to never copy
     */
    public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target, Map supplementalUncopyable, Map<String, Class<?>> uncopyableFields) {
        List<String> fieldNames = new ArrayList<String>();
        Field[] fields = base.getDeclaredFields();


        for (Field field : fields) {
            if (!Modifier.isTransient(field.getModifiers())) {
                fieldNames.add(field.getName());
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("field " + field.getName() + " is transient, skipping ");
                }
            }
        }
        int counter = 0;
        for (String fieldName : fieldNames) {
            if ((isProcessableField(base, fieldName, uncopyableFields)) && (isProcessableField(base, fieldName, supplementalUncopyable))) {
                attemptCopyOfFieldName(base.getName(), fieldName, src, target, supplementalUncopyable);
                counter++;
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Population complete for " + counter + " fields out of a total of " + fieldNames.size() + " potential fields in object with base class '" + base + "'");
        }
    }

    /**
     * Attempts to copy a field
     *
     * @param baseClass              the base class
     * @param fieldName              the field name to determine if processable
     * @param sourceObject           source object
     * @param targetObject           target object
     * @param supplementalUncopyable
     */
    protected static void attemptCopyOfFieldName(String baseClassName, String fieldName, BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyable) {
        try {

            Object propertyValue = ObjectUtils.getPropertyValue(sourceObject, fieldName);
            if ((ObjectUtils.isNotNull(propertyValue)) && (Collection.class.isAssignableFrom(propertyValue.getClass()))) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("attempting to copy collection field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValue.getClass() + "'");
                }
                copyCollection(fieldName, targetObject, (Collection) propertyValue, supplementalUncopyable);
            } else {
                String propertyValueClass = (ObjectUtils.isNotNull(propertyValue)) ? propertyValue.getClass().toString() : "(null)";
                if (LOG.isDebugEnabled()) {
                    LOG.debug("attempting to set field '" + fieldName + "' using base class '" + baseClassName + "' and property value class '" + propertyValueClass + "'");
                }
                ObjectUtils.setObjectProperty(targetObject, fieldName, propertyValue);
            }
        } catch (Exception e) {
            // purposefully skip for now
            // (I wish objectUtils getPropertyValue threw named errors instead of runtime) so I could
            // selectively skip
            if (LOG.isDebugEnabled()) {
                LOG.debug("couldn't set field '" + fieldName + "' using base class '" + baseClassName + "' due to exception with class name '" + e.getClass().getName() + "'", e);
            }
        }
    }

    /**
     * True if a field is processable
     *
     * @param baseClass          the base class
     * @param fieldName          the field name to detrmine if processable
     * @param excludedFieldNames field names to exclude
     * @return true if a field is processable
     */
    protected static boolean isProcessableField(Class baseClass, String fieldName, Map excludedFieldNames) {
        if (excludedFieldNames.containsKey(fieldName)) {
            Class potentialClassName = (Class) excludedFieldNames.get(fieldName);
            if ((ObjectUtils.isNull(potentialClassName)) || (potentialClassName.equals(baseClass))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Copies based on a class template it does not copy fields in Known Uncopyable Fields
     *
     * @param base   the base class
     * @param src    source
     * @param target target
     */
    public static void populateFromBaseClass(Class base, BusinessObject src, BusinessObject target, Map<String, Class<?>> uncopyableFields) {
        populateFromBaseClass(base, src, target, new HashMap(), uncopyableFields);
    }

    /**
     * Populates from a base class traversing up the object hierarchy.
     *
     * @param sourceObject                     object to copy from
     * @param targetObject                     object to copy to
     * @param supplementalUncopyableFieldNames fields to exclude
     * @param classesToExclude                 classes to exclude
     */
    public static void populateFromBaseWithSuper(BusinessObject sourceObject, BusinessObject targetObject, Map supplementalUncopyableFieldNames, Set<Class> classesToExclude) {
        List<Class> classesToCopy = new ArrayList<Class>();
        Class sourceObjectClass = sourceObject.getClass();
        classesToCopy.add(sourceObjectClass);
        while (sourceObjectClass.getSuperclass() != null) {
            sourceObjectClass = sourceObjectClass.getSuperclass();
            if (!classesToExclude.contains(sourceObjectClass)) {
                classesToCopy.add(sourceObjectClass);
            }
        }
        for (int i = (classesToCopy.size() - 1); i >= 0; i--) {
            Class temp = classesToCopy.get(i);
            populateFromBaseClass(temp, sourceObject, targetObject, supplementalUncopyableFieldNames);
        }
    }

    /**
     * Copies a collection
     *
     * @param fieldName              field to copy
     * @param targetObject           the object of the collection
     * @param propertyValue          value to copy
     * @param supplementalUncopyable uncopyable fields
     * @throws FormatException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     */
    protected static <T extends BusinessObject> void copyCollection(String fieldName, BusinessObject targetObject, Collection<T> sourceList, Map supplementalUncopyable) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Collection listToSet = null;

        // materialize collections
        if (ObjectUtils.isNotNull(sourceList)) {
            ObjectUtils.materializeObjects(sourceList);
        }

        // ArrayList requires argument so handle differently than below
        try {
            listToSet = sourceList.getClass().newInstance();
        } catch (Exception e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("couldn't set class '" + sourceList.getClass() + "' on collection..." + fieldName + " using " + sourceList.getClass());
            }
            listToSet = new ArrayList<T>();
        }

        for (Iterator iterator = sourceList.iterator(); iterator.hasNext(); ) {
            BusinessObject sourceCollectionObject = (BusinessObject) iterator.next();
            if (LOG.isDebugEnabled()) {
                LOG.debug("attempting to copy collection member with class '" + sourceCollectionObject.getClass() + "'");
            }
            BusinessObject targetCollectionObject = (BusinessObject) createNewObjectFromClass(sourceCollectionObject.getClass());
            populateFromBaseWithSuper(sourceCollectionObject, targetCollectionObject, supplementalUncopyable, new HashSet<Class>());
            // BusinessObject targetCollectionObject = (BusinessObject)ObjectUtils.deepCopy((Serializable)sourceCollectionObject);
            Map pkMap = SpringContext.getBean(PersistenceService.class).getPrimaryKeyFieldValues(targetCollectionObject);
            Set<String> pkFields = pkMap.keySet();
            for (String field : pkFields) {
                ObjectUtils.setObjectProperty(targetCollectionObject, field, null);
            }
            listToSet.add(targetCollectionObject);
        }
        ObjectUtils.setObjectProperty(targetObject, fieldName, listToSet);
    }

    /**
     * This method safely creates a object from a class
     * Convenience method to create new object and throw a runtime exception if it cannot
     * If the class is an {@link ExternalizableBusinessObject}, this method will determine the interface for the EBO and query the
     * appropriate module service to create a new instance.
     *
     * @param boClass
     * @return a newInstance() of clazz
     */
    protected static Object createNewObjectFromClass(Class clazz) {
        if (clazz == null) {
            throw new RuntimeException("BO class was passed in as null");
        }
        try {
            if (clazz.getSuperclass().equals(ExternalizableBusinessObject.class)) {
                Class eboInterface = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(clazz);
                ModuleService moduleService = SpringContext.getBean(KualiModuleService.class).getResponsibleModuleService(eboInterface);
                return moduleService.createNewObjectFromExternalizableClass(eboInterface);
            } else {
                return clazz.newInstance();
            }
        } catch (Exception e) {
            throw new RuntimeException("Error occured while trying to create a new instance for class " + clazz);
        }
    }
}
