package org.jboss.seam.framework;

import static org.jboss.seam.international.StatusMessage.Severity.INFO;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.core.Expressions;
import org.jboss.seam.core.Expressions.ValueExpression;

/**
 * Base class for components which provide persistence
 * operations to a managed entity instance. This class 
 * may be reused by either configuration or extension, 
 * and may be bound directly to a view, or accessed by 
 * some intermediate Seam component.
 * 
 * @author Gavin King
 *
 */
@Scope(ScopeType.CONVERSATION)
public abstract class Home<T, E> extends MutableController<T>
{
   private static final long serialVersionUID = -5462396456614090423L;
   
   private Object id;
   protected E instance;
   private Class<E> entityClass;
   protected ValueExpression<T> newInstance;

   private ValueExpression deletedMessage;
   private ValueExpression createdMessage;
   private ValueExpression updatedMessage;
   
   /**
    * Add a {@link javax.faces.application.FacesMessage} and log a message when 
    * the entity instance is updated.
    * 
    * Utility method to add a {@link javax.faces.application.FacesMessage} from
    * the Seam managed resource bundle or, if not specified in the resource 
    * bundle, from {@link #getUpdatedMessage()} and log the entity when the 
    * managed entity is updated.
    * 
    * @see #getUpdatedMessage()
    * @see #getUpdatedMessageKey()
    */
   protected void updatedMessage()
   {
      debug("updated entity #0 #1", getEntityClass().getName(), getId());
      getStatusMessages().addFromResourceBundleOrDefault( INFO, getUpdatedMessageKey(), getUpdatedMessage().getExpressionString() );
   }

   /**
    * Add a {@link javax.faces.application.FacesMessage} and log a message when
    * the entity instance is deleted.
    * 
    * Utility method to add a {@link javax.faces.application.FacesMessage} from
    * the Seam managed resource bundle or, if not specified in the resource
    * bundle, from {@link #getDeletedMessage()} and log the entity when the 
    * managed entity is deleted.
    * 
    * @see #getDeletedMessage()
    * @see #getDeletedMessageKey()
    */
   protected void deletedMessage()
   {
      debug("deleted entity #0 #1", getEntityClass().getName(), getId());
      getStatusMessages().addFromResourceBundleOrDefault( INFO, getDeletedMessageKey(), getDeletedMessage().getExpressionString() );
   }
   
   /**
    * Add a {@link javax.faces.application.FacesMessage} and log a message when
    * the entity instance is created.
    * 
    * Utility method to add a {@link javax.faces.application.FacesMessage} from
    * the Seam managed resource bundle or, if not specified in the resource 
    * bundle, from {@link #getUpdatedMessage()} and log the entity when the 
    * managed entity is updated.
    * 
    * @see #getCreatedMessage()
    * @see #getCreatedMessageKey()
    */
   protected void createdMessage()
   {
      debug("created entity #0 #1", getEntityClass().getName(), getId());
      getStatusMessages().addFromResourceBundleOrDefault( INFO, getCreatedMessageKey(), getCreatedMessage().getExpressionString() );
   }

   /**
    * Run on {@link Home} instantiation to check the Home component is in a 
    * valid state.
    * <br />
    * Validates that the class of the entity to be managed has been specified.
    */
   @Create
   public void create()
   {
      if ( getEntityClass()==null )
      {
         throw new IllegalStateException("entityClass is null");
      }
      initDefaultMessages();
   }
   
   protected void initDefaultMessages()
   {
      Expressions expressions = new Expressions();
      if (createdMessage == null) {
         createdMessage = expressions.createValueExpression("Successfully created");
      }
      if (updatedMessage == null) {
         updatedMessage = expressions.createValueExpression("Successfully updated");
      }
      if (deletedMessage == null) {
         deletedMessage = expressions.createValueExpression("Successfully deleted");
      }
   }

   /**
    * Get the managed entity, using the id from {@link #getId()} to load it from
    * the Persistence Context or creating a new instance if the id is not 
    * defined.
    * 
    * @see #getId()
    */
   @Transactional
   public E getInstance()
   {
      joinTransaction();
      if (instance==null)
      {
         initInstance();
      }
      return instance;
   }
   
