/*
 * Copyright 2000-2014 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.openapi.wm.impl;

import com.intellij.ide.FrameStateManager;
import com.intellij.ide.IdeEventQueue;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectManagerListener;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.*;
import com.intellij.openapi.wm.ex.ToolWindowEx;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.openapi.wm.impl.commands.DesktopRequestFocusInToolWindowCmd;
import com.intellij.openapi.wm.impl.commands.FinalizableCommand;
import com.intellij.openapi.wm.impl.commands.UpdateRootPaneCmd;
import com.intellij.ui.BalloonImpl;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Alarm;
import com.intellij.util.IJSwingUtilities;
import com.intellij.util.ObjectUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.HashMap;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.PositionTracker;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.UiNotifyConnector;
import consulo.annotation.access.RequiredWriteAction;
import consulo.awt.TargetAWT;
import consulo.desktop.util.awt.migration.AWTComponentProviderUtil;
import consulo.disposer.Disposer;
import consulo.fileEditor.impl.EditorWindow;
import consulo.fileEditor.impl.EditorWithProviderComposite;
import consulo.fileEditor.impl.EditorsSplitters;
import consulo.localize.LocalizeValue;
import consulo.logging.Logger;
import consulo.ui.UIAccess;
import consulo.ui.annotation.RequiredUIAccess;
import consulo.ui.ex.ToolWindowInternalDecorator;
import consulo.ui.ex.ToolWindowStripeButton;
import consulo.ui.image.Image;
import consulo.ui.shared.Rectangle2D;
import consulo.wm.impl.DesktopCommandProcessorImpl;
import consulo.wm.impl.ToolWindowManagerBase;
import gnu.trove.THashSet;
import org.intellij.lang.annotations.JdkConstants;
import org.jdom.Element;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.*;

/**
 * @author Anton Katilin
 * @author Vladimir Kondratyev
 */
@State(name = ToolWindowManagerBase.ID, storages = @Storage(value = StoragePathMacros.WORKSPACE_FILE, roamingType = RoamingType.DISABLED))
@Singleton
public final class DesktopToolWindowManagerImpl extends ToolWindowManagerBase {

  /**
   * Translates events from InternalDecorator into ToolWindowManager method invocations.
   */
  private final class MyInternalDecoratorListener extends MyInternalDecoratorListenerBase {
    /**
     * Handles event from decorator and modify weight/floating bounds of the
     * tool window depending on decoration type.
     */
    @Override
    public void resized(@Nonnull final ToolWindowInternalDecorator s) {
      DesktopInternalDecorator source = (DesktopInternalDecorator)s;

      if (!source.isShowing()) {
        return; // do not recalculate the tool window size if it is not yet shown (and, therefore, has 0,0,0,0 bounds)
      }

      final WindowInfoImpl info = getInfo(source.getToolWindow().getId());
      if (info.isFloating()) {
        final Window owner = SwingUtilities.getWindowAncestor(source);
        if (owner != null) {
          info.setFloatingBounds(TargetAWT.from(owner.getBounds()));
        }
      }
      else if (info.isWindowed()) {
        DesktopWindowedDecorator decorator = getWindowedDecorator(info.getId());
        Window frame = decorator != null ? decorator.getFrame() : null;
        if (frame == null || !frame.isShowing()) return;
        info.setFloatingBounds(getRootBounds((JFrame)frame));
      }
      else { // docked and sliding windows
        ToolWindowAnchor anchor = info.getAnchor();
        DesktopInternalDecorator another = null;
        if (source.getParent() instanceof Splitter) {
          float sizeInSplit = anchor.isSplitVertically() ? source.getHeight() : source.getWidth();
          Splitter splitter = (Splitter)source.getParent();
          if (splitter.getSecondComponent() == source) {
            sizeInSplit += splitter.getDividerWidth();
            another = (DesktopInternalDecorator)splitter.getFirstComponent();
          }
          else {
            another = (DesktopInternalDecorator)splitter.getSecondComponent();
          }
          if (anchor.isSplitVertically()) {
            info.setSideWeight(sizeInSplit / (float)splitter.getHeight());
          }
          else {
            info.setSideWeight(sizeInSplit / (float)splitter.getWidth());
          }
        }

        float paneWeight = anchor.isHorizontal()
                           ? (float)source.getHeight() / (float)getToolWindowPanel().getMyLayeredPane().getHeight()
                           : (float)source.getWidth() / (float)getToolWindowPanel().getMyLayeredPane().getWidth();
        info.setWeight(paneWeight);
        if (another != null && anchor.isSplitVertically()) {
          paneWeight = anchor.isHorizontal()
                       ? (float)another.getHeight() / (float)getToolWindowPanel().getMyLayeredPane().getHeight()
                       : (float)another.getWidth() / (float)getToolWindowPanel().getMyLayeredPane().getWidth();
          another.getWindowInfo().setWeight(paneWeight);
        }
      }
    }
  }

  private static final Logger LOG = Logger.getInstance(DesktopToolWindowManagerImpl.class);

  private final Map<String, FocusWatcher> myId2FocusWatcher = new HashMap<>();

  private DesktopIdeFrameImpl myFrame;

  private final Map<String, Balloon> myWindow2Balloon = new HashMap<>();

