package com.intellij.indentation;

import com.intellij.lang.ASTNode;
import consulo.lang.LanguageVersion;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.PsiParser;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author oleg
 */
public abstract class IndentationParser implements PsiParser {
  @NotNull
  private final IElementType myEolTokenType;
  @NotNull
  private final IElementType myIndentTokenType;
  @NotNull
  private final IElementType myBlockElementType;
  @NotNull
  private final IElementType myDocumentType;

  public IndentationParser(
    @NotNull IElementType documentType,
    @NotNull final IElementType blockElementType,
    @NotNull final IElementType eolTokenType,
    @NotNull final IElementType indentTokenType)
  {
    myDocumentType = documentType;
    myBlockElementType = blockElementType;
    myEolTokenType = eolTokenType;
    myIndentTokenType = indentTokenType;
  }

  @Override
  @NotNull
  public final ASTNode parse(@NotNull final IElementType root, @NotNull final PsiBuilder builder, @NotNull LanguageVersion languageVersion) {
    final PsiBuilder.Marker fileMarker = builder.mark();
    final PsiBuilder.Marker documentMarker = builder.mark();

    final Stack<BlockInfo> stack = new Stack<BlockInfo>();
    stack.push(new BlockInfo(0, builder.mark(), builder.getTokenType()));

    PsiBuilder.Marker startLineMarker = null;
    int currentIndent = 0;
    boolean eolSeen = false;

    while (!builder.eof()) {
      final IElementType type = builder.getTokenType();
      // EOL
      if (type == myEolTokenType) {
        // Handle variant with several EOLs
        if (startLineMarker == null) {
          startLineMarker = builder.mark();
        }
        eolSeen = true;
      }
      else {
        if (type == myIndentTokenType) {
          //noinspection ConstantConditions
          currentIndent = builder.getTokenText().length();
        }
        else {
          if (!eolSeen && !stack.isEmpty() && currentIndent > 0 && currentIndent < stack.peek().getIndent()) {
            // sometimes we do not have EOL between indents
            eolSeen = true;
          }
          if (eolSeen) {
            if (startLineMarker != null) {
              startLineMarker.rollbackTo();
              startLineMarker = null;
            }
            // Close indentation blocks
            while (!stack.isEmpty() && currentIndent < stack.peek().getIndent()) {
              final BlockInfo blockInfo = stack.pop();
              closeBlock(builder, blockInfo.getMarker(), blockInfo.getStartTokenType());
            }

            if (!stack.isEmpty()) {
              final BlockInfo blockInfo = stack.peek();
              if (currentIndent >= blockInfo.getIndent()) {
                if (currentIndent == blockInfo.getIndent()) {
                  final BlockInfo info = stack.pop();
                  closeBlock(builder, info.getMarker(), info.getStartTokenType());
                }
                passEOLsAndIndents(builder);
                stack.push(new BlockInfo(currentIndent, builder.mark(), type));
              }
            }
            eolSeen = false;
            currentIndent = 0;
          }
        }
      }
      advanceLexer(builder);
    }

    // Close all left opened markers
    if (startLineMarker != null){
      startLineMarker.drop();
    }
    while (!stack.isEmpty()){
      final BlockInfo blockInfo = stack.pop();
      closeBlock(builder, blockInfo.getMarker(), blockInfo.getStartTokenType());
    }

    documentMarker.done(myDocumentType);
    fileMarker.done(root);
    return builder.getTreeBuilt();
  }

  protected void closeBlock(final @NotNull PsiBuilder builder,
                            final @NotNull PsiBuilder.Marker marker,
                            final @Nullable IElementType startTokenType)
  {
    marker.done(myBlockElementType);
  }

  protected void advanceLexer(@NotNull PsiBuilder builder) {
    builder.advanceLexer();
  }

  private void passEOLsAndIndents(@NotNull final PsiBuilder builder) {
    IElementType tokenType = builder.getTokenType();
    while (tokenType == myEolTokenType || tokenType == myIndentTokenType) {
      builder.advanceLexer();
      tokenType = builder.getTokenType();
    }
  }

  private static final class BlockInfo {
    private final int myIndent;
    @NotNull
    private final PsiBuilder.Marker myMarker;
    @Nullable
    private final IElementType myStartTokenType;

    private BlockInfo(final int indent, final @NotNull PsiBuilder.Marker marker, final @Nullable IElementType type) {
      myIndent = indent;
      myMarker = marker;
      myStartTokenType = type;
    }

    public int getIndent() {
      return myIndent;
    }

    @NotNull
    public PsiBuilder.Marker getMarker() {
      return myMarker;
    }

    @Nullable
    public IElementType getStartTokenType() {
      return myStartTokenType;
    }
  }
}

@@ -62,16 +62,53 @@
 STNode;%0A
+import consulo.annotations.Exported;%0A
 import c
@@ -405,16 +405,26 @@
 leg%0A */%0A
+@Exported%0A
 public a
@@ -649,38 +649,39 @@
 lementType;%0A  @N
-otN
 ull
+able
 %0A  private final
@@ -738,29 +738,25 @@
 nParser(
-%0A    @NotNull
+@Nullable
  IElemen
@@ -767,32 +767,55 @@
 e documentType,%0A
+                       
     @NotNull fin
@@ -803,38 +803,32 @@
        @NotNull 
-final 
 IElementType blo
@@ -838,24 +838,47 @@
 lementType,%0A
+                       
     @NotNull
@@ -870,38 +870,32 @@
        @NotNull 
-final 
 IElementType eol
@@ -901,24 +901,47 @@
 lTokenType,%0A
+                       
     @NotNull
@@ -933,38 +933,32 @@
        @NotNull 
-final 
 IElementType ind
@@ -962,34 +962,32 @@
 indentTokenType)
-%0A 
  %7B%0A    myDocumen
@@ -1390,32 +1390,64 @@
 documentMarker =
+ myDocumentType == null ? null :
  builder.mark();
@@ -1493,25 +1493,16 @@
 w Stack%3C
-BlockInfo
 %3E();%0A   
@@ -3537,16 +3537,17 @@
 != null)
+ 
 %7B%0A      
@@ -3604,16 +3604,17 @@
 Empty())
+ 
 %7B%0A      
@@ -3738,24 +3738,60 @@
 ());%0A    %7D%0A%0A
+    if (documentMarker != null) %7B%0A  
     document
@@ -3819,16 +3819,22 @@
 tType);%0A
+    %7D%0A
     file
@@ -3958,36 +3958,8 @@
 der,
-%0A                           
  fin
@@ -3999,36 +3999,8 @@
 ker,
-%0A                           
  fin
@@ -4040,18 +4040,16 @@
 kenType)
-%0A 
  %7B%0A    m

allow doc type null