/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.javascript.v8debug;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.CheckReturnValue;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerInfo;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.io.IOProvider;
import org.netbeans.api.io.InputOutput;
import org.netbeans.api.io.OutputColor;
import org.netbeans.lib.v8debug.PropertyBoolean;
import org.netbeans.lib.v8debug.V8Arguments;
import org.netbeans.lib.v8debug.V8Breakpoint;
import org.netbeans.lib.v8debug.V8Command;
import org.netbeans.lib.v8debug.V8Event;
import org.netbeans.lib.v8debug.V8Request;
import org.netbeans.lib.v8debug.V8Response;
import org.netbeans.lib.v8debug.V8Script;
import org.netbeans.lib.v8debug.connection.ClientConnection;
import org.netbeans.lib.v8debug.connection.IOListener;
import org.netbeans.lib.v8debug.commands.Backtrace;
import org.netbeans.lib.v8debug.commands.ClearBreakpoint;
import org.netbeans.lib.v8debug.commands.Frame;
import org.netbeans.lib.v8debug.commands.Scripts;
import org.netbeans.lib.v8debug.commands.SetBreakpoint;
import org.netbeans.lib.v8debug.events.AfterCompileEventBody;
import org.netbeans.lib.v8debug.events.BreakEventBody;
import org.netbeans.lib.v8debug.events.CompileErrorEventBody;
import org.netbeans.lib.v8debug.events.ExceptionEventBody;
import org.netbeans.lib.v8debug.events.ScriptCollectedEventBody;
import org.netbeans.modules.javascript.v8debug.api.Connector;
import org.netbeans.modules.javascript.v8debug.breakpoints.BreakpointsHandler;
import org.netbeans.modules.javascript.v8debug.frames.CallFrame;
import org.netbeans.modules.javascript.v8debug.frames.CallStack;
import org.netbeans.modules.javascript.v8debug.sources.ChangeLiveSupport;
import org.netbeans.modules.javascript.v8debug.vars.VarValuesLoader;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;

/**
 *
 * @author Martin Entlicher
 */
public final class V8Debugger {
    
    private static final Logger LOG = Logger.getLogger(V8Debugger.class.getName());
    private static final boolean SHOW_V8_PROTOCOL = Boolean.getBoolean("show.v8.protocol");
    
    private final String host;
    private final int port;
    private final ClientConnection connection;
    private final ScriptsHandler scriptsHandler;
    private final ChangeLiveSupport changeLiveSupport;
    private final BreakpointsHandler breakpointsHandler;
    @NullAllowed
    private final Runnable finishCallback;
    private DebuggerEngine engine;
    private DebuggerEngine.Destructor engineDestructor;
    private final RequestProcessor rp = new RequestProcessor(V8Debugger.class);
    private final Set<Listener> listeners = new CopyOnWriteArraySet<>();
    
    private final AtomicLong requestSequence = new AtomicLong(1l);
    private final Map<Long, Pair<V8Request, CommandResponseCallback>> commandCallbacks = new HashMap<>();
    private boolean suspended = false;
    private final Object suspendedUpdateLock = new Object();
    private volatile boolean finished = false;
    private final ReentrantReadWriteLock accessLock = new ReentrantReadWriteLock(true);
    
    private CallFrame currentFrame;
    private CallStack currentCallStack;
    private final Object currentFrameRetrieveLock = new Object();
    private final Object currentCallStackRetrieveLock = new Object();
    
    private long runToBreakpointId = -1;
    private CommandResponseCallback runToCallBack;
    private final ErrorMessageHandler errMsgHandler;
    
    public static DebuggerEngine startSession(Connector.Properties properties,
                                              @NullAllowed Runnable finishCallback) throws IOException {
        V8Debugger dbg = new V8Debugger(properties, finishCallback);
        VarValuesLoader vvl = new VarValuesLoader(dbg);
        DebuggerInfo dInfo = DebuggerInfo.create(V8DebuggerSessionProvider.DEBUG_INFO, new Object[]{ dbg, vvl });
        DebuggerEngine[] engines = DebuggerManager.getDebuggerManager().startDebugging(dInfo);
        if (engines.length > 0) {
            dbg.setEngine(engines[0]);
            return engines[0];
        } else {
            if (finishCallback != null) {
                finishCallback.run();
            }
            return null;
        }
    }

