/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */
package com.sun.enterprise.util;

import java.io.Serializable;
import java.util.logging.*;
import java.util.Map;

import javax.persistence.*;

import com.sun.enterprise.Switch;
import com.sun.enterprise.J2EETransactionManager;
import com.sun.enterprise.distributedtx.J2EETransaction;
import com.sun.ejb.ContainerFactory;
import com.sun.enterprise.deployment.types.EntityManagerReference;

import com.sun.logging.*;

/**
 * Implementation of a container-managed entity manager.  
 * A new instance of this class will be created for each injected
 * EntityManager reference or each lookup of an EntityManager
 * reference within the component jndi environment. 
 * The underlying EntityManager object does not support concurrent access.
 * Likewise, this wrapper does not support concurrent access.  
 *
 * @author Kenneth Saks
 */
public class EntityManagerWrapper implements EntityManager, Serializable {

    static Logger _logger=LogDomains.getLogger(LogDomains.UTIL_LOGGER);

    static private LocalStringManagerImpl localStrings =
        new LocalStringManagerImpl(EntityManagerWrapper.class);

    // Serializable state

    private String unitName;
    private PersistenceContextType contextType;
    private Map emProperties;

    // transient state

    transient private EntityManagerFactory entityManagerFactory;
    transient private J2EETransactionManager txManager;
    transient private ContainerFactory containerFactory;
    
    // Only used to cache entity manager with EXTENDED persistence context
    transient private EntityManager extendedEntityManager;

    // Cache entity manager for use outside of a transaction.  
    transient private EntityManager nonTxEntityManager;

    // set after each call to _getDelegate()
    transient private DelegateInfo delegateInfo;

    public EntityManagerWrapper(EntityManagerReference 
                                referenceDescriptor) {
        this.unitName = referenceDescriptor.getUnitName();
        this.contextType = referenceDescriptor.getPersistenceContextType();
        this.emProperties = referenceDescriptor.getProperties();
    }

    private void init() {

        entityManagerFactory = EntityManagerFactoryWrapper.
            lookupEntityManagerFactory(unitName);
        
        if( entityManagerFactory == null ) {
            throw new IllegalStateException
                ("Unable to retrieve EntityManagerFactory for unitName "
                 + unitName);
        }
        
        txManager = Switch.getSwitch().getTransactionManager();
        containerFactory = Switch.getSwitch().getContainerFactory();
        
        delegateInfo = new DelegateInfo();
    }

    private void doTransactionScopedTxCheck() {
        
        if( contextType != PersistenceContextType.TRANSACTION) {
            return;
        }
        
        doTxRequiredCheck();

    }

    private void doTxRequiredCheck() {

        if( entityManagerFactory == null ) {
            init();
        }

        J2EETransaction tx = null;
        try {
            tx = (J2EETransaction) txManager.getTransaction();
        } catch(Exception e) {
            throw new IllegalStateException("exception retrieving tx", e);
        }
            
        if( tx == null ) {
            throw new TransactionRequiredException();
        }

    }
    private EntityManager _getDelegate() {

        // Populate any transient objects the first time 
        // this method is called.

        if( entityManagerFactory == null ) {
            init();
        }

        EntityManager delegate = null;

        delegateInfo.nonTxEntityManager = false;

        if( contextType == PersistenceContextType.TRANSACTION ) {

            J2EETransaction tx = null;
            try {
                tx = (J2EETransaction) txManager.getTransaction();
            } catch(Exception e) {
                throw new IllegalStateException("exception retrieving tx", e);
            }
            
            if( tx != null ) {

                // If there is an active extended persistence context
                // for the same entity manager factory and the same tx,
                // it takes precendence.
                delegate = tx.getExtendedEntityManager(entityManagerFactory);

                if( delegate == null ) {

                    delegate = tx.getTxEntityManager(entityManagerFactory);

                    if( delegate == null ) {

                        // If there is a transaction and this is the first
                        // access of the wrapped entity manager, create an
                        // actual entity manager and associate it with the
                        // entity manager factory.
                        delegate = entityManagerFactory.
                            createEntityManager(emProperties);

                        tx.addTxEntityManagerMapping(entityManagerFactory, 
                                                     delegate);
                    }
                }

            } else {

                if( nonTxEntityManager == null ) {
                    nonTxEntityManager = 
                        entityManagerFactory.createEntityManager(emProperties);
                }

                delegateInfo.nonTxEntityManager = true;

                // Return a non-transactional entity manager.
                delegate = nonTxEntityManager;
            }

        } else {

            // EXTENDED Persitence Context

            if( extendedEntityManager == null ) {
                extendedEntityManager = containerFactory.
                    lookupExtendedEntityManager(entityManagerFactory);
                    
            }          
      
            delegate = extendedEntityManager;

        }
        
        if( _logger.isLoggable(Level.FINE) ) {
            _logger.fine("In EntityManagerWrapper::_getDelegate(). " +
                         "Logical entity manager  = " + this);
            _logger.fine("Physical entity manager = " + delegate);
        }

        return delegate;

    }

