/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript.tools.debugger;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Kit;
import org.mozilla.javascript.NativeCall;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.SecurityUtilities;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.debug.DebugFrame;
import org.mozilla.javascript.debug.DebuggableObject;
import org.mozilla.javascript.debug.DebuggableScript;
import org.mozilla.javascript.debug.Debugger;

/** Dim or Debugger Implementation for Rhino. */
public class Dim {

    // Constants for instructing the debugger what action to perform
    // to end interruption.  Used by 'returnValue'.
    public static final int STEP_OVER = 0;
    public static final int STEP_INTO = 1;
    public static final int STEP_OUT = 2;
    public static final int GO = 3;
    public static final int BREAK = 4;
    public static final int EXIT = 5;

    // Constants for the DimIProxy interface implementation class.
    private static final int IPROXY_DEBUG = 0;
    private static final int IPROXY_LISTEN = 1;
    private static final int IPROXY_COMPILE_SCRIPT = 2;
    private static final int IPROXY_EVAL_SCRIPT = 3;
    private static final int IPROXY_STRING_IS_COMPILABLE = 4;
    private static final int IPROXY_OBJECT_TO_STRING = 5;
    private static final int IPROXY_OBJECT_PROPERTY = 6;
    private static final int IPROXY_OBJECT_IDS = 7;

    /** Interface to the debugger GUI. */
    private GuiCallback callback;

    /** Whether the debugger should break. */
    private boolean breakFlag;

    /** The ScopeProvider object that provides the scope in which to evaluate script. */
    private ScopeProvider scopeProvider;

    /** The SourceProvider object that provides the source of evaluated scripts. */
    private SourceProvider sourceProvider;

    /** The index of the current stack frame. */
    private int frameIndex = -1;

    /** Information about the current stack at the point of interruption. */
    private volatile ContextData interruptedContextData;

    /** The ContextFactory to listen to for debugging information. */
    private ContextFactory contextFactory;

    /**
     * Synchronization object used to allow script evaluations to happen when a thread is resumed.
     */
    private final Object monitor = new Object();

    /** Synchronization object used to wait for valid {@link #interruptedContextData}. */
    private final Object eventThreadMonitor = new Object();

    /** The action to perform to end the interruption loop. */
    private volatile int returnValue = -1;

    /** Whether the debugger is inside the interruption loop. */
    private boolean insideInterruptLoop;

    /** The requested script string to be evaluated when the thread has been resumed. */
    private String evalRequest;

    /** The stack frame in which to evaluate {@link #evalRequest}. */
    private StackFrame evalFrame;

    /** The result of evaluating {@link #evalRequest}. */
    private String evalResult;

    /** Whether the debugger should break when a script exception is thrown. */
    private boolean breakOnExceptions;

    /** Whether the debugger should break when a script function is entered. */
    private boolean breakOnEnter;

    /** Whether the debugger should break when a script function is returned from. */
    private boolean breakOnReturn;

    /** Table mapping URLs to information about the script source. */
    private final Map<String, SourceInfo> urlToSourceInfo =
            Collections.synchronizedMap(new HashMap<String, SourceInfo>());

    /** Table mapping function names to information about the function. */
    private final Map<String, FunctionSource> functionNames =
            Collections.synchronizedMap(new HashMap<String, FunctionSource>());

    /** Table mapping functions to information about the function. */
    private final Map<DebuggableScript, FunctionSource> functionToSource =
            Collections.synchronizedMap(new HashMap<DebuggableScript, FunctionSource>());

    /** ContextFactory.Listener instance attached to {@link #contextFactory}. */
    private DimIProxy listener;

    /** Sets the GuiCallback object to use. */
    public void setGuiCallback(GuiCallback callback) {
        this.callback = callback;
    }

    /** Tells the debugger to break at the next opportunity. */
    public void setBreak() {
        this.breakFlag = true;
    }

    /** Sets the ScopeProvider to be used. */
    public void setScopeProvider(ScopeProvider scopeProvider) {
        this.scopeProvider = scopeProvider;
    }

    /** Sets the ScopeProvider to be used. */
    public void setSourceProvider(final SourceProvider sourceProvider) {
        this.sourceProvider = sourceProvider;
    }

    /** Switches context to the stack frame with the given index. */
    public void contextSwitch(int frameIndex) {
        this.frameIndex = frameIndex;
    }

    /** Sets whether the debugger should break on exceptions. */
    public void setBreakOnExceptions(boolean breakOnExceptions) {
        this.breakOnExceptions = breakOnExceptions;
    }

