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

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

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

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

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

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

	private static final long serialVersionUID = -7858974447945480434L;

	/**
	 * The unique unwrapped identifier for this object (eg, BigDecimal). Solely
	 * used for persistence mechanism
	 */
	protected Long id = null;

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

	// FIX
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	/** Returns the standardized key for the application, wraps the identifier */
	@SuppressWarnings("unchecked")
	public EntityKey<T> getEntityKey() {
		EntityKey<T> key = null;

		if (getIdentifier() != null) {
			try {
				// see if subclass specified custom implementation...
				if (getEntityKeyClass() == null)
					key = EntityKeyFactory.createEntityKey(getIdentifier(), getVersion(),
							(Class<T>) 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[] { getIdentifier() };
					if (VersionedEntityKey.class.isAssignableFrom(getEntityKeyClass())
							&& getVersion() != null)
						args = new Object[] { getIdentifier(), getVersion() };
					key = (EntityKey<T>) 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 <T extends AbstractKeyedEntity<T>> AbstractKeyedEntity<T> getEntityByEntityKey(
			Collection<T> collection, EntityKey<T> identifier) {
		if (collection != null) {
			// Loop through all elements in the collection
			for (T object : collection) {
				EntityKey<T> entityKey = object.getEntityKey();
				if (identifier.equals(entityKey)) {
					return object;
				}
			}

		}

		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 <T extends AbstractKeyedEntity<T>> AbstractKeyedEntity<T> removeEntityByEntityKey(
			Collection<T> collection, EntityKey<T> identifier) {
		AbstractKeyedEntity<T> 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 this.getId();
	}

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

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

	private int noIdentifierHashCode() {
		return super.hashCode();
	}

	@SuppressWarnings("unchecked")
	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<T> myKey = getEntityKey();
			EntityKey<T> yourKey = ((AbstractKeyedEntity<T>) o).getEntityKey();
			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.getIdentifier());
		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);
		}
	}
}