/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.scripting.rhino;

import helma.scripting.rhino.extensions.*;
import helma.scripting.rhino.debug.HelmaDebugger;
import helma.framework.core.*;
import helma.framework.repository.Resource;
import helma.objectmodel.*;
import helma.objectmodel.db.DbMapping;
import helma.objectmodel.db.NodeHandle;
import helma.scripting.*;
import helma.util.*;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.LazilyLoadedCtor;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.Wrapper;
import org.mozilla.javascript.commonjs.module.RequireBuilder;
import org.mozilla.javascript.commonjs.module.provider.StrongCachingModuleScriptProvider;
import org.mozilla.javascript.tools.debugger.ScopeProvider;

import java.io.*;
import java.net.URI;
import java.text.*;
import java.util.*;
import java.lang.ref.WeakReference;

/**
 * This is the implementation of ScriptingEnvironment for the Mozilla Rhino EcmaScript interpreter.
 */
public final class RhinoCore implements ScopeProvider {
    // the application we're running in
    public final Application app;

    // our context factory
    ContextFactory contextFactory;

    // the global object
    GlobalObject global;

    // caching table for JavaScript object wrappers
    CacheMap wrappercache;

    // table containing JavaScript prototypes
    Hashtable prototypes;

    // timestamp of last type update
    volatile long lastUpdate = 0;

    // the wrap factory
    WrapFactory wrapper;

    // the prototype for HopObject
    ScriptableObject hopObjectProto;

    // the prototype for path objects
    PathWrapper pathProto;

    // Any error that may have been found in global code
    String globalError;

    // the debugger, if active
    HelmaDebugger debugger = null;

    // optimization level for rhino engine, ranges from -1 to 9
    int optLevel = 0;

    // language version - default to ES6
    int languageVersion = 200;

    // debugger/tracer flags
    boolean hasDebugger = false;
    boolean hasTracer = false;
    boolean hasProfiler = false;
    private boolean isInitialized = false;

    // dynamic portion of the type check sleep that grows
    // as the app remains unchanged
    long updateSnooze = 500;

    /**
     *  Create a Rhino evaluator for the given application and request evaluator.
     */
    public RhinoCore(Application app) {
        this.app = app;
        wrappercache = new WeakCacheMap(500);
        prototypes = new Hashtable();
        contextFactory = new HelmaContextFactory();
        contextFactory.initApplicationClassLoader(app.getClassLoader());
    }

    /**
     *  Initialize the evaluator, making sure the minimum type information
     *  necessary to bootstrap the rest is parsed.
     */
    protected synchronized void initialize() {

        hasDebugger = "true".equalsIgnoreCase(app.getProperty("rhino.debug"));
        hasTracer = "true".equalsIgnoreCase(app.getProperty("rhino.trace"));
        hasProfiler = "true".equalsIgnoreCase(app.getProperty("rhino.profile"));

        // Set default optimization level according to whether debugger is on
        if (hasDebugger || hasTracer || hasProfiler) {
            optLevel = -1;
        } else {
            String opt = app.getProperty("rhino.optlevel");
            if (opt != null) {
                try {
                    optLevel = Integer.parseInt(opt);
                } catch (Exception ignore) {
                    app.logError("Invalid rhino optlevel: " + opt);
                }
            }
        }
        String v = app.getProperty("rhino.languageVersion");
        if (v != null) {
            try {
                languageVersion = Integer.parseInt(v);
            } catch (Exception ignore) {
                app.logError("Invalid rhino.languageVersion: " + v);
            }
        }
        wrapper = new WrapMaker();
        wrapper.setJavaPrimitiveWrap(false);

        Context context = contextFactory.enterContext();

        try {
            // create global object
            global = new GlobalObject(this, app, false);
            // call the initStandardsObject in ImporterTopLevel so that
            // importClass() and importPackage() are set up.
            global.initStandardObjects(context, false);
            global.init();

            // Enable loading and exporting of CommonJS modules with require and module.exports, resp.
            // Inspiration: http://stackoverflow.com/a/30355409/5281580

            List<URI> commonJsPaths = new ArrayList<URI>();
            commonJsPaths.add(app.getAppDir().toURI());
            String commonJsAppPath = app.getProperty("commonjs.dir");

            if (commonJsAppPath != null) {
                File commonJsAppDir = new File(app.getAppDir(), commonJsAppPath);
                if (commonJsAppDir.isDirectory()) {
                    commonJsPaths.add(commonJsAppDir.toURI());
                }
            }

            // install the global require() function using our custom modules provider, so that
            // CommonJS-style as well as NodeJS-style modules can be required
            new RequireBuilder()
                .setModuleScriptProvider(new CompiledOrInterpretedModuleScriptProvider(
                        new NodeModulesProvider(commonJsPaths, null)))
                .setSandboxed(true)
                .createRequire(context, global)
                .install(global);

            pathProto = new PathWrapper(this);

            hopObjectProto =  HopObject.init(this);
            // use lazy loaded constructors for all extension objects that
            // adhere to the ScriptableObject.defineClass() protocol
            new LazilyLoadedCtor(global, "File",
                    "helma.scripting.rhino.extensions.FileObject", false);
            new LazilyLoadedCtor(global, "Ftp",
                    "helma.scripting.rhino.extensions.FtpObject", false);
            new LazilyLoadedCtor(global, "Image",
                    "helma.scripting.rhino.extensions.ImageObject", false);
            new LazilyLoadedCtor(global, "Remote",
                    "helma.scripting.rhino.extensions.XmlRpcObject", false);
            MailObject.init(global, app.getProperties());
            JSAdapter.init(context, global, false);

            // add some convenience functions to string, date and number prototypes
            Scriptable stringProto = ScriptableObject.getClassPrototype(global, "String");
            stringProto.put("trim", stringProto, new StringTrim());

            Scriptable dateProto = ScriptableObject.getClassPrototype(global, "Date");
            dateProto.put("format", dateProto, new DateFormat());

            Scriptable numberProto = ScriptableObject.getClassPrototype(global, "Number");
            numberProto.put("format", numberProto, new NumberFormat());

            Collection protos = app.getPrototypes();
            for (Iterator i = protos.iterator(); i.hasNext();) {
                Prototype proto = (Prototype) i.next();
                initPrototype(proto);
            }

            // always fully initialize global prototype, because
            // we always need it and there's no chance to trigger
            // creation on demand.
            getPrototype("global");

        } catch (Exception e) {
            app.logError("Cannot initialize interpreter", e);
            throw new RuntimeException(e.getMessage(), e);
        } finally {
            Context.exit();
            isInitialized = true;
        }
    }