    /** Sets whether the debugger should break on function entering. */
    public void setBreakOnEnter(boolean breakOnEnter) {
        this.breakOnEnter = breakOnEnter;
    }

    /** Sets whether the debugger should break on function return. */
    public void setBreakOnReturn(boolean breakOnReturn) {
        this.breakOnReturn = breakOnReturn;
    }

    /** Attaches the debugger to the given ContextFactory. */
    public void attachTo(ContextFactory factory) {
        detach();
        this.contextFactory = factory;
        this.listener = new DimIProxy(this, IPROXY_LISTEN);
        factory.addListener(this.listener);
    }

    /** Detaches the debugger from the current ContextFactory. */
    public void detach() {
        if (listener != null) {
            contextFactory.removeListener(listener);
            contextFactory = null;
            listener = null;
        }
    }

    /** Releases resources associated with this debugger. */
    public void dispose() {
        detach();
    }

    /** Returns the FunctionSource object for the given script or function. */
    private FunctionSource getFunctionSource(DebuggableScript fnOrScript) {
        FunctionSource fsource = functionSource(fnOrScript);
        if (fsource == null) {
            String url = getNormalizedUrl(fnOrScript);
            SourceInfo si = sourceInfo(url);
            if (si == null) {
                if (!fnOrScript.isGeneratedScript()) {
                    // Not eval or Function, try to load it from URL
                    String source = loadSource(url);
                    if (source != null) {
                        DebuggableScript top = fnOrScript;
                        for (; ; ) {
                            DebuggableScript parent = top.getParent();
                            if (parent == null) {
                                break;
                            }
                            top = parent;
                        }
                        registerTopScript(top, source);
                        fsource = functionSource(fnOrScript);
                    }
                }
            }
        }
        return fsource;
    }

    /** Loads the script at the given URL. */
    private String loadSource(String sourceUrl) {
        String source = null;
        int hash = sourceUrl.indexOf('#');
        if (hash >= 0) {
            sourceUrl = sourceUrl.substring(0, hash);
        }
        try {
            InputStream is;
            openStream:
            {
                if (sourceUrl.indexOf(':') < 0) {
                    // Can be a file name
                    try {
                        if (sourceUrl.startsWith("~/")) {
                            String home = SecurityUtilities.getSystemProperty("user.home");
                            if (home != null) {
                                String pathFromHome = sourceUrl.substring(2);
                                File f = new File(new File(home), pathFromHome);
                                if (f.exists()) {
                                    is = new FileInputStream(f);
                                    break openStream;
                                }
                            }
                        }
                        File f = new File(sourceUrl);
                        if (f.exists()) {
                            is = new FileInputStream(f);
                            break openStream;
                        }
                    } catch (SecurityException ex) {
                    }
                    // No existing file, assume missed http://
                    if (sourceUrl.startsWith("//")) {
                        sourceUrl = "http:" + sourceUrl;
                    } else if (sourceUrl.startsWith("/")) {
                        sourceUrl = "http://127.0.0.1" + sourceUrl;
                    } else {
                        sourceUrl = "http://" + sourceUrl;
                    }
                }

                is = new URL(sourceUrl).openStream();
            }

            try {
                source = Kit.readReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            } finally {
                is.close();
            }
        } catch (IOException ex) {
            System.err.println("Failed to load source from " + sourceUrl + ": " + ex);
        }
        return source;
    }

    /** Registers the given script as a top-level script in the debugger. */
    private void registerTopScript(DebuggableScript topScript, String source) {
        if (!topScript.isTopLevel()) {
            throw new IllegalArgumentException();
        }
        String url = getNormalizedUrl(topScript);
        DebuggableScript[] functions = getAllFunctions(topScript);
        if (sourceProvider != null) {
            final String providedSource = sourceProvider.getSource(topScript);
            if (providedSource != null) {
                source = providedSource;
            }
        }

        final SourceInfo sourceInfo = new SourceInfo(source, functions, url);

        synchronized (urlToSourceInfo) {
            SourceInfo old = urlToSourceInfo.get(url);
            if (old != null) {
                sourceInfo.copyBreakpointsFrom(old);
            }
            urlToSourceInfo.put(url, sourceInfo);
            for (int i = 0; i != sourceInfo.functionSourcesTop(); ++i) {
                FunctionSource fsource = sourceInfo.functionSource(i);
                String name = fsource.name();
                if (name.length() != 0) {
                    functionNames.put(name, fsource);
                }
            }
        }

        synchronized (functionToSource) {
            for (int i = 0; i != functions.length; ++i) {
                FunctionSource fsource = sourceInfo.functionSource(i);
                functionToSource.put(functions[i], fsource);
            }
        }

        callback.updateSourceText(sourceInfo);
    }

