// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.embedding;

import com.intellij.lexer.Lexer;
import com.intellij.lexer.LexerBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.tree.IElementType;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public abstract class TemplateMasqueradingLexer extends MasqueradingLexer {
  public static final IElementType MINUS_TYPE = new IElementType("MINUS", null);

  public TemplateMasqueradingLexer(@NotNull Lexer delegate) {
    super(delegate);
  }

  protected abstract static class MyLexer extends LexerBase {
    protected static final int LEXING_BY_SELF = 0;
    protected static final int DELEGATE_IS_LEXING_LINE = 1;
    protected static final int DELEGATE_IS_LEXING_BLOCK = 2;
    protected static final int EOF = 3;
    private static final int NON_ZERO_INDENTATION = 4;

    private  static final int STATE_SHIFT = 3;

    protected final int myIndent;
    protected final Lexer myDelegate;

    protected int myStartOffset;
    protected int myEndOffset;
    protected CharSequence myBuffer;

    @MagicConstant(intValues = {LEXING_BY_SELF, DELEGATE_IS_LEXING_LINE, DELEGATE_IS_LEXING_BLOCK, EOF})
    protected int myState;
    private boolean hasIndentation;
    protected IElementType myTokenType;
    protected int myTokenStart;
    protected int myTokenEnd;

    public MyLexer(int indent, Lexer delegateLexer) {
      myIndent = indent;
      myDelegate = delegateLexer;
    }

    protected abstract IElementType getIndentTokenType();
    protected abstract IElementType getEmbeddedContentTokenType();

    protected abstract int getEmbeddedCodeStartMarkerLength();

    protected int getDelegateState(int state) {
      return state;
    }

    @Override
    public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
      myBuffer = buffer;
      myStartOffset = startOffset;
      myEndOffset = endOffset;
      //noinspection MagicConstant
      myState = initialState % ( 1 << STATE_SHIFT);

      if ((myState & NON_ZERO_INDENTATION) != 0)
        throw new IllegalStateException("Cannot restart lexer if there is non-zero indentation.");

      myTokenEnd = startOffset;

      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        myTokenEnd = (myState == DELEGATE_IS_LEXING_LINE)
                     ? findEol(startOffset)
                     : findEndByIndent(startOffset);
        myDelegate.start(buffer, startOffset, myTokenEnd, getDelegateState(initialState >> STATE_SHIFT));
      }
      else
      {
        myDelegate.start(buffer, startOffset, startOffset, getDelegateState(0));
        advance();
      }
    }

    @Override
    public int getState() {
      return (myDelegate.getState() << STATE_SHIFT) + myState + (hasIndentation ? NON_ZERO_INDENTATION : 0);
    }

    @Override
    public @Nullable IElementType getTokenType() {
      if (myState == EOF) {
        return null;
      }
      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        return myDelegate.getTokenType();
      }

      return myTokenType;
    }

    @Override
    public @NotNull String getTokenText() {
      if (myState == EOF) {
        return "";
      }
      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        return myDelegate.getTokenText();
      }

      return super.getTokenText();
    }

    @Override
    public int getTokenStart() {
      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        return myDelegate.getTokenStart();
      }

      return myTokenStart;
    }

    @Override
    public int getTokenEnd() {
      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        return myDelegate.getTokenEnd();
      }

      return myTokenEnd;
    }

    @Override
    public void advance() {
      if (myState == DELEGATE_IS_LEXING_LINE || myState == DELEGATE_IS_LEXING_BLOCK) {
        myDelegate.advance();
        if (myDelegate.getTokenType() == null) {
          myState = LEXING_BY_SELF;
          myTokenEnd = myDelegate.getBufferEnd();
          advance();
        }
      }
      else {
        if (myTokenEnd == myEndOffset) {
          myState = EOF;
          return;
        }

        myTokenStart = myTokenEnd;

        if (StringUtil.isWhiteSpace(myBuffer.charAt(myTokenStart))) {
          myTokenType = getIndentTokenType();
          myTokenEnd = findNonWhitespace(myTokenStart);
          hasIndentation = true;
          return;
        }

        int curIndent = calcIndent(myTokenStart);
        if (curIndent > myIndent) {
          myTokenEnd = findEndByIndent(myTokenStart + 1);
          if (prevLineWithMinusIsEmpty(myTokenStart)) {
            myDelegate.start(myBuffer, myTokenStart, myTokenEnd, myDelegate.getState());
            myState = DELEGATE_IS_LEXING_BLOCK;
          }
          else {
            myTokenType = getEmbeddedContentTokenType();
          }
          hasIndentation = true;
          return;
        }
        hasIndentation = false;

        int embeddedCodeStartMarkerLength = getEmbeddedCodeStartMarkerLength();
        if (embeddedCodeStartMarkerLength > 0) {
          myTokenType = MINUS_TYPE;
          myTokenEnd = myTokenStart + embeddedCodeStartMarkerLength;
        }
        else {
          myTokenEnd = findEol(myTokenStart);
          myDelegate.start(myBuffer, myTokenStart, myTokenEnd, getDelegateState(myDelegate.getState()));
          myState = DELEGATE_IS_LEXING_LINE;
        }
      }
    }

    private boolean prevLineWithMinusIsEmpty(int offset) {
      final int minusOffset = StringUtil.lastIndexOf(myBuffer, '-', myStartOffset, offset);
      if (minusOffset == -1) {
        return false;
      }
      final int eolPos = findEol(minusOffset);
      if (eolPos == -1) {
        return false;
      }
      return StringUtil.isEmptyOrSpaces(myBuffer.subSequence(minusOffset + 1, eolPos));
    }

    protected int findEndByIndent(int offset) {
      while (offset < myEndOffset) {
        if (offset > myStartOffset
            && !StringUtil.isWhiteSpace(myBuffer.charAt(offset))
            && StringUtil.isWhiteSpace(myBuffer.charAt(offset - 1))) {
          int indent = calcIndent(offset);
          if (indent != -1 && indent <= myIndent) {
            return offset;
          }

          offset = findEol(offset);
        }
        else {
          offset++;
        }
      }

      return offset;
    }

    protected int calcIndent(int offset) {
      if (offset == myStartOffset) {
        return myIndent;
      }

      int answer = 0;
      offset--;
      while (offset >= myStartOffset) {
        final char c = myBuffer.charAt(offset);
        if (c == '\n') {
          return answer;
        }

        if (!StringUtil.isWhiteSpace(c)) {
          return -1;
        }

        answer++;
        offset--;
      }
      return answer + myIndent;
    }

    protected int findNonWhitespace(int offset) {
      while (offset < myEndOffset) {
        if (!StringUtil.isWhiteSpace(myBuffer.charAt(offset))) {
          return offset;
        }
        offset++;
      }
      return offset;
    }

    @Override
    public @NotNull CharSequence getBufferSequence() {
      return myBuffer;
    }

    @Override
    public int getBufferEnd() {
      return myEndOffset;
    }

    protected int findEol(int offset) {
      while (offset < myEndOffset) {
        if (myBuffer.charAt(offset) == '\n') {
          return offset;
        }
        offset++;
      }
      return offset;
    }
  }
}
