/*
 * Copyright 2000-2017 JetBrains s.r.o.
 *
 * Licensed 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 com.intellij.xdebugger.impl;

import com.intellij.execution.ExecutionManager;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunProfile;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.filters.OpenFileHyperlinkInfo;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.execution.ui.RunnerLayoutUi;
import com.intellij.ide.DataManager;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.ui.AppUIUtil;
import com.intellij.util.EventDispatcher;
import com.intellij.util.SmartList;
import com.intellij.util.containers.SmartHashSet;
import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.*;
import com.intellij.xdebugger.breakpoints.*;
import com.intellij.xdebugger.frame.XExecutionStack;
import com.intellij.xdebugger.frame.XStackFrame;
import com.intellij.xdebugger.frame.XSuspendContext;
import com.intellij.xdebugger.frame.XValueMarkerProvider;
import com.intellij.xdebugger.impl.breakpoints.*;
import com.intellij.xdebugger.impl.evaluate.XDebuggerEditorLinePainter;
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueLookupManager;
import com.intellij.xdebugger.impl.frame.XValueMarkers;
import com.intellij.xdebugger.impl.frame.XWatchesViewImpl;
import com.intellij.xdebugger.impl.settings.XDebuggerSettingManagerImpl;
import com.intellij.xdebugger.impl.ui.XDebugSessionData;
import com.intellij.xdebugger.impl.ui.XDebugSessionTab;
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import com.intellij.xdebugger.stepping.XSmartStepIntoVariant;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author nik
 */
public class XDebugSessionImpl implements XDebugSession {
  private static final Logger LOG = Logger.getInstance("#com.intellij.xdebugger.impl.XDebugSessionImpl");
  public static final NotificationGroup NOTIFICATION_GROUP = NotificationGroup.toolWindowGroup("Debugger messages", ToolWindowId.DEBUG,
                                                                                               false);
  private XDebugProcess myDebugProcess;
  private final Map<XBreakpoint<?>, CustomizedBreakpointPresentation> myRegisteredBreakpoints =
    new THashMap<>();
  private final Set<XBreakpoint<?>> myInactiveSlaveBreakpoints = Collections.synchronizedSet(new SmartHashSet<>());
  private boolean myBreakpointsDisabled;
  private final XDebuggerManagerImpl myDebuggerManager;
  private MyBreakpointListener myBreakpointListener;
  private XSuspendContext mySuspendContext;
  private XExecutionStack myCurrentExecutionStack;
  private XStackFrame myCurrentStackFrame;
  private boolean myIsTopFrame;
  private volatile XSourcePosition myTopFramePosition;
  private final AtomicBoolean myPaused = new AtomicBoolean();
  private MyDependentBreakpointListener myDependentBreakpointListener;
  private XValueMarkers<?, ?> myValueMarkers;
  private final String mySessionName;
  private @Nullable XDebugSessionTab mySessionTab;
  private final XDebugSessionData mySessionData;
  private XBreakpoint<?> myActiveNonLineBreakpoint;
  private final EventDispatcher<XDebugSessionListener> myDispatcher = EventDispatcher.create(XDebugSessionListener.class);
  private final Project myProject;
  private final @Nullable ExecutionEnvironment myEnvironment;
  private final AtomicBoolean myStopped = new AtomicBoolean();
  private boolean myPauseActionSupported;
  private final AtomicBoolean myShowTabOnSuspend;
  private final List<AnAction> myRestartActions = new SmartList<>();
  private final List<AnAction> myExtraStopActions = new SmartList<>();
  private final List<AnAction> myExtraActions = new SmartList<>();
  private ConsoleView myConsoleView;
  private final Icon myIcon;

  private volatile boolean breakpointsInitialized;

  public XDebugSessionImpl(@NotNull ExecutionEnvironment environment, @NotNull XDebuggerManagerImpl debuggerManager) {
    this(environment, debuggerManager, environment.getRunProfile().getName(), environment.getRunProfile().getIcon(), false, null);
  }

