/*
 * Copyright 2002-2005 Sascha Weinreuter
 *
 * 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 org.intellij.plugins.xpathView.util;

import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.highlighting.HighlightManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import org.intellij.plugins.xpathView.Config;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class HighlighterUtil {
    private static final Key<List<RangeHighlighter>> HIGHLIGHTERS_KEY = Key.create("XPATH_HIGHLIGHTERS");

    private HighlighterUtil() {
    }

    /**
     * Clear all highlighters in an editor that are set up by this class
     *
     * @param editor the editor
     */
    public static void clearHighlighters(final Editor editor) {
        final List<RangeHighlighter> hl = editor.getUserData(HIGHLIGHTERS_KEY);
        if (hl != null) {
            if (purgeInvalidHighlighters(editor, hl)) {
                final HighlightManager mgr = HighlightManager.getInstance(editor.getProject());
                for (Iterator<RangeHighlighter> iterator = hl.iterator(); iterator.hasNext();) {
                    RangeHighlighter highlighter = iterator.next();
                    mgr.removeSegmentHighlighter(editor, highlighter);
                    iterator.remove();
                }
            }
        }
    }

    /**
     * Add a new highlighter to an editor
     *
     * @param editor      the editor
     * @param highlighter the highlighter
     */
    public static void addHighlighter(Editor editor, RangeHighlighter highlighter) {
        List<RangeHighlighter> hl = editor.getUserData(HIGHLIGHTERS_KEY);
        if (hl == null) {
            hl = new LinkedList<>();
            editor.putUserData(HIGHLIGHTERS_KEY, hl);
        } else {
            purgeInvalidHighlighters(editor, hl);
        }
        hl.add(highlighter);
    }

    public static void removeHighlighter(Editor editor, RangeHighlighter h) {
        final HighlightManager mgr = HighlightManager.getInstance(editor.getProject());
        mgr.removeSegmentHighlighter(editor, h);
    }

    public static boolean hasHighlighters(Editor editor) {
        final List<RangeHighlighter> hl = editor.getUserData(HIGHLIGHTERS_KEY);
        if (hl != null) {
            if (hl.isEmpty()) {
                return false;
            }
            return purgeInvalidHighlighters(editor, hl);
        }
        return false;
    }

    private static boolean purgeInvalidHighlighters(Editor editor, List<RangeHighlighter> hl) {
        final Set<RangeHighlighter> set = new ReferenceOpenHashSet<>(editor.getMarkupModel().getAllHighlighters());
        boolean hasHighlighter = false;
        for (Iterator<RangeHighlighter> iterator = hl.iterator(); iterator.hasNext();) {
            final RangeHighlighter h = iterator.next();
            if (!h.isValid() || !set.contains(h)) {
                iterator.remove();
            } else {
                hasHighlighter = true;
            }
        }
        return hasHighlighter;
    }

    public static List<RangeHighlighter> getHighlighters(Editor editor) {
        if (!hasHighlighters(editor)) {
          return Collections.emptyList();
        } else {
            return editor.getUserData(HIGHLIGHTERS_KEY);
        }
    }

    /**
     * Highlight a node in the editor.
     * @param editor the editor
     * @param node the node to be highlighted
     * @param attrs the attributes for the highlighter
     * @param cfg the plugin configuration
     * @return The created highlighter object
     */
    public static RangeHighlighter highlightNode(Editor editor, final PsiElement node, TextAttributes attrs, Config cfg) {
        TextRange range;
        final PsiElement realElement;
        if ((node instanceof XmlTag tag) && cfg.isHighlightStartTagOnly()) {
          realElement = MyPsiUtil.getNameElement(tag);
            range = realElement.getTextRange();
        } else {
            range = node.getTextRange();
            realElement = node;
        }

        // TODO: break at line boundaries
        final ArrayList<RangeHighlighter> highlighters = new ArrayList<>(1);
        final HighlightManager mgr = HighlightManager.getInstance(editor.getProject());
        mgr.addRangeHighlight(editor, range.getStartOffset(), range.getEndOffset(), attrs, false, highlighters);
        final RangeHighlighter rangeHighlighter = highlighters.get(0);

        if (cfg.isAddErrorStripe()) {
            rangeHighlighter.setErrorStripeMarkColor(attrs.getBackgroundColor());
            rangeHighlighter.setErrorStripeTooltip(formatTooltip(editor, realElement));
        } else {
            rangeHighlighter.setErrorStripeMarkColor(null);
        }
        return rangeHighlighter;
    }

    private static String formatTooltip(Editor e, PsiElement element) {
        if (!(element instanceof XmlTag)) {
          final String text = element.getText();
          if ((text == null || text.isEmpty()) && MyPsiUtil.isNameElement(element)) {
            final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class, true);
            if (tag != null) {
              return tag.getName();
            }
          }
          return text;
        }
        // have to use html/preformatted or else the tooltip gets formatted totally weird.

        final int tabSize = CodeStyle.getSettings(element.getProject()).getTabSize(FileTypeManager.getInstance().getFileTypeByExtension("xml"));
        final char[] spaces = new char[tabSize];
        Arrays.fill(spaces, ' ');

        final int textOffset = element.getTextOffset();
        final int lineStartOffset = e.logicalPositionToOffset(new LogicalPosition(e.offsetToLogicalPosition(textOffset).line, 0));
        final CharSequence chars = e.getDocument().getCharsSequence();
        int indent = 0;
        for (int i=lineStartOffset; i<textOffset; i++) {
            if (chars.charAt(i) == ' ') {
                indent++;
            } else if (chars.charAt(i) == '\t') {
                indent += ((indent + tabSize) / tabSize) * tabSize - indent;
            } else {
                break;
            }
        }

        final String text = element.getText().replaceAll("\\t", new String(spaces)).replaceAll("&", "&amp;").replaceAll("<", "&lt;");
        final Pattern indentPattern = Pattern.compile("^(\\s*).+");

        final StringBuilder sb = new StringBuilder("<html><pre>");
        final String[] lines = text.split("\\n");
        for (String line : lines) {
            final Matcher matcher = indentPattern.matcher(line);
            if (matcher.matches()) {
                // strip off the amount of spaces the top-level element is indented with
                line = line.substring(Math.min(matcher.group(1).length(), indent));
            }
            sb.append(line).append("\n");
        }
        return sb.append("</pre></html>").toString();
    }
}
