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

// Package
package gov.va.med.fw.model;

// Java classes
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.OrderedMap;
import org.apache.commons.collections.map.ListOrderedMap;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;

import gov.va.med.fw.model.lookup.ModelPropertiesApplicationType;
import gov.va.med.fw.service.config.SingletonApplicationContext;
import gov.va.med.fw.util.ModelPropertiesManager;
import gov.va.med.fw.util.builder.AbstractEntityEqualsBuilder;

/**
 * An abstract class that can be used as a base class for entity classes.
 *
 * @author Vu Le
 * @version 1.0
 */
public abstract class AbstractEntity implements Serializable, Cloneable {

    private static final long serialVersionUID = 5480146452774529239L;

    // The Logger.  Don't use directly, but rather use the getLogger() method.
    // This is needed for when the Person is cloned which uses SerializationUtils.
    // The resultant AbstractEntity objects all then have their logger set as null.
    private transient Log logger;

    private static ModelPropertiesManager modelPropertiesService;

    /**
     * A default constructor
     */
    protected AbstractEntity() {
        super();
    }

    protected Log getLogger()
    {
        if (logger == null)
        {
            logger = LogFactory.getLog(getClass());
        }
        return logger;
    }

    /**
     * Overriden by subclasses to customize the string representation of the
     * object.
     *
     * @param builder
     *           the org.apache.commons.lang.builder.ToStringBuilder that
     *           provides the string representation.
     */
    protected abstract void buildToString(ToStringBuilder builder);

    /**
     * Indicates whether some other object is "equal to" this one. Derived
     * classes are encouraged to override this method since this implementation
     * uses reflection to compare all attributes, which is a lot slower than
     * manually compare the equality of significant attributes.
     *
     * @param o
     *           the reference object with which to compare
     * @return Returns true if the reference object is equal to this one. False
     *         otherwise
     */
    public boolean equals(Object o) {
        return this.isModelPropertiesEqual(o, ModelPropertiesApplicationType.IDENTITY.getName());
    }

    /**
     * Indicates whether some other object matches the domain values of this one.
     * This is an alternative to the equals method with the exception that this
     * method compares for domain/business equality rather than "hash code" style equality.
     * Derived classes are encouraged to override this method since this implementation
     * uses reflection to compare all attributes, which is a lot slower than
     * manually compare the equality of significant attributes.
     *
     * @param o the reference object with which to compare
     * @return Returns true if the reference object matches this one. False
     *         otherwise.
     */
    public boolean matchesDomainValues(Object o)
    {
        return this.isModelPropertiesEqual(o, ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName());
    }

    /**
     * Indicates whether some other object matches the domain concept of this one.
     * A domain concept match is true when the objects are conceptually the same.
     * That could be determined by having the same identifiers or having some core
     * fields be the same (e.g. phone type).
     *
     * @param o the reference object with which to compare
     * @return Returns true if the reference object matches this one. False
     *         otherwise.
     */
    public boolean matchesDomainConcept(Object o)
    {
        return this.isModelPropertiesEqual(o, ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName());
    }

    /**
     * Returns a hash code value for the object. This method is supported for the
     * benefit of hashtables such as those provided by java.util.Hashtable
     *
     * @return Returns a hash code value for the object
     */
    public int hashCode() {
        Object[] myProps = this.getPropertyValueMap(ModelPropertiesApplicationType.IDENTITY.getName()).values().toArray();
        if (myProps == null)
            throw new IllegalStateException(getClass().getName()
                    + " contains no IdentityProperties for hashCode calculation");

        return new HashCodeBuilder().append(myProps).toHashCode();
    }

    /**
     * Returns a string representation of the object in multiple line.
     *
     * @return A contextual string representation of the object
     */
    public String toString() {
        ToStringBuilder builder = new ToStringBuilder(this,
                ToStringStyle.MULTI_LINE_STYLE);

        this.buildToString(builder);

        return builder.toString();
    }