  public XDebugSessionImpl(@Nullable ExecutionEnvironment environment,
                           @NotNull XDebuggerManagerImpl debuggerManager,
                           @NotNull String sessionName,
                           @Nullable Icon icon,
                           boolean showTabOnSuspend,
                           @Nullable RunContentDescriptor contentToReuse) {
    myEnvironment = environment;
    mySessionName = sessionName;
    myDebuggerManager = debuggerManager;
    myShowTabOnSuspend = new AtomicBoolean(showTabOnSuspend);
    myProject = debuggerManager.getProject();
    ValueLookupManager.getInstance(myProject).startListening();
    myIcon = icon;

    XDebugSessionData oldSessionData = null;
    if (contentToReuse == null) {
      contentToReuse = environment != null ? environment.getContentToReuse() : null;
    }
    if (contentToReuse != null) {
      JComponent component = contentToReuse.getComponent();
      if (component != null) {
        oldSessionData = XDebugSessionData.DATA_KEY.getData(DataManager.getInstance().getDataContext(component));
      }
    }

    String currentConfigurationName = getConfigurationName();
    if (oldSessionData == null || !oldSessionData.getConfigurationName().equals(currentConfigurationName)) {
      oldSessionData = new XDebugSessionData(getWatchExpressions(), currentConfigurationName);
    }
    mySessionData = oldSessionData;
  }

  @Override
  @NotNull
  public String getSessionName() {
    return mySessionName;
  }

  @Override
  @NotNull
  public RunContentDescriptor getRunContentDescriptor() {
    assertSessionTabInitialized();
    //noinspection ConstantConditions
    return mySessionTab.getRunContentDescriptor();
  }

  private void assertSessionTabInitialized() {
    if (myShowTabOnSuspend.get()) {
      LOG.error("Debug tool window isn't shown yet because debug process isn't suspended");
    }
    else {
      LOG.assertTrue(mySessionTab != null, "Debug tool window not initialized yet!");
    }
  }

  @Override
  public void setPauseActionSupported(final boolean isSupported) {
    myPauseActionSupported = isSupported;
  }

  @NotNull
  public List<AnAction> getRestartActions() {
    return myRestartActions;
  }

  public void addRestartActions(AnAction... restartActions) {
    if (restartActions != null) {
      Collections.addAll(myRestartActions, restartActions);
    }
  }

  @NotNull
  public List<AnAction> getExtraActions() {
    return myExtraActions;
  }

  public void addExtraActions(AnAction... extraActions) {
    if (extraActions != null) {
      Collections.addAll(myExtraActions, extraActions);
    }
  }

  public List<AnAction> getExtraStopActions() {
    return myExtraStopActions;
  }

  public void addExtraStopActions(AnAction... extraStopActions) {
    if (extraStopActions != null) {
      Collections.addAll(myExtraStopActions, extraStopActions);
    }
  }

  @Override
  public void rebuildViews() {
    if (!myShowTabOnSuspend.get() && mySessionTab != null) {
      mySessionTab.rebuildViews();
    }
  }

  @Override
  @Nullable
  public RunProfile getRunProfile() {
    return myEnvironment != null ? myEnvironment.getRunProfile() : null;
  }

  public boolean isPauseActionSupported() {
    return myPauseActionSupported;
  }

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

  @Override
  @NotNull
  public XDebugProcess getDebugProcess() {
    return myDebugProcess;
  }

  @Override
  public boolean isSuspended() {
    return myPaused.get() && mySuspendContext != null;
  }

  @Override
  public boolean isPaused() {
    return myPaused.get();
  }

  @Override
  @Nullable
  public XStackFrame getCurrentStackFrame() {
    return myCurrentStackFrame;
  }

  @Override
  public XSuspendContext getSuspendContext() {
    return mySuspendContext;
  }

  @Override
  @Nullable
  public XSourcePosition getCurrentPosition() {
    return myCurrentStackFrame != null ? myCurrentStackFrame.getSourcePosition() : null;
  }

  @Nullable
  @Override
  public XSourcePosition getTopFramePosition() {
    return myTopFramePosition;
  }

  XDebugSessionTab init(@NotNull XDebugProcess process, @Nullable RunContentDescriptor contentToReuse) {
    LOG.assertTrue(myDebugProcess == null);
    myDebugProcess = process;

    if (myDebugProcess.checkCanInitBreakpoints()) {
      initBreakpoints();
    }

    myDebugProcess.getProcessHandler().addProcessListener(new ProcessAdapter() {
      @Override
      public void processTerminated(final ProcessEvent event) {
        stopImpl();
        myDebugProcess.getProcessHandler().removeProcessListener(this);
      }
    });
    //todo[nik] make 'createConsole()' method return ConsoleView
    myConsoleView = (ConsoleView)myDebugProcess.createConsole();
    if (!myShowTabOnSuspend.get()) {
      initSessionTab(contentToReuse);
    }

    return mySessionTab;
  }

