// Package 
package gov.va.med.fw.util.builder;

import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.model.lookup.AbstractLookup;
import gov.va.med.fw.model.lookup.ModelPropertiesApplicationType;

import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;

import org.apache.commons.collections.OrderedMap;
import org.apache.commons.lang.builder.EqualsBuilder;

/**
 * This AbstractEntityEqualsBuilder extends the commons lang EqualsBuilder. It
 * adds the ability to track the property names being compared for debugging
 * purposes and handles various modes of operation for equality and matching. It
 * also is tailored to work with AbstractEntity objects.
 */
public class AbstractEntityEqualsBuilder extends EqualsBuilder {
	// The model properties application for this equals builder.
	private String modelPropertiesApplication;

	// The property that caused the equality check to fail
	private String notEqualsProperty = null;

	// The lhs property that caused the equality check to fail
	private String notEqualsLhs = null;

	// The rhs property that caused the equality check to fail
	private String notEqualsRhs = null;

	/**
	 * Construct an AbstractEntityEqualsBuilder with the model properties
	 * application. This is the application that determines what type of
	 * comparison to perform.
	 * 
	 * @param modelPropertiesApplication
	 *            The model properties application.
	 * 
	 * @see ModelPropertiesApplicationType for some common applications.
	 */
	public AbstractEntityEqualsBuilder(String modelPropertiesApplication) {
		this.modelPropertiesApplication = modelPropertiesApplication;
	}