    /**
     * Performs a deep copy of an object
     */
    public Object clone()
    {
        return SerializationUtils.clone(this);
    }

    public boolean isModelPropertiesEqual(Object o, String modelPropertiesApplication) {
        return ((o instanceof AbstractEntity) && this.isModelPropertiesEqual((AbstractEntity) o, modelPropertiesApplication));
    }

    private boolean isModelPropertiesEqual(AbstractEntity entity, String modelPropertiesApplication) {
        try
        {
            if(this == entity) {
                return true;
            }

            if ((entity == null) || (!getClass().equals(entity.getClass()))) {
                return false;
            }

            OrderedMap namesToValues = getPropertyValueMap(modelPropertiesApplication);
            OrderedMap otherNamesToValues = entity.getPropertyValueMap(modelPropertiesApplication);

            if (namesToValues.isEmpty())
            {
                throw new IllegalStateException(getClass().getName() + " (this) contains no Model Properties for application: " + modelPropertiesApplication);
            }
            if (otherNamesToValues == null)
            {
                throw new IllegalStateException(entity.getClass().getName() + " (other) contains no Model Properties for application: " + modelPropertiesApplication);
            }

            AbstractEntityEqualsBuilder equalsBuilder =
                new AbstractEntityEqualsBuilder(modelPropertiesApplication).append(namesToValues, otherNamesToValues);

            logNotEqualsDebugMessage(equalsBuilder, modelPropertiesApplication);

            return equalsBuilder.isEquals();
        }
        catch(StackOverflowError e) {
            getLogger().error("Received a stack overflow in isModelPropertiesEqual for application: " + modelPropertiesApplication +
            		" and instance class: " + getClass().getName());
            throw e;
        }
    }

    /**
     * Currently this only logs information for ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.
     *
     * @param equalsBuilder the equals builder
     * @param modelPropertiesApplication the model properties application
     */
    protected void logNotEqualsDebugMessage(AbstractEntityEqualsBuilder equalsBuilder, String modelPropertiesApplication)
    {
        // When the objects are not equal, print debug information so we know the reason why
        if ((!equalsBuilder.isEquals()) &&
            (!ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(modelPropertiesApplication) &&
            !ModelPropertiesApplicationType.IDENTITY.getName().equals(modelPropertiesApplication)))
        {
            if (getLogger().isDebugEnabled())
	        {
	            getLogger().debug("Model Properties are not equal for Entity: " + this.getClass().getName() +
	                ",  Property: " + equalsBuilder.getNotEqualsProperty() + ", LHS: " +
	                equalsBuilder.getNotEqualsLhs() + ", RHS: " + equalsBuilder.getNotEqualsRhs() + ".");
	        }
    	}
    }

    public static boolean equals(AbstractEntity entity1, AbstractEntity entity2)
    {
        return entity1 == null ? entity2 == null : entity1.equals(entity2);
    }

    public static boolean matchesDomainConcept(AbstractEntity entity1, AbstractEntity entity2)
    {
        return entity1 == null ? entity2 == null : entity1.matchesDomainConcept(entity2);
    }

    public static boolean matchesDomainValues(AbstractEntity entity1, AbstractEntity entity2)
    {
        return entity1 == null ? entity2 == null : entity1.matchesDomainValues(entity2);
    }

    public static boolean equals(Collection collection1, Collection collection2)
    {
        return isModelPropertiesEqual(collection1, collection2, ModelPropertiesApplicationType.IDENTITY.getName());
    }

    public static boolean matchesDomainConcept(Collection collection1, Collection collection2)
    {
        return isModelPropertiesEqual(collection1, collection2, ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName());
    }

    /**
     * Determines if all AbstractEntity's in collection 1 "match" the domain values for all AbstractEntity's in collection 2.
     * The collections are first ordered by domain concept and then individual elements are compared for matching
     * domain values.
     *
     * @param collection1 The first collection of AbstractEntity objects
     * @param collection2 The second collection of AbstractEntity objects
     * @return True if all entity keys match domain values or false if not.  Matching is determined by the collections
     * containing the same number of entities and finding unique matches for each entity.
     */
    public static boolean matchesDomainValues(Collection collection1, Collection collection2)
    {
        return isModelPropertiesEqual(collection1, collection2, ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName());
    }