  public void reset() {
    breakpointsInitialized = false;
  }

  @Override
  public void initBreakpoints() {
    ApplicationManager.getApplication().assertReadAccessAllowed();
    LOG.assertTrue(!breakpointsInitialized);
    breakpointsInitialized = true;

    XBreakpointManagerImpl breakpointManager = myDebuggerManager.getBreakpointManager();
    XDependentBreakpointManager dependentBreakpointManager = breakpointManager.getDependentBreakpointManager();
    disableSlaveBreakpoints(dependentBreakpointManager);
    processAllBreakpoints(true, false);

    if (myBreakpointListener == null) {
      myBreakpointListener = new MyBreakpointListener();
      breakpointManager.addBreakpointListener(myBreakpointListener);
    }
    if (myDependentBreakpointListener == null) {
      myDependentBreakpointListener = new MyDependentBreakpointListener();
      dependentBreakpointManager.addListener(myDependentBreakpointListener);
    }
  }

  @Override
  public ConsoleView getConsoleView() {
    return myConsoleView;
  }

  @Nullable
  public XDebugSessionTab getSessionTab() {
    return mySessionTab;
  }

  @Override
  public RunnerLayoutUi getUI() {
    assertSessionTabInitialized();
    assert mySessionTab != null;
    return mySessionTab.getUi();
  }

  private void initSessionTab(@Nullable RunContentDescriptor contentToReuse) {
    mySessionTab = XDebugSessionTab.create(this, myIcon, myEnvironment, contentToReuse);
    myDebugProcess.sessionInitialized();
  }

  public XDebugSessionData getSessionData() {
    return mySessionData;
  }

  private void disableSlaveBreakpoints(final XDependentBreakpointManager dependentBreakpointManager) {
    Set<XBreakpoint<?>> slaveBreakpoints = dependentBreakpointManager.getAllSlaveBreakpoints();
    if (slaveBreakpoints.isEmpty()) {
      return;
    }

    Set<XBreakpointType<?, ?>> breakpointTypes = new THashSet<>();
    for (XBreakpointHandler<?> handler : myDebugProcess.getBreakpointHandlers()) {
      breakpointTypes.add(getBreakpointTypeClass(handler));
    }
    for (XBreakpoint<?> slaveBreakpoint : slaveBreakpoints) {
      if (breakpointTypes.contains(slaveBreakpoint.getType())) {
        myInactiveSlaveBreakpoints.add(slaveBreakpoint);
      }
    }
  }

  public void showSessionTab() {
    RunContentDescriptor descriptor = getRunContentDescriptor();
    ExecutionManager.getInstance(getProject()).getContentManager()
      .showRunContent(DefaultDebugExecutor.getDebugExecutorInstance(), descriptor);
  }

  @Nullable
  public XValueMarkers<?, ?> getValueMarkers() {
    if (myValueMarkers == null) {
      XValueMarkerProvider<?, ?> provider = myDebugProcess.createValueMarkerProvider();
      if (provider != null) {
        myValueMarkers = XValueMarkers.createValueMarkers(provider);
      }
    }
    return myValueMarkers;
  }

  @SuppressWarnings("unchecked") //need to compile under 1.8, please do not remove before checking
  private static XBreakpointType getBreakpointTypeClass(final XBreakpointHandler handler) {
    return XDebuggerUtil.getInstance().findBreakpointType(handler.getBreakpointTypeClass());
  }

  private <B extends XBreakpoint<?>> void processBreakpoints(final XBreakpointHandler<B> handler,
                                                             boolean register,
                                                             final boolean temporary) {
    Collection<? extends B> breakpoints = myDebuggerManager.getBreakpointManager().getBreakpoints(handler.getBreakpointTypeClass());
    for (B b : breakpoints) {
      handleBreakpoint(handler, b, register, temporary);
    }
  }

