/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.lang.reflect.InvocationTargetException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.naming.Context;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.ow2.jonas.deployment.ejb.SessionStatelessDesc;
import org.ow2.jonas.lib.timer.TraceTimer;



import org.objectweb.util.monolog.api.BasicLevel;

/**
 * This class is a factory for a Session Stateless Bean.
 * @author Philippe Durieux
 */
public class JStatelessFactory extends JSessionFactory {

    /**
     *  instance pool management (list of available JSessionContext objects)
     * Contexts are pooled only for Stateless Session beans because for Stateful
     * sessions a newInstance() is required by the spec.
     * We don't need synchronizedList here, because anyway, we
     * synchronize already everywhere.
     */
    protected List bctxlist = new ArrayList();

    // service endpoint home
    protected JServiceEndpointHome sehome = null;

    protected int instanceCount = 0;

    // initial value for pool size
    protected int minPoolSize = 0;

    // nb max of instances in pool
    protected int maxCacheSize = 0;

	private static final int MAX_NB_RETRY = 2;


    /**
     * constructor
     * @param dd Session Stateless Deployment Descriptor
     * @param cnt Container where the bean is defined
     */
    public JStatelessFactory(SessionStatelessDesc dd, JContainer cont) {
        super(dd, cont);
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        // Create the ServiceEndpointHome if defined in DD
        Class sehomeclass = null;
        String clname = dd.getFullWrpSEHomeName();
        if (clname != null) {
            try {
                sehomeclass = cont.getClassLoader().loadClass(clname);
            } catch (ClassNotFoundException e) {
                throw new EJBException(ejbname + " Cannot load " + clname, e);
            }
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, ejbname + ": " + clname + " loaded");
            }
            try {
                // new JServiceEndpointHome(dd, this)
                int nbp = 2;
                Class[] ptype = new Class[nbp];
                Object[] pobj = new Object[nbp];
                ptype[0] = org.ow2.jonas.deployment.ejb.SessionStatelessDesc.class;
                pobj[0] = (Object) dd;
                ptype[1] = org.ow2.jonas.lib.ejb21.JStatelessFactory.class;
                pobj[1] = (Object) this;
                sehome = (JServiceEndpointHome) sehomeclass.getConstructor(ptype).newInstance(pobj);
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot create serviceEndpointHome ", e);
            }
            // register it in JNDI
            try {
                sehome.register();
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot register serviceEndpointHome ", e);
            }
        }

        isStateful = false;
        maxCacheSize = dd.getCacheMax();
        minPoolSize = dd.getPoolMin();
        if (maxCacheSize > 0 && TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, " maxCacheSize = " + maxCacheSize);
        }
        singleswitch = (timeout == 0) && dd.isSingleton();
    }

    /**
     * Init pool of instances.
     */
    public void initInstancePool() {
        JStatelessSwitch ss = null;
        // Create a SessionSwitch if needed
        if (minPoolSize != 0 || singleswitch) {
            try {
                ss = (JStatelessSwitch) createEJB();
            } catch (RemoteException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot create new session", e);
                throw new EJBException(ejbname + " Cannot create session for pool: ", e);
            }
        }
        // pre-allocate a set of JSessionContext (bean instances)
        if (minPoolSize != 0) {
            TraceEjb.interp.log(BasicLevel.INFO, "pre-allocate a set of " + minPoolSize
                    + " stateless session instances");
            Context bnctx = setComponentContext(); // for createNewInstance
            // Set also the EJB classloader as context classloader
            ClassLoader old = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(this.myClassLoader());
            try {
                synchronized (bctxlist) {
                    for (int i = 0; i < minPoolSize; i++) {
                        JSessionContext ctx = null;
                        try {
                            // must pass a session switch (for ejbCreate)
                            ctx = createNewInstance(ss);
                            if (ctx != null) {
                                bctxlist.add(ctx);
                            } else {
                                // Should never go here.
                                TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " null instance");
                            }
                        } catch (Exception e) {
                            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot create new instance", e);
                            throw new EJBException(ejbname + " Cannot init pool of instances ", e);
                        }
                    }
                }
            } finally {
                // reset the context classloader
                Thread.currentThread().setContextClassLoader(old);

                // reset java:comp/env
                resetComponentContext(bnctx);
            }
        }
        // Remove Temporary session switch
        if (ss != null && !singleswitch) {
            ss.stopTimer();
            ss.noLongerUsed();
        }
    }

    public JServiceEndpointHome getSEHome() {
        return sehome;
    }

    // ---------------------------------------------------------------
    // BeanFactory implementation
    // ---------------------------------------------------------------

    /**
     * @return the Instance pool size for this Ejb
     */
    public int getPoolSize() {
        return bctxlist.size();
    }

    /**
     * Reduce number of instances in memory in the free list we reduce to the
     * minPoolSize
     */
    public void reduceCache() {
        // reduce the pool to the minPoolSize
        synchronized (bctxlist) {
            while (bctxlist.size() > minPoolSize) {
                ListIterator i = bctxlist.listIterator();
                if (i.hasNext()) {
                    i.next();
                    i.remove();
                    instanceCount--;
                }
            }
        }
    }

    // ---------------------------------------------------------------
    // other public methods
    // ---------------------------------------------------------------

    /**
     * Obtains the TimerService associated for this Bean
     * @return a JTimerService instance.
     */
    public TimerService getTimerService() {
        if (myTimerService == null) {
            // TODO : Check that instance implements TimedObject ?
            myTimerService = new JTimerService(this);
        }
        return myTimerService;
    }

    /**
     * Creates a new Session Stateless
     * @return the new JSessionSwitch
     */
    public JSessionSwitch createNewSession() throws RemoteException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        JStatelessSwitch bs = new JStatelessSwitch(this);
        return bs;
    }

    /**
     * @return a SessionContext for Stateless Session Bean
     */
    public JSessionContext getJContext(JSessionSwitch ss) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        JStatelessContext bctx = null;

        // try to find a free context in the pool
        synchronized (bctxlist) {
            try {
                ListIterator i = bctxlist.listIterator();
                if (i.hasNext()) {
                    bctx = (JStatelessContext) i.next();
                    i.remove();
                    if (bctx != null) { // workaround for an old bug.
                        bctx.initSessionContext(ss);
                    } else {
                        TraceEjb.interp.log(BasicLevel.ERROR, "null elt in bctxlist");
                    }
                }
            } catch (IndexOutOfBoundsException ex) {
                // pool is empty
            }
        }

        if (bctx == null) {
            // create a new one if pool empty
            try {
                bctx = createNewInstance(ss);
            } catch (Exception e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "exception:" + e);
                throw new EJBException("Cannot create a new instance", e);
            }
        }
        if (maxCacheSize != 0 && TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, "nb instances " + getCacheSize());
        }
        return bctx;
    }

    /**
     * Called after each method call
     * @param ctx the Session Context
     */
    public void releaseJContext(JContext ctx) {
        if (ctx == null) {
            TraceEjb.swapper.log(BasicLevel.ERROR, "null ctx!");
            return;
        }
        JStatelessContext bctx = (JStatelessContext) ctx;

        // if the max number of instances is reached
        // we can reduce the number of instances
        synchronized (bctxlist) {
            if (maxCacheSize != 0 && instanceCount > maxCacheSize) {
                if (TraceEjb.isDebugSwapper()) {
                    TraceEjb.swapper.log(BasicLevel.DEBUG, "too many instances :" + instanceCount + ", max="
                            + maxCacheSize);
                }
                instanceCount--;
            } else {
                bctxlist.add(bctx);
            }
        }
        if (maxCacheSize != 0 && TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, "nb instances " + getCacheSize());
        }
    }

    /**
     * Notify a timeout for this bean
     * @param timer timer whose expiration caused this notification.
     */
    public void notifyTimeout(Timer timer) {
        if (stopped) {
            TraceEjb.mdb.log(BasicLevel.WARN, "Container stopped");
            return;
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "");
        }

        // Get a JStatelessSwitch (similar to createEJB)
        // In fact, we don't need the Local and Remote objects here.
        // This object is just needed to initilize the SessionContext.
        JSessionSwitch bs = null;
        if (sessionList.size() > 0) {
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, "get a JStatelessSwitch from the pool");
            }
            bs = (JSessionSwitch) sessionList.remove(0);
            // must re-export the remote object in the Orb since EJBObjects
            // to avoids a fail when object is put in the pool.
            JSessionRemote remote = bs.getRemote();
            if (remote != null) {
                if (!remote.exportObject()) {
                    TraceEjb.logger.log(BasicLevel.ERROR, "bad JSessionSwitch found in pool.");
                }
            }
        } else {
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, "create a new JStatelessSwitch");
            }
            try {
                bs = new JStatelessSwitch(this);
            } catch (RemoteException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "Notify Timeout - Unexpected : ", e);
                return;
            }
        }
        //EJBInvocation ejbInv = new EJBInvocation();
        //ejbInv.methodPermissionSignature = getEjbTimeoutSignature();
        //ejbInv.arguments = new Object [] {timer};

        for (int nbretry = 0; nbretry < MAX_NB_RETRY; nbretry++) {
            RequestCtx rctx = preInvoke(getTimerTxAttribute());
            JSessionContext bctx = null;
            try {
                bctx = bs.getICtx(rctx.currTx);
                TimedObject instance = (TimedObject) bctx.getInstance();
                //ejbInv.bean = (EnterpriseBean) instance;
                checkSecurity(null);
                instance.ejbTimeout(timer);

                // Must release bctx (not done any longer in timeoutExpired)
                //bctx.setRemoved(); ???
                releaseJContext(bctx);

                if (rctx.currTx == null || (rctx.currTx.getStatus() != Status.STATUS_MARKED_ROLLBACK)) {
                    break;
                }
            } catch (EJBException e) {
                rctx.sysExc = e;
                throw e;
            } catch (RuntimeException e) {
                rctx.sysExc = e;
                throw new EJBException("RuntimeException thrown by an enterprise Bean", e);
            } catch (Error e) {
                rctx.sysExc = e;
                throw new EJBException("Error thrown by an enterprise Bean" + e);
            } catch (RemoteException e) {
                rctx.sysExc = e;
                throw new EJBException("Remote Exception raised:", e);
            } catch (SystemException e) {
                rctx.sysExc = e;
                throw new EJBException("Cannot get transaction status:", e);
            } finally {
                postInvoke(rctx);
            }
        }

        // release everything - Exactly as if the session timeout had
        // expired.
        bs.timeoutExpired(null);
    }

    /**
     * @return min pool size for Jmx
     */
    public int getMinPoolSize() {
        return minPoolSize;
    }

    /**
     * @return max cache size for Jmx
     */
    public int getMaxCacheSize() {
        return maxCacheSize;
    }

    /**
     * @return current cache size ( = nb of instance created) for Jmx
     */
    public int getCacheSize() {
        return instanceCount;
    }

    // ---------------------------------------------------------------
    // private methods
    // ---------------------------------------------------------------

    /**
     * Create a new instance of the bean and its StatelessContext
     */
    private JStatelessContext createNewInstance(JSessionSwitch ss) throws Exception {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        // create the bean instance
        SessionBean bean = null;
        try {
            bean = (SessionBean) beanclass.newInstance();
        } catch (InstantiationException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot instantiate session bean");
            throw e;
        } catch (IllegalAccessException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " Cannot instantiate SessionBean");
            throw e;
        }

        // create a new StatelessContext and bind it to the instance
        JStatelessContext bctx = new JStatelessContext(this, bean);
        bean.setSessionContext(bctx);
        bctx.setPassive();
        bctx.initSessionContext(ss);
        // call ejbCreate on the instance
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "call ejbCreate on the instance");
        }
        try {
            beanclass.getMethod("ejbCreate", (Class[]) null).invoke(bean, (Object[]) null);
        } catch (InvocationTargetException ite) {
            // get the root cause
            Throwable t = ite.getTargetException();
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot invoke ejbCreate on Stateless Session " + t.getMessage(), t);
        } catch (Exception e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot call ejbCreate on Stateless Session " + e.getMessage(), e);
        }
        bctx.setActive();

        // We always create an instance even if the max is reached
        // see releaseJContext
        synchronized (bctxlist) {
            instanceCount++;
        }
        return bctx;
    }

    /*
     * Make sense only for entities
     */
    public void storeInstances(Transaction tx) {
        // unused
    }
}