    private V8Debugger(Connector.Properties properties, Runnable finishCallback) throws IOException {
        this.host = properties.getHostName();
        this.port = properties.getPort();
        this.connection = new ClientConnection(properties.getHostName(), properties.getPort());
        if (SHOW_V8_PROTOCOL) {
            connection.addIOListener(new CommListener());
        }
        this.scriptsHandler = new ScriptsHandler(properties.getLocalPaths(),
                                                 properties.getServerPaths(),
                                                 properties.getLocalPathExclusionFilters(),
                                                 this);
        this.changeLiveSupport = new ChangeLiveSupport(this);
        this.breakpointsHandler = new BreakpointsHandler(this);
        this.finishCallback = finishCallback;
        errMsgHandler = Lookup.getDefault().lookup(ErrorMessageHandler.class);
    }

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
    
    public void start() {
        spawnResponseLoop();
        initScripts();
    }
    
    public void finish() {
        if (finished) {
            return ;
        }
        finished = true;
        try {
            connection.close();
        } catch (IOException ioex) {}
        notifyFinished();
        finishCallbacks();
        engineDestructor.killEngine();
        if (finishCallback != null) {
            finishCallback.run();
        }
    }
    
    public boolean isFinished() {
        return finished;
    }
    
    public ScriptsHandler getScriptsHandler() {
        return scriptsHandler;
    }
    
    public ChangeLiveSupport getChangeLiveSupport() {
        return changeLiveSupport;
    }
    
    public BreakpointsHandler getBreakpointsHandler() {
        return breakpointsHandler;
    }
    
    @CheckForNull
    public V8Request sendCommandRequest(V8Command cmd, V8Arguments args) {
        return sendCommandRequest(cmd, args, null);
    }
    
    @CheckForNull
    @CheckReturnValue
    public V8Request sendCommandRequest(V8Command cmd, V8Arguments args, CommandResponseCallback callback) {
        V8Request request = new V8Request(requestSequence.getAndIncrement(), cmd, args);
        if (callback != null) {
            synchronized (commandCallbacks) {
                commandCallbacks.put(request.getSequence(), Pair.of(request, callback));
            }
        }
        if (cmd.equals(V8Command.Continue) || cmd.equals(V8Command.Restartframe)) {
            // Going to resume...
            notifySuspended(false);
        }
        try {
            connection.send(request);
            return request;
        } catch (IOException ex) {
            return null;
        }
    }
    
    public void suspend() {
        sendCommandRequest(V8Command.Suspend, null);
    }
    
    public void resume() {
        sendCommandRequest(V8Command.Continue, null);
    }
    
    public boolean isSuspended() {
        assert !accessLock.isWriteLockedByCurrentThread();
        synchronized (suspendedUpdateLock) {
            return suspended;
        }
    }
    