  private <B extends XBreakpoint<?>> void handleBreakpoint(final XBreakpointHandler<B> handler, final B b, final boolean register,
                                                           final boolean temporary) {
    if (register) {
      boolean active = ReadAction.compute(() -> isBreakpointActive(b));
      if (active) {
        synchronized (myRegisteredBreakpoints) {
          myRegisteredBreakpoints.put(b, new CustomizedBreakpointPresentation());
        }
        handler.registerBreakpoint(b);
      }
    }
    else {
      boolean removed;
      synchronized (myRegisteredBreakpoints) {
        removed = myRegisteredBreakpoints.remove(b) != null;
      }
      if (removed) {
        handler.unregisterBreakpoint(b, temporary);
      }
    }
  }

  @Nullable
  public CustomizedBreakpointPresentation getBreakpointPresentation(@NotNull XBreakpoint<?> breakpoint) {
    synchronized (myRegisteredBreakpoints) {
      return myRegisteredBreakpoints.get(breakpoint);
    }
  }

  private void processAllHandlers(final XBreakpoint<?> breakpoint, final boolean register) {
    for (XBreakpointHandler<?> handler : myDebugProcess.getBreakpointHandlers()) {
      processBreakpoint(breakpoint, handler, register);
    }
  }

  private <B extends XBreakpoint<?>> void processBreakpoint(final XBreakpoint<?> breakpoint,
                                                            final XBreakpointHandler<B> handler,
                                                            boolean register) {
    XBreakpointType<?, ?> type = breakpoint.getType();
    if (handler.getBreakpointTypeClass().equals(type.getClass())) {
      //noinspection unchecked
      B b = (B)breakpoint;
      handleBreakpoint(handler, b, register, false);
    }
  }

  public boolean isBreakpointActive(@NotNull XBreakpoint<?> b) {
    ApplicationManager.getApplication().assertReadAccessAllowed();
    return !areBreakpointsMuted() && b.isEnabled() && !isInactiveSlaveBreakpoint(b) && !((XBreakpointBase)b).isDisposed();
  }

  @Override
  public boolean areBreakpointsMuted() {
    return mySessionData.isBreakpointsMuted();
  }

  @Override
  public void addSessionListener(@NotNull XDebugSessionListener listener, @NotNull Disposable parentDisposable) {
    myDispatcher.addListener(listener, parentDisposable);
  }

  @Override
  public void addSessionListener(@NotNull final XDebugSessionListener listener) {
    myDispatcher.addListener(listener);
  }

  @Override
  public void removeSessionListener(@NotNull final XDebugSessionListener listener) {
    myDispatcher.removeListener(listener);
  }

  @Override
  public void setBreakpointMuted(boolean muted) {
    ApplicationManager.getApplication().assertReadAccessAllowed();
    if (areBreakpointsMuted() == muted) return;
    mySessionData.setBreakpointsMuted(muted);
    processAllBreakpoints(!muted, muted);
    myDebuggerManager.getBreakpointManager().getLineBreakpointManager().queueAllBreakpointsUpdate();
  }

  @Override
  public void stepOver(final boolean ignoreBreakpoints) {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    if (ignoreBreakpoints) {
      disableBreakpoints();
    }
    myDebugProcess.startStepOver(doResume());
  }

  @Override
  public void stepInto() {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    myDebugProcess.startStepInto(doResume());
  }

  @Override
  public void stepOut() {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    myDebugProcess.startStepOut(doResume());
  }

  @Override
  public <V extends XSmartStepIntoVariant> void smartStepInto(XSmartStepIntoHandler<V> handler, V variant) {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    final XSuspendContext context = doResume();
    handler.startStepInto(variant, context);
  }

  @Override
  public void forceStepInto() {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    myDebugProcess.startForceStepInto(doResume());
  }

  @Override
  public void runToPosition(@NotNull final XSourcePosition position, final boolean ignoreBreakpoints) {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    if (ignoreBreakpoints) {
      disableBreakpoints();
    }
    myDebugProcess.runToPosition(position, doResume());
  }

  @Override
  public void pause() {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    myDebugProcess.startPausing();
  }

  private void processAllBreakpoints(final boolean register, final boolean temporary) {
    for (XBreakpointHandler<?> handler : myDebugProcess.getBreakpointHandlers()) {
      processBreakpoints(handler, register, temporary);
    }
  }