    boolean isInitialized() {
        return isInitialized;
    }

    public void shutdown() {
        if (debugger != null) {
            debugger.dispose();
            debugger = null;
        }
    }

    void initDebugger(Context context) {
        context.setGeneratingDebug(true);
        try {
            if (debugger == null) {
                debugger = new HelmaDebugger(app.getName());
                debugger.setScopeProvider(this);
                debugger.attachTo(contextFactory);
            }
        } catch (Exception x) {
            app.logError("Error setting up debugger", x);
        }
    }

    /**
     *   Initialize a prototype info without compiling its script files.
     *
     *  @param prototype the prototype to be created
     */
    protected synchronized TypeInfo initPrototype(Prototype prototype) {

        String name = prototype.getName();
        String lowerCaseName = prototype.getLowerCaseName();

        TypeInfo type = (TypeInfo) prototypes.get(lowerCaseName);

        // check if the prototype info exists already
        ScriptableObject op = (type == null) ? null : type.objProto;

        // if prototype info doesn't exist (i.e. is a standard prototype
        // built by HopExtension), create it.
        if (op == null) {
            if ("global".equals(lowerCaseName)) {
                op = global;
            } else if ("hopobject".equals(lowerCaseName)) {
                op = hopObjectProto;
            } else {
                op = new HopObject(name, this);
            }
            type = registerPrototype(prototype, op);
        }

        // Register a constructor for all types except global.
        // This will first create a new prototyped HopObject and then calls
        // the actual (scripted) constructor on it.
        if (!"global".equals(lowerCaseName)) {
            try {
                new HopObjectCtor(name, this, op);
                op.setParentScope(global);
            } catch (Exception x) {
                app.logError("Error adding ctor for " + name,  x);
            }
        }

        return type;
    }

    /**
     *  Set up a prototype, parsing and compiling all its script files.
     *
     *  @param type the info, containing the object proto, last update time and
     *         the set of compiled functions properties
     */
    private synchronized void evaluatePrototype(final TypeInfo type) {

        type.prepareCompilation();
        final Prototype prototype = type.frameworkProto;

        // set the parent prototype in case it hasn't been done before
        // or it has changed...
        setParentPrototype(prototype, type);

        type.error = null;
        if ("global".equals(prototype.getLowerCaseName())) {
            globalError = null;
        }

        contextFactory.call(new ContextAction() {
            public Object run(Context cx) {
                // loop through the prototype's code elements and evaluate them
                Iterator code = prototype.getCodeResources();
                while (code.hasNext()) {
                    evaluate(cx, type, (Resource) code.next());
                }
                return null;
            }
        });
        type.commitCompilation();
    }