    protected static boolean isModelPropertiesEqual(Collection collection1, Collection collection2, String modelPropertiesApplication)
    {

        // Handle null collections
        if ((collection1 == null) || (collection2 == null))
        {
            return collection1 == null && collection2 == null;
        }

        // Must wrap in case someone is using an Unmodifiable Collection
        List list1 = new ArrayList(collection1);
        List list2 = new ArrayList(collection2);
        
        // Handle different sized sets
        if (list1.size() != list2.size())
        {
            return false;
        } 
        
        if(list1.isEmpty() && list2.isEmpty())
            return true;
        
        //To handle cases where elements are not instances of AbstractEntity, default to object.equals 
        if(!(AbstractEntity.class.isAssignableFrom(list1.get(0).getClass())))
        {
            return list1.equals(list2);
        }

        // Order the 2nd collection based on the domain concept of the first collection.
        List orderedList2 = new ArrayList();
        for (Iterator iterator1 = list1.iterator(); iterator1.hasNext();)
        {
            // Get an entity from collection 1
            AbstractEntity entity1 = (AbstractEntity)iterator1.next();

            // See if a matching entity exists in collection 2
            AbstractEntity matchingEntity = null;
            for (int i=0; (matchingEntity == null) && (i < list2.size()); i++)
            {
                // Compare the entity with the appropriate equality check
                AbstractEntity entity2 = (AbstractEntity)list2.get(i);
                boolean matches = false;
                if ((ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(modelPropertiesApplication)) ||
                    (ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName().equals(modelPropertiesApplication)))
                {
                    matches = entity1.matchesDomainConcept(entity2);
                }
                else
                {
                    matches = entity1.equals(entity2);
                }

                // Process a match
                if (matches)
                {
                    // Store the matching entity and remove it from collection 2 for efficiency
                    matchingEntity = entity2;
                    list2.remove(i);
                }
            }

            // If we didn't find a match, then return false since the collections don't match
            if (matchingEntity == null)
            {
                return false;
            }

            // Add the matching entity into the orderedList
            orderedList2.add(matchingEntity);
        }

        // If we're matching domain values, compare all entities
        if (ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName().equals(modelPropertiesApplication))
        {
            // Now that the lists have the same domain concept order, compare the like elements of both lists
            for (int i=0; i < list1.size(); i++)
            {
                AbstractEntity entity1 = (AbstractEntity)list1.get(i);
                AbstractEntity entity2 = (AbstractEntity)orderedList2.get(i);
                if (!AbstractEntity.matchesDomainValues(entity1, entity2))
                {
                    return false;
                }
            }
        }

        // If we get here, that means that all elements match so return true
        return true;
    }

    public static boolean isModelPropertiesEqual(Map mapParam1, Map mapParam2, String modelPropertiesApplication)
    {
        // Must wrap in case someone is using an Unmodifiable Collection
        Map map1 = new HashMap(mapParam1);
        Map map2 = new HashMap(mapParam2);

        // Handle null maps
        if ((map1 == null) || (map2 == null))
        {
            return map1 == null && map2 == null;
        }

        // Handle different sized maps
        if (map1.size() != map2.size())
        {
            return false;
        }

        // See if the key sets are the same
        if (!(map1.keySet().equals(map2.keySet())))
        {
            return false;
        }

        // Process the values of each map with our standard collection processing
        return isModelPropertiesEqual(map1.values(), map2.values(), modelPropertiesApplication);
    }