  private void disableBreakpoints() {
    myBreakpointsDisabled = true;
    processAllBreakpoints(false, true);
  }

  @Override
  public void resume() {
    if (!myDebugProcess.checkCanPerformCommands()) return;

    myDebugProcess.resume(doResume());
  }

  @Nullable
  private XSuspendContext doResume() {
    if (!myPaused.getAndSet(false)) {
      return null;
    }

    myDispatcher.getMulticaster().beforeSessionResume();
    XSuspendContext context = mySuspendContext;
    mySuspendContext = null;
    myCurrentExecutionStack = null;
    myCurrentStackFrame = null;
    myTopFramePosition = null;
    myActiveNonLineBreakpoint = null;
    updateExecutionPosition();
    UIUtil.invokeLaterIfNeeded(() -> {
      if (mySessionTab != null) {
        mySessionTab.getUi().clearAttractionBy(XDebuggerUIConstants.LAYOUT_VIEW_BREAKPOINT_CONDITION);
      }
    });
    myDispatcher.getMulticaster().sessionResumed();
    return context;
  }

  @Override
  public void updateExecutionPosition() {
    // allowed only for the active session
    if (myDebuggerManager.getCurrentSession() == this) {
      boolean isTopFrame = isTopFrameSelected();
      myDebuggerManager.updateExecutionPoint(getCurrentPosition(), !isTopFrame, getPositionIconRenderer(isTopFrame));
    }
  }

  public boolean isTopFrameSelected() {
    return myCurrentExecutionStack != null && myIsTopFrame;
  }


  @Override
  public void showExecutionPoint() {
    if (mySuspendContext != null) {
      XExecutionStack executionStack = mySuspendContext.getActiveExecutionStack();
      if (executionStack != null) {
        XStackFrame topFrame = executionStack.getTopFrame();
        if (topFrame != null) {
          setCurrentStackFrame(executionStack, topFrame, true);
          myDebuggerManager.showExecutionPosition();
        }
      }
    }
  }

  @Override
  public void setCurrentStackFrame(@NotNull XExecutionStack executionStack, @NotNull XStackFrame frame, boolean isTopFrame) {
    if (mySuspendContext == null) return;

    boolean frameChanged = myCurrentStackFrame != frame;
    myCurrentExecutionStack = executionStack;
    myCurrentStackFrame = frame;
    myIsTopFrame = isTopFrame;
    activateSession();

    if (frameChanged) {
      myDispatcher.getMulticaster().stackFrameChanged();
    }
  }

  void activateSession() {
    myDebuggerManager.setCurrentSession(this);
    updateExecutionPosition();
  }

  public XBreakpoint<?> getActiveNonLineBreakpoint() {
    if (myActiveNonLineBreakpoint != null) {
      XSourcePosition breakpointPosition = myActiveNonLineBreakpoint.getSourcePosition();
      XSourcePosition position = getTopFramePosition();
      if (breakpointPosition == null ||
          (position != null && !(breakpointPosition.getFile().equals(position.getFile()) && breakpointPosition.getLine() == position.getLine()))) {
        return myActiveNonLineBreakpoint;
      }
    }
    return null;
  }

  @Nullable
  private GutterIconRenderer getPositionIconRenderer(boolean isTopFrame) {
    if (!isTopFrame) {
      return null;
    }
    XBreakpoint<?> activeNonLineBreakpoint = getActiveNonLineBreakpoint();
    if (activeNonLineBreakpoint != null) {
      return ((XBreakpointBase<?, ?, ?>)activeNonLineBreakpoint).createGutterIconRenderer();
    }
    if (myCurrentExecutionStack != null) {
      return myCurrentExecutionStack.getExecutionLineIconRenderer();
    }
    return null;
  }

  @Override
  public void updateBreakpointPresentation(@NotNull final XLineBreakpoint<?> breakpoint,
                                           @Nullable final Icon icon,
                                           @Nullable final String errorMessage) {
    CustomizedBreakpointPresentation presentation;
    synchronized (myRegisteredBreakpoints) {
      presentation = myRegisteredBreakpoints.get(breakpoint);
      if (presentation == null ||
          (Comparing.equal(presentation.getIcon(), icon) && Comparing.strEqual(presentation.getErrorMessage(), errorMessage))) {
        return;
      }

      presentation.setErrorMessage(errorMessage);
      presentation.setIcon(icon);
    }
    myDebuggerManager.getBreakpointManager().getLineBreakpointManager().queueBreakpointUpdate((XLineBreakpointImpl<?>)breakpoint);
  }

