/*
 * 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 com.sun.enterprise.admin.monitor.callflow.Agent;
import com.sun.enterprise.admin.monitor.callflow.EntityManagerMethod;
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;

    // set and cleared after each non-tx, non EXTENDED call to _getDelegate()
    transient private EntityManager nonTxEntityManager;
    
    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();

    }

    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;

        if( nonTxEntityManager != null ) {
            cleanupNonTxEntityManager();
        }

        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 {

                nonTxEntityManager = entityManagerFactory.createEntityManager
                    (emProperties);

                // Return a new 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;

    }
    
    private void cleanupNonTxEntityManager() {
        if( nonTxEntityManager != null ) {
            nonTxEntityManager.close();
            nonTxEntityManager = null;
        }
    }

    public void persist(Object entity) {
        doTransactionScopedTxCheck();
        
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.PERSIST);
            }
            _getDelegate().persist(entity);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }

        // tx is required so there's no need to do any non-tx cleanup
    }

    public <T> T merge(T entity) {
        doTransactionScopedTxCheck();
        
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.MERGE);
            }
            return _getDelegate().merge(entity);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }

        // tx is required so there's no need to do any non-tx cleanup
    }

    public void remove(Object entity) {
        doTransactionScopedTxCheck();
        
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.REMOVE);
            }
            _getDelegate().remove(entity);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        
        // tx is required so there's no need to do any non-tx cleanup
    }

    public <T> T find(Class<T> entityClass, Object primaryKey) {
        T returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.FIND);
            }
            returnValue = _getDelegate().find(entityClass, primaryKey);
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public <T> T getReference(Class<T> entityClass, Object primaryKey) {
        T returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.GET_REFERENCE);
            }
            returnValue = _getDelegate().getReference(entityClass, primaryKey);
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public void flush() {
        // tx is ALWAYS required, regardless of persistence context type.
        doTxRequiredCheck();
        
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.FLUSH);
            }
            _getDelegate().flush();
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        
        // tx is required so there's no need to do any non-tx cleanup
    }

    public Query createQuery(String ejbqlString) {
        Query returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CREATE_QUERY);
            }
            EntityManager delegate = _getDelegate();
            returnValue = delegate.createQuery(ejbqlString);

            if( nonTxEntityManager != null ) {
                Query queryDelegate = returnValue;
                returnValue = QueryWrapper.createQueryWrapper
                    (entityManagerFactory, emProperties, delegate,
                     queryDelegate, ejbqlString);
                // It's now the responsibility of the QueryWrapper to 
                // close the non-tx EM delegate
                nonTxEntityManager = null;
            }
        } catch(RuntimeException re) {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            throw re;
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public Query createNamedQuery(String name) {
        Query returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CREATE_NAMED_QUERY);
            }
            EntityManager delegate = _getDelegate();
            returnValue = delegate.createNamedQuery(name);

            if( nonTxEntityManager != null ) {
                Query queryDelegate = returnValue;
                returnValue = QueryWrapper.createNamedQueryWrapper
                    (entityManagerFactory, emProperties, delegate,
                     queryDelegate, name);
                // It's now the responsibility of the QueryWrapper to 
                // close the non-tx EM delegate
                nonTxEntityManager = null;
            }
        } catch(RuntimeException re) {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            throw re;
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }

        return returnValue;
    }

    public Query createNativeQuery(String sqlString) {
        Query returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CREATE_NATIVE_QUERY_STRING);
            }
            EntityManager delegate = _getDelegate();
            returnValue = delegate.createNativeQuery(sqlString);

            if( nonTxEntityManager != null ) {
                Query queryDelegate = returnValue;
                returnValue = QueryWrapper.createNativeQueryWrapper
                    (entityManagerFactory, emProperties, delegate,
                     queryDelegate, sqlString);
                // It's now the responsibility of the QueryWrapper to 
                // close the non-tx EM delegate
                nonTxEntityManager = null;
            }
        } catch(RuntimeException re) {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            throw re;
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public Query createNativeQuery(String sqlString, Class resultClass) {
        Query returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CREATE_NATIVE_QUERY_STRING_CLASS);
            }
            EntityManager delegate = _getDelegate();
            returnValue = delegate.createNativeQuery(sqlString, resultClass);

            if( nonTxEntityManager != null ) {
                Query queryDelegate = returnValue;
                returnValue = QueryWrapper.createNativeQueryWrapper
                    (entityManagerFactory, emProperties, delegate,
                     queryDelegate, sqlString, resultClass);
                // It's now the responsibility of the QueryWrapper to 
                // close the non-tx EM delegate
                nonTxEntityManager = null;
            }
        } catch(RuntimeException re) {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            throw re;
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public Query createNativeQuery(String sqlString, String resultSetMapping) {
        Query returnValue = null;
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CREATE_NATIVE_QUERY_STRING_STRING);
            }
            EntityManager delegate = _getDelegate();
            returnValue = delegate.createNativeQuery
                (sqlString, resultSetMapping);

            if( nonTxEntityManager != null ) {
                Query queryDelegate = returnValue;
                returnValue = QueryWrapper.createNativeQueryWrapper
                    (entityManagerFactory, emProperties, delegate,
                     queryDelegate, sqlString, resultSetMapping);
                // It's now the responsibility of the QueryWrapper to 
                // close the non-tx EM delegate
                nonTxEntityManager = null;
            }
        } catch(RuntimeException re) {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            throw re;
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        return returnValue;
    }

    public void refresh(Object entity) {
        doTransactionScopedTxCheck();
        
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.REFRESH);
            }
            _getDelegate().refresh(entity);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
        
        // tx is required so there's no need to do any non-tx cleanup
    }

    public boolean contains(Object entity) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CONTAINS);
            }
            EntityManager delegate = _getDelegate();
            return delegate.contains(entity);
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    public void close() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        if(callFlowAgent.isEnabled()) {
            callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CLOSE);
            callFlowAgent.entityManagerMethodEnd();
        }
        // close() not allowed on container-managed EMs.
        throw new IllegalStateException();
    }

    public boolean isOpen() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        if(callFlowAgent.isEnabled()) {
            callFlowAgent.entityManagerMethodStart(EntityManagerMethod.IS_OPEN);
            callFlowAgent.entityManagerMethodEnd();
        }
        // Not relevant for container-managed EMs.  Just return true.
        return true;
    }

    public EntityTransaction getTransaction() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.GET_TRANSACTION);
            }
            return _getDelegate().getTransaction();
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    public void lock(Object entity, LockModeType lockMode) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.LOCK);
            }
            _getDelegate().lock(entity, lockMode);
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    public void clear() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.CLEAR);
            }
            _getDelegate().clear();
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    public Object getDelegate() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.GET_DELEGATE);
            }
            return _getDelegate();
        } finally {
            if( nonTxEntityManager != null ) {
                // In this case we can't close the physical EntityManager
                // before returning it to the application, so we just clear
                // the EM wrapper's reference to it.  
                nonTxEntityManager = null;
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }

    }

    public FlushModeType getFlushMode() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.GET_FLUSH_MODE);
            }
            return _getDelegate().getFlushMode();
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    public void setFlushMode(FlushModeType flushMode) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodStart(EntityManagerMethod.SET_FLUSH_MODE);
            }
            _getDelegate().setFlushMode(flushMode);
        } finally {
            if( nonTxEntityManager != null ) {
                cleanupNonTxEntityManager();
            }
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerMethodEnd();
            }
        }
    }

    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();
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        if(callFlowAgent.isEnabled()) {
            callFlowAgent.entityManagerMethodStart(EntityManagerMethod.JOIN_TRANSACTION);
            callFlowAgent.entityManagerMethodEnd();
        }

        // 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.
    }

}