    @CheckForNull
    public CallFrame getCurrentFrame() {
        CallFrame f;
        Lock rl = accessLock.readLock();
        rl.lock();
        try {
            f = currentFrame;
            //LOG.fine("getCurrentFrame(): currentFrame = "+f+", currentCallStack = "+currentCallStack);
            if (f == null && currentCallStack != null) {
                rl.unlock();
                rl = null;
                synchronized (currentFrameRetrieveLock) {
                    f = currentCallStack.getTopFrame();
                    if (f != null) {
                        Lock wl = accessLock.writeLock();
                        wl.lock();
                        try {
                            currentFrame = f;
                        } finally {
                            wl.unlock();
                        }
                    }
                }
                if (f != null) {
                    fireCurrentFrame(f);
                    return f;
                }
            }
            //LOG.fine("  getCurrentFrame(): f = "+f+", isSuspended() = "+isSuspended());
            if (f == null && isSuspended()) {
                final CallFrame[] fRef = new CallFrame[] { null };
                if (rl != null) {
                    rl.unlock();
                    rl = null;
                }
                // Synchronize not to start retrieving the current frame multiple times at once
                synchronized (currentFrameRetrieveLock) {
                    rl = accessLock.readLock();
                    rl.lock();
                    try {
                        // Things might have changed:
                        if (currentFrame != null || !isSuspended()) {
                            return currentFrame;
                        }
                    } finally {
                        rl.unlock();
                        rl = null;
                    }
                    //if (currentFrame != null) 
                    V8Request request = sendCommandRequest(V8Command.Frame, null, new CommandResponseCallback() {
                        @Override
                        public void notifyResponse(V8Request request, V8Response response) {
                            if (response != null && response.isSuccess()) {
                                Frame.ResponseBody frb = (Frame.ResponseBody) response.getBody();
                                fRef[0] = new CallFrame(V8Debugger.this, frb.getFrame(), new ReferencedValues(response), true);
                            }
                            synchronized (currentFrameRetrieveLock) {
                                currentFrameRetrieveLock.notifyAll();
                            }
                        }
                    });
                    if (request != null) {
                        try {
                            currentFrameRetrieveLock.wait();
                        } catch (InterruptedException iex) {}
                    }
                    f = fRef[0];
                    Lock wl = accessLock.writeLock();
                    wl.lock();
                    try {
                        currentFrame = f;
                    } finally {
                        wl.unlock();
                    }
                }
                fireCurrentFrame(f);
            }
        } finally {
            if (rl != null) {
                rl.unlock();
            }
        }
        //LOG.fine("getCurrentFrame(): return "+f);
        return f;
    }

    public void setCurrentFrame(@NonNull CallFrame callFrame) {
        assert callFrame != null: "Only existing frames can be set";
        Lock wl = accessLock.writeLock();
        wl.lock();
        try {
            this.currentFrame = callFrame;
        } finally {
            wl.unlock();
        }
        fireCurrentFrame(callFrame);
    }
    
    private void fireCurrentFrame(CallFrame cf) {
        for (Listener l : listeners) {
            l.notifyCurrentFrame(cf);
        }
    }
    