  @Override
  public boolean breakpointReached(@NotNull final XBreakpoint<?> breakpoint, @NotNull final XSuspendContext suspendContext) {
    return breakpointReached(breakpoint, null, suspendContext);
  }

  @Override
  public boolean breakpointReached(@NotNull final XBreakpoint<?> breakpoint, @Nullable String evaluatedLogExpression,
                                   @NotNull XSuspendContext suspendContext) {
    return breakpointReached(breakpoint, evaluatedLogExpression, suspendContext, true);
  }

  public void breakpointReachedNoProcessing(@NotNull final XBreakpoint<?> breakpoint, @NotNull XSuspendContext suspendContext) {
    breakpointReached(breakpoint, null, suspendContext, false);
  }

  private boolean breakpointReached(@NotNull final XBreakpoint<?> breakpoint, @Nullable String evaluatedLogExpression,
                                   @NotNull XSuspendContext suspendContext, boolean doProcessing) {
    if (doProcessing) {
      if (breakpoint.isLogMessage()) {
        XSourcePosition position = breakpoint.getSourcePosition();
        OpenFileHyperlinkInfo hyperlinkInfo =
          position != null ? new OpenFileHyperlinkInfo(myProject, position.getFile(), position.getLine()) : null;
        printMessage(XDebuggerBundle.message("xbreakpoint.reached.text") + " ", XBreakpointUtil.getShortText(breakpoint), hyperlinkInfo);
      }

      if (evaluatedLogExpression != null) {
        printMessage(evaluatedLogExpression, null, null);
      }

      processDependencies(breakpoint);

      if (breakpoint.getSuspendPolicy() == SuspendPolicy.NONE) {
        return false;
      }
    }

    myActiveNonLineBreakpoint =
      (!(breakpoint instanceof XLineBreakpoint) || ((XLineBreakpoint)breakpoint).getType().canBeHitInOtherPlaces()) ? breakpoint : null;

    // set this session active on breakpoint, update execution position will be called inside positionReached
    myDebuggerManager.setCurrentSession(this);

    positionReachedInternal(suspendContext, true);

    if (doProcessing && breakpoint instanceof XLineBreakpoint<?> && ((XLineBreakpoint)breakpoint).isTemporary()) {
      handleTemporaryBreakpointHit(breakpoint);
    }
    return true;
  }

  private void handleTemporaryBreakpointHit(final XBreakpoint<?> breakpoint) {
    addSessionListener(new XDebugSessionListener() {
      private void removeBreakpoint() {
        XDebuggerUtil.getInstance().removeBreakpoint(myProject, breakpoint);
        removeSessionListener(this);
      }

      @Override
      public void sessionResumed() {
        removeBreakpoint();
      }

      @Override
      public void sessionStopped() {
        removeBreakpoint();
      }
    });
  }

  public void processDependencies(final XBreakpoint<?> breakpoint) {
    XDependentBreakpointManager dependentBreakpointManager = myDebuggerManager.getBreakpointManager().getDependentBreakpointManager();
    if (!dependentBreakpointManager.isMasterOrSlave(breakpoint)) return;

    List<XBreakpoint<?>> breakpoints = dependentBreakpointManager.getSlaveBreakpoints(breakpoint);
    myInactiveSlaveBreakpoints.removeAll(breakpoints);
    for (XBreakpoint<?> slaveBreakpoint : breakpoints) {
      processAllHandlers(slaveBreakpoint, true);
    }

    if (dependentBreakpointManager.getMasterBreakpoint(breakpoint) != null && !dependentBreakpointManager.isLeaveEnabled(breakpoint)) {
      boolean added = myInactiveSlaveBreakpoints.add(breakpoint);
      if (added) {
        processAllHandlers(breakpoint, false);
        myDebuggerManager.getBreakpointManager().getLineBreakpointManager().queueBreakpointUpdate(breakpoint);
      }
    }
  }

