/*
 * 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.ui;

import com.intellij.icons.AllIcons;
import com.intellij.ide.*;
import com.intellij.openapi.MnemonicHelper;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.ui.impl.ShadowBorderPainter;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupListener;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.FocusRequestor;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeGlassPaneUtil;
import com.intellij.openapi.wm.impl.IdeGlassPaneEx;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.panels.NonOpaquePanel;
import com.intellij.ui.components.panels.Wrapper;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.HashSet;
import com.intellij.util.ui.*;
import com.intellij.util.ui.accessibility.AccessibleContextUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageFilter;
import java.awt.image.RGBImageFilter;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class BalloonImpl implements Balloon, IdeTooltip.Ui {
  public static final int DIALOG_ARC = 6;
  public static final int ARC = 3;
  public static final int DIALOG_TOPBOTTOM_POINTER_WIDTH = 24;
  public static final int DIALOG_POINTER_WIDTH = 17;
  public static final int TOPBOTTOM_POINTER_WIDTH = 14;
  public static final int POINTER_WIDTH = 11;
  public static final int DIALOG_TOPBOTTOM_POINTER_LENGTH = 16;
  public static final int DIALOG_POINTER_LENGTH = 14;
  public static final int TOPBOTTOM_POINTER_LENGTH = 10;
  public static final int POINTER_LENGTH = 8;

  private final Alarm myFadeoutAlarm = new Alarm(this);
  private long myFadeoutRequestMillis = 0;
  private int myFadeoutRequestDelay = 0;

  private boolean mySmartFadeout;
  private boolean mySmartFadeoutPaused;
  private int mySmartFadeoutDelay;

  private MyComponent myComp;
  private JLayeredPane myLayeredPane;
  private AbstractPosition myPosition;
  private Point myTargetPoint;
  private final boolean myHideOnFrameResize;
  private final boolean myHideOnLinkClick;

  private final Color myBorderColor;
  private final Insets myBorderInsets;
  private final Color myFillColor;

  private final Insets myContainerInsets;

  private boolean myLastMoveWasInsideBalloon;

  private Rectangle myForcedBounds;

  private ActionProvider myActionProvider;
  private List<ActionButton> myActionButtons;

  private final AWTEventListener myAwtActivityListener = new AWTEventListener() {
    @Override
    public void eventDispatched(final AWTEvent e) {
      if (mySmartFadeoutDelay > 0) {
        startFadeoutTimer(mySmartFadeoutDelay);
        mySmartFadeoutDelay = 0;
      }

      final int id = e.getID();
      if (e instanceof MouseEvent) {
        final MouseEvent me = (MouseEvent)e;
        final boolean insideBalloon = isInsideBalloon(me);

        if (myHideOnMouse && id == MouseEvent.MOUSE_PRESSED) {
          if (!insideBalloon && !isWithinChildWindow(me)) {
            if (myHideListener == null) {
              hide();
            }
            else {
              myHideListener.run();
            }
          }
          return;
        }

        if (myClickHandler != null && id == MouseEvent.MOUSE_CLICKED) {
          if (!(me.getComponent() instanceof CloseButton) && insideBalloon) {
            myClickHandler.actionPerformed(new ActionEvent(me, ActionEvent.ACTION_PERFORMED, "click", me.getModifiersEx()));
            if (myCloseOnClick) {
              hide();
              return;
            }
          }
        }

        if (myEnableButtons && id == MouseEvent.MOUSE_MOVED) {
          final boolean moveChanged = insideBalloon != myLastMoveWasInsideBalloon;
          myLastMoveWasInsideBalloon = insideBalloon;
          if (moveChanged) {
            if (insideBalloon && myFadeoutAlarm.getActiveRequestCount() > 0) { //Pause hiding timer when mouse is hover
              myFadeoutAlarm.cancelAllRequests();
              myFadeoutRequestDelay -= System.currentTimeMillis() - myFadeoutRequestMillis;
            }
            if (!insideBalloon && myFadeoutRequestDelay > 0) {
              startFadeoutTimer(myFadeoutRequestDelay);
            }
            myComp.repaintButton();
          }
        }

        if (myHideOnCloseClick && UIUtil.isCloseClick(me)) {
          if (isInsideBalloon(me)) {
            hide();
            me.consume();
          }
          return;
        }
      }

      if ((myHideOnKey || myHideListener != null) && e instanceof KeyEvent && id == KeyEvent.KEY_PRESSED) {
        final KeyEvent ke = (KeyEvent)e;
        if (myHideListener != null) {
          if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
            myHideListener.run();
          }
          return;
        }
        if (ke.getKeyCode() != KeyEvent.VK_SHIFT &&
            ke.getKeyCode() != KeyEvent.VK_CONTROL &&
            ke.getKeyCode() != KeyEvent.VK_ALT &&
            ke.getKeyCode() != KeyEvent.VK_META) {
          boolean doHide = false;
          // Close the balloon is ESC is pressed inside the balloon
          if ((ke.getKeyCode() == KeyEvent.VK_ESCAPE) && SwingUtilities.isDescendingFrom(ke.getComponent(), myComp)) {
            doHide = true;
          }
          // Close the balloon if any key is pressed outside the balloon
          if (myHideOnKey && !SwingUtilities.isDescendingFrom(ke.getComponent(), myComp)) {
            doHide = true;
          }
          if (doHide)
            hide();
        }
      }
    }
  };

  private boolean isWithinChildWindow(MouseEvent event) {
    Component owner = UIUtil.getWindow(myContent);
    if (owner != null) {
      Component child = UIUtil.getWindow(event.getComponent());
      if (child != owner) {
        for (; child != null; child = child.getParent()) {
          if (child == owner) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private final long myFadeoutTime;
  private Dimension myDefaultPrefSize;
  private final ActionListener myClickHandler;
  private final boolean myCloseOnClick;
  private int myShadowSize = Registry.intValue("ide.balloon.shadow.size");
  private ShadowBorderProvider myShadowBorderProvider;

  private final CopyOnWriteArraySet<JBPopupListener> myListeners = new CopyOnWriteArraySet<>();
  private boolean myVisible;
  private PositionTracker<Balloon> myTracker;
  private final int myAnimationCycle;

  private boolean myFadedIn;
  private boolean myFadedOut;
  private final int myCalloutShift;

  private final int myPositionChangeXShift;
  private final int myPositionChangeYShift;
  private boolean myDialogMode;
  private IdeFocusManager myFocusManager;
  private final String myTitle;
  private JLabel myTitleLabel;

  private boolean myAnimationEnabled = true;
  private boolean myShadow = UIUtil.isUnderDarcula();
  private final Layer myLayer;
  private final boolean myBlockClicks;
  private RelativePoint myPrevMousePoint = null;

  public boolean isInsideBalloon(MouseEvent me) {
    return isInside(new RelativePoint(me));
  }

  @Override
  public boolean isInside(@NotNull RelativePoint target) {
    if (myComp == null) return false;
    Component cmp = target.getOriginalComponent();

    if (!cmp.isShowing()) return true;
    if (cmp instanceof MenuElement) return false;
    if (myActionButtons != null) {
      for (ActionButton button : myActionButtons) {
        if (cmp == button) return true;
      }
    }
    if (UIUtil.isDescendingFrom(cmp, myComp)) return true;
    if (myComp == null || !myComp.isShowing()) return false;
    Point point = target.getScreenPoint();
    SwingUtilities.convertPointFromScreen(point, myComp);
    return myComp.contains(point);
  }

  public boolean isMovingForward(RelativePoint target) {
    try {
      if (myComp == null || !myComp.isShowing()) return false;
      if (myPrevMousePoint == null) return true;
      if (myPrevMousePoint.getComponent() != target.getComponent()) return false;
      Rectangle rectangleOnScreen = new Rectangle(myComp.getLocationOnScreen(), myComp.getSize());
      return ScreenUtil.isMovementTowards(myPrevMousePoint.getScreenPoint(), target.getScreenPoint(), rectangleOnScreen);
    }
    finally {
      myPrevMousePoint = target;
    }
  }

  private final ComponentAdapter myComponentListener = new ComponentAdapter() {
    @Override
    public void componentResized(final ComponentEvent e) {
      if (myHideOnFrameResize) {
        hide();
      }
    }
  };
  private Animator myAnimator;
  private boolean myShowPointer;

  private boolean myDisposed;
  private final JComponent myContent;
  private boolean myHideOnMouse;
  private Runnable myHideListener;
  private final boolean myHideOnKey;
  private final boolean myHideOnAction;
  private final boolean myHideOnCloseClick;
  private final boolean myRequestFocus;
  private Component myOriginalFocusOwner;
  private final boolean myEnableButtons;
  private final Dimension myPointerSize;
  private final int myCornerToPointerDistance;

  public BalloonImpl(@NotNull JComponent content,
                     @NotNull Color borderColor,
                     Insets borderInsets,
                     @NotNull Color fillColor,
                     boolean hideOnMouse,
                     boolean hideOnKey,
                     boolean hideOnAction,
                     boolean hideOnCloseClick,
                     boolean showPointer,
                     boolean enableButtons,
                     long fadeoutTime,
                     boolean hideOnFrameResize,
                     boolean hideOnLinkClick,
                     ActionListener clickHandler,
                     boolean closeOnClick,
                     int animationCycle,
                     int calloutShift,
                     int positionChangeXShift,
                     int positionChangeYShift,
                     boolean dialogMode,
                     String title,
                     Insets contentInsets,
                     boolean shadow,
                     boolean smallVariant,
                     boolean blockClicks,
                     Layer layer,
                     boolean requestFocus,
                     Dimension pointerSize,
                     int cornerToPointerDistance) {
    myBorderColor = borderColor;
    myBorderInsets = borderInsets != null ? borderInsets : JBUI.insets(3);
    myFillColor = fillColor;
    myContent = content;
    myHideOnMouse = hideOnMouse;
    myHideOnKey = hideOnKey;
    myHideOnAction = hideOnAction;
    myHideOnCloseClick = hideOnCloseClick;
    myShowPointer = showPointer;
    myEnableButtons = enableButtons;
    myHideOnFrameResize = hideOnFrameResize;
    myHideOnLinkClick = hideOnLinkClick;
    myClickHandler = clickHandler;
    myCloseOnClick = closeOnClick;
    myCalloutShift = calloutShift;
    myPositionChangeXShift = positionChangeXShift;
    myPositionChangeYShift = positionChangeYShift;
    myDialogMode = dialogMode;
    myTitle = title;
    myLayer = layer != null ? layer : Layer.normal;
    myBlockClicks = blockClicks;
    myRequestFocus = requestFocus;
    MnemonicHelper.init(content);

    if (!myDialogMode) {
      for (Component component : UIUtil.uiTraverser(myContent)) {
        if (component instanceof JLabel) {
          JLabel label = (JLabel)component;
          if (label.getDisplayedMnemonic() != '\0' || label.getDisplayedMnemonicIndex() >= 0) {
            myDialogMode = true;
            break;
          }
        }
        else if (component instanceof JCheckBox) {
          JCheckBox checkBox = (JCheckBox)component;
          if (checkBox.getMnemonic() >= 0 || checkBox.getDisplayedMnemonicIndex() >= 0) {
            myDialogMode = true;
            break;
          }
        }
      }
    }

    myShadow = shadow;
    myShadowSize = Registry.intValue("ide.balloon.shadow.size");
    myContainerInsets = contentInsets;

    myFadeoutTime = fadeoutTime;
    myAnimationCycle = animationCycle;

    myPointerSize = pointerSize;
    myCornerToPointerDistance = cornerToPointerDistance;

    if (smallVariant) {
      for (Component component : UIUtil.uiTraverser(myContent)) {
        UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, component);
      }
    }
  }

  @Override
  public void show(final RelativePoint target, final Balloon.Position position) {
    show(target, getAbstractPositionFor(position));
  }

  public int getLayer() {
    Integer result = JLayeredPane.DEFAULT_LAYER;
    switch (myLayer) {
      case normal:
        result = JLayeredPane.POPUP_LAYER;
        break;
      case top:
        result = JLayeredPane.DRAG_LAYER;
        break;
    }

    return result;
  }

  private static AbstractPosition getAbstractPositionFor(Position position) {
    switch (position) {
      case atLeft:
        return AT_LEFT;
      case atRight:
        return AT_RIGHT;
      case below:
        return BELOW;
      case above:
        return ABOVE;
      default:
        return BELOW;
    }
  }

  @Override
  public void show(PositionTracker<Balloon> tracker, Balloon.Position position) {
    show(tracker, getAbstractPositionFor(position));
  }

  private Insets getInsetsCopy() {
    return new Insets(myBorderInsets.top, myBorderInsets.left, myBorderInsets.bottom, myBorderInsets.right);
  }

  private void show(RelativePoint target, AbstractPosition position) {
    show(new PositionTracker.Static<>(target), position);
  }

  private void show(PositionTracker<Balloon> tracker, AbstractPosition position) {
    assert !myDisposed : "Balloon is already disposed";

    if (isVisible()) return;
    final Component comp = tracker.getComponent();
    if (!comp.isShowing()) return;

    myTracker = tracker;
    myTracker.init(this);

    JRootPane root = ObjectUtils.notNull(UIUtil.getRootPane(comp));

    myVisible = true;

    myLayeredPane = root.getLayeredPane();
    myPosition = position;
    UIUtil.setFutureRootPane(myContent, root);

    myFocusManager = IdeFocusManager.findInstanceByComponent(myLayeredPane);
    final Ref<Component> originalFocusOwner = new Ref<>();
    final Ref<FocusRequestor> focusRequestor = new Ref<>();
    final Ref<ActionCallback> proxyFocusRequest = new Ref<>(ActionCallback.DONE);

    boolean mnemonicsFix = myDialogMode && SystemInfo.isMac && Registry.is("ide.mac.inplaceDialogMnemonicsFix");
    if (mnemonicsFix) {
      final IdeGlassPaneEx glassPane = (IdeGlassPaneEx)IdeGlassPaneUtil.find(myLayeredPane);
      assert glassPane != null;

      proxyFocusRequest.set(new ActionCallback());

      myFocusManager.doWhenFocusSettlesDown(new ExpirableRunnable() {
        @Override
        public boolean isExpired() {
          return isDisposed();
        }

        @Override
        public void run() {
          IdeEventQueue.getInstance().disableInputMethods(BalloonImpl.this);
          originalFocusOwner.set(myFocusManager.getFocusOwner());
          focusRequestor.set(myFocusManager.getFurtherRequestor());
        }
      });
    }
    if (myRequestFocus) {
      myFocusManager.doWhenFocusSettlesDown(new ExpirableRunnable() {
        @Override
        public boolean isExpired() {
          return isDisposed();
        }

        @Override
        public void run() {
          myOriginalFocusOwner = myFocusManager.getFocusOwner();

          // Set the accessible parent so that screen readers don't announce
          // a window context change -- the tooltip is "logically" hosted
          // inside the component (e.g. editor) it appears on top of.
          AccessibleContextUtil.setParent(myContent, myOriginalFocusOwner);

          // Set the focus to "myContent"
          myFocusManager.requestFocus(getContentToFocus(), true);
        }
      });
    }

    myLayeredPane.addComponentListener(myComponentListener);

    myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutShift);

    int positionChangeFix = 0;
    if (myShowPointer) {
      Rectangle rec = getRecForPosition(myPosition, true);

      if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) {
        rec = getRecForPosition(myPosition, false);

        Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize());
        lp.width -= myContainerInsets.right;
        lp.height -= myContainerInsets.bottom;

        if (!lp.contains(rec)) {
          Rectangle2D currentSquare = lp.createIntersection(rec);

          double maxSquare = currentSquare.getWidth() * currentSquare.getHeight();
          AbstractPosition targetPosition = myPosition;

          for (AbstractPosition eachPosition : myPosition.getOtherPositions()) {
            Rectangle2D eachIntersection = lp.createIntersection(getRecForPosition(eachPosition, false));
            double eachSquare = eachIntersection.getWidth() * eachIntersection.getHeight();
            if (maxSquare < eachSquare) {
              maxSquare = eachSquare;
              targetPosition = eachPosition;
            }
          }

          myPosition = targetPosition;
          positionChangeFix = myPosition.getChangeShift(position, myPositionChangeXShift, myPositionChangeYShift);
        }
      }
    }

    if (myPosition != position) {
      myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane),
                                                 myCalloutShift > 0 ? myCalloutShift + positionChangeFix : positionChangeFix);
    }

    createComponent();

    myComp.validate();

    Rectangle rec = myComp.getContentBounds();

    if (myShowPointer &&
        !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) {
      myShowPointer = false;
      myComp.removeAll();
      myLayeredPane.remove(myComp);

      createComponent();
      if (!new Rectangle(myLayeredPane.getSize())
        .contains(new Rectangle(myComp.getSize()))) { // Balloon is bigger than window, don't show it at all.
        myComp.removeAll();
        myLayeredPane.remove(myComp);
        myLayeredPane = null;
        hide();
        return;
      }
    }

    for (JBPopupListener each : myListeners) {
      each.beforeShown(new LightweightWindowEvent(this));
    }

    runAnimation(true, myLayeredPane, null);

    myLayeredPane.revalidate();
    myLayeredPane.repaint();

    if (mnemonicsFix) {
      proxyFocusRequest.get().doWhenDone(() -> myFocusManager.requestFocus(originalFocusOwner.get(), true));
    }

    Toolkit.getDefaultToolkit().addAWTEventListener(
      myAwtActivityListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);

    if (ApplicationManager.getApplication() != null) {
      ActionManager.getInstance().addAnActionListener(new AnActionListener.Adapter() {
        @Override
        public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
          if (myHideOnAction) {
            hide();
          }
        }
      }, this);
    }

    if (myHideOnLinkClick) {
      JEditorPane editorPane = UIUtil.uiTraverser(myContent).traverse().filter(JEditorPane.class).first();
      if (editorPane != null) {
        editorPane.addHyperlinkListener(new HyperlinkAdapter() {
          @Override
          protected void hyperlinkActivated(HyperlinkEvent e) {
            hide();
          }
        });
      }
    }
  }

  /**
   * Figure out the component to focus inside the {@link myContent} field.
   */
  @NotNull
  private Component getContentToFocus() {
    Component focusComponent = myContent;
    while (true) {
      // Setting focus to a JScrollPane is not very useful. Better setting focus to the
      // contained view. This is useful for Tooltip popups, for example.
      if (focusComponent instanceof JScrollPane) {
        JViewport viewport = ((JScrollPane)focusComponent).getViewport();
        if (viewport == null) {
          break;
        }
        Component child = viewport.getView();
        if (child == null) {
          break;
        }
        focusComponent = child;
        continue;
      }

      // Done if we can't find anything to dive into
      break;
    }
    return focusComponent;
  }

  private Rectangle getRecForPosition(AbstractPosition position, boolean adjust) {
    Dimension size = getContentSizeFor(position);
    Rectangle rec = new Rectangle(new Point(0, 0), size);

    position.setRecToRelativePosition(rec, myTargetPoint);

    if (adjust) {
      rec = myPosition.getUpdatedBounds(this, rec.getSize());
    }

    return rec;
  }

  private Dimension getContentSizeFor(AbstractPosition position) {
    Dimension size = myContent.getPreferredSize();
    if (myShadowBorderProvider == null) {
      JBInsets.addTo(size, position.createBorder(this).getBorderInsets());
    }
    return size;
  }

  private void disposeButton(ActionButton button) {
    if (button != null && button.getParent() != null) {
      Container parent = button.getParent();
      parent.remove(button);
      //noinspection RedundantCast
      ((JComponent)parent).revalidate();
      parent.repaint();
    }
  }

  public JComponent getContent() {
    return myContent;
  }

  public JComponent getComponent() {
    return myComp;
  }

  private void createComponent() {
    myComp = new MyComponent(myContent, this, myShadowBorderProvider != null ? null :
                                              myShowPointer ? myPosition.createBorder(this) : getPointlessBorder());

    if (myActionProvider == null) {
      final Consumer<MouseEvent> listener = event -> {
        //noinspection SSBasedInspection
        SwingUtilities.invokeLater(() -> hide());
      };

      myActionProvider = new ActionProvider() {
        private ActionButton myCloseButton;

        @NotNull
        @Override
        public List<ActionButton> createActions() {
          myCloseButton = new CloseButton(listener);
          return Collections.singletonList(myCloseButton);
        }

        @Override
        public void layout(@NotNull Rectangle lpBounds) {
          if (!myCloseButton.isVisible()) {
            return;
          }

          Icon icon = getCloseButton();
          int iconWidth = icon.getIconWidth();
          int iconHeight = icon.getIconHeight();
          Rectangle r =
            new Rectangle(lpBounds.x + lpBounds.width - iconWidth + (int)(iconWidth * 0.3), lpBounds.y - (int)(iconHeight * 0.3), iconWidth,
                          iconHeight);

          Insets border = getShadowBorderInsets();
          r.x -= border.left;
          r.y -= border.top;

          myCloseButton.setBounds(r);
        }
      };
    }

    myComp.clear();
    myComp.myAlpha = isAnimationEnabled() ? 0f : -1;

    myComp.setBorder(new EmptyBorder(getShadowBorderInsets()));

    myLayeredPane.add(myComp);
    myLayeredPane.setLayer(myComp, getLayer(), 0); // the second balloon must be over the first one
    myPosition.updateBounds(this);
    if (myBlockClicks) {
      myComp.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {
          e.consume();
        }

        @Override
        public void mousePressed(MouseEvent e) {
          e.consume();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
          e.consume();
        }
      });
    }
  }


  @NotNull
  private EmptyBorder getPointlessBorder() {
    return new EmptyBorder(myBorderInsets);
  }

  @Override
  public void revalidate() {
    revalidate(myTracker);
  }

  @Override
  public void revalidate(@NotNull PositionTracker<Balloon> tracker) {
    RelativePoint newPosition = tracker.recalculateLocation(this);

    if (newPosition != null) {
      myTargetPoint = myPosition.getShiftedPoint(newPosition.getPoint(myLayeredPane), myCalloutShift);
      myPosition.updateBounds(this);
    }
  }

  public void setShadowBorderProvider(@NotNull ShadowBorderProvider provider) {
    myShadowBorderProvider = provider;
  }

  public int getShadowBorderSize() {
    return hasShadow() ? myShadowSize : 0;
  }

  @NotNull
  public Insets getShadowBorderInsets() {
    if (myShadowBorderProvider != null) {
      return myShadowBorderProvider.getInsets();
    }
    int size = getShadowBorderSize();
    return new Insets(size, size, size, size);
  }

  public boolean hasShadow() {
    return myShadowBorderProvider != null || (myShadow && Registry.is("ide.balloon.shadowEnabled"));
  }

  public interface ShadowBorderProvider {
    @NotNull
    Insets getInsets();

    void paintShadow(@NotNull JComponent component, @NotNull Graphics g);

    void paintBorder(@NotNull Rectangle bounds, @NotNull Graphics2D g);

    void paintPointingShape(@NotNull Rectangle bounds, @NotNull Point pointTarget, @NotNull Position position, @NotNull Graphics2D g);
  }

  @Override
  public void show(JLayeredPane pane) {
    show(pane, null);
  }

  @Override
  public void showInCenterOf(JComponent component) {
    final Dimension size = component.getSize();
    show(new RelativePoint(component, new Point(size.width / 2, size.height / 2)), Balloon.Position.above);
  }

  public void show(JLayeredPane pane, @Nullable Rectangle bounds) {
    if (bounds != null) {
      myForcedBounds = bounds;
    }
    show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above);
  }


  private void runAnimation(boolean forward, final JLayeredPane layeredPane, @Nullable final Runnable onDone) {
    if (myAnimator != null) {
      Disposer.dispose(myAnimator);
    }

    myAnimator = new Animator("Balloon", 8, isAnimationEnabled() ? myAnimationCycle : 0, false, forward) {
      @Override
      public void paintNow(final int frame, final int totalFrames, final int cycle) {
        if (myComp == null || myComp.getParent() == null || !isAnimationEnabled()) return;
        myComp.setAlpha((float)frame / totalFrames);
      }

      @Override
      protected void paintCycleEnd() {
        if (myComp == null || myComp.getParent() == null) return;

        if (isForward()) {
          myComp.clear();
          myComp.repaint();

          myFadedIn = true;

          if (!myFadeoutAlarm.isDisposed()) {
            startFadeoutTimer((int)myFadeoutTime);
          }
        }
        else {
          layeredPane.remove(myComp);
          layeredPane.revalidate();
          layeredPane.repaint();
        }
        Disposer.dispose(this);
      }

      @Override
      public void dispose() {
        super.dispose();
        myAnimator = null;
        if (onDone != null) {
          onDone.run();
        }
      }
    };

    myAnimator.resume();
  }

  public void runWithSmartFadeoutPause(@NotNull Runnable handler) {
    if (mySmartFadeout) {
      mySmartFadeoutPaused = true;
      handler.run();
      if (mySmartFadeoutPaused) {
        mySmartFadeoutPaused = false;
      }
      else {
        setAnimationEnabled(true);
        hide();
      }
    }
    else {
      handler.run();
    }
  }

  public void startSmartFadeoutTimer(int delay) {
    mySmartFadeout = true;
    mySmartFadeoutDelay = delay;
    FrameStateManager.getInstance().addListener(new FrameStateListener.Adapter() {
      @Override
      public void onFrameDeactivated() {
        if (myFadeoutAlarm.getActiveRequestCount() > 0) {
          myFadeoutAlarm.cancelAllRequests();
          mySmartFadeoutDelay = myFadeoutRequestDelay - (int)(System.currentTimeMillis() - myFadeoutRequestMillis);
          if (mySmartFadeoutDelay <= 0) {
            mySmartFadeoutDelay = 1;
          }
        }
      }
    }, this);
  }

  public void startFadeoutTimer(final int fadeoutDelay) {
    if (fadeoutDelay > 0) {
      myFadeoutAlarm.cancelAllRequests();
      myFadeoutRequestMillis = System.currentTimeMillis();
      myFadeoutRequestDelay = fadeoutDelay;
      myFadeoutAlarm.addRequest(() -> {
        if (mySmartFadeout) {
          setAnimationEnabled(true);
        }
        hide();
      }, fadeoutDelay, null);
    }
  }


  int getArc() {
    return myDialogMode ? DIALOG_ARC : ARC;
  }

  int getPointerWidth(AbstractPosition position) {
    if (myPointerSize == null || myPointerSize.width <= 0) {
      if (myDialogMode) {
        return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_WIDTH : DIALOG_POINTER_WIDTH;
      }
      else {
        return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_WIDTH : POINTER_WIDTH;
      }
    } else {
      return myPointerSize.width;
    }
  }

  public static int getNormalInset() {
    return 3;
  }

  int getPointerLength(AbstractPosition position) {
    return (myPointerSize == null || myPointerSize.height <= 0) ? getPointerLength(position, myDialogMode) : myPointerSize.height;
  }

  static int getPointerLength(AbstractPosition position, boolean dialogMode) {
    if (dialogMode) {
      return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_LENGTH : DIALOG_POINTER_LENGTH;
    }
    else {
      return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_LENGTH : POINTER_LENGTH;
    }
  }

  public static int getPointerLength(Position position, boolean dialogMode) {
    return getPointerLength(getAbstractPositionFor(position), dialogMode);
  }

  @Override
  public void hide() {
    hide(false);
  }

  @Override
  public void hide(boolean ok) {
    hideAndDispose(ok);
  }

  @Override
  public void dispose() {
    hideAndDispose(false);
  }

  private boolean myTraceDispose;

  public void traceDispose(boolean value) {
    assert !isDisposed() : "Balloon is already disposed";
    myTraceDispose = value;
  }

  private void hideAndDispose(final boolean ok) {
    if (myDisposed) return;

    if (mySmartFadeoutPaused) {
      mySmartFadeoutPaused = false;
      return;
    }

    if (myTraceDispose) {
      Logger.getInstance("#com.intellij.ui.BalloonImpl").error("Dispose balloon before showing", new Throwable());
    }

    myDisposed = true;
    hideComboBoxPopups();

    final Runnable disposeRunnable = () -> {
      myFadedOut = true;
      if (myRequestFocus) {
        if (myOriginalFocusOwner != null) {
          myFocusManager.requestFocus(myOriginalFocusOwner, false);
        }
      }

      for (JBPopupListener each : myListeners) {
        each.onClosed(new LightweightWindowEvent(this, ok));
      }

      Disposer.dispose(this);
      onDisposed();
    };

    Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener);
    if (myLayeredPane != null) {
      myLayeredPane.removeComponentListener(myComponentListener);

      runAnimation(false, myLayeredPane, disposeRunnable);
    }
    else {
      disposeRunnable.run();
    }

    myVisible = false;
    myTracker = null;
  }

  private void hideComboBoxPopups() {
    List<JComboBox> comboBoxes = UIUtil.findComponentsOfType(myComp, JComboBox.class);
    for (JComboBox box : comboBoxes) {
      box.hidePopup();
    }
  }

  protected void onDisposed() {
  }

  @Override
  public void addListener(@NotNull JBPopupListener listener) {
    myListeners.add(listener);
  }

  public boolean isVisible() {
    return myVisible;
  }

  public void setHideOnClickOutside(boolean hideOnMouse) {
    myHideOnMouse = hideOnMouse;
  }

  public void setHideListener(@NotNull Runnable listener) {
    myHideListener = listener;
    myHideOnMouse = true;
  }

  public void setShowPointer(final boolean show) {
    myShowPointer = show;
  }

  @SuppressWarnings("MethodMayBeStatic")
  public Icon getCloseButton() {
    return AllIcons.General.BalloonClose;
  }

  @Override
  public void setBounds(Rectangle bounds) {
    myForcedBounds = bounds;
    if (myPosition != null) {
      myPosition.updateBounds(this);
    }
  }

  public void setShadowSize(int shadowSize) {
    myShadowSize = shadowSize;
  }

  @Override
  public Dimension getPreferredSize() {
    if (myComp != null) {
      return myComp.getPreferredSize();
    }
    if (myDefaultPrefSize == null) {
      final EmptyBorder border = myShadowBorderProvider == null ? getPointlessBorder() : null;
      final MyComponent c = new MyComponent(myContent, this, border);
      if (myShadowBorderProvider != null) {
        c.setBorder(new EmptyBorder(getShadowBorderInsets()));
      }
      myDefaultPrefSize = c.getPreferredSize();
    }
    return myDefaultPrefSize;
  }

  private abstract static class AbstractPosition {
    abstract EmptyBorder createBorder(final BalloonImpl balloon);


    abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint);

    abstract int getChangeShift(AbstractPosition original, int xShift, int yShift);

    public void updateBounds(final BalloonImpl balloon) {
      if (balloon.myLayeredPane == null || balloon.myComp == null) return;

      Rectangle bounds = getUpdatedBounds(balloon, balloon.myComp.getPreferredSize());

      if (balloon.myShadowBorderProvider == null) {
        bounds = new Rectangle(getShiftedPoint(bounds.getLocation(), balloon.getShadowBorderInsets()), bounds.getSize());
      }
      balloon.myComp._setBounds(bounds);
    }

    public Rectangle getUpdatedBounds(BalloonImpl balloon, Dimension preferredSize) {
      Dimension layeredPaneSize = balloon.myLayeredPane.getSize();
      Point point = balloon.myTargetPoint;

      Rectangle bounds = balloon.myForcedBounds;
      if (bounds == null) {
        int distance = getDistance(balloon, preferredSize);
        Point location = balloon.myShowPointer
                         ? getLocation(layeredPaneSize, point, preferredSize, distance) // Now distance is used for pointer enabled balloons only
                         : new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2);
        bounds = new Rectangle(location.x, location.y, preferredSize.width, preferredSize.height);

        ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), balloon.myContainerInsets);
      }

      return bounds;
    }

    private int getDistance(BalloonImpl balloon, Dimension size) {
      if (balloon.myCornerToPointerDistance < 0) return -1;

      int indent = balloon.getArc() + balloon.getPointerWidth(this)/2;
      if (balloon.myCornerToPointerDistance < indent) return indent;

      int limit = (this == ABOVE || this == BELOW) ? size.width - indent : size.height - indent;
      if (balloon.myCornerToPointerDistance > limit) return limit;

      return balloon.myCornerToPointerDistance;
    }

    abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize, int distance);

    void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) {
      final GraphicsConfig cfg = new GraphicsConfig(g);
      cfg.setAntialiasing(true);

      if (balloon.myShadowBorderProvider != null) {
        balloon.myShadowBorderProvider.paintBorder(bounds, g);
        if (balloon.myShowPointer) {
          Position position;
          if (this == ABOVE) {
            position = Position.above;
          }
          else if (this == BELOW) {
            position = Position.below;
          }
          else if (this == AT_LEFT) {
            position = Position.atLeft;
          }
          else {
            position = Position.atRight;
          }
          balloon.myShadowBorderProvider.paintPointingShape(bounds, pointTarget, position, g);
        }
        cfg.restore();
        return;
      }

      Shape shape;
      if (balloon.myShowPointer) {
        shape = getPointingShape(bounds, pointTarget, balloon);
      }
      else {
        shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc());
      }

      g.setColor(balloon.myFillColor);
      g.fill(shape);
      g.setColor(balloon.myBorderColor);

      if (balloon.myTitleLabel != null) {
        Rectangle titleBounds = balloon.myTitleLabel.getBounds();

        Insets inset = getTitleInsets(getNormalInset() - 1, balloon.getPointerLength(this) + 50);
        Insets borderInsets = balloon.getShadowBorderInsets();
        inset.top += borderInsets.top;
        inset.bottom += borderInsets.bottom;
        inset.left += borderInsets.left;
        inset.right += borderInsets.right;

        titleBounds.x -= inset.left + 1;
        titleBounds.width += inset.left + inset.right + 50;
        titleBounds.y -= inset.top + 1;
        titleBounds.height += inset.top + inset.bottom + 1;

        Area area = new Area(shape);
        area.intersect(new Area(titleBounds));


        Color fgColor = UIManager.getColor("Label.foreground");
        fgColor = ColorUtil.toAlpha(fgColor, 140);
        g.setColor(fgColor);
        g.fill(area);

        g.setColor(balloon.myBorderColor);
        g.draw(area);
      }

      g.draw(shape);
      cfg.restore();
    }

    protected abstract Insets getTitleInsets(int normalInset, int pointerLength);

    protected abstract Shape getPointingShape(final Rectangle bounds,
                                              final Point pointTarget,
                                              final BalloonImpl balloon);

    public boolean isOkToHavePointer(Point targetPoint, Rectangle bounds, int pointerLength, int pointerWidth, int arc) {
      if (bounds.x < targetPoint.x &&
          bounds.x + bounds.width > targetPoint.x &&
          bounds.y < targetPoint.y &&
          bounds.y + bounds.height > targetPoint.y) {
        return false;
      }

      Rectangle pointless = getPointlessContentRec(bounds, pointerLength);

      int distance = getDistanceToTarget(pointless, targetPoint);
      if (distance < pointerLength - 1 || distance > 2 * pointerLength) return false;

      UnfairTextRange balloonRange;
      UnfairTextRange pointerRange;
      if (isTopBottomPointer()) {
        balloonRange = new UnfairTextRange(bounds.x + arc - 1, bounds.x + bounds.width - arc * 2 + 1);
        pointerRange = new UnfairTextRange(targetPoint.x - pointerWidth / 2, targetPoint.x + pointerWidth / 2);
      }
      else {
        balloonRange = new UnfairTextRange(bounds.y + arc - 1, bounds.y + bounds.height - arc * 2 + 1);
        pointerRange = new UnfairTextRange(targetPoint.y - pointerWidth / 2, targetPoint.y + pointerWidth / 2);
      }
      return balloonRange.contains(pointerRange);
    }

    protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint);

    protected boolean isTopBottomPointer() {
      return this instanceof Below || this instanceof Above;
    }

    protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength);

    public Set<AbstractPosition> getOtherPositions() {
      HashSet<AbstractPosition> all = new HashSet<>();
      all.add(BELOW);
      all.add(ABOVE);
      all.add(AT_RIGHT);
      all.add(AT_LEFT);

      all.remove(this);

      return all;
    }

    public abstract Point getShiftedPoint(Point targetPoint, int shift);

    public abstract Point getShiftedPoint(Point targetPoint, Insets shift);
  }

  public static final AbstractPosition BELOW = new Below();
  public static final AbstractPosition ABOVE = new Above();
  public static final AbstractPosition AT_RIGHT = new AtRight();
  public static final AbstractPosition AT_LEFT = new AtLeft();


  private static class Below extends AbstractPosition {
    @Override
    public Point getShiftedPoint(Point targetPoint, int shift) {
      return new Point(targetPoint.x, targetPoint.y + shift);
    }

    @Override
    public Point getShiftedPoint(Point targetPoint, Insets shift) {
      return getShiftedPoint(targetPoint, -shift.top);
    }

    @Override
    int getChangeShift(AbstractPosition original, int xShift, int yShift) {
      return original == ABOVE ? yShift : 0;
    }


    @Override
    protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
      return rectangle.y - targetPoint.y;
    }

    @Override
    protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
      return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength);
    }

    @Override
    EmptyBorder createBorder(final BalloonImpl balloon) {
      Insets insets = balloon.getInsetsCopy();
      insets.top += balloon.getPointerLength(this);
      return new EmptyBorder(insets);
    }

    @Override
    void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
      rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y));
    }

    @Override
    Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize, int distance) {
      if (distance > 0) {
        return new Point(targetPoint.x - distance, targetPoint.y);
      } else {
        final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
        return new Point(center.x, targetPoint.y);
      }
    }

    @Override
    protected Insets getTitleInsets(int normalInset, int pointerLength) {
      return new Insets(pointerLength, normalInset, normalInset, normalInset);
    }

    @Override
    protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
      final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.TOP);
      shaper.line(balloon.getPointerWidth(this) / 2, balloon.getPointerLength(this)).toRightCurve().roundRightDown().toBottomCurve()
        .roundLeftDown()
        .toLeftCurve().roundLeftUp().toTopCurve().roundUpRight()
        .lineTo(pointTarget.x - balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y);
      shaper.close();

      return shaper.getShape();
    }
  }

  private static class Above extends AbstractPosition {
    @Override
    public Point getShiftedPoint(Point targetPoint, int shift) {
      return new Point(targetPoint.x, targetPoint.y - shift);
    }

    @Override
    public Point getShiftedPoint(Point targetPoint, Insets shift) {
      return getShiftedPoint(targetPoint, -shift.top);
    }

    @Override
    int getChangeShift(AbstractPosition original, int xShift, int yShift) {
      return original == BELOW ? -yShift : 0;
    }

    @Override
    protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
      return targetPoint.y - (int)rectangle.getMaxY();
    }

    @Override
    protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
      return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength);
    }

    @Override
    EmptyBorder createBorder(final BalloonImpl balloon) {
      Insets insets = balloon.getInsetsCopy();
      insets.bottom = balloon.getPointerLength(this);
      return new EmptyBorder(insets);
    }

    @Override
    void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
      rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height);
    }

    @Override
    Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize, int distance) {
      if (distance > 0) {
        return new Point(targetPoint.x - distance, targetPoint.y - balloonSize.height);
      } else {
        final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
        return new Point(center.x, targetPoint.y - balloonSize.height);
      }
    }

    @Override
    protected Insets getTitleInsets(int normalInset, int pointerLength) {
      return new Insets(normalInset, normalInset, normalInset, normalInset);
    }

    @Override
    protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
      final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.BOTTOM);
      shaper.line(-balloon.getPointerWidth(this) / 2, -balloon.getPointerLength(this) + 1);
      shaper.toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown().toBottomCurve().line(0, 2)
        .roundLeftDown().lineTo(pointTarget.x + balloon.getPointerWidth(this) / 2, shaper.getCurrent().y)
        .lineTo(pointTarget.x, pointTarget.y)
        .close();


      return shaper.getShape();
    }
  }

  private static class AtRight extends AbstractPosition {
    @Override
    public Point getShiftedPoint(Point targetPoint, int shift) {
      return new Point(targetPoint.x + shift, targetPoint.y);
    }

    @Override
    public Point getShiftedPoint(Point targetPoint, Insets shift) {
      return getShiftedPoint(targetPoint, -shift.left);
    }

    @Override
    int getChangeShift(AbstractPosition original, int xShift, int yShift) {
      return original == AT_LEFT ? xShift : 0;
    }

    @Override
    protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
      return rectangle.x - targetPoint.x;
    }

    @Override
    protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
      return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height);
    }

    @Override
    EmptyBorder createBorder(final BalloonImpl balloon) {
      Insets insets = balloon.getInsetsCopy();
      insets.left += balloon.getPointerLength(this);
      return new EmptyBorder(insets);
    }

    @Override
    void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
      rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2);
    }

    @Override
    Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize, int distance) {
      if (distance > 0) {
        return new Point(targetPoint.x, targetPoint.y - distance);
      } else {
        final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
        return new Point(targetPoint.x, center.y);
      }
    }

    @Override
    protected Insets getTitleInsets(int normalInset, int pointerLength) {
      return new Insets(normalInset, pointerLength, normalInset, normalInset);
    }

    @Override
    protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
      final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.LEFT);
      shaper.line(balloon.getPointerLength(this), -balloon.getPointerWidth(this) / 2).toTopCurve().roundUpRight().toRightCurve()
        .roundRightDown()
        .toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp()
        .lineTo(shaper.getCurrent().x, pointTarget.y + balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();

      return shaper.getShape();
    }
  }

  private static class AtLeft extends AbstractPosition {
    @Override
    public Point getShiftedPoint(Point targetPoint, int shift) {
      return new Point(targetPoint.x - shift, targetPoint.y);
    }

    @Override
    public Point getShiftedPoint(Point targetPoint, Insets shift) {
      return getShiftedPoint(targetPoint, -shift.left);
    }

    @Override
    int getChangeShift(AbstractPosition original, int xShift, int yShift) {
      return original == AT_RIGHT ? -xShift : 0;
    }


    @Override
    protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
      return targetPoint.x - (int)rectangle.getMaxX();
    }

    @Override
    protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
      return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height);
    }

    @Override
    EmptyBorder createBorder(final BalloonImpl balloon) {
      Insets insets = balloon.getInsetsCopy();
      insets.right += balloon.getPointerLength(this);
      return new EmptyBorder(insets);
    }

    @Override
    void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
      rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2);
    }

    @Override
    Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize, int distance) {
      if (distance > 0) {
        return new Point(targetPoint.x - balloonSize.width, targetPoint.y - distance);
      } else {
        final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
        return new Point(targetPoint.x - balloonSize.width, center.y);
      }
    }

    @Override
    protected Insets getTitleInsets(int normalInset, int pointerLength) {
      return new Insets(normalInset, pointerLength, normalInset, normalInset);
    }

    @Override
    protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
      final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.RIGHT);
      shaper
        .lineTo((int)bounds.getMaxX() - shaper.getTargetDelta(SwingConstants.RIGHT) - 1, pointTarget.y + balloon.getPointerWidth(this) / 2);
      shaper.toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown()
        .lineTo(shaper.getCurrent().x, pointTarget.y - balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
      return shaper.getShape();
    }
  }

  public interface ActionProvider {
    @NotNull
    List<ActionButton> createActions();

    void layout(@NotNull Rectangle bounds);
  }

  public class ActionButton extends NonOpaquePanel {
    private final Icon myIcon;
    private final Icon myHoverIcon;
    private final Consumer<MouseEvent> myListener;
    protected final BaseButtonBehavior myButton;

    public ActionButton(@NotNull Icon icon, @Nullable Icon hoverIcon, @Nullable String hint, @NotNull Consumer<MouseEvent> listener) {
      myIcon = icon;
      myHoverIcon = hoverIcon;
      myListener = listener;

      setToolTipText(hint);

      myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
        @Override
        protected void execute(MouseEvent e) {
          myListener.consume(e);
        }
      };
    }

    @Override
    public Dimension getPreferredSize() {
      return new Dimension(myIcon.getIconWidth(), myIcon.getIconHeight());
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      if (hasPaint()) {
        paintIcon(g, myHoverIcon != null && myButton.isHovered() ? myHoverIcon : myIcon);
      }
    }

    public boolean hasPaint() {
      return getWidth() > 0 && myLastMoveWasInsideBalloon;
    }

    protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon) {
      icon.paintIcon(this, g, 0, 0);
    }
  }

  private class CloseButton extends ActionButton {
    private CloseButton(@NotNull Consumer<MouseEvent> listener) {
      super(getCloseButton(), null, null, listener);
      setVisible(myEnableButtons);
    }

    @Override
    protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon) {
      if (myEnableButtons) {
        final boolean pressed = myButton.isPressedByMouse();
        icon.paintIcon(this, g, pressed ? 1 : 0, pressed ? 1 : 0);
      }
    }
  }

  private class MyComponent extends JPanel implements ComponentWithMnemonics {

    private BufferedImage myImage;
    private float myAlpha;
    private final BalloonImpl myBalloon;

    private final JComponent myContent;
    private ShadowBorderPainter.Shadow myShadow;

    private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) {
      setOpaque(false);
      setLayout(null);
      myBalloon = balloon;

      // When a screen reader is active, TAB/Shift-TAB should allow moving the focus
      // outside the balloon in the event the balloon acquired the focus.
      if (!ScreenReader.isActive()) {
        setFocusCycleRoot(true);
      }
      putClientProperty(Balloon.KEY, BalloonImpl.this);

      myContent = new JPanel(new BorderLayout(2, 2));
      Wrapper contentWrapper = new Wrapper(content);
      if (myTitle != null) {
        myTitleLabel = new JLabel(myTitle, SwingConstants.CENTER);
        myTitleLabel.setForeground(UIManager.getColor("List.background"));
        myTitleLabel.setBorder(new EmptyBorder(0, 4, 0, 4));
        myContent.add(myTitleLabel, BorderLayout.NORTH);
        contentWrapper.setBorder(new EmptyBorder(1, 1, 1, 1));
      }
      myContent.add(contentWrapper, BorderLayout.CENTER);
      myContent.setBorder(shapeBorder);
      myContent.setOpaque(false);

      add(myContent);
    }

    public Rectangle getContentBounds() {
      Rectangle bounds = super.getBounds();
      JBInsets.removeFrom(bounds, getInsets());
      return bounds;
    }

    public void clear() {
      myImage = null;
      myAlpha = -1;
    }

    @Override
    public void doLayout() {
      Rectangle bounds = new Rectangle(getWidth(), getHeight());
      JBInsets.removeFrom(bounds, getInsets());

      myContent.setBounds(bounds);
    }

    @Override
    public Dimension getPreferredSize() {
      return addInsets(myContent.getPreferredSize());
    }

    @Override
    public Dimension getMinimumSize() {
      return addInsets(myContent.getMinimumSize());
    }

    private Dimension addInsets(Dimension size) {
      JBInsets.addTo(size, getInsets());
      return size;
    }

    @Override
    protected void paintChildren(Graphics g) {
      if (myImage == null || myAlpha == -1) {
        super.paintChildren(g);
      }
    }

    private void paintChildrenImpl(Graphics g) {
      // Paint to an image without alpha to preserve fonts subpixel antialiasing
      @SuppressWarnings("UndesirableClassUsage")
      BufferedImage image = UIUtil.createImage(g, getWidth(), getHeight(),
                                               BufferedImage.TYPE_INT_RGB);//new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
      Graphics2D imageGraphics = image.createGraphics();
      //noinspection UseJBColor
      imageGraphics.setColor(new Color(myFillColor.getRGB())); // create a copy to remove alpha
      imageGraphics.fillRect(0, 0, getWidth(), getHeight());

      super.paintChildren(imageGraphics);
      imageGraphics.dispose();
      Graphics2D g2d = (Graphics2D)g.create();
      try {
        if (UIUtil.isJreHiDPI(g2d)) {
          float s = 1 / JBUI.sysScale(g2d);
          g2d.scale(s, s);
        }
        UIUtil.drawImage(g2d, makeColorTransparent(image, myFillColor), 0, 0, null);
      }
      finally {
        g2d.dispose();
      }
    }

    private Image makeColorTransparent(Image image, Color color) {
      final int markerRGB = color.getRGB() | 0xFF000000;
      ImageFilter filter = new RGBImageFilter() {
        @Override
        public int filterRGB(int x, int y, int rgb) {
          if ((rgb | 0xFF000000) == markerRGB) {
            return 0x00FFFFFF & rgb; // set alpha to 0
          }
          else {
            return rgb;
          }
        }
      };
      return ImageUtil.filter(image, filter);
    }

    @Override
    protected void paintComponent(final Graphics g) {
      super.paintComponent(g);

      final Graphics2D g2d = (Graphics2D)g;

      Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
      Rectangle shapeBounds = myContent.getBounds();
      int shadowSize = myBalloon.getShadowBorderSize();

      if (shadowSize > 0 && myShadow == null && myShadowBorderProvider == null) {
        initComponentImage(pointTarget, shapeBounds);
        myShadow = ShadowBorderPainter.createShadow(myImage, 0, 0, false, shadowSize / 2);
      }

      if (myImage == null && myAlpha != -1) {
        initComponentImage(pointTarget, shapeBounds);
      }

      if (myImage != null && myAlpha != -1) {
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha));
      }

      if (myShadowBorderProvider != null) {
        myShadowBorderProvider.paintShadow(this, g);
      }

      if (myImage != null && myAlpha != -1) {
        paintShadow(g);
        UIUtil.drawImage(g2d, myImage, 0, 0, null);
      }
      else {
        paintShadow(g);
        myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget);
      }
    }

    private void paintShadow(Graphics graphics) {
      if (myShadow != null) {
        Graphics2D g2d = (Graphics2D)graphics;
        try {
          if (UIUtil.isJreHiDPI(g2d)) {
            g2d = (Graphics2D)graphics.create();
            float s = 1 / JBUI.sysScale(this);
            g2d.scale(s, s);
          }
          UIUtil.drawImage(g2d, myShadow.getImage(), myShadow.getX(), myShadow.getY(), null);
        }
        finally {
          if (g2d != graphics) g2d.dispose();
        }
      }
    }

    @Override
    public boolean contains(int x, int y) {
      Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
      Rectangle bounds = myContent.getBounds();
      Shape shape;
      if (myShowPointer) {
        shape = myBalloon.myPosition.getPointingShape(bounds, pointTarget, myBalloon);
      }
      else {
        shape =
          new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, myBalloon.getArc(), myBalloon.getArc());
      }
      return shape.contains(x, y);
    }

    private void initComponentImage(Point pointTarget, Rectangle shapeBounds) {
      if (myImage != null) return;

      myImage = UIUtil.createImage(myComp, getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
      Graphics2D imageGraphics = (Graphics2D)myImage.getGraphics();
      myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, imageGraphics, pointTarget);
      paintChildrenImpl(imageGraphics);
      imageGraphics.dispose();
    }


    @Override
    public void removeNotify() {
      super.removeNotify();

      if (!ScreenUtil.isStandardAddRemoveNotify(this)) {
        return;
      }

      final List<ActionButton> buttons = myActionButtons;
      myActionButtons = null;
      if (buttons != null) {
        //noinspection SSBasedInspection
        SwingUtilities.invokeLater(() -> {
          for (ActionButton button : buttons) {
            disposeButton(button);
          }
        });
      }
    }

    public void setAlpha(float alpha) {
      myAlpha = alpha;
      paintImmediately(0, 0, getWidth(), getHeight());
    }

    public void _setBounds(Rectangle bounds) {
      Rectangle currentBounds = getBounds();
      if (currentBounds.width != bounds.width || currentBounds.height != bounds.height) {
        invalidateShadowImage();
      }

      super.setBounds(bounds);


      if (getParent() != null) {
        if (myActionButtons == null) {
          myActionButtons = myActionProvider.createActions();
        }

        for (ActionButton button : myActionButtons) {
          if (button.getParent() == null) {
            myLayeredPane.add(button);
            myLayeredPane.setLayer(button, JLayeredPane.DRAG_LAYER);
          }
        }
      }

      if (isVisible()) {
        Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane);
        lpBounds = myPosition
          .getPointlessContentRec(lpBounds, myBalloon.myShadowBorderProvider == null ? myBalloon.getPointerLength(myPosition) : 0);
        myActionProvider.layout(lpBounds);
      }

      if (isVisible()) {
        revalidate();
        repaint();
      }
    }

    private void invalidateShadowImage() {
      myImage = null;
      myShadow = null;
    }

    public void repaintButton() {
      if (myActionButtons != null) {
        for (ActionButton button : myActionButtons) {
          button.repaint();
        }
      }
    }
  }

  private static class Shaper {
    private final GeneralPath myPath = new GeneralPath();

    Rectangle myBounds;
    @JdkConstants.TabPlacement
    private final int myTargetSide;
    private final BalloonImpl myBalloon;

    public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, @JdkConstants.TabPlacement int targetSide) {
      myBalloon = balloon;
      myBounds = bounds;
      myTargetSide = targetSide;
      start(targetPoint);
    }

    private void start(Point start) {
      myPath.moveTo(start.x, start.y);
    }

    public Shaper roundUpRight() {
      myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(),
                    getCurrent().y - myBalloon.getArc());
      return this;
    }

    public Shaper roundRightDown() {
      myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(),
                    getCurrent().y + myBalloon.getArc());
      return this;
    }

    public Shaper roundLeftUp() {
      myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(),
                    getCurrent().y - myBalloon.getArc());
      return this;
    }

    public Shaper roundLeftDown() {
      myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(),
                    getCurrent().y + myBalloon.getArc());
      return this;
    }

    public Point getCurrent() {
      return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY());
    }

    public Shaper line(final int deltaX, final int deltaY) {
      myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY);
      return this;
    }

    public Shaper lineTo(final int x, final int y) {
      myPath.lineTo(x, y);
      return this;
    }

    private int getTargetDelta(@JdkConstants.TabPlacement int effectiveSide) {
      return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0;
    }

    public Shaper toRightCurve() {
      myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingConstants.RIGHT) - 1, getCurrent().y);
      return this;
    }

    public Shaper toBottomCurve() {
      myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingConstants.BOTTOM) - 1);
      return this;
    }

    public Shaper toLeftCurve() {
      myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingConstants.LEFT), getCurrent().y);
      return this;
    }

    public Shaper toTopCurve() {
      myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingConstants.TOP));
      return this;
    }

    public void close() {
      myPath.closePath();
    }

    public Shape getShape() {
      return myPath;
    }
  }

  @Override
  public boolean wasFadedIn() {
    return myFadedIn;
  }

  @Override
  public boolean wasFadedOut() {
    return myFadedOut;
  }

  @Override
  public boolean isDisposed() {
    return myDisposed;
  }

  @Override
  public void setTitle(String title) {
    myTitleLabel.setText(title);
  }

  public void setActionProvider(@NotNull ActionProvider actionProvider) {
    myActionProvider = actionProvider;
  }

  @Override
  public RelativePoint getShowingPoint() {
    Point p = myPosition.getShiftedPoint(myTargetPoint, myCalloutShift * -1);
    return new RelativePoint(myLayeredPane, p);
  }

  @Override
  public void setAnimationEnabled(boolean enabled) {
    myAnimationEnabled = enabled;
  }

  public boolean isAnimationEnabled() {
    return myAnimationEnabled && myAnimationCycle > 0 && !RemoteDesktopService.isRemoteSession();
  }

  public boolean isBlockClicks() {
    return myBlockClicks;
  }

  // Returns true if balloon is 'prepared' to process clicks by itself.
  // For example balloon would ignore clicks and won't hide explicitly or would trigger some actions/navigation
  public boolean isClickProcessor() {
    return myClickHandler != null || !myCloseOnClick || isBlockClicks();
  }
}
