/*
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@objectweb.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
 *
 * Initial developer(s): Philippe Coq, Philippe Durieux
 * Contributor(s):
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas_ejb.container;

import java.lang.InstantiationException;
import java.lang.reflect.InvocationTargetException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.ListIterator;
import javax.ejb.EJBException;
import javax.ejb.EntityBean;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.InitialContext;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.sql.DataSource;
import javax.transaction.SystemException;
import org.enhydra.naming.ContainerNaming;
import org.objectweb.jonas_ejb.deployment.api.EntityDesc;
import org.objectweb.jonas_ejb.deployment.api.EntityJdbcCmpDesc;

/**
 * This class is a factory for an Entity Bean. It is responsible for
 * - managing Home and LocalHome.
 * - managing a pool of instances/contexts
 * - keeping the list of PKs and the associated JEntitySwitch's
 * - keeping the JNDI context for this component (java:comp/env)
 */
public class JEntityFactory extends JFactory {

    protected JEntityHome home = null;
    protected JEntityLocalHome localhome = null;
    protected boolean reentrant = false;
    protected boolean shared = false;

    // instance pool management (list of available JEntityContext objects)
    protected List bctxlist = Collections.synchronizedList(new ArrayList());
    protected int instanceCount = 0;
    protected int maxCacheSize;
    protected int minPoolSize;

    // PKs management
    // At each PK is associated a JEntitySwitch object
    protected Hashtable pklist = new Hashtable();

    // Datasource in case of CMP
    protected DataSource datasource = null;
    
    /**
     * constructor
     * must be without parameters (required by Jorm)
     */
    public JEntityFactory() {
	TraceEjb.debugIc("JEntityFactory constructor");
    }

    /**
     * Init this object 
     * @args dd the deployment descriptor
     * @args cont the Container
     */
    public void init(EntityDesc dd, JContainer cont) {
	super.init(dd, cont);
	reentrant = dd.isReentrant();
	shared = dd.isShared();
	maxCacheSize = dd.getCacheMax();
	minPoolSize = dd.getPoolMin();

	// Finds the DataSource associated to the bean
	// Useful for Entity with Container Managed Persistence.
	if (dd instanceof EntityJdbcCmpDesc) {
	    String dsname = ((EntityJdbcCmpDesc)dd).getDatasourceJndiName();
	    if (dsname != null) {
		try {
		    datasource = (DataSource) getInitialContext().lookup(dsname);
		} catch(NamingException e) {
		    TraceEjb.error("JEntityFactory: "+dsname+"' is not known by the EJB server.");
		    throw new EJBException(dsname+" unknown");
		}
	    }
	}

	// Create the Home if defined in DD
	Class homeclass = null;
	String clname = bn.getFullWrpHomeName();
	if (clname != null) {
	    try {
		homeclass = cont.getClassLoader().loadClass(clname);
	    } catch (ClassNotFoundException e) {
		throw new EJBException(ejbname+"Cannot load "+ clname);
	    }
	    if (TraceEjb.isDebugIc())
		TraceEjb.debugIc(ejbname+": "+clname+" loaded");
	    try {
		// new JEntityHome(dd, this)
		int nbp = 2;
		Class ptype[] = new Class[nbp];
		Object pobj[] = new Object[nbp];
		ptype[0] = org.objectweb.jonas_ejb.deployment.api.EntityDesc.class;
		pobj[0] = (Object) dd;
		ptype[1] = org.objectweb.jonas_ejb.container.JEntityFactory.class;
		pobj[1] = (Object) this;
		home = (JEntityHome) homeclass.getConstructor(ptype).newInstance(pobj);
	    } catch (Exception e) {
		throw new EJBException(ejbname+" Cannot create home ", e);
	    }
	    // register it in JNDI
	    try {
		home.register();
	    } catch (Exception e) {
		throw new EJBException(ejbname+" Cannot register home ", e);
	    }
	}

	// Create the LocalHome if defined in DD
	clname = bn.getFullWrpLocalHomeName();
	if (clname != null) {
	    try {
		homeclass = cont.getClassLoader().loadClass(clname);
	    } catch (ClassNotFoundException e) {
		TraceEjb.error(ejbname+" Cannot load "+ clname);
		throw new EJBException("Cannot load "+ clname);
	    }
	    if (TraceEjb.isDebugIc())
		TraceEjb.debugIc(ejbname+": "+clname+" loaded");
	    try {
		// new JEntityLocalHome(dd, this)
		int nbp = 2;
		Class ptype[] = new Class[nbp];
		Object pobj[] = new Object[nbp];
		ptype[0] = org.objectweb.jonas_ejb.deployment.api.EntityDesc.class;
		pobj[0] = (Object) dd;
		ptype[1] = org.objectweb.jonas_ejb.container.JEntityFactory.class;
		pobj[1] = (Object) this;
		localhome = (JEntityLocalHome) homeclass.getConstructor(ptype).newInstance(pobj);
	    } catch (Exception e) {
		throw new EJBException(ejbname+" Cannot create localhome ", e);
	    }
	    // register it in JNDI
	    try {
		localhome.register();
	    } catch (Exception e) {
		throw new EJBException(ejbname+" Cannot register localhome ", e);
	    }
	}

	// pre-allocate a set of JEntityContext (bean instances)
	for (int i = 0; i < minPoolSize; i++) {
	    JEntityContext ctx = null;
	    try {
		ctx = createNewInstance();
		bctxlist.add(ctx);
	    } catch (Exception e) {
		TraceEjb.error(ejbname+" Cannot create New Instance", e);
		throw new EJBException(ejbname+" Cannot init pool of instances ", e);
	    }
	}
    }
	