	/**
	 * <p>
	 * Performs a deep comparison of two Property Name to Property Value
	 * <code>OrderedMap</code>'s.
	 * </p>
	 * Note that the property names must be the same in both maps
	 * 
	 * @param lhsMap
	 *            the left hand <code>OrderedMap</code> which maps property
	 *            names to property values
	 * @param rhsMap
	 *            the right hand <code>OrderedMap</code> which maps property
	 *            names to property values
	 * 
	 * @return AbstractEntityEqualsBuilder - used to chain calls.
	 */
	public AbstractEntityEqualsBuilder append(OrderedMap lhsMap, OrderedMap rhsMap) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhsMap == rhsMap) {
			return this;
		}

		// If either map is null, consider them not equal
		if (lhsMap == null || rhsMap == null) {
			setNotEqualsNullMap(lhsMap, rhsMap);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhsMap.size() != rhsMap.size()) {
			this.setNotEqualsDifferentSizedMaps(lhsMap, rhsMap);
			return this;
		}

		// Attempt to append each element of the map. It is assumed that the
		// elements of both maps are in
		// the same order for comparisons.
		for (Object propertyObj : lhsMap.keySet()) {
			String propertyName = (String) propertyObj;
			append(lhsMap.get(propertyName), rhsMap.get(propertyName), propertyName);
		}
		return this;
	}

	/**
	 * <p>
	 * Test if two <code>AbstractEntity</code> objects are equal using their
	 * application specific comparison method.
	 * 
	 * @param lhs
	 *            the left hand object
	 * @param rhs
	 *            the right hand object
	 * @param propertyName
	 *            The property name for these objects
	 * 
	 * @return AbstractEntityEqualsBuilder - used to chain calls.
	 */
	@SuppressWarnings("unchecked")
	public AbstractEntityEqualsBuilder append(Object lhs, Object rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		Class<?> lhsClass = lhs.getClass();
		if (!lhsClass.isArray()) {
			// The simpler case, not an array, just test the element

			if (Collection.class.isAssignableFrom(lhsClass)) {
				// We have collections so test them as a collection of
				// AbstractEntity objects
				Collection<AbstractEntity> lhsCollection = (Collection<AbstractEntity>) lhs;
				Collection<AbstractEntity> rhsCollection = (Collection<AbstractEntity>) rhs;
				processCollection(lhsCollection, rhsCollection, propertyName);
			} else {
				if (Map.class.isAssignableFrom(lhsClass)) {
					// We have collections so test them as a collection of
					// AbstractEntity objects
					Map<?, ?> lhsMap = (Map<?, ?>) lhs;
					Map<?, ?> rhsMap = (Map<?, ?>) rhs;
					processMap(lhsMap, rhsMap, propertyName);
				} else {
					// Variable to hold whether lhs and rhs are equal
					boolean isEqual = false;

					// We have AbstractEntity objects so test them directly
					if (AbstractEntity.class.isAssignableFrom(lhsClass)) {
						AbstractEntity lhsEntity = (AbstractEntity) lhs;

						if (ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(
								this.modelPropertiesApplication)) {
							isEqual = lhsEntity.matchesDomainConcept(rhs);
						} else {
							if (ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName()
									.equals(this.modelPropertiesApplication)) {
								isEqual = lhsEntity.matchesDomainValues(rhs);
							} else {
								isEqual = lhsEntity.equals(rhs);
							}
						}
					} else {
						if (Date.class.isAssignableFrom(lhsClass)) {
							Date lhsDate = (Date) lhs;
							Date rhsDate = (Date) rhs;
							isEqual = (lhsDate.getTime() == rhsDate.getTime());
						} else {
							// Not an AbstractEntity so default to the Object
							// equals check
							isEqual = lhs.equals(rhs);
						}
					}

					// Update the "not equal" information if not equal
					if (!isEqual) {
						setNotEquals(lhs, rhs, propertyName);
					}
				}
			}
		} else if (lhs.getClass() != rhs.getClass()) {
			// Here when we compare different dimensions, for example: a
			// boolean[][] to a boolean[]
			setNotEquals(lhs, rhs, propertyName);
		}

		// 'Switch' on type of array, to dispatch to the correct handler
		// This handles multi dimensional arrays of the same depth
		else if (lhs instanceof long[]) {
			append((long[]) lhs, (long[]) rhs, propertyName);
		} else if (lhs instanceof int[]) {
			append((int[]) lhs, (int[]) rhs, propertyName);
		} else if (lhs instanceof short[]) {
			append((short[]) lhs, (short[]) rhs, propertyName);
		} else if (lhs instanceof char[]) {
			append((char[]) lhs, (char[]) rhs, propertyName);
		} else if (lhs instanceof byte[]) {
			append((byte[]) lhs, (byte[]) rhs, propertyName);
		} else if (lhs instanceof double[]) {
			append((double[]) lhs, (double[]) rhs, propertyName);
		} else if (lhs instanceof float[]) {
			append((float[]) lhs, (float[]) rhs, propertyName);
		} else if (lhs instanceof boolean[]) {
			append((boolean[]) lhs, (boolean[]) rhs, propertyName);
		} else {
			// Not an array of primitives, so must be array of Objects.
			// We're going to assume each object is an AbstractEntity since this
			// is an AbstractEntityEqualsBuilder.
			// Convert the arrays to Collections and process them.
			Collection lhsCollection = Arrays.asList((Object[]) lhs);
			Collection rhsCollection = Arrays.asList((Object[]) rhs);
			processCollection(lhsCollection, rhsCollection, propertyName);
		}
		return this;
	}

	/**
	 * Tests the collections for equality by calling the appropriate Helper
	 * method on AbstractEntity.
	 * 
	 * @param lhsCollection
	 *            The left hand side collection
	 * @param rhsCollection
	 *            The right hand side collection
	 * @param propertyName
	 *            The property name for the collections
	 */
	protected void processCollection(Collection<? extends AbstractEntity> lhsCollection,
			Collection<? extends AbstractEntity> rhsCollection, String propertyName) {
		// Variable to hold whether lhs and rhs are equal
		boolean isEqual = false;

		if (ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(
				this.modelPropertiesApplication)) {
			isEqual = AbstractEntity.matchesDomainConcept(lhsCollection, rhsCollection);
		} else {
			if (ModelPropertiesApplicationType.MATCH_DOMAIN_VALUES.getName().equals(
					this.modelPropertiesApplication)) {
				isEqual = AbstractEntity.matchesDomainValues(lhsCollection, rhsCollection);
			} else {
				isEqual = AbstractEntity.equals(lhsCollection, rhsCollection);
			}
		}

		// Update the "not equal" information if not equal
		if (!isEqual) {
			setNotEquals(lhsCollection, rhsCollection, propertyName);
		}
	}

	/**
	 * Tests the collections for equality by calling the appropriate Helper
	 * method on AbstractEntity.
	 * 
	 * @param lhsMap
	 *            The left hand side map
	 * @param rhsMap
	 *            The right hand side map
	 * @param propertyName
	 *            The property name for the collections
	 */
	protected void processMap(Map<?, ?> lhsMap, Map<?, ?> rhsMap, String propertyName) {
		// Variable to hold whether lhs and rhs are equal
		boolean isEqual = AbstractEntity.isModelPropertiesEqual(lhsMap, rhsMap,
				this.modelPropertiesApplication);

		// Update the "not equal" information if not equal
		if (!isEqual) {
			setNotEquals(lhsMap, rhsMap, propertyName);
		}
	}

	public AbstractEntityEqualsBuilder append(long[] lhs, long[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(int[] lhs, int[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(short[] lhs, short[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(char[] lhs, char[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(byte[] lhs, byte[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(double[] lhs, double[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(float[] lhs, float[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(boolean[] lhs, boolean[] rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		// If same object reference, they are equal so just return this
		if (lhs == rhs) {
			return this;
		}

		// If either object is null, consider them not equal
		if (lhs == null || rhs == null) {
			setNotEquals(lhs, rhs, propertyName);
			return this;
		}

		// If the length's aren't equal, consider them not equal
		if (lhs.length != rhs.length) {
			this.setNotEqualsDifferentSizedArrays(lhs.length, rhs.length);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals(); ++i) {
			append(lhs[i], rhs[i], propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(long lhs, long rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(int lhs, int rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(short lhs, short rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(char lhs, char rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(byte lhs, byte rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public AbstractEntityEqualsBuilder append(double lhs, double rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		return append(Double.doubleToLongBits(lhs), Double.doubleToLongBits(rhs), propertyName);
	}

	public AbstractEntityEqualsBuilder append(float lhs, float rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		return append(Float.floatToIntBits(lhs), Float.floatToIntBits(rhs), propertyName);
	}

	public AbstractEntityEqualsBuilder append(boolean lhs, boolean rhs, String propertyName) {
		// If already not equal, just return this
		if (!isEquals()) {
			return this;
		}

		if (lhs != rhs) {
			setNotEquals(String.valueOf(lhs), String.valueOf(rhs), propertyName);
		}
		return this;
	}

	public String getModelPropertiesApplication() {
		return modelPropertiesApplication;
	}

	protected void setNotEqualsNullMap(Map<?, ?> lhsMap, Map<?, ?> rhsMap) {
		this.setNotEquals(lhsMap, rhsMap, lhsMap == null ? "Left hand side Map"
				: "Right hand side Map" + " is null");
	}

	protected void setNotEqualsDifferentSizedMaps(Map<?, ?> lhsMap, Map<?, ?> rhsMap) {
		this.setNotEquals(String.valueOf(lhsMap.size()), String.valueOf(rhsMap.size()),
				"Maps contain different sizes");
	}

	protected void setNotEqualsDifferentSizedArrays(int lhsSize, int rhsSize) {
		this.setNotEquals(String.valueOf(lhsSize), String.valueOf(rhsSize),
				"Arrays contain different sizes");
	}

	protected void setNotEquals(Object lhs, Object rhs, String notEqualsProperty) {
		// Only update our "not equals" information when all previous objects
		// are already equal
		if (isEquals()) {
			setEquals(false);
			setNotEqualsProperty(notEqualsProperty);
			setNotEqualsLhs(getObjectAsString(lhs));
			setNotEqualsRhs(getObjectAsString(rhs));
		}
	}

	protected String getObjectAsString(Object o) {
		if (o == null) {
			return "null";
		}

		Class<?> oClass = o.getClass();

		if (String.class.isAssignableFrom(oClass)) {
			return (String) o;
		}

		if (AbstractLookup.class.isAssignableFrom(oClass)) {
			return ((AbstractLookup) o).getCode();
		}

		if ((oClass.isArray()) || (Collection.class.isAssignableFrom(oClass))
				|| (Map.class.isAssignableFrom(oClass))
				|| (AbstractEntity.class.isAssignableFrom(oClass))) {
			return oClass.getName();
		}

		// Default to the toString method
		return o.toString();
	}

	protected void setNotEqualsProperty(String notEqualsProperty) {
		this.notEqualsProperty = notEqualsProperty;
	}

	public String getNotEqualsProperty() {
		return notEqualsProperty;
	}

	public String getNotEqualsLhs() {
		return notEqualsLhs;
	}

	protected void setNotEqualsLhs(String notEqualsLhs) {
		this.notEqualsLhs = notEqualsLhs;
	}

	public String getNotEqualsRhs() {
		return notEqualsRhs;
	}

	protected void setNotEqualsRhs(String notEqualsRhs) {
		this.notEqualsRhs = notEqualsRhs;
	}
}
