/*
 * 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.InterruptedException;
import java.lang.Object;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.util.Hashtable;
import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.TransactionRolledbackLocalException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import org.objectweb.common.RemoteObject;

/**
 * JEntitySwitch is used internally to synchronize accesses to the entity context 
 * and thus to the entity bean instance.
 * All parts common to EJBObject and EJBLocalObject should be here.
 * Different policies can be applied to manage context/instance pairs:
 * - only 1 pair (container manages the transaction isolation)
 * - 1 pair for each transaction (transaction isolation managed by DataBase)
 * - 2 pairs (1 for transactional accesses, 1 for non transaction accesses)
 * In the current version, we implement only the first. Others will be implemented later.
 */
public class JEntitySwitch {

    protected JEntityFactory bf;
    protected Object pk = null;
    protected JEntityLocal local = null;
    protected JEntityRemote remote = null;
    private JContainer cont;
    protected boolean shared;
    protected boolean reentrant;

    // nb of non transacted requests running
    protected int IHcount = 0;

    // unique EntityContext 
    protected JEntityContext Icontext = null;

    // ident is used mainly for debugging
    private static int counter = 0;
    protected String ident;

    // nb of threads waiting for the Context
    private int waiters = 0;

    // True if ctx has been used outside transactions.
    private boolean dirty = false;

    // True if a TX need this instance currently used outside tx
    private boolean mustStore = false;

    // True if we must reload the bean state before use.
    private boolean mustLoad = false;

    // True when context/instance has been discarded (outside tx only)
    private boolean IHdiscarded = false;

    // current tx running on this instance and not yet committed.
    protected Transaction currtx = null;

    // max time to wait when in a transaction, before checking if
    // transaction has been set rollback only (deadlock detection)
    private static int maxtime = 5000; // 5 sec.

    /**
     * constructor. 
     * A new object is build when a new PK is known in the container,
     * either when a new bean is created, or when a find occurs.
     * For create(), PK is not known yet when this object is build.
     */
    public JEntitySwitch(JEntityFactory bf, Object pk) {
	TraceEjb.debugIc("JEntitySwitch constructor");
	this.bf = bf;
	this.pk = pk;

	// keep these in local, to be more efficient.
	cont = bf.getContainer();
	shared = bf.isShared();
	reentrant = bf.isReentrant();

	// generate new ident (debug)
	ident = "<"+counter+">";
	counter++;

	// Create EJBObject if bean has a Remote Interface
	if (bf.getHome() != null) {
	    try {
		remote = ((JEntityHome)bf.getHome()).createRemoteObject();
		remote.setEntitySwitch(this);
	    } catch (RemoteException e) {
		throw new EJBException("cannot create Remote Object", e);
	    }
	}

	// Create EJBLocalObject if bean has a Local Interface
	if (bf.getLocalHome() != null) {
	    local = ((JEntityLocalHome)bf.getLocalHome()).createLocalObject();
	    local.setEntitySwitch(this);
	}
    }

    /**
     * Return the underlaying EJBLocalObject
     */
    public JEntityLocal getLocal() {
	return local;
    }

    /**
     * Return the underlaying EJBObject
     */
    public JEntityRemote getRemote() {
	return remote;
    }

    /**
     * Returns the PK
     */
    public Object getPrimaryKey() {
	if (pk == null) {
	    throw new EJBException("No Primary Key yet");
	}
	return pk;
    }

    /**
     * bind a JEntityContext for a create method.
     * No need to synchronize since object was unknown before.
     * Do not activate the bean instance since ejbCreate has been done.
     * @param tx - the Transaction object
     * @param bctx - the JEntityContext to bind
     */
    public synchronized void bindICtx(Transaction tx, JEntityContext bctx) {

	if (tx == null) {
	    TraceEjb.debugIc(ident, "IH create");
	    Icontext = bctx;
	    bctx.initEntityContext(this, tx);
	} else {
	    TraceEjb.debugIc(ident, "IT create");
	    Icontext = bctx;
	    bctx.initEntityContext(this, tx);
	    if (currtx == null) {
		currtx = tx;
		try {
		    tx.registerSynchronization(bctx);
		} catch (RollbackException e) {
		    TraceEjb.verbose(ident, "IT create: transaction has been marked rollbackOnly");
		} catch (SystemException e) {
		    TraceEjb.error(ident, "IT create: ", e);
		    currtx = null; // not registered -> no tx (avoids deadlocks)
		    throw new IllegalStateException("cannot register synchro");
		} catch (IllegalStateException e) {
		    TraceEjb.error(ident, "IT create: Transaction is in an illegal state");
		    currtx = null; // not registered -> no tx (avoids deadlocks)
		    throw e;
		}
	    }
	}
    }

