/*
 * 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
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 javax.sql.DataSource;

import org.objectweb.jonas.common.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;
    // CMP2 bean
    ProductLocalHome productLocalHome = null;
    ProductLocal prod0 = null;
    // CMP1 bean
    ProductEC1Home home1 = null;
    ProductLocal prod1 = null;

    Connection savedconn = null;

    // ------------------------------------------------------------------
    // 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, "");
        ejbCreate(ival, false);
    }

    /**
     * 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, "");

        Context ictx = null;

        // lookup AccountLocalHome
        try {
            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");
        }

        // lookup ProductLocalHome for CMP2
        try {
            String productlink = "java:comp/env/ejb/product";
            productLocalHome = (ProductLocalHome) ictx.lookup(productlink);
        } catch (NamingException e) {
            history.log(BasicLevel.ERROR, "Cannot get ProductLocalHome:" + e);
            throw new CreateException("Cannot get ProductLocalHome");
        }

        // lookup local ProductEC1Home (CMP1)
        try {
            home1 = (ProductEC1Home) PortableRemoteObject.narrow(ictx.lookup("ProductEC1Home_L"),
                                                                 ProductEC1Home.class);
        } catch (NamingException e) {
            history.log(BasicLevel.ERROR, "Cannot get ProductEC1Home:" + e);
            throw new CreateException("Cannot get ProductEC1Home");
        }
        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, "");
        for (int i = 0; i < nb; i++) {
            try {
                accountLocalHome.findByNum(i);
            } catch (Exception e) {
                // create accounts
                newAccount(i);
            }
        }
        // create 1 product
        String pk = "prod0";
        try {
            prod0 = productLocalHome.findByPrimaryKey(pk);
        } catch (Exception e) {
            prod0 = productLocalHome.create(pk);
            prod0.setNum(0);
        }
        // create 1 product CMP1
        pk = "prod1";
        try {
            prod1 = home1.findByPrimaryKey(pk);
        } catch (Exception e) {
            prod1 = home1.create(pk);
            prod1.setNum(1);
        }
    }

    /**
     * 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 (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 {
        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;
    }

    /**
     * read balance for this Account
     * @param a num of the Account.
     * @param value new balance
     */
    public void setBalance(int a, int value) 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);
            }
            acc.setBalance(value);
        } catch (Exception e) {
            history.log(BasicLevel.ERROR, "Cannot set balance for " + a + ": " + e);
            throw new RemoteException("Cannot set balance for " + a);
        }
        history.log(BasicLevel.DEBUG, "SET " + a + " = " + value);
    }

    /**
     * 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;
    }

    public int readProdNum() throws RemoteException {
        int ret;
        try {
            ret = prod0.getNum();
        } catch (NoSuchObjectLocalException e) {
            throw new NoSuchObjectException("Prod0 destroyed");
        } catch (Exception e) {
            throw new RemoteException("Cannot read Product");
        }
        return ret;
    }

    public int readCMP1Num() throws RemoteException {
        int ret;
        try {
            ret = prod1.getNum();
        } catch (NoSuchObjectLocalException e) {
            throw new NoSuchObjectException("Prod1 destroyed");
        } catch (Exception e) {
            throw new RemoteException("Cannot read ProductCMP1");
        }
        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();
    }
    
    /*
     * This operation is transaction "Required"
     * @param c1 num of the first Account.
     * @param c2 num of the second Account.
     * @param newval value to be set in the RequiresNew account
     */
    public void mixOpe(int c1, int c2, int newval, boolean commit) throws RemoteException {
        AccountLocal reqnew = null;
        AccountLocal setrbo = null;
        try {
            reqnew = getAccount(c1);
            setrbo = getAccount(c2);
        } catch (EJBException e) {
            history.log(BasicLevel.ERROR, "Cannot get Account:" + e);
            throw new RemoteException("Cannot get account");
        }

        // Access another bean inside this tx
        // this should be eventually rolled back !
        prod0.setNum(100);

        // Calls Ejb with RequiresNew method
        reqnew.setBalanceRN(newval);

        // Call EJB supports method, that commit/rollback transaction.
        if (commit) {
            setrbo.credit(300000);
        } else {
            setrbo.debit(300000);
        }
    }

    /*
     * This operation is transaction "Required"
     * @param c1 num of the first Account.
     * @param c2 num of the second Account.
     * @param newval value to be set in the RequiresNew account
     */
    public void mixOpeCmp1(int c1, int c2, int newval, boolean commit) throws RemoteException {
        AccountLocal reqnew = null;
        AccountLocal setrbo = null;
        try {
            reqnew = getAccount(c1);
            setrbo = getAccount(c2);
        } catch (EJBException e) {
            history.log(BasicLevel.ERROR, "Cannot get Account:" + e);
            throw new RemoteException("Cannot get account");
        }

        // Access another bean inside this tx
        // this should be eventually rolled back !
        prod1.setNum(100);

        // Calls Ejb with RequiresNew method
        reqnew.setBalanceRN(newval);

        // Call EJB supports method, that commit/rollback transaction.
        if (commit) {
            setrbo.credit(300000);
        } else {
            setrbo.debit(300000);
        }
    }

    public void openLongConnection() throws RemoteException {
        if (savedconn == null) {
            savedconn = openConnection();
        }
    }

    public void closeLongConnection() throws RemoteException {
        if (savedconn != null) {
            try {
                savedconn.close();
                savedconn = null;
            } catch (Exception e) {
                throw new RemoteException("Cannot close saved Connection");
            }
        }
    }

    private Connection openConnection() throws RemoteException {
        Context ictx = null;
        DataSource jdbc2 = null;
        // lookup jdbc_2 DataSource
        try {
            ictx = new InitialContext();
            jdbc2 = (DataSource) ictx.lookup("jdbc_2"); 
        } catch (NamingException e) {
            history.log(BasicLevel.ERROR, "Cannot lookup jdbc_2:" + e);
            throw new RemoteException("Cannot lookup jdbc_2");
        }
        
        // Get a Connection on jdbc_2
        Connection conn = null;
        try {
            conn = jdbc2.getConnection(); 
        } catch (Exception e) {
            history.log(BasicLevel.ERROR, "Cannot get Connection on jdbc_2:" + e);
            throw new RemoteException("Cannot get Connection on jdbc_2");
        }
        return conn;
    }

    /*
     * This operation is transaction "Required"
     * @param c1 num of the first Account.
     * @param c2 num of the second Account.
     * @param newval value to be set in the RequiresNew account
     */
    public void mixOpeJdbc(int c1, int c2, int newval, boolean commit) throws RemoteException {
        AccountLocal reqnew = null;
        AccountLocal setrbo = null;
        try {
            reqnew = getAccount(c1);
            setrbo = getAccount(c2);
        } catch (EJBException e) {
            history.log(BasicLevel.ERROR, "Cannot get Account:" + e);
            throw new RemoteException("Cannot get account");
        }

        // Make jdbc request on another ds (jdbc_2)
        Connection conn = null;
        if (savedconn != null) {
            conn = savedconn;
        } else {
            conn = openConnection();
        }
        PreparedStatement stmt = null;
        ResultSet rs = null;

        // Change value. 
        // this should be automatically rolled back at the end of the method.
        try {
            stmt = conn.prepareStatement("update ebankProductEC1 set c_num=? where c_name=?");
            stmt.setInt(1, 100);
            stmt.setString(2, "prod1");
            stmt.executeUpdate();
        } catch (Exception e) {
            throw new RemoteException("Failed to modify bean prod1:" + e);
        } finally {
            // close statement and connection
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null && savedconn == null) {
                    conn.close();
                }
            } catch (Exception ignore) {
            }
        }


        // Calls Ejb with RequiresNew method
        reqnew.setBalanceRN(newval);

        // Call EJB supports method, that commit/rollback transaction.
        if (commit) {
            setrbo.credit(300000);
        } else {
            setrbo.debit(300000);
        }
    }

    // ------------------------------------------------------------------
    // 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;
    }
    public boolean findAccountTx(int c1) throws RemoteException {
        return findAccount(c1);
    }

    /**
     * Find an account by its pk. Don't create if not found.
     * @param c1 account number
     * @return boolean true if found
     */
    public boolean findAccount(int c1) throws RemoteException {
        history.log(BasicLevel.DEBUG, "ManagerSF.findAccount\t" + c1);
        // We want to use specifically findByPrimaryKey here.
        String name = "a_"+(new Integer(c1)).toString();
        AccountLocal cred1;
        try {
            cred1 = accountLocalHome.findByPrimaryKey(name);
        } catch (FinderException e) {
            history.log(BasicLevel.DEBUG, "ManagerSF.findAccount FinderException raised " + c1);
            cred1 = null;
        } catch (Exception e) {
            history.log(BasicLevel.WARN, "Error: " + e);
            throw new RemoteException("Bad exception raised: " + e);
        }
        boolean found = (cred1 != null);
        history.log(BasicLevel.DEBUG, "ManagerSF.findAccount\t" + c1 + " found=" + found);
        return found;
    }

    /**
     * sleep n seconds
     * @param n seconds
     */
    private void sleep(int n) {
        history.log(BasicLevel.DEBUG, "ManagerSF.sleep " + n);
        try {
            Thread.sleep(1000 * n);
        } catch (InterruptedException e) {
        }
    }

}