   /**
    * Clear the managed entity (and id), allowing the {@link EntityHome} to be
    * reused.
    */
   public void clearInstance()
   {
      setInstance(null);
      setId(null);
   }

   /**
    * Load the instance if the id is defined otherwise create a new instance
    * <br />
    * Utility method called by {@link #getInstance()} to load the instance from 
    * the Persistence Context if the id is defined. Otherwise a new instance is 
    * created.
    * 
    * @see #find()
    * @see #createInstance()
    */
   protected void initInstance()
   {
      if ( isIdDefined() )
      {
         if ( !isTransactionMarkedRollback() )
         {
            //we cache the instance so that it does not "disappear"
            //after remove() is called on the instance
            //is this really a Good Idea??
            setInstance( find() );
         }
      }
      else
      {
         setInstance( createInstance() );
      }
   }
   
   /**
    * Hook method called to allow the implementation to join the current 
    * transaction when necessary.
    */
   protected void joinTransaction() {}
   
   /**
    * Hook method called by {@link #initInstance()} to allow the implementation 
    * to load the entity from the Persistence Context.
    */
   protected E find()
   {
      return null;
   }

   /**
    * Utility method called by the framework when no entity is found in the
    * Persistence Context.
    */
   protected E handleNotFound()
   {
      throw new EntityNotFoundException( getId(), getEntityClass() );
   }

   /**
    * Create a new instance of the entity.
    * <br />
    * Utility method called by {@link #initInstance()} to create a new instance 
    * of the entity.
    */
   protected E createInstance()
   {
      if (newInstance!=null)
      {
         return (E) newInstance.getValue();
      }
      else if (getEntityClass()!=null)
      {
         try
         {
            return getEntityClass().newInstance();
         }
         catch (Exception e)
         {
            throw new RuntimeException(e);
         }
      }
      else
      {
         return null;
      }
   }

   /**
    * Get the class of the entity being managed.
    * <br />
    * If not explicitly specified, the generic type of implementation is used.
    */
   public Class<E> getEntityClass()
   {
      if (entityClass == null)
      {
         Type type = getClass().getGenericSuperclass();
         if (type instanceof ParameterizedType)
         {
            ParameterizedType paramType = (ParameterizedType) type;
            if (paramType.getActualTypeArguments().length == 2)
            {
               // likely dealing with -> new EntityHome<Person>().getEntityClass()
               if (paramType.getActualTypeArguments()[1] instanceof TypeVariable)
               {
                  throw new IllegalArgumentException("Could not guess entity class by reflection");
               }
               // likely dealing with -> new Home<EntityManager, Person>() { ... }.getEntityClass()
               else
               {
                  entityClass = (Class<E>) paramType.getActualTypeArguments()[1];
               }
            }
            else
            {
               // likely dealing with -> new PersonHome().getEntityClass() where PersonHome extends EntityHome<Person>
               entityClass = (Class<E>) paramType.getActualTypeArguments()[0];
            }
         }
         else
         {
            throw new IllegalArgumentException("Could not guess entity class by reflection");
         }
      }
      return entityClass;
   }

   /**
    * Set the class of the entity being managed. 
    * <br />
    * Useful for configuring {@link Home} components from 
    * <code>components.xml</code>.
    */
   public void setEntityClass(Class<E> entityClass)
   {
      this.entityClass = entityClass;
   }
   
   /**
    * Get the id of the object being managed.
    */
   public Object getId()
   {
      return id;
   }

   /**
    * Set/change the entity being managed by id.
    * 
    * @see #assignId(Object)
    */
   public void setId(Object id)
   {
      if ( setDirty(this.id, id) ) setInstance(null);
      this.id = id;
   }
   
   /**
    * Set the id of entity being managed.
    * <br />
    * Does not alter the instance so used if the id of the managed object is 
    * changed.
    * 
    * @see #setId(Object)
    */
   protected void assignId(Object id)
   {
      setDirty(this.id, id);
      this.id = id;
   }
   
   /**
    * Returns true if the id of the object managed is known.
    */
   public boolean isIdDefined()
   {
      return getId()!=null && !"".equals( getId() );
   }

   /**
    * Set/change the entity being managed.
    */
   public void setInstance(E instance)
   {
      setDirty(this.instance, instance);
      this.instance = instance;
   }