    /**
     * Try to bind a JEntityContext if none already bound.
     * Called by finder methods
     * @param tx - the Transaction object
     */
    public synchronized void tryBindICtx(Transaction tx, JEntityContext bctx) {

	// Don't bind if already a context.
	if (Icontext != null) {
		bf.releaseJContext(bctx);
		return;
	}
	    
	// No synchronization, since this is used for readonly methods (finder)
	if (tx == null) {
	    TraceEjb.debugIc(ident, "IH find");
	    Icontext = bctx;
	    bctx.initEntityContext(this, tx);
	    bctx.activate(true);
	} else {
	    TraceEjb.debugIc(ident, "IT find");
	    Icontext = bctx;
	    bctx.initEntityContext(this, tx);
	    bctx.activate(true);
	    if (currtx == null) {
		currtx = tx;
		try {
		    tx.registerSynchronization(bctx);
		} catch (RollbackException e) {
		    TraceEjb.verbose(ident, "bindICtx IT: transaction has been marked rollbackOnly");
		} catch (SystemException e) {
		    TraceEjb.error(ident, "bindICtx IT: ", e);
		    currtx = null;
		    throw new IllegalStateException("cannot register synchro");
		} catch (IllegalStateException e) {
		    TraceEjb.error(ident, "bindICtx IT: Transaction is in an illegal state");
		    currtx = null; // not registered -> no tx (avoids deadlocks)
		    throw e;
		}
	    }
	}
    }

    /**
     * bind a JEntityContext for a remove method.
     * @param tx - the Transaction object
     * @param bctx - the JEntityContext to bind
     * @return bctx - the BeanContext
     */
    public synchronized JEntityContext getICtx(Transaction tx, JEntityContext newctx) {

	JEntityContext bctx = newctx;

	if (tx == null) {
	    TraceEjb.debugIc(ident, "IH remove");

	    // Synchronization
	    // TODO

	    if (Icontext == null) {
		Icontext = bctx;
		bctx.initEntityContext(this, tx);
	    } else {
		bf.releaseJContext(bctx);
		bctx = Icontext;
		bctx.reuseEntityContext(tx);
	    }
	    // Do not increment IHcount since we won't call releaseICtx
	    // We consider that this instance can be modified.
	    dirty = true;
	} else {
	    TraceEjb.debugIc(ident, "IT remove");

	    // wait instance/context available
	    // TODO

	    if (Icontext == null) {
		Icontext = bctx;
		bctx.initEntityContext(this, tx);
	    } else {
		bctx = Icontext;
		bctx.reuseEntityContext(tx);
	    }
	    if (currtx == null) {
		currtx = tx;
		try {
		    tx.registerSynchronization(bctx);
		} catch (RollbackException e) {
		    TraceEjb.verbose(ident, "IT remove: transaction has been marked rollbackOnly");
		} catch (SystemException e) {
		    TraceEjb.error(ident, "IT remove: ", e);
		    currtx = null;
		    throw new IllegalStateException("cannot register synchro");
		} catch (IllegalStateException e) {
		    TraceEjb.error(ident, "IT remove: Transaction is in an illegal state");
		    currtx = null; // not registered -> no tx (avoids deadlocks)
		    throw e;
		}
	    }
	}
	return bctx;
    }

