/*
 * 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.reflect.*;
import java.util.*;
import org.apache.bsf.*;
import org.apache.log4j.Logger;
import com.silverwrist.util.*;
import com.silverwrist.dynamo.Namespaces;
import com.silverwrist.dynamo.event.*;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;
import com.silverwrist.dynamo.util.*;

class ScriptEngine implements ScriptExecute
{
  /*--------------------------------------------------------------------------------
   * Internal class for registering new beans with the engine
   *--------------------------------------------------------------------------------
   */

  private class Registrar implements ScriptEngineRegisterObject
  {
    /*====================================================================
     * Constructor
     *====================================================================
     */

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

    /*====================================================================
     * Implementations from interface ScriptEngineRegisterObject
     *====================================================================
     */

    public void registerObject(String name, Object obj)
    {
      Map tmp = (Map)(m_register_maps.getFirst());
      tmp.put(name,obj);
      m_bsf.registerBean(name,obj);

    } // end registerObject

  } // end class Registrar

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

  private static Logger logger = Logger.getLogger(ScriptEngine.class);

  private static final Set RESERVED_DECLS;

  private static final Class[] fixupRhino_parmtypes = { Boolean.TYPE };
  private static final Object[] fixupRhino_parms = { Boolean.FALSE };

  private static final LibraryCast singleton_LibraryCast = new LibraryCast();
  private static final IDUtils singleton_IDUtils = new IDUtils();
  private static final IOUtils singleton_IOUtils = new IOUtils();
  private static final StringUtils singleton_StringUtils = new StringUtils();

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

  private ScriptExecuteHelper m_helper;  // helper to implement ScriptExecute
  private BSFManager m_bsf;              // BSF script manager
  private LinkedList m_register_maps;    // sets of registered beans
  private Registrar m_registrar;         // registrar
  private LinkedList m_logger_stack;     // stack of loggers
  private LinkedList m_return_stack;     // stack of return values

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

  /**
   * Constructs a new <CODE>ScriptEngine</CODE>.
   *
   * @param ctrl The {@link com.silverwrist.dynamo.script.ScriptController ScriptController} this
   *             <CODE>ScriptEngine</CODE> is associated with.
   * @exception com.silverwrist.dynamo.except.ScriptingException If creation of the script engine failed.
   */
  ScriptEngine(ScriptController ctrl) throws ScriptingException
  {
    m_helper = new ScriptExecuteHelper(this);
    m_bsf = new BSFManager();
    m_bsf.setTempDir(ctrl.getTempPath());

    if (BSFManager.isLanguageRegistered("javascript"))
    { // we need to so some fixing up to make Rhino work
      try
      { // ensure the engine is loaded, and do our fixup routine
	m_bsf.loadScriptingEngine("javascript");
	fixupRhino();

      } // end if
      catch (BSFException e)
      { // do nothing - we don't have JavaScript loaded
      } // end catch

    } // end if

    try
    { // declare the standard beans
      m_bsf.declareBean("cast",singleton_LibraryCast,LibraryCast.class);
      m_bsf.declareBean("dynamo",new LibraryDynamo(this),LibraryDynamo.class);
      m_bsf.declareBean("idutils",singleton_IDUtils,IDUtils.class);
      m_bsf.declareBean("ioutils",singleton_IOUtils,IOUtils.class);
      m_bsf.declareBean("logger",new LibraryLogger(this),LibraryLogger.class);
      m_bsf.declareBean("stringutils",singleton_StringUtils,StringUtils.class);

      // get the list of beans declared in the manager and declare them in BSF
      List beanlist = ctrl.getDeclarations();
      Iterator it = beanlist.iterator();
      while (it.hasNext())
      { // get all declared objects and declare them
	DeclaredObject decl = (DeclaredObject)(it.next());
	m_bsf.declareBean(decl.getName(),decl.getObject(),decl.getObjectClass());

      } // end while

    } // end try
    catch (BSFException e)
    { // throw an error - declaration failed
      logger.error("bean declarations threw a BSFException",e);
      throw new ScriptingException(ScriptEngine.class,"ScriptEngineMessages","engine.declFail",e);

    } // end catch

    m_register_maps = new LinkedList();
    m_registrar = new Registrar();
    m_logger_stack = new LinkedList();
    m_return_stack = new LinkedList();

  } // end constructor

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

  /**
   * What fixupRhino does is to turn off the fancy caching in the JavaScript engine.  This is because
   * caching involves dynamic creation of classes on the fly, and, for some reason (probably because
   * of differing ClassLoader implementations), that doesn't work, whether Rhino is loaded in the
   * Web application or in the JRE extensions.  When caching is turned off, Rhino can be loaded as
   * part of the web application, and it can see all the standard classes and all the Web application
   * classes.  But we have to turn it off by manipulating it via the Reflection API, as, for all we know,
   * Rhino isn't even being loaded right now.  (Though we have BSF load Rhino right away if it can,
   * to try and get this straightened out.)
   */
  private static void fixupRhino()
  {
    try
    { // get a pointer to the Context class
      Class klass = Class.forName("org.mozilla.javascript.Context");

      // get a pointer to its setCachingEnabled method
      Method m = klass.getMethod("setCachingEnabled",fixupRhino_parmtypes);

      // invoke it to turn caching off!
      m.invoke(null,fixupRhino_parms);

      // all done!
      if (logger.isDebugEnabled())
	logger.debug("fixupRhino COMPLETED SUCCESSFULLY");

    } // end try
    catch (Exception e)
    { // just log the exception and proceed
      if (logger.isDebugEnabled())
	logger.debug("fixupRhino NOT COMPLETED",e);

    } // end catch

  } // end fixupRhino

  private static final String createLoggerName(String source_name)
  {
    // Strip all extensions from the end of the name.
    int limit = source_name.lastIndexOf('/');
    int x = source_name.lastIndexOf('.');
    while (x>limit)
    { // remove all extensions from the end
      source_name = source_name.substring(0,x);
      x = source_name.lastIndexOf('.');

    } // end while

    // Strip all slashes from the start of the name.
    x = 0;
    while (source_name.charAt(x)=='/')
      x++;
    if (x>0)
      source_name = source_name.substring(x);

    // convert the name to a logger name
    StringBuffer rc = new StringBuffer("SCRIPT.");
    for (x=0; x<source_name.length(); x++)
    { // process all the characters
      char c = source_name.charAt(x);
      if (c=='/')
	rc.append('.');
      else if (c=='.')
	rc.append('_');
      else
	rc.append(c);

    } // end for

    return rc.toString();

  } // end createLoggerName

  private void translateException(String language, String source_name, BSFException e)
      throws ScriptingException
  {
    if (logger.isDebugEnabled())
      logger.debug("translateException for " + source_name + " [" + language + "]");
    int reason = e.getReason();
    Throwable t = e.getTargetException();

    if (t instanceof ExitScript)
    { // that was a deliberate exit from the script - discard it and continue
      if (logger.isDebugEnabled())
	logger.debug("ExitScript - normal condition");
      return;

    } // end if

    if ((reason==BSFException.REASON_EXECUTION_ERROR) || (reason==BSFException.REASON_OTHER_ERROR))
    { // throw a ScriptingException
      logger.error("Script compiler or execution exception",e);
      if (t!=null)
	logger.error("Root cause:",t);
      ScriptingException se = new ScriptingException(source_name,language,ScriptEngine.class,
						     "ScriptEngineMessages","null.message",
						     (t==null) ? e : t);
      se.setParameter(0,e.getMessage());
      throw se;

    } // end if
    else
    { // unknown exception type here!
      logger.error("Unknown BSFException in script execution (reason " + reason + ")",e);
      if (t!=null)
	logger.error("Root cause:",t);
      ScriptingException se = new ScriptingException(source_name,language,ScriptEngine.class,
						     "ScriptEngineMessages","exception.unexpected",e);
      se.setParameter(0,e.getMessage());
      throw se;

    } // end else

  } // end translateException

  /**
   * "Unwraps" the given <CODE>ReturnValue</CODE> object and determines whether it represents an error
   * that must be thrown from the script engine.
   *
   * @param language The language the current script is executing.
   * @param source_name The name of the script that is currently executing.
   * @param return_val The <CODE>ReturnValue</CODE> containing the script's return value.
   * @return The return value from the script.
   * @exception com.silverwrist.dynamo.except.ScriptingException If the return value actually represented
   *            an error return.
   */
  private Object translateReturnValue(String language, String source_name, ReturnValue return_val)
      throws ScriptingException
  {
    Object obj = return_val.get();
    if (obj==null)
      return null;  // nothing to do

    Class klass = obj.getClass();

    // Special processing for JavaScript NativeError objects
    if (klass.getName().equals("org.mozilla.javascript.NativeError"))
    { // this is a Rhino runtime error - throw it
      ScriptingException se = new ScriptingException(source_name,language,ScriptEngine.class,
						     "ScriptEngineMessages","rhino.runtimeError");
      se.setParameter(0,obj.toString());
      throw se;

    } // end if

    return obj;

  } // end translateReturnValue

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

  public Object executeScript(Request request, String language, String source_name, String code)
      throws ScriptingException
  {
    return exec(request,language,source_name,code,true);

  } // 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 static operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns <CODE>true</CODE> if the given name is one that's pre-declared in the <CODE>ScriptEngine</CODE>.
   *
   * @param name The object name to check.
   * @return <CODE>true</CODE> if the given name is pre-declared in the <CODE>ScriptEngine</CODE>, <CODE>false</CODE>
   *         if not.
   */
  static boolean isReservedDeclaration(String name)
  {
    return RESERVED_DECLS.contains(name);

  } // end isReservedDeclaration

  static String getLanguageFromSourceName(String source_name) throws ScriptingException
  {
    try
    { // call down to BSF to get the language
      return BSFManager.getLangFromFilename(source_name);

    } // end try
    catch (BSFException e)
    { // translate exception at return time
      ScriptingException se = new ScriptingException(ScriptEngine.class,"ScriptEngineMessages",
						     "language.unknown",e);
      se.setParameter(0,source_name);
      throw se;

    } // end catch

  } // end getLanguageFromSourceName

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

  void start(Request request, List start_hooks) throws ScriptingException
  {
    m_register_maps.addFirst(new HashMap());  // push new context
    Iterator it = start_hooks.iterator();
    ScriptEngineStartEvent evt = null;
    while (it.hasNext())
    { // call into each hook in turn
      if (evt==null)
	evt = new ScriptEngineStartEvent(request,m_registrar);
      ScriptEngineStartListener hook = (ScriptEngineStartListener)(it.next());
      hook.scriptEngineStarting(evt);

    } // end while

  } // end start

  void finish()
  {
    Map old = (Map)(m_register_maps.removeFirst());
    Map pmap;
    if (m_register_maps.isEmpty())
      pmap = Collections.EMPTY_MAP;
    else
      pmap = (Map)(m_register_maps.getFirst());
    Iterator it = old.keySet().iterator();
    while (it.hasNext())
    { // unregister all objects at this level
      String s = (String)(it.next());
      Object foo = pmap.get(s);
      if (foo!=null)
	m_bsf.registerBean(s,foo);
      else
	m_bsf.unregisterBean(s);

    } // end while

    old.clear();

  } // end finish

  Object exec(Request request, String language, String source_name, String code, boolean same_engine)
      throws ScriptingException
  {
    if (logger.isDebugEnabled())
      logger.debug("Exec'ing script " + source_name + " [" + language + "]");

    // register the request object
    if (!same_engine)
      request = new InternalRequest(request,this);
    m_bsf.registerBean("request",request);
    m_bsf.registerBean("request_help",new RequestHelper(request));

    // register the application object
    ObjectProvider op = (ObjectProvider)(request.queryService(ObjectProvider.class));
    Application app = (Application)(op.getObject(Namespaces.DYNAMO_APPLICATION_NAMESPACE,"application"));
    m_bsf.registerBean("application",app);

    // push elements onto the execution stacks
    m_logger_stack.addFirst(Logger.getLogger(createLoggerName(source_name)));
    ReturnValue return_val = new ReturnValue();
    m_return_stack.addFirst(return_val);

    try
    { // Execute the script!
      m_bsf.exec(language,source_name,1,1,code);

    } // end try
    catch (BSFException e)
    { // translate exceptions thrown by the code
      translateException(language,source_name,e);

    } // end catch
    finally
    { // pop the execution stacks at return time
      m_return_stack.removeFirst();
      m_logger_stack.removeFirst();
      m_bsf.unregisterBean("application");
      m_bsf.unregisterBean("request_help");
      m_bsf.unregisterBean("request");

    } // end finally

    // Some return values are errors that need to be translated into thrown exceptions.
    return translateReturnValue(language,source_name,return_val);

  } // end exec

  /**
   * Looks up an object that has been registered with the scripting engine.
   *
   * @param name The name of the object to look up.
   * @return The object, or <CODE>null</CODE> if no such object was registered with the scripting engine.
   */
  Object lookupObject(String name)
  {
    return m_bsf.lookupBean(name);

  } // end lookupObject

  /**
   * Sets the return value of the current script.
   *
   * @param obj The return value to be set.
   */
  void setReturnValue(Object obj)
  {
    if (m_return_stack.isEmpty())
      return;
    ReturnValue return_val = (ReturnValue)(m_return_stack.getFirst());
    return_val.set(obj);

  } // end setReturnValue

  Logger getCurrentLogger()
  {
    if (m_logger_stack.isEmpty())
      return logger;
    else
      return (Logger)(m_logger_stack.getFirst());

  } // end getCurrentLogger

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  {
    HashSet tmp = new HashSet();
    tmp.add("cast");
    tmp.add("dynamo");
    tmp.add("idutils");
    tmp.add("ioutils");
    tmp.add("logger");
    tmp.add("stringutils");
    RESERVED_DECLS = Collections.unmodifiableSet(tmp);

  } // end static initializer

} // end class ScriptEngine
