/*
 * 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.jtests.beans.bank;

import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Iterator;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.RemoveException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.ejb.SessionSynchronization;
import javax.ejb.TransactionRolledbackLocalException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;

import org.ow2.jonas.lib.util.Log;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * Manager Implementation
 * @author Philippe Durieux
 */
public class ManagerSF implements SessionBean, SessionSynchronization {

    protected static Logger history = null;
    SessionContext ejbContext;
    AccountLocalHome accountLocalHome = null;
    AccountLocal last = null;
    int initialValue;

    // ------------------------------------------------------------------
    // SessionBean implementation
    // ------------------------------------------------------------------

    /**
     * Set the associated session context. The container calls this method
     * after the instance creation.
     * The enterprise Bean instance should store the reference to the context
     * object in an instance variable.
     * This method is called with no transaction context.
     *
     * @param ctx A SessionContext interface for the instance.
     * @throws EJBException Thrown by the method to indicate a failure caused by
     * a system-level error.
     */
    public void setSessionContext(SessionContext ctx) {
        if (history == null) {
            history = Log.getLogger("org.objectweb.jonas_tests.history");
        }
        history.log(BasicLevel.DEBUG, "");
        ejbContext = ctx;
    }

    /**
     * A container invokes this method before it ends the life of the session object.
     * This happens as a result of a client's invoking a remove operation, or when a
     *  container decides to terminate the session object after a timeout.
     * This method is called with no transaction context.
     *
     * @throws EJBException Thrown by the method to indicate a failure caused by
     * a system-level error.
     */
    public void ejbRemove() {
        history.log(BasicLevel.DEBUG, "");
    }

    /**
     * Create a session.
     * @param ival initial balance value for new accounts.
     * @throws CreateException Failure to create a session EJB object.
     */
    public void ejbCreate(int ival) throws CreateException {
        history.log(BasicLevel.DEBUG, "");

        // lookup AccountLocalHome
        try {
            Context ictx = new InitialContext();
            accountLocalHome = (AccountLocalHome) ictx.lookup("java:comp/env/ejb/bank");
        } catch (NamingException e) {
            history.log(BasicLevel.ERROR, "Cannot get AccountLocalHome:" + e);
            throw new CreateException("Cannot get AccountLocalHome");
        }

        initialValue = ival;
    }

    /**
     * Create a session.
     * @param ival initial balance value for new accounts.
     * @throws CreateException Failure to create a session EJB object.
     */
    public void ejbCreate(int ival, boolean prefetch) throws CreateException {
        history.log(BasicLevel.DEBUG, "");

        // lookup AccountLocalHome
        try {
            Context ictx = new InitialContext();
            String ejblink = prefetch ? "java:comp/env/ejb/bankpf" : "java:comp/env/ejb/bank";
            accountLocalHome = (AccountLocalHome) ictx.lookup(ejblink);
        } catch (NamingException e) {
            history.log(BasicLevel.ERROR, "Cannot get AccountLocalHome:" + e);
            throw new CreateException("Cannot get AccountLocalHome");
        }

        initialValue = ival;
    }

    /**
     * A container invokes this method on an instance before the instance
     * becomes disassociated with a specific EJB object.
     */
    public void ejbPassivate() {
        history.log(BasicLevel.DEBUG, "");
    }

    /**
     * A container invokes this method when the instance is taken out of
     * the pool of available instances to become associated with a specific
     * EJB object.
     */
    public void ejbActivate() {
        history.log(BasicLevel.DEBUG, "");
    }

    // ------------------------------------------------------------------
    // SessionSynchronization implementation
    // ------------------------------------------------------------------

    public void afterBegin() {
        history.log(BasicLevel.DEBUG, "");
    }

    public void beforeCompletion() {
        history.log(BasicLevel.DEBUG, "");
    }

    public void afterCompletion(boolean committed) {
        if (committed) {
            history.log(BasicLevel.DEBUG, "TX committed");
        } else {
            history.log(BasicLevel.DEBUG, "TX rolled back");
        }
    }

    // ------------------------------------------------------------------
    // Manager implementation
    // ------------------------------------------------------------------

    /**
     * create a set of Accounts
     * @param nb nb of Accounts created.
     */
    public void createAll(int nb) throws RemoteException, CreateException {
        // Check if accounts are already created.
        history.log(BasicLevel.DEBUG, " nb= "+nb);
        try {
            accountLocalHome.findByNum(nb - 1);
        } catch (Exception e) {
            // create accounts
            for (int i = 0; i < nb; i++) {
                newAccount(i);
            }
        }
    }

    /**
     * reinit all created accounts to their initial value.
     */
    public void reinitAll() throws RemoteException {
        try {
            Collection coll = accountLocalHome.findAll();
            for (Iterator it = coll.iterator(); it.hasNext();) {
                AccountLocal a = (AccountLocal) it.next();
                a.setBalance(initialValue);
            }
        } catch (Exception e) {
            history.log(BasicLevel.ERROR, "reinitAll:" + e);
        }
    }


    /**
     * Remove an Account
     * @param d1 num of the Account.
     */
    public void delAccount(int d1) throws RemoteException, RemoveException {
        try {
            AccountLocal deb1 = accountLocalHome.findByNum(d1);
            deb1.remove();
            history.log(BasicLevel.DEBUG, d1 + "\tREMOVED");
        } catch (FinderException e) {
            history.log(BasicLevel.INFO, d1 + "\tNot Found for remove");
        }
    }

