// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package org.jetbrains.kotlin.idea.test;

import com.google.common.collect.Lists;
import com.intellij.codeInsight.daemon.LineMarkerInfo;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.stream.Collectors;

public final class TagsTestDataUtil {
    public static String insertInfoTags(List<LineMarkerInfo> lineMarkers, boolean withDescription, String text) {
        List<LineMarkerTagPoint> lineMarkerPoints = toLineMarkerTagPoints(lineMarkers, withDescription);

        return insertTagsInText(lineMarkerPoints, text, (TagInfo t) -> null);
    }

    public static String insertInfoTags(List<HighlightInfo> highlights, String text) {
        List<HighlightTagPoint> highlightPoints = toHighlightTagPoints(highlights);

        return insertTagsInText(highlightPoints, text, (TagInfo t) -> null);
    }

    public static String generateTextWithCaretAndSelection(@NotNull Editor editor) {
        List<TagInfo> points = new ArrayList<>();
        points.add(new TagInfo<>(editor.getCaretModel().getOffset(), true, "caret"));
        if (editor.getSelectionModel().hasSelection()) {
            points.add(new TagInfo<>(editor.getSelectionModel().getSelectionStart(), true, "selection"));
            points.add(new TagInfo<>(editor.getSelectionModel().getSelectionEnd(), false, "selection"));
        }

        return insertTagsInText(points, editor.getDocument().getText(), (TagInfo t) -> null);
    }

    public static String insertTagsInText(List<? extends TagInfo> tags, String text, Function<TagInfo, String> computeExtraAttributes) {
        StringBuilder builder = new StringBuilder(text);

        // Need to sort tags for inserting them in reverse order to have valid offsets for yet not inserted tags.
        // Can't just sort in reverse order but do sort plus reverse instead to preserve final order for tags with the same offset.
        List<? extends TagInfo> sortedTagPoints = Lists.reverse(tags.stream().sorted().collect(Collectors.toList()));

        // Insert tags into text starting from the end for preventing invalidating previous tags offsets
        for (TagInfo point : sortedTagPoints) {
            String tagText;
            if (point.isStart) {
                String attributesString = point.getAttributesString();
                String extraAttributes = computeExtraAttributes.apply(point);

                String allAttributes;
                if (extraAttributes != null) {
                    allAttributes = attributesString + " " + extraAttributes;
                } else {
                    allAttributes = attributesString;
                }

                String closeSuffix = point.isClosed ? "/" : "";
                if (attributesString.isEmpty()) {
                    tagText = String.format("<%s%s>", point.getName(), closeSuffix);
                } else {
                    tagText = String.format("<%s %s%s>", point.getName(), allAttributes, closeSuffix);
                }
            } else {
                tagText = String.format("</%s>", point.getName());
            }

            builder.insert(point.offset, tagText);
        }

        return builder.toString();
    }

    @NotNull
    public static List<LineMarkerTagPoint> toLineMarkerTagPoints(
            Collection<LineMarkerInfo> lineMarkers,
            boolean withDescription
    ) {
        List<LineMarkerTagPoint> lineMarkerPoints = new ArrayList<>();
        for (LineMarkerInfo markerInfo : lineMarkers) {
            lineMarkerPoints.add(new LineMarkerTagPoint(markerInfo.startOffset, true, markerInfo, withDescription));
            lineMarkerPoints.add(new LineMarkerTagPoint(markerInfo.endOffset, false, markerInfo, withDescription));
        }
        return lineMarkerPoints;
    }

    @NotNull
    public static List<HighlightTagPoint> toHighlightTagPoints(Collection<HighlightInfo> highlights) {
        List<HighlightTagPoint> highlightPoints = new ArrayList<>();
        for (HighlightInfo highlight : highlights) {
            highlightPoints.add(new HighlightTagPoint(highlight.startOffset, true, highlight));
            highlightPoints.add(new HighlightTagPoint(highlight.endOffset, false, highlight));
        }
        return highlightPoints;
    }

    public static class TagInfo<Data> implements Comparable<TagInfo<?>> {
        protected final int offset;
        protected final boolean isStart;
        protected final boolean isClosed;
        protected final boolean isFixed;
        public final Data data;

        public TagInfo(int offset, boolean start, Data data) {
            this(offset, start, false, false, data);
        }

        public TagInfo(int offset, boolean isStart, boolean isClosed, boolean isFixed, Data data) {
            if (isClosed && !isStart) {
                throw new IllegalArgumentException("isClosed should be true only for start tags");
            }

            this.offset = offset;
            this.isStart = isStart;
            this.isClosed = isClosed;
            this.isFixed = isFixed;
            this.data = data;
        }

        @Override
        public int compareTo(@NotNull TagInfo<?> other) {
            if (offset != other.offset) {
                return Integer.compare(offset, other.offset);
            }

            if (isStart != other.isStart) {
                // All "starts" should go after "ends" for same offset
                return isStart ? -1 : 1;
            }

            if (isFixed || other.isFixed) {
                return 0;
            }

            String thisTag = this.getName() + this.getAttributesString();
            String otherTag = other.getName() + other.getAttributesString();

            // Invert order for end tags
            return thisTag.compareTo(otherTag) * (isStart ? -1 : 1);
        }

        @NotNull
        public String getName() {
            return data.toString();
        }

        @NotNull
        public String getAttributesString() {
            return "";
        }
    }

    public static class HighlightTagPoint extends TagInfo<HighlightInfo> {
        private final HighlightInfo highlightInfo;

        private HighlightTagPoint(int offset, boolean start, HighlightInfo info) {
            super(offset, start, info);
            highlightInfo = info;
        }

        @NotNull
        @Override
        public String getName() {
            return highlightInfo.getSeverity().equals(HighlightSeverity.INFORMATION)
                   ? "info"
                   : highlightInfo.getSeverity().getName().toLowerCase(Locale.ROOT);
        }

        @NotNull
        @Override
        public String getAttributesString() {
            if (isStart) {
                if (highlightInfo.getDescription() != null) {
                    return String.format("descr=\"%s\" textAttributesKey=\"%s\"",
                                         sanitizeLineBreaks(highlightInfo.getDescription()),
                                         highlightInfo.forcedTextAttributesKey
                    );
                } else {
                    return String.format("textAttributesKey=\"%s\"", highlightInfo.forcedTextAttributesKey);
                }
            } else {
                return "";
            }
        }
    }

    public static class LineMarkerTagPoint extends TagInfo<LineMarkerInfo> {
        private final boolean withDescription;

        public LineMarkerTagPoint(int offset, boolean start, LineMarkerInfo info, boolean withDescription) {
            super(offset, start, info);
            this.withDescription = withDescription;
        }

        @NotNull
        @Override
        public String getName() {
            return "lineMarker";
        }

        @NotNull
        @Override
        public String getAttributesString() {
            return withDescription ? String.format("descr=\"%s\"", sanitizeLineMarkerTooltip(data.getLineMarkerTooltip())) : "descr=\"*\"";
        }
    }

    private static @NotNull String sanitizeLineMarkerTooltip(@Nullable String originalText) {
        if (originalText == null) return "null";
        String noHtmlTags = StringUtil.removeHtmlTags(originalText);
        return sanitizeLineBreaks(noHtmlTags);
    }

    private static String sanitizeLineBreaks(String originalText) {
        return StringUtil.replace(originalText, "\n", " ");
    }
}