    /**
     *  Set the parent prototype on the ObjectPrototype.
     *
     *  @param prototype the prototype spec
     *  @param type the prototype object info
     */
    protected void setParentPrototype(Prototype prototype, TypeInfo type) {
        String name = prototype.getName();
        String lowerCaseName = prototype.getLowerCaseName();

        if (!"global".equals(lowerCaseName) && !"hopobject".equals(lowerCaseName)) {

            // get the prototype's prototype if possible and necessary
            TypeInfo parentType = null;
            Prototype parent = prototype.getParentPrototype();

            if (parent != null) {
                // see if parent prototype is already registered. if not, register it
                parentType = getPrototypeInfo(parent.getName());
            }

            if (parentType == null && !app.isJavaPrototype(name)) {
                // FIXME: does this ever occur?
                parentType = getPrototypeInfo("hopobject");
            }

            type.setParentType(parentType);
        }
    }

    /**
     *  This method is called before an execution context is entered to let the
     *  engine know it should update its prototype information. The update policy
     *  here is to check for update those prototypes which already have been compiled
     *  before. Others will be updated/compiled on demand.
     */
    public synchronized void updatePrototypes() throws IOException {
        if ((System.currentTimeMillis() - lastUpdate) < 1000L + updateSnooze) {
            return;
        }

        // init prototypes and/or update prototype checksums
        app.typemgr.checkPrototypes();

        // get a collection of all prototypes (code directories)
        Collection protos = app.getPrototypes();

        // in order to respect inter-prototype dependencies, we try to update
        // the global prototype before all other prototypes, and parent
        // prototypes before their descendants.

        HashSet checked = new HashSet(protos.size() * 2);

        TypeInfo type = (TypeInfo) prototypes.get("global");

        if (type != null) {
            updatePrototype(type, checked);
        }

        for (Iterator i = protos.iterator(); i.hasNext();) {
            Prototype proto = (Prototype) i.next();

            if (checked.contains(proto)) {
                continue;
            }

            type = (TypeInfo) prototypes.get(proto.getLowerCaseName());

            if (type == null) {
                // a prototype we don't know anything about yet. Init local update info.
                initPrototype(proto);
            } else if (type.lastUpdate > -1) {
                // only need to update prototype if it has already been initialized.
                // otherwise, this will be done on demand.
                updatePrototype(type, checked);
            }
        }

        lastUpdate = System.currentTimeMillis();
        // max updateSnooze is 4 seconds, reached after 66.6 idle minutes
        long newSnooze = (lastUpdate - app.typemgr.getLastCodeUpdate()) / 1000;
        updateSnooze = Math.min(4000, Math.max(0, newSnooze));
    }

    /**
     * Check one prototype for updates. Used by <code>upatePrototypes()</code>.
     *
     * @param type the type info to check
     * @param checked a set of prototypes that have already been checked
     */
    private void updatePrototype(TypeInfo type, HashSet checked) {
        // first, remember prototype as updated
        checked.add(type.frameworkProto);

        if (type.parentType != null &&
                !checked.contains(type.parentType.frameworkProto)) {
            updatePrototype(type.getParentType(), checked);
        }

        // let the prototype check if its resources have changed
        type.frameworkProto.checkForUpdates();

        // and re-evaluate if necessary
        if (type.needsUpdate()) {
            evaluatePrototype(type);
        }
    }

    /**
     * A version of getPrototype() that retrieves a prototype and checks
     * if it is valid, i.e. there were no errors when compiling it. If
     * invalid, a ScriptingException is thrown.
     */
    public Scriptable getValidPrototype(String protoName) {
        if (globalError != null) {
            throw new EvaluatorException(globalError);
        }
        TypeInfo type = getPrototypeInfo(protoName);
        if (type != null) {
            if (type.hasError()) {
                throw new EvaluatorException(type.getError());
            }
            return type.objProto;
        }
        return null;
    }

    /**
     *  Get the object prototype for a prototype name and initialize/update it
     *  if necessary. The policy here is to update the prototype only if it
     *  hasn't been updated before, otherwise we assume it already was updated
     *  by updatePrototypes(), which is called for each request.
     */
    public Scriptable getPrototype(String protoName) {
        TypeInfo type = getPrototypeInfo(protoName);
        return type == null ? null : type.objProto;
    }

    /**
     * Get an array containing the property ids of all properties that were
     * compiled from scripts for the given prototype.
     *
     * @param protoName the name of the prototype
     * @return an array containing all compiled properties of the given prototype
     */
    public Map getPrototypeProperties(String protoName) {
        TypeInfo type = getPrototypeInfo(protoName);
        SystemMap map = new SystemMap();
        Iterator it = type.compiledProperties.iterator();
        while(it.hasNext()) {
            Object key = it.next();
            if (key instanceof String)
                map.put(key, type.objProto.get((String) key, type.objProto));
        }
        return map;
    }

