/*
 * 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@ricochet.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.ui.script;

import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import org.apache.bsf.*;
import org.apache.log4j.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.ui.*;
import com.silverwrist.venice.ui.helpers.ThrowableContent;
import com.silverwrist.venice.ui.rpc.XmlRpcLibrary;

public class ScriptManager
{
  /*--------------------------------------------------------------------------------
   * Internal class for script utility
   *--------------------------------------------------------------------------------
   */

  public class MyScriptLibrary extends ScriptLibrary
  {
    public Object exec(String script) throws ScriptingException, ThrowableContent
    {
      RequestInput ri = (RequestInput)(ScriptManager.this.lookupObject("request"));
      ScriptSupport ssup = (ScriptSupport)(ri.queryService(ScriptSupport.class));
      String script_file = ssup.getScriptName(script);
      String logger_name = ssup.getScriptLoggerName(script);
      if (logger.isDebugEnabled())
	logger.debug("EXECUTING " + script_file);

      ScriptReturn sro = new ScriptReturn();
      ScriptManager.this.exec(new File(script_file),logger_name,sro);
      return sro.get();

    } // end exec

    public void output(Object o)
    {
      if (m_return_stack.size()>0)
      { // set the return value
	ScriptReturn sro = (ScriptReturn)(m_return_stack.getFirst());
	sro.set(o);

      } // end if

    } // end output

  } // end class MyScriptLibrary

  /*--------------------------------------------------------------------------------
   * Internal class for doing logging
   *--------------------------------------------------------------------------------
   */

  public class ScriptLogger
  {
    public void fatal(Object message)
    {
      if (m_logger_stack.size()>0)
	((Logger)(m_logger_stack.getFirst())).fatal(message);

    } // end fatal

    public void error(Object message)
    {
      if (m_logger_stack.size()>0)
	((Logger)(m_logger_stack.getFirst())).error(message);

    } // end error

    public void warn(Object message)
    {
      if (m_logger_stack.size()>0)
	((Logger)(m_logger_stack.getFirst())).warn(message);

    } // end warn

    public void info(Object message)
    {
      if (m_logger_stack.size()>0)
      { // check to make sure info is enabled first
	Logger l = (Logger)(m_logger_stack.getFirst());
	if (l.isInfoEnabled())
	  l.info(message);

      } // end if

    } // end info

    public void debug(Object message)
    {
      if (m_logger_stack.size()>0)
      { // check to make sure debug is enabled first
	Logger l = (Logger)(m_logger_stack.getFirst());
	if (l.isDebugEnabled())
	  l.debug(message);

      } // end if

    } // end debug

  } // end class ScriptLogger

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

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

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

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

  private BSFManager m_bsf;                // Bean Scripting Framework manager
  private LinkedList m_registered_names;   // stack of lists of registered names
  private LinkedList m_logger_stack;       // stack of loggers for nested evaluation
  private LinkedList m_return_stack;       // stack of return values

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

  /**
   * Creates a new instance of <CODE>ScriptManager</CODE>.
   *
   * @param temp_dir Temporary directory that the scripting engines managed by this
   *                 <CODE>ScriptManager</CODE> can use for storing temporary files.
   */
  public ScriptManager(String temp_dir) throws ScriptingException
  {
    if (logger.isDebugEnabled())
      logger.debug("creating new ScriptManager");

    // initialize the BSFManager
    m_bsf = new BSFManager();
    m_bsf.setTempDir(temp_dir);

    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 beans used by the scripting engines
      m_bsf.declareBean("vlib",new MyScriptLibrary(),MyScriptLibrary.class);
      m_bsf.declareBean("logger",new ScriptLogger(),ScriptLogger.class);
      m_bsf.declareBean("rpclib",XmlRpcLibrary.get(),XmlRpcLibrary.class);

    } // end try
    catch (BSFException e)
    { // translate the exception
      logger.error("bean declarations threw a BSFException",e);
      throw new ScriptingException("initialization error in ScriptManager",e);

    } // end catch

    // initialize the rest of the object
    m_registered_names = new LinkedList();
    m_registered_names.addFirst(new HashSet());
    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()
  {
    if (logger.isDebugEnabled())
      logger.debug("Entered 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 void translateReturnedObject(ScriptReturn sro) throws ScriptingException
  {
    Object obj = sro.get();
    if (obj==null)
      return;  // nothing to do

    Class klass = obj.getClass();
    final String klassname = klass.getName();

    // Special processing for JavaScript NativeError objects
    if (klassname.equals("org.mozilla.javascript.NativeError"))
      throw new ScriptingException("JavaScript runtime error: " + obj.toString());

  } // end translateReturnedObject

  /**
   * Translates an exception thrown by script execution into one of the exception types that
   * <CODE>ScriptManager</CODE> officially supports throwing.
   *
   * @param source Source script name.
   * @param lang Language the source script is in (JavaScript, Tcl, etc.)
   * @param e <CODE>BSFException</CODE> that was thrown by a BSF method, which will be
   *          &quot;unwrapped&quot; by this method.
   * @exception com.silverwrist.venice.ui.script.ScriptingException In most cases, the
   *            <CODE>BSFException</CODE> will translate to one of these.
   * @exception com.silverwrist.venice.ui.helpers.ThrowableContent This is actually ordinary content
   *            which may have been &quot;thrown&quot; by the script, and will be processed at a
   *            higher level.
   */
  private void translateScriptException(String source, String lang, BSFException e)
      throws ScriptingException, ThrowableContent
  {
    if (logger.isDebugEnabled())
      logger.debug("translateScriptException for " + source + " [" + lang + "]");

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

    } // end if

    if (t instanceof ThrowableContent)
    { // treat ThrowableContent specially
      if (logger.isDebugEnabled())
	logger.debug("ThrowableContent (" + t.getClass().getName() + ") - no need to process");
      throw (ThrowableContent)t;

    } // end if

    if (   (e.getReason()==BSFException.REASON_EXECUTION_ERROR)
        || (e.getReason()==BSFException.REASON_OTHER_ERROR))
    { // throw a ScriptingException
      logger.error("Script compiler or execution exception",t);
      if (t!=null)
      { // throw THIS exception
	logger.error("Exception type: " + t.getClass().getName());
	throw new ScriptingException(source,lang,e.getMessage(),t);

      } // end if
      else  // throw the regular old name
	throw new ScriptingException(source,lang,e.getMessage(),e);
      
    } // end if
    else
    { // unknown exception type here!
      logger.error("Unknown BSFException in script execution (reason " + e.getReason() + ")",e);
      if (t!=null)
	logger.error("Root cause (type " + t.getClass().getName() + "):",t);
      throw new ScriptingException("unexpected exception while executing " + source + " [" + lang + "]",e);

    } // end else

  } // end translateScriptException

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

  public void shutdown()
  {
    m_bsf.terminate();
    m_registered_names.clear();
    m_logger_stack.clear();

  } // end shutdown

  public void exec(File file, String logger_name, ScriptReturn sro) throws ScriptingException, ThrowableContent
  {
    String filename = file.getAbsolutePath();
    String lang;
    StringBuffer code;

    if (logger.isDebugEnabled())
      logger.debug("Exec'ing " + filename + ", logger " + logger_name);

    try
    { // look up the scripting language for this file
      lang = m_bsf.getLangFromFilename(filename);
      code = IOUtil.loadText(file);
      if (logger.isDebugEnabled())
	logger.debug("Code language is " + lang + ", loaded " + code.length() + " characters");

    } // end try
    catch (BSFException e)
    { // this may be thrown by getLangFromFilename
      throw new ScriptingException("unknown scripting language: file " + filename);

    } // end catch
    catch (IOException ie)
    { // this may be thrown by IOUtil.load
      throw new ScriptingException("unable to load script file: " + filename,ie);

    } // end catch

    if (logger_name!=null)  // push a new logger onto the stack
      m_logger_stack.addFirst(Logger.getLogger(logger_name));
    if (sro!=null)
      m_return_stack.addFirst(sro);

    try
    { // execute the script!
      m_bsf.exec(lang,filename,1,1,code);

    } // end try
    catch (BSFException e)
    { // translate the exception thrown by the script
      translateScriptException(filename,lang,e);

    } // end catch
    finally
    { // pop the logger stack if necessary
      if (logger_name!=null)
	m_logger_stack.removeFirst();
      if (sro!=null)
	m_return_stack.removeFirst();

    } // end finally

    // if we've gotten this far, see if the returned object is actually an error that needs
    // translation before it can be presented to the world
    translateReturnedObject(sro);

  } // end exec

  public void register(String name, Object obj)
  {
    m_bsf.registerBean(name,obj);
    HashSet tbl = (HashSet)(m_registered_names.getFirst());
    tbl.add(name);

  } // end register

  public void pushContext()
  {
    m_registered_names.addFirst(new HashSet());

  } // end pushContext

  public void popContext()
  {
    HashSet tbl = (HashSet)(m_registered_names.removeFirst());
    Iterator it = tbl.iterator();
    while (it.hasNext())
      m_bsf.unregisterBean((String)(it.next()));
    if (m_registered_names.isEmpty())
      m_registered_names.addFirst(new HashSet());

  } // end popContext

  public Object lookupObject(String name)
  {
    return m_bsf.lookupBean(name);

  } // end lookupObject

} // end class ScriptManager
