/*
 * Copyright 2007 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.relaxNG.compact.folding;

import com.intellij.codeInsight.folding.CodeFoldingSettings;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.intellij.plugins.relaxNG.compact.RncElementTypes;
import org.intellij.plugins.relaxNG.compact.RncTokenTypes;
import org.intellij.plugins.relaxNG.compact.psi.RncAnnotation;
import org.intellij.plugins.relaxNG.compact.psi.RncName;
import org.intellij.plugins.relaxNG.compact.psi.util.EscapeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;

public class RncFoldingBuilder implements FoldingBuilder {
  @Override
  public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {

    final ArrayList<FoldingDescriptor> regions = new ArrayList<>();
    process(node, document, regions);

    return !regions.isEmpty()
           ? regions.toArray(FoldingDescriptor.EMPTY_ARRAY)
           : FoldingDescriptor.EMPTY_ARRAY;
  }

  @Override
  public String getPlaceholderText(@NotNull ASTNode node) {
    final IElementType type = node.getElementType();
    if (type == RncTokenTypes.LBRACE) {
      return "{ ... }";
    } else if (isCommentLike(type)) {
      return "# ...";
    } else if (isAnnotation(type)) {
      final RncName element = ((RncAnnotation)node.getPsi()).getNameElement();
      if (element != null) {
        final ASTNode n = element.getNode();
        assert n != null;
        return EscapeUtil.unescapeText(n) + " [ ... ]";
      }
      return "[ ... ]";
    } else {
      return "...";
    }
  }

  private static boolean isAnnotation(IElementType type) {
    return RncElementTypes.ANNOTATION == type || RncElementTypes.ANNOTATION_ELEMENT == type || RncElementTypes.FORWARD_ANNOTATION == type;
  }

  private static boolean isCommentLike(IElementType type) {
    return RncTokenTypes.COMMENTS.contains(type) || RncTokenTypes.DOC_TOKENS.contains(type);
  }

  @Override
  public boolean isCollapsedByDefault(@NotNull ASTNode node) {
    return isCommentLike(node.getElementType()) && CodeFoldingSettings.getInstance().COLLAPSE_DOC_COMMENTS;
  }

  private static void process(@Nullable ASTNode node, Document document, ArrayList<? super FoldingDescriptor> regions) {
    if (node == null) {
      return;
    }

    final ASTNode[] braces = node.getChildren(RncTokenTypes.BRACES);
    if (braces.length == 2) {
      final ASTNode lbrace = braces[0];
      final ASTNode rbrace = braces[1];
      if (shouldFold(lbrace, rbrace, document)) {
        final TextRange range = new TextRange(lbrace.getStartOffset(), rbrace.getTextRange().getEndOffset());
        regions.add(new FoldingDescriptor(lbrace, range));
      }
    } else if (isAnnotation(node.getElementType())) {
      if (isOnDifferentLine(node.getFirstChildNode(), node.getLastChildNode(), document)) {
        regions.add(new FoldingDescriptor(node, node.getTextRange()));
      }
    }

    node = node.getFirstChildNode();
    while (node != null) {
      node = checkNodeAndSiblings(node, RncTokenTypes.DOC_TOKENS, regions, document);

      node = checkNodeAndSiblings(node, RncTokenTypes.COMMENTS, regions, document);

      process(node, document, regions);

      if (node != null) {
        node = node.getTreeNext();
      }
    }
  }

  private static @Nullable ASTNode checkNodeAndSiblings(@Nullable ASTNode node, TokenSet tokens, ArrayList<? super FoldingDescriptor> regions, Document document) {
    if (node != null && tokens.contains(node.getElementType())) {
      final ASTNode start = node;
      ASTNode end = start;

      node = node.getTreeNext();
      if (node != null) {
        do {
          end = node;
          node = node.getTreeNext();
        } while (node != null && tokens.contains(node.getElementType()));
      }
      if (end != start) {
        while (end.getPsi() instanceof PsiWhiteSpace) {
          end = end.getTreePrev();
        }
        if (isOnDifferentLine(start, end, document)) {
          regions.add(new FoldingDescriptor(start, new TextRange(start.getStartOffset(), end.getTextRange().getEndOffset())));
        }
      }
    }
    return node;
  }

  private static boolean shouldFold(ASTNode first, ASTNode second, Document document) {
    if (first.getElementType() != RncTokenTypes.LBRACE) {
      return false;
    } else if (second.getElementType() != RncTokenTypes.RBRACE) {
      return false;
    } else {
      return isOnDifferentLine(first, second, document);
    }
  }

  private static boolean isOnDifferentLine(ASTNode first, ASTNode second, Document document) {
    return document.getLineNumber(first.getStartOffset()) != document.getLineNumber(second.getStartOffset());
  }
}
