/*
 * 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.stests.manac;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
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.sql.DataSource;

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

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

    protected static Logger logger = null;
    protected static Logger history = null;
    SessionContext ejbContext;
    ManacLocalHome manacLocalHome = null;
    int delay = 0;
    int c1 = 0;
    int c2 = 0;
    int d1 = 0;
    int d2 = 0;
    ManacLocal cred1 = null;
    ManacLocal cred2 = null;
    ManacLocal deb1 = null;
    ManacLocal deb2 = null;
    int initialValue = 1000;
    int value = 10;

    private void log(String message) {
        history.log(BasicLevel.INFO, "a_" + d1 + "\ta_" + d2 + "\ta_" + c1 + "\ta_" + c2 + "\tv_" + value + "\t" + message);
    }

    // ------------------------------------------------------------------
    // init DataBase for Manac beans
    // ------------------------------------------------------------------
    private void initDB() {
        logger.log(BasicLevel.DEBUG, "");

        // Get my DataSource from JNDI
        DataSource ds = null;
        InitialContext ictx = null;
        String myTable = null;
        try {
            ictx = new InitialContext();
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "new InitialContext() : " + e);
            throw new EJBException("Cannot get JNDI InitialContext");
        }
        try {
            ds = (DataSource) ictx.lookup("java:comp/env/jdbc/mydb");
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "cannot lookup datasource " + e);
            throw new EJBException("cannot lookup datasource");
        }
        try {
            // Create table if specified in dd (in fact, only if CMP1.x)
            myTable = (String) ictx.lookup("java:comp/env/tablename");
            createTable(ds, myTable);
        } catch (Exception e) {
            logger.log(BasicLevel.WARN, "No init necessary in CMP2");
        }
    }

    private void createTable(DataSource ds, String myTable) {
        // Drop table
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = ds.getConnection();
            stmt = conn.createStatement();
            stmt.execute("drop table " + myTable);
            stmt.close();
        } catch (SQLException e) {
            // The first time, table will not exist.
            logger.log(BasicLevel.INFO, "Exception in dropTable : " + e);
        }

        // Create table.
        try {
            stmt = conn.createStatement();
            stmt.execute("create table " + myTable + "(name varchar(30) not null primary key, num integer, balance integer)");
            stmt.close();
            conn.close();
        } catch (SQLException e) {
            logger.log(BasicLevel.ERROR, "Exception in createTable : " + e);
            throw new EJBException("Exception in createTable");
        }
    }

    // ------------------------------------------------------------------
    // 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 (logger == null) {
            logger = Log.getLogger(Log.JONAS_TESTS_PREFIX);
        }
        if (history == null) {
            history = Log.getLogger("org.objectweb.jonas_tests.history");
        }
        logger.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() {
        logger.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 {
        logger.log(BasicLevel.DEBUG, "");

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

        initialValue = ival;
    }

    /**
     * A container invokes this method on an instance before the instance
     * becomes disassociated with a specific EJB object.
     */
    public void ejbPassivate() {
        logger.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() {
        logger.log(BasicLevel.DEBUG, "");
    }

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

    public void afterBegin() {
        log("TX after_begin");
    }

    public void beforeCompletion() {
        log("TX before_completion");
    }

    public void afterCompletion(boolean committed) {
        if (committed) {
            log("TX committed");
        } else {
            log("TX rolled back");
        }
    }

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

    /**
     * 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 ManacLocal newAccount(int i) throws RemoteException {
        ManacLocal ml = null;
        try {
            ml = manacLocalHome.create(i, initialValue);
            logger.log(BasicLevel.INFO, "New Account created : " + i);
            history.log(BasicLevel.INFO, "New Account\t" + i);
        } catch (CreateException e) {
            // Sometimes another one has just created it
            try {
                ml = manacLocalHome.findByNum(i);
                logger.log(BasicLevel.INFO, "Account was created already : " + i);
            } catch (FinderException e2) {
                logger.log(BasicLevel.ERROR, "newAccount:" + e);
                history.log(BasicLevel.INFO, "CANNOT CREATE ACCOUNT\t" + i);
                throw new RemoteException("Cannot create Manac", e);
            }
        }
        return ml;
    }

    /**
     * Initializes the database by creating a set of accounts.
     * @param nb number of account created.
     */
    public void createAll(int nb) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");

        // init database for Manac bean
        initDB();

        // Check if accounts are already created.
        try {
            manacLocalHome.findByNum(nb - 1);
        } catch (Exception e) {
            // create accounts
            for (int i = 0; i < nb; i++) {
                newAccount(i);
            }
        }
    }

    /**
     * Reinit the balances for each created account to its initial value.
     */
    public void reinitAll() throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");
        try {
            Enumeration enumer = manacLocalHome.findAll();
            while (enumer.hasMoreElements()) {
                ManacLocal a = (ManacLocal) enumer.nextElement();
                a.setBalance(initialValue);
            }
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "reinitAll:" + e);
        }
    }

    /**
     * Set a delay value, in seconds, for the transaction.
     */
    public void setDelay(int d) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "delay (sec) = " + d);
        delay = d;
    }

    /**
     * Each operation debits 2 accounts and credits 2 accounts.
     * Lookup all accounts and create them if they not exist yet
     * @param d1 first account to credit
     * @param d2 second account to credit
     * @param c1 first account to debit
     * @param c2 second account to debit
     * @throws RemoteException
     */
    public void setAccounts(int d1, int d2, int c1, int c2) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "d1=" + d1 + " d2=" + d2 + " c1=" + c1 + " c2=" + c2);
        log("SA beg");

        // debit account d1
        this.d1 = d1;
        try {
            deb1 = manacLocalHome.findByNum(d1);
        } catch (FinderException e) {
            deb1 = newAccount(d1);
        }

        // debit account d2
        this.d2 = d2;
        try {
            deb2 = manacLocalHome.findByNum(d2);
        } catch (FinderException e) {
            deb2 = newAccount(d2);
        }

        // credit account c1
        this.c1 = c1;
        try {
            cred1 = manacLocalHome.findByNum(c1);
        } catch (FinderException e) {
            cred1 = newAccount(c1);
        }

        // credit account c2
        this.c2 = c2;
        try {
            cred2 = manacLocalHome.findByNum(c2);
        } catch (FinderException e) {
            cred2 = newAccount(c2);
        }
        log("SA end");
    }

    /**
     * Find an account and create it if not found.
     * @param c1 account number
     */
    public void getAccount(int c1) throws RemoteException {
        logger.log(BasicLevel.DEBUG, " c1=" + c1);
        this.c1 = c1;
        try {
            cred1 = manacLocalHome.findByNum(c1);
        } catch (FinderException e) {
            cred1 = newAccount(c1);
        }
    }

    /**
     * Find an account and create it if not found.
     * Then, remove it.
     * @param d1 account number
     */
    public void delAccount(int d1) throws RemoteException, RemoveException {
        logger.log(BasicLevel.DEBUG, " d1=" + d1);
        this.d1 = d1;
        try {
            deb1 = manacLocalHome.findByNum(d1);
        } catch (FinderException e) {
            deb1 = newAccount(d1);
        }
        deb1.remove();
        history.log(BasicLevel.INFO, d1 + "\tREMOVED");
    }

    /**
     * Set the value to withdraw.
     * @param v value to withdraw.
     */
    public void setValue(int v) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");
        this.value = v;
    }

    /**
     * Do the movement.
     */
    public void movement() throws RemoteException {
        logger.log(BasicLevel.DEBUG, "move " + value + " from " + d1 + "," + d2 + " to " + c1 + "," + c2);
        log("MV start");

        // credit accounts first because we don't want a rollback if
        // same account is debited and credited in the same operation.
        try {
            cred1.credit(value);
            cred2.credit(value);
        } catch (TransactionRolledbackLocalException e) {
            logger.log(BasicLevel.WARN, "Credit -> Rollback transaction");
            log("MV rbc1");
            return;
        } catch (EJBException e) {
            logger.log(BasicLevel.ERROR, "Cannot credit account:" + e);
            log("MV rbc2");
            return;
        }

        // wait a little if delay
        if (delay > 0) {
            sleep(delay);
        }

        // debit accounts
        try {
            deb1.debit(value);
            deb2.debit(value);
        } catch (TransactionRolledbackLocalException e) {
            logger.log(BasicLevel.WARN, "Debit -> Rollback transaction");
            log("MV rbd1");
            return;
        } catch (EJBException e) {
            logger.log(BasicLevel.ERROR, "debit:" + e);
            log("MV rbd2");
            return;
        }

        log("MV commit");
    }

    /**
     * Simple withdraw operation, on existing accounts only.
     */
    public void withdraw(int wd, int wc, int wv) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "move " + wv + " from " + wd + " to " + wc);
        d1 = wd;
        c1 = wc;
        log("W move start: ");

        // credit accounts first because we don't want a rollback if
        // same account is debited and credited in the same operation.
        // credit account wc
        try {
            manacLocalHome.findByNum(wc).credit(wv);
        } catch (FinderException e) {
            logger.log(BasicLevel.WARN, "Credit -> Unknown account");
            log("W cred0");
            return;
        } catch (TransactionRolledbackLocalException e) {
            logger.log(BasicLevel.WARN, "Credit -> Rollback transaction");
            log("W cred1");
            return;
        } catch (EJBException e) {
            logger.log(BasicLevel.ERROR, "Cannot credit account:" + e);
            log("W cred2");
            return;
        }
        // debit account
        try {
            manacLocalHome.findByNum(wd).debit(wv);
        } catch (FinderException e) {
            logger.log(BasicLevel.WARN, "Debit -> Unknown account");
            log("W ded0");
            return;
        } catch (TransactionRolledbackLocalException e) {
            logger.log(BasicLevel.WARN, "Debit -> Rollback transaction");
            log("W deb1");
            return;
        } catch (EJBException e) {
            logger.log(BasicLevel.ERROR, "Cannot debit account:" + e);
            log("W deb2");
            return;
        }
        log("W move end: ");
    }

    public void sleep(int seconds) {
        try {
            Thread.sleep(1000 * seconds);
        } catch (InterruptedException e) {
        }
    }

    /**
     * This is a read operation, executed outside transaction.
     */
    public int readBalances() throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");
        int ret = 1000000;
        int bal;
        try {
            bal = deb1.getBalance();
            if (bal < ret) {
                ret = bal;
            }
            bal = deb2.getBalance();
            if (bal < ret) {
                ret = bal;
            }
            bal = cred1.getBalance();
            if (bal < ret) {
                ret = bal;
            }
            bal = cred2.getBalance();
            if (bal < ret) {
                ret = bal;
            }
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "cannot read account" + e);
            throw new RemoteException("cannot read account" + e);
        }
        return ret;
    }

    public boolean checkAccount(int a) throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");

        boolean ret = false;
        ManacLocal 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.INFO, "\ta_" + a + "\tCHECKED try #" + retry);
                m = manacLocalHome.findByNum(a);
                int b = m.getBalance();
                if (b >= 0) {
                    ret = true;
                } else {
                    logger.log(BasicLevel.WARN, "bad balance=" + b);
                }
                return ret;
            } catch (Exception e) {
                exc = e;
                logger.log(BasicLevel.INFO, "retrying " + retry);
                sleep(retry + 1);
            }
        }
        logger.log(BasicLevel.WARN, "cannot check account: " + exc);
        return ret;
    }

    public boolean checkAll() throws RemoteException {
        logger.log(BasicLevel.DEBUG, "");

        int count = 0;
        int total = 0;
        try {
            Enumeration enumer = manacLocalHome.findAll();
            while (enumer.hasMoreElements()) {
                count++;
                ManacLocal a = (ManacLocal) enumer.nextElement();
                int balance = a.getBalance();
                if (balance < 0) {
                    logger.log(BasicLevel.ERROR, "checkAllAccounts: bad balance: " + balance);
                    return false;
                }
                String name = a.getName();
                logger.log(BasicLevel.DEBUG, name + " : FINAL BALANCE=" + balance);
                history.log(BasicLevel.INFO, name + "\t" + balance);
                total += balance;
            }
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "checkAllAccounts:" + e);
            return false;
        }
        int exp = initialValue * count;
        if (total != exp) {
            logger.log(BasicLevel.ERROR, "checkAllAccounts: bad total: " + total + " (expected: " + exp + ")");
            history.log(BasicLevel.INFO, "CheckAll ERROR: " + total + "\t" + exp);
            return false;
        }
        logger.log(BasicLevel.DEBUG, "total=" + total);
        history.log(BasicLevel.INFO, "CheckAll OK");
        return true;
    }

}