    /**
     * Get a context/instance associated with this transaction
     * Called at least at each request
     * @param tx - the Transaction object
     * @return bctx - the BeanContext
     */
    public synchronized JEntityContext getICtx(Transaction tx) {

	JEntityContext bctx;

	if (tx == null) {
	    // ------------- IH begin ------------- 
	    TraceEjb.debugIc(ident, "getICtx IH");

	    // Synchronization
	    while (currtx != null || (!reentrant && IHcount > 0)) {
		if (currtx != null) {
		    TraceEjb.debugIc(ident, "getICtx IH: WAIT end IT");
		} else {
		    TraceEjb.debugIc(ident, "getICtx IH: WAIT end other IH");
		}
		waiters++;
		try {
		    wait();
		} catch (Exception e) {
		    TraceEjb.error(ident, "getICtx IH:", e);
		    throw new EJBException("EntityContext Synchronization problem", e);
		} finally {
		    waiters--;
		}
		TraceEjb.debugIc(ident, "getICtx IH: NOTIFIED");
	    }

	    bctx = Icontext;

	    if (bctx == null) {
		// No context mapped: get one from the pool.
		TraceEjb.debugIc(ident, "getICtx IH: get new context from the pool");
		bctx = (JEntityContext) bf.getJContext();
		bctx.initEntityContext(this, tx);
		bctx.activate(true);
		Icontext = bctx;
	    } else {
		bctx.reuseEntityContext(tx);
		if (mustLoad) {	// "shared" has been proposed
		    TraceEjb.debugIc(ident, "getICtx IH: reload bean state");
		    bctx.activate(false);
		    mustLoad = false;
		}
	    }
	    IHcount++;
	    // We consider that this instance can be modified.
	    dirty = true;

	} else {
	    // ------------- IT begin ------------- 
	    TraceEjb.debugIc(ident, "getICtx IT");

	    // wait instance/context available
	    while (dirty || (currtx != null && !tx.equals(currtx))) {
		
		if (dirty) {
		    if (IHcount == 0) {
			// Cannot passivate here, because must be done outside tx context
			cont.registerEJB(this);
		    } else {
			mustStore = true;
		    }
		}
		if (dirty) {
		    TraceEjb.debugSynchro(ident, "getICtx IT: WAIT passivation");
		} else {
		    TraceEjb.debugSynchro(ident, "getICtx IT: WAIT end IT");
		}
		waiters++;
		try {
		    wait(maxtime);
		    TraceEjb.debugSynchro(ident, "getICtx IT: NOTIFIED"); 
		} catch (InterruptedException e) {
		    TraceEjb.debugSynchro(ident, "getICtx IT: INTERRUPTED"); 
		} catch (Exception e) {
		    TraceEjb.error(ident, "getICtx IT:", e);
		    throw new EJBException("EntityContext Synchronization problem", e);
		} finally {
		    waiters--;
		}

		// If transaction has been rolledback or set rollback only, give up.
		int status = Status.STATUS_ROLLEDBACK;
		try {
		    status = tx.getStatus();
		} catch (SystemException e) {
		    TraceEjb.error(ident+"getICtx IT: unexpected exception getting transaction status");
		}
		switch (status) {
		case Status.STATUS_MARKED_ROLLBACK:
		case Status.STATUS_ROLLEDBACK:
		case Status.STATUS_ROLLING_BACK:
		    TraceEjb.verbose(ident+"getICtx IT: transaction rolled back");
		    throw new TransactionRolledbackLocalException("rollback occured while waiting");
		}
	    }

	    bctx = Icontext;
		
	    if (bctx == null) {
		TraceEjb.debugIc(ident, "getICtx IT: get new context from the pool");
		// Get a new context from the pool
		bctx = (JEntityContext) bf.getJContext();
		bctx.initEntityContext(this, tx);
		// Activate this instance
		bctx.activate(true);
		Icontext = bctx;
	    } else {
		bctx.reuseEntityContext(tx);
		if (mustLoad) {
		    // reload the bean state, because for example bean is shared.
		    TraceEjb.debugIc(ident, "getICtx IT: reload bean state");
		    bctx.activate(false);
		    mustLoad = false;
		}
	    }

	    // Register this Context to release instance at commit
	    if (currtx == null) {
		TraceEjb.debugIc(ident, "getICtx IT: new tx, registerSynchronization");
		currtx = tx;
		try {
		    tx.registerSynchronization(bctx);
		} catch (RollbackException e) {
		    TraceEjb.verbose(ident, "getICtx IT: transaction has been marked rollbackOnly");
		} catch (SystemException e) {
		    TraceEjb.error(ident, "getICtx IT: ", e);
		    currtx = null;
		    throw new IllegalStateException("cannot register synchro");
		} catch (IllegalStateException e) {
		    TraceEjb.error(ident, "getICtx IT: Transaction is in an illegal state");
		    currtx = null; // not registered -> no tx (avoids deadlocks)
		    throw e;
		}
	    }
	}
	return bctx;
    }