    /** Returns the FunctionSource object for the given function or script. */
    private FunctionSource functionSource(DebuggableScript fnOrScript) {
        return functionToSource.get(fnOrScript);
    }

    /** Returns an array of all function names. */
    public String[] functionNames() {
        synchronized (urlToSourceInfo) {
            return functionNames.keySet().toArray(new String[functionNames.size()]);
        }
    }

    /** Returns the FunctionSource object for the function with the given name. */
    public FunctionSource functionSourceByName(String functionName) {
        return functionNames.get(functionName);
    }

    /** Returns the SourceInfo object for the given URL. */
    public SourceInfo sourceInfo(String url) {
        return urlToSourceInfo.get(url);
    }

    /** Returns the source URL for the given script or function. */
    private String getNormalizedUrl(DebuggableScript fnOrScript) {
        String url = fnOrScript.getSourceName();
        if (url == null) {
            url = "<stdin>";
        } else {
            // Not to produce window for eval from different lines,
            // strip line numbers, i.e. replace all #[0-9]+\(eval\) by
            // (eval)
            // Option: similar teatment for Function?
            char evalSeparator = '#';
            StringBuilder sb = null;
            int urlLength = url.length();
            int cursor = 0;
            for (; ; ) {
                int searchStart = url.indexOf(evalSeparator, cursor);
                if (searchStart < 0) {
                    break;
                }
                String replace = null;
                int i = searchStart + 1;
                while (i != urlLength) {
                    int c = url.charAt(i);
                    if (!('0' <= c && c <= '9')) {
                        break;
                    }
                    ++i;
                }
                if (i != searchStart + 1) {
                    // i points after #[0-9]+
                    if ("(eval)".regionMatches(0, url, i, 6)) {
                        cursor = i + 6;
                        replace = "(eval)";
                    }
                }
                if (replace == null) {
                    break;
                }
                if (sb == null) {
                    sb = new StringBuilder();
                    sb.append(url.substring(0, searchStart));
                }
                sb.append(replace);
            }
            if (sb != null) {
                if (cursor != urlLength) {
                    sb.append(url.substring(cursor));
                }
                url = sb.toString();
            }
        }
        return url;
    }

    /** Returns an array of all functions in the given script. */
    private static DebuggableScript[] getAllFunctions(DebuggableScript function) {
        ArrayList<DebuggableScript> functions = new ArrayList<>();
        collectFunctions_r(function, functions);
        DebuggableScript[] result = new DebuggableScript[functions.size()];
        functions.toArray(result);
        return result;
    }

    /** Helper function for {@link #getAllFunctions(DebuggableScript)}. */
    private static void collectFunctions_r(
            DebuggableScript function, List<DebuggableScript> array) {
        array.add(function);
        for (int i = 0; i != function.getFunctionCount(); ++i) {
            collectFunctions_r(function.getFunction(i), array);
        }
    }

    /** Clears all breakpoints. */
    public void clearAllBreakpoints() {
        for (SourceInfo si : urlToSourceInfo.values()) {
            si.removeAllBreakpoints();
        }
    }

    /** Called when a breakpoint has been hit. */
    private void handleBreakpointHit(StackFrame frame, Context cx) {
        breakFlag = false;
        interrupted(cx, frame, null);
    }

    /** Called when a script exception has been thrown. */
    private void handleExceptionThrown(Context cx, Throwable ex, StackFrame frame) {
        if (breakOnExceptions) {
            ContextData cd = frame.contextData();
            if (cd.lastProcessedException != ex) {
                interrupted(cx, frame, ex);
                cd.lastProcessedException = ex;
            }
        }
    }

    /** Returns the current ContextData object. */
    public ContextData currentContextData() {
        return interruptedContextData;
    }

    /** Sets the action to perform to end interruption. */
    public void setReturnValue(int returnValue) {
        synchronized (monitor) {
            this.returnValue = returnValue;
            monitor.notify();
        }
    }

    /** Resumes execution of script. */
    public void go() {
        synchronized (monitor) {
            this.returnValue = GO;
            monitor.notifyAll();
        }
    }

