/*-*- mode: Java; tab-width:8 -*-*/

package php.java.bridge.http;

import java.util.HashMap;
import java.util.Iterator;

import php.java.bridge.ISession;
import php.java.bridge.JavaBridge;
import php.java.bridge.JavaBridgeClassLoader;
import php.java.bridge.SessionFactory;
import php.java.bridge.Util;


/**
 * Create session, jsr223 contexts.<p>
 * The ContextFactory may keep a promise (a "proxy") which one may evaluate to a
 * session reference (for PHP/JSP session sharing), and/or it may
 * reference a "half-executed" bridge for local channel re-directs (for
 * "high speed" communication links). 
 *
 *<p>
 * A unique context
 * instance should be created for each request and destroyed when the request
 * is done.
 * </p>
 * <p>
 * Clients of the PHP clients may attach additional data and run with
 * a customized ContextFactory by using the visitor pattern, @see #accept(IContextFactory).
 * </p>
 * 
 * The string ID of the instance should be passed to the client, which may
 * pass it back together with the getSession request or the "local
 * channel re-direct". If the former happens, we invoke the promise
 * and return the session object to the client. Different promises can
 * evaluate to the same session object.  For local channel re-directs
 * the ContextFactory is given to a ContextRunner which handles the
 * local channel communication.
 * <p>
 * There can be only one instance of a ContextFactory per classloader.
 * </p>
 * @see php.java.servlet.ServletContextFactory
 * @see php.java.bridge.http.ContextServer
 */
public final class ContextFactory extends SessionFactory implements IContextFactory {
    private boolean removed=false;

    private static final HashMap contexts = new HashMap();
    private static final long ORPHANED_TIMEOUT = 600000; // every 10min
    private JavaBridge bridge = null;
	
    private String id;
    private long timestamp;

    private ContextServer contextServer = null;
    private IContextFactory visitor; 

    private static synchronized String addNext(ContextFactory thiz) {
        int next = contexts.size()+1;
        String id = String.valueOf(next);
        contexts.put(id, thiz);
        return id;
    }
    private static synchronized void remove(String id) {
        if(Util.logLevel>5) Util.logDebug("removing context: " + id);
        contexts.remove(id);
    }
    public ContextFactory() {
      super();
      id=addNext(this);
      timestamp = System.currentTimeMillis();
      if(Util.logLevel>5) Util.logDebug("new context: " + id);
    }

    /**
     * Create a new ContextFactory and add it to the list of context factories kept by this classloader.
     * @return The created ContextFactory.
     * @see php.java.bridge.http.ContextFactory#get(String, ContextServer)
     */
    public static IContextFactory addNew() {
    	ContextFactory factory = new ContextFactory();
    	factory.visitor = factory;
    	return factory;
    }
    