    public void persist(Object entity) {
        doTransactionScopedTxCheck();
        _getDelegate().persist(entity);
    }

    public <T> T merge(T entity) {
        doTransactionScopedTxCheck();
        return _getDelegate().merge(entity);
    }

    public void remove(Object entity) {
        doTransactionScopedTxCheck();
        _getDelegate().remove(entity);
    }

    public <T> T find(Class<T> entityClass, Object primaryKey) {
        T returnValue = null;
        try {
            returnValue =  _getDelegate().find(entityClass, primaryKey);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public <T> T getReference(Class<T> entityClass, Object primaryKey) {
        T returnValue = null;
        try {
            returnValue = _getDelegate().getReference(entityClass, primaryKey);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public void flush() {
        // tx is ALWAYS required, regardless of persistence context type.
        doTxRequiredCheck();
        _getDelegate().flush();
    }

    public Query createQuery(String ejbqlString) {
        Query returnValue = null;
        try {
            returnValue = _getDelegate().createQuery(ejbqlString);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public Query createNamedQuery(String name) {
        Query returnValue = null;
        try {
            returnValue = _getDelegate().createNamedQuery(name);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public Query createNativeQuery(String sqlString) {
        Query returnValue = null;
        try {
            returnValue = _getDelegate().createNativeQuery(sqlString);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public Query createNativeQuery(String sqlString, Class resultClass) {
        Query returnValue = null;
        try {
            returnValue = _getDelegate().createNativeQuery
                (sqlString, resultClass);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public Query createNativeQuery(String sqlString, String resultSetMapping) {
        Query returnValue = null;
        try {
            returnValue = _getDelegate().createNativeQuery
                (sqlString, resultSetMapping);
        } finally {
            if( delegateInfo.nonTxEntityManager ) {
                nonTxEntityManager.clear();
            }
        }
        return returnValue;
    }

    public void refresh(Object entity) {
        doTransactionScopedTxCheck();
        _getDelegate().refresh(entity);
    }

    public boolean contains(Object entity) {
        return _getDelegate().contains(entity);
    }

    public void close() {
        // close() not allowed on container-managed EMs.
        throw new IllegalStateException();
    }

    public boolean isOpen() {
        // Not relevant for container-managed EMs.  Just return true.
        return true;
    }

    public EntityTransaction getTransaction() {
        return _getDelegate().getTransaction();
    }

    public void lock(Object entity, LockModeType lockMode) {
        _getDelegate().lock(entity, lockMode);
    }

    public void clear() {
        _getDelegate().clear();
    }

    public Object getDelegate() {
        return _getDelegate();
    }

    public FlushModeType getFlushMode() {
        return _getDelegate().getFlushMode();
    }

    public void setFlushMode(FlushModeType flushMode) {
        _getDelegate().setFlushMode(flushMode);
    }

    public void joinTransaction() {
        // Doesn't apply to the container-managed case, but all the
        // spec says is that an exception should be thrown if called
        // without a tx.
        doTxRequiredCheck();

        // There's no point in calling anything on the physical 
        // entity manager since in all tx cases it will be
        // correctly associated with a tx already.
    }

    private static class DelegateInfo {
        boolean nonTxEntityManager;
    }
}