    /** Evaluates the given script. */
    public String eval(String expr) {
        String result = "undefined";
        if (expr == null) {
            return result;
        }
        ContextData contextData = currentContextData();
        if (contextData == null || frameIndex >= contextData.frameCount()) {
            return result;
        }
        StackFrame frame = contextData.getFrame(frameIndex);
        if (contextData.eventThreadFlag) {
            Context cx = Context.getCurrentContext();
            result = do_eval(cx, frame, expr);
        } else {
            synchronized (monitor) {
                if (insideInterruptLoop) {
                    evalRequest = expr;
                    evalFrame = frame;
                    monitor.notify();
                    do {
                        try {
                            monitor.wait();
                        } catch (InterruptedException exc) {
                            Thread.currentThread().interrupt();
                            break;
                        }
                    } while (evalRequest != null);
                    result = evalResult;
                }
            }
        }
        return result;
    }

    /** Compiles the given script. */
    public void compileScript(String url, String text) {
        DimIProxy action = new DimIProxy(this, IPROXY_COMPILE_SCRIPT);
        action.url = url;
        action.text = text;
        action.withContext();
    }

    /** Evaluates the given script. */
    public void evalScript(final String url, final String text) {
        DimIProxy action = new DimIProxy(this, IPROXY_EVAL_SCRIPT);
        action.url = url;
        action.text = text;
        action.withContext();
    }

