/********************************************************************
 * Copyright © 2006 VHA. All rights reserved
 ********************************************************************/

package gov.va.med.fw.util;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ClassUtils;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;

import gov.va.med.fw.model.ConfigurableModelProperties;
import gov.va.med.fw.model.lookup.ModelPropertiesApplicationType;
import gov.va.med.fw.service.AbstractComponent;

/**
 * Concrete service for a configurable set of model properties. Supports well
 * known applications as defined in ModelPropertiesApplicationType, as well as
 * project specific applications via the overloaded getModelProperties method.
 * 
 * <p>
 * The default behavior is that if a Class is not found in configuration, then
 * all its properties will be used (global still has veto rights). If a Class is
 * in an inheritance hierarchy, then each of its superclasses are also checked
 * aginst configuration, each giving veto power to the global.
 * 
 * @see gov.va.med.fw.model.lookup.ModelPropertiesApplicationType
 * @see #getModelProperties(Class, String) Created Mar 13, 2006 2:34:48 PM
 * @author DNS   BOHMEG
 */
public class ApplicationContextModelProperties extends AbstractComponent implements
		ModelPropertiesManager {
	private final static String GLOBAL_PER_DOMAIN_OBJECT = "global";

	private static List applicationContextList = new ArrayList();

	private Map classModelProperties = new HashMap();

	/** Global is only overruled by instance configuration. */
	private Map globalModelProperties = new HashMap();

	private String rootClassName;

	private boolean useTransients = false;

	public void flushCache() {
		classModelProperties.clear();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * gov.va.med.fw.service.ModelPropertiesManager#getIdentityProperties(java
	 * .lang.Class)
	 */
	public List getIdentityProperties(Class clazz) {
		return getModelProperties(clazz, ModelPropertiesApplicationType.IDENTITY.getName());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * gov.va.med.fw.service.ModelPropertiesManager#getCopyProperties(java.lang
	 * .Class)
	 */
	public List getCopyProperties(Class clazz) {
		return getModelProperties(clazz, ModelPropertiesApplicationType.COPY.getName());
	}

	public List getMatchDomainValuesProperties(Class clazz) {
		return getModelProperties(clazz, ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES
				.getName());
	}

	public List getMatchDomainConceptProperties(Class clazz) {
		return getModelProperties(clazz, ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT
				.getName());
	}

	/**
	 * Returns the set (could be any number) of properties for the interested
	 * application.
	 * 
	 * @param clazz
	 * @param application
	 * @return List
	 */
	public List getModelProperties(Class clazz, String application) {
		String key = clazz.getName() + "." + application;
		List propertyNames = (List) classModelProperties.get(key);
		if (propertyNames == null) {
			/*
			 * Since this internally calls Spring and it performs locking via
			 * object synchronization (ie, singletonCache) then we can not
			 * synchronize as it caused in testing competing threads to
			 * deadlock. Worst case here is double set-up for same
			 * classModelProperties. This is livable, and obviously more
			 * preferable to deadlocking.
			 */
			// synchronized (this) {
			propertyNames = initializeModelProperties(clazz, application);
			classModelProperties.put(key, propertyNames);
			// }
		}
		return propertyNames;
	}

	/**
	 * Gets the properties for the specified class and application type. If the
	 * application isn't found, then the identity properties are tried. If any
	 * properties couldn't be found, the global ones are used.
	 * 
	 * @param clazz
	 *            The class of properties to get
	 * @param application
	 *            The application type
	 * @return The properties
	 */
	public ConfigurableModelProperties getConfigurableModelProperties(Class clazz,
			String application) {
		// Get the properties for this application type.
		ConfigurableModelProperties modelProperties = getModelPropertiesBean(clazz, application);
		// for now, if there is no entry for this non-identity application type,
		// default to using the .identity one
		if (modelProperties == null
				&& !ModelPropertiesApplicationType.IDENTITY.getName().equals(application))
			modelProperties = getModelPropertiesBean(clazz, ModelPropertiesApplicationType.IDENTITY
					.getName());

		// now add on the global-per-domain object properties
		ConfigurableModelProperties globalPerDomainObject = getModelPropertiesBean(clazz,
				GLOBAL_PER_DOMAIN_OBJECT);
		if (globalPerDomainObject != null) {
			if (modelProperties != null) {
				modelProperties.addInclusions(globalPerDomainObject.getInclusions());
				modelProperties.addExclusions(globalPerDomainObject.getExclusions());
			} else {
				modelProperties = globalPerDomainObject;
			}
		}

		// now make sure modelProperties has a global-per-application object
		// properties
		ConfigurableModelProperties globalPerApplication = getGlobalModelPropertiesObject(application);
		if (globalPerApplication != null) {
			if (modelProperties != null) {
				modelProperties.setGlobalProperties(globalPerApplication);
			} else {
				if (globalPerApplication != null)
					modelProperties = new ConfigurableModelProperties(globalPerApplication);
			}
		}

		if (modelProperties == null) {
			// no configuration for domain-per-application, global-per-domain,
			// and global-per-application....allow all properties
			modelProperties = new ConfigurableModelProperties();
			modelProperties.allowAllProperties();
		}

		// Return the final properties
		return modelProperties;
	}

	/**
	 * Gets the properties bean for the specified class and application type.
	 * 
	 * @param clazz
	 *            The class of properties to get
	 * @param application
	 *            The application type
	 * @return The properties bean
	 */
	private ConfigurableModelProperties getModelPropertiesBean(Class clazz, String application) {
		// Get the properties for this application type.
		ConfigurableModelProperties modelProperties = null;
		try {
			modelProperties = (ConfigurableModelProperties) getBean(clazz.getName() + "."
					+ application);
		} catch (NoSuchBeanDefinitionException e) {
			// Leave modelProperties null
		} catch (BeanNotOfRequiredTypeException e) {
			// Leave modelProperties null
		}
		return modelProperties;
	}

	/**
	 * Gets the global properties for the specified application type.
	 * 
	 * @param application
	 *            The application type
	 * @return The global properties
	 */
	private ConfigurableModelProperties getGlobalModelPropertiesObject(String application) {
		// should not synchronize this method as this could cause deadlocks in
		// recursion scenarios with competing threads
		ConfigurableModelProperties modelProperties = (ConfigurableModelProperties) globalModelProperties
				.get(application);
		if (modelProperties == null) {
			modelProperties = new ConfigurableModelProperties();
			modelProperties.rejectAllProperties(); // remember this does not
			// mean everything is
			// rejected, just the global
			// will say so
			globalModelProperties.put(application, modelProperties);
		}
		return modelProperties;
	}

	/**
	 * Here is the general algorithm: 1) For targetClass, get
	 * ConfigurableModelProperties from configuration (with globals attached) 2)
	 * Build candidate Fields from inheritance hierarchy 3) For each Field, loop
	 * through inheritance hierarchy 4) For each Class in inheritance hierarchy
	 * (starting with targetClass), get its ConfigurableModelProperties from
	 * configuration (with globals attached) 5) Ask its
	 * ConfigurableModelProperties (with globals attached) if it is ok to use
	 * it. If not, stop. If ok, determine why ok. 6) Determine ok logic
	 * (implicit or explicit):
	 * 
	 * If was explicitly specified to be ok (ie, in inclusions), use it and do
	 * not keep going up the inheritance hierarchy. If was not explicitly
	 * specified to be ok and none were (ie, inclusions was empty), continue up
	 * the inheritance hierarchy (step 4). If was not explicitly specified to be
	 * ok and others were (ie, inclusions was not empty but did not include it),
	 * then do not use it.
	 * 
	 * The rationale was once (at least one) property was specified to be
	 * "included", then they were the only ones allowed.
	 * 
	 * @param clazz
	 * @return the list of properties
	 */
	private List initializeModelProperties(Class clazz, String application) {
		List props = new ArrayList();

		// initialize from Class definition and configuration
		ConfigurableModelProperties instanceModelProperties = getConfigurableModelProperties(clazz,
				application);

		List classes = new ArrayList();
		classes.add(clazz);
		if (instanceModelProperties.isInheritAncestorsProperties())
			classes.addAll(ClassUtils.getAllSuperclasses(clazz));
		Iterator itrClasses = classes.iterator();
		Class targetClazz = null;
		boolean isDescendantOfRootClassName = false;
		String realizedRootClassName = rootClassName;

		// first build list of all candidate fields
		List candidateFields = new ArrayList();
		Field[] fields = null;
		while (itrClasses.hasNext()) {
			targetClazz = (Class) itrClasses.next();
			if (!isDescendantOfRootClassName)
				isDescendantOfRootClassName = targetClazz.getName().equals(rootClassName);
			fields = targetClazz.getDeclaredFields(); // this gets all
			// (including private)
			AccessibleObject.setAccessible(fields, true); // would fail with
			// SecurityManager
			for (int i = 0; fields != null && i < fields.length; i++) {
				if ((fields[i].getName().indexOf('$') == -1)
						&& (useTransients || !Modifier.isTransient(fields[i].getModifiers()))
						&& (!Modifier.isStatic(fields[i].getModifiers()))) {
					candidateFields.add(fields[i]);
				}
			}
		}
		if (!isDescendantOfRootClassName)
			realizedRootClassName = Object.class.getName();

		// loop through each Field
		Iterator itrFields = candidateFields.iterator();
		Field field = null;
		ConfigurableModelProperties targetClassModelProperties = null;
		while (itrFields.hasNext()) {
			field = (Field) itrFields.next();
			// log("dealing with Field: " + field.getName());
			// loop through class hierarchy
			itrClasses = classes.iterator();
			while (itrClasses.hasNext()) {
				targetClazz = (Class) itrClasses.next();
				// log("dealing with Class: " + targetClazz.getName());
				if (targetClazz == clazz)
					targetClassModelProperties = instanceModelProperties;
				else
					targetClassModelProperties = getConfigurableModelProperties(targetClazz,
							application);

				if (!targetClassModelProperties.shouldInclude(field.getName())) {
					// log("Class " + targetClazz.getName() +
					// " is NOT ok with this field: " + field.getName());
					break;
				}

				// determine if we should continue up inheritance chain
				if (targetClassModelProperties.isPropertySpecified(field.getName())
						|| targetClazz.getName().equals(realizedRootClassName)) {
					// ok, we're done processing, add Field
					props.add(field);
					break;
				}
			}
		}

		return props;
	}

	/**
	 * @return Returns the rootClassName.
	 */
	public String getRootClassName() {
		return rootClassName;
	}

	/**
	 * @param rootClassName
	 *            The rootClassName to set.
	 */
	public void setRootClassName(String rootClassName) {
		this.rootClassName = rootClassName;
	}

	/**
	 * @return Returns the useTransients.
	 */
	public boolean isUseTransients() {
		return useTransients;
	}

	/**
	 * @param useTransients
	 *            The useTransients to set.
	 */
	public void setUseTransients(boolean useTransients) {
		this.useTransients = useTransients;
	}

	public Map getGlobalModelProperties() {
		return globalModelProperties;
	}

	public void setGlobalModelProperties(Map globalModelProperties) {
		this.globalModelProperties = globalModelProperties;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		// TODO Auto-generated method stub
		super.afterPropertiesSet();
		applicationContextList.add(0, getApplicationContext());
	}

	private ConfigurableModelProperties getBean(String beanName) {
		ConfigurableModelProperties bean = null;
		for (int i = 0; i < applicationContextList.size(); i++) {
			bean = (ConfigurableModelProperties) ((ApplicationContext) applicationContextList
					.get(i)).getBean(beanName);
			if (bean != null) {
				return bean;
			}
		}
		return null;
	}
}
