/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.objects;

import static jdk.nashorn.internal.lookup.Lookup.MH;
import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import jdk.internal.dynalink.linker.GuardedInvocation;
import jdk.internal.dynalink.linker.LinkRequest;
import jdk.nashorn.internal.objects.annotations.Attribute;
import jdk.nashorn.internal.objects.annotations.Property;
import jdk.nashorn.internal.objects.annotations.ScriptClass;
import jdk.nashorn.internal.runtime.ConsString;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.GlobalFunctions;
import jdk.nashorn.internal.runtime.GlobalObject;
import jdk.nashorn.internal.runtime.JSType;
import jdk.nashorn.internal.runtime.NativeJavaPackage;
import jdk.nashorn.internal.runtime.PropertyDescriptor;
import jdk.nashorn.internal.runtime.PropertyMap;
import jdk.nashorn.internal.runtime.Scope;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.ScriptFunction;
import jdk.nashorn.internal.runtime.ScriptObject;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import jdk.nashorn.internal.runtime.ScriptingFunctions;
import jdk.nashorn.internal.runtime.Source;
import jdk.nashorn.internal.runtime.arrays.ArrayData;
import jdk.nashorn.internal.runtime.linker.Bootstrap;
import jdk.nashorn.internal.runtime.linker.InvokeByName;
import jdk.nashorn.internal.runtime.regexp.RegExpResult;
import jdk.nashorn.internal.scripts.JO;

/**
 * Representation of global scope.
 */
@ScriptClass("Global")
public final class Global extends ScriptObject implements GlobalObject, Scope {
    private final InvokeByName TO_STRING = new InvokeByName("toString", ScriptObject.class);
    private final InvokeByName VALUE_OF  = new InvokeByName("valueOf",  ScriptObject.class);

    /** ECMA 15.1.2.2 parseInt (string , radix) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object parseInt;

    /** ECMA 15.1.2.3 parseFloat (string) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object parseFloat;

    /** ECMA 15.1.2.4 isNaN (number) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object isNaN;

    /** ECMA 15.1.2.5 isFinite (number) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object isFinite;

    /** ECMA 15.1.3.3 encodeURI */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object encodeURI;

    /** ECMA 15.1.3.4 encodeURIComponent */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object encodeURIComponent;

    /** ECMA 15.1.3.1 decodeURI */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object decodeURI;

    /** ECMA 15.1.3.2 decodeURIComponent */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object decodeURIComponent;

    /** ECMA B.2.1 escape (string) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object escape;

    /** ECMA B.2.2 unescape (string) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object unescape;

    /** Nashorn extension: global.print */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object print;

    /** Nashorn extension: global.load */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object load;

    /** Nashorn extension: global.loadWithNewGlobal */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object loadWithNewGlobal;

    /** Nashorn extension: global.exit */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object exit;

    /** Nashorn extension: global.quit */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object quit;

    /** Value property NaN of the Global Object - ECMA 15.1.1.1 NaN */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public final Object NaN = Double.NaN;

    /** Value property Infinity of the Global Object - ECMA 15.1.1.2 Infinity */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public final Object Infinity = Double.POSITIVE_INFINITY;

    /** Value property Undefined of the Global Object - ECMA 15.1.1.3 Undefined */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public final Object undefined = UNDEFINED;

    /** ECMA 15.1.2.1 eval(x) */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public Object eval;