  private KeyState myCurrentState = KeyState.waiting;
  private final Alarm myWaiterForSecondPress = new Alarm();
  private final Runnable mySecondPressRunnable = () -> {
    if (myCurrentState != KeyState.hold) {
      resetHoldState();
    }
  };

  private final Alarm myUpdateHeadersAlarm = new Alarm();

  private enum KeyState {
    waiting,
    pressed,
    released,
    hold
  }

  @Inject
  public DesktopToolWindowManagerImpl(Project project, Provider<WindowManager> windowManager) {
    super(project, windowManager);

    if (project.isDefault()) {
      return;
    }

    MessageBusConnection busConnection = project.getMessageBus().connect();
    busConnection.subscribe(AnActionListener.TOPIC, new AnActionListener() {
      @Override
      public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
        if (myCurrentState != KeyState.hold) {
          resetHoldState();
        }
      }
    });
    busConnection.subscribe(ProjectManager.TOPIC, new ProjectManagerListener() {
      @Override
      public void projectOpened(@Nonnull Project project, @Nonnull UIAccess uiAccess) {
        if (project == myProject) {
          uiAccess.giveAndWaitIfNeed(DesktopToolWindowManagerImpl.this::projectOpened);
        }
      }

      @Override
      public void projectClosed(@Nonnull Project project, @Nonnull UIAccess uiAccess) {
        if (project == myProject) {
          uiAccess.giveAndWaitIfNeed(DesktopToolWindowManagerImpl.this::projectClosed);
        }
      }
    });

    myLayout.copyFrom(((WindowManagerEx)windowManager.get()).getLayout());

    busConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() {
      @Override
      public void fileClosed(@Nonnull FileEditorManager source, @Nonnull VirtualFile file) {
        getFocusManagerImpl(myProject).doWhenFocusSettlesDown(new ExpirableRunnable.ForProject(myProject) {
          @Override
          public void run() {
            if (!hasOpenEditorFiles()) {
              focusToolWindowByDefault(null);
            }
          }
        });
      }
    });

    PropertyChangeListener focusListener = it -> {
      if ("focusOwner".equals(it.getPropertyName())) {
        myUpdateHeadersAlarm.cancelAllRequests();
        myUpdateHeadersAlarm.addRequest(this::updateToolWindowHeaders, 50);
      }
    };
    KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener(focusListener);
    Disposer.register(this, () -> KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener(focusListener));
  }

  @Nonnull
  @Override
  protected CommandProcessorBase createCommandProcessor() {
    return new DesktopCommandProcessorImpl();
  }

  @Nonnull
  @Override
  protected InternalDecoratorListener createInternalDecoratorListener() {
    return new MyInternalDecoratorListener();
  }

  @Nonnull
  @Override
  protected ToolWindowStripeButton createStripeButton(ToolWindowInternalDecorator internalDecorator) {
    return new DesktopStripeButton((DesktopInternalDecorator)internalDecorator, (DesktopToolWindowPanelImpl)myToolWindowPanel);
  }

  @Nonnull
  @Override
  protected ToolWindowEx createToolWindow(String id, LocalizeValue displayName, boolean canCloseContent, @Nullable Object component, boolean shouldBeAvailable) {
    return new DesktopToolWindowImpl(this, id, displayName, canCloseContent, (JComponent)component, shouldBeAvailable);
  }

  @Nonnull
  @Override
  protected ToolWindowInternalDecorator createInternalDecorator(Project project, @Nonnull WindowInfoImpl info, ToolWindowEx toolWindow, boolean dumbAware) {
    return new DesktopInternalDecorator(project, info, (DesktopToolWindowImpl)toolWindow, dumbAware);
  }

  private void updateToolWindowHeaders() {
    getFocusManager().doWhenFocusSettlesDown(new ExpirableRunnable.ForProject(myProject) {
      @Override
      public void run() {
        WindowInfoImpl[] infos = myLayout.getInfos();
        for (WindowInfoImpl each : infos) {
          if (each.isVisible()) {
            ToolWindow tw = getToolWindow(each.getId());
            if (tw instanceof DesktopToolWindowImpl) {
              DesktopInternalDecorator decorator = (DesktopInternalDecorator)((DesktopToolWindowImpl)tw).getDecorator();
              if (decorator != null) {
                decorator.repaint();
              }
            }
          }
        }
      }
    });
  }

  public boolean dispatchKeyEvent(KeyEvent e) {
    if (e.getKeyCode() != KeyEvent.VK_CONTROL && e.getKeyCode() != KeyEvent.VK_ALT && e.getKeyCode() != KeyEvent.VK_SHIFT && e.getKeyCode() != KeyEvent.VK_META) {
      if (e.getModifiers() == 0) {
        resetHoldState();
      }
      return false;
    }
    if (e.getID() != KeyEvent.KEY_PRESSED && e.getID() != KeyEvent.KEY_RELEASED) return false;

    Component parent = UIUtil.findUltimateParent(e.getComponent());
    if(parent instanceof Window) {
      consulo.ui.Window uiWindow = TargetAWT.from((Window)parent);

      IdeFrame ideFrame = uiWindow.getUserData(IdeFrame.KEY);
      if (ideFrame != null && ideFrame.getProject() != myProject) {
        resetHoldState();
        return false;
      }
    }

    Set<Integer> vks = getActivateToolWindowVKs();

    if (vks.isEmpty()) {
      resetHoldState();
      return false;
    }

    if (vks.contains(e.getKeyCode())) {
      boolean pressed = e.getID() == KeyEvent.KEY_PRESSED;
      int modifiers = e.getModifiers();

      int mouseMask = InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK;
      if ((e.getModifiersEx() & mouseMask) == 0) {
        if (areAllModifiersPressed(modifiers, vks) || !pressed) {
          processState(pressed);
        }
        else {
          resetHoldState();
        }
      }
    }


    return false;
  }

  private static boolean areAllModifiersPressed(@JdkConstants.InputEventMask int modifiers, Set<Integer> modifierCodes) {
    int mask = 0;
    for (Integer each : modifierCodes) {
      if (each == KeyEvent.VK_SHIFT) {
        mask |= InputEvent.SHIFT_MASK;
      }

      if (each == KeyEvent.VK_CONTROL) {
        mask |= InputEvent.CTRL_MASK;
      }

      if (each == KeyEvent.VK_META) {
        mask |= InputEvent.META_MASK;
      }

      if (each == KeyEvent.VK_ALT) {
        mask |= InputEvent.ALT_MASK;
      }
    }

    return (modifiers ^ mask) == 0;
  }

  public static Set<Integer> getActivateToolWindowVKs() {
    if (ApplicationManager.getApplication() == null) return new HashSet<>();

    Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
    Shortcut[] baseShortcut = keymap.getShortcuts("ActivateProjectToolWindow");
    int baseModifiers = 0;
    for (Shortcut each : baseShortcut) {
      if (each instanceof KeyboardShortcut) {
        KeyStroke keyStroke = ((KeyboardShortcut)each).getFirstKeyStroke();
        baseModifiers = keyStroke.getModifiers();
        if (baseModifiers > 0) {
          break;
        }
      }
    }
    return getModifiersVKs(baseModifiers);
  }

  @Nonnull
  private static Set<Integer> getModifiersVKs(int mask) {
    Set<Integer> codes = new THashSet<>();
    if ((mask & InputEvent.SHIFT_MASK) > 0) {
      codes.add(KeyEvent.VK_SHIFT);
    }
    if ((mask & InputEvent.CTRL_MASK) > 0) {
      codes.add(KeyEvent.VK_CONTROL);
    }

    if ((mask & InputEvent.META_MASK) > 0) {
      codes.add(KeyEvent.VK_META);
    }

    if ((mask & InputEvent.ALT_MASK) > 0) {
      codes.add(KeyEvent.VK_ALT);
    }

    return codes;
  }

  private void resetHoldState() {
    myCurrentState = KeyState.waiting;
    processHoldState();
  }

  private void processState(boolean pressed) {
    if (pressed) {
      if (myCurrentState == KeyState.waiting) {
        myCurrentState = KeyState.pressed;
      }
      else if (myCurrentState == KeyState.released) {
        myCurrentState = KeyState.hold;
        processHoldState();
      }
    }
    else {
      if (myCurrentState == KeyState.pressed) {
        myCurrentState = KeyState.released;
        restartWaitingForSecondPressAlarm();
      }
      else {
        resetHoldState();
      }
    }
  }

  private void processHoldState() {
    if (myToolWindowPanel != null) {
      getToolWindowPanel().setStripesOverlayed(myCurrentState == KeyState.hold);
    }
  }

  private void restartWaitingForSecondPressAlarm() {
    myWaiterForSecondPress.cancelAllRequests();
    myWaiterForSecondPress.addRequest(mySecondPressRunnable, SystemProperties.getIntProperty("actionSystem.keyGestureDblClickTime", 650));
  }

  @Nonnull
  public DesktopToolWindowPanelImpl getToolWindowPanel() {
    return (DesktopToolWindowPanelImpl)myToolWindowPanel;
  }

  private static IdeFocusManager getFocusManagerImpl(Project project) {
    return IdeFocusManager.getInstance(project);
  }

  public void projectOpened() {
    final MyUIManagerPropertyChangeListener uiManagerPropertyListener = new MyUIManagerPropertyChangeListener();
    final MyLafManagerListener lafManagerListener = new MyLafManagerListener();

    UIManager.addPropertyChangeListener(uiManagerPropertyListener);
    LafManager.getInstance().addLafManagerListener(lafManagerListener, this);

    Disposer.register(this, () -> {
      UIManager.removePropertyChangeListener(uiManagerPropertyListener);
    });

    WindowManagerEx windowManager = (WindowManagerEx)myWindowManager.get();

    myFrame = (DesktopIdeFrameImpl)windowManager.allocateFrame(myProject);

    myToolWindowPanel = new DesktopToolWindowPanelImpl(myFrame, this);
    Disposer.register(myProject, getToolWindowPanel());
    JFrame jFrame = (JFrame)TargetAWT.to(myFrame.getWindow());
    ((IdeRootPane)jFrame.getRootPane()).setToolWindowsPane(myToolWindowPanel);
    jFrame.setTitle(FrameTitleBuilder.getInstance().getProjectTitle(myProject));
    ((IdeRootPane)jFrame.getRootPane()).updateToolbar();

    IdeEventQueue.getInstance().addDispatcher(e -> {
      if (e instanceof KeyEvent) {
        dispatchKeyEvent((KeyEvent)e);
      }
      if (e instanceof WindowEvent && e.getID() == WindowEvent.WINDOW_LOST_FOCUS && e.getSource() == myFrame) {
        resetHoldState();
      }
      return false;
    }, myProject);
  }

  @Override
  protected void initAll(List<FinalizableCommand> commandsList) {
    appendUpdateToolWindowsPaneCmd(commandsList);

    JComponent editorComponent = getEditorComponent(myProject);
    editorComponent.setFocusable(false);

    appendSetEditorComponentCmd(editorComponent, commandsList);
  }

  @Override
  protected void installFocusWatcher(String id, ToolWindow toolWindow) {
    myId2FocusWatcher.put(id, new ToolWindowFocusWatcher((DesktopToolWindowImpl)toolWindow));
  }

  @Override
  protected void uninstallFocusWatcher(String id) {
    ToolWindowFocusWatcher watcher = (ToolWindowFocusWatcher)myId2FocusWatcher.remove(id);
    watcher.deinstall();
  }

  private JComponent getEditorComponent(Project project) {
    return FileEditorManagerEx.getInstanceEx(project).getComponent();
  }

  @Nonnull
  @Override
  protected JLabel createInitializingLabel() {
    JLabel label = new JLabel("Initializing...", SwingConstants.CENTER);
    label.setOpaque(true);
    final Color treeBg = UIManager.getColor("Tree.background");
    label.setBackground(ColorUtil.toAlpha(treeBg, 180));
    final Color treeFg = UIUtil.getTreeForeground();
    label.setForeground(ColorUtil.toAlpha(treeFg, 180));
    return label;
  }

  @RequiredUIAccess
  @Override
  protected void doWhenFirstShown(Object component, Runnable runnable) {
    UiNotifyConnector.doWhenFirstShown((JComponent)component, () -> ApplicationManager.getApplication().invokeLater(runnable));
  }

  public void projectClosed() {
    if (myFrame == null) {
      return;
    }
    final String[] ids = getToolWindowIds();

    WindowManagerEx windowManager = (WindowManagerEx)myWindowManager.get();

    // Remove ToolWindowsPane
    JFrame window = (JFrame)TargetAWT.to(myFrame.getWindow());
    ((IdeRootPane)window.getRootPane()).setToolWindowsPane(null);
    windowManager.releaseFrame(myFrame);
    List<FinalizableCommand> commandsList = new ArrayList<>();
    appendUpdateToolWindowsPaneCmd(commandsList);

    // Hide all tool windows

    for (final String id : ids) {
      deactivateToolWindowImpl(id, true, commandsList);
    }

    // Remove editor component

    final JComponent editorComponent = getEditorComponent(myProject);
    appendSetEditorComponentCmd(null, commandsList);
    execute(commandsList);
    myFrame = null;
  }

  @Override
  public void activateEditorComponent() {
    focusDefaultElementInSelectedEditor();
  }

  private void focusDefaultElementInSelectedEditor() {
    EditorsSplitters splittersToFocus = getSplittersToFocus();
    if (splittersToFocus != null) {
      final EditorWindow window = splittersToFocus.getCurrentWindow();
      if (window != null) {
        final EditorWithProviderComposite editor = window.getSelectedEditor();
        if (editor != null) {
          JComponent defaultFocusedComponentInEditor = editor.getPreferredFocusedComponent();
          if (defaultFocusedComponentInEditor != null) {
            defaultFocusedComponentInEditor.requestFocus();
          }
        }
      }
    }
  }

  /**
   * @return floating decorator for the tool window with specified <code>ID</code>.
   */
  @Override
  protected DesktopFloatingDecorator getFloatingDecorator(final String id) {
    return (DesktopFloatingDecorator)super.getFloatingDecorator(id);
  }

  /**
   * @return windowed decorator for the tool window with specified <code>ID</code>.
   */
  @Override
  protected DesktopWindowedDecorator getWindowedDecorator(String id) {
    return (DesktopWindowedDecorator)super.myId2WindowedDecorator.get(id);
  }

  /**
   * @return internal decorator for the tool window with specified <code>ID</code>.
   */
  @Override
  @Nullable
  protected DesktopInternalDecorator getInternalDecorator(final String id) {
    return (DesktopInternalDecorator)super.getInternalDecorator(id);
  }

  /**
   * @return tool button for the window with specified <code>ID</code>.
   */
  @Override
  @Nullable
  protected DesktopStripeButton getStripeButton(final String id) {
    return (DesktopStripeButton)super.getStripeButton(id);
  }

  @Override
  public boolean canShowNotification(@Nonnull final String toolWindowId) {
    if (!Arrays.asList(getToolWindowIds()).contains(toolWindowId)) {
      return false;
    }
    final DesktopStripePanelImpl stripe = getToolWindowPanel().getStripeFor(toolWindowId);
    return stripe != null && stripe.getButtonFor(toolWindowId) != null;
  }

  @Override
  public void notifyByBalloon(@Nonnull final String toolWindowId, @Nonnull final MessageType type, @Nonnull final String htmlBody) {
    notifyByBalloon(toolWindowId, type, htmlBody, null, null);
  }

  @Override
  public void notifyByBalloon(@Nonnull final String toolWindowId, @Nonnull final MessageType type, @Nonnull final String text, @Nullable final Image icon, @Nullable final HyperlinkListener listener) {
    checkId(toolWindowId);


    Balloon existing = myWindow2Balloon.get(toolWindowId);
    if (existing != null) {
      existing.hide();
    }

    final DesktopStripePanelImpl stripe = getToolWindowPanel().getStripeFor(toolWindowId);
    if (stripe == null) {
      return;
    }
    final DesktopToolWindowImpl window = getInternalDecorator(toolWindowId).getToolWindow();
    if (!window.isAvailable()) {
      window.setPlaceholderMode(true);
      stripe.updatePresentation();
      stripe.revalidate();
      stripe.repaint();
    }

    final ToolWindowAnchor anchor = getInfo(toolWindowId).getAnchor();
    final Ref<Balloon.Position> position = Ref.create(Balloon.Position.below);
    if (ToolWindowAnchor.TOP == anchor) {
      position.set(Balloon.Position.below);
    }
    else if (ToolWindowAnchor.BOTTOM == anchor) {
      position.set(Balloon.Position.above);
    }
    else if (ToolWindowAnchor.LEFT == anchor) {
      position.set(Balloon.Position.atRight);
    }
    else if (ToolWindowAnchor.RIGHT == anchor) {
      position.set(Balloon.Position.atLeft);
    }

    final BalloonHyperlinkListener listenerWrapper = new BalloonHyperlinkListener(listener);
    final Balloon balloon = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text.replace("\n", "<br>"), TargetAWT.to(icon), type.getPopupBackground(), listenerWrapper).setHideOnClickOutside(false)
            .setHideOnFrameResize(false).createBalloon();
    FrameStateManager.getInstance().getApplicationActive().doWhenDone(() -> {
      final Alarm alarm = new Alarm();
      alarm.addRequest(() -> {
        ((BalloonImpl)balloon).setHideOnClickOutside(true);
        Disposer.dispose(alarm);
      }, 100);
    });
    listenerWrapper.myBalloon = balloon;
    myWindow2Balloon.put(toolWindowId, balloon);
    Disposer.register(balloon, () -> {
      window.setPlaceholderMode(false);
      stripe.updatePresentation();
      stripe.revalidate();
      stripe.repaint();
      myWindow2Balloon.remove(toolWindowId);
    });
    Disposer.register(getProject(), balloon);

    execute(new ArrayList<>(Arrays.<FinalizableCommand>asList(new FinalizableCommand(null) {
      @Override
      public void run() {
        final DesktopStripeButton button = stripe.getButtonFor(toolWindowId);
        LOG.assertTrue(button != null, "Button was not found, popup won't be shown. Toolwindow id: " + toolWindowId + ", message: " + text + ", message type: " + type);
        if (button == null) return;

        final Runnable show = () -> {
          if (button.isShowing()) {
            PositionTracker<Balloon> tracker = new PositionTracker<Balloon>(button) {
              @Override
              @Nullable
              public RelativePoint recalculateLocation(Balloon object) {
                DesktopStripePanelImpl twStripe = getToolWindowPanel().getStripeFor(toolWindowId);
                DesktopStripeButton twButton = twStripe != null ? twStripe.getButtonFor(toolWindowId) : null;

                if (twButton == null) return null;

                if (getToolWindow(toolWindowId).getAnchor() != anchor) {
                  object.hide();
                  return null;
                }

                final Point point = new Point(twButton.getBounds().width / 2, twButton.getHeight() / 2 - 2);
                return new RelativePoint(twButton, point);
              }
            };
            if (!balloon.isDisposed()) {
              balloon.show(tracker, position.get());
            }
          }
          else {
            final Rectangle bounds = getToolWindowPanel().getBounds();
            final Point target = UIUtil.getCenterPoint(bounds, new Dimension(1, 1));
            if (ToolWindowAnchor.TOP == anchor) {
              target.y = 0;
            }
            else if (ToolWindowAnchor.BOTTOM == anchor) {
              target.y = bounds.height - 3;
            }
            else if (ToolWindowAnchor.LEFT == anchor) {
              target.x = 0;
            }
            else if (ToolWindowAnchor.RIGHT == anchor) {
              target.x = bounds.width;
            }
            if (!balloon.isDisposed()) {
              balloon.show(new RelativePoint(getToolWindowPanel(), target), position.get());
            }
          }
        };

        if (!button.isValid()) {
          SwingUtilities.invokeLater(() -> show.run());
        }
        else {
          show.run();
        }
      }
    })));
  }

  @Override
  public Balloon getToolWindowBalloon(String id) {
    return myWindow2Balloon.get(id);
  }

  @Override
  public boolean isEditorComponentActive() {
    UIAccess.assertIsUIThread();

    Component owner = getFocusManager().getFocusOwner();
    EditorsSplitters splitters = AWTComponentProviderUtil.findParent(owner, EditorsSplitters.class);
    return splitters != null;
  }

  @Override
  protected void appendRemoveWindowedDecoratorCmd(final WindowInfoImpl info, final List<FinalizableCommand> commandsList) {
    final RemoveWindowedDecoratorCmd command = new RemoveWindowedDecoratorCmd(info);
    commandsList.add(command);
  }

  @Override
  protected void appendAddFloatingDecorator(ToolWindowInternalDecorator decorator, List<FinalizableCommand> commandList, WindowInfoImpl toBeShownInfo) {
    commandList.add(new AddFloatingDecoratorCmd((DesktopInternalDecorator)decorator, toBeShownInfo));
  }

  @Override
  protected void appendAddWindowedDecorator(ToolWindowInternalDecorator decorator, List<FinalizableCommand> commandList, WindowInfoImpl toBeShownInfo) {
    commandList.add(new AddWindowedDecoratorCmd((DesktopInternalDecorator)decorator, toBeShownInfo));
  }

  @Override
  public void appendRequestFocusInToolWindowCmd(final String id, List<FinalizableCommand> commandList, boolean forced) {
    final DesktopToolWindowImpl toolWindow = (DesktopToolWindowImpl)getToolWindow(id);
    final FocusWatcher focusWatcher = myId2FocusWatcher.get(id);
    commandList.add(new DesktopRequestFocusInToolWindowCmd(getFocusManager(), toolWindow, focusWatcher, myCommandProcessor, myProject));
  }

  @Override
  protected void appendUpdateToolWindowsPaneCmd(final List<FinalizableCommand> commandsList) {
    final JRootPane rootPane = ((JFrame)TargetAWT.to(myFrame.getWindow())).getRootPane();
    if (rootPane != null) {
      final FinalizableCommand command = new UpdateRootPaneCmd(rootPane, myCommandProcessor);
      commandsList.add(command);
    }
  }

  private EditorsSplitters getSplittersToFocus() {
    WindowManagerEx windowManager = (WindowManagerEx)myWindowManager.get();

    Window activeWindow = TargetAWT.to(windowManager.getMostRecentFocusedWindow());

    if (activeWindow instanceof DesktopFloatingDecorator) {
      IdeFocusManager ideFocusManager = IdeFocusManager.findInstanceByComponent(activeWindow);
      IdeFrame lastFocusedFrame = ideFocusManager.getLastFocusedFrame();
      JComponent frameComponent = lastFocusedFrame != null ? lastFocusedFrame.getComponent() : null;
      Window lastFocusedWindow = frameComponent != null ? SwingUtilities.getWindowAncestor(frameComponent) : null;
      activeWindow = ObjectUtil.notNull(lastFocusedWindow, activeWindow);
    }

    FileEditorManagerEx fem = FileEditorManagerEx.getInstanceEx(myProject);
    EditorsSplitters splitters = activeWindow != null ? fem.getSplittersFor(activeWindow) : null;
    return splitters != null ? splitters : fem.getSplitters();
  }

  /**
   * @return <code>true</code> if tool window with the specified <code>id</code>
   * is floating and has modal showing child dialog. Such windows should not be closed
   * when auto-hide windows are gone.
   */
  @Override
  protected boolean hasModalChild(final WindowInfoImpl info) {
    if (!info.isVisible() || !info.isFloating()) {
      return false;
    }
    final DesktopFloatingDecorator decorator = getFloatingDecorator(info.getId());
    LOG.assertTrue(decorator != null);
    return isModalOrHasModalChild(decorator);
  }

  private static boolean isModalOrHasModalChild(final Window window) {
    if (window instanceof Dialog) {
      final Dialog dialog = (Dialog)window;
      if (dialog.isModal() && dialog.isShowing()) {
        return true;
      }
      final Window[] ownedWindows = dialog.getOwnedWindows();
      for (int i = ownedWindows.length - 1; i >= 0; i--) {
        if (isModalOrHasModalChild(ownedWindows[i])) {
          return true;
        }
      }
    }
    return false;
  }

  @RequiredUIAccess
  @Override
  @Nullable
  public String getLastActiveToolWindowId(@Nullable Condition<JComponent> condition) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    String lastActiveToolWindowId = null;
    for (int i = 0; i < myActiveStack.getPersistentSize(); i++) {
      final String id = myActiveStack.peekPersistent(i);
      final ToolWindow toolWindow = getToolWindow(id);
      LOG.assertTrue(toolWindow != null);
      if (toolWindow.isAvailable()) {
        if (condition == null || condition.value(toolWindow.getComponent())) {
          lastActiveToolWindowId = id;
          break;
        }
      }
    }
    return lastActiveToolWindowId;
  }

  @Override
  public Element getStateFromUI() {
    if (myFrame == null) {
      // do nothing if the project was not opened
      return null;
    }

    // Update size of all open floating windows. See SCR #18439
    for (final String id : getToolWindowIds()) {
      final WindowInfoImpl info = getInfo(id);
      if (info.isVisible()) {
        final DesktopInternalDecorator decorator = getInternalDecorator(id);
        LOG.assertTrue(decorator != null);
        decorator.fireResized();
      }
    }

    Element element = new Element("state");

    // Save frame's bounds
    JFrame jFrame = (JFrame)TargetAWT.to(myFrame.getWindow());
    final Rectangle frameBounds = jFrame.getBounds();
    final Element frameElement = new Element(FRAME_ELEMENT);
    element.addContent(frameElement);
    frameElement.setAttribute(X_ATTR, Integer.toString(frameBounds.x));
    frameElement.setAttribute(Y_ATTR, Integer.toString(frameBounds.y));
    frameElement.setAttribute(WIDTH_ATTR, Integer.toString(frameBounds.width));
    frameElement.setAttribute(HEIGHT_ATTR, Integer.toString(frameBounds.height));
    frameElement.setAttribute(EXTENDED_STATE_ATTR, Integer.toString(jFrame.getExtendedState()));

    // Save whether editor is active or not
    if (isEditorComponentActive()) {
      Element editorElement = new Element(EDITOR_ELEMENT);
      editorElement.setAttribute(ACTIVE_ATTR_VALUE, "true");
      element.addContent(editorElement);
    }

    // Save layout of tool windows
    Element layoutElement = myLayout.writeExternal(ToolWindowLayout.TAG);
    if (layoutElement != null) {
      element.addContent(layoutElement);
    }

    Element layoutToRestoreElement = myLayoutToRestoreLater == null ? null : myLayoutToRestoreLater.writeExternal(LAYOUT_TO_RESTORE);
    if (layoutToRestoreElement != null) {
      element.addContent(layoutToRestoreElement);
    }

    return element;
  }

  @RequiredWriteAction
  @Nullable
  @Override
  public Element getState(Element element) {
    return element;
  }

  public void stretchWidth(DesktopToolWindowImpl toolWindow, int value) {
    getToolWindowPanel().stretchWidth(toolWindow, value);
  }

  @Override
  public boolean isMaximized(@Nonnull ToolWindow wnd) {
    return getToolWindowPanel().isMaximized(wnd);
  }

  @Override
  public void setMaximized(@Nonnull ToolWindow wnd, boolean maximized) {
    getToolWindowPanel().setMaximized(wnd, maximized);
  }

  public void stretchHeight(DesktopToolWindowImpl toolWindow, int value) {
    getToolWindowPanel().stretchHeight(toolWindow, value);
  }

  private static class BalloonHyperlinkListener implements HyperlinkListener {
    private Balloon myBalloon;
    private final HyperlinkListener myListener;

    public BalloonHyperlinkListener(HyperlinkListener listener) {
      myListener = listener;
    }

    @Override
    public void hyperlinkUpdate(HyperlinkEvent e) {
      if (myBalloon != null && e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
        myBalloon.hide();
      }
      if (myListener != null) {
        myListener.hyperlinkUpdate(e);
      }
    }
  }


  /**
   * This command creates and shows <code>FloatingDecorator</code>.
   */
  private final class AddFloatingDecoratorCmd extends FinalizableCommand {
    private final DesktopFloatingDecorator myFloatingDecorator;

    /**
     * Creates floating decorator for specified floating decorator.
     */
    private AddFloatingDecoratorCmd(final DesktopInternalDecorator decorator, final WindowInfoImpl info) {
      super(myCommandProcessor);
      myFloatingDecorator = new DesktopFloatingDecorator(myFrame, info.copy(), decorator);
      myId2FloatingDecorator.put(info.getId(), myFloatingDecorator);
      final Rectangle2D bounds = info.getFloatingBounds();
      if (bounds != null && bounds.getWidth() > 0 && bounds.getHeight() > 0 && myWindowManager.get().isInsideScreenBounds(bounds.getX(), bounds.getY(), bounds.getWidth())) {
        myFloatingDecorator.setBounds(TargetAWT.to(bounds));
      }
      else { // place new frame at the center of main frame if there are no floating bounds
        Dimension size = decorator.getSize();
        if (size.width == 0 || size.height == 0) {
          size = decorator.getPreferredSize();
        }
        myFloatingDecorator.setSize(size);
        myFloatingDecorator.setLocationRelativeTo(TargetAWT.to(myFrame.getWindow()));
      }
    }

    @Override
    public void run() {
      try {
        myFloatingDecorator.show();
      }
      finally {
        finish();
      }
    }
  }

  /**
   * This command creates and shows <code>WindowedDecorator</code>.
   */
  private final class AddWindowedDecoratorCmd extends FinalizableCommand {
    private final DesktopWindowedDecorator myWindowedDecorator;

    /**
     * Creates floating decorator for specified floating decorator.
     */
    private AddWindowedDecoratorCmd(@Nonnull DesktopInternalDecorator decorator, @Nonnull WindowInfoImpl info) {
      super(myCommandProcessor);
      myWindowedDecorator = new DesktopWindowedDecorator(myProject, info.copy(), decorator);
      Window window = myWindowedDecorator.getFrame();
      final Rectangle2D bounds = info.getFloatingBounds();
      if (bounds != null && bounds.getWidth() > 0 && bounds.getHeight() > 0 && myWindowManager.get().isInsideScreenBounds(bounds.getX(), bounds.getY(), bounds.getWidth())) {
        window.setBounds(TargetAWT.to(bounds));
      }
      else { // place new frame at the center of main frame if there are no floating bounds
        Dimension size = decorator.getSize();
        if (size.width == 0 || size.height == 0) {
          size = decorator.getPreferredSize();
        }
        window.setSize(size);
        window.setLocationRelativeTo(TargetAWT.to(myFrame.getWindow()));
      }
      myId2WindowedDecorator.put(info.getId(), myWindowedDecorator);
      myWindowedDecorator.addDisposable(() -> {
        if (myId2WindowedDecorator.get(info.getId()) != null) {
          hideToolWindow(info.getId(), false);
        }
      });
    }

    @Override
    public void run() {
      try {
        myWindowedDecorator.show(false);
        Window window = myWindowedDecorator.getFrame();
        JRootPane rootPane = ((RootPaneContainer)window).getRootPane();
        Rectangle rootPaneBounds = rootPane.getBounds();
        Point point = rootPane.getLocationOnScreen();
        Rectangle windowBounds = window.getBounds();
        //Point windowLocation = windowBounds.getLocation();
        //windowLocation.translate(windowLocation.x - point.x, windowLocation.y - point.y);
        window.setLocation(2 * windowBounds.x - point.x, 2 * windowBounds.y - point.y);
        window.setSize(2 * windowBounds.width - rootPaneBounds.width, 2 * windowBounds.height - rootPaneBounds.height);
      }
      finally {
        finish();
      }
    }
  }

  /**
   * This command hides and destroys floating decorator for tool window
   * with specified <code>ID</code>.
   */
  private final class RemoveWindowedDecoratorCmd extends FinalizableCommand {
    private final DesktopWindowedDecorator myWindowedDecorator;

    private RemoveWindowedDecoratorCmd(final WindowInfoImpl info) {
      super(myCommandProcessor);
      myWindowedDecorator = getWindowedDecorator(info.getId());
      myId2WindowedDecorator.remove(info.getId());

      Window frame = myWindowedDecorator.getFrame();
      if (!frame.isShowing()) return;
      Rectangle2D bounds = getRootBounds((JFrame)frame);
      info.setFloatingBounds(bounds);
    }

    @Override
    public void run() {
      try {
        Disposer.dispose(myWindowedDecorator);
      }
      finally {
        finish();
      }
    }

    @Override
    @Nullable
    public Condition getExpireCondition() {
      return ApplicationManager.getApplication().getDisposed();
    }
  }


  /**
   * Notifies window manager about focus traversal in tool window
   */
  private final class ToolWindowFocusWatcher extends FocusWatcher {
    private final String myId;
    private final DesktopToolWindowImpl myToolWindow;

    private ToolWindowFocusWatcher(@Nonnull DesktopToolWindowImpl toolWindow) {
      myId = toolWindow.getId();
      install(toolWindow.getComponent());
      myToolWindow = toolWindow;
    }

    public void deinstall() {
      deinstall(myToolWindow.getComponent());
    }

    @Override
    protected boolean isFocusedComponentChangeValid(final Component comp, final AWTEvent cause) {
      return myCommandProcessor.getCommandCount() == 0 && comp != null;
    }

    @Override
    protected void focusedComponentChanged(final Component component, final AWTEvent cause) {
      if (myCommandProcessor.getCommandCount() > 0 || component == null) {
        return;
      }
      final WindowInfoImpl info = getInfo(myId);
      //getFocusManagerImpl(myProject)..cancelAllRequests();

      if (!info.isActive()) {
        getFocusManagerImpl(myProject).doWhenFocusSettlesDown(new EdtRunnable() {
          @Override
          public void runEdt() {
            WindowInfoImpl windowInfo = myLayout.getInfo(myId, true);
            if (windowInfo == null || !windowInfo.isVisible()) return;
            activateToolWindow(myId, false, false);
          }
        });
      }
    }
  }

  private void updateComponentTreeUI() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    final WindowInfoImpl[] infos = myLayout.getInfos();
    for (WindowInfoImpl info : infos) {
      // the main goal is to update hidden TW components because they are not in the hierarchy
      // and will not be updated automatically but unfortunately the visibility of a TW may change
      // during the same actionPerformed() so we can't optimize and have to process all of them
      IJSwingUtilities.updateComponentTreeUI(getInternalDecorator(info.getId()));
    }
  }

  private final class MyUIManagerPropertyChangeListener implements PropertyChangeListener {
    @Override
    public void propertyChange(final PropertyChangeEvent e) {
      updateComponentTreeUI();
    }
  }

  private final class MyLafManagerListener implements LafManagerListener {
    @Override
    public void lookAndFeelChanged(final LafManager source) {
      updateComponentTreeUI();
    }
  }

  /**
   * Delegate method for compatibility with older versions of IDEA
   */
  @Nonnull
  public AsyncResult<Void> requestFocus(@Nonnull Component c, boolean forced) {
    return IdeFocusManager.getInstance(myProject).requestFocus(c, forced);
  }

  public void doWhenFocusSettlesDown(@Nonnull Runnable runnable) {
    IdeFocusManager.getInstance(myProject).doWhenFocusSettlesDown(runnable);
  }

  @Nonnull
  private static Rectangle2D getRootBounds(JFrame frame) {
    JRootPane rootPane = frame.getRootPane();
    Rectangle bounds = rootPane.getBounds();
    bounds.setLocation(frame.getX() + rootPane.getX(), frame.getY() + rootPane.getY());
    return TargetAWT.from(bounds);
  }
}