    public CallStack getCurrentCallStack() {
        CallStack cs;
        Lock rl = accessLock.readLock();
        rl.lock();
        try {
            cs = currentCallStack;
            //LOG.fine("getCurrentCallStack(): currentCallStack = "+cs+", isSuspended() = "+isSuspended());
            if (cs == null && isSuspended()) {
                final CallStack[] csRef = new CallStack[] { null };
                rl.unlock();
                rl = null;
                // Synchronize not to start retrieving the current frame multiple times at once
                synchronized (currentCallStackRetrieveLock) {
                    rl = accessLock.readLock();
                    rl.lock();
                    try {
                        // Things might have changed:
                        if (currentCallStack != null && suspended) {
                            //LOG.fine("getCurrentCallStack(): currentCallStack computed in the mean time: "+currentCallStack);
                            return currentCallStack;
                        }
                    } finally {
                        rl.unlock();
                        rl = null;
                    }
                    //LOG.fine("getCurrentCallStack(): retrieving the number of frames...");
                    // Find the number of frames first:
                    Backtrace.Arguments bta = new Backtrace.Arguments(0l, 1l, false, true);
                    final Object csRetrievingLock = new Object();
                    synchronized (csRetrievingLock) {
                        V8Request request = sendCommandRequest(V8Command.Backtrace, bta, new CommandResponseCallback() {
                            @Override
                            public void notifyResponse(V8Request request, V8Response response) {
                                if (response == null) {
                                    synchronized (csRetrievingLock) {
                                        csRetrievingLock.notifyAll();
                                    }
                                    return ;
                                }
                                Backtrace.ResponseBody btrb = (Backtrace.ResponseBody) response.getBody();
                                long numFrames = btrb.getTotalFrames();
                                //LOG.fine("getCurrentCallStack(): "+numFrames+" frames retrieved.");
                                if (numFrames == 0) {
                                    synchronized (csRetrievingLock) {
                                        csRef[0] = CallStack.EMPTY;
                                        csRetrievingLock.notifyAll();
                                    }
                                }
                                if (numFrames == 1l) {
                                    synchronized (csRetrievingLock) {
                                        csRef[0] = new CallStack(V8Debugger.this, btrb.getFrames(), response.getReferencedValues());
                                        csRetrievingLock.notifyAll();
                                    }
                                } else {
                                    //LOG.fine("getCurrentCallStack(): retrieving "+numFrames+" frames...");
                                    // Find numFrames frames now:
                                    Backtrace.Arguments bta = new Backtrace.Arguments(0l, numFrames, false, true);
                                    V8Request request2 = sendCommandRequest(V8Command.Backtrace, bta, new CommandResponseCallback() {
                                        @Override
                                        public void notifyResponse(V8Request request, V8Response response) {
                                            if (response != null) {
                                                Backtrace.ResponseBody btrb = (Backtrace.ResponseBody) response.getBody();
                                                csRef[0] = new CallStack(V8Debugger.this, btrb.getFrames(), response.getReferencedValues());
                                                //LOG.fine("getCurrentCallStack(): All frames retrieved: "+csRef[0]);
                                            }
                                            synchronized (csRetrievingLock) {
                                                csRetrievingLock.notifyAll();
                                            }
                                        }
                                    });
                                    if (request2 == null) {
                                        // Failed, notify
                                        synchronized (csRetrievingLock) {
                                            csRetrievingLock.notifyAll();
                                        }
                                    }
                                }
                            }
                        });
                        if (request != null) {
                            try {
                                csRetrievingLock.wait();
                            } catch (InterruptedException iex) {}
                        }
                    }
                    cs = csRef[0];
                    Lock wl = accessLock.writeLock();
                    wl.lock();
                    try {
                        currentCallStack = cs;
                        //LOG.fine("getCurrentCallStack(): set currentCallStack to "+cs);
                    } finally {
                        wl.unlock();
                    }
                }
            }
        } finally {
            if (rl != null) {
                rl.unlock();
            }
        }
        //LOG.fine("getCurrentCallStack(): return "+cs);
        return cs;
    }

    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    private void spawnResponseLoop() {
        new RequestProcessor("V8Debugger Response Loop").post(new Runnable() {
            @Override
            public void run() {
                responseLoop();
            }
        });
    }
    