    /**
     *  Private helper function that retrieves a prototype's TypeInfo
     *  and creates it if not yet created. This is used by getPrototype() and
     *  getValidPrototype().
     */
    private TypeInfo getPrototypeInfo(String protoName) {
        if (protoName == null) {
            return null;
        }

        TypeInfo type = (TypeInfo) prototypes.get(protoName.toLowerCase());

        // if type exists and hasn't been evaluated (used) yet, evaluate it now.
        // otherwise, it has already been evaluated for this request by updatePrototypes(),
        // which is called before a request is handled.
        if ((type != null) && (type.lastUpdate == -1)) {
            type.frameworkProto.checkForUpdates();

            if (type.needsUpdate()) {
                evaluatePrototype(type);
            }
        }

        return type;
    }

    /**
     * Register an object prototype for a prototype name.
     */
    private TypeInfo registerPrototype(Prototype proto, ScriptableObject op) {
        TypeInfo type = new TypeInfo(proto, op);
        prototypes.put(proto.getLowerCaseName(), type);
        return type;
    }

    /**
    * Check if an object has a function property (public method if it
    * is a java object) with that name.
    */
    public boolean hasFunction(String protoname, String fname) {
        // throws EvaluatorException if type has a syntax error
        Scriptable op = getValidPrototype(protoname);

        // if this is an untyped object return false
        if (op == null) {
            return false;
        }

        return ScriptableObject.getProperty(op, fname) instanceof Function;
    }

    /**
     *  Convert an input argument from Java to the scripting runtime
     *  representation.
     */
    public Object processXmlRpcArgument (Object arg) {
        if (arg == null)
            return null;
        if (arg instanceof Vector) {
            Vector v = (Vector) arg;
            Object[] a = v.toArray();
            for (int i=0; i<a.length; i++) {
                a[i] = processXmlRpcArgument(a[i]);
            }
            return Context.getCurrentContext().newArray(global, a);
        }
        if (arg instanceof Hashtable) {
            Hashtable t = (Hashtable) arg;
            for (Enumeration e=t.keys(); e.hasMoreElements(); ) {
                Object key = e.nextElement();
                t.put(key, processXmlRpcArgument(t.get(key)));
            }
            return Context.toObject(new SystemMap(t), global);
        }
        if (arg instanceof String)
            return arg;
        if (arg instanceof Number)
            return arg;
        if (arg instanceof Boolean)
            return arg;
        if (arg instanceof Date) {
            Date d = (Date) arg;
            Object[] args = { new Long(d.getTime()) };
            return Context.getCurrentContext().newObject(global, "Date", args);
        }
        return Context.toObject(arg, global);
    }

    /**
     * convert a JavaScript Object object to a generic Java object stucture.
     */
    public Object processXmlRpcResponse (Object arg) {
        // unwrap if argument is a Wrapper
        if (arg instanceof Wrapper) {
            arg = ((Wrapper) arg).unwrap();
        }
        if (arg instanceof NativeObject) {
            NativeObject no = (NativeObject) arg;
            Object[] ids = no.getIds();
            Hashtable ht = new Hashtable(ids.length*2);
            for (int i=0; i<ids.length; i++) {
                if (ids[i] instanceof String) {
                    String key = (String) ids[i];
                    Object o = no.get(key, no);
                    if (o != null) {
                        ht.put(key, processXmlRpcResponse(o));
                    }
                }
            }
            return ht;
        } else if (arg instanceof NativeArray) {
            NativeArray na = (NativeArray) arg;
            Number n = (Number) na.get("length", na);
            int l = n.intValue();
            Vector retval = new Vector(l);
            for (int i=0; i<l; i++) {
                retval.add(i, processXmlRpcResponse(na.get(i, na)));
            }
            return retval;
        } else if (arg instanceof Map) {
            Map map = (Map) arg;
            Hashtable ht = new Hashtable(map.size()*2);
            for (Iterator it=map.entrySet().iterator(); it.hasNext();) {
                Map.Entry entry = (Map.Entry) it.next();
                ht.put(entry.getKey().toString(),
                       processXmlRpcResponse(entry.getValue()));
            }
            return ht;
        } else if (arg instanceof Number) {
            Number n = (Number) arg;
            if (arg instanceof Float || arg instanceof Long) {
                return new Double(n.doubleValue());
            } else if (!(arg instanceof Double)) {
                return new Integer(n.intValue());
            }
        } else if (arg instanceof INode) {
            // interpret HopObject as object/dict
            INode n = (INode) arg;
            Hashtable ht = new Hashtable();
            Enumeration props = n.properties();
            while (props.hasMoreElements()) {
                String key = (String) props.nextElement();
                IProperty prop = n.get(key);
                if (prop != null) {
                    ht.put(key, processXmlRpcResponse(prop.getValue()));
                }
            }
            return ht;
        } else if (arg instanceof Scriptable) {
            Scriptable s = (Scriptable) arg;
            if ("Date".equals(s.getClassName())) {
                return new Date((long) ScriptRuntime.toNumber(s));
            }
        }
        return arg;
    }