    /**
     * Gets the property value map.  Allows sub-classing.
     * @param modelPropertiesApplication the model properties application
     * @return the property to value map.
     */
    protected OrderedMap getPropertyValueMap(String modelPropertiesApplication) {
		// do switching on well-known applications (allows for easier subclassing to customize the well-known ones)
		if(ModelPropertiesApplicationType.IDENTITY.getName().equals(modelPropertiesApplication))
			return getPropertyValueMapForIdentity();
		else if(ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(modelPropertiesApplication))
			return getPropertyValueMapForMatchDomainConcept();
		else if(ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName().equals(modelPropertiesApplication))
			return getPropertyValueMapForMatchDomainValues();
		else
			return doGetPropertyValueMap(modelPropertiesApplication);
	}

    /**
     * Gets the property value map for the identity application.  Allows for sub-classing.
     * @return the property to value map.
     */
    protected OrderedMap getPropertyValueMapForIdentity() {
		return doGetPropertyValueMap(ModelPropertiesApplicationType.IDENTITY.getName());
	}

    /**
     * Gets the property value map for the match domain concept application.  Allows for sub-classing.
     * @return the property to value map.
     */
	protected OrderedMap getPropertyValueMapForMatchDomainConcept() {
		return doGetPropertyValueMap(ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName());
	}

    /**
     * Gets the property value map for the match domain values application.  Allows for sub-classing.
     * @return the property to value map.
     */
	protected OrderedMap getPropertyValueMapForMatchDomainValues() {
		return doGetPropertyValueMap(ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName());
	}

	private final OrderedMap doGetPropertyValueMap(String modelPropertiesApplication) {
		OrderedMap data = new ListOrderedMap();
		List propertyFields = getPropertyFields(modelPropertiesApplication);

		try {
			Iterator itr = propertyFields != null ? propertyFields.iterator() : null;
			Field fld = null;
			while (itr != null && itr.hasNext()) {
				fld = (Field) itr.next();
				data.put(fld.getName(), fld.get(this));
			}
		}
		catch (Exception e) {
			throw new RuntimeException(
					"Unable to retrieve model property values from object "
							+ getClass().getName(), e);
		}
		return data;
	}

    /**
     * Gets the property field list for the specified application name.  Allows for sub-classing.
     * @param modelPropertiesApplication the application name.
     * @return the list of property fields.
     */
    protected List getPropertyFields(String modelPropertiesApplication) {
		// do switching on well-known applications (allows for easier subclassing to customize the well-known ones)
		if(ModelPropertiesApplicationType.IDENTITY.getName().equals(modelPropertiesApplication))
			return getPropertyFieldsForIdentity();
		else if(ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(modelPropertiesApplication))
			return getPropertyFieldsForMatchDomainConcept();
		else if(ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName().equals(modelPropertiesApplication))
			return getPropertyFieldsForMatchDomainValues();
		else
			return getModelPropertiesService().getModelProperties(getClass(), modelPropertiesApplication);
	}

    /**
     * Gets the property field list for the identity application.  Allows for sub-classing.
     * @return the list of property fields.
     */
	protected List getPropertyFieldsForIdentity() {
		return getModelPropertiesService().getModelProperties(getClass(), ModelPropertiesApplicationType.IDENTITY.getName());
	}

    /**
     * Gets the property field list for the match domain values application.  Allows for sub-classing.
     * @return the list of property fields.
     */
	protected List getPropertyFieldsForMatchDomainValues() {
		return getModelPropertiesService().getModelProperties(getClass(), ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName());
	}

    /**
     * Gets the property field list for the match domain concept application.  Allows for sub-classing.
     * @return the list of property fields.
     */
	protected List getPropertyFieldsForMatchDomainConcept() {
		return getModelPropertiesService().getModelProperties(getClass(), ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName());
	}

    private ModelPropertiesManager getModelPropertiesService() {
        if (modelPropertiesService == null) {
           SingletonApplicationContext locator = SingletonApplicationContext.getInstance();
           ApplicationContext ctx = locator.getSingletonContext();
            modelPropertiesService = 
               (ModelPropertiesManager)ctx.getBean( ModelPropertiesManager.CONFIG_SERVICE_KEY, ModelPropertiesManager.class );
        }
        return modelPropertiesService;
    }
}