/*
 * Copyright 2000-2016 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.codeInsight.documentation;

import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.hint.ElementLocationUtil;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.actions.BaseNavigateToSourceAction;
import com.intellij.ide.actions.ExternalJavaDocAction;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.lang.documentation.ExternalDocumentationProvider;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.ActionButton;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.ColorKey;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.colors.EditorColorsUtil;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.options.FontSize;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.JBColor;
import com.intellij.ui.SideBorder;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.Consumer;
import com.intellij.util.Url;
import com.intellij.util.Urls;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.JBDimension;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.BuiltInServerManager;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.*;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import java.awt.*;
import java.awt.event.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.List;

public class DocumentationComponent extends JPanel implements Disposable, DataProvider {

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

  private static final Color DOCUMENTATION_COLOR = new JBColor(new Color(0xf6f6f6), new Color(0x4d4f51));
  public static final ColorKey COLOR_KEY = ColorKey.createColorKey("DOCUMENTATION_COLOR", DOCUMENTATION_COLOR);

  private static final Highlighter.HighlightPainter LINK_HIGHLIGHTER = new LinkHighlighter();
  @NonNls private static final String DOCUMENTATION_TOPIC_ID = "reference.toolWindows.Documentation";

  private static final int PREFERRED_WIDTH_EM = 37;
  private static final int PREFERRED_HEIGHT_MIN_EM = 7;
  private static final int PREFERRED_HEIGHT_MAX_EM = 20;

  private DocumentationManager myManager;
  private SmartPsiElementPointer myElement;
  private long myModificationCount;

  private static final String QUICK_DOC_FONT_SIZE_PROPERTY = "quick.doc.font.size";

  private final Stack<Context> myBackStack = new Stack<>();
  private final Stack<Context> myForwardStack = new Stack<>();
  private final ActionToolbar myToolBar;
  private volatile boolean myIsEmpty;
  private boolean myIsShown;
  private final JLabel myElementLabel;
  private final MutableAttributeSet myFontSizeStyle = new SimpleAttributeSet();
  private JSlider myFontSizeSlider;
  private final JComponent mySettingsPanel;
  private final MyShowSettingsButton myShowSettingsButton;
  private boolean myIgnoreFontSizeSliderChange;
  private String myEffectiveExternalUrl;
  private final MyDictionary<String, Image> myImageProvider = new MyDictionary<String, Image>() {
    @Override
    public Image get(Object key) {
      if (myManager == null || key == null) return null;
      PsiElement element = getElement();
      if (element == null) return null;
      URL url = (URL)key;
      Image inMemory = myManager.getElementImage(element, url.toExternalForm());
      if (inMemory != null) {
        return inMemory;
      }

      Url parsedUrl = Urls.parseEncoded(url.toExternalForm());
      BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance();
      if (parsedUrl != null && builtInServerManager.isOnBuiltInWebServer(parsedUrl)) {
        try {
          url = new URL(builtInServerManager.addAuthToken(parsedUrl).toExternalForm());
        }
        catch (MalformedURLException e) {
          LOG.warn(e);
        }
      }
      return Toolkit.getDefaultToolkit().createImage(url);
    }
  };

  private static class Context {
    private final SmartPsiElementPointer element;
    private final String text;
    private final String externalUrl;
    private final Rectangle viewRect;
    private final int highlightedLink;

    public Context(SmartPsiElementPointer element, String text, String externalUrl, Rectangle viewRect, int highlightedLink) {
      this.element = element;
      this.text = text;
      this.externalUrl = externalUrl;
      this.viewRect = viewRect;
      this.highlightedLink = highlightedLink;
    }
  }

  private final JScrollPane myScrollPane;
  private final JEditorPane myEditorPane;
  private String myText; // myEditorPane.getText() surprisingly crashes.., let's cache the text
  private final JPanel myControlPanel;
  private boolean myControlPanelVisible;
  private final ExternalDocAction myExternalDocAction;
  private Consumer<PsiElement> myNavigateCallback;
  private int myHighlightedLink = -1;
  private Object myHighlightingTag;

  private JBPopup myHint;

  private final Map<KeyStroke, ActionListener> myKeyboardActions = new HashMap<>();

  @Override
  public boolean requestFocusInWindow() {
    // With a screen reader active, set the focus directly to the editor because
    // it makes it easier for users to read/navigate the documentation contents.
    if (ScreenReader.isActive())
      return myEditorPane.requestFocusInWindow();
    else
      return myScrollPane.requestFocusInWindow();
  }


  @Override
  public void requestFocus() {
    // With a screen reader active, set the focus directly to the editor because
    // it makes it easier for users to read/navigate the documentation contents.
    if (ScreenReader.isActive())
      myEditorPane.requestFocus();
    else
      myScrollPane.requestFocus();
  }

  public DocumentationComponent(final DocumentationManager manager, final AnAction[] additionalActions) {
    myManager = manager;
    myIsEmpty = true;
    myIsShown = false;

    myEditorPane = new JEditorPane(UIUtil.HTML_MIME, "") {
      @Override
      public Dimension getPreferredScrollableViewportSize() {
        int em = myEditorPane.getFont().getSize();
        int prefWidth = PREFERRED_WIDTH_EM * em;
        int prefHeightMin = PREFERRED_HEIGHT_MIN_EM * em;
        int prefHeightMax = PREFERRED_HEIGHT_MAX_EM * em;

        if (getWidth() == 0 || getHeight() == 0) {
          setSize(prefWidth, prefHeightMax);
        }

        Insets ins = myEditorPane.getInsets();
        View rootView = myEditorPane.getUI().getRootView(myEditorPane);
        rootView.setSize(prefWidth, prefHeightMax);  // Necessary! Without this line, the size won't increase when the content does

        int prefHeight = (int)rootView.getPreferredSpan(View.Y_AXIS) + ins.bottom + ins.top +
                         myScrollPane.getHorizontalScrollBar().getMaximumSize().height;
        prefHeight = Math.max(prefHeightMin, Math.min(prefHeightMax, prefHeight));
        return new Dimension(prefWidth, prefHeight);
      }

      {
        enableEvents(AWTEvent.KEY_EVENT_MASK);
      }

      @Override
      protected void processKeyEvent(KeyEvent e) {
        KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
        ActionListener listener = myKeyboardActions.get(keyStroke);
        if (listener != null) {
          listener.actionPerformed(new ActionEvent(DocumentationComponent.this, 0, ""));
          e.consume();
          return;
        }
        super.processKeyEvent(e);
      }

      @Override
      protected void paintComponent(Graphics g) {
        GraphicsUtil.setupAntialiasing(g);
        super.paintComponent(g);
      }

      @Override
      public void setDocument(Document doc) {
        super.setDocument(doc);
        if (doc instanceof StyledDocument) {
          doc.putProperty("imageCache", myImageProvider);
        }
      }
    };
    DataProvider helpDataProvider = new DataProvider() {
      @Override
      public Object getData(@NonNls String dataId) {
        return PlatformDataKeys.HELP_ID.is(dataId) ? DOCUMENTATION_TOPIC_ID : null;
      }
    };
    myEditorPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);
    myText = "";
    myEditorPane.setEditable(false);
    if (ScreenReader.isActive()) {
      // Note: Making the caret visible is merely for convenience
      myEditorPane.getCaret().setVisible(true);
    }
    myEditorPane.setBackground(EditorColorsUtil.getGlobalOrDefaultColor(COLOR_KEY));
    myEditorPane.setEditorKit(UIUtil.getHTMLEditorKit(false));
    myScrollPane = new JBScrollPane(myEditorPane) {
      @Override
      protected void processMouseWheelEvent(MouseWheelEvent e) {
        if (!EditorSettingsExternalizable.getInstance().isWheelFontChangeEnabled() || !EditorUtil.isChangeFontSize(e)) {
          super.processMouseWheelEvent(e);
          return;
        }

        int rotation = e.getWheelRotation();
        if (rotation == 0) return;
        int change = Math.abs(rotation);
        boolean increase = rotation <= 0;
        FontSize newFontSize = getQuickDocFontSize();
        for (; change > 0; change--) {
          if (increase) {
            newFontSize = newFontSize.larger();
          }
          else {
            newFontSize = newFontSize.smaller();
          }
        }

        if (newFontSize == getQuickDocFontSize()) {
          return;
        }

        setQuickDocFontSize(newFontSize);
        applyFontSize();
        setFontSizeSliderSize(newFontSize);
      }
    };
    myScrollPane.setBorder(null);
    myScrollPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);

    final MouseListener mouseAdapter = new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        myManager.requestFocus();
        myShowSettingsButton.hideSettings();
      }
    };
    myEditorPane.addMouseListener(mouseAdapter);
    Disposer.register(this, new Disposable() {
      @Override
      public void dispose() {
        myEditorPane.removeMouseListener(mouseAdapter);
      }
    });

    final FocusListener focusAdapter = new FocusAdapter() {
      @Override
      public void focusLost(FocusEvent e) {
        Component previouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(manager.getProject(getElement()));

        if (previouslyFocused != myEditorPane) {
          if (myHint != null && !myHint.isDisposed()) myHint.cancel();
        }
      }
    };
    myEditorPane.addFocusListener(focusAdapter);

    Disposer.register(this, new Disposable() {
      @Override
      public void dispose() {
        myEditorPane.removeFocusListener(focusAdapter);
      }
    });

    setLayout(new BorderLayout());
    JLayeredPane layeredPane = new JBLayeredPane() {
      @Override
      public void doLayout() {
        final Rectangle r = getBounds();
        for (Component component : getComponents()) {
          if (component instanceof JScrollPane) {
            component.setBounds(0, 0, r.width, r.height);
          }
          else {
            int insets = 2;
            Dimension d = component.getPreferredSize();
            component.setBounds(r.width - d.width - insets, insets, d.width, d.height);
          }
        }
      }

      @Override
      public Dimension getPreferredSize() {
        Dimension editorPaneSize = myEditorPane.getPreferredScrollableViewportSize();
        Dimension controlPanelSize = myControlPanel.getPreferredSize();
        return getSize(editorPaneSize, controlPanelSize);
      }

      @Override
      public Dimension getMinimumSize() {
        Dimension editorPaneSize = new JBDimension(20, 20);
        Dimension controlPanelSize = myControlPanel.getMinimumSize();
        return getSize(editorPaneSize, controlPanelSize);
      }

      private Dimension getSize(Dimension editorPaneSize, Dimension controlPanelSize) {
        return new Dimension(Math.max(editorPaneSize.width, controlPanelSize.width), editorPaneSize.height + controlPanelSize.height);
      }
    };
    layeredPane.add(myScrollPane);
    layeredPane.setLayer(myScrollPane, 0);

    mySettingsPanel = createSettingsPanel();
    layeredPane.add(mySettingsPanel);
    layeredPane.setLayer(mySettingsPanel, JLayeredPane.POPUP_LAYER);
    add(layeredPane, BorderLayout.CENTER);
    setOpaque(true);
    myScrollPane.setViewportBorder(JBScrollPane.createIndentBorder());

    final DefaultActionGroup actions = new DefaultActionGroup();
    final BackAction back = new BackAction();
    final ForwardAction forward = new ForwardAction();
    EditDocumentationSourceAction edit = new EditDocumentationSourceAction();
    actions.add(back);
    actions.add(forward);
    actions.add(myExternalDocAction = new ExternalDocAction());
    actions.add(edit);

    try {
      String backKey = ScreenReader.isActive() ? "alt LEFT" : "LEFT";
      CustomShortcutSet backShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString(backKey),
                                                                KeymapUtil.parseMouseShortcut("button4"));

      String forwardKey = ScreenReader.isActive() ? "alt RIGHT" : "RIGHT";
      CustomShortcutSet forwardShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString(forwardKey),
                                                                   KeymapUtil.parseMouseShortcut("button5"));
      back.registerCustomShortcutSet(backShortcutSet, this);
      forward.registerCustomShortcutSet(forwardShortcutSet, this);
      // mouse actions are checked only for exact component over which click was performed, 
      // so we need to register shortcuts for myEditorPane as well
      back.registerCustomShortcutSet(backShortcutSet, myEditorPane); 
      forward.registerCustomShortcutSet(forwardShortcutSet, myEditorPane);
    }
    catch (InvalidDataException e) {
      LOG.error(e);
    }
    
    myExternalDocAction.registerCustomShortcutSet(CustomShortcutSet.fromString("UP"), this);
    edit.registerCustomShortcutSet(CommonShortcuts.getEditSource(), this);
    if (additionalActions != null) {
      for (final AnAction action : additionalActions) {
        actions.add(action);
        ShortcutSet shortcutSet = action.getShortcutSet();
        if (shortcutSet != null) {
          action.registerCustomShortcutSet(shortcutSet, this);
        }
      }
    }

    new NextLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("TAB"), this);
    new PreviousLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("shift TAB"), this);
    new ActivateLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), this);

    myToolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.JAVADOC_TOOLBAR, actions, true);

    myControlPanel = new JPanel(new BorderLayout(5, 5));
    myControlPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));

    myElementLabel = new JLabel();
    myElementLabel.setMinimumSize(new Dimension(100, 0)); // do not recalculate size according to the text

    myControlPanel.add(myToolBar.getComponent(), BorderLayout.WEST);
    myControlPanel.add(myElementLabel, BorderLayout.CENTER);
    myControlPanel.add(myShowSettingsButton = new MyShowSettingsButton(), BorderLayout.EAST);
    myControlPanelVisible = false;

    final HyperlinkListener hyperlinkListener = new HyperlinkListener() {
      @Override
      public void hyperlinkUpdate(HyperlinkEvent e) {
        HyperlinkEvent.EventType type = e.getEventType();
        if (type == HyperlinkEvent.EventType.ACTIVATED) {
          manager.navigateByLink(DocumentationComponent.this, e.getDescription());
        }
      }
    };
    myEditorPane.addHyperlinkListener(hyperlinkListener);
    Disposer.register(this, new Disposable() {
      @Override
      public void dispose() {
        myEditorPane.removeHyperlinkListener(hyperlinkListener);
      }
    });

    registerActions();

    updateControlState();
  }

  public DocumentationComponent(final DocumentationManager manager) {
    this(manager, null);
  }

  @Override
  public Object getData(@NonNls String dataId) {
    if (DocumentationManager.SELECTED_QUICK_DOC_TEXT.getName().equals(dataId)) {
      // Javadocs often contain &nbsp; symbols (non-breakable white space). We don't want to copy them as is and replace
      // with raw white spaces. See IDEA-86633 for more details.
      String selectedText = myEditorPane.getSelectedText();
      return selectedText == null? null : selectedText.replace((char)160, ' ');
    }

    return null;
  }

  private JComponent createSettingsPanel() {
    JPanel result = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0));
    result.add(new JLabel(ApplicationBundle.message("label.font.size")));
    myFontSizeSlider = new JSlider(SwingConstants.HORIZONTAL, 0, FontSize.values().length - 1, 3);
    myFontSizeSlider.setMinorTickSpacing(1);
    myFontSizeSlider.setPaintTicks(true);
    myFontSizeSlider.setPaintTrack(true);
    myFontSizeSlider.setSnapToTicks(true);
    UIUtil.setSliderIsFilled(myFontSizeSlider, true);
    result.add(myFontSizeSlider);
    result.setBorder(BorderFactory.createLineBorder(JBColor.border(), 1));

    myFontSizeSlider.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent e) {
        if (myIgnoreFontSizeSliderChange) {
          return;
        }
        setQuickDocFontSize(FontSize.values()[myFontSizeSlider.getValue()]);
        applyFontSize();
      }
    });

    String tooltipText = ApplicationBundle.message("quickdoc.tooltip.font.size.by.wheel");
    result.setToolTipText(tooltipText);
    myFontSizeSlider.setToolTipText(tooltipText);
    result.setVisible(false);
    result.setOpaque(true);
    myFontSizeSlider.setOpaque(true);
    return result;
  }

  @NotNull
  public static FontSize getQuickDocFontSize() {
    String strValue = PropertiesComponent.getInstance().getValue(QUICK_DOC_FONT_SIZE_PROPERTY);
    if (strValue != null) {
      try {
        return FontSize.valueOf(strValue);
      }
      catch (IllegalArgumentException iae) {
        // ignore, fall back to default font.
      }
    }
    return FontSize.SMALL;
  }

  public void setQuickDocFontSize(@NotNull FontSize fontSize) {
    PropertiesComponent.getInstance().setValue(QUICK_DOC_FONT_SIZE_PROPERTY, fontSize.toString());
  }

  private void setFontSizeSliderSize(FontSize fontSize) {
    myIgnoreFontSizeSliderChange = true;
    try {
      FontSize[] sizes = FontSize.values();
      for (int i = 0; i < sizes.length; i++) {
        if (fontSize == sizes[i]) {
          myFontSizeSlider.setValue(i);
          break;
        }
      }
    }
    finally {
      myIgnoreFontSizeSliderChange = false;
    }
  }

  public boolean isEmpty() {
    return myIsEmpty;
  }

  public void startWait() {
    myIsEmpty = true;
  }

  private void setControlPanelVisible(boolean visible) {
    if (visible == myControlPanelVisible) return;
    if (visible) {
      add(myControlPanel, BorderLayout.NORTH);
    }
    else {
      remove(myControlPanel);
    }
    myControlPanelVisible = visible;
  }

  public void setHint(JBPopup hint) {
    myHint = hint;
  }

  public JBPopup getHint() {
    return myHint;
  }

  public JComponent getComponent() {
    return myEditorPane;
  }

  @Nullable
  public PsiElement getElement() {
    return myElement != null ? myElement.getElement() : null;
  }

  private void setElement(SmartPsiElementPointer element) {
    myElement = element;
    myModificationCount = getCurrentModificationCount();
  }

  public boolean isUpToDate() {
    return getElement() != null && myModificationCount == getCurrentModificationCount();
  }

  private long getCurrentModificationCount() {
    return myElement != null ? PsiModificationTracker.SERVICE.getInstance(myElement.getProject()).getModificationCount() : -1;
  }

  public void setNavigateCallback(Consumer<PsiElement> navigateCallback) {
    myNavigateCallback = navigateCallback;
  }

  public void setText(String text, @Nullable PsiElement element, boolean clearHistory) {
    setText(text, element, false, clearHistory);
  }

  public void setText(String text, PsiElement element, boolean clean, boolean clearHistory) {
    if (clean && myElement != null) {
      myBackStack.push(saveContext());
      myForwardStack.clear();
    }
    updateControlState();
    setData(element, text, clearHistory, null);
    if (clean) {
      myIsEmpty = false;
    }

    if (clearHistory) clearHistory();
  }

  public void replaceText(String text, PsiElement element) {
    PsiElement current = getElement();
    if (current == null || !current.getManager().areElementsEquivalent(current, element)) return;
    setText(text, element, false);
    if (!myBackStack.empty()) myBackStack.pop();
  }

  private void clearHistory() {
    myForwardStack.clear();
    myBackStack.clear();
  }

  public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl) {
    setData(_element, text, clearHistory, effectiveExternalUrl, null);
  }
  
  public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl, String ref) {
    if (myElement != null) {
      myBackStack.push(saveContext());
      myForwardStack.clear();
    }
    myEffectiveExternalUrl = effectiveExternalUrl;

    final SmartPsiElementPointer element = _element != null && _element.isValid()
                                           ? SmartPointerManager.getInstance(_element.getProject()).createSmartPsiElementPointer(_element)
                                           : null;

    if (element != null) {
      setElement(element);
    }

    myIsEmpty = false;
    updateControlState();
    setDataInternal(element, text, new Rectangle(0, 0), ref);

    if (clearHistory) clearHistory();
  }

  private void setDataInternal(SmartPsiElementPointer element, String text, final Rectangle viewRect, final String ref) {
    setElement(element);
    
    highlightLink(-1);

    myEditorPane.setText(text);
    applyFontSize();

    if (!myIsShown && myHint != null && !ApplicationManager.getApplication().isUnitTestMode()) {
      myManager.showHint(myHint);
      myIsShown = true;
    }

    myText = text;

    //noinspection SSBasedInspection
    SwingUtilities.invokeLater(() -> {
      myEditorPane.scrollRectToVisible(viewRect); // if ref is defined but is not found in document, this provides a default location
      if (ref != null) {
        myEditorPane.scrollToReference(ref);
      } else if (ScreenReader.isActive()) {
        myEditorPane.setCaretPosition(0);
      }});
  }

  private void applyFontSize() {
    Document document = myEditorPane.getDocument();
    if (!(document instanceof StyledDocument)) {
      return;
    }

    final StyledDocument styledDocument = (StyledDocument)document;

    EditorColorsManager colorsManager = EditorColorsManager.getInstance();
    EditorColorsScheme scheme = colorsManager.getGlobalScheme();
    StyleConstants.setFontSize(myFontSizeStyle, JBUI.scale(getQuickDocFontSize().getSize()));
    if (Registry.is("documentation.component.editor.font")) {
      StyleConstants.setFontFamily(myFontSizeStyle, scheme.getEditorFontName());
    }

    ApplicationManager.getApplication().executeOnPooledThread(
      () -> styledDocument.setCharacterAttributes(0, styledDocument.getLength(), myFontSizeStyle, false));
  }

  private void goBack() {
    if (myBackStack.isEmpty()) return;
    Context context = myBackStack.pop();
    myForwardStack.push(saveContext());
    restoreContext(context);
    updateControlState();
  }

  private void goForward() {
    if (myForwardStack.isEmpty()) return;
    Context context = myForwardStack.pop();
    myBackStack.push(saveContext());
    restoreContext(context);
    updateControlState();
  }

  private Context saveContext() {
    Rectangle rect = myScrollPane.getViewport().getViewRect();
    return new Context(myElement, myText, myEffectiveExternalUrl, rect, myHighlightedLink);
  }

  private void restoreContext(Context context) {
    setDataInternal(context.element, context.text, context.viewRect, null);
    myEffectiveExternalUrl = context.externalUrl;
    if (myNavigateCallback != null) {
      final PsiElement element = context.element.getElement();
      if (element != null) {
        myNavigateCallback.consume(element);
      }
    }
    highlightLink(context.highlightedLink);
  }

  private void updateControlState() {
    ElementLocationUtil.customizeElementLabel(myElement != null ? myElement.getElement() : null, myElementLabel);
    myToolBar.updateActionsImmediately(); // update faster
    setControlPanelVisible(true);//(!myBackStack.isEmpty() || !myForwardStack.isEmpty());
  }

  private class BackAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    public BackAction() {
      super(CodeInsightBundle.message("javadoc.action.back"), null, AllIcons.Actions.Back);
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      goBack();
    }

    @Override
    public void update(AnActionEvent e) {
      Presentation presentation = e.getPresentation();
      presentation.setEnabled(!myBackStack.isEmpty());
    }
  }

  private class ForwardAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    public ForwardAction() {
      super(CodeInsightBundle.message("javadoc.action.forward"), null, AllIcons.Actions.Forward);
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      goForward();
    }

    @Override
    public void update(AnActionEvent e) {
      Presentation presentation = e.getPresentation();
      presentation.setEnabled(!myForwardStack.isEmpty());
    }
  }

  private class EditDocumentationSourceAction extends BaseNavigateToSourceAction {

    private EditDocumentationSourceAction() {
      super(true);
      getTemplatePresentation().setIcon(AllIcons.Actions.EditSource);
      getTemplatePresentation().setText("Edit Source");
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      super.actionPerformed(e);
      final JBPopup hint = myHint;
      if (hint != null && hint.isVisible()) {
        hint.cancel();
      }
    }

    @Nullable
    @Override
    protected Navigatable[] getNavigatables(DataContext dataContext) {
      SmartPsiElementPointer element = myElement;
      if (element != null) {
        PsiElement psiElement = element.getElement();
        return psiElement instanceof Navigatable ? new Navigatable[] {(Navigatable)psiElement} : null;
      }
      return null;
    }
  }


  private class ExternalDocAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    private ExternalDocAction() {
      super(CodeInsightBundle.message("javadoc.action.view.external"), null, AllIcons.Actions.Browser_externalJavaDoc);
      registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), null);
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      if (myElement == null) {
        return;
      }

      final PsiElement element = myElement.getElement();
      final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
      
      ExternalJavaDocAction.showExternalJavadoc(element, originalElement, myEffectiveExternalUrl, e.getDataContext());
    }

    @Override
    public void update(AnActionEvent e) {
      final Presentation presentation = e.getPresentation();
      presentation.setEnabled(false);
      if (myElement != null) {
        final PsiElement element = myElement.getElement();
        final DocumentationProvider provider = DocumentationManager.getProviderFromElement(element);
        final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
        if (provider instanceof ExternalDocumentationProvider) {
          presentation.setEnabled(element != null && ((ExternalDocumentationProvider)provider).hasDocumentationFor(element, originalElement));
        }
        else {
          final List<String> urls = provider.getUrlFor(element, originalElement);
          presentation.setEnabled(element != null && urls != null && !urls.isEmpty());
        }
      }
    }
  }

  private void registerActions() {
    myExternalDocAction
      .registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), myEditorPane);

    // With screen readers, we want the default keyboard behavior inside
    // the document text editor, i.e. the caret moves with cursor keys, etc.
    if (!ScreenReader.isActive()) {
      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
          value = Math.max(value, 0);
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
          value = Math.min(value, scrollBar.getMaximum());
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
          int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
          value = Math.max(value, 0);
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
          int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
          value = Math.min(value, scrollBar.getMaximum());
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          int value = scrollBar.getValue() - scrollBar.getBlockIncrement(-1);
          value = Math.max(value, 0);
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          int value = scrollBar.getValue() + scrollBar.getBlockIncrement(+1);
          value = Math.min(value, scrollBar.getMaximum());
          scrollBar.setValue(value);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
          scrollBar.setValue(0);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
          scrollBar.setValue(scrollBar.getMaximum());
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_MASK), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          scrollBar.setValue(0);
        }
      });

      myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.CTRL_MASK), new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
          scrollBar.setValue(scrollBar.getMaximum());
        }
      });
    }
  }

  public String getText() {
    return myText;
  }

  @Override
  public void dispose() {
    myBackStack.clear();
    myForwardStack.clear();
    myKeyboardActions.clear();
    myElement = null;
    myManager = null;
    myHint = null;
    myNavigateCallback = null;
  }

  private int getLinkCount() {
    HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
    int linkCount = 0;
    for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
      if (it.getAttributes().isDefined(HTML.Attribute.HREF)) linkCount++;
    }
    return linkCount;
  }

  @Nullable
  private HTMLDocument.Iterator getLink(int n) {
    if (n >= 0) {
      HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
      int linkCount = 0;
      for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
        if (it.getAttributes().isDefined(HTML.Attribute.HREF) && linkCount++ == n) return it;
      }
    }
    return null;
  }
  
  private void highlightLink(int n) {
    myHighlightedLink = n;
    Highlighter highlighter = myEditorPane.getHighlighter();
    HTMLDocument.Iterator link = getLink(n);
    if (link != null) {
      int startOffset = link.getStartOffset();
      int endOffset = link.getEndOffset();
      try {
        if (myHighlightingTag == null) {
          myHighlightingTag = highlighter.addHighlight(startOffset, endOffset, LINK_HIGHLIGHTER);
        }
        else {
          highlighter.changeHighlight(myHighlightingTag, startOffset, endOffset);
        }
        myEditorPane.setCaretPosition(startOffset);
      }
      catch (BadLocationException e) {
        LOG.warn("Error highlighting link", e);
      }
    }
    else if (myHighlightingTag != null) {
      highlighter.removeHighlight(myHighlightingTag);
      myHighlightingTag = null;
    }
  }

  private void activateLink(int n) {
    HTMLDocument.Iterator link = getLink(n);
    if (link != null) {
      String href = (String)link.getAttributes().getAttribute(HTML.Attribute.HREF);
      myManager.navigateByLink(this, href);
    }
  }

  private class MyShowSettingsButton extends ActionButton {

    private MyShowSettingsButton() {
      this(new MyShowSettingsAction(), new Presentation(), ActionPlaces.JAVADOC_INPLACE_SETTINGS, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
    }

    private MyShowSettingsButton(AnAction action, Presentation presentation, String place, @NotNull Dimension minimumSize) {
      super(action, presentation, place, minimumSize);
      myPresentation.setIcon(AllIcons.General.SecondaryGroup);
    }

    private void hideSettings() {
      if (!mySettingsPanel.isVisible()) {
        return;
      }
      AnActionEvent event = AnActionEvent.createFromDataContext(myPlace, myPresentation, DataContext.EMPTY_CONTEXT);
      myAction.actionPerformed(event);
    }
  }

  private class MyShowSettingsAction extends ToggleAction {

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySettingsPanel.isVisible();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      if (!state) {
        mySettingsPanel.setVisible(false);
        return;
      }

      setFontSizeSliderSize(getQuickDocFontSize());
      mySettingsPanel.setVisible(true);
    }
  }

  private abstract static class MyDictionary<K, V> extends Dictionary<K, V> {
    @Override
    public int size() {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean isEmpty() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Enumeration<K> keys() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Enumeration<V> elements() {
      throw new UnsupportedOperationException();
    }

    @Override
    public V put(K key, V value) {
      throw new UnsupportedOperationException();
    }

    @Override
    public V remove(Object key) {
      throw new UnsupportedOperationException();
    }
  }

  private class PreviousLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    @Override
    public void actionPerformed(AnActionEvent e) {
      int linkCount = getLinkCount();
      if (linkCount <= 0) return;
      highlightLink(myHighlightedLink < 0 ? (linkCount - 1) : (myHighlightedLink + linkCount - 1) % linkCount);
    }
  }

  private class NextLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    @Override
    public void actionPerformed(AnActionEvent e) {
      int linkCount = getLinkCount();
      if (linkCount <= 0) return;
      highlightLink((myHighlightedLink + 1) % linkCount);
    }
  }

  private class ActivateLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
    @Override
    public void actionPerformed(AnActionEvent e) {
      activateLink(myHighlightedLink);
    }
  }
  
  private static class LinkHighlighter implements Highlighter.HighlightPainter {
    private static final Stroke STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{1}, 0);
    
    @Override
    public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
      try {
        Rectangle target = c.getUI().getRootView(c).modelToView(p0, Position.Bias.Forward, p1, Position.Bias.Backward, bounds).getBounds();
        Graphics2D g2d = (Graphics2D)g.create();
        try {
          g2d.setStroke(STROKE);
          g2d.setColor(c.getSelectionColor());
          g2d.drawRect(target.x, target.y, target.width - 1, target.height - 1);
        }
        finally {
          g2d.dispose();
        }
      }
      catch (Exception e) {
        LOG.warn("Error painting link highlight", e);
      }
    }
  }
}