    private void responseLoop() {
        try {
        connection.runEventLoop(new ClientConnection.Listener() {

            @Override
            public void header(Map<String, String> properties) {
                for (Map.Entry pe : properties.entrySet()) {
                    LOG.fine("  "+pe.getKey() + " = " + pe.getValue());
                }
            }
            
            @Override
            public void response(V8Response response) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("response: "+response+", success = "+response.isSuccess()+", running = "+response.isRunning()+", body = "+response.getBody());
                }
                handleResponse(response);
            }

            @Override
            public void event(V8Event event) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("event: "+event+", name = "+event.getKind());
                }
                handleEvent(event);
            }

        });
        } catch (IOException ioex) {
            LOG.log(Level.FINE, null, ioex);
            Throwable cause = ioex.getCause();
            if (cause != null) {
                Exceptions.printStackTrace(cause);
            }
        } finally {
            try {
                connection.close();
            } catch (IOException ioex) {
            } finally {
                finish();
            }
        }
    }
    
    private void handleResponse(V8Response response) {
        long requestSequenceNum = response.getRequestSequence();
        synchronized (commandCallbacks) {
            Pair<V8Request, CommandResponseCallback> callback;
            synchronized (commandCallbacks) {
                callback = commandCallbacks.remove(requestSequenceNum);
            }
            if (callback != null) {
                callback.second().notifyResponse(callback.first(), response);
            }
        }
        String errorMessage = response.getErrorMessage();
        if (errorMessage != null) {
            if (errMsgHandler != null) {
                errMsgHandler.errorResponse(errorMessage);
            }
            return ;
        }
        if (!response.isSuccess()) {
            return ;
        }
        notifySuspended(!response.isRunning());
        switch (response.getCommand()) {
            
        }
    }

    private void handleEvent(V8Event event) {
        PropertyBoolean running = event.isRunning();
        V8Event.Kind eventKind = event.getKind();
        if (running.hasValue()) {
            notifySuspended(!event.isRunning().getValue());
        } else if (eventKind == V8Event.Kind.Break ||
                   eventKind == V8Event.Kind.Exception) {
            notifySuspended(true);
        }
        PropertyBoolean success = event.getSuccess();
        if (success.hasValue() && !success.getValue() && event.getErrorMessage() != null) {
            // an error is reported
            if (errMsgHandler != null) {
                errMsgHandler.errorEvent(event.getErrorMessage());
            }
            return ;
        }
        switch (eventKind) {
            case AfterCompile:
                AfterCompileEventBody aceb = (AfterCompileEventBody) event.getBody();
                V8Script script = aceb.getScript();
                scriptsHandler.add(script);
                break;
            case CompileError:
                CompileErrorEventBody ceeb = (CompileErrorEventBody) event.getBody();
                script = ceeb.getScript();
                scriptsHandler.add(script);
                break;
            case ScriptCollected:
                ScriptCollectedEventBody sceb = (ScriptCollectedEventBody) event.getBody();
                long scriptId = sceb.getScriptId();
                scriptsHandler.remove(scriptId);
                break;
            case Break:
                BreakEventBody beb = (BreakEventBody) event.getBody();
                long[] breakpoints = beb.getBreakpoints();
                if (breakpoints != null && breakpoints.length > 0) {
                    if (runToBreakpointId > 0) {
                        for (long b : breakpoints) {
                            if (b == runToBreakpointId) {
                                clearRunTo();
                                if (breakpoints.length == 1) {
                                    return ;
                                }
                            }
                        }
                    }
                    breakpointsHandler.event(beb);
                }
                //System.out.println("stopped at "+beb.getScript().getName()+", line = "+(beb.getSourceLine()+1)+" : "+beb.getSourceColumn()+"\ntext = "+beb.getSourceLineText());
                break;
            case Exception:
                ExceptionEventBody eeb = (ExceptionEventBody) event.getBody();
                //System.out.println("exception '"+eeb.getException()+"' stopped in "+eeb.getScript().getName()+", line = "+(eeb.getSourceLine()+1)+" : "+eeb.getSourceColumn()+"\ntext = "+eeb.getSourceLineText());
                break;
            default:
                LOG.info("Unknown event: "+event.getKind());
        }
    }
    
    private void notifySuspended(boolean suspended) {
        assert !accessLock.isWriteLockedByCurrentThread();
        LOG.fine("notifySuspended("+suspended+")");
        boolean fireNullCF = false;
        synchronized (suspendedUpdateLock) {
            if (this.suspended == suspended) {
                return ;
            }
            Lock wl = accessLock.writeLock();
            wl.lock();
            try {
                if (!suspended) {
                    // Resumed
                    fireNullCF = currentFrame != null;
                    currentFrame = null;
                    currentCallStack = null;
                }
                this.suspended = suspended;
                LOG.fine("notifySuspended():  suspended property is set to "+suspended);
            } finally {
                wl.unlock();
            }
        }
        if (fireNullCF) {
            for (Listener l : listeners) {
                l.notifyCurrentFrame(null);
            }
        }
        for (Listener l : listeners) {
            l.notifySuspended(suspended);
        }
    }
    
    private void notifyFinished() {
        for (Listener l : listeners) {
            l.notifyFinished();
        }
    }
    
    private void finishCallbacks() {
        List<Pair<V8Request, CommandResponseCallback>> callBacks = new ArrayList<>();
        synchronized (commandCallbacks) {
            callBacks.addAll(commandCallbacks.values());
        }
        for (Pair<V8Request, CommandResponseCallback> cb : callBacks) {
            cb.second().notifyResponse(cb.first(), null);
        }
    }
    
    private void setEngine(DebuggerEngine debuggerEngine) {
        this.engine = debuggerEngine;
    }

    void setEngineDestructor(DebuggerEngine.Destructor destructor) {
        this.engineDestructor = destructor;
    }

    private void initScripts() {
        Scripts.Arguments sa = new Scripts.Arguments(null, null, false, null);
        sendCommandRequest(V8Command.Scripts, sa, new CommandResponseCallback() {
            @Override
            public void notifyResponse(V8Request request, V8Response response) {
                if (response != null) {
                    Scripts.ResponseBody srb = (Scripts.ResponseBody) response.getBody();
                    V8Script[] scripts = srb.getScripts();
                    scriptsHandler.add(scripts);
                }
            }
        });
    }
    
    private void clearRunTo() {
        if (runToBreakpointId > 0) {
            ClearBreakpoint.Arguments cbargs = new ClearBreakpoint.Arguments(runToBreakpointId);
            V8Request request = sendCommandRequest(V8Command.Clearbreakpoint, cbargs);
            runToBreakpointId = -1;
        }
    }
    
    public void runTo(FileObject fo, long line) {
        clearRunTo();
        if (fo == null) {
            return ;
        }
        String serverPath = scriptsHandler.getServerPath(fo);
        if (serverPath == null) {
            return ;
        }
        SetBreakpoint.Arguments args = new SetBreakpoint.Arguments(
                V8Breakpoint.Type.scriptName,
                serverPath,
                line, null, true,
                null, null, null);
        if (runToCallBack == null) {
            runToCallBack = new RunToResponseCallback();
        }
        V8Request request = sendCommandRequest(V8Command.Setbreakpoint, args,
                                               runToCallBack);
    }
    
    private final class RunToResponseCallback implements CommandResponseCallback {

        @Override
        public void notifyResponse(V8Request request, V8Response response) {
            if (response != null) {
                SetBreakpoint.ResponseBody sbrb = (SetBreakpoint.ResponseBody) response.getBody();
                long id = sbrb.getBreakpoint();
                runToBreakpointId = id;
                resume();
            }
        }
        
    }

    @NbBundle.Messages("V8DebugProtocolPane=V8 Debug Protocol")
    private final class CommListener implements IOListener {
        
        private final OutputColor sentColor = OutputColor.rgb(0, 178, 0);       //Color.GREEN.darker();
        private final OutputColor receivedColor = OutputColor.rgb(0, 0, 255);   //Color.BLUE;
        private final InputOutput ioLogger = IOProvider.getDefault().getIO(Bundle.V8DebugProtocolPane(), false);
        private final long startTime = System.currentTimeMillis();

        @Override
        public synchronized void sent(String str) {
            long time = System.currentTimeMillis() - startTime;
            ioLogger.getOut().append("Sent at "+(time/1000.0)+":");
            ioLogger.getOut().print(str, sentColor);
            ioLogger.getOut().println();
        }

        @Override
        public synchronized void received(String str) {
            long time = System.currentTimeMillis() - startTime;
            ioLogger.getOut().append("Got at "+(time/1000.0)+":");
            ioLogger.getOut().print(str, receivedColor);
            ioLogger.getOut().println();
        }

        @Override
        public void closed() {
            ioLogger.getOut().close();
        }

    }

    public interface Listener {
        
        void notifySuspended(boolean suspended);
        
        void notifyCurrentFrame(@NullAllowed CallFrame cf);
        
        void notifyFinished();
        
    }
    
    public interface CommandResponseCallback {
        
        /**
         * Command response result.
         * @param request The original request
         * @param response The response, or <code>null</code> when the command was canceled (e.g. debugger killed)
         */
        void notifyResponse(V8Request request, @NullAllowed V8Response response);
    }

    /**
     * Register an implementation of this interface into the default lookup
     * to handle the debugger protocol errors.
     */
    public static interface ErrorMessageHandler {
        
        void errorResponse(String error);
        
        void errorEvent(String error);
    }
}