   /**
    * {@link javax.el.ValueExpression} to execute to load a new instance.
    * <br />
    * Mainly used when configuring the {@link Home} components in 
    * <code>components.xml</code>.
    */
   public ValueExpression getNewInstance()
   {
      return newInstance;
   }

   /**
    * {@link javax.el.ValueExpression} to execute to load a new instance.
    * <br />
    * Mainly used when configuring the {@link Home} components in 
    * <code>components.xml</code>.
    */
   public void setNewInstance(ValueExpression newInstance)
   {
      this.newInstance = newInstance;
   }

   /**
    * Message displayed to user when the managed entity is created.
    */
   public ValueExpression getCreatedMessage()
   {
      return createdMessage;
   }

   /**
    * Message displayed to user when the managed entity is created.
    */
   public void setCreatedMessage(ValueExpression createdMessage)
   {
      this.createdMessage = createdMessage;
   }

   /**
    * Message displayed to user when the managed entity is deleted.
    */
   public ValueExpression getDeletedMessage()
   {
      return deletedMessage;
   }

   /**
    * Message displayed to user when the managed entity is deleted.
    */
   public void setDeletedMessage(ValueExpression deletedMessage)
   {
      this.deletedMessage = deletedMessage;
   }

   /**
    * Message displayed to user when the managed entity is updated.
    */
   public ValueExpression getUpdatedMessage()
   {
      return updatedMessage;
   }

   /**
    * Message displayed to user when the managed entity is updated.
    */
   public void setUpdatedMessage(ValueExpression updatedMessage)
   {
      this.updatedMessage = updatedMessage;
   }
   
   /**
    * The prefix of the key to look up messages in the Seam managed resource 
    * bundle.
    * <br />
    * By default the simple name of the class suffixed with an underscore.
    */
   protected String getMessageKeyPrefix()
   {
      String className = getEntityClass().getName();
      return className.substring( className.lastIndexOf('.') + 1 ) + '_';
   }
   
   /**
    * The key to look up in the Seam managed resource bundle the message
    * displayed when the managed entity is created. 
    * <br />
    * By default the {@link #getMessageKeyPrefix()} suffixed with created.
    */
   protected String getCreatedMessageKey()
   {
      return getMessageKeyPrefix() + "created";
   }
   
   /**
    * The key to look up in the Seam managed resource bundle the message
    * displayed when the managed entity is updated. 
    * <br />
    * By default the {@link #getMessageKeyPrefix()} suffixed with updated.
    */
   protected String getUpdatedMessageKey()
   {
      return getMessageKeyPrefix() + "updated";
   }
   
   /**
    * The key to look up in the Seam managed resource bundle the message
    * displayed when the managed entity is deleted. 
    * <br />
    * By default the {@link #getMessageKeyPrefix()} suffixed with deleted.
    */
   protected String getDeletedMessageKey()
   {
      return getMessageKeyPrefix() + "deleted";
   }
   
   /**
    * Raise events when a CRUD operation succeeds.
    * <br />
    * Utility method to raise two events: an event of type 
    * <code>org.jboss.seam.afterTransactionSuccess</code> is raised, along with 
    * an event of type 
    * <code>org.jboss.seam.afterTransactionSuccess.&lt;entityName&gt;</code>.
    */
   protected void raiseAfterTransactionSuccessEvent()
   {
      raiseTransactionSuccessEvent("org.jboss.seam.afterTransactionSuccess");
      String simpleEntityName = getSimpleEntityName();
      if (simpleEntityName != null)
      {
         raiseTransactionSuccessEvent("org.jboss.seam.afterTransactionSuccess." + simpleEntityName);
      }
   }
   
   /**
    * The simple name of the managed entity
    */
   protected String getSimpleEntityName()
   {
      String name = getEntityName();
      if (name != null)
      {
         return name.lastIndexOf(".") > 0 && name.lastIndexOf(".") < name.length()  ? name.substring(name.lastIndexOf(".") + 1, name.length()) : name;
      }
      else
      {
         return null;
      }
   }
   
   /**
    * Hook method to get the name of the managed entity
    */
   protected abstract String getEntityName();
   
}
