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

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

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;

import gov.va.med.fw.model.lookup.ModelPropertiesApplicationType;
import gov.va.med.fw.util.Reflector;

/**
 * An abstract class that can be used as primary key entity.
 * 
 * @author Vu Le
 * @version 1.0
 */
public abstract class AbstractKeyedEntity extends AbstractVersionedEntity { 

    private static final long serialVersionUID = -7858974447945480434L;
    
	/**
	 * The unique unwrapped identifier for this object (eg, BigDecimal). Solely
	 * used for persistence mechanism
	 */
	private Serializable identifier = null;

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

	/**
	 * A constructor initialized with the specific values
	 * 
	 * @param identifier
	 *            The unique identifier for this object.
	 */
	protected AbstractKeyedEntity(EntityKey identifier) {
		super();
		this.identifier = identifier;
	}

	/**
	 * A constructor initialized with the specific values
	 * 
	 * @param identifier
	 *            The unique identifier for this object.
	 * @param createdOn
	 *            The date/time when the object was created.
	 * @param modifiedOn
	 *            The date/time when the object was last modified.
	 * @param version
	 *            The version of the object's data.
	 */
	protected AbstractKeyedEntity(EntityKey identifier, Date createdOn,
			Date modifiedOn, Integer version) {
		super(createdOn, modifiedOn, version);
		this.identifier = identifier;
	}

	/** Returns the standardized key for the application, wraps the identifier */
	public EntityKey getEntityKey() {
		EntityKey key = null;
		if (identifier != null) {
			try {
				// see if subclass specified custom implementation...
				if (getEntityKeyClass() == null)
					key = EntityKeyFactory.createEntityKey(identifier,
							getVersion(), getClass());
				else {
					if (!EntityKey.class
							.isAssignableFrom((getEntityKeyClass())))
						throw new IllegalArgumentException(
								"Subclass defined getEntityKeyClass() is not assignable from EntityKey");

					// assume custom EntityKey type sets its own class
					Object[] args = new Object[] { identifier };
					if (VersionedEntityKey.class
							.isAssignableFrom(getEntityKeyClass()) && getVersion() != null)
						args = new Object[] { identifier, getVersion() };
					key = (EntityKey) Reflector.instantiate(
							getEntityKeyClass(), args);
				}
			} catch (Exception e) {
				throw new IllegalArgumentException(
						"Unable to create EntityKey(identifier, Class) for AbstractKeyedEntity");
			}
		}
		return key;
	}

	/**
	 * Gets the entity from the collection that matches the specified
	 * identifier. A match is determined by calling the equals method on the key
	 * value of the passed in identifier and the identifier of each
	 * AbstractKeyedEntity in the collection. If no matching entity is found,
	 * null is returned.
	 * 
	 * @param collection
	 *            The collection to get the entity from. An element in the
	 *            collection must be of type AbstractKeyedEntity for a match to
	 *            be found.
	 * @param identifier
	 *            The identifier to check
	 * @return The matching entity or null.
	 */
	public static AbstractKeyedEntity getEntityByEntityKey(
			Collection collection, EntityKey identifier) {
		// Ensure we have a valid collection
		if (collection != null) {
			// Loop through all elements in the collection
			for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
				// Ensure the element in the collection is an
				// AbstractKeyedEntity
				Object object = iterator.next();
				if (object instanceof AbstractKeyedEntity) {
					// Get the key values from the passed in identifier and the
					// one
					// from the abstract keyed entity
					// in the collection
					AbstractKeyedEntity keyedEntity = (AbstractKeyedEntity) object;
					EntityKey entityKey = keyedEntity.getEntityKey();
					if (identifier.equals(entityKey)) {
						return keyedEntity;
					}
				}
			}
		}

		// No match was found so return null.
		return null;
	}

	/**
	 * Removes an entity from a collection that matches the specified
	 * identifier. getEntityByEntityKey is used to determine if an identifier
	 * match is found. The entity that was removed is returned or null if no
	 * matching entity was found.
	 * 
	 * @param collection
	 *            The collection to remove the entity from.
	 * @param identifier
	 *            The identifier to check
	 * @return The removed entity or null.
	 */
	public static AbstractKeyedEntity removeEntityByEntityKey(
			Collection collection, EntityKey identifier) {
		AbstractKeyedEntity keyedEntity = getEntityByEntityKey(collection,
				identifier);
		if (keyedEntity != null) {
			collection.remove(keyedEntity);
		}
		return keyedEntity;
	}

	/**
	 * Subclasses can override this if they need polymorphic-like (ie, more than
	 * one type floating around) behavior
	 */
	protected Class getEntityKeyClass() {
		return null;
	}

	/**
	 * @return Returns the identifier.
	 */
	protected Serializable getIdentifier() {
		return identifier;
	}

	/**
	 * Sets the unique identifier for this object. This api will most likely
	 * never be called but is a convenience for simple implementations of the
	 * framework.
	 * 
	 * @param identifier
	 *            The unique identifier for this object.
	 */
	protected void setIdentifier(Serializable identifier) {
		this.identifier = identifier;
	}

	/**
	 * @see com.VHA.fw.model.AbstractVersionedEntity#finalize()
	 */
	protected void finalize() throws Throwable {
		super.finalize();
		this.identifier = null;
	}

	/**
	 * @see com.VHA.fw.model.AbstractEntity#hashCode()
	 */
	public int hashCode() {
		int hashCode = 0;
		if (identifier == null) {
			hashCode = noIdentifierHashCode();
		} else {
			HashCodeBuilder builder = new HashCodeBuilder();
			builder.append(this.getClass());
			builder.append(this.identifier);
			hashCode = builder.toHashCode();
		}
		return hashCode;
	}

	private int noIdentifierHashCode() {
		return super.hashCode();
	}
	
	public final boolean isModelPropertiesEqual(Object o, String modelPropertiesApplication) {
		if(ModelPropertiesApplicationType.MATCH_DOMAIN_CONCEPT.getName().equals(modelPropertiesApplication) && 
                (o instanceof AbstractKeyedEntity)) { 
			// special case handling for matching for domain concept - it gives control to EntityKey checking (but only if both there)                                               
			EntityKey myKey = getEntityKey(); 
			EntityKey yourKey = (o != null ? ((AbstractKeyedEntity) o).getEntityKey() : null); 
			if ((myKey != null) && (yourKey != null))  {
				// if both have keys, then only use that for match domain concept basis
				EqualsBuilder builder = new EqualsBuilder(); 
				builder.append( myKey, yourKey ); 
				return builder.isEquals(); 
			} 
		} 
		return super.isModelPropertiesEqual(o, modelPropertiesApplication); 
	}	

	/**
	 * @see com.va.med.fw.model.AbstractVersionedEntity#buildToString()
	 */
	protected void buildToString(ToStringBuilder builder) {
		builder.append("identifier", this.identifier);
		super.buildToString(builder);
	}

	/**
	 * Validate if the existingOwner is not same as the newOwner.
	 * 
	 * @param existingOwner -
	 *            The current Owner.
	 * @param newOwner -
	 *            The new Owner
	 * @throws IllegalArgumentException
	 *             if existingOwner is same as newOwner
	 */
	protected void validateOwner(AbstractKeyedEntity existingOwner,
			AbstractKeyedEntity newOwner) {
		if (existingOwner != null 
		        && newOwner != null 
		        && !(existingOwner==newOwner)) {
			String msg = "Object is already assigned to another owner ["
					+ existingOwner.getClass().getName() + " Id="
					+ existingOwner.getIdentifier() + "]";
			throw new IllegalArgumentException(msg);
		}
	}
}