/*
 * 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.debugger.jpda.truffle;

import com.sun.jdi.ArrayReference;
import com.sun.jdi.ArrayType;
import com.sun.jdi.BooleanValue;
import com.sun.jdi.ByteValue;
import com.sun.jdi.ClassLoaderReference;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassObjectReference;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StringReference;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.debugger.jpda.JPDAThread;
import org.netbeans.api.debugger.jpda.MethodBreakpoint;
import org.netbeans.api.debugger.jpda.event.JPDABreakpointEvent;
import org.netbeans.api.debugger.jpda.event.JPDABreakpointListener;

import org.netbeans.modules.debugger.jpda.jdi.ArrayReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ArrayTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ClassNotPreparedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ClassObjectReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ClassTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.IllegalThreadStateExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.InternalExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ObjectCollectedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ObjectReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ReferenceTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ThreadReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.UnsupportedOperationExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.VMDisconnectedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.VirtualMachineWrapper;
import org.netbeans.modules.debugger.jpda.models.JPDAThreadImpl;

import org.openide.util.Exceptions;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakSet;

/**
 * Upload backend classed to the JVM.
 */
public final class RemoteServices {
    
    private static final Logger logger = Logger.getLogger(RemoteServices.class.getName());
    
    static final String REMOTE_CLASSES_ZIPFILE = "/org/netbeans/modules/debugger/jpda/truffle/resources/JPDATruffleBackend.jar";    // NOI18N

    private static final String TRUFFLE_CLASS = "com.oracle.truffle.api.Truffle";       // NOI18N
    private static final String[] TRUFFLE_PACKAGES = {
            "com.oracle.truffle.api",           // NOI18N
            "com.oracle.truffle.api.debug",     // NOI18N
            "com.oracle.truffle.api.frame",     // NOI18N
            "com.oracle.truffle.api.instrumentation",   // NOI18N
            "com.oracle.truffle.api.nodes",     // NOI18N
            "com.oracle.truffle.api.source" };  // NOI18N
    
    private static final Map<JPDADebugger, ClassObjectReference> remoteServiceClasses = new WeakHashMap<>();
    private static final Map<JPDADebugger, ThreadReference> remoteServiceAccess = new WeakHashMap<>();
    
    private static final RequestProcessor AUTORESUME_AFTER_SUSPEND_RP = new RequestProcessor("Autoresume after suspend", 1);    // NOI18N
    
    private static final Set<PropertyChangeListener> serviceListeners = new WeakSet<>();

    private RemoteServices() {}
    
    public static void addServiceListener(PropertyChangeListener listener) {
        synchronized (serviceListeners) {
            serviceListeners.add(listener);
        }
    }

    private static void fireServiceClass(JPDADebugger debugger) {
        PropertyChangeEvent pche = new PropertyChangeEvent(RemoteServices.class, "serviceClass", null, debugger);
        PropertyChangeListener[] listeners;
        synchronized (serviceListeners) {
            listeners = serviceListeners.toArray(new PropertyChangeListener[]{});
        }
        for (PropertyChangeListener l : listeners) {
            l.propertyChange(pche);
        }
    }
    
    private static ObjectReference getTruffleClassLoader(ThreadReference tawt, VirtualMachine vm) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, IOException, PropertyVetoException, InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper {
        /* Use:
           com.oracle.truffle.api.impl.TruffleLocator.class.getClassLoader()
        */
        ClassType truffleLocatorClass = getClass(vm, TRUFFLE_CLASS);
        return truffleLocatorClass.classLoader();
    }
    
