/**
 * 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_ejb.container;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.TimerService;
import javax.transaction.Transaction;

import org.objectweb.jonas.jndi.checker.IResourceChecker;
import org.objectweb.jonas.jndi.checker.ResourceCheckpoints;
import org.objectweb.jonas_ejb.deployment.api.SessionDesc;
import org.objectweb.jonas_ejb.deployment.api.SessionStatefulDesc;
import org.objectweb.jonas_ejb.lib.EJBInvocation;
import org.objectweb.util.monolog.api.BasicLevel;

/**
 * This class is a factory for a Session Stateful Bean.
 * @author Philippe Durieux
 */
public class JStatefulFactory extends JSessionFactory {

    /**
     * List of JStatefulSwitch objects (pool of session instances)
     * Key is the sessionId
     */
    protected HashMap statefulList = new HashMap();

    /**
     * Current Cache Size
     */
    protected int cacheSize = 0;

    /**
     * Context id (increment counter)
     */
    private int sessionCount = 0;

    /**
     * constructor
     * @param dd Bean Deployment Descriptor
     * @param cont Container where the bean is defined
     */
    public JStatefulFactory(SessionStatefulDesc dd, JContainer cont) {
        super((SessionDesc) dd, cont);
        TraceEjb.interp.log(BasicLevel.DEBUG, "");
        isSynchro = javax.ejb.SessionSynchronization.class.isAssignableFrom(beanclass);
        isStateful = true;
        singleswitch = false;
    }

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

    /**
     * @return the Instance cache size for this Ejb
     */
    public int getCacheSize() {
        return sessionList.size();
    }

    /**
     * No pool for stateful session beans
     * @return 0
     */
    public int getPoolSize() {
        return 0;
    }

    /**
     * Reduce number of instances in memory
     */
    public void reduceCache() {
    }

    /**
     * No pool of instances for stateful session beans
     */
    public void initInstancePool() {
    }

    // ---------------------------------------------------------------
    // preInvoke / postInvoke
    // ---------------------------------------------------------------