    /**
     * Return the application we're running in
     */
    public Application getApplication() {
        return app;
    }


    /**
     *  Get a Script wrapper for any given object. If the object implements the IPathElement
     *  interface, the getPrototype method will be used to retrieve the name of the prototype
     * to use. Otherwise, a Java-Class-to-Script-Prototype mapping is consulted.
     */
    public Scriptable getElementWrapper(Object e) {
        WeakReference ref = (WeakReference) wrappercache.get(e);
        Wrapper wrapper = ref == null ? null : (Wrapper) ref.get();

        if (wrapper == null || wrapper.unwrap() != e) {
            // Gotta find out the prototype name to use for this object...
            String prototypeName = app.getPrototypeName(e);
            Scriptable op = getPrototype(prototypeName);

            if (op == null) {
                // no prototype found, return an unscripted wrapper
                wrapper = new NativeJavaObject(global, e, e.getClass());
            } else {
                wrapper = new JavaObject(global, e, prototypeName, op, this);
            }

            wrappercache.put(e, new WeakReference(wrapper));
        }

        return (Scriptable) wrapper;
    }

    /**
     *  Get a script wrapper for an instance of helma.objectmodel.INode
     */
    public Scriptable getNodeWrapper(INode node) {
        if (node == null) {
            return null;
        }

        HopObject hobj = (HopObject) wrappercache.get(node);

        if (hobj == null) {
            String protoname = node.getPrototype();
            Scriptable op = getValidPrototype(protoname);

            // no prototype found for this node
            if (op == null) {
                // maybe this object has a prototype name that has been
                // deleted, but the storage layer was able to set a
                // DbMapping matching the relational table the object
                // was fetched from.
                DbMapping dbmap = node.getDbMapping();
                if (dbmap != null && (protoname = dbmap.getTypeName()) != null) {
                    op = getValidPrototype(protoname);
                }

                // if not found, fall back to HopObject prototype
                if (op == null) {
                    protoname = "HopObject";
                    op = getValidPrototype("HopObject");
                }
            }

            hobj = new HopObject(protoname, this, node, op);
            wrappercache.put(node, hobj);
        }

        return hobj;
    }

    /**
     * Get a node wrapper for a node that may not have been fetched yet
     * @param handle a node handle
     * @return a wrapper for the node
     */
    public Scriptable getNodeWrapper(NodeHandle handle) {
        Scriptable hobj = (HopObject) wrappercache.get(handle);
        if (hobj != null) {
            return hobj;
        } else if (handle.hasNode()) {
            hobj = getNodeWrapper(handle.getNode(app.getWrappedNodeManager()));
        }

        if (hobj == null) {
            String protoName = handle.getKey().getStorageName();
            Scriptable op = getValidPrototype(protoName);

            // no prototype found for this node
            if (op == null) {
                protoName = "HopObject";
                op = getValidPrototype("HopObject");
            }
            hobj = new HopObject(protoName, this, handle, op);
        }
        wrappercache.put(handle, hobj);
        return hobj;
    }


    protected String postProcessHref(Object obj, String protoName, String href)
            throws UnsupportedEncodingException, IOException {
        // check if the app.properties specify a href-function to post-process the
        // basic href.
        String hrefFunction = app.getProperty("hrefFunction", null);

        if (hrefFunction != null) {

            Object handler = obj;
            String proto = protoName;

            while (handler != null) {
                if (hasFunction(proto, hrefFunction)) {

                    // get the currently active rhino engine and invoke the function
                    RhinoEngine eng = RhinoEngine.getRhinoEngine();
                    Object result;

                    try {
                        result = eng.invoke(handler, hrefFunction,
                                               new Object[] {href},
                                               ScriptingEngine.ARGS_WRAP_DEFAULT,
                                               false);
                    } catch (ScriptingException x) {
                        throw new EvaluatorException("Error in hrefFunction: " + x);
                    }

                    if (result == null) {
                        throw new EvaluatorException("hrefFunction " + hrefFunction +
                                                       " returned null");
                    }

                    href = result.toString();
                    break;
                }
                handler = app.getParentElement(handler);
                proto = app.getPrototypeName(handler);

            }
        }

        // check if the app.properties specify a href-skin to post-process the
        // basic href.
        String hrefSkin = app.getProperty("hrefSkin", null);

        if (hrefSkin != null) {
            // we need to post-process the href with a skin for this application
            // first, look in the object href was called on.
            Skin skin = null;
            Object handler = obj;
            // get the currently active rhino engine and render the skin
            RhinoEngine eng = RhinoEngine.getRhinoEngine();

            while (handler != null) {
                Prototype proto = app.getPrototype(handler);

                if (proto != null) {
                    skin = eng.getSkin(proto.getName(), hrefSkin);
                }

                if (skin != null) {
                    Scriptable param = Context.getCurrentContext().newObject(global);
                    param.put("path", param, href);
                    href = skin.renderAsString(eng.getRequestEvaluator(), handler, param).trim();
                    break;
                }

                handler = app.getParentElement(handler);
            }
        }

        return href;
    }