    /** Converts the given script object to a string. */
    public String objectToString(Object object) {
        DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_TO_STRING);
        action.object = object;
        action.withContext();
        return action.stringResult;
    }

    /** Returns whether the given string is syntactically valid script. */
    public boolean stringIsCompilableUnit(String str) {
        DimIProxy action = new DimIProxy(this, IPROXY_STRING_IS_COMPILABLE);
        action.text = str;
        action.withContext();
        return action.booleanResult;
    }

    /** Returns the value of a property on the given script object. */
    public Object getObjectProperty(Object object, Object id) {
        DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_PROPERTY);
        action.object = object;
        action.id = id;
        action.withContext();
        return action.objectResult;
    }

    /** Returns an array of the property names on the given script object. */
    public Object[] getObjectIds(Object object) {
        DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_IDS);
        action.object = object;
        action.withContext();
        return action.objectArrayResult;
    }

    /** Returns the value of a property on the given script object. */
    private Object getObjectPropertyImpl(Context cx, Object object, Object id) {
        Scriptable scriptable = (Scriptable) object;
        Object result;
        if (id instanceof String) {
            String name = (String) id;
            if (name.equals("this")) {
                result = scriptable;
            } else if (name.equals("__proto__")) {
                result = scriptable.getPrototype();
            } else if (name.equals("__parent__")) {
                result = scriptable.getParentScope();
            } else {
                result = ScriptableObject.getProperty(scriptable, name);
                if (result == ScriptableObject.NOT_FOUND) {
                    result = Undefined.instance;
                }
            }
        } else {
            int index = ((Integer) id).intValue();
            result = ScriptableObject.getProperty(scriptable, index);
            if (result == ScriptableObject.NOT_FOUND) {
                result = Undefined.instance;
            }
        }
        return result;
    }

    /** Returns an array of the property names on the given script object. */
    private Object[] getObjectIdsImpl(Context cx, Object object) {
        if (!(object instanceof Scriptable) || object == Undefined.instance) {
            return Context.emptyArgs;
        }

        Object[] ids;
        Scriptable scriptable = (Scriptable) object;
        if (scriptable instanceof DebuggableObject) {
            ids = ((DebuggableObject) scriptable).getAllIds();
        } else {
            ids = scriptable.getIds();
        }

        Scriptable proto = scriptable.getPrototype();
        Scriptable parent = scriptable.getParentScope();
        int extra = 0;
        if (proto != null) {
            ++extra;
        }
        if (parent != null) {
            ++extra;
        }
        if (extra != 0) {
            Object[] tmp = new Object[extra + ids.length];
            System.arraycopy(ids, 0, tmp, extra, ids.length);
            ids = tmp;
            extra = 0;
            if (proto != null) {
                ids[extra++] = "__proto__";
            }
            if (parent != null) {
                ids[extra++] = "__parent__";
            }
        }

        return ids;
    }

    /** Interrupts script execution. */
    private void interrupted(Context cx, final StackFrame frame, Throwable scriptException) {
        ContextData contextData = frame.contextData();
        boolean eventThreadFlag = callback.isGuiEventThread();
        contextData.eventThreadFlag = eventThreadFlag;

        boolean recursiveEventThreadCall = false;

        interruptedCheck:
        synchronized (eventThreadMonitor) {
            if (eventThreadFlag) {
                if (interruptedContextData != null) {
                    recursiveEventThreadCall = true;
                    break interruptedCheck;
                }
            } else {
                while (interruptedContextData != null) {
                    try {
                        eventThreadMonitor.wait();
                    } catch (InterruptedException exc) {
                        return;
                    }
                }
            }
            interruptedContextData = contextData;
        }

        if (recursiveEventThreadCall) {
            // XXX: For now the following is commented out as on Linux
            // too deep recursion of dispatchNextGuiEvent causes GUI lockout.
            // Note: it can make GUI unresponsive if long-running script
            // will be called on GUI thread while processing another interrupt
            if (false) {
                // Run event dispatch until gui sets a flag to exit the initial
                // call to interrupted.
                while (this.returnValue == -1) {
                    try {
                        callback.dispatchNextGuiEvent();
                    } catch (InterruptedException exc) {
                    }
                }
            }
            return;
        }

        if (interruptedContextData == null) Kit.codeBug();

        try {
            do {
                int frameCount = contextData.frameCount();
                this.frameIndex = frameCount - 1;

                final String threadTitle = Thread.currentThread().toString();
                final String alertMessage;
                if (scriptException == null) {
                    alertMessage = null;
                } else {
                    alertMessage = scriptException.toString();
                }

                int returnValue = -1;
                if (!eventThreadFlag) {
                    synchronized (monitor) {
                        if (insideInterruptLoop) Kit.codeBug();
                        this.insideInterruptLoop = true;
                        this.evalRequest = null;
                        this.returnValue = -1;
                        callback.enterInterrupt(frame, threadTitle, alertMessage);
                        try {
                            for (; ; ) {
                                try {
                                    monitor.wait();
                                } catch (InterruptedException exc) {
                                    Thread.currentThread().interrupt();
                                    break;
                                }
                                if (evalRequest != null) {
                                    this.evalResult = null;
                                    try {
                                        evalResult = do_eval(cx, evalFrame, evalRequest);
                                    } finally {
                                        evalRequest = null;
                                        evalFrame = null;
                                        monitor.notify();
                                    }
                                    continue;
                                }
                                if (this.returnValue != -1) {
                                    returnValue = this.returnValue;
                                    break;
                                }
                            }
                        } finally {
                            insideInterruptLoop = false;
                        }
                    }
                } else {
                    this.returnValue = -1;
                    callback.enterInterrupt(frame, threadTitle, alertMessage);
                    while (this.returnValue == -1) {
                        try {
                            callback.dispatchNextGuiEvent();
                        } catch (InterruptedException exc) {
                        }
                    }
                    returnValue = this.returnValue;
                }
                switch (returnValue) {
                    case STEP_OVER:
                        contextData.breakNextLine = true;
                        contextData.stopAtFrameDepth = contextData.frameCount();
                        break;
                    case STEP_INTO:
                        contextData.breakNextLine = true;
                        contextData.stopAtFrameDepth = -1;
                        break;
                    case STEP_OUT:
                        if (contextData.frameCount() > 1) {
                            contextData.breakNextLine = true;
                            contextData.stopAtFrameDepth = contextData.frameCount() - 1;
                        }
                        break;
                }
            } while (false);
        } finally {
            synchronized (eventThreadMonitor) {
                interruptedContextData = null;
                eventThreadMonitor.notifyAll();
            }
        }
    }

    /** Evaluates script in the given stack frame. */
    private static String do_eval(Context cx, StackFrame frame, String expr) {
        String resultString;
        Debugger saved_debugger = cx.getDebugger();
        Object saved_data = cx.getDebuggerContextData();
        boolean wasInterpreted = cx.isInterpretedMode();

        cx.setDebugger(null, null);
        cx.setInterpretedMode(false);
        cx.setGeneratingDebug(false);
        try {
            Callable script = (Callable) cx.compileString(expr, "", 0, null);
            Object result = script.call(cx, frame.scope, frame.thisObj, ScriptRuntime.emptyArgs);
            if (result == Undefined.instance) {
                resultString = "";
            } else {
                resultString = ScriptRuntime.toString(result);
            }
        } catch (Exception exc) {
            resultString = exc.getMessage();
        } finally {
            cx.setGeneratingDebug(true);
            cx.setInterpretedMode(wasInterpreted);
            cx.setDebugger(saved_debugger, saved_data);
        }
        if (resultString == null) {
            resultString = "null";
        }
        return resultString;
    }

    /** Proxy class to implement debug interfaces without bloat of class files. */
    private static class DimIProxy implements ContextAction, ContextFactory.Listener, Debugger {

        /** The debugger. */
        private Dim dim;

        /**
         * The interface implementation type. One of the IPROXY_* constants defined in {@link Dim}.
         */
        private int type;

        /** The URL origin of the script to compile or evaluate. */
        private String url;

        /** The text of the script to compile, evaluate or test for compilation. */
        private String text;

        /** The object to convert, get a property from or enumerate. */
        private Object object;

        /** The property to look up in {@link #object}. */
        private Object id;

        /** The boolean result of the action. */
        private boolean booleanResult;

        /** The String result of the action. */
        private String stringResult;

        /** The Object result of the action. */
        private Object objectResult;

        /** The Object[] result of the action. */
        private Object[] objectArrayResult;

        /** Creates a new DimIProxy. */
        private DimIProxy(Dim dim, int type) {
            this.dim = dim;
            this.type = type;
        }

        // ContextAction

        /** Performs the action given by {@link #type}. */
        @Override
        public Object run(Context cx) {
            switch (type) {
                case IPROXY_COMPILE_SCRIPT:
                    cx.compileString(text, url, 1, null);
                    break;

                case IPROXY_EVAL_SCRIPT:
                    {
                        Scriptable scope = null;
                        if (dim.scopeProvider != null) {
                            scope = dim.scopeProvider.getScope();
                        }
                        if (scope == null) {
                            scope = new ImporterTopLevel(cx);
                        }
                        cx.evaluateString(scope, text, url, 1, null);
                    }
                    break;

                case IPROXY_STRING_IS_COMPILABLE:
                    booleanResult = cx.stringIsCompilableUnit(text);
                    break;

                case IPROXY_OBJECT_TO_STRING:
                    if (object == Undefined.instance) {
                        stringResult = "undefined";
                    } else if (object == null) {
                        stringResult = "null";
                    } else if (object instanceof NativeCall) {
                        stringResult = "[object Call]";
                    } else {
                        stringResult = Context.toString(object);
                    }
                    break;

                case IPROXY_OBJECT_PROPERTY:
                    objectResult = dim.getObjectPropertyImpl(cx, object, id);
                    break;

                case IPROXY_OBJECT_IDS:
                    objectArrayResult = dim.getObjectIdsImpl(cx, object);
                    break;

                default:
                    throw Kit.codeBug();
            }
            return null;
        }

        /** Performs the action given by {@link #type} with the attached {@link ContextFactory}. */
        @SuppressWarnings("unchecked")
        private void withContext() {
            dim.contextFactory.call(this);
        }

        // ContextFactory.Listener

        /** Called when a Context is created. */
        @Override
        public void contextCreated(Context cx) {
            if (type != IPROXY_LISTEN) Kit.codeBug();
            ContextData contextData = new ContextData();
            Debugger debugger = new DimIProxy(dim, IPROXY_DEBUG);
            cx.setDebugger(debugger, contextData);
            cx.setGeneratingDebug(true);
            cx.setInterpretedMode(true);
        }

        /** Called when a Context is destroyed. */
        @Override
        public void contextReleased(Context cx) {
            if (type != IPROXY_LISTEN) Kit.codeBug();
        }

        // Debugger

        /** Returns a StackFrame for the given function or script. */
        @Override
        public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) {
            if (type != IPROXY_DEBUG) Kit.codeBug();

            FunctionSource item = dim.getFunctionSource(fnOrScript);
            if (item == null) {
                // Can not debug if source is not available
                return null;
            }
            return new StackFrame(cx, dim, item);
        }

        /** Called when compilation is finished. */
        @Override
        public void handleCompilationDone(Context cx, DebuggableScript fnOrScript, String source) {
            if (type != IPROXY_DEBUG) Kit.codeBug();

            if (!fnOrScript.isTopLevel()) {
                return;
            }
            dim.registerTopScript(fnOrScript, source);
        }
    }

    /** Class to store information about a stack. */
    public static class ContextData {

        /** The stack frames. */
        private ArrayList<StackFrame> frameStack = new ArrayList<>();

        /** Whether the debugger should break at the next line in this context. */
        private boolean breakNextLine;

        /**
         * The frame depth the debugger should stop at. Used to implement "step over" and "step
         * out".
         */
        private int stopAtFrameDepth = -1;

        /** Whether this context is in the event thread. */
        private boolean eventThreadFlag;

        /** The last exception that was processed. */
        private Throwable lastProcessedException;

        /** Returns the ContextData for the given Context. */
        public static ContextData get(Context cx) {
            return (ContextData) cx.getDebuggerContextData();
        }

        /** Returns the number of stack frames. */
        public int frameCount() {
            return frameStack.size();
        }

        /** Returns the stack frame with the given index. */
        public StackFrame getFrame(int frameNumber) {
            int num = frameStack.size() - frameNumber - 1;
            return (StackFrame) frameStack.get(num);
        }

        /** Pushes a stack frame on to the stack. */
        private void pushFrame(StackFrame frame) {
            frameStack.add(frame);
        }

        /** Pops a stack frame from the stack. */
        private void popFrame() {
            frameStack.remove(frameStack.size() - 1);
        }
    }

    /** Object to represent one stack frame. */
    public static class StackFrame implements DebugFrame {

        /** The debugger. */
        private Dim dim;

        /** The ContextData for the Context being debugged. */
        private ContextData contextData;

        /** The scope. */
        private Scriptable scope;

        /** The 'this' object. */
        private Scriptable thisObj;

        /** Information about the function. */
        private FunctionSource fsource;

        /** Array of breakpoint state for each source line. */
        private boolean[] breakpoints;

        /** Current line number. */
        private int lineNumber;

        /** Creates a new StackFrame. */
        private StackFrame(Context cx, Dim dim, FunctionSource fsource) {
            this.dim = dim;
            this.contextData = ContextData.get(cx);
            this.fsource = fsource;
            this.breakpoints = fsource.sourceInfo().breakpoints;
            this.lineNumber = fsource.firstLine();
        }

        /** Called when the stack frame is entered. */
        @Override
        public void onEnter(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
            contextData.pushFrame(this);
            this.scope = scope;
            this.thisObj = thisObj;
            if (dim.breakOnEnter) {
                dim.handleBreakpointHit(this, cx);
            }
        }

        /** Called when the current position has changed. */
        @Override
        public void onLineChange(Context cx, int lineno) {
            this.lineNumber = lineno;

            if (!breakpoints[lineno] && !dim.breakFlag) {
                boolean lineBreak = contextData.breakNextLine;
                if (lineBreak && contextData.stopAtFrameDepth >= 0) {
                    lineBreak = (contextData.frameCount() <= contextData.stopAtFrameDepth);
                }
                if (!lineBreak) {
                    return;
                }
                contextData.stopAtFrameDepth = -1;
                contextData.breakNextLine = false;
            }

            dim.handleBreakpointHit(this, cx);
        }

        /** Called when an exception has been thrown. */
        @Override
        public void onExceptionThrown(Context cx, Throwable exception) {
            dim.handleExceptionThrown(cx, exception, this);
        }

        /** Called when the stack frame has been left. */
        @Override
        public void onExit(Context cx, boolean byThrow, Object resultOrException) {
            if (dim.breakOnReturn && !byThrow) {
                dim.handleBreakpointHit(this, cx);
            }
            contextData.popFrame();
        }

        /** Called when a 'debugger' statement is executed. */
        @Override
        public void onDebuggerStatement(Context cx) {
            dim.handleBreakpointHit(this, cx);
        }

        /** Returns the SourceInfo object for the function. */
        public SourceInfo sourceInfo() {
            return fsource.sourceInfo();
        }

        /** Returns the ContextData object for the Context. */
        public ContextData contextData() {
            return contextData;
        }

        /** Returns the scope object for this frame. */
        public Object scope() {
            return scope;
        }

        /** Returns the 'this' object for this frame. */
        public Object thisObj() {
            return thisObj;
        }

        /** Returns the source URL. */
        public String getUrl() {
            return fsource.sourceInfo().url();
        }

        /** Returns the current line number. */
        public int getLineNumber() {
            return lineNumber;
        }

        /** Returns the current function name. */
        public String getFunctionName() {
            return fsource.name();
        }
    }

    /** Class to store information about a function. */
    public static class FunctionSource {

        /** Information about the source of the function. */
        private SourceInfo sourceInfo;

        /** Line number of the first line of the function. */
        private int firstLine;

        /** The function name. */
        private String name;

        /** Creates a new FunctionSource. */
        private FunctionSource(SourceInfo sourceInfo, int firstLine, String name) {
            if (name == null) throw new IllegalArgumentException();
            this.sourceInfo = sourceInfo;
            this.firstLine = firstLine;
            this.name = name;
        }

        /** Returns the SourceInfo object that describes the source of the function. */
        public SourceInfo sourceInfo() {
            return sourceInfo;
        }

        /** Returns the line number of the first line of the function. */
        public int firstLine() {
            return firstLine;
        }

        /** Returns the name of the function. */
        public String name() {
            return name;
        }
    }

    /** Class to store information about a script source. */
    public static class SourceInfo {

        /** An empty array of booleans. */
        private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];

        /** The script. */
        private String source;

        /** The URL of the script. */
        private String url;

        /** Array indicating which lines can have breakpoints set. */
        private boolean[] breakableLines;

        /** Array indicating whether a breakpoint is set on the line. */
        private boolean[] breakpoints;

        /** Lock for same */
        private final Object breakpointsLock = new Object();

        /** Array of FunctionSource objects for the functions in the script. */
        private FunctionSource[] functionSources;

        /** Creates a new SourceInfo object. */
        private SourceInfo(String source, DebuggableScript[] functions, String normilizedUrl) {
            this.source = source;
            this.url = normilizedUrl;

            int N = functions.length;
            int[][] lineArrays = new int[N][];
            for (int i = 0; i != N; ++i) {
                lineArrays[i] = functions[i].getLineNumbers();
            }

            int minAll = 0, maxAll = -1;
            int[] firstLines = new int[N];
            for (int i = 0; i != N; ++i) {
                int[] lines = lineArrays[i];
                if (lines == null || lines.length == 0) {
                    firstLines[i] = -1;
                } else {
                    int min, max;
                    min = max = lines[0];
                    for (int j = 1; j != lines.length; ++j) {
                        int line = lines[j];
                        if (line < min) {
                            min = line;
                        } else if (line > max) {
                            max = line;
                        }
                    }
                    firstLines[i] = min;
                    if (minAll > maxAll) {
                        minAll = min;
                        maxAll = max;
                    } else {
                        if (min < minAll) {
                            minAll = min;
                        }
                        if (max > maxAll) {
                            maxAll = max;
                        }
                    }
                }
            }

            if (minAll > maxAll) {
                // No line information
                this.breakableLines = EMPTY_BOOLEAN_ARRAY;
                this.breakpoints = EMPTY_BOOLEAN_ARRAY;
            } else {
                if (minAll < 0) {
                    // Line numbers can not be negative
                    throw new IllegalStateException(String.valueOf(minAll));
                }
                int linesTop = maxAll + 1;
                this.breakableLines = new boolean[linesTop];
                this.breakpoints = new boolean[linesTop];
                for (int i = 0; i != N; ++i) {
                    int[] lines = lineArrays[i];
                    if (lines != null && lines.length != 0) {
                        for (int j = 0; j != lines.length; ++j) {
                            int line = lines[j];
                            this.breakableLines[line] = true;
                        }
                    }
                }
            }
            this.functionSources = new FunctionSource[N];
            for (int i = 0; i != N; ++i) {
                String name = functions[i].getFunctionName();
                if (name == null) {
                    name = "";
                }
                this.functionSources[i] = new FunctionSource(this, firstLines[i], name);
            }
        }

        /** Returns the source text. */
        public String source() {
            return this.source;
        }

        /** Returns the script's origin URL. */
        public String url() {
            return this.url;
        }

        /** Returns the number of FunctionSource objects stored in this object. */
        public int functionSourcesTop() {
            return functionSources.length;
        }

        /** Returns the FunctionSource object with the given index. */
        public FunctionSource functionSource(int i) {
            return functionSources[i];
        }

        /** Copies the breakpoints from the given SourceInfo object into this one. */
        private void copyBreakpointsFrom(SourceInfo old) {
            int end = old.breakpoints.length;
            if (end > this.breakpoints.length) {
                end = this.breakpoints.length;
            }
            for (int line = 0; line != end; ++line) {
                if (old.breakpoints[line]) {
                    this.breakpoints[line] = true;
                }
            }
        }

        /** Returns whether the given line number can have a breakpoint set on it. */
        public boolean breakableLine(int line) {
            return (line < this.breakableLines.length) && this.breakableLines[line];
        }

        /** Returns whether there is a breakpoint set on the given line. */
        public boolean breakpoint(int line) {
            if (!breakableLine(line)) {
                throw new IllegalArgumentException(String.valueOf(line));
            }
            return line < this.breakpoints.length && this.breakpoints[line];
        }

        /** Sets or clears the breakpoint flag for the given line. */
        public boolean breakpoint(int line, boolean value) {
            if (!breakableLine(line)) {
                throw new IllegalArgumentException(String.valueOf(line));
            }
            boolean changed;
            synchronized (breakpointsLock) {
                if (breakpoints[line] != value) {
                    breakpoints[line] = value;
                    changed = true;
                } else {
                    changed = false;
                }
            }
            return changed;
        }

        /** Removes all breakpoints from the script. */
        public void removeAllBreakpoints() {
            synchronized (breakpointsLock) {
                for (int line = 0; line != breakpoints.length; ++line) {
                    breakpoints[line] = false;
                }
            }
        }
    }
}