    /**
     * get EJB by its PK
     * Creates if not exist yet.
     */
    public synchronized JEntitySwitch getEJB(Object pk) {

	TraceEjb.debugIc("JEntityFactory: getEJB");
	
	JEntitySwitch bs = (JEntitySwitch) pklist.get(pk);
	if (bs == null) {
	    bs = new JEntitySwitch(this, pk);
	    pklist.put(pk, bs);
	}
	return bs;
    }

    /**
     * remove an EJB by its PK
     */
    public synchronized void removeEJB(Object pk) {

	TraceEjb.debugIc("JEntityFactory: removeEJB");

	pklist.remove(pk);
    }

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

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

    /**
     * stop this EJB.
     * Mainly unregister it in JNDI.
     */
    public void stop() {
	TraceEjb.debugIc("JEntityFactory stop");
	try {
	    if (home != null) {
		home.unregister();
	    }
	    if (localhome != null) {
		localhome.unregister();
	    }
	} catch (NamingException e) {
	}
    }

    /**
     * synchronize bean instances if needed
     */
    public void sync() {
	TraceEjb.debugIc("JEntityFactory sync");
	passivateAllIH(false);
    }

    /**
     * Reduce number of instances in memory
     */
    public void reduceCache() {
	TraceEjb.debugIc("JEntityFactory.reduceCache");

	// passivate all instances that are not busy
 	int count = passivateAllIH(true);

	// compute the new size we want for the cache
	// if more busy instances than min size, we want an empty cache.
	int poolsz = minPoolSize - count;
	if (poolsz < 0) {
	    poolsz = 0;
	}

	// reduce the pool to this new value
	synchronized(bctxlist) {
	    while (bctxlist.size() > poolsz) {
		ListIterator i = bctxlist.listIterator();
		if (i.hasNext()) {
		    i.next();
		    i.remove();
		    instanceCount--;
		}
	    }
	}
    }

    /**
     * returns the home if exist
     */
    public JHome getHome() {
	return home;
    }

    /**
     * returns the local home if exist
     */
    public JLocalHome getLocalHome() {
	return localhome;
    }

    // ---------------------------------------------------------------
    // Context Pool
    // ---------------------------------------------------------------

    /**
     * Get a Context from the pool, or create a new one if no more
     * available in the pool. 
     * This JContext must be initialized then by the caller.
     */
    public JEntityContext getJContext() {
	TraceEjb.debugIc("JEntityFactory.getJContext");
	JEntityContext bctx = null;

	// try to find a free context in the pool
	synchronized(bctxlist) {
	    try {
		ListIterator i = bctxlist.listIterator();
		if (i.hasNext()) {
		    bctx = (JEntityContext) i.next();
		    i.remove();
		}
	    } catch(IndexOutOfBoundsException ex) {
		// pool is empty
	    }
	}

	if (bctx == null) {
	    // create a new one if pool empty
	    try {
		bctx = createNewInstance();
	    } catch (Exception e) {
		throw new EJBException("Cannot create a new instance");
	    }
	}
	return bctx;
    }


    /**
     * Release a Context
     */
    public void releaseJContext(JContext ctx) {
	TraceEjb.debugIc("JEntityFactory.releaseJContext");
	JEntityContext bctx = (JEntityContext) ctx;
	bctx.razEntityContext();
	bctxlist.add(bctx);
    }

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

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

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

    /**
     * returns current cache size
     * for Jmx
     */
    public int getCacheSize() {
	return 0;		// TODO
    }

    /**
     * return true if reentrant
     */
    public boolean isReentrant() {
	return reentrant;
    }

    /**
     * return true if shared
     */
    public boolean isShared() {
	return shared;
    }

    /**
     * returns the Datasource used for CMP
     */
    public DataSource getDataSource() {
	if (datasource == null) {
	    TraceEjb.error("Datasource not defined in JEntityFactory");
	}
	return datasource;
    }
	
    /**
     * For Entity beans, the only possible case is "Container Managed Tx"
     */
    public void checkTransaction(RequestCtx rctx) throws SystemException {
	checkTransactionContainer(rctx);
    }

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

    /**
     * Synchronizes all instances that are used outside a transaction.
     * @return int count of instances that where busy.
     */
    private int passivateAllIH(boolean passivation) {
	TraceEjb.debugIc("passivateAllIH");
	int busy = 0;

	// Set the JNDI component context (for ejbStore)
	Context bnctx = setComponentContext();

	Enumeration enum = null;
	synchronized(this) {
	    enum = pklist.elements();
	}

	JEntitySwitch bs = null;
	while (enum.hasMoreElements()) {
	    bs = (JEntitySwitch) enum.nextElement();
	    if (bs.passivateIH(passivation) == false) {
		busy++;
	    }
	}

	// Reset old value for component context
	resetComponentContext(bnctx);

	return busy;
    }

    /**
     * Create a new instance of the bean and its EntityContext
     * In case of CMP, the bean class is derived to manage entity persistence.
     */
    private JEntityContext createNewInstance() throws Exception {
	TraceEjb.debugIc("JentityFactory.createNewInstance");

	// create the bean instance
	EntityBean bean = null;
	try {
	    bean = (EntityBean) beanclass.newInstance();
	} catch (InstantiationException e) {
	    TraceEjb.error(ejbname+" Cannot instantiate bean");
	    throw e;
	} catch (IllegalAccessException e) {
	    TraceEjb.error(ejbname+" Cannot instantiate bean");
	    throw e;
	}

	// create a new EntityContext and bind it to the instance
	JEntityContext ec = new JEntityContext(this, bean);
	bean.setEntityContext(ec);

	// If too much instances, ask swapper to clean memory.
	instanceCount++;
	if (maxCacheSize != 0 && instanceCount > maxCacheSize) {
	    cont.registerBF(this);
	}

	return ec;
    }
}