  private void printMessage(final String message, final String hyperLinkText, @Nullable final HyperlinkInfo info) {
    AppUIUtil.invokeOnEdt(() -> {
      myConsoleView.print(message, ConsoleViewContentType.SYSTEM_OUTPUT);
      if (info != null) {
        myConsoleView.printHyperlink(hyperLinkText, info);
      }
      else if (hyperLinkText != null) {
        myConsoleView.print(hyperLinkText, ConsoleViewContentType.SYSTEM_OUTPUT);
      }
      myConsoleView.print("\n", ConsoleViewContentType.SYSTEM_OUTPUT);
    });
  }

  public void unsetPaused() {
    myPaused.set(false);
  }

  private void positionReachedInternal(@NotNull final XSuspendContext suspendContext, boolean attract) {
    enableBreakpoints();
    mySuspendContext = suspendContext;
    myCurrentExecutionStack = suspendContext.getActiveExecutionStack();
    myCurrentStackFrame = myCurrentExecutionStack != null ? myCurrentExecutionStack.getTopFrame() : null;
    myIsTopFrame = true;
    myTopFramePosition = myCurrentStackFrame != null ? myCurrentStackFrame.getSourcePosition() : null;

    myPaused.set(true);

    updateExecutionPosition();

    final boolean showOnSuspend = myShowTabOnSuspend.compareAndSet(true, false);
    if (showOnSuspend || attract) {
      AppUIUtil.invokeLaterIfProjectAlive(myProject, () -> {
        if (showOnSuspend) {
          initSessionTab(null);
          showSessionTab();
        }

        // user attractions should only be made if event happens independently (e.g. program paused/suspended)
        // and should not be made when user steps in the code
        if (attract) {
          if (mySessionTab == null) {
            LOG.debug("Cannot request focus because Session Tab is not initialized yet");
            return;
          }

          if (XDebuggerSettingManagerImpl.getInstanceImpl().getGeneralSettings().isShowDebuggerOnBreakpoint()) {
            mySessionTab.toFront(true, this::updateExecutionPosition);
          }

          if (myTopFramePosition == null) {
            // if there is no source position available, we should somehow tell the user that session is stopped.
            // the best way is to show the stack frames.
            XDebugSessionTab.showFramesView(this);
          }

          mySessionTab.getUi().attractBy(XDebuggerUIConstants.LAYOUT_VIEW_BREAKPOINT_CONDITION);
        }
      });
    }

    myDispatcher.getMulticaster().sessionPaused();
  }

  @Override
  public void positionReached(@NotNull final XSuspendContext suspendContext) {
    positionReached(suspendContext, false);
  }

  public void positionReached(@NotNull XSuspendContext suspendContext, boolean attract) {
    myActiveNonLineBreakpoint = null;
    positionReachedInternal(suspendContext, attract);
  }

  @Override
  public void sessionResumed() {
    doResume();
  }

  private void enableBreakpoints() {
    if (myBreakpointsDisabled) {
      myBreakpointsDisabled = false;
      new ReadAction() {
        @Override
        protected void run(@NotNull Result result) {
          processAllBreakpoints(true, false);
        }
      }.execute();
    }
  }

  @Override
  public boolean isStopped() {
    return myStopped.get();
  }

  private void stopImpl() {
    if (!myStopped.compareAndSet(false, true)) {
      return;
    }

    try {
      if (breakpointsInitialized) {
        XBreakpointManagerImpl breakpointManager = myDebuggerManager.getBreakpointManager();
        if (myBreakpointListener != null) {
          breakpointManager.removeBreakpointListener(myBreakpointListener);
        }
        if (myDependentBreakpointListener != null) {
          breakpointManager.getDependentBreakpointManager().removeListener(myDependentBreakpointListener);
        }
      }
    }
    finally {
      //noinspection unchecked
      myDebugProcess.stopAsync().done(aVoid -> {
        if (!myProject.isDisposed()) {
          myProject.getMessageBus().syncPublisher(XDebuggerManager.TOPIC).processStopped(myDebugProcess);
        }

        if (mySessionTab != null) {
          AppUIUtil.invokeOnEdt(() -> {
            ((XWatchesViewImpl)mySessionTab.getWatchesView()).updateSessionData();
            mySessionTab.detachFromSession();
          });
        }
        else if (myConsoleView != null) {
          AppUIUtil.invokeOnEdt(() -> Disposer.dispose(myConsoleView));
        }

        myTopFramePosition = null;
        myCurrentExecutionStack = null;
        myCurrentStackFrame = null;
        mySuspendContext = null;

        updateExecutionPosition();

        if (myValueMarkers != null) {
          myValueMarkers.clear();
        }
        if (XDebuggerSettingManagerImpl.getInstanceImpl().getGeneralSettings().isUnmuteOnStop()) {
          mySessionData.setBreakpointsMuted(false);
        }
        myDebuggerManager.removeSession(this);
        myDispatcher.getMulticaster().sessionStopped();
        myDispatcher.getListeners().clear();

        myProject.putUserData(XDebuggerEditorLinePainter.CACHE, null);

        synchronized (myRegisteredBreakpoints) {
          myRegisteredBreakpoints.clear();
        }
      });
    }
  }

