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

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

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

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

/**
 * 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 (Iterator iterator = lhsMap.keySet().iterator(); iterator.hasNext();)
        {
            String propertyName = (String)iterator.next();
            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.
     */
    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 lhsCollection = (Collection)lhs;
                Collection rhsCollection = (Collection)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 lhsCollection, Collection 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;
    }
}
