/*
 * 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.application.options.colors;

import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.ide.actions.ShowSettingsUtilImpl;
import com.intellij.injected.editor.EditorWindow;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.options.colors.AttributesDescriptor;
import com.intellij.openapi.options.colors.ColorSettingsPage;
import com.intellij.openapi.options.colors.ColorSettingsPages;
import com.intellij.openapi.options.ex.Settings;
import com.intellij.openapi.options.newEditor.SettingsDialog;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.ColoredListCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.components.JBList;
import com.intellij.util.ObjectUtils;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.intellij.ui.SimpleTextAttributes.*;

/**
 * @author gregsh
 */
public class JumpToColorsAndFontsAction extends DumbAwareAction {

  public JumpToColorsAndFontsAction() {
    setInjectedContext(true);
  }

  @Override
  public void update(AnActionEvent e) {
    Project project = e.getData(CommonDataKeys.PROJECT);
    Editor editor = e.getData(CommonDataKeys.EDITOR);
    e.getPresentation().setEnabledAndVisible(project != null && editor != null);
  }

  @Override
  public void actionPerformed(AnActionEvent e) {
    // todo handle ColorKey's as well
    Project project = e.getData(CommonDataKeys.PROJECT);
    Editor editor = e.getData(CommonDataKeys.EDITOR);
    if (project == null || editor == null) return;
    Map<TextAttributesKey, Pair<ColorSettingsPage, AttributesDescriptor>> keyMap = ContainerUtil.newHashMap();
    Processor<RangeHighlighterEx> processor = r -> {
      Object tt = r.getErrorStripeTooltip();
      TextAttributesKey key = tt instanceof HighlightInfo ? ObjectUtils.chooseNotNull(
        ((HighlightInfo)tt).forcedTextAttributesKey, ((HighlightInfo)tt).type.getAttributesKey()) : null;
      Pair<ColorSettingsPage, AttributesDescriptor> p = key == null ? null : ColorSettingsPages.getInstance().getAttributeDescriptor(key);
      if (p != null) keyMap.put(key, p);
      return true;
    };
    JBIterable<Editor> editors = editor instanceof EditorWindow ? JBIterable.of(editor, ((EditorWindow)editor).getDelegate()) : JBIterable.of(editor);
    for (Editor ed : editors) {
      TextRange selection = EditorUtil.getSelectionInAnyMode(ed);
      MarkupModel forDocument = DocumentMarkupModel.forDocument(ed.getDocument(), project, false);
      if (forDocument != null) {
        ((MarkupModelEx)forDocument).processRangeHighlightersOverlappingWith(selection.getStartOffset(), selection.getEndOffset(), processor);
      }
      ((MarkupModelEx)ed.getMarkupModel()).processRangeHighlightersOverlappingWith(selection.getStartOffset(), selection.getEndOffset(), processor);
      EditorHighlighter highlighter = ed instanceof EditorEx ? ((EditorEx)ed).getHighlighter() : null;
      SyntaxHighlighter syntaxHighlighter = highlighter instanceof LexerEditorHighlighter ? ((LexerEditorHighlighter)highlighter).getSyntaxHighlighter() : null;
      if (syntaxHighlighter != null) {
        HighlighterIterator iterator = highlighter.createIterator(selection.getStartOffset());
        while (!iterator.atEnd()) {
          for (TextAttributesKey key : syntaxHighlighter.getTokenHighlights(iterator.getTokenType())) {
            Pair<ColorSettingsPage, AttributesDescriptor> p = key == null ? null : ColorSettingsPages.getInstance().getAttributeDescriptor(key);
            if (p != null) keyMap.put(key, p);
          }
          if (iterator.getEnd() >= selection.getEndOffset()) break;
          iterator.advance();
        }
      }
    }

    if (keyMap.isEmpty()) {
      HintManager.getInstance().showErrorHint(editor, "No text attributes found");
    }
    else if (keyMap.size() == 1) {
      Pair<ColorSettingsPage, AttributesDescriptor> p = keyMap.values().iterator().next();
      if (!openSettingsAndSelectKey(project, p.first, p.second)) {
        HintManager.getInstance().showErrorHint(editor, "No appropriate settings page found");
      }
    }
    else {
      ArrayList<Pair<ColorSettingsPage, AttributesDescriptor>> attrs = ContainerUtil.newArrayList(keyMap.values());
      Collections.sort(attrs, (o1, o2) -> StringUtil.naturalCompare(
        o1.first.getDisplayName() + o1.second.getDisplayName(), o2.first.getDisplayName() + o2.second.getDisplayName()));

      EditorColorsScheme colorsScheme = editor.getColorsScheme();
      JBList<Pair<ColorSettingsPage, AttributesDescriptor>> list = new JBList<>(attrs);
      list.setCellRenderer(new ColoredListCellRenderer<Pair<ColorSettingsPage, AttributesDescriptor>>() {
        @Override
        protected void customizeCellRenderer(@NotNull JList<? extends Pair<ColorSettingsPage, AttributesDescriptor>> list,
                                             Pair<ColorSettingsPage, AttributesDescriptor> value,
                                             int index,
                                             boolean selected,
                                             boolean hasFocus) {
          TextAttributes ta = colorsScheme.getAttributes(value.second.getKey());
          Color fg = ObjectUtils.chooseNotNull(ta.getForegroundColor(), colorsScheme.getDefaultForeground());
          Color bg = ObjectUtils.chooseNotNull(ta.getBackgroundColor(), colorsScheme.getDefaultBackground());
          SimpleTextAttributes sa = fromTextAttributes(ta);
          SimpleTextAttributes saOpaque = sa.derive(STYLE_OPAQUE | sa.getStyle(), fg, bg, null);
          SimpleTextAttributes saSelected = REGULAR_ATTRIBUTES.derive(sa.getStyle(), null, null, null);
          SimpleTextAttributes saCur = REGULAR_ATTRIBUTES;
          List<String> split = StringUtil.split(value.first.getDisplayName() + "//" + value.second.getDisplayName(), "//");
          for (int i = 0, len = split.size(); i < len; i++) {
            boolean last = i == len - 1;
            saCur = !last ? REGULAR_ATTRIBUTES : selected ? saSelected : saOpaque;
            if (last) append(" ", saCur);
            append(split.get(i), saCur);
            if (last) append(" ", saCur);
            else append(" > ", GRAYED_ATTRIBUTES);
          }
          Color stripeColor = ta.getErrorStripeColor();
          boolean addStripe = stripeColor != null && stripeColor != saCur.getBgColor();
          boolean addBoxed = ta.getEffectType() == EffectType.BOXED && ta.getEffectColor() != null;
          if (addBoxed) {
            append("\u25A2" + (addStripe ? "" : " "), saCur.derive(-1, ta.getEffectColor(), null, null));
          }
          if (addStripe) {
            append(" ", saCur.derive(STYLE_OPAQUE, null, stripeColor, null));
          }
        }
      });
      JBPopupFactory.getInstance().createListPopupBuilder(list)
        .setTitle(StringUtil.notNullize(e.getPresentation().getText()))
        .setMovable(false)
        .setResizable(false)
        .setRequestFocus(true)
        .setItemChoosenCallback(() -> {
          Pair<ColorSettingsPage, AttributesDescriptor> p = list.getSelectedValue();
          if (p != null && !openSettingsAndSelectKey(project, p.first, p.second)) {
            HintManager.getInstance().showErrorHint(editor, "No appropriate settings page found");
          }
        })
        .createPopup().showInBestPositionFor(editor);
    }
  }

  private static boolean openSettingsAndSelectKey(@NotNull Project project, @NotNull ColorSettingsPage page, @NotNull AttributesDescriptor descriptor) {
    SettingsDialog dialog = (SettingsDialog)ShowSettingsUtilImpl.getDialog(
      project, ShowSettingsUtilImpl.getConfigurableGroups(project, true), null);
    Settings settings = Settings.KEY.getData(dialog);
    ColorAndFontOptions configurable0 = settings == null ? null : settings.find(ColorAndFontOptions.class);
    SearchableConfigurable configurable = configurable0 == null ? null : configurable0.findSubConfigurable(page.getDisplayName());
    if (configurable == null) return false;
    Runnable runnable = configurable.enableSearch(descriptor.getDisplayName());
    ActionCallback callback = settings.select(configurable);
    if (runnable != null) callback.doWhenDone(runnable);
    dialog.show();
    return true;
  }
}
