/********************************************************************
 * 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 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 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) getApplicationContext().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;
    }
}