    /** ECMA 15.1.4.1 Object constructor. */
    @Property(name = "Object", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object object;

    /** ECMA 15.1.4.2 Function constructor. */
    @Property(name = "Function", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object function;

    /** ECMA 15.1.4.3 Array constructor. */
    @Property(name = "Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object array;

    /** ECMA 15.1.4.4 String constructor */
    @Property(name = "String", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object string;

    /** ECMA 15.1.4.5 Boolean constructor */
    @Property(name = "Boolean", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object _boolean;

    /** ECMA 15.1.4.6 - Number constructor */
    @Property(name = "Number", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object number;

    /** ECMA 15.1.4.7 Date constructor */
    @Property(name = "Date", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object date;

    /** ECMA 15.1.4.8 RegExp constructor */
    @Property(name = "RegExp", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object regexp;

    /** ECMA 15.12 - The JSON object */
    @Property(name = "JSON", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object json;

    /** Nashorn extension: global.JSAdapter */
    @Property(name = "JSAdapter", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object jsadapter;

    /** ECMA 15.8 - The Math object */
    @Property(name = "Math", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object math;

    /** Error object */
    @Property(name = "Error", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object error;

    /** EvalError object */
    @Property(name = "EvalError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object evalError;

    /** RangeError object */
    @Property(name = "RangeError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object rangeError;

    /** ReferenceError object */
    @Property(name = "ReferenceError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object referenceError;

    /** SyntaxError object */
    @Property(name = "SyntaxError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object syntaxError;

    /** TypeError object */
    @Property(name = "TypeError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object typeError;

    /** URIError object */
    @Property(name = "URIError", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object uriError;

    /** ArrayBuffer object */
    @Property(name = "ArrayBuffer", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object arrayBuffer;

    /** TypedArray (int8) */
    @Property(name = "Int8Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object int8Array;

    /** TypedArray (uint8) */
    @Property(name = "Uint8Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object uint8Array;

    /** TypedArray (uint8) - Clamped */
    @Property(name = "Uint8ClampedArray", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object uint8ClampedArray;

    /** TypedArray (int16) */
    @Property(name = "Int16Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object int16Array;

    /** TypedArray (uint16) */
    @Property(name = "Uint16Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object uint16Array;

    /** TypedArray (int32) */
    @Property(name = "Int32Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object int32Array;

    /** TypedArray (uint32) */
    @Property(name = "Uint32Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object uint32Array;

    /** TypedArray (float32) */
    @Property(name = "Float32Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object float32Array;

    /** TypedArray (float64) */
    @Property(name = "Float64Array", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object float64Array;

    /** Nashorn extension: Java access - global.Packages */
    @Property(name = "Packages", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object packages;

    /** Nashorn extension: Java access - global.com */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object com;

    /** Nashorn extension: Java access - global.edu */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object edu;

    /** Nashorn extension: Java access - global.java */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object java;

    /** Nashorn extension: Java access - global.javafx */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object javafx;

    /** Nashorn extension: Java access - global.javax */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object javax;

    /** Nashorn extension: Java access - global.org */
    @Property(attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object org;

    /** Nashorn extension: Java access - global.javaImporter */
    @Property(name = "JavaImporter", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object javaImporter;

    /** Nashorn extension: global.Java Object constructor. */
    @Property(name = "Java", attributes = Attribute.NOT_ENUMERABLE)
    public volatile Object javaApi;

    /** Nashorn extension: current script's file name */
    @Property(name = "__FILE__", attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public Object __FILE__;

    /** Nashorn extension: current script's directory */
    @Property(name = "__DIR__", attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public Object __DIR__;

    /** Nashorn extension: current source line number being executed */
    @Property(name = "__LINE__", attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public Object __LINE__;

    /** Used as Date.prototype's default value */
    public NativeDate   DEFAULT_DATE;

    /** Used as RegExp.prototype's default value */
    public NativeRegExp DEFAULT_REGEXP;

    /*
     * Built-in constructor objects: Even if user changes dynamic values of
     * "Object", "Array" etc., we still want to keep original values of these
     * constructors here. For example, we need to be able to create array,
     * regexp literals even after user overwrites global "Array" or "RegExp"
     * constructor - see also ECMA 262 spec. Annex D.
     */
    private ScriptFunction builtinFunction;
    private ScriptFunction builtinObject;
    private ScriptFunction builtinArray;
    private ScriptFunction builtinBoolean;
    private ScriptFunction builtinDate;
    private ScriptObject   builtinJSON;
    private ScriptFunction builtinJSAdapter;
    private ScriptObject   builtinMath;
    private ScriptFunction builtinNumber;
    private ScriptFunction builtinRegExp;
    private ScriptFunction builtinString;
    private ScriptFunction builtinError;
    private ScriptFunction builtinEval;
    private ScriptFunction builtinEvalError;
    private ScriptFunction builtinRangeError;
    private ScriptFunction builtinReferenceError;
    private ScriptFunction builtinSyntaxError;
    private ScriptFunction builtinTypeError;
    private ScriptFunction builtinURIError;
    private ScriptObject   builtinPackages;
    private ScriptObject   builtinCom;
    private ScriptObject   builtinEdu;
    private ScriptObject   builtinJava;
    private ScriptObject   builtinJavafx;
    private ScriptObject   builtinJavax;
    private ScriptObject   builtinOrg;
    private ScriptObject   builtinJavaImporter;
    private ScriptObject   builtinJavaApi;
    private ScriptObject   builtinArrayBuffer;
    private ScriptObject   builtinInt8Array;
    private ScriptObject   builtinUint8Array;
    private ScriptObject   builtinUint8ClampedArray;
    private ScriptObject   builtinInt16Array;
    private ScriptObject   builtinUint16Array;
    private ScriptObject   builtinInt32Array;
    private ScriptObject   builtinUint32Array;
    private ScriptObject   builtinFloat32Array;
    private ScriptObject   builtinFloat64Array;

    private PropertyMap    accessorPropertyDescriptorMap;
    private PropertyMap    arrayBufferViewMap;
    private PropertyMap    dataPropertyDescriptorMap;
    private PropertyMap    genericPropertyDescriptorMap;
    private PropertyMap    nativeArgumentsMap;
    private PropertyMap    nativeArrayMap;
    private PropertyMap    nativeArrayBufferMap;
    private PropertyMap    nativeBooleanMap;
    private PropertyMap    nativeDateMap;
    private PropertyMap    nativeErrorMap;
    private PropertyMap    nativeEvalErrorMap;
    private PropertyMap    nativeJSAdapterMap;
    private PropertyMap    nativeJavaImporterMap;
    private PropertyMap    nativeNumberMap;
    private PropertyMap    nativeRangeErrorMap;
    private PropertyMap    nativeReferenceErrorMap;
    private PropertyMap    nativeRegExpMap;
    private PropertyMap    nativeRegExpExecResultMap;
    private PropertyMap    nativeStrictArgumentsMap;
    private PropertyMap    nativeStringMap;
    private PropertyMap    nativeSyntaxErrorMap;
    private PropertyMap    nativeTypeErrorMap;
    private PropertyMap    nativeURIErrorMap;
    private PropertyMap    prototypeObjectMap;
    private PropertyMap    objectMap;
    private PropertyMap    functionMap;
    private PropertyMap    anonymousFunctionMap;
    private PropertyMap    strictFunctionMap;
    private PropertyMap    boundFunctionMap;

    // Flag to indicate that a split method issued a return statement
    private int splitState = -1;

    // class cache
    private ClassCache classCache;

    // Used to store the last RegExp result to support deprecated RegExp constructor properties
    private RegExpResult lastRegExpResult;

    private static final MethodHandle EVAL              = findOwnMH("eval",              Object.class, Object.class, Object.class);
    private static final MethodHandle PRINT             = findOwnMH("print",             Object.class, Object.class, Object[].class);
    private static final MethodHandle PRINTLN           = findOwnMH("println",           Object.class, Object.class, Object[].class);
    private static final MethodHandle LOAD              = findOwnMH("load",              Object.class, Object.class, Object.class);
    private static final MethodHandle LOADWITHNEWGLOBAL = findOwnMH("loadWithNewGlobal", Object.class, Object.class, Object[].class);
    private static final MethodHandle EXIT              = findOwnMH("exit",              Object.class, Object.class, Object.class);

    // initialized by nasgen
    private static PropertyMap $nasgenmap$;

    // context to which this global belongs to
    private final Context context;

    @Override
    protected Context getContext() {
        return context;
    }

    // performs initialization checks for Global constructor and returns the
    // PropertyMap, if everything is fine.
    private static PropertyMap checkAndGetMap(final Context context) {
        // security check first
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission(Context.NASHORN_CREATE_GLOBAL));
        }

        // null check on context
        context.getClass();

        /*
         * Duplicate global's map and use it. This way the initial Map filled
         * by nasgen (referenced from static field in this class) is retained
         * 'as is' (as that one is process wide singleton.
         */
        return $nasgenmap$.duplicate();
    }

    /**
     * Constructor
     *
     * @param context the context
     */
    public Global(final Context context) {
        super(checkAndGetMap(context));
        this.context = context;
        this.setIsScope();

        final int cacheSize = context.getEnv()._class_cache_size;
        if (cacheSize > 0) {
            classCache = new ClassCache(cacheSize);
        }
    }

    /**
     * Script access to "current" Global instance
     *
     * @return the global singleton
     */
    public static Global instance() {
        ScriptObject global = Context.getGlobal();
        if (! (global instanceof Global)) {
            throw new IllegalStateException("no current global instance");
        }
        return (Global)global;
    }

    /**
     * Script access to {@link ScriptEnvironment}
     *
     * @return the script environment
     */
    static ScriptEnvironment getEnv() {
        return instance().getContext().getEnv();
    }

    /**
     * Script access to {@link Context}
     *
     * @return the context
     */
    static Context getThisContext() {
        return instance().getContext();
    }

    // GlobalObject interface implementation

    @Override
    public boolean isOfContext(final Context ctxt) {
        return this.context == ctxt;
    }

    @Override
    public boolean isStrictContext() {
        return context.getEnv()._strict;
    }

    @Override
    public void initBuiltinObjects() {
        if (this.builtinObject != null) {
            // already initialized, just return
            return;
        }

        init();
    }

    @Override
    public ScriptFunction newScriptFunction(final String name, final MethodHandle handle, final ScriptObject scope, final boolean strict) {
        return new ScriptFunctionImpl(name, handle, scope, null, strict, false, true);
    }

    @Override
    public Object wrapAsObject(final Object obj) {
        if (obj instanceof Boolean) {
            return new NativeBoolean((Boolean)obj, this);
        } else if (obj instanceof Number) {
            return new NativeNumber(((Number)obj).doubleValue(), this);
        } else if (obj instanceof String || obj instanceof ConsString) {
            return new NativeString((CharSequence)obj, this);
        } else if (obj instanceof Object[]) { // extension
            return new NativeArray((Object[])obj);
        } else if (obj instanceof double[]) { // extension
            return new NativeArray((double[])obj);
        } else if (obj instanceof long[]) {
            return new NativeArray((long[])obj);
        } else if (obj instanceof int[]) {
            return new NativeArray((int[])obj);
        } else {
            // FIXME: more special cases? Map? List?
            return obj;
        }
    }

    @Override
    public GuardedInvocation primitiveLookup(final LinkRequest request, final Object self) {
        if (self instanceof String || self instanceof ConsString) {
            return NativeString.lookupPrimitive(request, self);
        } else if (self instanceof Number) {
            return NativeNumber.lookupPrimitive(request, self);
        } else if (self instanceof Boolean) {
            return NativeBoolean.lookupPrimitive(request, self);
        }
        throw new IllegalArgumentException("Unsupported primitive: " + self);
    }

    @Override
    public ScriptObject newObject() {
        return new JO(getObjectPrototype(), getObjectMap());
    }

    @Override
    public Object getDefaultValue(final ScriptObject sobj, final Class<?> typeHint) {
        // When the [[DefaultValue]] internal method of O is called with no hint,
        // then it behaves as if the hint were Number, unless O is a Date object
        // in which case it behaves as if the hint were String.
        Class<?> hint = typeHint;
        if (hint == null) {
            hint = Number.class;
        }

        try {
            if (hint == String.class) {

                final Object toString = TO_STRING.getGetter().invokeExact(sobj);

                if (Bootstrap.isCallable(toString)) {
                    final Object value = TO_STRING.getInvoker().invokeExact(toString, sobj);
                    if (JSType.isPrimitive(value)) {
                        return value;
                    }
                }

                final Object valueOf = VALUE_OF.getGetter().invokeExact(sobj);
                if (Bootstrap.isCallable(valueOf)) {
                    final Object value = VALUE_OF.getInvoker().invokeExact(valueOf, sobj);
                    if (JSType.isPrimitive(value)) {
                        return value;
                    }
                }
                throw typeError(this, "cannot.get.default.string");
            }

            if (hint == Number.class) {
                final Object valueOf = VALUE_OF.getGetter().invokeExact(sobj);
                if (Bootstrap.isCallable(valueOf)) {
                    final Object value = VALUE_OF.getInvoker().invokeExact(valueOf, sobj);
                    if (JSType.isPrimitive(value)) {
                        return value;
                    }
                }

                final Object toString = TO_STRING.getGetter().invokeExact(sobj);
                if (Bootstrap.isCallable(toString)) {
                    final Object value = TO_STRING.getInvoker().invokeExact(toString, sobj);
                    if (JSType.isPrimitive(value)) {
                        return value;
                    }
                }

                throw typeError(this, "cannot.get.default.number");
            }
        } catch (final RuntimeException | Error e) {
            throw e;
        } catch (final Throwable t) {
            throw new RuntimeException(t);
        }

        return UNDEFINED;
    }

    @Override
    public boolean isError(final ScriptObject sobj) {
        final ScriptObject errorProto = getErrorPrototype();
        ScriptObject proto = sobj.getProto();
        while (proto != null) {
            if (proto == errorProto) {
                return true;
            }
            proto = proto.getProto();
        }
        return false;
    }

    @Override
    public ScriptObject newError(final String msg) {
        return new NativeError(msg, this);
    }

    @Override
    public ScriptObject newEvalError(final String msg) {
        return new NativeEvalError(msg, this);
    }

    @Override
    public ScriptObject newRangeError(final String msg) {
        return new NativeRangeError(msg, this);
    }

    @Override
    public ScriptObject newReferenceError(final String msg) {
        return new NativeReferenceError(msg, this);
    }

    @Override
    public ScriptObject newSyntaxError(final String msg) {
        return new NativeSyntaxError(msg, this);
    }

    @Override
    public ScriptObject newTypeError(final String msg) {
        return new NativeTypeError(msg, this);
    }

    @Override
    public ScriptObject newURIError(final String msg) {
        return new NativeURIError(msg, this);
    }

    @Override
    public PropertyDescriptor newGenericDescriptor(final boolean configurable, final boolean enumerable) {
        return new GenericPropertyDescriptor(configurable, enumerable, this);
    }

    @Override
    public PropertyDescriptor newDataDescriptor(final Object value, final boolean configurable, final boolean enumerable, final boolean writable) {
        return new DataPropertyDescriptor(configurable, enumerable, writable, value, this);
    }

    @Override
    public PropertyDescriptor newAccessorDescriptor(final Object get, final Object set, final boolean configurable, final boolean enumerable) {
        final AccessorPropertyDescriptor desc = new AccessorPropertyDescriptor(configurable, enumerable, get == null ? UNDEFINED : get, set == null ? UNDEFINED : set, this);

        if (get == null) {
            desc.delete(PropertyDescriptor.GET, false);
        }

        if (set == null) {
            desc.delete(PropertyDescriptor.SET, false);
        }

        return desc;
    }


    /**
     * Cache for compiled script classes.
     */
    @SuppressWarnings("serial")
    private static class ClassCache extends LinkedHashMap<Source, ClassReference> {
        private final int size;
        private final ReferenceQueue<Class<?>> queue;

        ClassCache(int size) {
            super(size, 0.75f, true);
            this.size = size;
            this.queue = new ReferenceQueue<>();
        }

        void cache(final Source source, final Class<?> clazz) {
            put(source, new ClassReference(clazz, queue, source));
        }

        @Override
        protected boolean removeEldestEntry(final Map.Entry<Source, ClassReference> eldest) {
            return size() > size;
        }

        @Override
        public ClassReference get(Object key) {
            for (ClassReference ref; (ref = (ClassReference)queue.poll()) != null; ) {
                remove(ref.source);
            }
            return super.get(key);
        }

    }

    private static class ClassReference extends SoftReference<Class<?>> {
        private final Source source;

        ClassReference(final Class<?> clazz, final ReferenceQueue<Class<?>> queue, final Source source) {
            super(clazz, queue);
            this.source = source;
        }
    }

    // Class cache management
    @Override
    public Class<?> findCachedClass(final Source source) {
        assert classCache != null : "Class cache used without being initialized";
        ClassReference ref = classCache.get(source);
        return ref != null ? ref.get() : null;
    }

    @Override
    public void cacheClass(final Source source, final Class<?> clazz) {
        assert classCache != null : "Class cache used without being initialized";
        classCache.cache(source, clazz);
    }

    private static <T> T getLazilyCreatedValue(final Object key, final Callable<T> creator, final Map<Object, T> map) {
        final T obj = map.get(key);
        if (obj != null) {
            return obj;
        }

        try {
            final T newObj = creator.call();
            final T existingObj = map.putIfAbsent(key, newObj);
            return existingObj != null ? existingObj : newObj;
        } catch (final Exception exp) {
            throw new RuntimeException(exp);
        }
    }

    private final Map<Object, InvokeByName> namedInvokers = new ConcurrentHashMap<>();

    @Override
    public InvokeByName getInvokeByName(final Object key, final Callable<InvokeByName> creator) {
        return getLazilyCreatedValue(key, creator, namedInvokers);
    }

    private final Map<Object, MethodHandle> dynamicInvokers = new ConcurrentHashMap<>();

    @Override
    public MethodHandle getDynamicInvoker(final Object key, final Callable<MethodHandle> creator) {
        return getLazilyCreatedValue(key, creator, dynamicInvokers);
    }

    /**
     * This is the eval used when 'indirect' eval call is made.
     *
     * var global = this;
     * global.eval("print('hello')");
     *
     * @param self  eval scope
     * @param str   eval string
     *
     * @return the result of eval
     */
    public static Object eval(final Object self, final Object str) {
        return directEval(self, str, UNDEFINED, UNDEFINED, UNDEFINED);
    }

    /**
     * Direct eval
     *
     * @param self     The scope of eval passed as 'self'
     * @param str      Evaluated code
     * @param callThis "this" to be passed to the evaluated code
     * @param location location of the eval call
     * @param strict   is eval called a strict mode code?
     *
     * @return the return value of the eval
     *
     * This is directly invoked from generated when eval(code) is called in user code
     */
    public static Object directEval(final Object self, final Object str, final Object callThis, final Object location, final Object strict) {
        if (!(str instanceof String || str instanceof ConsString)) {
            return str;
        }
        final Global global = Global.instance();
        final ScriptObject scope = (self instanceof ScriptObject) ? (ScriptObject)self : global;

        return global.getContext().eval(scope, str.toString(), callThis, location, Boolean.TRUE.equals(strict));
    }

    /**
     * Global print implementation - Nashorn extension
     *
     * @param self    scope
     * @param objects arguments to print
     *
     * @return result of print (undefined)
     */
    public static Object print(final Object self, final Object... objects) {
        return printImpl(false, objects);
    }

    /**
     * Global println implementation - Nashorn extension
     *
     * @param self    scope
     * @param objects arguments to print
     *
     * @return result of println (undefined)
     */
    public static Object println(final Object self, final Object... objects) {
        return printImpl(true, objects);
    }

    /**
     * Global load implementation - Nashorn extension
     *
     * @param self    scope
     * @param source  source to load
     *
     * @return result of load (undefined)
     *
     * @throws IOException if source could not be read
     */
    public static Object load(final Object self, final Object source) throws IOException {
        final Global global = Global.instance();
        final ScriptObject scope = (self instanceof ScriptObject) ? (ScriptObject)self : global;
        return global.getContext().load(scope, source);
    }

    /**
     * Global loadWithNewGlobal implementation - Nashorn extension
     *
     * @param self scope
     * @param args from plus (optional) arguments to be passed to the loaded script
     *
     * @return result of load (may be undefined)
     *
     * @throws IOException if source could not be read
     */
    public static Object loadWithNewGlobal(final Object self, final Object...args) throws IOException {
        final Global global = Global.instance();
        final int length = args.length;
        final boolean hasArgs = 0 < length;
        final Object from = hasArgs ? args[0] : UNDEFINED;
        final Object[] arguments = hasArgs ? Arrays.copyOfRange(args, 1, length) : args;

        return global.getContext().loadWithNewGlobal(from, arguments);
    }

    /**
     * Global exit and quit implementation - Nashorn extension: perform a {@code System.exit} call from the script
     *
     * @param self  self reference
     * @param code  exit code
     *
     * @return undefined (will never be reacheD)
     */
    public static Object exit(final Object self, final Object code) {
        System.exit(JSType.toInt32(code));
        return UNDEFINED;
    }

    // builtin prototype accessors
    ScriptObject getFunctionPrototype() {
        return ScriptFunction.getPrototype(builtinFunction);
    }

    ScriptObject getObjectPrototype() {
        return ScriptFunction.getPrototype(builtinObject);
    }

    ScriptObject getArrayPrototype() {
        return ScriptFunction.getPrototype(builtinArray);
    }

    ScriptObject getBooleanPrototype() {
        return ScriptFunction.getPrototype(builtinBoolean);
    }

    ScriptObject getNumberPrototype() {
        return ScriptFunction.getPrototype(builtinNumber);
    }

    ScriptObject getDatePrototype() {
        return ScriptFunction.getPrototype(builtinDate);
    }

    ScriptObject getRegExpPrototype() {
        return ScriptFunction.getPrototype(builtinRegExp);
    }

    ScriptObject getStringPrototype() {
        return ScriptFunction.getPrototype(builtinString);
    }

    ScriptObject getErrorPrototype() {
        return ScriptFunction.getPrototype(builtinError);
    }

    ScriptObject getEvalErrorPrototype() {
        return ScriptFunction.getPrototype(builtinEvalError);
    }

    ScriptObject getRangeErrorPrototype() {
        return ScriptFunction.getPrototype(builtinRangeError);
    }

    ScriptObject getReferenceErrorPrototype() {
        return ScriptFunction.getPrototype(builtinReferenceError);
    }

    ScriptObject getSyntaxErrorPrototype() {
        return ScriptFunction.getPrototype(builtinSyntaxError);
    }

    ScriptObject getTypeErrorPrototype() {
        return ScriptFunction.getPrototype(builtinTypeError);
    }

    ScriptObject getURIErrorPrototype() {
        return ScriptFunction.getPrototype(builtinURIError);
    }

    ScriptObject getJavaImporterPrototype() {
        return ScriptFunction.getPrototype(builtinJavaImporter);
    }

    ScriptObject getJSAdapterPrototype() {
        return ScriptFunction.getPrototype(builtinJSAdapter);
    }

    ScriptObject getArrayBufferPrototype() {
        return ScriptFunction.getPrototype(builtinArrayBuffer);
    }

    ScriptObject getInt8ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinInt8Array);
    }

    ScriptObject getUint8ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinUint8Array);
    }

    ScriptObject getUint8ClampedArrayPrototype() {
        return ScriptFunction.getPrototype(builtinUint8ClampedArray);
    }

    ScriptObject getInt16ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinInt16Array);
    }

    ScriptObject getUint16ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinUint16Array);
    }

    ScriptObject getInt32ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinInt32Array);
    }

    ScriptObject getUint32ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinUint32Array);
    }

    ScriptObject getFloat32ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinFloat32Array);
    }

    ScriptObject getFloat64ArrayPrototype() {
        return ScriptFunction.getPrototype(builtinFloat64Array);
    }

    // Builtin PropertyMap accessors
    PropertyMap getAccessorPropertyDescriptorMap() {
        return accessorPropertyDescriptorMap;
    }

    PropertyMap getArrayBufferViewMap() {
        return arrayBufferViewMap;
    }

    PropertyMap getDataPropertyDescriptorMap() {
        return dataPropertyDescriptorMap;
    }

    PropertyMap getGenericPropertyDescriptorMap() {
        return genericPropertyDescriptorMap;
    }

    PropertyMap getArgumentsMap() {
        return nativeArgumentsMap;
    }

    PropertyMap getArrayMap() {
        return nativeArrayMap;
    }

    PropertyMap getArrayBufferMap() {
        return nativeArrayBufferMap;
    }

    PropertyMap getBooleanMap() {
        return nativeBooleanMap;
    }

    PropertyMap getDateMap() {
        return nativeDateMap;
    }

    PropertyMap getErrorMap() {
        return nativeErrorMap;
    }

    PropertyMap getEvalErrorMap() {
        return nativeEvalErrorMap;
    }

    PropertyMap getJSAdapterMap() {
        return nativeJSAdapterMap;
    }

    PropertyMap getJavaImporterMap() {
        return nativeJavaImporterMap;
    }

    PropertyMap getNumberMap() {
        return nativeNumberMap;
    }

    PropertyMap getRangeErrorMap() {
        return nativeRangeErrorMap;
    }

    PropertyMap getReferenceErrorMap() {
        return nativeReferenceErrorMap;
    }

    PropertyMap getRegExpMap() {
        return nativeRegExpMap;
    }

    PropertyMap getRegExpExecResultMap() {
        return nativeRegExpExecResultMap;
    }

    PropertyMap getStrictArgumentsMap() {
        return nativeStrictArgumentsMap;
    }

    PropertyMap getStringMap() {
        return nativeStringMap;
    }

    PropertyMap getSyntaxErrorMap() {
        return nativeSyntaxErrorMap;
    }

    PropertyMap getTypeErrorMap() {
        return nativeTypeErrorMap;
    }

    PropertyMap getURIErrorMap() {
        return nativeURIErrorMap;
    }

    PropertyMap getPrototypeObjectMap() {
        return prototypeObjectMap;
    }

    PropertyMap getObjectMap() {
        return objectMap;
    }

    PropertyMap getFunctionMap() {
        return functionMap;
    }

    PropertyMap getAnonymousFunctionMap() {
        return anonymousFunctionMap;
    }

    PropertyMap getStrictFunctionMap() {
        return strictFunctionMap;
    }

    PropertyMap getBoundFunctionMap() {
        return boundFunctionMap;
    }

    private ScriptFunction getBuiltinArray() {
        return builtinArray;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin array has not been overridden
     */
    public static boolean isBuiltinArray() {
        final Global instance = Global.instance();
        return instance.array == instance.getBuiltinArray();
    }

    private ScriptFunction getBuiltinBoolean() {
        return builtinBoolean;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin boolean has not been overridden
     */
    public static boolean isBuiltinBoolean() {
        final Global instance = Global.instance();
        return instance._boolean == instance.getBuiltinBoolean();
    }

    private ScriptFunction getBuiltinDate() {
        return builtinDate;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin date has not been overridden
     */
    public static boolean isBuiltinDate() {
        final Global instance = Global.instance();
        return instance.date == instance.getBuiltinDate();
    }

    private ScriptFunction getBuiltinError() {
        return builtinError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin error has not been overridden
     */
    public static boolean isBuiltinError() {
        final Global instance = Global.instance();
        return instance.error == instance.getBuiltinError();
    }

    private ScriptFunction getBuiltinEvalError() {
        return builtinEvalError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin eval error has not been overridden
     */
    public static boolean isBuiltinEvalError() {
        final Global instance = Global.instance();
        return instance.evalError == instance.getBuiltinEvalError();
    }

    private ScriptFunction getBuiltinFunction() {
        return builtinFunction;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin function has not been overridden
     */
    public static boolean isBuiltinFunction() {
        final Global instance = Global.instance();
        return instance.function == instance.getBuiltinFunction();
    }

    private ScriptFunction getBuiltinJSAdapter() {
        return builtinJSAdapter;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin JSAdapter has not been overridden
     */
    public static boolean isBuiltinJSAdapter() {
        final Global instance = Global.instance();
        return instance.jsadapter == instance.getBuiltinJSAdapter();
    }

    private ScriptObject getBuiltinJSON() {
        return builtinJSON;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin JSON has has not been overridden
     */
    public static boolean isBuiltinJSON() {
        final Global instance = Global.instance();
        return instance.json == instance.getBuiltinJSON();
    }

    private ScriptObject getBuiltinJava() {
        return builtinJava;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin Java has not been overridden
     */
    public static boolean isBuiltinJava() {
        final Global instance = Global.instance();
        return instance.java == instance.getBuiltinJava();
    }

    private ScriptObject getBuiltinJavax() {
        return builtinJavax;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin Javax has not been overridden
     */
    public static boolean isBuiltinJavax() {
        final Global instance = Global.instance();
        return instance.javax == instance.getBuiltinJavax();
    }

    private ScriptObject getBuiltinJavaImporter() {
        return builtinJavaImporter;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin Java importer has not been overridden
     */
    public static boolean isBuiltinJavaImporter() {
        final Global instance = Global.instance();
        return instance.javaImporter == instance.getBuiltinJavaImporter();
    }

    private ScriptObject getBuiltinMath() {
        return builtinMath;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin math has not been overridden
     */
    public static boolean isBuiltinMath() {
        final Global instance = Global.instance();
        return instance.math == instance.getBuiltinMath();
    }

    private ScriptFunction getBuiltinNumber() {
        return builtinNumber;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin number has not been overridden
     */
    public static boolean isBuiltinNumber() {
        final Global instance = Global.instance();
        return instance.number == instance.getBuiltinNumber();
    }

    private ScriptFunction getBuiltinObject() {
        return builtinObject;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin object has not been overridden
     */
    public static boolean isBuiltinObject() {
        final Global instance = Global.instance();
        return instance.object == instance.getBuiltinObject();
    }

    private ScriptObject getBuiltinPackages() {
        return builtinPackages;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin package has not been overridden
     */
    public static boolean isBuiltinPackages() {
        final Global instance = Global.instance();
        return instance.packages == instance.getBuiltinPackages();
    }

    private ScriptFunction getBuiltinRangeError() {
        return builtinRangeError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin range error has not been overridden
     */
    public static boolean isBuiltinRangeError() {
        final Global instance = Global.instance();
        return instance.rangeError == instance.getBuiltinRangeError();
    }

    private ScriptFunction getBuiltinReferenceError() {
        return builtinReferenceError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin reference error has not been overridden
     */
    public static boolean isBuiltinReferenceError() {
        final Global instance = Global.instance();
        return instance.referenceError == instance.getBuiltinReferenceError();
    }

    private ScriptFunction getBuiltinRegExp() {
        return builtinRegExp;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin regexp has not been overridden
     */
    public static boolean isBuiltinRegExp() {
        final Global instance = Global.instance();
        return instance.regexp == instance.getBuiltinRegExp();
    }

    private ScriptFunction getBuiltinString() {
        return builtinString;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin Java has not been overridden
     */
    public static boolean isBuiltinString() {
        final Global instance = Global.instance();
        return instance.string == instance.getBuiltinString();
    }

    private ScriptFunction getBuiltinSyntaxError() {
        return builtinSyntaxError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin syntax error has not been overridden
     */
    public static boolean isBuiltinSyntaxError() {
        final Global instance = Global.instance();
        return instance.syntaxError == instance.getBuiltinSyntaxError();
    }

    private ScriptFunction getBuiltinTypeError() {
        return builtinTypeError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin type error has not been overridden
     */
    public static boolean isBuiltinTypeError() {
        final Global instance = Global.instance();
        return instance.typeError == instance.getBuiltinTypeError();
    }

    private ScriptFunction getBuiltinURIError() {
        return builtinURIError;
    }

    /**
     * Called from compiled script code to test if builtin has been overridden
     *
     * @return true if builtin URI error has not been overridden
     */
    public static boolean isBuiltinURIError() {
        final Global instance = Global.instance();
        return instance.uriError == instance.getBuiltinURIError();
    }

    @Override
    public String getClassName() {
        return "global";
    }

    /**
     * Copy function used to clone NativeRegExp objects.
     *
     * @param regexp a NativeRegExp to clone
     *
     * @return copy of the given regexp object
     */
    public static Object regExpCopy(final Object regexp) {
        return new NativeRegExp((NativeRegExp)regexp);
    }

    /**
     * Convert given object to NativeRegExp type.
     *
     * @param obj object to be converted
     * @return NativeRegExp instance
     */
    public static NativeRegExp toRegExp(final Object obj) {
        if (obj instanceof NativeRegExp) {
            return (NativeRegExp)obj;
        }
        return new NativeRegExp(JSType.toString(obj));
    }

    /**
     * ECMA 9.9 ToObject implementation
     *
     * @param obj  an item for which to run ToObject
     * @return ToObject version of given item
     */
    public static Object toObject(final Object obj) {
        if (obj == null || obj == UNDEFINED) {
            throw typeError("not.an.object", ScriptRuntime.safeToString(obj));
        }

        if (obj instanceof ScriptObject) {
            return obj;
        }

        return instance().wrapAsObject(obj);
    }

    /**
     * Allocate a new object array.
     *
     * @param initial object values.
     * @return the new array
     */
    public static NativeArray allocate(final Object[] initial) {
        ArrayData arrayData = ArrayData.allocate(initial);

        for (int index = 0; index < initial.length; index++) {
            final Object value = initial[index];

            if (value == ScriptRuntime.EMPTY) {
                arrayData = arrayData.delete(index);
            }
        }

        return new NativeArray(arrayData);
    }

    /**
     * Allocate a new number array.
     *
     * @param initial number values.
     * @return the new array
     */
    public static NativeArray allocate(final double[] initial) {
        return new NativeArray(ArrayData.allocate(initial));
    }

    /**
     * Allocate a new long array.
     *
     * @param initial number values.
     * @return the new array
     */
    public static NativeArray allocate(final long[] initial) {
        return new NativeArray(ArrayData.allocate(initial));
    }

    /**
     * Allocate a new integer array.
     *
     * @param initial number values.
     * @return the new array
     */
    public static NativeArray allocate(final int[] initial) {
        return new NativeArray(ArrayData.allocate(initial));
    }

    /**
     * Allocate a new object array for arguments.
     *
     * @param arguments initial arguments passed.
     * @param callee reference to the function that uses arguments object
     * @param numParams actual number of declared parameters
     *
     * @return the new array
     */
    public static ScriptObject allocateArguments(final Object[] arguments, final Object callee, final int numParams) {
        return NativeArguments.allocate(arguments, (ScriptFunction)callee, numParams);
    }

    /**
     * Called from generated to check if given function is the builtin 'eval'. If
     * eval is used in a script, a lot of optimizations and assumptions cannot be done.
     *
     * @param  fn function object that is checked
     * @return true if fn is the builtin eval
     */
    public static boolean isEval(final Object fn) {
        return fn == Global.instance().builtinEval;
    }

    /**
     * Create a new RegExp object.
     *
     * @param expression Regular expression.
     * @param options    Search options.
     *
     * @return New RegExp object.
     */
    public static Object newRegExp(final String expression, final String options) {
        if (options == null) {
            return new NativeRegExp(expression);
        }
        return new NativeRegExp(expression, options);
    }

    /**
     * Get the object prototype
     *
     * @return the object prototype
     */
    public static ScriptObject objectPrototype() {
        return Global.instance().getObjectPrototype();
    }

    /**
     * Create a new empty object instance.
     *
     * @return New empty object.
     */
    public static ScriptObject newEmptyInstance() {
        return Global.instance().newObject();
    }

    /**
     * Check if a given object is a ScriptObject, raises an exception if this is
     * not the case
     *
     * @param obj and object to check
     */
    public static void checkObject(final Object obj) {
        if (!(obj instanceof ScriptObject)) {
            throw typeError("not.an.object", ScriptRuntime.safeToString(obj));
        }
    }

    /**
     * ECMA 9.10 - implementation of CheckObjectCoercible, i.e. raise an exception
     * if this object is null or undefined.
     *
     * @param obj an object to check
     */
    public static void checkObjectCoercible(final Object obj) {
        if (obj == null || obj == UNDEFINED) {
            throw typeError("not.an.object", ScriptRuntime.safeToString(obj));
        }
    }

    /**
     * Get the current split state.
     *
     * @return current split state
     */
    @Override
    public int getSplitState() {
        return splitState;
    }

    /**
     * Set the current split state.
     *
     * @param state current split state
     */
    @Override
    public void setSplitState(final int state) {
        splitState = state;
    }

    private void init() {
        assert Context.getGlobal() == this : "this global is not set as current";

        final ScriptEnvironment env = getContext().getEnv();

        // duplicate PropertyMaps of Native* classes
        copyInitialMaps(env);

        // initialize Function and Object constructor
        initFunctionAndObject();

        // Now fix Global's own proto.
        this.setProto(getObjectPrototype());

        // initialize global function properties
        this.eval = this.builtinEval = ScriptFunctionImpl.makeFunction("eval", EVAL);

        this.parseInt           = ScriptFunctionImpl.makeFunction("parseInt",   GlobalFunctions.PARSEINT);
        this.parseFloat         = ScriptFunctionImpl.makeFunction("parseFloat", GlobalFunctions.PARSEFLOAT);
        this.isNaN              = ScriptFunctionImpl.makeFunction("isNaN",      GlobalFunctions.IS_NAN);
        this.isFinite           = ScriptFunctionImpl.makeFunction("isFinite",   GlobalFunctions.IS_FINITE);
        this.encodeURI          = ScriptFunctionImpl.makeFunction("encodeURI",  GlobalFunctions.ENCODE_URI);
        this.encodeURIComponent = ScriptFunctionImpl.makeFunction("encodeURIComponent", GlobalFunctions.ENCODE_URICOMPONENT);
        this.decodeURI          = ScriptFunctionImpl.makeFunction("decodeURI",  GlobalFunctions.DECODE_URI);
        this.decodeURIComponent = ScriptFunctionImpl.makeFunction("decodeURIComponent", GlobalFunctions.DECODE_URICOMPONENT);
        this.escape             = ScriptFunctionImpl.makeFunction("escape",     GlobalFunctions.ESCAPE);
        this.unescape           = ScriptFunctionImpl.makeFunction("unescape",   GlobalFunctions.UNESCAPE);
        this.print              = ScriptFunctionImpl.makeFunction("print",      env._print_no_newline ? PRINT : PRINTLN);
        this.load               = ScriptFunctionImpl.makeFunction("load",       LOAD);
        this.loadWithNewGlobal  = ScriptFunctionImpl.makeFunction("loadWithNewGlobal", LOADWITHNEWGLOBAL);
        this.exit               = ScriptFunctionImpl.makeFunction("exit",       EXIT);
        this.quit               = ScriptFunctionImpl.makeFunction("quit",       EXIT);

        // built-in constructors
        this.builtinArray     = (ScriptFunction)initConstructor("Array");
        this.builtinBoolean   = (ScriptFunction)initConstructor("Boolean");
        this.builtinDate      = (ScriptFunction)initConstructor("Date");
        this.builtinJSON      = initConstructor("JSON");
        this.builtinJSAdapter = (ScriptFunction)initConstructor("JSAdapter");
        this.builtinMath      = initConstructor("Math");
        this.builtinNumber    = (ScriptFunction)initConstructor("Number");
        this.builtinRegExp    = (ScriptFunction)initConstructor("RegExp");
        this.builtinString    = (ScriptFunction)initConstructor("String");

        // initialize String.prototype.length to 0
        // add String.prototype.length
        final ScriptObject stringPrototype = getStringPrototype();
        stringPrototype.addOwnProperty("length", Attribute.NON_ENUMERABLE_CONSTANT, 0.0);

        // set isArray flag on Array.prototype
        final ScriptObject arrayPrototype = getArrayPrototype();
        arrayPrototype.setIsArray();

        this.DEFAULT_DATE = new NativeDate(Double.NaN, this);

        // initialize default regexp object
        this.DEFAULT_REGEXP = new NativeRegExp("(?:)", this);

        // RegExp.prototype should behave like a RegExp object. So copy the
        // properties.
        final ScriptObject regExpProto = getRegExpPrototype();
        regExpProto.addBoundProperties(DEFAULT_REGEXP);

        // Error stuff
        initErrorObjects();

        // java access
        if (! env._no_java) {
            initJavaAccess();
        }

        if (! env._no_typed_arrays) {
            initTypedArray();
        }

        if (env._scripting) {
            initScripting(env);
        }

        if (Context.DEBUG && System.getSecurityManager() == null) {
            initDebug();
        }

        copyBuiltins();

        // initialized with strings so that typeof will work as expected.
        this.__FILE__ = "";
        this.__DIR__  = "";
        this.__LINE__ = 0.0;

        // expose script (command line) arguments as "arguments" property of global
        final List<String> arguments = env.getArguments();
        final Object argsObj = wrapAsObject(arguments.toArray());

        addOwnProperty("arguments", Attribute.NOT_ENUMERABLE, argsObj);
        if (env._scripting) {
            // synonym for "arguments" in scripting mode
            addOwnProperty("$ARG", Attribute.NOT_ENUMERABLE, argsObj);
        }
    }

    private void initErrorObjects() {
        // Error objects
        this.builtinError = (ScriptFunction)initConstructor("Error");
        final ScriptObject errorProto = getErrorPrototype();

        // Nashorn specific accessors on Error.prototype - stack, lineNumber, columnNumber and fileName
        final ScriptFunction getStack = ScriptFunctionImpl.makeFunction("getStack", NativeError.GET_STACK);
        final ScriptFunction setStack = ScriptFunctionImpl.makeFunction("setStack", NativeError.SET_STACK);
        errorProto.addOwnProperty("stack", Attribute.NOT_ENUMERABLE, getStack, setStack);
        final ScriptFunction getLineNumber = ScriptFunctionImpl.makeFunction("getLineNumber", NativeError.GET_LINENUMBER);
        final ScriptFunction setLineNumber = ScriptFunctionImpl.makeFunction("setLineNumber", NativeError.SET_LINENUMBER);
        errorProto.addOwnProperty("lineNumber", Attribute.NOT_ENUMERABLE, getLineNumber, setLineNumber);
        final ScriptFunction getColumnNumber = ScriptFunctionImpl.makeFunction("getColumnNumber", NativeError.GET_COLUMNNUMBER);
        final ScriptFunction setColumnNumber = ScriptFunctionImpl.makeFunction("setColumnNumber", NativeError.SET_COLUMNNUMBER);
        errorProto.addOwnProperty("columnNumber", Attribute.NOT_ENUMERABLE, getColumnNumber, setColumnNumber);
        final ScriptFunction getFileName = ScriptFunctionImpl.makeFunction("getFileName", NativeError.GET_FILENAME);
        final ScriptFunction setFileName = ScriptFunctionImpl.makeFunction("setFileName", NativeError.SET_FILENAME);
        errorProto.addOwnProperty("fileName", Attribute.NOT_ENUMERABLE, getFileName, setFileName);

        // ECMA 15.11.4.2 Error.prototype.name
        // Error.prototype.name = "Error";
        errorProto.set(NativeError.NAME, "Error", false);
        // ECMA 15.11.4.3 Error.prototype.message
        // Error.prototype.message = "";
        errorProto.set(NativeError.MESSAGE, "", false);

        this.builtinEvalError = initErrorSubtype("EvalError", errorProto);
        this.builtinRangeError = initErrorSubtype("RangeError", errorProto);
        this.builtinReferenceError = initErrorSubtype("ReferenceError", errorProto);
        this.builtinSyntaxError = initErrorSubtype("SyntaxError", errorProto);
        this.builtinTypeError = initErrorSubtype("TypeError", errorProto);
        this.builtinURIError = initErrorSubtype("URIError", errorProto);
    }

    private ScriptFunction initErrorSubtype(final String name, final ScriptObject errorProto) {
        final ScriptObject cons = initConstructor(name);
        final ScriptObject prototype = ScriptFunction.getPrototype(cons);
        prototype.set(NativeError.NAME, name, false);
        prototype.set(NativeError.MESSAGE, "", false);
        prototype.setProto(errorProto);
        return (ScriptFunction)cons;
    }

    private void initJavaAccess() {
        final ScriptObject objectProto = getObjectPrototype();
        this.builtinPackages = new NativeJavaPackage("", objectProto);
        this.builtinCom = new NativeJavaPackage("com", objectProto);
        this.builtinEdu = new NativeJavaPackage("edu", objectProto);
        this.builtinJava = new NativeJavaPackage("java", objectProto);
        this.builtinJavafx = new NativeJavaPackage("javafx", objectProto);
        this.builtinJavax = new NativeJavaPackage("javax", objectProto);
        this.builtinOrg = new NativeJavaPackage("org", objectProto);
        this.builtinJavaImporter = initConstructor("JavaImporter");
        this.builtinJavaApi = initConstructor("Java");
    }

    private void initScripting(final ScriptEnvironment scriptEnv) {
        Object value;
        value = ScriptFunctionImpl.makeFunction("readLine", ScriptingFunctions.READLINE);
        addOwnProperty("readLine", Attribute.NOT_ENUMERABLE, value);

        value = ScriptFunctionImpl.makeFunction("readFully", ScriptingFunctions.READFULLY);
        addOwnProperty("readFully", Attribute.NOT_ENUMERABLE, value);

        final String execName = ScriptingFunctions.EXEC_NAME;
        value = ScriptFunctionImpl.makeFunction(execName, ScriptingFunctions.EXEC);
        addOwnProperty(execName, Attribute.NOT_ENUMERABLE, value);

        // Nashorn extension: global.echo (scripting-mode-only)
        // alias for "print"
        value = get("print");
        addOwnProperty("echo", Attribute.NOT_ENUMERABLE, value);

        // Nashorn extension: global.$OPTIONS (scripting-mode-only)
        final ScriptObject options = newObject();
        copyOptions(options, scriptEnv);
        addOwnProperty("$OPTIONS", Attribute.NOT_ENUMERABLE, options);

        // Nashorn extension: global.$ENV (scripting-mode-only)
        if (System.getSecurityManager() == null) {
            // do not fill $ENV if we have a security manager around
            // Retrieve current state of ENV variables.
            final ScriptObject env = newObject();
            env.putAll(System.getenv(), scriptEnv._strict);
            addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, env);
        } else {
            addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
        }

        // add other special properties for exec support
        addOwnProperty(ScriptingFunctions.OUT_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
        addOwnProperty(ScriptingFunctions.ERR_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
        addOwnProperty(ScriptingFunctions.EXIT_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
    }

    private static void copyOptions(final ScriptObject options, final ScriptEnvironment scriptEnv) {
        for (Field f : scriptEnv.getClass().getFields()) {
            try {
                options.set(f.getName(), f.get(scriptEnv), false);
            } catch (final IllegalArgumentException | IllegalAccessException exp) {
                throw new RuntimeException(exp);
            }
        }
    }

    private void initTypedArray() {
        this.builtinArrayBuffer       = initConstructor("ArrayBuffer");
        this.builtinInt8Array         = initConstructor("Int8Array");
        this.builtinUint8Array        = initConstructor("Uint8Array");
        this.builtinUint8ClampedArray = initConstructor("Uint8ClampedArray");
        this.builtinInt16Array        = initConstructor("Int16Array");
        this.builtinUint16Array       = initConstructor("Uint16Array");
        this.builtinInt32Array        = initConstructor("Int32Array");
        this.builtinUint32Array       = initConstructor("Uint32Array");
        this.builtinFloat32Array      = initConstructor("Float32Array");
        this.builtinFloat64Array      = initConstructor("Float64Array");
    }

    private void copyBuiltins() {
        this.array             = this.builtinArray;
        this._boolean          = this.builtinBoolean;
        this.date              = this.builtinDate;
        this.error             = this.builtinError;
        this.evalError         = this.builtinEvalError;
        this.function          = this.builtinFunction;
        this.jsadapter         = this.builtinJSAdapter;
        this.json              = this.builtinJSON;
        this.com               = this.builtinCom;
        this.edu               = this.builtinEdu;
        this.java              = this.builtinJava;
        this.javafx            = this.builtinJavafx;
        this.javax             = this.builtinJavax;
        this.org               = this.builtinOrg;
        this.javaImporter      = this.builtinJavaImporter;
        this.javaApi           = this.builtinJavaApi;
        this.math              = this.builtinMath;
        this.number            = this.builtinNumber;
        this.object            = this.builtinObject;
        this.packages          = this.builtinPackages;
        this.rangeError        = this.builtinRangeError;
        this.referenceError    = this.builtinReferenceError;
        this.regexp            = this.builtinRegExp;
        this.string            = this.builtinString;
        this.syntaxError       = this.builtinSyntaxError;
        this.typeError         = this.builtinTypeError;
        this.uriError          = this.builtinURIError;
        this.arrayBuffer       = this.builtinArrayBuffer;
        this.int8Array         = this.builtinInt8Array;
        this.uint8Array        = this.builtinUint8Array;
        this.uint8ClampedArray = this.builtinUint8ClampedArray;
        this.int16Array        = this.builtinInt16Array;
        this.uint16Array       = this.builtinUint16Array;
        this.int32Array        = this.builtinInt32Array;
        this.uint32Array       = this.builtinUint32Array;
        this.float32Array      = this.builtinFloat32Array;
        this.float64Array      = this.builtinFloat64Array;
    }

    private void initDebug() {
        this.addOwnProperty("Debug", Attribute.NOT_ENUMERABLE, initConstructor("Debug"));
    }

    @SuppressWarnings("resource")
    private static Object printImpl(final boolean newLine, final Object... objects) {
        final PrintWriter out = Global.getEnv().getOut();
        final StringBuilder sb = new StringBuilder();

        for (final Object object : objects) {
            if (sb.length() != 0) {
                sb.append(' ');
            }

            sb.append(JSType.toString(object));
        }

        // Print all at once to ensure thread friendly result.
        if (newLine) {
            out.println(sb.toString());
        } else {
            out.print(sb.toString());
        }

        out.flush();

        return UNDEFINED;
    }

    /**
     * These classes are generated by nasgen tool and so we have to use
     * reflection to load and create new instance of these classes.
     */
    private ScriptObject initConstructor(final String name) {
        try {
            // Assuming class name pattern for built-in JS constructors.
            final StringBuilder sb = new StringBuilder("jdk.nashorn.internal.objects.");

            sb.append("Native");
            sb.append(name);
            sb.append("$Constructor");

            final Class<?>     funcClass = Class.forName(sb.toString());
            final ScriptObject res       = (ScriptObject)funcClass.newInstance();

            if (res instanceof ScriptFunction) {
                // All global constructor prototypes are not-writable,
                // not-enumerable and not-configurable.
                final ScriptFunction func = (ScriptFunction)res;
                func.modifyOwnProperty(func.getProperty("prototype"), Attribute.NON_ENUMERABLE_CONSTANT);
            }

            if (res.getProto() == null) {
                res.setProto(getObjectPrototype());
            }

            return res;

        } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private void copyInitialMaps(final ScriptEnvironment env) {
        this.accessorPropertyDescriptorMap = AccessorPropertyDescriptor.getInitialMap().duplicate();
        this.dataPropertyDescriptorMap = DataPropertyDescriptor.getInitialMap().duplicate();
        this.genericPropertyDescriptorMap = GenericPropertyDescriptor.getInitialMap().duplicate();
        this.nativeArgumentsMap = NativeArguments.getInitialMap().duplicate();
        this.nativeArrayMap = NativeArray.getInitialMap().duplicate();
        this.nativeBooleanMap = NativeBoolean.getInitialMap().duplicate();
        this.nativeDateMap = NativeDate.getInitialMap().duplicate();
        this.nativeErrorMap = NativeError.getInitialMap().duplicate();
        this.nativeEvalErrorMap = NativeEvalError.getInitialMap().duplicate();
        this.nativeJSAdapterMap = NativeJSAdapter.getInitialMap().duplicate();
        this.nativeNumberMap = NativeNumber.getInitialMap().duplicate();
        this.nativeRangeErrorMap = NativeRangeError.getInitialMap().duplicate();
        this.nativeReferenceErrorMap = NativeReferenceError.getInitialMap().duplicate();
        this.nativeRegExpMap = NativeRegExp.getInitialMap().duplicate();
        this.nativeRegExpExecResultMap = NativeRegExpExecResult.getInitialMap().duplicate();
        this.nativeStrictArgumentsMap = NativeStrictArguments.getInitialMap().duplicate();
        this.nativeStringMap = NativeString.getInitialMap().duplicate();
        this.nativeSyntaxErrorMap = NativeSyntaxError.getInitialMap().duplicate();
        this.nativeTypeErrorMap = NativeTypeError.getInitialMap().duplicate();
        this.nativeURIErrorMap = NativeURIError.getInitialMap().duplicate();
        this.prototypeObjectMap = PrototypeObject.getInitialMap().duplicate();
        this.objectMap = JO.getInitialMap().duplicate();
        this.functionMap = ScriptFunctionImpl.getInitialMap().duplicate();
        this.anonymousFunctionMap = ScriptFunctionImpl.getInitialAnonymousMap().duplicate();
        this.strictFunctionMap = ScriptFunctionImpl.getInitialStrictMap().duplicate();
        this.boundFunctionMap = ScriptFunctionImpl.getInitialBoundMap().duplicate();

        // java
        if (! env._no_java) {
            this.nativeJavaImporterMap = NativeJavaImporter.getInitialMap().duplicate();
        }

        // typed arrays
        if (! env._no_typed_arrays) {
            this.arrayBufferViewMap = ArrayBufferView.getInitialMap().duplicate();
            this.nativeArrayBufferMap = NativeArrayBuffer.getInitialMap().duplicate();
        }
    }

    // Function and Object constructors are inter-dependent. Also,
    // Function.prototype
    // functions are not properly initialized. We fix the references here.
    // NOTE: be careful if you want to re-order the operations here. You may
    // have
    // to play with object references carefully!!
    private void initFunctionAndObject() {
        // First-n-foremost is Function
        this.builtinFunction = (ScriptFunction)initConstructor("Function");

        // create global anonymous function
        final ScriptFunction anon = ScriptFunctionImpl.newAnonymousFunction(this);
        // need to copy over members of Function.prototype to anon function
        anon.addBoundProperties(getFunctionPrototype());

        // Function.prototype === Object.getPrototypeOf(Function) ===
        // <anon-function>
        builtinFunction.setProto(anon);
        builtinFunction.setPrototype(anon);
        anon.set("constructor", builtinFunction, false);
        anon.deleteOwnProperty(anon.getMap().findProperty("prototype"));

        // now initialize Object
        this.builtinObject = (ScriptFunction)initConstructor("Object");
        final ScriptObject ObjectPrototype = getObjectPrototype();
        // Object.getPrototypeOf(Function.prototype) === Object.prototype
        anon.setProto(ObjectPrototype);

        // Function valued properties of Function.prototype were not properly
        // initialized. Because, these were created before global.function and
        // global.object were not initialized.
        jdk.nashorn.internal.runtime.Property[] properties = getFunctionPrototype().getMap().getProperties();
        for (final jdk.nashorn.internal.runtime.Property property : properties) {
            final Object key = property.getKey();
            final Object value = builtinFunction.get(key);

            if (value instanceof ScriptFunction && value != anon) {
                final ScriptFunction func = (ScriptFunction)value;
                func.setProto(getFunctionPrototype());
                final ScriptObject prototype = ScriptFunction.getPrototype(func);
                if (prototype != null) {
                    prototype.setProto(ObjectPrototype);
                }
            }
        }

        // For function valued properties of Object and Object.prototype, make
        // sure prototype's proto chain ends with Object.prototype
        for (final jdk.nashorn.internal.runtime.Property property : builtinObject.getMap().getProperties()) {
            final Object key = property.getKey();
            final Object value = builtinObject.get(key);

            if (value instanceof ScriptFunction) {
                final ScriptFunction func = (ScriptFunction)value;
                final ScriptObject prototype = ScriptFunction.getPrototype(func);
                if (prototype != null) {
                    prototype.setProto(ObjectPrototype);
                }
            }
        }

        properties = getObjectPrototype().getMap().getProperties();
        for (final jdk.nashorn.internal.runtime.Property property : properties) {
            final Object key   = property.getKey();
            final Object value = ObjectPrototype.get(key);

            if (key.equals("constructor")) {
                continue;
            }

            if (value instanceof ScriptFunction) {
                final ScriptFunction func = (ScriptFunction)value;
                final ScriptObject prototype = ScriptFunction.getPrototype(func);
                if (prototype != null) {
                    prototype.setProto(ObjectPrototype);
                }
            }
        }
    }

    private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
        return MH.findStatic(MethodHandles.lookup(), Global.class, name, MH.type(rtype, types));
    }

    RegExpResult getLastRegExpResult() {
        return lastRegExpResult;
    }

    void setLastRegExpResult(final RegExpResult regExpResult) {
        this.lastRegExpResult = regExpResult;
    }

}
