/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.intellij.codeInsight.hint;

import com.google.common.collect.ImmutableMap;
import com.intellij.lang.parameterInfo.ParameterInfoHandler;
import com.intellij.lang.parameterInfo.ParameterInfoUIContextEx;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.ui.*;
import com.intellij.util.Function;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.AccessibleContextUtil;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.util.*;

public class ParameterInfoComponent extends JPanel {
  private final Object[] myObjects;
  private int myCurrentParameterIndex;

  private PsiElement myParameterOwner;
  private Object myHighlighted;
  @NotNull private final ParameterInfoHandler myHandler;

  private final OneElementComponent[] myPanels;

  private final Font NORMAL_FONT;
  private final Font BOLD_FONT;

  private static final Border LAST_ITEM_BORDER = BorderFactory.createEmptyBorder();
  private static final Border BOTTOM_BORDER = new SideBorder(new JBColor(JBColor.LIGHT_GRAY, Gray._90), SideBorder.BOTTOM);

  protected int myWidthLimit = 500;

  private static final Map<ParameterInfoUIContextEx.Flag, String> FLAG_TO_TAG =
    ImmutableMap.of(ParameterInfoUIContextEx.Flag.HIGHLIGHT, "b", ParameterInfoUIContextEx.Flag.DISABLE, "font color=gray",
                    ParameterInfoUIContextEx.Flag.STRIKEOUT, "strike");

  private static final Comparator<TextRange> TEXT_RANGE_COMPARATOR = (o1, o2) -> {
    if (o1.getStartOffset() == o2.getStartOffset()) {
      return o1.getEndOffset() > o2.getEndOffset() ? 1 : -1;
    }
    if (o1.getStartOffset() > o2.getStartOffset()) return 1;
    if (o1.getEndOffset() > o2.getEndOffset()) return 1;
    return -1;
  };
  private boolean myRequestFocus;

  @TestOnly
  public static ParameterInfoUIContextEx createContext(Object[] objects, Editor editor, @NotNull ParameterInfoHandler handler, int currentParameterIndex) {
    return createContext(objects, editor, handler, currentParameterIndex, null);
  }

  @TestOnly
  public static ParameterInfoUIContextEx createContext(Object[] objects, Editor editor, @NotNull ParameterInfoHandler handler, int currentParameterIndex, @Nullable PsiElement parameterOwner) {
    final ParameterInfoComponent infoComponent = new ParameterInfoComponent(objects, editor, handler);
    infoComponent.setCurrentParameterIndex(currentParameterIndex);
    infoComponent.setParameterOwner(parameterOwner);
    return infoComponent.new MyParameterContext();
  }

  ParameterInfoComponent(Object[] objects, Editor editor, @NotNull ParameterInfoHandler handler) {
    this(objects, editor, handler, false);
  }

  ParameterInfoComponent(Object[] objects, Editor editor, @NotNull ParameterInfoHandler handler, boolean requestFocus) {
    super(new BorderLayout());
    myRequestFocus = requestFocus;

    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      JComponent editorComponent = editor.getComponent();
      JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane();
      myWidthLimit = layeredPane.getWidth();
    }

    NORMAL_FONT = UIUtil.getLabelFont();
    BOLD_FONT = NORMAL_FONT.deriveFont(Font.BOLD);

    myObjects = objects;

    setBackground(HintUtil.getInformationColor());

    myHandler = handler;
    myPanels = new OneElementComponent[myObjects.length];
    final JPanel panel = new JPanel(new GridBagLayout());
    for (int i = 0; i < myObjects.length; i++) {
      myPanels[i] = new OneElementComponent();
      panel.add(myPanels[i], new GridBagConstraints(0, i, 1, 1, 1, 0,
                                                    GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
                                                    new Insets(0, 0, 0, 0), 0, 0));
    }
    if (myRequestFocus) {
      AccessibleContextUtil.setName(this, "Parameter Info. Press TAB to navigate through each element. Press ESC to close.");
    }

    final JScrollPane pane = ScrollPaneFactory.createScrollPane(panel);
    pane.setBorder(null);
    pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    add(pane, BorderLayout.CENTER);