   /**
     * Only for internal use.
     *  
     * Returns the context factory associated with the given <code>id</code>
     * @param id The ID
     * @param server Your context server.
     * @return The ContextFactory or null.
     * @see #addNew()
     * @throws SecurityException if id belongs to a different ContextServer.
     */
    /* See PhpJavaServlet#contextServer, http.ContextRunner#contextServer and 
     * JavaBridgeRunner#ctxServer. */
    public static IContextFactory get(String id, ContextServer server) {
        if(server == null) throw new NullPointerException("server");

        ContextFactory factory = (ContextFactory)contexts.get(id); 
        if(factory==null) return null;
        
        if(factory.contextServer==null) factory.contextServer = server;
        if(factory.contextServer != server) throw new SecurityException("Illegal access");
        return factory.visitor;
    }
    /**
     * Override this method, if you want synchronize the current id with the persistent id.
     * @param target The fresh ContextFactory (which was passed via the current X_JAVABRIDGE_CONTEXT header).
     */
    public void recycle(ContextFactory target) {}
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#recycle(java.lang.String)
     */
    public void recycle(String id) throws SecurityException {
        IContextFactory target = ((ContextFactory)contexts.get(id)).visitor;
        target.remove();
        target.recycle(this);
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#remove()
     */
    public synchronized void remove() {
	if(Util.logLevel>5) Util.logDebug("context finished: " +getId());
	remove(getId());
	bridge=null;
	removed=true;
	notify();      
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#destroy()
     */
    public void destroy() {
        remove();
	destroyOrphaned();
    }
    
    /**
     * @deprecated
     * @see #destroyAll()
     */
    public static void removeAll() {
        destroyAll();
    }
    private boolean isInitialized() {
        return (contextServer!=null);
    }
    private static boolean lastTimestampSet = false;
    private static long lastTimestamp = 0;
    private static synchronized void destroyOrphaned() {
	long timestamp = System.currentTimeMillis();
	if(!lastTimestampSet) lastTimestampSet = true;
	else if(lastTimestamp+ORPHANED_TIMEOUT>=timestamp) return;
	lastTimestamp = timestamp;
	
        for(Iterator ii=contexts.values().iterator(); ii.hasNext();) {
	    ContextFactory ctx = ((ContextFactory)ii.next());
	    if(ctx.isInitialized() || ctx.removed) continue;
	    if(ctx.timestamp+ORPHANED_TIMEOUT<timestamp) {
	        ctx.removed=true;
	        Util.logDebug("Orphaned context: " + ctx.getId() + " removed.");
	        ii.remove();
	    }
	}        
    }
    /**
     * Remove all context factories from the classloader.
     * May only be called by the ContextServer.
     * @see php.java.bridge.http.ContextServer
     */
    public static synchronized void destroyAll() {
	for(Iterator ii=contexts.values().iterator(); ii.hasNext();) {
	    ContextFactory ctx = ((ContextFactory)ii.next());
	    synchronized(ctx) {
		ctx.removed=true;
		ctx.notify();
	    }
	    ii.remove();
	}
    }
    
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#waitFor()
     */
    public synchronized void waitFor() throws InterruptedException {
    	if(!removed) wait();
    }
    
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#getId()
     */
    public String getId() { 
	return id; 
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#toString()
     */
    public String toString() {
	return "Context# " +id + ", isInitialized: " + isInitialized();
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#getContext()
     */
    public Object getContext() {
	return visitor.getContext();
    }
	
    /**
     * Set the Context into this factory.
     * Should be called by Context.addNew() only.
     * @param context
     * @see #addNew()
     */
    public void setContext(Object context) {
        visitor.setContext(context);
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#setBridge(php.java.bridge.JavaBridge)
     */
    public void setBridge(JavaBridge bridge) {
	this.bridge = bridge;
    }
    /* (non-Javadoc)
     * @see php.java.bridge.http.IContextFactory#getBridge()
     */
    public JavaBridge getBridge() {
        if(bridge != null) return bridge;
        setBridge(new JavaBridge());
        bridge.setClassLoader(new JavaBridgeClassLoader(bridge, null));
        bridge.setSessionFactory(this);
	return bridge;
    }
    private void setVisitor(IContextFactory newVisitor) {
        visitor = newVisitor;
    }
    /**
     * Use this method to attach a visitor to the ContextFactory.
     * @param visitor The custom ContextFactory
     */
    public void accept(IContextFactoryVisitor visitor) {
        visitor.visit(this);
        setVisitor(visitor);
    }
    
    /**
     * Return a simple session which cannot be shared with JSP
     * @param name The session name
     * @param clientIsNew true, if the client wants a new session
     * @param timeout expires in n seconds
     */
    public ISession getSimpleSession(String name, boolean clientIsNew, int timeout) {
        return super.getSession(name, clientIsNew, timeout);
    }
    /**
     * Return a standard session, shared with JSP
     * @param name The session name
     * @param clientIsNew true, if the client wants a new session
     * @param timeout expires in n seconds
     */
    public ISession getSession(String name, boolean clientIsNew, int timeout) {
        return visitor.getSession(name, clientIsNew, timeout);
    }
}