package com.haskforce.features;

import com.haskforce.HaskellParserDefinition;
import com.haskforce.psi.HaskellFile;
import com.haskforce.psi.HaskellTypes;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.CustomFoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.TokenType;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.Set;

/**
 * Collapses constructs into something small, such as "{- -}".
 */
public class HaskellFoldingBuilder extends CustomFoldingBuilder implements DumbAware {
    @Override
    protected void
    buildLanguageFoldRegions(@NotNull final List<FoldingDescriptor> descriptors,
                                            @NotNull PsiElement root,
                                            @NotNull Document document,
                                            boolean quick) {
        if (!(root instanceof HaskellFile)) return;
        HaskellFile file = (HaskellFile) root;
        final Set<PsiElement> seenComments = ContainerUtil.newHashSet();

        if (!quick) {
            PsiTreeUtil.processElements(file, new PsiElementProcessor() {
                @Override
                public boolean execute(@NotNull PsiElement element) {
                    if (element.getNode().getElementType().equals(HaskellTypes.COMMENT)) {
                        addCommentFolds((PsiComment) element, seenComments, descriptors);
                    } else if (HaskellParserDefinition.COMMENTS.contains(element.getNode().getElementType())) {
                        TextRange range = element.getTextRange();
                        String placeholderText = getPlaceholderText(element.getNode());
                        // Only fold if we actually save space to prevent
                        // assertions from kicking in. Means {- -} will not fold.
                        if (placeholderText != null && range.getLength() > 1 &&
                                range.getLength() > placeholderText.length()) {
                            descriptors.add(new FoldingDescriptor(element, range));
                        }
                    }
                    return true;
                }
            });
        }
    }

    /**
     * Provides the text displayed on folded elements.
     */
    @Override
    protected String
    getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
        IElementType type = node.getElementType();
        if (HaskellTypes.OPENCOM.equals(type)) return "{-";
        // Need two character placeholder for hoovering to work.
        if (HaskellTypes.COMMENTTEXT.equals(type)) return "  ";
        if (HaskellTypes.CLOSECOM.equals(type)) return "-}";
        if (HaskellTypes.COMMENT.equals(type)) return "--";
        return "..";
    }

    /**
     * The default collapsed state for a folding region related to a node.
     */
    @Override
    protected boolean isRegionCollapsedByDefault(@NotNull ASTNode node) {
        return false;
    }

    /**
     * Single out nodes that we custom fold. Comment is the only current
     * element.
     */
    @Override
    protected boolean isCustomFoldingCandidate(@NotNull ASTNode node) {
        return HaskellTypes.COMMENT.equals(node.getElementType());
    }

    // Cut and paste from JavaFoldingBuilderEx.
    private static void
    addCommentFolds(@NotNull PsiComment c,
                    @NotNull Set<PsiElement> processedComments,
                    @NotNull List<FoldingDescriptor> foldElements) {
        if (processedComments.contains(c) ||
                !HaskellTypes.COMMENT.equals(c.getTokenType())) {
            return;
        }

        PsiElement end = null;
        for (PsiElement curr = c.getNextSibling(); curr != null; curr = curr.getNextSibling()) {
            ASTNode node = curr.getNode();
            if (node == null) {
                break;
            }
            IElementType elementType = node.getElementType();
            if (HaskellTypes.COMMENT.equals(elementType)) {
                end = curr;
                // We don't want to process, say, the second comment in case of
                // three subsequent comments when it's being examined during all
                // elements traversal. I.e. we expect to start from the first
                // comment and grab as many subsequent comments as possible
                // during the single iteration.
                processedComments.add(curr);
                continue;
            }
            if (TokenType.WHITE_SPACE.equals(elementType)) {
                continue;
            }
            break;
        }

        if (end != null) {
            TextRange rng = new TextRange(c.getTextRange().getStartOffset(),
                                          end.getTextRange().getEndOffset());
            foldElements.add(new FoldingDescriptor(c, rng));
        }
    }
}