    myCurrentParameterIndex = -1;
  }

  @Override
  public Dimension getPreferredSize() {
    int size = myPanels.length;
    final Dimension preferredSize = super.getPreferredSize();
    if (size >= 0 && size <= 20) {
      return preferredSize;
    }
    else {
      return new Dimension(preferredSize.width + 20, 200);
    }
  }

  public Object getHighlighted() {
    return myHighlighted;
  }

  public void setRequestFocus(boolean requestFocus) {
    myRequestFocus = requestFocus;
  }

  public boolean isRequestFocus() {
    return myRequestFocus;
  }

  class MyParameterContext implements ParameterInfoUIContextEx {
    private int i;
    private Function<String, String> myEscapeFunction;

    @Override
    public String setupUIComponentPresentation(String text,
                                               int highlightStartOffset,
                                               int highlightEndOffset,
                                               boolean isDisabled,
                                               boolean strikeout,
                                               boolean isDisabledBeforeHighlight,
                                               Color background) {
      final String resultedText =
        myPanels[i].setup(text, myEscapeFunction, highlightStartOffset, highlightEndOffset, isDisabled, strikeout, isDisabledBeforeHighlight, background);
      myPanels[i].setBorder(isLastParameterOwner() ? LAST_ITEM_BORDER : BOTTOM_BORDER);
      return resultedText;
    }

    @Override
    public String setupUIComponentPresentation(final String[] texts, final EnumSet<Flag>[] flags, final Color background) {
      final String resultedText = myPanels[i].setup(texts, myEscapeFunction, flags, background);
      myPanels[i].setBorder(isLastParameterOwner() ? LAST_ITEM_BORDER : BOTTOM_BORDER);
      return resultedText;
    }

    @Override
    public void setEscapeFunction(@Nullable Function<String, String> escapeFunction) {
      myEscapeFunction = escapeFunction;
    }

    @Override
    public boolean isUIComponentEnabled() {
      return isEnabled(i);
    }

    @Override
    public void setUIComponentEnabled(boolean enabled) {
      setEnabled(i, enabled);
    }

    public boolean isLastParameterOwner() {
      return i == myPanels.length - 1;
    }

    @Override
    public int getCurrentParameterIndex() {
      return myCurrentParameterIndex;
    }

    @Override
    public PsiElement getParameterOwner() {
      return myParameterOwner;
    }

    @Override
    public Color getDefaultParameterColor() {
      Color color = HintUtil.getInformationColor();
      return !myObjects[i].equals(myHighlighted) ? color :
             ColorUtil.isDark(color) ? ColorUtil.brighter(color, 2) : ColorUtil.darker(color, 2);
    }
  }

  public void update() {
    MyParameterContext context = new MyParameterContext();

    for (int i = 0; i < myObjects.length; i++) {
      context.i = i;
      final Object o = myObjects[i];

      //noinspection unchecked
      myHandler.updateUI(o, context);
    }

    invalidate();
    validate();
    repaint();
  }

  public Object[] getObjects() {
    return myObjects;
  }

  void setEnabled(int index, boolean enabled) {
    myPanels[index].setEnabled(enabled);
  }

  boolean isEnabled(int index) {
    return myPanels[index].isEnabled();
  }

  public void setCurrentParameterIndex(int currentParameterIndex) {
    myCurrentParameterIndex = currentParameterIndex;
  }

  public int getCurrentParameterIndex() {
    return myCurrentParameterIndex;
  }

  public void setParameterOwner(PsiElement element) {
    myParameterOwner = element;
  }

  public PsiElement getParameterOwner() {
    return myParameterOwner;
  }

  public void setHighlightedParameter(Object element) {
    myHighlighted = element;
  }

  private class OneElementComponent extends JPanel {
    private OneLineComponent[] myOneLineComponents;

    public OneElementComponent() {
      super(new GridBagLayout());
      myOneLineComponents = new OneLineComponent[0]; //TODO ???
    }

    private String setup(String text,
                         Function<String, String> escapeFunction,
                         int highlightStartOffset,
                         int highlightEndOffset,
                         boolean isDisabled,
                         boolean strikeout,
                         boolean isDisabledBeforeHighlight,
                         Color background) {
      StringBuilder buf = new StringBuilder();
      removeAll();

      String[] lines = UIUtil.splitText(text, getFontMetrics(BOLD_FONT), myWidthLimit, ',');

      myOneLineComponents = new OneLineComponent[lines.length];

      int lineOffset = 0;

      boolean hasHighlighting = highlightStartOffset >= 0 && highlightEndOffset > highlightStartOffset;
      TextRange highlightingRange = hasHighlighting ? new TextRange(highlightStartOffset, highlightEndOffset) : null;

      for (int i = 0; i < lines.length; i++) {
        String line = lines[i];

        myOneLineComponents[i] = new OneLineComponent();

        TextRange lRange = new TextRange(lineOffset, lineOffset + line.length());
        TextRange hr = highlightingRange == null ? null : lRange.intersection(highlightingRange);
        hr = hr == null ? null : hr.shiftRight(-lineOffset);

        String before = escapeString(hr == null ? line : line.substring(0, hr.getStartOffset()), escapeFunction);
        String in = hr == null ? "" : escapeString(hr.substring(line), escapeFunction);
        String after = hr == null ? "" : escapeString(line.substring(hr.getEndOffset(), line.length()), escapeFunction);

        TextRange escapedHighlightingRange = in.isEmpty() ? null : TextRange.create(before.length(), before.length() + in.length());
        buf.append(myOneLineComponents[i].setup(before + in + after, isDisabled, strikeout, background, escapedHighlightingRange));

        if (isDisabledBeforeHighlight) {
          if (highlightStartOffset < 0 || highlightEndOffset > lineOffset) {
            myOneLineComponents[i].setDisabledBeforeHighlight();
          }
        }

        add(myOneLineComponents[i], new GridBagConstraints(0,i,1,1,1,0,GridBagConstraints.WEST,GridBagConstraints.HORIZONTAL,new Insets(0,0,0,0),0,0));

        lineOffset += line.length();
      }
      return buf.toString();
    }

    private String escapeString(String line, Function<String, String> escapeFunction) {
      line = XmlStringUtil.escapeString(line);
      return escapeFunction == null ? line : escapeFunction.fun(line);
    }

    public String setup(final String[] texts,
                        Function<String, String> escapeFunction,
                        final EnumSet<ParameterInfoUIContextEx.Flag>[] flags,
                        final Color background) {
      StringBuilder buf = new StringBuilder();
      removeAll();
      setBackground(background);
      int index = 0;
      int curOffset = 0;
      final ArrayList<OneLineComponent> components = new ArrayList<>();

      Map<TextRange, ParameterInfoUIContextEx.Flag> flagsMap = new TreeMap<>(TEXT_RANGE_COMPARATOR);

      String line = "";
      for (int i = 0; i < texts.length; i++) {
        String paramText = escapeString(texts[i], escapeFunction);
        if (paramText == null) break;
        line += texts[i];
        final EnumSet<ParameterInfoUIContextEx.Flag> flag = flags[i];
        if (flag.contains(ParameterInfoUIContextEx.Flag.HIGHLIGHT)) {
          flagsMap.put(TextRange.create(curOffset, curOffset + paramText.trim().length()), ParameterInfoUIContextEx.Flag.HIGHLIGHT);
        }

        if (flag.contains(ParameterInfoUIContextEx.Flag.DISABLE)) {
          flagsMap.put(TextRange.create(curOffset, curOffset + paramText.trim().length()), ParameterInfoUIContextEx.Flag.DISABLE);
        }

        if (flag.contains(ParameterInfoUIContextEx.Flag.STRIKEOUT)) {
          flagsMap.put(TextRange.create(curOffset, curOffset + paramText.trim().length()), ParameterInfoUIContextEx.Flag.STRIKEOUT);
        }

        curOffset += paramText.length();
        if (line.length() >= 50) {
          final OneLineComponent component = new OneLineComponent();
          buf.append(component.setup(escapeString(line, escapeFunction), flagsMap, background));
          add(component, new GridBagConstraints(0, index, 1, 1, 1, 0, GridBagConstraints.WEST,
                                                                 GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
          index += 1;
          flagsMap.clear();
          curOffset = 0;
          line = "";
          components.add(component);
        }
      }
      final OneLineComponent component = new OneLineComponent();
      buf.append(component.setup(escapeString(line, escapeFunction), flagsMap, background));
      add(component, new GridBagConstraints(0, index, 1, 1, 1, 0, GridBagConstraints.WEST,
                                            GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
      components.add(component);
      myOneLineComponents = components.toArray(new OneLineComponent[components.size()]);
      return buf.toString();
    }
  }

  private class OneLineComponent extends JPanel {
    JLabel myLabel = new JLabel("", SwingConstants.LEFT);
    private boolean isDisabledBeforeHighlight = false;

    private OneLineComponent(){
      super(new GridBagLayout());

      myLabel.setOpaque(true);
      myLabel.setFont(NORMAL_FONT);
      if (myRequestFocus)
        myLabel.setFocusable(true);

      add(myLabel, new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.WEST, GridBagConstraints.NONE,
                                          new Insets(0, 0, 0, 0), 0, 0));
    }

    private String setup(String text,
                         boolean isDisabled,
                         boolean isStrikeout,
                         Color background, @Nullable TextRange range) {
      Map<TextRange, ParameterInfoUIContextEx.Flag> flagsMap = new TreeMap<>(TEXT_RANGE_COMPARATOR);
      if (range != null)
        flagsMap.put(range, ParameterInfoUIContextEx.Flag.HIGHLIGHT);
      if (isDisabled)
        flagsMap.put(TextRange.create(0, text.length()), ParameterInfoUIContextEx.Flag.DISABLE);
      if (isStrikeout)
        flagsMap.put(TextRange.create(0, text.length()), ParameterInfoUIContextEx.Flag.STRIKEOUT);
      return setup(text, flagsMap, background);
    }

    private String setup(@NotNull String text, @NotNull Map<TextRange, ParameterInfoUIContextEx.Flag> flagsMap, @NotNull Color background) {
      myLabel.setBackground(background);
      setBackground(background);

      myLabel.setForeground(JBColor.foreground());

      if (flagsMap.isEmpty()) {
        myLabel.setText(XmlStringUtil.wrapInHtml(text));
      }
      else {
        String labelText = buildLabelText(text, flagsMap);
        myLabel.setText(labelText);
      }

      //IDEA-95904 Darcula parameter info pop-up colors hard to read
      if (UIUtil.isUnderDarcula()) {
        myLabel.setText(myLabel.getText().replace("<b>", "<b color=ffC800>"));
      }
      return myLabel.getText();
    }
    private String buildLabelText(@NotNull final String text, @NotNull final Map<TextRange, ParameterInfoUIContextEx.Flag> flagsMap) {
      final StringBuilder labelText = new StringBuilder(text);
      final String disabledTag = FLAG_TO_TAG.get(ParameterInfoUIContextEx.Flag.DISABLE);

      final Map<Integer, Integer> faultMap = new HashMap<>();
      if (isDisabledBeforeHighlight) {
        final String tag = getTag(disabledTag);
        labelText.insert(0, tag);
        faultMap.put(0, tag.length());
      }

      for (Map.Entry<TextRange, ParameterInfoUIContextEx.Flag> entry : flagsMap.entrySet()) {
        final TextRange highlightRange = entry.getKey();
        final ParameterInfoUIContextEx.Flag flag = entry.getValue();

        final String tagValue = FLAG_TO_TAG.get(flag);
        final String tag = getTag(tagValue);

        int startOffset = highlightRange.getStartOffset();
        int endOffset = highlightRange.getEndOffset() + tag.length();

        for (Map.Entry<Integer, Integer> entry1 : faultMap.entrySet()) {
          if (entry1.getKey() < highlightRange.getStartOffset()) {
            startOffset += entry1.getValue();
          }
          if (entry1.getKey() < highlightRange.getEndOffset()) {
            endOffset += entry1.getValue();
          }
        }

        if (flag == ParameterInfoUIContextEx.Flag.HIGHLIGHT && isDisabledBeforeHighlight) {
          final String disableCloseTag = getClosingTag(disabledTag);
          labelText.insert(startOffset, disableCloseTag);
          faultMap.put(highlightRange.getStartOffset(), disableCloseTag.length());
        }

        labelText.insert(startOffset, tag);
        faultMap.put(highlightRange.getStartOffset(), tag.length());

        final String endTag = getClosingTag(tagValue);
        labelText.insert(endOffset, endTag);
        faultMap.put(highlightRange.getEndOffset(), endTag.length());

      }
      return XmlStringUtil.wrapInHtml(labelText);
    }

    private String getTag(@NotNull final String tagValue) {
      return "<" + tagValue + ">";
    }

    private String getClosingTag(@NotNull final String tagValue) {
      return "</" + tagValue + ">";
    }

    public void setDisabledBeforeHighlight() {
      isDisabledBeforeHighlight = true;
    }
  }
}