    /**
     * preInvoke for Session beans stateful
     * @param txa Transaction Attribute (Supports, Required, ...)
     * @return A RequestCtx object
     * @throws EJBException
     */
    public RequestCtx preInvoke(int txa) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        RequestCtx rctx = super.preInvoke(txa);
        return rctx;
    }

    /**
     * Check if the access to the bean is authorized
     * @param ejbInv object containing security signature of the method, args of
     *        method, etc
     */
     public void checkSecurity(EJBInvocation ejbInv) {
         if (TraceEjb.isDebugIc()) {
             TraceEjb.interp.log(BasicLevel.DEBUG, "");
         }
         super.checkSecurity(ejbInv);
     }

     /**
     * postinvoke
     * @param rctx The RequestCtx that was returned at preInvoke()
     * @throws EJBException
     */
    public void postInvoke(RequestCtx rctx) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        super.postInvoke(rctx);
    }

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

    /**
     * Obtains the TimerService associated for this Bean
     * @return a JTimerService instance.
     */
    public TimerService getTimerService() {
        throw new EJBException("No TimerService for Stateful Session beans");
    }

    /**
     * Creates a new Session Stateful called back from createEJB
     * @return The Session Switch object
     */
    public JSessionSwitch createNewSession() throws RemoteException {
        JStatefulSwitch bs = new JStatefulSwitch(this);
        return bs;
    }

    public int getNewSessionId(JStatefulSwitch jss) {
        // Check if we must passivate some beans first
        while (maxCacheSize > 0 && cacheSize >= maxCacheSize) {
            // LRU algo
            long maxtime = Long.MAX_VALUE;
            JStatefulSwitch victim = null;
            synchronized (this) {
                for (Iterator i = statefulList.values().iterator(); i.hasNext(); ) {
                    JStatefulSwitch ss = (JStatefulSwitch) i.next();
                    long time = ss.getLastAccessTime();
                    if (time < maxtime) {
                        if (ss.canPassivate()) {
                            victim = ss;
                        }
                    }
                }
            }
            if (victim != null) {
                // Keeping the lock here may leads to deadlocks.
                TraceEjb.ssfpool.log(BasicLevel.DEBUG, "try to passivate a bean");
                if (victim.passivate()) {
                    cacheSize--;
               }
            } else {
                TraceEjb.ssfpool.log(BasicLevel.DEBUG, "Could not find enough beans to passivate");
                break;
            }
        }
        // Get a unique identifier
        synchronized(this) {
            int sid = sessionCount++;
            if (TraceEjb.isDebugSsfpool()) {
                TraceEjb.ssfpool.log(BasicLevel.DEBUG, "#" + sid);
            }
            // Put in in the statefulList
            statefulList.put(new Integer(sid), jss);
            cacheSize++;
            return sid;
        }
    }

    /**
     * Return the name of the stateful passivated file
     * @param sid session ident
     * @return name of the stateful passivated file
     * @throws IOException
     */
    private File getSsfFileName(int sid) throws IOException {
        return new File(passivationDir, String.valueOf(sid) + ".ssf");
    }

    private FileOutputStream getFileOutputStream(int sid) throws IOException {
        File file = getSsfFileName(sid);
        file.createNewFile();
        return new FileOutputStream(file);
    }

    private FileInputStream getFileInputStream(int sid) throws IOException {
        File file = getSsfFileName(sid);
        return new FileInputStream(file);
    }

    private void removePassivatedState(int sid) throws IOException {
        File file = getSsfFileName(sid);
        file.delete();
    }

    public boolean passivateStateful(JStatefulSwitch jss) {
        int sid = jss.getSessionId();
        if (TraceEjb.isDebugSsfpool()) {
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "#" + sid);
        }
        JStatefulContext ctx = jss.getStatefulContext();

        // Check stateful before passivating
        EJBResourceCheckerInfo checkerInfo = new EJBResourceCheckerInfo(this, ResourceCheckpoints.EJB_PRE_PASSIVATE);
        List/*<IResourceChecker>*/ listResources = jss.getResources();
        if (listResources != null) {
            for (Iterator i = listResources.iterator(); i.hasNext();) {
                IResourceChecker resourceChecker = (IResourceChecker) i.next();
                resourceChecker.detect(checkerInfo);
            }
        }

        try {
            SessionBean instance = ctx.getInstance();
            instance.ejbPassivate();
            JStatefulOutputStream out =
                new JStatefulOutputStream(new BufferedOutputStream(getFileOutputStream(sid)));
            out.writeObject(instance);
            out.close();
        } catch (Exception e) {
            TraceEjb.ssfpool.log(BasicLevel.WARN, "Cannot passivate instance:" + e);
            return false;
        }
        TraceEjb.ssfpool.log(BasicLevel.WARN, "passivated: #" + sid);
        return true;
    }

    public JStatefulContext activateStateful(JStatefulSwitch jss) {
        int sid = jss.getSessionId();
        if (TraceEjb.isDebugSsfpool()) {
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "#" + sid);
        }
        // In this version, only instance is garbaged at passivation (not Context)
        JStatefulContext bctx = (JStatefulContext) jss.getStatefulContext();
        try {
            JStatefulInputStream in =
                new JStatefulInputStream(new BufferedInputStream(getFileInputStream(sid)), jss);
            Object obj = in.readObject();
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "Deserialized object:" + obj);
            SessionBean sb = (SessionBean) obj;
            in.close();
            if (sb == null) {
                TraceEjb.ssfpool.log(BasicLevel.ERROR, "Bad stateful deserialization");
                return null;
            }
            bctx.setInstance(sb);
            sb.ejbActivate();
            // remove passivated file
            removePassivatedState(sid);
            cacheSize++;
        } catch (Exception e) {
            TraceEjb.ssfpool.log(BasicLevel.WARN, "Cannot activate instance:" + e);
            return null;
        }
        TraceEjb.ssfpool.log(BasicLevel.WARN, "reactivated : #" + sid);
        return bctx;
    }

    public synchronized void removeStateful(int sid) {
        if (TraceEjb.isDebugSsfpool()) {
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "#" + sid);
        }
        // Remove from statefulList
        JStatefulSwitch jss = (JStatefulSwitch) statefulList.remove(new Integer(sid));

        // Check stateful before removing them
        EJBResourceCheckerInfo checkerInfo = new EJBResourceCheckerInfo(this, ResourceCheckpoints.EJB_PRE_DESTROY);
        List/*<IResourceChecker>*/ listResources = jss.getResources();
        if (listResources != null) {
            for (Iterator i = listResources.iterator(); i.hasNext();) {
                IResourceChecker resourceChecker = (IResourceChecker) i.next();
                resourceChecker.detect(checkerInfo);
            }
        }

        if (jss.isPassivated()) {
            // remove passivated file
            try {
                removePassivatedState(sid);
            } catch (Exception e) {
                TraceEjb.ssfpool.log(BasicLevel.WARN, "Cannot remove passivated instance:" + e);
            }
        } else {
            cacheSize--;
        }
        if (TraceEjb.isDebugSsfpool()) {
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "statefulList size = " + statefulList.size());
            TraceEjb.ssfpool.log(BasicLevel.DEBUG, "cache size = " + cacheSize);
        }
    }

    /**
     * get a new session context must call newInstance (EJB specs) => no pool
     * should be used.
     * @return a new Session Context
     */
    public JSessionContext getJContext(JSessionSwitch ss) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        JStatefulContext bctx = null;
        try {
            bctx = createNewInstance(ss);
        } catch (Exception e) {
            throw new EJBException("Cannot create a new instance", e);
        }
        return bctx;
    }

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

    /**
     * Create a new instance of the bean and its StatefulContext
     */
    private JStatefulContext createNewInstance(JSessionSwitch ss) throws Exception {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        // create the bean instance
        SessionBean bean = null;
        try {
            bean = (SessionBean) beanclass.newInstance();
        } catch (InstantiationException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot instantiate session bean");
            throw e;
        } catch (IllegalAccessException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot instantiate session bean");
            throw e;
        }
        // create a new StatefulContext and bind it to the instance

        JStatefulContext bctx = null;
        if (!dd.isClusterReplicated()) {
            bctx = new JStatefulContext(this, bean, isSynchro);
        } else {
            bctx = new JRepStatefulContext(this, bean, isSynchro);
        }

        bean.setSessionContext(bctx);
        bctx.setState(1);
        return bctx;
    }

    /*
     * Make sense only for entities
     */
    public void storeInstances(Transaction tx) {
        // unused
    }
}