    /**
     * Check all existing Accounts
     * @return true if all are OK.
     */
    public boolean checkAll() throws RemoteException {
        int count = 0;
        int total = 0;
        boolean ret = true;
        try {
            Collection coll = accountLocalHome.findAll();
            for (Iterator it = coll.iterator(); it.hasNext();) {
                count++;
                AccountLocal a = (AccountLocal) it.next();
                int balance = a.getBalance();
                String name = a.getName();
                if (balance < 0) {
                    history.log(BasicLevel.ERROR, name + " bad balance: " + balance);
                    ret = false;
                } else {
                    history.log(BasicLevel.DEBUG, name + " : FINAL BALANCE=" + balance);
                    total += balance;
                }
            }
        } catch (Exception e) {
            history.log(BasicLevel.ERROR, "checkAllAccounts:" + e);
            return false;
        }
        int exp = initialValue * count;
        if (total != exp) {
            history.log(BasicLevel.ERROR, "checkAllAccounts: bad total: " + total + " (expected: " + exp + ")");
            return false;
        }
        history.log(BasicLevel.DEBUG, "CheckAll OK");
        return ret;
    }

    /**
     * Check an existing Account
     * @param a num of the Account.
     * @return true if OK.
     */
    public boolean checkAccount(int a) throws RemoteException {
        boolean ret = false;
        AccountLocal m = null;

        // retry several times, because this operation may be rolledback
        // in case of deadlock.
        Exception exc = null;
        int retry;
        for (retry = 0; retry < 20; retry++) {
            try {
                history.log(BasicLevel.DEBUG, "\ta_" + a + "\tCHECKED try #" + retry);
                m = accountLocalHome.findByNum(a);
                int b = m.getBalance();
                if (b >= 0) {
                    ret = true;
                } else {
                    history.log(BasicLevel.WARN, "bad balance=" + b);
                }
                return ret;
            } catch (javax.ejb.ObjectNotFoundException e1) {
                try {
                    newAccount(a);
                    ret = true;
                    break;
                }catch (Exception ex) {
                    ret= false;
                    break;
                }
            } catch (Exception e) {
                exc = e;
                history.log(BasicLevel.DEBUG, "retrying " + retry);
                sleep(retry + 1);
            }
        }
        history.log(BasicLevel.WARN, "cannot check account: " + exc);
        return ret;
    }

    /*
     * read balance for this Account, in a transaction.
     * @param a num of the Account.
     * @return balance
     */
    public int readBalanceTx(int a) throws RemoteException {
        //checkAccount(a);
        return readBalance(a);
    }

    /**
     * read balance for this Account
     * @param a num of the Account.
     * @return balance
     */
    public int readBalance(int a) throws RemoteException {
        int ret;
        try {
            AccountLocal acc = getAccount(a);
            if (acc == null) {
                history.log(BasicLevel.ERROR, "Cannot get account");
                throw new RemoteException("Cannot get account " + a);
            }
            ret = acc.getBalance();
        } catch (Exception e) {
            history.log(BasicLevel.ERROR, "Cannot read balance for " + a + ": " + e);
            throw new RemoteException("Cannot read balance for " + a);
        }
        history.log(BasicLevel.DEBUG, "READ " + a + " = " + ret);
        return ret;
    }

    /**
     * move form an Account to another one.
     * @param d num of the debit Account.
     * @param c num of the credit Account.
     * @param v value to be moved
     * @param d delay in second for the operation.
     */
    public void move(int d, int c, int v, int delay) throws RemoteException {
        history.log(BasicLevel.DEBUG, "MOVE " + v + " from " + d + " to " + c);
        try {
            AccountLocal cred = getAccount(c);
            AccountLocal deb = getAccount(d);
            cred.credit(v);
            sleep(delay);
            deb.debit(v);
        } catch (TransactionRolledbackLocalException e) {
            history.log(BasicLevel.WARN, "move: Rollback transaction");
            return;
        } catch (EJBException e) {
            history.log(BasicLevel.ERROR, "Cannot move:" + e);
            return;
        }
    }

    /**
     * Read balance on last accessed account
     */
    public int readBalance() throws RemoteException {
        int ret;
        try {
            ret = last.getBalance();
        } catch (NoSuchObjectLocalException e) {
            throw new NoSuchObjectException("Account destroyed");
        } catch (Exception e) {
            throw new RemoteException("Cannot read last balance");
        }
        return ret;
    }

    /**
     * Create an Account, but set rollback only the transaction
     * @return
     * @throws RemoteException
     */
    public void createRollbackOnly(int i) throws RemoteException {
        try {
            last = newAccount(i);
        } catch (CreateException c) {
            throw new RemoteException("Cannot create account");
        }
        ejbContext.setRollbackOnly();
    }

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

    /**
     * Create a new Account. The account may exist (for example, when running
     * tests twice without restarting the Container, or if created by another
     * session meanwhile)
     * @param i account number (its PK)
     */
    private AccountLocal newAccount(int i) throws RemoteException, CreateException {
        AccountLocal ml = null;
        ml = accountLocalHome.create(i, initialValue);
        history.log(BasicLevel.DEBUG, "New Account has been created\t" + i);
        return ml;
    }

    /**
     * Create an Account if it does not exist yet.
     * @param c1 num of the Account.
     */
    private AccountLocal getAccount(int c1) throws RemoteException {
        history.log(BasicLevel.DEBUG, "Get Account\t" + c1);
        try {
            last = accountLocalHome.findByNum(c1);
        } catch (FinderException e) {
            try {
                last = newAccount(c1);
            } catch (CreateException c) {
                throw new RemoteException("Cannot create account");
            }
        }
        return last;
    }

    /**
     * sleep n seconds
     * @param n seconds
     */
    private void sleep(int n) {
        try {
            Thread.sleep(1000 * n);
        } catch (InterruptedException e) {
        }
    }

}