    private static ObjectReference getBootstrapClassLoader(ThreadReference tawt, VirtualMachine vm) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, IOException, PropertyVetoException, InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper {
        /* Run this code:
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            ClassLoader bcl;
            do {
                bcl = cl;
                cl = cl.getParent();
            } while (cl != null);
            return bcl;
         */
        ClassType classLoaderClass = getClass(vm, ClassLoader.class.getName());
        Method getSystemClassLoader = ClassTypeWrapper.concreteMethodByName(classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
        ObjectReference cl = (ObjectReference) ClassTypeWrapper.invokeMethod(classLoaderClass, tawt, getSystemClassLoader, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
        Method getParent = ClassTypeWrapper.concreteMethodByName(classLoaderClass, "getParent", "()Ljava/lang/ClassLoader;");
        ObjectReference bcl;
        do {
            bcl = cl;
            if ("sun.misc.Launcher$AppClassLoader".equals(cl.referenceType().name())) {     // NOI18N
                break;
            }
            cl = (ObjectReference) ObjectReferenceWrapper.invokeMethod(cl, tawt, getParent, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
        } while (cl != null);
        return bcl;
    }
    
    private static ObjectReference getContextClassLoader(ThreadReference tawt, VirtualMachine vm) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, IOException, PropertyVetoException, InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper {
        ReferenceType threadType = tawt.referenceType();
        Method getContextCl = ClassTypeWrapper.concreteMethodByName((ClassType) threadType, "getContextClassLoader", "()Ljava/lang/ClassLoader;");
        ObjectReference cl = (ObjectReference) ObjectReferenceWrapper.invokeMethod(tawt, tawt, getContextCl, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
        ClassType classLoaderClass = null;
        if (cl == null) {
            classLoaderClass = getClass(vm, ClassLoader.class.getName());
            Method getSystemClassLoader = ClassTypeWrapper.concreteMethodByName(classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
            cl = (ObjectReference) ClassTypeWrapper.invokeMethod(classLoaderClass, tawt, getSystemClassLoader, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
        }
        return cl;
    }
    
    private static int getTargetMajorVersion(VirtualMachine vm) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper {
        String version = VirtualMachineWrapper.version(vm);
        int dot = version.indexOf(".");
        if (dot < 0) {
            dot = version.length();
        }
        return Integer.parseInt(version.substring(0, dot));
    }

    public static ClassObjectReference uploadBasicClasses(JPDAThreadImpl t, String basicClassName) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, IOException, PropertyVetoException, InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper {
        ThreadReference tawt = t.getThreadReference();
        VirtualMachine vm = tawt.virtualMachine();
        
        t.notifyMethodInvoking();
        try {
            ClassObjectReference basicClass = null;
            t.accessLock.writeLock().lock();
            try {
                List<RemoteClass> remoteClasses = getRemoteClasses();
                for (RemoteClass rc : remoteClasses) {
                    String className = rc.name;
                    if (basicClass == null && className.endsWith(basicClassName) && className.indexOf('$') < 0) {
                        List<ReferenceType> classesByName = VirtualMachineWrapper.classesByName(vm, className);
                        if (!classesByName.isEmpty()) {
                            basicClass = ReferenceTypeWrapper.classObject(classesByName.get(0));
                        }
                        break;
                    }
                }
                // Suppose that when there's the basic class loaded, there are all.
                if (basicClass == null) {  // Load the classes only if there's not the basic one.
                    basicClass = doUpload(vm, tawt, remoteClasses);
                }
                if (basicClass != null) {
                    // Initialize the class:
                    ClassType theClass = getClass(vm, Class.class.getName());
                    // Perhaps it's not 100% correct, we should be calling the new class' newInstance() method, not Class.newInstance() method.
                    Method newInstance = ClassTypeWrapper.concreteMethodByName(theClass, "newInstance", "()Ljava/lang/Object;");
                    ObjectReference newInstanceOfBasicClass = (ObjectReference) ObjectReferenceWrapper.invokeMethod(basicClass, tawt, newInstance, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
                }
            } finally {
                t.accessLock.writeLock().unlock();
            }
            if (basicClass != null) {
                synchronized (remoteServiceClasses) {
                    remoteServiceClasses.put(t.getDebugger(), basicClass);
                    t.getDebugger().addPropertyChangeListener(new RemoteServiceDebuggerListener());
                }
                fireServiceClass(t.getDebugger());
            }
            return basicClass;
        } finally {
            t.notifyMethodInvokeDone();
        }
    }
    
    private static ClassObjectReference doUpload(VirtualMachine vm, ThreadReference tawt, List<RemoteClass> remoteClasses) throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, InvocationException, IOException, PropertyVetoException, InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper, UnsupportedOperationExceptionWrapper, ClassNotPreparedExceptionWrapper {
        String agentClassLoaderName = "AgentClassLoader";
        PersistentValues values = new PersistentValues(vm);
        ByteValue[] mirrorBytesCache = new ByteValue[256];
        ClassLoaderReference classLoader;
        try {
            if (getTargetMajorVersion(vm) > 8) { // Module system is in place
                RemoteClass agent = null;
                for (RemoteClass rc : remoteClasses) {
                    if (rc.name.endsWith(agentClassLoaderName)) {
                        agent = rc;
                        break;
                    }
                }
                if (agent == null) {
                    throw new IllegalStateException("The " + agentClassLoaderName + " class is missing.");
                }
                // Upload the class loader first, using the Truffle's class loader:
                ClassType truffleLocatorClass = getClass(vm, TRUFFLE_CLASS);
                ClassLoaderReference truffleClassLoader = truffleLocatorClass.classLoader();
                ClassType classLoaderClass = (ClassType) ObjectReferenceWrapper.referenceType(truffleClassLoader);
                // Define the class loader's code:
                Method defineClass = ClassTypeWrapper.concreteMethodByName(classLoaderClass, "defineClass", "(Ljava/lang/String;[BII)Ljava/lang/Class;");
                ArrayReference byteArray = createTargetBytes(vm, agent.bytes, mirrorBytesCache, values);
                StringReference nameMirror = values.mirrorOf(agent.name);
                ClassObjectReference theUploadedClassLoader = (ClassObjectReference) ObjectReferenceWrapper.invokeMethod(truffleClassLoader, tawt, defineClass, Arrays.asList(nameMirror, byteArray, vm.mirrorOf(0), vm.mirrorOf(agent.bytes.length)), ObjectReference.INVOKE_SINGLE_THREADED);
                // We have the class loader's class. Create it's instance now.
                // Find the constructor and call newInstance on it:
                ClassType theClass = getClass(vm, Class.class.getName());
                Method getDeclaredConstructors = ClassTypeWrapper.concreteMethodByName(theClass, "getDeclaredConstructors", "()[Ljava/lang/reflect/Constructor;");
                ArrayReference constructors = (ArrayReference) ObjectReferenceWrapper.invokeMethod(theUploadedClassLoader, tawt, getDeclaredConstructors, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
                ObjectReference constructor = (ObjectReference) constructors.getValue(0);
                ClassType constructorClass = getClass(vm, Constructor.class.getName());
                Method newInstance = ClassTypeWrapper.concreteMethodByName(constructorClass, "newInstance", "([Ljava/lang/Object;)Ljava/lang/Object;");
                ClassLoaderReference newInstanceOfClassLoader = values.invokeOf(() -> (ClassLoaderReference) constructor.invokeMethod(tawt, newInstance, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED));
                classLoader = (ClassLoaderReference) newInstanceOfClassLoader;

                // We have an agent class loader that we'll use to define Truffle backend debugging classes.
                // We need to export the agent class loader to Truffle so that we can upload classes that access Truffle APIs.
                // We need to export packages from Truffle to our unnamed module.
                // We must call the Modeule.addExports() method from the Truffle module, we must be suspended in a Truffle class.
                Method getModule = ClassTypeWrapper.concreteMethodByName(theClass, "getModule", "()Ljava/lang/Module;");
                // truffleModule = Truffle.class.getModule()
                ObjectReference truffleModule = (ObjectReference) ObjectReferenceWrapper.invokeMethod(ReferenceTypeWrapper.classObject(truffleLocatorClass), tawt, getModule, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
                ClassType newClassLoaderClass = (ClassType) ObjectReferenceWrapper.referenceType(classLoader);
                Method getUnnamedModule = ClassTypeWrapper.concreteMethodByName(newClassLoaderClass, "getUnnamedModule", "()Ljava/lang/Module;");
                // unnamedModule = newInstanceOfClassLoader.getUnnamedModule()
                ObjectReference unnamedModule = (ObjectReference) ObjectReferenceWrapper.invokeMethod(classLoader, tawt, getUnnamedModule, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
                Method addExports = ClassTypeWrapper.concreteMethodByName((ClassType) ObjectReferenceWrapper.referenceType(truffleModule), "addExports", "(Ljava/lang/String;Ljava/lang/Module;)Ljava/lang/Module;");
                for (String tPackage : TRUFFLE_PACKAGES) {
                    // Add exports: truffleModule.addExports(package, unnamedModule)
                    ObjectReferenceWrapper.invokeMethod(truffleModule, tawt, addExports, Arrays.asList(values.mirrorOf(tPackage), unnamedModule), ObjectReference.INVOKE_SINGLE_THREADED);
                }
            } else {
                ObjectReference cl;
                cl = getTruffleClassLoader(tawt, vm);
                if (cl == null) {
                    cl = getBootstrapClassLoader(tawt, vm);
                }
                if (cl == null) {
                    cl = getContextClassLoader(tawt, vm);
                }
                classLoader = (ClassLoaderReference) cl;
            }

            ClassType classLoaderClass = (ClassType) ObjectReferenceWrapper.referenceType(classLoader);
            ClassObjectReference basicClass = null;
            for (RemoteClass rc : remoteClasses) {
                String className = rc.name;
                if (className.endsWith(agentClassLoaderName)) {
                    continue;
                }
                ClassObjectReference theUploadedClass;
                ArrayReference byteArray = createTargetBytes(vm, rc.bytes, mirrorBytesCache, values);
                Method defineClass = ClassTypeWrapper.concreteMethodByName(classLoaderClass, "defineClass", "(Ljava/lang/String;[BII)Ljava/lang/Class;");
                StringReference nameMirror = values.mirrorOf(className);
                theUploadedClass = values.invokeOf(() -> (ClassObjectReference) ObjectReferenceWrapper.invokeMethod(classLoader, tawt, defineClass, Arrays.asList(nameMirror, byteArray, vm.mirrorOf(0), vm.mirrorOf(rc.bytes.length)), ObjectReference.INVOKE_SINGLE_THREADED));
                if (basicClass == null && rc.name.indexOf('$') < 0 && rc.name.endsWith("Accessor")) {
                    // Disable collection only of the basic class
                    ObjectReferenceWrapper.disableCollection(theUploadedClass);
                    basicClass = theUploadedClass;
                }
            }
            return basicClass;
        } finally {
            values.collect();
        }
    }

    private static void runOnBreakpoint(final JPDAThread awtThread, String bpClass, String bpMethod, final Runnable runnable, final CountDownLatch latch) {
        final MethodBreakpoint mb = MethodBreakpoint.create(bpClass, bpMethod);
        final JPDADebugger dbg = ((JPDAThreadImpl)awtThread).getDebugger();
        final PropertyChangeListener[] listenerPtr = new PropertyChangeListener[] { null };
        
        mb.setBreakpointType(MethodBreakpoint.TYPE_METHOD_ENTRY);
        mb.setSuspend(MethodBreakpoint.SUSPEND_EVENT_THREAD);
        mb.setHidden(true);
        mb.setThreadFilters(dbg, new JPDAThread[] { awtThread });
        mb.addJPDABreakpointListener(new JPDABreakpointListener() {
            @Override
            public void breakpointReached(JPDABreakpointEvent event) {
                if (dbg.equals(event.getDebugger())) {
                    try {
                        DebuggerManager.getDebuggerManager().removeBreakpoint(mb);
                        //System.err.println("BREAKPOINT "+mb+" REMOVED after reached."+" ID = "+System.identityHashCode(mb));
                        PropertyChangeListener listener = listenerPtr[0];
                        if (listener != null) {
                            dbg.removePropertyChangeListener(JPDADebugger.PROP_STATE, listener);
                            listenerPtr[0] = null;
                        }
                        try {
                            ((JPDAThreadImpl)awtThread).notifyMethodInvoking();
                            runnable.run();
                        } catch (PropertyVetoException e) {
                        } finally {
                            ((JPDAThreadImpl)awtThread).notifyMethodInvokeDone();
                        }
                    } finally {
                        event.resume();
                        latch.countDown();
                    }
                }
            }
        });
        PropertyChangeListener listener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (dbg.getState() == JPDADebugger.STATE_DISCONNECTED) {
                    DebuggerManager.getDebuggerManager().removeBreakpoint(mb);
                    //System.err.println("BREAKPOINT "+mb+" REMOVED after debugger finished."+" ID = "+System.identityHashCode(mb));
                    dbg.removePropertyChangeListener(JPDADebugger.PROP_STATE, this);
                    listenerPtr[0] = null;
                    latch.countDown();
                }
            }
        };
        dbg.addPropertyChangeListener(JPDADebugger.PROP_STATE, listener);
        listenerPtr[0] = listener;
        if (dbg.getState() != JPDADebugger.STATE_DISCONNECTED) {
            DebuggerManager.getDebuggerManager().addBreakpoint(mb);
            //System.err.println("ADD BP: "+mb+" ID = "+System.identityHashCode(mb));
        } else {
            dbg.removePropertyChangeListener(JPDADebugger.PROP_STATE, listener);
            //System.err.println("NOT ADDED BP: "+mb+" ID = "+System.identityHashCode(mb));
            latch.countDown();
        }
    }
    
    private static final Map<JPDAThread, RequestProcessor.Task> tasksByThreads = new WeakHashMap<JPDAThread, RequestProcessor.Task> ();
    
    /**
     * Run the provided runnable after the thread is assured to be stopped on an event.
     * If the thread was initially running, it's resumed with some delay
     * (to allow another execution of runOnStoppedThread() without the expensive thread preparation).
     * It's assumed that the runnable will invoke methods on the thread.
     * Therefore method invoke notification methods are executed automatically.
     * @param thread The remote thread.
     * @param run The Runnable that is executed when the thread is assured to be stopped on an event.
     * @throws PropertyVetoException when can not invoke methods.
     */
    public static void runOnStoppedThread(JPDAThread thread, final Runnable run) throws PropertyVetoException {
        final JPDAThreadImpl t = (JPDAThreadImpl) thread;
        
        Lock lock = t.accessLock.writeLock();
        lock.lock();
        try {
            ThreadReference threadReference = t.getThreadReference();
            boolean wasSuspended = t.isSuspended();
            if (t.isSuspended() && !threadReference.isAtBreakpoint()) {
                // TODO: Suspended, but will not be able to invoke methods
                
            }
            if (!t.isSuspended()) {
                final CountDownLatch latch = new CountDownLatch(1);
                lock.unlock();
                lock = null;
                VirtualMachine vm = ((JPDAThreadImpl) thread).getThreadReference().virtualMachine();
                ClassObjectReference serviceClassObject;
                synchronized (remoteServiceClasses) {
                    serviceClassObject = remoteServiceClasses.get(((JPDAThreadImpl) thread).getDebugger());
                }
                if (serviceClassObject == null) {
                    // The debugger session has finished already, do not run anything.
                    return ;
                }
                runOnBreakpoint(
                    thread, 
                    "org.netbeans.modules.debugger.jpda.visual.remote.RemoteAWTService", // NOI18N
                    "calledInAWT", // NOI18N
                    run,
                    latch
                );
                try {
                    ClassType serviceClass = (ClassType) ClassObjectReferenceWrapper.reflectedType(serviceClassObject);//getClass(vm, "org.netbeans.modules.debugger.jpda.visual.remote.RemoteService");
                    Field awtAccess = ReferenceTypeWrapper.fieldByName(serviceClass, "awtAccess"); // NOI18N
                    ClassTypeWrapper.setValue(serviceClass, awtAccess, VirtualMachineWrapper.mirrorOf(vm, true));
                } catch (InternalExceptionWrapper iex) {
                } catch (VMDisconnectedExceptionWrapper vmdex) {
                } catch (Exception ex) {
                    Exceptions.printStackTrace(ex);
                }
                try {
                    // wait for the async operation to finish
                    latch.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            } else {
                RequestProcessor.Task autoresumeTask;
                if (!wasSuspended) {
                    AutoresumeTask resumeTask = new AutoresumeTask(t);
                    autoresumeTask = AUTORESUME_AFTER_SUSPEND_RP.create(resumeTask);
                    synchronized (tasksByThreads) {
                        tasksByThreads.put(thread, autoresumeTask);
                    }
                } else {
                    synchronized (tasksByThreads) {
                        autoresumeTask = tasksByThreads.get(thread);
                    }
                }
                t.notifyMethodInvoking();
                if (autoresumeTask != null) {
                    autoresumeTask.schedule(Integer.MAX_VALUE); // wait for run.run() to finish...
                }
                try {
                    run.run();
                } finally {
                    t.notifyMethodInvokeDone();
                    if (autoresumeTask != null) {
                        autoresumeTask.schedule(AutoresumeTask.WAIT_TIME);
                    }
                }
            }
        } finally {
            if (lock != null) {
                lock.unlock();
            }
        }
    }
    
    private static final Map<JPDADebugger, LoggingListeners> loggingListeners =
            new WeakHashMap<JPDADebugger, LoggingListeners>();
    
    private static final class LoggingListeners {
        
        private final Map<ObjectReference, Map<ClassObjectReference, Set<LoggingListenerCallBack>>> componentListeners =
                new HashMap<ObjectReference, Map<ClassObjectReference, Set<LoggingListenerCallBack>>>();
        
        static LoggingListeners get(JPDADebugger dbg) {
            synchronized (loggingListeners) {
                return loggingListeners.get(dbg);
            }
        }

        private synchronized boolean add(ObjectReference component, ClassObjectReference listenerClass, LoggingListenerCallBack listener) {
            Map<ClassObjectReference, Set<LoggingListenerCallBack>> listeners = componentListeners.get(component);
            if (listeners == null) {
                listeners = new HashMap<ClassObjectReference, Set<LoggingListenerCallBack>>();
                componentListeners.put(component, listeners);
            }
            Set<LoggingListenerCallBack> lcb = listeners.get(listenerClass);
            if (lcb == null) {
                lcb = new HashSet<LoggingListenerCallBack>();
                listeners.put(listenerClass, lcb);
            }
            return lcb.add(listener);
        }
        
        private synchronized boolean remove(ObjectReference component, ClassObjectReference listenerClass, LoggingListenerCallBack listener) {
            Map<ClassObjectReference, Set<LoggingListenerCallBack>> listeners = componentListeners.get(component);
            if (listeners == null) {
                return false;
            }
            Set<LoggingListenerCallBack> lcb = listeners.get(listenerClass);
            if (lcb == null) {
                return false;
            }
            boolean removed = lcb.remove(listener);
            if (removed) {
                if (lcb.isEmpty()) {
                    listeners.remove(listenerClass);
                    if (listeners.isEmpty()) {
                        componentListeners.remove(component);
                    }
                }
            }
            return removed;
        }
        
        synchronized Set<LoggingListenerCallBack> getListeners(ObjectReference component, ClassObjectReference listenerClass) {
            Map<ClassObjectReference, Set<LoggingListenerCallBack>> listeners = componentListeners.get(component);
            if (listeners == null) {
                return null;
            }
            return listeners.get(listenerClass);
        }
        
        private synchronized boolean isEmpty() {
            return componentListeners.isEmpty();
        }
        
    }
    
    static void setAccessLoopStarted(JPDADebugger debugger, ThreadReference accessThread) {
        synchronized (remoteServiceAccess) {
            remoteServiceAccess.put(debugger, accessThread);
        }
        fireServiceClass(debugger);
    }
    
    public static boolean interruptServiceAccessThread(JPDADebugger debugger) {
        ClassObjectReference serviceClass = getServiceClass(debugger);
        if (serviceClass != null) {
            ThreadReference accessThread;
            synchronized (remoteServiceAccess) {
                accessThread = remoteServiceAccess.get(debugger);
            }
            if (accessThread == null) {
                return false;
            }
            logger.fine("RemoteServices.interruptServiceAccessThread()");
            try {
                ClassType serviceClassType = (ClassType) ClassObjectReferenceWrapper.reflectedType(serviceClass);
                Field accessLoopSleepingField = ReferenceTypeWrapper.fieldByName(serviceClassType, "accessLoopSleeping");
                synchronized (accessThread) {
                    boolean isSleeping;
                    int repeatCheck = 10;
                    do {
                        BooleanValue sleepingValue = (BooleanValue) serviceClassType.getValue(accessLoopSleepingField);
                        isSleeping = sleepingValue.booleanValue();
                        if (isSleeping || --repeatCheck < 0) {
                            break;
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException iex) {}
                        }
                        logger.log(Level.FINE, "  isSleeping = {0}", isSleeping);
                    } while (!isSleeping);
                    if (isSleeping) {
                        ThreadReferenceWrapper.interrupt(accessThread);
                        //System.err.println("  INTERRUPTED.");
                        return true;
                    }
                }
            } catch (InternalExceptionWrapper |
                     VMDisconnectedExceptionWrapper |
                     ObjectCollectedExceptionWrapper |
                     ClassNotPreparedExceptionWrapper |
                     IllegalThreadStateExceptionWrapper ex) {
                logger.log(Level.FINE, "  NOT interrupted: ", ex);
            }
            logger.fine("  NOT Interrupted.");
        }
        return false;
    }
    
    public static ClassObjectReference getServiceClass(JPDADebugger debugger) {
        synchronized (remoteServiceClasses) {
            return remoteServiceClasses.get(debugger);
        }
        
    }
    
    static ClassType getClass(VirtualMachine vm, String name) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper {
        List<ReferenceType> classList = VirtualMachineWrapper.classesByName(vm, name);
        ReferenceType clazz = null;
        for (ReferenceType c : classList) {
            if (ReferenceTypeWrapper.classLoader(c) == null) {
                clazz = c;
                break;
            }
        }
        if (clazz == null && classList.size() > 0) {
            clazz = classList.get(0);
        }
        return (ClassType) clazz;
    }
    
    static ArrayType getArrayClass(VirtualMachine vm, String name) throws InternalExceptionWrapper, VMDisconnectedExceptionWrapper, ObjectCollectedExceptionWrapper {
        List<ReferenceType> classList = VirtualMachineWrapper.classesByName(vm, name);
        ReferenceType clazz = null;
        for (ReferenceType c : classList) {
            if (ReferenceTypeWrapper.classLoader(c) == null) {
                clazz = c;
                break;
            }
        }
        return (ArrayType) clazz;
    }
    
    private static List<RemoteClass> getRemoteClasses() throws IOException {
        InputStream in = openRemoteClasses();
        try {
            ZipInputStream zin = new ZipInputStream(in);
            ZipEntry ze;
            List<RemoteClass> rcl = new ArrayList<RemoteClass>();
            while((ze = zin.getNextEntry()) != null) {
                String fileName = ze.getName();
                if (!fileName.endsWith(".class")) {
                    continue;
                }
                String name = fileName.substring(0, fileName.length() - ".class".length());
                int baseStart = name.lastIndexOf('/');
                if (baseStart < 0) {
                    continue;
                }
                /*baseStart++;
                int baseEnd = name.indexOf('$', baseStart);
                if (baseEnd < 0) {
                    baseEnd = name.length();
                }*/
                RemoteClass rc = new RemoteClass();
                rc.name = name.replace('/', '.');
                int l = (int) ze.getSize();
                byte[] bytes = new byte[l];
                int num = 0;
                while (num < l) {
                    int r = zin.read(bytes, num, l - num);
                    if (r < 0) {
                        Exceptions.printStackTrace(new IllegalStateException("Can not read full content of "+name+" entry. Length = "+l+", read num = "+num));
                        break;
                    }
                    num += r;
                }
                rc.bytes = bytes;
                rcl.add(rc);
            }
            return rcl;
        } finally {
            in.close();
        }
    }

    static InputStream openRemoteClasses() {
        return RemoteServices.class.getResourceAsStream(REMOTE_CLASSES_ZIPFILE);
    }
    
    private static ArrayReference createTargetBytes(VirtualMachine vm, byte[] bytes,
                                                    ByteValue[] mirrorBytesCache,
                                                    PersistentValues persistValues) throws InvalidTypeException,
                                                                                         ClassNotLoadedException,
                                                                                         InternalExceptionWrapper,
                                                                                         VMDisconnectedExceptionWrapper,
                                                                                         ObjectCollectedExceptionWrapper,
                                                                                         UnsupportedOperationExceptionWrapper {
        ArrayType bytesArrayClass = getArrayClass(vm, "byte[]");
        ArrayReference array = persistValues.valueOf(() -> ArrayTypeWrapper.newInstance(bytesArrayClass, bytes.length));
        List<Value> values = new ArrayList<>(bytes.length);
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            ByteValue mb = mirrorBytesCache[128 + b];
            if (mb == null) {
                mb = VirtualMachineWrapper.mirrorOf(vm, b);
                mirrorBytesCache[128 + b] = mb;
            }
            values.add(mb);
        }
        ArrayReferenceWrapper.setValues(array, values);
        return array;
    }
    
    private static class RemoteServiceDebuggerListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (JPDADebugger.PROP_STATE.equals(evt.getPropertyName())) {
                JPDADebugger d = (JPDADebugger) evt.getSource();
                if (JPDADebugger.STATE_DISCONNECTED == d.getState()) {
                    d.removePropertyChangeListener(this);
                    synchronized (remoteServiceClasses) {
                        remoteServiceClasses.remove(d);
                    }
                }
            }
        }
        
    }
    
    private static class RemoteClass {
        private String name;
        private byte[] bytes;
    }
    
    public static class RemoteListener {
        
        private String type;
        private List<String> allTypesList;
        private String[] allTypes;
        //private String classType;
        private ObjectReference l;
        
        public RemoteListener(String type, ObjectReference l) {
            this.type = type;
            this.l = l;
        }
        
        public String getType() {
            return type;
        }
        
        public void setAllTypes(String[] allTypes) {
            this.allTypes = allTypes;
        }
        
        private void addType(String listenerType) {
            if (allTypesList == null) {
                allTypesList = new ArrayList<String>();
                allTypesList.add(type);
            }
            allTypesList.add(listenerType);
        }
        
        public String[] getTypes() {
            if (allTypes == null) {
                if (allTypesList != null) {
                    allTypes = allTypesList.toArray(new String[] {});
                } else {
                    allTypes = new String[] { type };
                }
            }
            return allTypes;
        }
        
        //public String getClassType() {
        //    return classType;
        //}
        
        public ObjectReference getListener() {
            return l;
        }

        @Override
        public String toString() {
            return "RemoteListener("+type+")["+l+"]";
        }

    }
    
    public static interface LoggingListenerCallBack {
        
        public void eventsData(String[] data, String[] stack);
        
    }
    
    private static class AutoresumeTask implements Runnable, PropertyChangeListener {
        
        private static final int WAIT_TIME = 500;
        
        private volatile JPDAThreadImpl t;

        public AutoresumeTask(JPDAThreadImpl t) {
            this.t = t;
            t.addPropertyChangeListener(this);
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            JPDAThreadImpl thread = this.t;
            if (thread == null) {
                return ;
            }
            if (JPDAThread.PROP_SUSPENDED.equals(evt.getPropertyName()) &&
                !"methodInvoke".equals(evt.getPropagationId())) {               // NOI18N
                
                thread.removePropertyChangeListener(this);
                logger.fine("AutoresumeTask: autoresume canceled, thread changed suspended state: suspended = "+thread.isSuspended());
                synchronized (tasksByThreads) {
                    tasksByThreads.remove(thread);
                }
                t = null;
            }
        }
        
        @Override
        public void run() {
            JPDAThreadImpl thread = this.t;
            this.t = null;
            if (thread != null) {
                thread.removePropertyChangeListener(this);
                thread.resume();
            }
        }
    }

}
