// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.debugger.engine;

import com.intellij.Patches;
import com.intellij.ProjectTopics;
import com.intellij.debugger.*;
import com.intellij.debugger.actions.DebuggerAction;
import com.intellij.debugger.actions.DebuggerActions;
import com.intellij.debugger.engine.evaluation.*;
import com.intellij.debugger.engine.events.DebuggerCommandImpl;
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
import com.intellij.debugger.engine.jdi.ThreadReferenceProxy;
import com.intellij.debugger.engine.requests.MethodReturnValueWatcher;
import com.intellij.debugger.engine.requests.RequestManagerImpl;
import com.intellij.debugger.impl.*;
import com.intellij.debugger.impl.attach.PidRemoteConnection;
import com.intellij.debugger.jdi.EmptyConnectorArgument;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.debugger.settings.NodeRendererSettings;
import com.intellij.debugger.ui.breakpoints.BreakpointManager;
import com.intellij.debugger.ui.breakpoints.RunToCursorBreakpoint;
import com.intellij.debugger.ui.breakpoints.StackCapturingLineBreakpoint;
import com.intellij.debugger.ui.breakpoints.StepIntoBreakpoint;
import com.intellij.debugger.ui.tree.ValueDescriptor;
import com.intellij.debugger.ui.tree.render.ArrayRenderer;
import com.intellij.debugger.ui.tree.render.ClassRenderer;
import com.intellij.debugger.ui.tree.render.NodeRenderer;
import com.intellij.debugger.ui.tree.render.PrimitiveRenderer;
import com.intellij.execution.CantRunException;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.configurations.RemoteConnection;
import com.intellij.execution.process.*;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdkType;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.impl.status.StatusBarUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.EventDispatcher;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.lang.JavaVersion;
import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.impl.XDebugSessionImpl;
import com.intellij.xdebugger.impl.XDebuggerManagerImpl;
import com.intellij.xdebugger.impl.actions.XDebuggerActions;
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.StepRequest;
import one.util.streamex.StreamEx;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public abstract class DebugProcessImpl extends UserDataHolderBase implements DebugProcess {
  private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.engine.DebugProcessImpl");

  @NonNls private static final String SOCKET_ATTACHING_CONNECTOR_NAME = "com.sun.jdi.SocketAttach";
  @NonNls private static final String SHMEM_ATTACHING_CONNECTOR_NAME = "com.sun.jdi.SharedMemoryAttach";
  @NonNls private static final String SOCKET_LISTENING_CONNECTOR_NAME = "com.sun.jdi.SocketListen";
  @NonNls private static final String SHMEM_LISTENING_CONNECTOR_NAME = "com.sun.jdi.SharedMemoryListen";

  private final Project myProject;
  private final RequestManagerImpl myRequestManager;

  private volatile VirtualMachineProxyImpl myVirtualMachineProxy = null;
  protected final EventDispatcher<DebugProcessListener> myDebugProcessDispatcher = EventDispatcher.create(DebugProcessListener.class);
  protected final EventDispatcher<EvaluationListener> myEvaluationDispatcher = EventDispatcher.create(EvaluationListener.class);

  private final List<ProcessListener> myProcessListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final StringBuilder myTextBeforeStart = new StringBuilder();

  enum State {INITIAL, ATTACHED, DETACHING, DETACHED}
  protected final AtomicReference<State> myState = new AtomicReference<>(State.INITIAL);

  private volatile ExecutionResult myExecutionResult;
  private RemoteConnection myConnection;
  private JavaDebugProcess myXDebugProcess;

  private volatile Map<String, Connector.Argument> myArguments;

  private final List<NodeRenderer> myRenderers = new ArrayList<>();

  // we use null key here
  private final Map<Type, NodeRenderer> myNodeRenderersMap = new HashMap<>();

  private final SuspendManagerImpl mySuspendManager = new SuspendManagerImpl(this);
  protected CompoundPositionManager myPositionManager = CompoundPositionManager.EMPTY;
  private final DebuggerManagerThreadImpl myDebuggerManagerThread;

  private final Semaphore myWaitFor = new Semaphore();
  private final AtomicBoolean myIsFailed = new AtomicBoolean(false);
  private final AtomicBoolean myIsStopped = new AtomicBoolean(false);
  protected DebuggerSession mySession;
  @Nullable protected MethodReturnValueWatcher myReturnValueWatcher;
  protected final Disposable myDisposable = Disposer.newDisposable();
  private final Alarm myStatusUpdateAlarm = new Alarm();

  private final ThreadBlockedMonitor myThreadBlockedMonitor = new ThreadBlockedMonitor(this, myDisposable);

  protected DebugProcessImpl(Project project) {
    myProject = project;
    myDebuggerManagerThread = new DebuggerManagerThreadImpl(myDisposable, myProject);
    myRequestManager = new RequestManagerImpl(this);
    NodeRendererSettings.getInstance().addListener(this::reloadRenderers, myDisposable);
    reloadRenderers();
    myDebugProcessDispatcher.addListener(new DebugProcessListener() {
      @Override
      public void paused(@NotNull SuspendContext suspendContext) {
        myThreadBlockedMonitor.stopWatching(
          suspendContext.getSuspendPolicy() != EventRequest.SUSPEND_ALL ? suspendContext.getThread() : null);
      }
    });
  }

  private void reloadRenderers() {
    getManagerThread().invoke(new DebuggerCommandImpl(PrioritizedTask.Priority.HIGH) {
      @Override
      protected void action() {
        myNodeRenderersMap.clear();
        myRenderers.clear();
        try {
          NodeRendererSettings.getInstance().getAllRenderers().stream().filter(NodeRenderer::isEnabled).forEachOrdered(myRenderers::add);
        }
        finally {
          DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
            final DebuggerSession session = mySession;
            if (session != null && session.isAttached()) {
              DebuggerAction.refreshViews(mySession.getXDebugSession());
            }
          });
        }
      }
    });
  }

  @Nullable
  public Pair<Method, Value> getLastExecutedMethod() {
    final MethodReturnValueWatcher watcher = myReturnValueWatcher;
    if (watcher == null) {
      return null;
    }
    final Method method = watcher.getLastExecutedMethod();
    if (method == null) {
      return null;
    }
    return Pair.create(method, watcher.getLastMethodReturnValue());
  }

  public void setWatchMethodReturnValuesEnabled(boolean enabled) {
    final MethodReturnValueWatcher watcher = myReturnValueWatcher;
    if (watcher != null) {
      watcher.setEnabled(enabled);
    }
  }

  public boolean canGetMethodReturnValue() {
    return myReturnValueWatcher != null;
  }

  public NodeRenderer getAutoRenderer(ValueDescriptor descriptor) {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    Type type = descriptor.getType();

    // in case evaluation is not possible, force default renderer
    if (!isEvaluationPossible()) {
      return getDefaultRenderer(type);
    }

    try {
      return myNodeRenderersMap.computeIfAbsent(type, t ->
        myRenderers.stream().
          filter(r -> DebuggerUtilsImpl.suppressExceptions(() -> r.isApplicable(type), false, true, ClassNotPreparedException.class)).
          findFirst().orElseGet(() -> getDefaultRenderer(type)));
    }
    catch (ClassNotPreparedException e) {
      LOG.info(e);
      // use default, but do not cache
      return getDefaultRenderer(type);
    }
  }

  @NotNull
  public static NodeRenderer getDefaultRenderer(Value value) {
    return getDefaultRenderer(value != null ? value.type() : null);
  }

  @NotNull
  public static NodeRenderer getDefaultRenderer(Type type) {
    final NodeRendererSettings settings = NodeRendererSettings.getInstance();

    final PrimitiveRenderer primitiveRenderer = settings.getPrimitiveRenderer();
    if(primitiveRenderer.isApplicable(type)) {
      return primitiveRenderer;
    }

    final ArrayRenderer arrayRenderer = settings.getArrayRenderer();
    if(arrayRenderer.isApplicable(type)) {
      return arrayRenderer;
    }

    final ClassRenderer classRenderer = settings.getClassRenderer();
    LOG.assertTrue(classRenderer.isApplicable(type), type.name());
    return classRenderer;
  }

  private static final int ourTraceMask;

  static {
    String traceStr = System.getProperty("idea.debugger.trace");
    int mask = 0;
    if (!StringUtil.isEmpty(traceStr)) {
      StringTokenizer tokenizer = new StringTokenizer(traceStr);
      while (tokenizer.hasMoreTokens()) {
        String token = tokenizer.nextToken();
        if ("SENDS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_SENDS;
        }
        else if ("RAW_SENDS".compareToIgnoreCase(token) == 0) {
          mask |= 0x01000000;
        }
        else if ("RECEIVES".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_RECEIVES;
        }
        else if ("RAW_RECEIVES".compareToIgnoreCase(token) == 0) {
          mask |= 0x02000000;
        }
        else if ("EVENTS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_EVENTS;
        }
        else if ("REFTYPES".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_REFTYPES;
        }
        else if ("OBJREFS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_OBJREFS;
        }
        else if ("ALL".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_ALL;
        }
      }
    }
    ourTraceMask = mask;
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  protected void commitVM(VirtualMachine vm) {
    if (!isInInitialState()) {
      LOG.error("State is invalid " + myState.get());
    }
    DebuggerManagerThreadImpl.assertIsManagerThread();
    myPositionManager = new CompoundPositionManager(new PositionManagerImpl(this));
    myProject.getMessageBus().connect(myDisposable).subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
      @Override
      public void rootsChanged(@NotNull final ModuleRootEvent event) {
        DumbService.getInstance(myProject).runWhenSmart(
          () -> getManagerThread().schedule(PrioritizedTask.Priority.HIGH, () -> {
            myPositionManager.clearCache();
            DebuggerUIUtil.invokeLater(() -> mySession.refresh(true));
          }));
      }
    });
    LOG.debug("*******************VM attached******************");
    checkVirtualMachineVersion(vm);

    myVirtualMachineProxy = new VirtualMachineProxyImpl(this, vm);

    vm.setDebugTraceMode(ourTraceMask);
  }

  private void stopConnecting() {
    Map<String, Connector.Argument> arguments = myArguments;
    try {
      if (arguments == null) {
        return;
      }
      if(myConnection.isServerMode()) {
        ((ListeningConnector)findConnector(SOCKET_LISTENING_CONNECTOR_NAME)).stopListening(arguments);
      }
    }
    catch (IOException | IllegalConnectorArgumentsException | IllegalArgumentException e) {
      LOG.debug(e);
    }
    catch (ExecutionException e) {
      LOG.error(e);
    }
  }

  @Override
  public void printToConsole(final String text) {
    synchronized (myProcessListeners) {
      if (myExecutionResult == null) {
        myTextBeforeStart.append(text);
      }
      else {
        printToConsoleImpl(text);
      }
    }
  }

  private void printToConsoleImpl(String text) {
    myExecutionResult.getProcessHandler().notifyTextAvailable(text, ProcessOutputTypes.SYSTEM);
  }

  @Override
  public ProcessHandler getProcessHandler() {
    return myExecutionResult != null ? myExecutionResult.getProcessHandler() : null;
  }

  /**
   *
   * @param suspendContext
   * @param stepThread
   * @param size the step size. One of {@link StepRequest#STEP_LINE} or {@link StepRequest#STEP_MIN}
   * @param depth
   * @param hint may be null
   */
  protected void doStep(final SuspendContextImpl suspendContext, final ThreadReferenceProxyImpl stepThread, int size, int depth,
                        RequestHint hint) {
    if (stepThread == null) {
      return;
    }
    try {
      final ThreadReference stepThreadReference = stepThread.getThreadReference();
      if (LOG.isDebugEnabled()) {
        LOG.debug("DO_STEP: creating step request for " + stepThreadReference);
      }
      deleteStepRequests(stepThreadReference);
      EventRequestManager requestManager = getVirtualMachineProxy().eventRequestManager();
      StepRequest stepRequest = requestManager.createStepRequest(stepThreadReference, size, depth);
      if (!(hint != null && hint.isIgnoreFilters()) /*&& depth == StepRequest.STEP_INTO*/) {
        checkPositionNotFiltered(stepThread, filters -> filters.forEach(f -> stepRequest.addClassExclusionFilter(f.getPattern())));
      }

      // suspend policy to match the suspend policy of the context:
      // if all threads were suspended, then during stepping all the threads must be suspended
      // if only event thread were suspended, then only this particular thread must be suspended during stepping
      stepRequest.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD
                                   ? EventRequest.SUSPEND_EVENT_THREAD
                                   : EventRequest.SUSPEND_ALL);

      stepRequest.addCountFilter(1);

      if (hint != null) {
        //noinspection HardCodedStringLiteral
        stepRequest.putProperty("hint", hint);
      }
      try {
        stepRequest.enable();
      } catch (IllegalThreadStateException e) { // thread is already dead
        requestManager.deleteEventRequest(stepRequest);
      }
    }
    catch (ObjectCollectedException ignored) {

    }
  }

  public void checkPositionNotFiltered(ThreadReferenceProxyImpl thread, Consumer<List<ClassFilter>> action) {
    List<ClassFilter> activeFilters = getActiveFilters();
    if (!activeFilters.isEmpty()) {
      String currentClassName = getCurrentClassName(thread);
      if (currentClassName == null || !DebuggerUtilsEx.isFiltered(currentClassName, activeFilters)) {
        action.consume(activeFilters);
      }
    }
  }

  @NotNull
  private static List<ClassFilter> getActiveFilters() {
    DebuggerSettings settings = DebuggerSettings.getInstance();
    StreamEx<ClassFilter> stream = StreamEx.of(DebuggerClassFilterProvider.EP_NAME.getExtensionList())
      .flatCollection(DebuggerClassFilterProvider::getFilters);
    if (settings.TRACING_FILTERS_ENABLED) {
      stream = stream.prepend(settings.getSteppingFilters());
    }
    return stream.filter(ClassFilter::isEnabled).toList();
  }

  void deleteStepRequests(@Nullable final ThreadReference stepThread) {
    EventRequestManager requestManager = getVirtualMachineProxy().eventRequestManager();
    for (StepRequest request : new ArrayList<>(requestManager.stepRequests())) { // need a copy here to avoid CME
      if (stepThread == null || stepThread.equals(request.thread())) {
        try {
          requestManager.deleteEventRequest(request);
        }
        catch (ObjectCollectedException ignored) {
        }
        catch (VMDisconnectedException e) {
          throw e;
        }
        catch (Exception e) {
          LOG.error(e); // report all for now
        }
      }
    }
  }

  @Nullable
  static String getCurrentClassName(ThreadReferenceProxyImpl thread) {
    try {
      if (thread != null && thread.frameCount() > 0) {
        StackFrameProxyImpl stackFrame = thread.frame(0);
        if (stackFrame != null) {
          Location location = stackFrame.location();
          ReferenceType referenceType = location == null ? null : location.declaringType();
          if (referenceType != null) {
            return referenceType.name();
          }
        }
      }
    }
    catch (EvaluateException ignored) {
    }
    return null;
  }

  private VirtualMachine createVirtualMachineInt() throws ExecutionException {
    try {
      if (myArguments != null) {
        throw new IOException(DebuggerBundle.message("error.debugger.already.listening"));
      }

      final String address = myConnection.getAddress();

      if (myConnection instanceof PidRemoteConnection && !((PidRemoteConnection)myConnection).isFixedAddress()) {
        PidRemoteConnection pidRemoteConnection = (PidRemoteConnection)myConnection;
        Connector connector = pidRemoteConnection.getConnector(this);
        String pid = pidRemoteConnection.getPid();
        if (StringUtil.isEmpty(pid)) {
          throw new CantRunException(DebuggerBundle.message("error.no.pid"));
        }
        myArguments = connector.defaultArguments();
        Connector.Argument pidArg = myArguments.get("pid");
        if (pidArg != null) {
          pidArg.setValue(pid);
        }
        if (connector instanceof AttachingConnector) {
          return attachConnector((AttachingConnector)connector);
        }
        else {
          return connectorListen(address, (ListeningConnector)connector);
        }
      }
      else if (myConnection.isServerMode()) {
        ListeningConnector connector = (ListeningConnector)findConnector(myConnection.isUseSockets(), true);
        myArguments = connector.defaultArguments();
        if (myArguments == null) {
          throw new CantRunException(DebuggerBundle.message("error.no.debug.listen.port"));
        }
        return connectorListen(address, connector);
      }
      else { // is client mode, should attach to already running process
        AttachingConnector connector = (AttachingConnector)findConnector(myConnection.isUseSockets(), false);
        myArguments = connector.defaultArguments();
        if (myConnection.isUseSockets()) {
          final Connector.Argument hostnameArg = myArguments.get("hostname");
          if (hostnameArg != null && myConnection.getHostName() != null) {
            hostnameArg.setValue(myConnection.getHostName());
          }
          if (address == null) {
            throw new CantRunException(DebuggerBundle.message("error.no.debug.attach.port"));
          }
          final Connector.Argument portArg = myArguments.get("port");
          if (portArg != null) {
            portArg.setValue(address);
          }
        }
        else {
          if (address == null) {
            throw new CantRunException(DebuggerBundle.message("error.no.shmem.address"));
          }
          final Connector.Argument nameArg = myArguments.get("name");
          if (nameArg != null) {
            nameArg.setValue(address);
          }
        }
        final Connector.Argument timeoutArg = myArguments.get("timeout");
        if (timeoutArg != null) {
          timeoutArg.setValue("0"); // wait forever
        }

        return attachConnector(connector);
      }
    }
    catch (IOException e) {
      throw new ExecutionException(processIOException(e, DebuggerBundle.getAddressDisplayName(myConnection)), e);
    }
    catch (IllegalConnectorArgumentsException e) {
      throw new ExecutionException(processError(e), e);
    }
    finally {
      myArguments = null;
    }
  }

  private VirtualMachine connectorListen(String address, ListeningConnector connector)
    throws CantRunException, IOException, IllegalConnectorArgumentsException {
    if (address == null) {
      throw new CantRunException(DebuggerBundle.message("error.no.debug.listen.port"));
    }
    // zero port number means the caller leaves to debugger to decide at which port to listen
    final Connector.Argument portArg = myConnection.isUseSockets() ? myArguments.get("port") : myArguments.get("name");
    if (portArg != null) {
      portArg.setValue(address);

      // to allow connector to listen on several auto generated addresses
      if (address.length() == 0 || address.equals("0")) {
        EmptyConnectorArgument uniqueArg = new EmptyConnectorArgument("argForUniqueness");
        myArguments.put(uniqueArg.name(), uniqueArg);
      }
    }
    final Connector.Argument timeoutArg = myArguments.get("timeout");
    if (timeoutArg != null) {
      timeoutArg.setValue("0"); // wait forever
    }
    try {
      String listeningAddress = connector.startListening(myArguments);
      String port = StringUtil.substringAfterLast(listeningAddress, ":");
      if (port != null) {
        listeningAddress = port;
      }
      myConnection.setAddress(listeningAddress);

      myDebugProcessDispatcher.getMulticaster().connectorIsReady();

      return connector.accept(myArguments);
    }
    catch (IllegalArgumentException e) {
      throw new CantRunException(e.getLocalizedMessage());
    }
    finally {
      if(myArguments != null) {
        try {
          connector.stopListening(myArguments);
        }
        catch (IllegalArgumentException | IllegalConnectorArgumentsException ignored) {
          // ignored
        }
      }
    }
  }

  private VirtualMachine attachConnector(AttachingConnector connector)
    throws IOException, IllegalConnectorArgumentsException, CantRunException {
    myDebugProcessDispatcher.getMulticaster().connectorIsReady();
    try {
      return connector.attach(myArguments);
    }
    catch (IllegalArgumentException e) {
      throw new CantRunException(e.getLocalizedMessage());
    }
  }

  public void showStatusText(final String text) {
    myStatusUpdateAlarm.cancelAllRequests();
    myStatusUpdateAlarm.addRequest(() -> {
      if (!myProject.isDisposed()) {
        StatusBarUtil.setStatusBarInfo(myProject, text);
      }
    }, 50);
  }

  @NotNull
  public static Connector findConnector(boolean useSockets, boolean listen) throws ExecutionException {
    if (listen) {
      return findConnector(useSockets ? SOCKET_LISTENING_CONNECTOR_NAME : SHMEM_LISTENING_CONNECTOR_NAME);
    }
    else {
      return findConnector(useSockets ? SOCKET_ATTACHING_CONNECTOR_NAME : SHMEM_ATTACHING_CONNECTOR_NAME);
    }
  }

  @NotNull
  public static Connector findConnector(String connectorName) throws ExecutionException {
    VirtualMachineManager virtualMachineManager;
    try {
      virtualMachineManager = Bootstrap.virtualMachineManager();
    }
    catch (Error e) {
      throw new ExecutionException(DebuggerBundle.message("debugger.jdi.bootstrap.error",
                                                          e.getClass().getName() + " : " + e.getLocalizedMessage()));
    }
    return StreamEx.of(virtualMachineManager.allConnectors())
      .findFirst(c -> connectorName.equals(c.name()))
      .orElseThrow(() -> new CantRunException(DebuggerBundle.message("error.debug.connector.not.found", connectorName)));
  }

  private void checkVirtualMachineVersion(VirtualMachine vm) {
    final String versionString = vm.version();
    if ("1.4.0".equals(versionString)) {
      DebuggerInvocationUtil.swingInvokeLater(myProject, () -> Messages.showMessageDialog(
        myProject,
        DebuggerBundle.message("warning.jdk140.unstable"), DebuggerBundle.message("title.jdk140.unstable"), Messages.getWarningIcon()
      ));
    }
    if (getSession().getAlternativeJre() == null) {
      Sdk runjre = getSession().getRunJre();
      JavaVersion version = JavaVersion.tryParse(versionString);
      if (version != null && (runjre == null || runjre.getSdkType() instanceof JavaSdkType) && !versionMatch(runjre, version)) {
        Arrays.stream(ProjectJdkTable.getInstance().getAllJdks())
          .filter(sdk -> versionMatch(sdk, version))
          .findFirst().ifPresent(sdk -> {
          XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(
            DebuggerBundle.message("message.remote.jre.version.mismatch",
                                   versionString,
                                   runjre != null ? runjre.getVersionString() : "unknown",
                                   sdk.getName())
            , MessageType.INFO).notify(myProject);
          getSession().setAlternativeJre(sdk);
        });
      }
    }
  }

  private static boolean versionMatch(@Nullable Sdk sdk, @NotNull JavaVersion version) {
    if (sdk != null && sdk.getSdkType() instanceof JavaSdkType) {
      String versionString = sdk.getVersionString();
      return versionString != null && version.equals(JavaVersion.tryParse(versionString));
    }
    return false;
  }

  /*Event dispatching*/
  public void addEvaluationListener(EvaluationListener evaluationListener) {
    myEvaluationDispatcher.addListener(evaluationListener);
  }

  public void removeEvaluationListener(EvaluationListener evaluationListener) {
    myEvaluationDispatcher.removeListener(evaluationListener);
  }

  @Override
  public void addDebugProcessListener(DebugProcessListener listener) {
    myDebugProcessDispatcher.addListener(listener);
  }

  @Override
  public void removeDebugProcessListener(DebugProcessListener listener) {
    myDebugProcessDispatcher.removeListener(listener);
  }

  public void addProcessListener(ProcessListener processListener) {
    synchronized(myProcessListeners) {
      if(getProcessHandler() != null) {
        getProcessHandler().addProcessListener(processListener);
      }
      else {
        myProcessListeners.add(processListener);
      }
    }
  }

  public void removeProcessListener(ProcessListener processListener) {
    synchronized (myProcessListeners) {
      if(getProcessHandler() != null) {
        getProcessHandler().removeProcessListener(processListener);
      }
      else {
        myProcessListeners.remove(processListener);
      }
    }
  }

  /* getters */
  public RemoteConnection getConnection() {
    return myConnection;
  }

  @Override
  public ExecutionResult getExecutionResult() {
    return myExecutionResult;
  }

  @NotNull
  @Override
  public Project getProject() {
    return myProject;
  }

  public boolean canRedefineClasses() {
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    return vm != null && vm.canRedefineClasses();
  }

  public boolean canWatchFieldModification() {
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    return vm != null && vm.canWatchFieldModification();
  }

  public boolean isInInitialState() {
    return myState.get() == State.INITIAL;
  }

  @Override
  public boolean isAttached() {
    return myState.get() == State.ATTACHED;
  }

  @Override
  public boolean isDetached() {
    return myState.get() == State.DETACHED;
  }

  @Override
  public boolean isDetaching() {
    return myState.get() == State.DETACHING;
  }

  @Override
  public RequestManagerImpl getRequestsManager() {
    return myRequestManager;
  }

  @NotNull
  @Override
  public VirtualMachineProxyImpl getVirtualMachineProxy() {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    if (vm == null) {
      if (isInInitialState()) {
        throw new IllegalStateException("Virtual machine is not initialized yet");
      }
      else {
        throw new VMDisconnectedException();
      }
    }
    return vm;
  }

  @Override
  public void appendPositionManager(final PositionManager positionManager) {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    myPositionManager.appendPositionManager(positionManager);
  }

  private volatile RunToCursorBreakpoint myRunToCursorBreakpoint;

  public void setRunToCursorBreakpoint(@Nullable RunToCursorBreakpoint breakpoint) {
    myRunToCursorBreakpoint = breakpoint;
  }

  public void cancelRunToCursorBreakpoint() {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    final RunToCursorBreakpoint runToCursorBreakpoint = myRunToCursorBreakpoint;
    if (runToCursorBreakpoint != null) {
      setRunToCursorBreakpoint(null);
      getRequestsManager().deleteRequest(runToCursorBreakpoint);
      if (runToCursorBreakpoint.isRestoreBreakpoints()) {
        DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager().enableBreakpoints(this);
      }
    }
  }

  protected void closeProcess(boolean closedByUser) {
    DebuggerManagerThreadImpl.assertIsManagerThread();

    if (myState.compareAndSet(State.INITIAL, State.DETACHING) || myState.compareAndSet(State.ATTACHED, State.DETACHING)) {
      try {
        getManagerThread().close();
      }
      finally {
        final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
        myVirtualMachineProxy = null;
        myPositionManager = CompoundPositionManager.EMPTY;
        myReturnValueWatcher = null;
        myNodeRenderersMap.clear();
        myRenderers.clear();
        DebuggerUtils.cleanupAfterProcessFinish(this);
        myState.compareAndSet(State.DETACHING, State.DETACHED);
        try {
          myDebugProcessDispatcher.getMulticaster().processDetached(this, closedByUser);
        }
        finally {
          //if (DebuggerSettings.getInstance().UNMUTE_ON_STOP) {
          //  XDebugSession session = mySession.getXDebugSession();
          //  if (session != null) {
          //    session.setBreakpointMuted(false);
          //  }
          //}
          if (vm != null) {
            try {
              vm.dispose(); // to be on the safe side ensure that VM mirror, if present, is disposed and invalidated
            }
            catch (Throwable ignored) {
            }
          }
          myWaitFor.up();
        }
      }

    }
  }

  private static String formatMessage(String message) {
    final int lineLength = 90;
    StringBuilder buf = new StringBuilder(message.length());
    int index = 0;
    while (index < message.length()) {
      buf.append(message, index, Math.min(index + lineLength, message.length())).append('\n');
      index += lineLength;
    }
    return buf.toString();
  }

  public static String processError(Exception e) {
    String message;

    if (e instanceof VMStartException) {
      VMStartException e1 = (VMStartException)e;
      message = e1.getLocalizedMessage();
    }
    else if (e instanceof IllegalConnectorArgumentsException) {
      IllegalConnectorArgumentsException e1 = (IllegalConnectorArgumentsException)e;
      final List<String> invalidArgumentNames = e1.argumentNames();
      message = formatMessage(DebuggerBundle.message("error.invalid.argument", invalidArgumentNames.size()) + ": "+ e1.getLocalizedMessage()) + invalidArgumentNames;
      LOG.debug(e1);
    }
    else if (e instanceof CantRunException) {
      message = e.getLocalizedMessage();
    }
    else if (e instanceof VMDisconnectedException) {
      message = DebuggerBundle.message("error.vm.disconnected");
    }
    else if (e instanceof IOException) {
      message = processIOException((IOException)e, null);
    }
    else if (e instanceof ExecutionException) {
      message = e.getLocalizedMessage();
    }
    else {
      message = DebuggerBundle.message("error.exception.while.connecting", e.getClass().getName(), e.getLocalizedMessage());
      LOG.debug(e);
    }
    return message;
  }

  @NotNull
  public static String processIOException(@NotNull IOException e, @Nullable String address) {
    if (e instanceof UnknownHostException) {
      return DebuggerBundle.message("error.unknown.host") + (address != null ? " (" + address + ")" : "") + ":\n" + e.getLocalizedMessage();
    }

    // Failed SA attach
    Throwable cause = e.getCause();
    if (cause instanceof InvocationTargetException) {
      if (cause.getCause() != null) {
        return cause.getCause().getLocalizedMessage();
      }
    }

    StringBuilder buf = new StringBuilder();
    if (address != null) {
      buf.append(DebuggerBundle.message("error.cannot.open.debugger.port"));
      buf.append(" (").append(address).append("): ");
    }
    buf.append(e.getClass().getName()).append(" ");
    if (!StringUtil.isEmpty(e.getLocalizedMessage())) {
      buf.append('"').append(e.getLocalizedMessage()).append('"');
    }
    if (cause != null && !StringUtil.isEmpty(cause.getLocalizedMessage())) {
      buf.append(" (").append(cause.getLocalizedMessage()).append(')');
    }
    LOG.debug(e);
    return buf.toString();
  }

  public void dispose() {
    Disposer.dispose(myDisposable);
    myRequestManager.setFilterThread(null);
  }

  @Override
  public DebuggerManagerThreadImpl getManagerThread() {
    return myDebuggerManagerThread;
  }

  private static int getInvokePolicy(SuspendContext suspendContext) {
    if (suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD || isResumeOnlyCurrentThread()) {
      return ObjectReference.INVOKE_SINGLE_THREADED;
    }
    return 0;
  }

  @Override
  public void waitFor() {
    LOG.assertTrue(!DebuggerManagerThreadImpl.isManagerThread());
    myWaitFor.waitFor();
  }

  @Override
  public void waitFor(long timeout) {
    LOG.assertTrue(!DebuggerManagerThreadImpl.isManagerThread());
    myWaitFor.waitFor(timeout);
  }

  private abstract class InvokeCommand <E extends Value> {
    private final Method myMethod;
    private final List<Value> myArgs;

    protected InvokeCommand(@NotNull Method method, @NotNull List<? extends Value> args) {
      myMethod = method;
      myArgs = new ArrayList<>(args);
    }

    public String toString() {
      return "INVOKE: " + super.toString();
    }

    protected abstract E invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                ClassNotLoadedException,
                                                                                IncompatibleThreadStateException,
                                                                                InvalidTypeException;


    E start(EvaluationContextImpl evaluationContext, boolean internalEvaluate) throws EvaluateException {
      while (true) {
        try {
          return startInternal(evaluationContext, internalEvaluate);
        }
        catch (ClassNotLoadedException e) {
          ReferenceType loadedClass = null;
          try {
            if (evaluationContext.isAutoLoadClasses()) {
              loadedClass = loadClass(evaluationContext, e.className(), evaluationContext.getClassLoader());
            }
          }
          catch (Exception ignored) {
            loadedClass = null;
          }
          if (loadedClass == null) {
            throw EvaluateExceptionUtil.createEvaluateException(e);
          }
        }
      }
    }

    E startInternal(EvaluationContextImpl evaluationContext, boolean internalEvaluate)
      throws EvaluateException, ClassNotLoadedException {
      DebuggerManagerThreadImpl.assertIsManagerThread();
      SuspendContextImpl suspendContext = evaluationContext.getSuspendContext();
      SuspendManagerUtil.assertSuspendContext(suspendContext);

      ThreadReferenceProxyImpl invokeThread = suspendContext.getThread();

      if (SuspendManagerUtil.isEvaluating(getSuspendManager(), invokeThread)) {
        throw EvaluateExceptionUtil.NESTED_EVALUATION_ERROR;
      }

      if (!suspendContext.suspends(invokeThread)) {
        throw EvaluateExceptionUtil.THREAD_WAS_RESUMED;
      }

      Set<SuspendContextImpl> suspendingContexts = SuspendManagerUtil.getSuspendingContexts(getSuspendManager(), invokeThread);
      final ThreadReference invokeThreadRef = invokeThread.getThreadReference();

      myEvaluationDispatcher.getMulticaster().evaluationStarted(suspendContext);
      beforeMethodInvocation(suspendContext, myMethod, internalEvaluate);

      Object resumeData = null;
      try {
        for (SuspendContextImpl suspendingContext : suspendingContexts) {
          final ThreadReferenceProxyImpl suspendContextThread = suspendingContext.getThread();
          if (suspendContextThread != invokeThread) {
            if (LOG.isDebugEnabled()) {
              LOG.debug("Resuming " + invokeThread + " that is paused by " + suspendContextThread);
            }
            LOG.assertTrue(suspendContextThread == null || !invokeThreadRef.equals(suspendContextThread.getThreadReference()));
            getSuspendManager().resumeThread(suspendingContext, invokeThread);
          }
        }

        resumeData = SuspendManagerUtil.prepareForResume(suspendContext);
        suspendContext.setIsEvaluating(evaluationContext);

        getVirtualMachineProxy().clearCaches();

        return invokeMethodAndFork(suspendContext);
      }
      catch (InvocationException | InternalException | UnsupportedOperationException | ObjectCollectedException |
        InvalidTypeException | IncompatibleThreadStateException e) {
        throw EvaluateExceptionUtil.createEvaluateException(e);
      }
      finally {
        suspendContext.setIsEvaluating(null);
        if (resumeData != null) {
          SuspendManagerUtil.restoreAfterResume(suspendContext, resumeData);
        }
        for (SuspendContextImpl suspendingContext : mySuspendManager.getEventContexts()) {
          if (suspendingContexts.contains(suspendingContext) &&
              !suspendingContext.isEvaluating() &&
              !suspendingContext.suspends(invokeThread)) {
            mySuspendManager.suspendThread(suspendingContext, invokeThread);
          }
        }

        LOG.debug("getVirtualMachine().clearCaches()");
        getVirtualMachineProxy().clearCaches();
        afterMethodInvocation(suspendContext, internalEvaluate);

        myEvaluationDispatcher.getMulticaster().evaluationFinished(suspendContext);
      }
    }

    private E invokeMethodAndFork(final SuspendContextImpl context) throws InvocationException,
                                                                           ClassNotLoadedException,
                                                                           IncompatibleThreadStateException,
                                                                           InvalidTypeException {
      final int invokePolicy = getInvokePolicy(context);
      final Exception[] exception = new Exception[1];
      final Value[] result = new Value[1];
      getManagerThread().startLongProcessAndFork(() -> {
        ThreadReferenceProxyImpl thread = context.getThread();
        try {
          try {
            if (LOG.isDebugEnabled()) {
              final VirtualMachineProxyImpl virtualMachineProxy = getVirtualMachineProxy();
              virtualMachineProxy.logThreads();
              LOG.debug("Invoke in " + thread.name());
              assertThreadSuspended(thread, context);
            }

            if (myMethod.isVarArgs()) {
              // See IDEA-63581
              // if vararg parameter array is of interface type and Object[] is expected, JDI wrap it into another array,
              // in this case we have to unroll the array manually and pass its elements to the method instead of array object
              int lastIndex = myMethod.argumentTypeNames().size() - 1;
              if (lastIndex >= 0 && myArgs.size() > lastIndex) { // at least one varargs param
                Object firstVararg = myArgs.get(lastIndex);
                if (myArgs.size() == lastIndex + 1) { // only one vararg param
                  if (firstVararg instanceof ArrayReference) {
                    ArrayReference arrayRef = (ArrayReference)firstVararg;
                    if (((ArrayType)arrayRef.referenceType()).componentType() instanceof InterfaceType) {
                      List<String> argTypes = myMethod.argumentTypeNames();
                      if (argTypes.size() > lastIndex && argTypes.get(lastIndex).startsWith(CommonClassNames.JAVA_LANG_OBJECT)) {
                        // unwrap array of interfaces for vararg param
                        myArgs.remove(lastIndex);
                        myArgs.addAll(arrayRef.getValues());
                      }
                    }
                  }
                }
                else if (firstVararg == null) { // more than one vararg params and the first one is null
                  // this is a workaround for a bug in jdi, see IDEA-157321
                  int argCount = myArgs.size();
                  List<Type> paramTypes = myMethod.argumentTypes();
                  int paramCount = paramTypes.size();
                  ArrayType lastParamType = (ArrayType)paramTypes.get(paramTypes.size() - 1);

                  int count = argCount - paramCount + 1;
                  ArrayReference argArray = lastParamType.newInstance(count);
                  argArray.setValues(0, myArgs, paramCount - 1, count);
                  myArgs.set(paramCount - 1, argArray);
                  myArgs.subList(paramCount, argCount).clear();
                }
              }
            }

            // workaround for multi-array args bug in jdi calls
            for (Value arg : myArgs) {
              if (arg instanceof ArrayReference) {
                Type type = arg.type();
                while (type instanceof ArrayType) {
                  type = ((ArrayType)type).componentType(); // this will throw ClassNotLoadedException if necessary
                }
              }
            }

            if (!Patches.IBM_JDK_DISABLE_COLLECTION_BUG) {
              // ensure args are not collected
              StreamEx.of(myArgs).select(ObjectReference.class).forEach(DebuggerUtilsEx::disableCollection);
            }

            if (Patches.JDK_BUG_ID_21275177 && (ourTraceMask & VirtualMachine.TRACE_SENDS) != 0) {
              //noinspection ResultOfMethodCallIgnored
              myArgs.forEach(Object::toString);
            }

            // workaround for jdi hang in trace mode, see IDEA-183387
            if (Patches.JDK_BUG_WITH_TRACE_SEND && (ourTraceMask & VirtualMachine.TRACE_SENDS) != 0) {
              StreamEx.of(myArgs).findAny(ThreadReference.class::isInstance).ifPresent(t -> {
                //noinspection UseOfSystemOutOrSystemErr
                System.err.println("[JDI: workaround for invocation of " + myMethod + "]");
                myMethod.virtualMachine().setDebugTraceMode(ourTraceMask & ~VirtualMachine.TRACE_SENDS);
              });
            }

            result[0] = invokeMethod(invokePolicy, myMethod, myArgs);
          }
          finally {
            if (Patches.JDK_BUG_WITH_TRACE_SEND && (ourTraceMask & VirtualMachine.TRACE_SENDS) != 0) {
              myMethod.virtualMachine().setDebugTraceMode(ourTraceMask);
            }
            //  assertThreadSuspended(thread, context);
            if (!Patches.IBM_JDK_DISABLE_COLLECTION_BUG) {
              // ensure args are not collected
              StreamEx.of(myArgs).select(ObjectReference.class).forEach(DebuggerUtilsEx::enableCollection);
            }
          }
        }
        catch (Exception e) {
          exception[0] = e;
        }
      });

      if (exception[0] != null) {
        if (exception[0] instanceof InvocationException) {
          throw (InvocationException)exception[0];
        }
        else if (exception[0] instanceof ClassNotLoadedException) {
          throw (ClassNotLoadedException)exception[0];
        }
        else if (exception[0] instanceof IncompatibleThreadStateException) {
          throw (IncompatibleThreadStateException)exception[0];
        }
        else if (exception[0] instanceof InvalidTypeException) {
          throw (InvalidTypeException)exception[0];
        }
        else if (exception[0] instanceof RuntimeException) {
          throw (RuntimeException)exception[0];
        }
        else {
          LOG.error("Unexpected exception", new Throwable().initCause(exception[0]));
        }
      }

      return (E)result[0];
    }

    private void assertThreadSuspended(final ThreadReferenceProxyImpl thread, final SuspendContextImpl context) {
      LOG.assertTrue(context.isEvaluating());
      try {
        final boolean isSuspended = thread.isSuspended();
        LOG.assertTrue(isSuspended, thread);
      }
      catch (ObjectCollectedException ignored) {
      }
    }
  }

  @Override
  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull ObjectReference objRef,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args) throws EvaluateException {
    return invokeInstanceMethod(evaluationContext, objRef, method, args, 0);
  }

  @Override
  public Value invokeInstanceMethod(@NotNull EvaluationContext evaluationContext,
                                    @NotNull final ObjectReference objRef,
                                    @NotNull Method method,
                                    @NotNull List<? extends Value> args,
                                    final int invocationOptions) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<Value>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + objRef.type().name() + "." + method.name());
        }
        return objRef.invokeMethod(thread, method, args, invokePolicy | invocationOptions);
      }
    }.start((EvaluationContextImpl)evaluationContext, false);
  }

  private static ThreadReference getEvaluationThread(final EvaluationContext evaluationContext) throws EvaluateException {
    ThreadReferenceProxy evaluationThread = evaluationContext.getSuspendContext().getThread();
    if(evaluationThread == null) {
      throw EvaluateExceptionUtil.NULL_STACK_FRAME;
    }
    return evaluationThread.getThreadReference();
  }

  @Override
  public Value invokeMethod(EvaluationContext evaluationContext,
                            ClassType classType,
                            Method method,
                            List<? extends Value> args) throws EvaluateException {
    return invokeMethod(evaluationContext, classType, method, args, 0,false);
  }

  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull final ClassType classType,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args,
                            boolean internalEvaluate) throws EvaluateException {
    return invokeMethod(evaluationContext, classType, method, args, 0, internalEvaluate);
  }

  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull final ClassType classType,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args,
                            int extraInvocationOptions,
                            boolean internalEvaluate) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<Value>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                             ClassNotLoadedException,
                                                                             IncompatibleThreadStateException,
                                                                             InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + classType.name() + "." + method.name());
        }
        return classType.invokeMethod(thread, method, args, invokePolicy | extraInvocationOptions);
      }
    }.start((EvaluationContextImpl)evaluationContext, internalEvaluate);
  }

  public Value invokeMethod(EvaluationContext evaluationContext,
                            InterfaceType interfaceType,
                            Method method,
                            List<? extends Value> args) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<Value>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                      ClassNotLoadedException,
                                                                                      IncompatibleThreadStateException,
                                                                                      InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + interfaceType.name() + "." + method.name());
        }

        try {
          return interfaceType.invokeMethod(thread, method, args, invokePolicy);
        }
        catch (LinkageError e) {
          throw new IllegalStateException("Interface method invocation is not supported in JVM " +
                                          SystemInfo.JAVA_VERSION +
                                          ". Use JVM 1.8.0_45 or higher to run " +
                                          ApplicationNamesInfo.getInstance().getFullProductName());
        }
      }
    }.start((EvaluationContextImpl)evaluationContext, false);
  }


  @Override
  public ArrayReference newInstance(final ArrayType arrayType,
                                    final int dimension)
    throws EvaluateException {
    try {
      return arrayType.newInstance(dimension);
    }
    catch (Exception e) {
      throw EvaluateExceptionUtil.createEvaluateException(e);
    }
  }

  @Override
  public ObjectReference newInstance(@NotNull final EvaluationContext evaluationContext,
                                     @NotNull final ClassType classType,
                                     @NotNull Method method,
                                     @NotNull List<? extends Value> args) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    InvokeCommand<ObjectReference> invokeCommand = new InvokeCommand<ObjectReference>(method, args) {
      @Override
      protected ObjectReference invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                       ClassNotLoadedException,
                                                                                       IncompatibleThreadStateException,
                                                                                       InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("New instance " + classType.name() + "." + method.name());
        }
        return classType.newInstance(thread, method, args, invokePolicy);
      }
    };
    return invokeCommand.start((EvaluationContextImpl)evaluationContext, false);
  }

  public void clearCashes(@MagicConstant(flagsFromClass = EventRequest.class) int suspendPolicy) {
    if (!isAttached()) return;
    switch (suspendPolicy) {
      case EventRequest.SUSPEND_ALL:
        getVirtualMachineProxy().clearCaches();
        break;
      case EventRequest.SUSPEND_EVENT_THREAD:
        getVirtualMachineProxy().clearCaches();
        //suspendContext.getThread().clearAll();
        break;
    }
  }

  protected void beforeSuspend(SuspendContextImpl suspendContext) {
    clearCashes(suspendContext.getSuspendPolicy());
  }

  private void beforeMethodInvocation(SuspendContextImpl suspendContext, Method method, boolean internalEvaluate) {
    if (LOG.isDebugEnabled()) {
      LOG.debug(
        "before invocation in  thread " + suspendContext.getThread().name() + " method " + (method == null ? "null" : method.name()));
    }

    if (!internalEvaluate) {
      if (method != null) {
        showStatusText(DebuggerBundle.message("progress.evaluating", DebuggerUtilsEx.methodName(method)));
      }
      else {
        showStatusText(DebuggerBundle.message("title.evaluating"));
      }
    }
  }

  private void afterMethodInvocation(SuspendContextImpl suspendContext, boolean internalEvaluate) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("after invocation in  thread " + suspendContext.getThread().name());
    }
    if (!internalEvaluate) {
      showStatusText("");
    }
  }

  @Override
  public ReferenceType findClass(@Nullable EvaluationContext evaluationContext, String className,
                                 ClassLoaderReference classLoader) throws EvaluateException {
    try {
      DebuggerManagerThreadImpl.assertIsManagerThread();
      ReferenceType result = null;
      for (final ReferenceType refType : getVirtualMachineProxy().classesByName(className)) {
        if (refType.isPrepared() && isVisibleFromClassLoader(classLoader, refType)) {
          result = refType;
          break;
        }
      }
      if (result == null && evaluationContext != null) {
        EvaluationContextImpl evalContext = (EvaluationContextImpl)evaluationContext;
        if (evalContext.isAutoLoadClasses()) {
          return loadClass(evalContext, className, classLoader);
        }
      }
      return result;
    }
    catch (InvocationException | InvalidTypeException | IncompatibleThreadStateException | ClassNotLoadedException e) {
      throw EvaluateExceptionUtil.createEvaluateException(e);
    }
  }

  private static boolean isVisibleFromClassLoader(final ClassLoaderReference fromLoader, final ReferenceType refType) {
    // IMPORTANT! Even if the refType is already loaded by some parent or bootstrap loader, it may not be visible from the given loader.
    // For example because there were no accesses yet from this loader to this class. So the loader is not in the list of "initialing" loaders
    // for this refType and the refType is not visible to the loader.
    // Attempt to evaluate method with this refType will yield ClassNotLoadedException.
    // The only way to say for sure whether the class is _visible_ to the given loader, is to use the following API call
    return fromLoader == null || fromLoader.equals(refType.classLoader()) || fromLoader.visibleClasses().contains(refType);
  }

  private static String reformatArrayName(String className) {
    if (className.indexOf('[') == -1) return className;

    int dims = 0;
    while (className.endsWith("[]")) {
      className = className.substring(0, className.length() - 2);
      dims++;
    }

    StringBuilder buffer = new StringBuilder();
    StringUtil.repeatSymbol(buffer, '[', dims);
    String primitiveSignature = JVMNameUtil.getPrimitiveSignature(className);
    if(primitiveSignature != null) {
      buffer.append(primitiveSignature);
    }
    else {
      buffer.append('L');
      buffer.append(className);
      buffer.append(';');
    }
    return buffer.toString();
  }

  @SuppressWarnings({"SpellCheckingInspection"})
  public ReferenceType loadClass(EvaluationContextImpl evaluationContext, String qName, ClassLoaderReference classLoader)
    throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException, EvaluateException {

    DebuggerManagerThreadImpl.assertIsManagerThread();
    qName = reformatArrayName(qName);
    ReferenceType refType = null;
    VirtualMachineProxyImpl virtualMachine = getVirtualMachineProxy();
    ClassType classClassType = (ClassType)ContainerUtil.getFirstItem(virtualMachine.classesByName(CommonClassNames.JAVA_LANG_CLASS));
    if (classClassType != null) {
      final Method forNameMethod;
      List<Value> args = new ArrayList<>(); // do not use unmodifiable lists because the list is modified by JPDA
      args.add(virtualMachine.mirrorOf(qName));
      if (classLoader != null) {
        //forNameMethod = classClassType.concreteMethodByName("forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
        forNameMethod = DebuggerUtils.findMethod(classClassType, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
        args.add(virtualMachine.mirrorOf(true));
        args.add(classLoader);
      }
      else {
        //forNameMethod = classClassType.concreteMethodByName("forName", "(Ljava/lang/String;)Ljava/lang/Class;");
        forNameMethod = DebuggerUtils.findMethod(classClassType, "forName", "(Ljava/lang/String;)Ljava/lang/Class;");
      }
      Value classReference = invokeMethod(evaluationContext, classClassType, forNameMethod, args);
      if (classReference instanceof ClassObjectReference) {
        refType = ((ClassObjectReference)classReference).reflectedType();
      }
    }
    return refType;
  }

  public void logThreads() {
    if (LOG.isDebugEnabled()) {
      try {
        Collection<ThreadReferenceProxyImpl> allThreads = getVirtualMachineProxy().allThreads();
        for (ThreadReferenceProxyImpl threadReferenceProxy : allThreads) {
          LOG.debug("Thread name=" + threadReferenceProxy.name() + " suspendCount()=" + threadReferenceProxy.getSuspendCount());
        }
      }
      catch (Exception e) {
        LOG.debug(e);
      }
    }
  }

  public void onHotSwapFinished() {
    getPositionManager().clearCache();
    StackCapturingLineBreakpoint.clearCaches(this);
  }

  @NotNull
  public SuspendManager getSuspendManager() {
    return mySuspendManager;
  }

  @NotNull
  @Override
  public CompoundPositionManager getPositionManager() {
    return myPositionManager;
  }
  //ManagerCommands

  @Override
  public void stop(boolean forceTerminate) {
    myIsStopped.set(true);
    stopConnecting(); // does this first place in case debugger manager hanged accepting debugger connection (forever)
    getManagerThread().terminateAndInvoke(createStopCommand(forceTerminate), ApplicationManager.getApplication().isUnitTestMode() ? 0 : DebuggerManagerThreadImpl.COMMAND_TIMEOUT);
  }

  @NotNull
  public StopCommand createStopCommand(boolean forceTerminate) {
    return new StopCommand(forceTerminate);
  }

  protected class StopCommand extends DebuggerCommandImpl {
    private final boolean myIsTerminateTargetVM;

    public StopCommand(boolean isTerminateTargetVM) {
      myIsTerminateTargetVM = isTerminateTargetVM;
    }

    @Override
    public Priority getPriority() {
      return Priority.HIGH;
    }

    @Override
    protected void action() {
      if (isAttached()) {
        VirtualMachineProxyImpl virtualMachineProxy = getVirtualMachineProxy();

        if (!virtualMachineProxy.canBeModified()) {
          closeProcess(false);
          return;
        }

        if (myIsTerminateTargetVM) {
          virtualMachineProxy.exit(-1);
        }
        else {
          // some VMs (like IBM VM 1.4.2 bundled with WebSphere) does not resume threads on dispose() like it should
          try {
            virtualMachineProxy.resume();
          }
          finally {
            virtualMachineProxy.dispose();
          }
        }
      }
      else {
        try {
          stopConnecting();
        }
        finally {
          closeProcess(true);
        }
      }
    }
  }

  private class StepOutCommand extends StepCommand {
    private final int myStepSize;

    StepOutCommand(SuspendContextImpl suspendContext, int stepSize) {
      super(suspendContext);
      myStepSize = stepSize;
    }

    @Override
    public void contextAction() {
      showStatusText(DebuggerBundle.message("status.step.out"));
      final SuspendContextImpl suspendContext = getSuspendContext();
      final ThreadReferenceProxyImpl thread = getContextThread();
      RequestHint hint = new RequestHint(thread, suspendContext, StepRequest.STEP_OUT);
      hint.setIgnoreFilters(mySession.shouldIgnoreSteppingFilters());
      applyThreadFilter(thread);
      startWatchingMethodReturn(thread);
      doStep(suspendContext, thread, myStepSize, StepRequest.STEP_OUT, hint);
      super.contextAction();
    }
  }

  private class StepIntoCommand extends StepCommand {
    private final boolean myForcedIgnoreFilters;
    private final MethodFilter mySmartStepFilter;
    @Nullable
    private final StepIntoBreakpoint myBreakpoint;
    private final int myStepSize;

    StepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, @Nullable final MethodFilter methodFilter,
                           int stepSize) {
      super(suspendContext);
      myForcedIgnoreFilters = ignoreFilters || methodFilter != null;
      mySmartStepFilter = methodFilter;
      myBreakpoint = methodFilter instanceof BreakpointStepMethodFilter ?
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().addStepIntoBreakpoint(((BreakpointStepMethodFilter)methodFilter)) :
        null;
      myStepSize = stepSize;
    }

    @Override
    public void contextAction() {
      showStatusText(DebuggerBundle.message("status.step.into"));
      final SuspendContextImpl suspendContext = getSuspendContext();
      final ThreadReferenceProxyImpl stepThread = getContextThread();
      final RequestHint hint = mySmartStepFilter != null?
                               new RequestHint(stepThread, suspendContext, mySmartStepFilter) :
                               new RequestHint(stepThread, suspendContext, StepRequest.STEP_INTO);
      hint.setResetIgnoreFilters(mySmartStepFilter != null && !mySession.shouldIgnoreSteppingFilters());
      if (myForcedIgnoreFilters) {
        try {
          mySession.setIgnoreStepFiltersFlag(stepThread.frameCount());
        }
        catch (EvaluateException e) {
          LOG.info(e);
        }
      }
      hint.setIgnoreFilters(myForcedIgnoreFilters || mySession.shouldIgnoreSteppingFilters());
      applyThreadFilter(stepThread);
      if (myBreakpoint != null) {
        myBreakpoint.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD? DebuggerSettings.SUSPEND_THREAD : DebuggerSettings.SUSPEND_ALL);
        myBreakpoint.createRequest(suspendContext.getDebugProcess());
        myBreakpoint.setRequestHint(hint);
        setRunToCursorBreakpoint(myBreakpoint);
      }
      doStep(suspendContext, stepThread, myStepSize, StepRequest.STEP_INTO, hint);
      super.contextAction();
    }
  }

  public class StepOverCommand extends StepCommand {
    private final boolean myIsIgnoreBreakpoints;
    private final int myStepSize;

    public StepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints, int stepSize) {
      super(suspendContext);
      myIsIgnoreBreakpoints = ignoreBreakpoints;
      myStepSize = stepSize;
    }

    protected int getStepSize() {
      return StepRequest.STEP_OVER;
    }

    @NotNull
    protected String getStatusText() {
      return DebuggerBundle.message("status.step.over");
    }

    @NotNull
    protected RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread) {
      // need this hint while stepping over for JSR45 support:
      // several lines of generated java code may correspond to a single line in the source file,
      // from which the java code was generated
      RequestHint hint = new RequestHint(stepThread, suspendContext, StepRequest.STEP_OVER);
      hint.setRestoreBreakpoints(myIsIgnoreBreakpoints);
      hint.setIgnoreFilters(myIsIgnoreBreakpoints || mySession.shouldIgnoreSteppingFilters());
      return hint;
    }

    @Override
    public void contextAction() {
      showStatusText(getStatusText());

      SuspendContextImpl suspendContext = getSuspendContext();
      ThreadReferenceProxyImpl stepThread = getContextThread();

      RequestHint hint = getHint(suspendContext, stepThread);

      applyThreadFilter(stepThread);

      startWatchingMethodReturn(stepThread);

      doStep(suspendContext, stepThread, myStepSize, getStepSize(), hint);

      if (myIsIgnoreBreakpoints) {
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().disableBreakpoints(DebugProcessImpl.this);
      }
      super.contextAction();
    }
  }

  private class RunToCursorCommand extends StepCommand {
    private final RunToCursorBreakpoint myRunToCursorBreakpoint;
    private final boolean myIgnoreBreakpoints;

    private RunToCursorCommand(SuspendContextImpl suspendContext, @NotNull XSourcePosition position, final boolean ignoreBreakpoints) {
      super(suspendContext);
      myIgnoreBreakpoints = ignoreBreakpoints;
      myRunToCursorBreakpoint =
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().addRunToCursorBreakpoint(position, ignoreBreakpoints);
    }

    @Override
    public void contextAction() {
      showStatusText(DebuggerBundle.message("status.run.to.cursor"));
      cancelRunToCursorBreakpoint();
      if (myRunToCursorBreakpoint == null) {
        return;
      }
      if (myIgnoreBreakpoints) {
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().disableBreakpoints(DebugProcessImpl.this);
      }
      applyThreadFilter(getContextThread());
      final SuspendContextImpl context = getSuspendContext();
      myRunToCursorBreakpoint.setSuspendPolicy(context.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD? DebuggerSettings.SUSPEND_THREAD : DebuggerSettings.SUSPEND_ALL);
      final DebugProcessImpl debugProcess = context.getDebugProcess();
      myRunToCursorBreakpoint.createRequest(debugProcess);
      setRunToCursorBreakpoint(myRunToCursorBreakpoint);

      if (debugProcess.getRequestsManager().getWarning(myRunToCursorBreakpoint) == null) {
        super.contextAction();
      }
      else {
        DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
          Messages.showErrorDialog(
            DebuggerBundle.message("error.running.to.cursor.no.executable.code",
                                   myRunToCursorBreakpoint.getSourcePosition().getFile().getName(),
                                   myRunToCursorBreakpoint.getLineIndex() + 1),
            UIUtil.removeMnemonic(ActionsBundle.actionText(XDebuggerActions.RUN_TO_CURSOR)));
          DebuggerSession session = debugProcess.getSession();
          session.getContextManager().setState(DebuggerContextUtil.createDebuggerContext(session, context),
                                               DebuggerSession.State.PAUSED, DebuggerSession.Event.CONTEXT, null);
        });
      }
    }
  }

  private abstract class StepCommand extends ResumeCommand {
    StepCommand(SuspendContextImpl suspendContext) {
      super(suspendContext);
    }

    @Override
    protected void resumeAction() {
      SuspendContextImpl context = getSuspendContext();
      if (context != null &&
          (context.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD || isResumeOnlyCurrentThread())) {
        myThreadBlockedMonitor.startWatching(myContextThread);
      }
      if (context != null
          && isResumeOnlyCurrentThread()
          && context.getSuspendPolicy() == EventRequest.SUSPEND_ALL
          && myContextThread != null) {
        getSuspendManager().resumeThread(context, myContextThread);
      }
      else {
        super.resumeAction();
      }
    }
  }

  public abstract class ResumeCommand extends SuspendContextCommandImpl {
    @Nullable protected final ThreadReferenceProxyImpl myContextThread;

    public ResumeCommand(SuspendContextImpl suspendContext) {
      super(suspendContext);
      final ThreadReferenceProxyImpl contextThread = getDebuggerContext().getThreadProxy();
      myContextThread = contextThread != null ? contextThread : (suspendContext != null? suspendContext.getThread() : null);
    }

    @Override
    public Priority getPriority() {
      return Priority.HIGH;
    }

    @Override
    public void contextAction() {
      showStatusText(DebuggerBundle.message("status.process.resumed"));
      resumeAction();
      myDebugProcessDispatcher.getMulticaster().resumed(getSuspendContext());
    }

    protected void resumeAction() {
      getSuspendManager().resume(getSuspendContext());
    }

    @Nullable
    public ThreadReferenceProxyImpl getContextThread() {
      return myContextThread;
    }

    protected void applyThreadFilter(ThreadReferenceProxy thread) {
      if (getSuspendContext().getSuspendPolicy() == EventRequest.SUSPEND_ALL) {
        // there could be explicit resume as a result of call to voteSuspend()
        // e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_
        // resuming and all breakpoints in other threads will be ignored.
        // As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens
        final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager();
        breakpointManager.applyThreadFilter(DebugProcessImpl.this, thread.getThreadReference());
      }
    }
  }

  private class PauseCommand extends DebuggerCommandImpl {
    PauseCommand() {
    }

    @Override
    public void action() {
      if (!isAttached() || getVirtualMachineProxy().isPausePressed()) {
        return;
      }
      logThreads();
      getVirtualMachineProxy().suspend();
      logThreads();
      SuspendContextImpl suspendContext = mySuspendManager.pushSuspendContext(EventRequest.SUSPEND_ALL, 0);
      myDebugProcessDispatcher.getMulticaster().paused(suspendContext);
    }
  }

  private class ResumeThreadCommand extends SuspendContextCommandImpl {
    private final ThreadReferenceProxyImpl myThread;

    ResumeThreadCommand(SuspendContextImpl suspendContext, @NotNull ThreadReferenceProxyImpl thread) {
      super(suspendContext);
      myThread = thread;
    }

    @Override
    public void contextAction() {
      // handle unfreeze through the regular context resume
      if (false && getSuspendManager().isFrozen(myThread)) {
        getSuspendManager().unfreezeThread(myThread);
        return;
      }

      final Set<SuspendContextImpl> suspendingContexts = SuspendManagerUtil.getSuspendingContexts(getSuspendManager(), myThread);
      for (SuspendContextImpl suspendContext : suspendingContexts) {
        if (suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD && suspendContext.getThread() == myThread) {
          getSession().getXDebugSession().sessionResumed();
          getManagerThread().invoke(createResumeCommand(suspendContext));
        }
        else {
          getSuspendManager().resumeThread(suspendContext, myThread);
        }
      }
    }
  }

  private class FreezeThreadCommand extends DebuggerCommandImpl {
    private final ThreadReferenceProxyImpl myThread;

    FreezeThreadCommand(ThreadReferenceProxyImpl thread) {
      myThread = thread;
    }

    @Override
    protected void action() {
      SuspendManager suspendManager = getSuspendManager();
      if (!suspendManager.isFrozen(myThread)) {
        suspendManager.freezeThread(myThread);
        SuspendContextImpl suspendContext = mySuspendManager.pushSuspendContext(EventRequest.SUSPEND_EVENT_THREAD, 0);
        suspendContext.setThread(myThread.getThreadReference());
        myDebugProcessDispatcher.getMulticaster().paused(suspendContext);
      }
    }
  }

  private class PopFrameCommand extends DebuggerContextCommandImpl {
    private final StackFrameProxyImpl myStackFrame;

    PopFrameCommand(DebuggerContextImpl context, StackFrameProxyImpl frameProxy) {
      super(context, frameProxy.threadProxy());
      myStackFrame = frameProxy;
    }

    @Override
    public void threadAction(@NotNull SuspendContextImpl suspendContext) {
      final ThreadReferenceProxyImpl thread = myStackFrame.threadProxy();
      try {
        if (!getSuspendManager().isSuspended(thread)) {
          notifyCancelled();
          return;
        }
      }
      catch (ObjectCollectedException ignored) {
        notifyCancelled();
        return;
      }

      if (!suspendContext.suspends(thread)) {
        suspendContext.postponeCommand(this);
        return;
      }

      if (myStackFrame.isBottom()) {
        DebuggerInvocationUtil.swingInvokeLater(myProject, () -> Messages.showMessageDialog(myProject, DebuggerBundle.message("error.pop.bottom.stackframe"), ActionsBundle.actionText(DebuggerActions.POP_FRAME), Messages.getErrorIcon()));
        return;
      }

      try {
        thread.popFrames(myStackFrame);
      }
      catch (final EvaluateException e) {
        DebuggerInvocationUtil.swingInvokeLater(myProject,
                                                () -> Messages.showMessageDialog(myProject, DebuggerBundle.message("error.pop.stackframe", e.getLocalizedMessage()), ActionsBundle.actionText(DebuggerActions.POP_FRAME), Messages.getErrorIcon()));
        LOG.info(e);
      }
      finally {
        getSuspendManager().popFrame(suspendContext);
      }
    }
  }

  @Override
  @NotNull
  public GlobalSearchScope getSearchScope() {
    LOG.assertTrue(mySession != null, "Accessing debug session before its initialization");
    return mySession.getSearchScope();
  }

  public void reattach(final DebugEnvironment environment) {
    if (!myIsStopped.get()) {
      getManagerThread().schedule(new DebuggerCommandImpl() {
        @Override
        protected void action() {
          closeProcess(false);
          doReattach();
        }

        @Override
        protected void commandCancelled() {
          doReattach(); // if the original process is already finished
        }

        private void doReattach() {
          DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
            ((XDebugSessionImpl)getXdebugProcess().getSession()).reset();
            myState.set(State.INITIAL);
            myConnection = environment.getRemoteConnection();
            getManagerThread().restartIfNeeded();
            createVirtualMachine(environment);
          });
        }
      });
    }
  }

  @Nullable
  public ExecutionResult attachVirtualMachine(final DebugEnvironment environment,
                                              final DebuggerSession session) throws ExecutionException {
    mySession = session;
    myWaitFor.down();

    ApplicationManager.getApplication().assertIsDispatchThread();
    LOG.assertTrue(isInInitialState());

    myConnection = environment.getRemoteConnection();

    createVirtualMachine(environment);

    ExecutionResult executionResult;
    try {
      synchronized (myProcessListeners) {
        executionResult = environment.createExecutionResult();
        myExecutionResult = executionResult;
        if (executionResult == null) {
          fail();
          return null;
        }
        for (ProcessListener processListener : myProcessListeners) {
          executionResult.getProcessHandler().addProcessListener(processListener);
        }
        myProcessListeners.clear();
        if (myTextBeforeStart.length() > 0) {
          printToConsoleImpl(myTextBeforeStart.toString());
          myTextBeforeStart.setLength(0);
        }
      }
    }
    catch (ExecutionException e) {
      fail();
      throw e;
    }

    // writing to volatile field ensures the other threads will see the right values in non-volatile fields

    if (ApplicationManager.getApplication().isUnitTestMode()) {
      return executionResult;
    }

    /*
    final Alarm debugPortTimeout = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);

    myExecutionResult.getProcessHandler().addProcessListener(new ProcessAdapter() {
      public void processTerminated(ProcessEvent event) {
        debugPortTimeout.cancelAllRequests();
      }

      public void startNotified(ProcessEvent event) {
        debugPortTimeout.addRequest(new Runnable() {
          public void run() {
            if(isInInitialState()) {
              ApplicationManager.getApplication().schedule(new Runnable() {
                public void run() {
                  String message = DebuggerBundle.message("status.connect.failed", DebuggerBundle.getAddressDisplayName(remoteConnection), DebuggerBundle.getTransportName(remoteConnection));
                  Messages.showErrorDialog(myProject, message, DebuggerBundle.message("title.generic.debug.dialog"));
                }
              });
            }
          }
        }, LOCAL_START_TIMEOUT);
      }
    });
    */

    return executionResult;
  }

  private void fail() {
    // need this in order to prevent calling stop() twice
    if (myIsFailed.compareAndSet(false, true)) {
      stop(false);
    }
  }

  private void createVirtualMachine(final DebugEnvironment environment) {
    final String sessionName = environment.getSessionName();
    final long pollTimeout = environment.getPollTimeout();
    final Semaphore semaphore = new Semaphore();
    semaphore.down();

    final AtomicBoolean connectorIsReady = new AtomicBoolean(false);
    myDebugProcessDispatcher.addListener(new DebugProcessListener() {
      @Override
      public void connectorIsReady() {
        connectorIsReady.set(true);
        semaphore.up();
        myDebugProcessDispatcher.removeListener(this);
      }
    });

    // reload to make sure that source positions are initialized
    DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().reloadBreakpoints();

    getManagerThread().schedule(new DebuggerCommandImpl() {
      @Override
      protected void action() {
        VirtualMachine vm = null;

        try {
          final long time = System.currentTimeMillis();
          do {
            try {
              vm = createVirtualMachineInt();
              break;
            }
            catch (final ExecutionException e) {
              if (pollTimeout > 0 && !myConnection.isServerMode() && e.getCause() instanceof IOException) {
                synchronized (this) {
                  try {
                    wait(500);
                  }
                  catch (InterruptedException ignored) {
                    break;
                  }
                }
              }
              else {
                ProcessHandler processHandler = getProcessHandler();
                boolean terminated =
                  processHandler != null && (processHandler.isProcessTerminating() || processHandler.isProcessTerminated());

                fail();
                DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
                  // propagate exception only in case we succeeded to obtain execution result,
                  // otherwise if the error is induced by the fact that there is nothing to debug, and there is no need to show
                  // this problem to the user
                  if ((myExecutionResult != null && !terminated) || !connectorIsReady.get()) {
                    ExecutionUtil.handleExecutionError(myProject, ToolWindowId.DEBUG, sessionName, e);
                  }
                });
                break;
              }
            }
          }
          while (System.currentTimeMillis() - time < pollTimeout);
        }
        finally {
          semaphore.up();
        }

        if (vm != null) {
          final VirtualMachine vm1 = vm;
          afterProcessStarted(() -> getManagerThread().schedule(new DebuggerCommandImpl() {
            @Override
            protected void action() {
              try {
                commitVM(vm1);
              } catch (VMDisconnectedException e) {
                fail();
              }
            }
          }));
        }
        else {
          fail();
        }
      }

      @Override
      protected void commandCancelled() {
        try {
          super.commandCancelled();
        }
        finally {
          semaphore.up();
        }
      }
    });

    semaphore.waitFor();
  }

  private void afterProcessStarted(final Runnable run) {
    class MyProcessAdapter extends ProcessAdapter {
      private boolean alreadyRun = false;

      public synchronized void run() {
        if(!alreadyRun) {
          alreadyRun = true;
          run.run();
        }
        removeProcessListener(this);
      }

      @Override
      public void startNotified(@NotNull ProcessEvent event) {
        run();
      }
    }
    MyProcessAdapter processListener = new MyProcessAdapter();
    addProcessListener(processListener);
    if (myExecutionResult != null) {
      if (myExecutionResult.getProcessHandler().isStartNotified()) {
        processListener.run();
      }
    }
  }

  public boolean isPausePressed() {
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    return vm != null && vm.isPausePressed();
  }

  @NotNull
  public DebuggerCommandImpl createPauseCommand() {
    return new PauseCommand();
  }

  @NotNull
  public ResumeCommand createResumeCommand(SuspendContextImpl suspendContext) {
    return createResumeCommand(suspendContext, PrioritizedTask.Priority.HIGH);
  }

  @NotNull
  public ResumeCommand createResumeCommand(SuspendContextImpl suspendContext, final PrioritizedTask.Priority priority) {
    final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager();
    return new ResumeCommand(suspendContext) {
      @Override
      public void contextAction() {
        breakpointManager.applyThreadFilter(DebugProcessImpl.this, null); // clear the filter on resume
        if (myReturnValueWatcher != null) {
          myReturnValueWatcher.clear();
        }
        super.contextAction();
      }

      @Override
      public Priority getPriority() {
        return priority;
      }
    };
  }

  @NotNull
  public ResumeCommand createStepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints) {
    return createStepOverCommand(suspendContext, ignoreBreakpoints, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints, int stepSize) {
    return new StepOverCommand(suspendContext, ignoreBreakpoints, stepSize);
  }

  @NotNull
  public ResumeCommand createStepOutCommand(SuspendContextImpl suspendContext) {
    return createStepOutCommand(suspendContext, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepOutCommand(SuspendContextImpl suspendContext, int stepSize) {
    return new StepOutCommand(suspendContext, stepSize);
  }

  @NotNull
  public ResumeCommand createStepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, final MethodFilter smartStepFilter) {
    return createStepIntoCommand(suspendContext, ignoreFilters, smartStepFilter, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, final MethodFilter smartStepFilter,
                                             int stepSize) {
    return new StepIntoCommand(suspendContext, ignoreFilters, smartStepFilter, stepSize);
  }

  @NotNull
  public ResumeCommand createRunToCursorCommand(SuspendContextImpl suspendContext,
                                                @NotNull XSourcePosition position,
                                                boolean ignoreBreakpoints)
    throws EvaluateException {
    RunToCursorCommand runToCursorCommand = new RunToCursorCommand(suspendContext, position, ignoreBreakpoints);
    if (runToCursorCommand.myRunToCursorBreakpoint == null) {
      PsiFile psiFile = PsiManager.getInstance(myProject).findFile(position.getFile());
      throw new EvaluateException(DebuggerBundle.message("error.running.to.cursor.no.executable.code", psiFile != null? psiFile.getName() : "<No File>",
                                                         position.getLine()), null);
    }
    return runToCursorCommand;
  }

  @NotNull
  public DebuggerCommandImpl createFreezeThreadCommand(ThreadReferenceProxyImpl thread) {
    return new FreezeThreadCommand(thread);
  }

  @NotNull
  public SuspendContextCommandImpl createResumeThreadCommand(SuspendContextImpl suspendContext, @NotNull ThreadReferenceProxyImpl thread) {
    return new ResumeThreadCommand(suspendContext, thread);
  }

  @NotNull
  public SuspendContextCommandImpl createPopFrameCommand(DebuggerContextImpl context, StackFrameProxyImpl stackFrame) {
    return new PopFrameCommand(context, stackFrame);
  }

  //public void setBreakpointsMuted(final boolean muted) {
  //  XDebugSession session = mySession.getXDebugSession();
  //  if (isAttached()) {
  //    getManagerThread().schedule(new DebuggerCommandImpl() {
  //      @Override
  //      protected void action() {
  //        // set the flag before enabling/disabling cause it affects if breakpoints will create requests
  //        if (myBreakpointsMuted.getAndSet(muted) != muted) {
  //          final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager();
  //          if (muted) {
  //            breakpointManager.disableBreakpoints(DebugProcessImpl.this);
  //          }
  //          else {
  //            breakpointManager.enableBreakpoints(DebugProcessImpl.this);
  //          }
  //        }
  //      }
  //    });
  //  }
  //  else {
  //    session.setBreakpointMuted(muted);
  //  }
  //}

  @NotNull
  public DebuggerContextImpl getDebuggerContext() {
    return mySession.getContextManager().getContext();
  }

  public void setXDebugProcess(JavaDebugProcess XDebugProcess) {
    myXDebugProcess = XDebugProcess;
  }

  @Nullable
  public JavaDebugProcess getXdebugProcess() {
    return myXDebugProcess;
  }

  public boolean areBreakpointsMuted() {
    XDebugSession session = mySession.getXDebugSession();
    return session != null && session.areBreakpointsMuted();
  }

  public DebuggerSession getSession() {
    return mySession;
  }

  static boolean isResumeOnlyCurrentThread() {
    return DebuggerSettings.getInstance().RESUME_ONLY_CURRENT_THREAD;
  }

  public boolean isEvaluationPossible() {
    return getSuspendManager().getPausedContext() != null;
  }

  public void startWatchingMethodReturn(ThreadReferenceProxyImpl thread) {
    if (myReturnValueWatcher != null) {
      myReturnValueWatcher.enable(thread.getThreadReference());
    }
  }

  void stopWatchingMethodReturn() {
    if (myReturnValueWatcher != null) {
      myReturnValueWatcher.disable();
    }
  }
}