    Properties scriptableToProperties(Scriptable obj) {
        Object[] ids = obj.getIds();
        Properties props = new ResourceProperties(app, null, null, true);
        for (int i = 0; i < ids.length; i++) {
            // we ignore non-string keys
            if (ids[i] instanceof String) {
                String key = (String) ids[i];
                Object value = obj.get(key, obj);
                // Normalize values to either null, string, or nested map
                if (value == Undefined.instance || value == Scriptable.NOT_FOUND) {
                    value = null;
                } else if (value instanceof Scriptable) {
                    value = scriptableToProperties((Scriptable) value);
                } else {
                    value = ScriptRuntime.toString(value);
                }
                props.put(key, value);
            }
        }
        return props;
    }

    /**
     * Get the RhinoCore instance associated with the current thread, or null
     * @return the RhinoCore instance associated with the current thread
     */
    public static RhinoCore getCore() {
        RhinoEngine eng = RhinoEngine.getRhinoEngine();
        return eng != null ? eng.core : null;
    }

    /////////////////////////////////////////////
    // skin related methods
    /////////////////////////////////////////////

    protected static Object[] unwrapSkinpath(Object[] skinpath) {
        if (skinpath != null) {
            for (int i=0; i<skinpath.length; i++) {
                if (skinpath[i] instanceof HopObject) {
                    skinpath[i] = ((HopObject) skinpath[i]).getNode();
                } else if (skinpath[i] instanceof Wrapper) {
                    skinpath[i] = ((Wrapper) skinpath[i]).unwrap();
                }
            }
        }
        return skinpath;
    }

    /**
     * Add a code resource to a given prototype by immediately compiling and evaluating it.
     *
     * @param typename the type this resource belongs to
     * @param code a code resource
     */
    public void injectCodeResource(String typename, final Resource code) {
        final TypeInfo type = (TypeInfo) prototypes.get(typename.toLowerCase());
        if (type == null || type.lastUpdate == -1)
            return;
        contextFactory.call(new ContextAction() {
            public Object run(Context cx) {
                evaluate(cx, type, code);
                return null;
            }
        });
    }

