/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002-03 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.script;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.dynamo.Namespaces;
import com.silverwrist.dynamo.event.*;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;

/**
 * The top-level scripting component in the Dynamo framework, which is created directly by
 * {@link com.silverwrist.dynamo.app.ApplicationContainer ApplicationContainer}.  It manages a dynamic pool
 * of {@link com.silverwrist.dynamo.script.ScriptEngine ScriptEngine} objects, which may be used to execute
 * multiple scripts simultaneously (in multiple threads, as from multiple requests).  The pool is managed
 * via soft memory references, so it is memory-sensitive.  Scripts that are executed by the framework are
 * usually passed here, where they are assigned a script engine to execute them.  This class also manages
 * the configuration of the script engines, which may be influenced by other Dynamo components.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class ScriptController
    implements ComponentInitialize, ComponentShutdown, ScriptEngineConfig, ScriptExecute
{
  /*--------------------------------------------------------------------------------
   * Internal class implementing sweep task
   *--------------------------------------------------------------------------------
   */

  private class ScriptSweeper implements BackgroundTask
  {
    /*====================================================================
     * Constructor
     *====================================================================
     */

    ScriptSweeper()
    { // do nothing
    } // end constructor

    /*====================================================================
     * Implementations from interface BackgroundTask
     *====================================================================
     */

    public void run(ServiceProvider services)
    {
      sweep();

    } // end run

  } // end class ScriptSweeper

  /*--------------------------------------------------------------------------------
   * Internal class allowing removal of declared object
   *--------------------------------------------------------------------------------
   */

  private class DeclareShutdown implements ComponentShutdown
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private String m_name;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    DeclareShutdown(String name)
    {
      m_name = name;

    } // end constructor

    /*====================================================================
     * Implementations from interface ComponentShutdown
     *====================================================================
     */

    public void shutdown()
    {
      synchronized (ScriptController.this)
      {
	m_declared_objects.remove(m_name);

      } // end synchronized block

    } // end shutdown

  } // end class DeclareShutdown

  /*--------------------------------------------------------------------------------
   * Internal class allowing removal of engine start hook
   *--------------------------------------------------------------------------------
   */

  private class StartHookShutdown implements ComponentShutdown
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private ScriptEngineStartListener m_hook;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    StartHookShutdown(ScriptEngineStartListener hook)
    {
      m_hook = hook;

    } // end constructor

    /*====================================================================
     * Implementations from interface ComponentShutdown
     *====================================================================
     */

    public void shutdown()
    {
      synchronized (ScriptController.this)
      {
	m_start_hooks.remove(m_hook);

      } // end synchronized block

    } // end shutdown

  } // end class StartHookShutdown

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static final long SWEEP_TIME = 600000L;  // 10 minutes

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private ScriptExecuteHelper m_helper;
  private String m_temp_path;
  private LinkedList m_engines = new LinkedList();
  private ReferenceQueue m_ref_queue = new ReferenceQueue();
  private ComponentShutdown m_sweep_task;
  private HashMap m_declared_objects = new HashMap();
  private ArrayList m_start_hooks = new ArrayList();

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates the <CODE>ScriptController</CODE>.
   */
  public ScriptController()
  {
    m_helper = new ScriptExecuteHelper(this);

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private final synchronized void sweep()
  {
    Reference r = m_ref_queue.poll();
    while (r!=null)
    { // remove the stale references
      m_engines.remove(r);
      r.clear();
      r = m_ref_queue.poll();

    } // end while

  } // end sweep

  /**
   * Locates a new instance of a {@link com.silverwrist.dynamo.script.ScriptEngine ScriptEngine} that's ready for
   * use, or creates a new one if no suitable engines exist.
   * 
   * @return The new scripting engine instance.
   * @exception com.silverwrist.dynamo.except.ScriptingException If there was an error in creating a new
   *            <CODE>ScriptEngine</CODE>.
   */
  private final ScriptEngine getEngine() throws ScriptingException
  {
    ScriptEngine rc = null;
    synchronized (this)
    { // fiddling with m_engines here...
      sweep();
      while ((rc==null) && !(m_engines.isEmpty()))
      { // look for a reference that contains a valid ScriptEngine
	SoftReference r = (SoftReference)(m_engines.removeFirst());
	rc = (ScriptEngine)(r.get());
	r.clear();

      } // end while

    } // end synchronized block

    if (rc==null)
      rc = new ScriptEngine(this);
    return rc;

  } // end getEngine

  /**
   * Returns a {@link com.silverwrist.dynamo.script.ScriptEngine ScriptEngine} instance to the pool for later use.
   *
   * @param engine The <CODE>ScriptEngine</CODE> to be returned to the pool.
   */
  private final synchronized void releaseEngine(ScriptEngine engine)
  {
    sweep();
    m_engines.addFirst(new SoftReference(engine,m_ref_queue));

  } // end releaseEngine

  /**
   * Returns a list of all defined start hooks for the script engine.
   *
   * @return The list of {@link com.silverwrist.dynamo.event.ScriptEngineStartListener ScriptEngineStartListener}
   *         objects.
   */
  private final synchronized List getStartHooks()
  {
    if (m_start_hooks.isEmpty())
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList(m_start_hooks.size());
    rc.addAll(m_start_hooks);
    return rc;

  } // end getStartHooks

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentInitialize
   *--------------------------------------------------------------------------------
   */

  /**
   * Initialize the component.
   *
   * @param config_root Pointer to the section of the Dynamo XML configuration file that configures this
   *                    particular component.  This is to be considered "read-only" by the component.
   * @param services An implementation of {@link com.silverwrist.dynamo.iface.ServiceProvider ServiceProvider}
   *                 which provides initialization services to the component.  This will include an implementation
   *                 of {@link com.silverwrist.dynamo.iface.ObjectProvider ObjectProvider} which may be used to
   *                 get information about other objects previously initialized by the application.
   * @exception com.silverwrist.dynamo.except.ConfigException If an error is encountered in the component
   *            configuration.
   */
  public void initialize(Element config_root, ServiceProvider services) throws ConfigException
  {
    try
    { // Start by querying the temp directory.
      ObjectProvider op = (ObjectProvider)(services.queryService(ObjectProvider.class));
      m_temp_path = (String)(op.getObject(Namespaces.SUBSTRATE_NAMESPACE,ApplicationSubstrate.OBJ_TEMP_PATH));
      File tester = new File(m_temp_path);
      if (!(tester.isDirectory()))
      { // temporary directory is bogus!
	ConfigException ce = new ConfigException(ScriptController.class,"ScriptEngineMessages",
						 "scriptCtrl.badTmpDir");
	ce.setParameter(0,m_temp_path);
	throw ce;

      } // end if

    } // end try
    catch (NoSuchObjectException e)
    { // translate to ConfigException
      throw new ConfigException(ScriptController.class,"ScriptEngineMessages","scriptCtrl.noObjTmp");

    } // end catch

    // Set up the background sweeping task.
    BackgroundScheduler sched = (BackgroundScheduler)(services.queryService(BackgroundScheduler.class));
    m_sweep_task = sched.runTaskPeriodic(new ScriptSweeper(),SWEEP_TIME,SWEEP_TIME);

  } // end initialize

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentShutdown
   *--------------------------------------------------------------------------------
   */

  /**
   * Shuts down the <CODE>ScriptController</CODE>, stopping the background sweep task and releasing all
   * {@link com.silverwrist.dynamo.script.ScriptEngine ScriptEngine} references.
   */
  public void shutdown()
  {
    // Shut down the background task.
    m_sweep_task.shutdown();
    m_sweep_task = null;

    // Do one more sweep to clear dead references.
    sweep();

    while (!(m_engines.isEmpty()))
    { // remove any other references to engines
      Reference r = (Reference)(m_engines.removeFirst());
      r.clear();

    } // end while

  } // end shutdown

  /*--------------------------------------------------------------------------------
   * Implementations from interface ScriptEngineConfig
   *--------------------------------------------------------------------------------
   */

  /**
   * Adds a new "declared object" to the scripting engine.  Such "declared objects" are typically exposed
   * as "global variables" in any scripts run by the scripting engine.  This is a handy way, for instance,
   * to implement native-code libraries usable by scripts.
   *
   * @param name Name of the object to be declared.  This will be the name of the variable exposed to scripts.
   * @param obj The object instance to be exposed to scripts.
   * @param klass Class the object is to be exposed as.  This is used by scripting engines to infer type
   *              and property/method information for the object.
   * @return An instance of {@link com.silverwrist.dynamo.iface.ComponentShutdown ComponentShutdown} which
   *         will "undeclare" the object when its
   *         {@link com.silverwrist.dynamo.iface.ComponentShutdown#shutdown() shutdown()} method is called.
   * @exception com.silverwrist.dynamo.except.ConfigException If the object cannot be declared (its name is
   *            reserved, for instance, or an object of that name has already been declared.
   */
  public ComponentShutdown addDeclaredObject(String name, Object obj, Class klass) throws ConfigException
  {
    if (ScriptEngine.isReservedDeclaration(name))
    { // this name is reserved...throw it
      ConfigException ce = new ConfigException(ScriptController.class,"ScriptEngineMessages",
					       "declObj.reserved");
      ce.setParameter(0,name);
      throw ce;

    } // end if

    synchronized (this)
    { // test and add declared object to map
      if (m_declared_objects.containsKey(name))
      { // name has already been used
	ConfigException ce = new ConfigException(ScriptController.class,"ScriptEngineMessages",
						 "declObj.already");
	ce.setParameter(0,name);
	throw ce;

      } // end if

      DeclaredObject dobj = new DeclaredObject(name,obj,klass);
      m_declared_objects.put(dobj.getName(),dobj);
      return new DeclareShutdown(dobj.getName());

    } // end synchronized block

  } // end addDeclaredObject

  /**
   * Adds a new "hook" to the script engine startup process in the form of an event listener.  The
   * {@link com.silverwrist.dynamo.event.ScriptEngineStartListener#scriptEngineStarting(com.silverwrist.dynamo.event.ScriptEngineStartEvent) scriptEngineStarting()}
   * method of the given object will be called when an instance of a script enine starts up.
   *
   * @param listener The event listener to register for handling startup events.
   * @return An instance of {@link com.silverwrist.dynamo.iface.ComponentShutdown ComponentShutdown} which will
   *         unregister the given event listener when its
   *         {@link com.silverwrist.dynamo.iface.ComponentShutdown#shutdown() shutdown()} method is called.
   */
  public synchronized ComponentShutdown addStartListener(ScriptEngineStartListener listener)
  {
    m_start_hooks.add(listener);
    return new StartHookShutdown(listener);

  } // end addStartHook

  /*--------------------------------------------------------------------------------
   * Implementations from interface ScriptExecute
   *--------------------------------------------------------------------------------
   */

  public Object executeScript(Request request, String language, String source_name, String code)
      throws ScriptingException
  {
    ScriptEngine engine = getEngine();
    try
    { // make the engine start up!
      engine.start(request,getStartHooks());
      return engine.exec(request,language,source_name,code,false);

    } // end try
    finally
    { // make sure we release the engine before we go
      engine.finish();
      releaseEngine(engine);

    } // end finally

  } // end executeScript

  public Object executeScript(Request request, String language, String source_name, InputStream code)
      throws ScriptingException
  {
    return m_helper.executeScript(request,language,source_name,code);

  } // end executeScript

  public Object executeScript(Request request, String language, String resource_name)
      throws ScriptingException
  {
    return m_helper.executeScript(request,language,resource_name);

  } // end try

  public Object executeScript(Request request, String resource_name) throws ScriptingException
  {
    return m_helper.executeScript(request,resource_name);

  } // end executeScript

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns the "temporary path" defined for scripts to use.
   *
   * @return The temporary pathname that script engines use.
   */
  String getTempPath()
  {
    return m_temp_path;

  } // end getTempPath

  synchronized List getDeclarations()
  {
    if (m_declared_objects.isEmpty())
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList(m_declared_objects.size());
    rc.addAll(m_declared_objects.values());
    return rc;

  } // end getDeclarations

} // end class ScriptController
