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

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

// Java classes
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.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 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
	 * 
	 * @throws CloneNotSupportedException
	 */
	public Object clone() throws CloneNotSupportedException {
		try {
			// Serialize the object
			ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
			ObjectOutputStream out = new ObjectOutputStream(baos);
			out.writeObject(this);
			byte[] objectData = baos.toByteArray();

			// Deserialize the object
			ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
			ObjectInputStream in = new ObjectInputStream(bais);
			return in.readObject();
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	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<? extends AbstractEntity> collection1,
			Collection<? extends AbstractEntity> collection2) {
		return isModelPropertiesEqual(collection1, collection2,
				ModelPropertiesApplicationType.IDENTITY.getName());
	}

	public static boolean matchesDomainConcept(Collection<? extends AbstractEntity> collection1,
			Collection<? extends AbstractEntity> 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<? extends AbstractEntity> collection1,
			Collection<? extends AbstractEntity> 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<Object> list1 = new ArrayList<Object>(collection1);
		List<Object> list2 = new ArrayList<Object>(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<AbstractEntity> orderedList2 = new ArrayList<AbstractEntity>();
		for (Iterator<Object> 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<Object, Object> map1 = new HashMap<Object, Object>(mapParam1);
		Map<Object, Object> map2 = new HashMap<Object, Object>(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) locator.getBean(
					ModelPropertiesManager.CONFIG_SERVICE_KEY, ModelPropertiesManager.class);
		}
		return modelPropertiesService;
	}
}