    ////////////////////////////////////////////////
    // private evaluation/compilation methods
    ////////////////////////////////////////////////
    private synchronized void evaluate(Context cx, TypeInfo type, Resource code) {
        String sourceName = code.getName();
        Reader reader = null;

        Resource previousCurrentResource = app.getCurrentCodeResource();
        app.setCurrentCodeResource(code);

        String encoding = app.getProperty("sourceCharset");

        try {
            Scriptable op = type.objProto;
            // do the update, evaluating the file
            if (sourceName.endsWith(".js")) {
                reader = encoding == null ?
                        new InputStreamReader(code.getInputStream()) :
                        new InputStreamReader(code.getInputStream(), encoding);
                cx.evaluateReader(op, reader, sourceName, 1, null);
            } else if (sourceName.endsWith(".hac")) {
                reader = new StringReader(HacHspConverter.convertHac(code, encoding));
                cx.evaluateReader(op, reader, sourceName, 0, null);
            } else if (sourceName.endsWith(".hsp")) {
                reader = new StringReader(HacHspConverter.convertHsp(code, encoding));
                cx.evaluateReader(op, reader, sourceName, 0, null);
                reader = new StringReader(HacHspConverter.convertHspAsString(code, encoding));
                cx.evaluateReader(op, reader, sourceName, 0, null);
            }

        } catch (Exception e) {
            ScriptingException sx = new ScriptingException(e.getMessage(), e);
            app.logError("Error parsing file " + sourceName, sx);
            // mark prototype as broken
            if (type.error == null) {
                type.error = e.getMessage();
                if (type.error == null) {
                    type.error = e.toString();
                }
                if ("global".equals(type.frameworkProto.getLowerCaseName())) {
                    globalError = type.error;
                }
                wrappercache.clear();
            }
        } finally {
            app.setCurrentCodeResource(previousCurrentResource);
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ignore) {
                    // shouldn't happen
                }
            }
        }
    }

    /**
     *  Return the global scope of this RhinoCore.
     */
    public Scriptable getScope() {
        return global;
    }

    /**
     *  TypeInfo helper class
     */
    class TypeInfo {

        // the framework prototype object
        Prototype frameworkProto;

        // the JavaScript prototype for this type
        ScriptableObject objProto;

        // timestamp of last update. This is -1 so even an empty prototype directory
        // (with lastUpdate == 0) gets evaluated at least once, which is necessary
        // to get the prototype chain set.
        long lastUpdate = -1;

        // the parent prototype info
        TypeInfo parentType;

        // a set of property keys that were in script compilation.
        // Used to decide which properties should be removed if not renewed.
        Set compiledProperties;

        // a set of property keys that were present before first script compilation
        final Set predefinedProperties;

        String error;

        public TypeInfo(Prototype proto, ScriptableObject op) {
            frameworkProto = proto;
            objProto = op;
            // remember properties already defined on this object prototype
            compiledProperties = new HashSet();
            predefinedProperties = new HashSet();
            Object[] keys = op.getAllIds();
            for (int i = 0; i < keys.length; i++) {
                predefinedProperties.add(keys[i].toString());
            }
        }

        /**
         * If prototype implements PropertyRecorder tell it to start
         * registering property puts.
         */
        public void prepareCompilation() {
            if (objProto instanceof PropertyRecorder) {
                ((PropertyRecorder) objProto).startRecording();
            }
            // mark this type as updated so injectCodeResource() knows it's initialized
            lastUpdate = frameworkProto.lastCodeUpdate();
        }

        /**
         * Compilation has been completed successfully - switch over to code
         * from temporary prototype, removing properties that haven't been
         * renewed.
         */
        public void commitCompilation() {
            // loop through properties defined on the prototype object
            // and remove thos properties which haven't been renewed during
            // this compilation/evaluation pass.
            if (objProto instanceof PropertyRecorder) {

                PropertyRecorder recorder = (PropertyRecorder) objProto;

                recorder.stopRecording();
                Set changedProperties = recorder.getChangeSet();

                if (changedProperties != null) {
                    recorder.clearChangeSet();

                    // ignore all  properties that were defined before we started
                    // compilation. We won't manage these properties, even
                    // if they were set during compilation.
                    changedProperties.removeAll(predefinedProperties);

                    // remove all renewed properties from the previously compiled
                    // property names so we can remove those properties that were not
                    // renewed in this compilation
                    compiledProperties.removeAll(changedProperties);

                    boolean isGlobal = "global".equals(frameworkProto.getLowerCaseName());

                    Iterator it = compiledProperties.iterator();
                    while (it.hasNext()) {
                        String key = (String) it.next();
                        if (isGlobal && (prototypes.containsKey(key.toLowerCase())
                                || "JavaPackage".equals(key))) {
                            // avoid removing HopObject constructor
                            predefinedProperties.add(key);
                            continue;
                        }
                        try {
                            objProto.setAttributes(key, 0);
                            objProto.delete(key);
                        } catch (Exception px) {
                            app.logEvent("Error unsetting property "+key+" on "+
                                    frameworkProto.getName());
                        }
                    }

                    // update compiled properties
                    compiledProperties = changedProperties;
                }
            }

            // mark this type as updated again so it reflects
            // resources added during compilation
            // lastUpdate = frameworkProto.lastCodeUpdate();

            // If this prototype defines a postCompile() function, call it
            Context cx = Context.getCurrentContext();
            try {
                Object fObj = ScriptableObject.getProperty(objProto,
                                                           "onCodeUpdate");
                if (fObj instanceof Function) {
                    Object[] args = {frameworkProto.getName()};
                    ((Function) fObj).call(cx, global, objProto, args);
                }
            } catch (Exception x) {
                app.logError("Exception in "+frameworkProto.getName()+
                             ".onCodeUpdate(): " + x, x);
            }
        }

        public boolean needsUpdate() {
            return frameworkProto.lastCodeUpdate() > lastUpdate;
        }

        public void setParentType(TypeInfo type) {
            parentType = type;
            if (type == null) {
                objProto.setPrototype(null);
            } else {
                objProto.setPrototype(type.objProto);
            }
        }

        public TypeInfo getParentType() {
            return parentType;
        }

        public boolean hasError() {
            TypeInfo p = this;
            while (p != null) {
                if (p.error != null)
                    return true;
                p = p.parentType;
            }
            return false;
        }

        public String getError() {
            TypeInfo p = this;
            while (p != null) {
                if (p.error != null)
                    return p.error;
                p = p.parentType;
            }
            return null;
        }

        public String toString() {
            return ("TypeInfo[" + frameworkProto + "," + new Date(lastUpdate) + "]");
        }
    }

    /**
     *  Object wrapper class
     */
    class WrapMaker extends WrapFactory {

        public Object wrap(Context cx, Scriptable scope, Object obj, Class staticType) {
            // taking a shortcut here on things normally defined by setJavaPrimitivesWrap()
            if (obj == null || obj == Undefined.instance
                    || obj instanceof Scriptable || obj instanceof String
                    || obj instanceof Number || obj instanceof Boolean) {
                return obj;
            }
            // Wrap Nodes
            if (obj instanceof INode) {
                return getNodeWrapper((INode) obj);
            }
            if (obj instanceof NodeHandle) {
                return getNodeWrapper((NodeHandle) obj);
            }

            // Masquerade SystemMap and WrappedMap as native JavaScript objects
            if (obj instanceof SystemMap || obj instanceof WrappedMap) {
                return new MapWrapper((Map) obj, RhinoCore.this);
            }

            // Convert java.util.Date objects to JavaScript Dates
            if (obj instanceof Date) {
                Object[] args = { new Long(((Date) obj).getTime()) };
                try {
                    return cx.newObject(global, "Date", args);
                 } catch (JavaScriptException nafx) {
                    return obj;
                }
            }

            // Wrap scripted Java objects
            if (obj != null && app.getPrototypeName(obj) != null) {
                return getElementWrapper(obj);
            }

            return super.wrap(cx, scope, obj, staticType);
        }

        public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) {
            if (obj instanceof Scriptable) {
                return (Scriptable) obj;
            }
            if (obj instanceof INode) {
                return getNodeWrapper((INode) obj);
            }

            if (obj != null && app.getPrototypeName(obj) != null) {
                return getElementWrapper(obj);
            }

            return super.wrapNewObject(cx, scope, obj);
        }
    }

    class StringTrim extends BaseFunction {
        private static final long serialVersionUID = -1515630068911501925L;

        public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
            String str = thisObj.toString();
            return str.trim();
        }
    }

    class DateFormat extends BaseFunction {
        private static final long serialVersionUID = 4694440247686532087L;

        public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
            Date date = new Date((long) ScriptRuntime.toNumber(thisObj));
            SimpleDateFormat df;

            if (args.length > 0 && args[0] != Undefined.instance && args[0] != null) {
                if (args.length > 1 && args[1] instanceof NativeJavaObject) {
                    Object locale = ((NativeJavaObject) args[1]).unwrap();
                    if (locale instanceof Locale) {
                        df = new SimpleDateFormat(args[0].toString(), (Locale) locale);
                    } else {
                        throw new IllegalArgumentException("Second argument to Date.format() not a java.util.Locale: " +
                                                            locale.getClass());
                    }
                } else {
                    df = new SimpleDateFormat(args[0].toString());
                }
            } else {
                df = new SimpleDateFormat();
            }
            return df.format(date);
        }
    }

    class NumberFormat extends BaseFunction {
        private static final long serialVersionUID = -6999409297243210875L;

        public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
            DecimalFormat df;
            if (args.length > 0 && args[0] != Undefined.instance) {
                df = new DecimalFormat(args[0].toString());
            } else {
                df = new DecimalFormat("#,##0.00");
            }
            return df.format(ScriptRuntime.toNumber(thisObj));
        }
    }

    class HelmaContextFactory extends ContextFactory {

        final boolean strictVars = "true".equalsIgnoreCase(app.getProperty("strictVars"));

        protected void onContextCreated(Context cx) {
            cx.setWrapFactory(wrapper);
            cx.setOptimizationLevel(optLevel);
            cx.setInstructionObserverThreshold(10000);
            if (Context.isValidLanguageVersion(languageVersion)) {
                cx.setLanguageVersion(languageVersion);
            } else {
                app.logError("Unsupported rhino.languageVersion: " + languageVersion);
            }

            // Set up visual debugger if rhino.debug = true
            if (hasDebugger)
                initDebugger(cx);
            super.onContextCreated(cx);
        }

        protected boolean hasFeature(Context cx, int featureIndex) {
            switch (featureIndex) {
                case Context.FEATURE_DYNAMIC_SCOPE:
                    return true;

                case Context.FEATURE_STRICT_VARS:
                case Context.FEATURE_WARNING_AS_ERROR:
                    return strictVars;

                default:
                    return super.hasFeature(cx, featureIndex);
            }
        }

        /**
         * Implementation of
         * {@link Context#observeInstructionCount(int instructionCount)}.
         * This can be used to customize {@link Context} without introducing
         * additional subclasses.
         */
        protected void observeInstructionCount(Context cx, int instructionCount) {
            RhinoEngine engine = RhinoEngine.getRhinoEngine();
            if (engine != null && engine.thread != Thread.currentThread()) {
                throw new EvaluatorException("Request timed out");
            }
        }
    }
}