  public boolean isInactiveSlaveBreakpoint(final XBreakpoint<?> breakpoint) {
    return myInactiveSlaveBreakpoints.contains(breakpoint);
  }

  @Override
  public void stop() {
    ProcessHandler processHandler = myDebugProcess.getProcessHandler();
    if (processHandler.isProcessTerminated() || processHandler.isProcessTerminating()) return;

    if (processHandler.detachIsDefault()) {
      processHandler.detachProcess();
    }
    else {
      processHandler.destroyProcess();
    }
  }

  @Override
  public void reportError(@NotNull final String message) {
    reportMessage(message, MessageType.ERROR);
  }

  @Override
  public void reportMessage(@NotNull final String message, @NotNull final MessageType type) {
    reportMessage(message, type, null);
  }

  @Override
  public void reportMessage(@NotNull final String message, @NotNull final MessageType type, @Nullable final HyperlinkListener listener) {
    NotificationListener notificationListener = listener == null ? null : (notification, event) -> {
      if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
        listener.hyperlinkUpdate(event);
      }
    };
    NOTIFICATION_GROUP.createNotification("", message, type.toNotificationType(), notificationListener).notify(myProject);
  }

  private class MyBreakpointListener implements XBreakpointListener<XBreakpoint<?>> {
    @Override
    public void breakpointAdded(@NotNull final XBreakpoint<?> breakpoint) {
      if (!myBreakpointsDisabled) {
        processAllHandlers(breakpoint, true);
      }
    }

    @Override
    public void breakpointRemoved(@NotNull final XBreakpoint<?> breakpoint) {
      if (getActiveNonLineBreakpoint() == breakpoint) {
        myActiveNonLineBreakpoint = null;
      }
      processAllHandlers(breakpoint, false);
    }

    @Override
    public void breakpointChanged(@NotNull final XBreakpoint<?> breakpoint) {
      breakpointRemoved(breakpoint);
      breakpointAdded(breakpoint);
    }
  }

  private class MyDependentBreakpointListener implements XDependentBreakpointListener {
    @Override
    public void dependencySet(final XBreakpoint<?> slave, final XBreakpoint<?> master) {
      boolean added = myInactiveSlaveBreakpoints.add(slave);
      if (added) {
        processAllHandlers(slave, false);
      }
    }

    @Override
    public void dependencyCleared(final XBreakpoint<?> breakpoint) {
      boolean removed = myInactiveSlaveBreakpoints.remove(breakpoint);
      if (removed) {
        processAllHandlers(breakpoint, true);
      }
    }
  }

  @NotNull
  private String getConfigurationName() {
    if (myEnvironment != null) {
      RunProfile profile = myEnvironment.getRunProfile();
      if (profile instanceof RunConfiguration) {
        return ((RunConfiguration)profile).getType().getId();
      }
    }
    return getSessionName();
  }

  public void setWatchExpressions(@NotNull XExpression[] watchExpressions) {
    mySessionData.setWatchExpressions(watchExpressions);
    myDebuggerManager.getWatchesManager().setWatches(getConfigurationName(), watchExpressions);
  }

  XExpression[] getWatchExpressions() {
    return myDebuggerManager.getWatchesManager().getWatches(getConfigurationName());
  }

  @Nullable
  public ExecutionEnvironment getExecutionEnvironment() {
    return myEnvironment;
  }
}