    /**
     * Release a context/instance at end of request.
     * @param tx - transaction associated to this context
     */
    public synchronized void releaseICtx(Transaction tx) {

	JEntityContext bctx = null;

	if (tx == null) {
	    // ------------- IH end ------------- 
	    TraceEjb.debugIc(ident, "releaseICtx IH");

	    bctx = Icontext;
	    IHcount--;
	    if (IHcount == 0) {
		if (bctx.isMarkedRemoved()) {
		    noLongerUsed();
		} else if (mustStore) {
		    try {
			bctx.storeIfModified();
		    } catch (EJBException e) {
			TraceEjb.verbose(ident, "releaseICtx:"+e);
		    }
		    mustStore = false;
		    dirty = false;
		}
		if (waiters > 0) {
		    TraceEjb.debugSynchro(ident, "releaseICtx IH: notify");
		    notifyAll();
		}
	    }
	} else {
	    // ------------- IT end ------------- 
	    // nothing to do now. Wait for tx completed.
	    TraceEjb.debugIc(ident, "releaseICtx IT");

	}
    }

    /**
     * Discard a context/instance at end of request. A problem occured
     * on this insance and it must be discarded.
     * @param tx - transaction associated to this context
     */
    public synchronized void discardICtx(Transaction tx) {
	if (tx == null) {
	    TraceEjb.debugIc(ident, "discardICtx IH");
	    JEntityContext bctx = Icontext;
	    IHcount--;
	    if (IHcount == 0) {
		bf.releaseJContext(bctx);
		Icontext = null;
		IHdiscarded = false;
		dirty = false;
	    } else {
		IHdiscarded = true;
	    }
	} else {
	    // nothing to do now. Wait for tx completed.
	    TraceEjb.debugIc(ident, "discardICtx IT");
	}
    }

    /**
     * This transaction is now over. We can dispose of the instance
     * for another transaction or discard it.
     * @params tx the transaction object
     * @params committed true if transaction was committed.
     */
    public synchronized void txCompleted(Transaction tx, boolean committed) {
	JEntityContext bctx = Icontext;
	currtx = null;

	if (waiters > 0) {
	    if (shared || !committed) {
		mustLoad = true;
	    }
	    TraceEjb.debugSynchro(ident, "txCompleted: notify");
	    notifyAll();
	} else {
	    if (bctx.isMarkedRemoved()) {
		noLongerUsed();
	    }
	    // If bean is shared, we implement commit option C.
	    // If bean is not shared, commit option A is better.
	    if (shared || !committed) {
		if (!bctx.isMarkedRemoved()) {
		    bctx.passivate();
		}
		bf.releaseJContext(bctx);
		Icontext = null;
		IHdiscarded = false;
		dirty = false;
	    }
	}
    }

    /**
     * try to passivate IH (called from swapper)
     * args passivation false if only store bean state.
     * @return true if OK, false if busy.
     */
    public synchronized boolean passivateIH(boolean passivation) {
	
	JEntityContext bctx = Icontext;

	// If no context mapped, nothing to do
	if (bctx == null) {
	    return true;
	}
	// If the instance is busy, don't touch it.
	if (IHcount > 0 || currtx != null) {
	    TraceEjb.debugIc(ident, "passivateIH: busy");
	    TraceEjb.debugIc(ident, "IHcount="+IHcount+" currtx="+currtx+" waiters="+waiters);
	    return false;
	}
	TraceEjb.debugIc(ident, "passivateIH");
	// Store the instance state
	// -> only if instance not discarded
	if (! IHdiscarded) {
	    try {
		bctx.storeIfModified();
	    } catch (Exception e) {
		TraceEjb.error(ident, "error while storing bean state:", e);
	    }
	    mustStore = false;
	    dirty = false;
	}

	// passivate this instance if required.
	if (passivation) {
	    bctx.passivate();
	    bf.releaseJContext(bctx);
	    Icontext = null;
	    IHdiscarded = false;
	    dirty = false;
	}
	if (waiters > 0) {
	    TraceEjb.debugSynchro(ident, "passivateIH: notify");
	    notifyAll();
	}
	return true;
    }

    /**
     * Remove this EJB and
     * UnExport the remote object.
     */
    public void noLongerUsed() {
	TraceEjb.debugIc(ident, "JEntitySwitch noLongerUsed");
	if (remote != null) {
	    try {
		remote.unexportObject();
	    } catch (NoSuchObjectException e) {
		TraceEjb.error(ident, "Unexport Entity failed: "+e);
	    }
	}
	bf.removeEJB(getPrimaryKey());
    }
}
