// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.editor.ex.util;

import com.intellij.diagnostic.PluginException;
import com.intellij.lexer.FlexAdapter;
import com.intellij.lexer.Lexer;
import com.intellij.lexer.LexerPosition;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * This class verifies that delegate lexer generates a continuous sequence of tokens (without gaps), and that it
 * does not stall during iteration (generating empty tokens for the same offset continuously).
 */
public class ValidatingLexerWrapper extends Lexer {
  private final Lexer myDelegate;
  private int myLastStartOffset;
  private int myLastEndOffset;
  private int myLastState;
  private IElementType myLastTokenType;
  private boolean myLastValuesActual;

  public ValidatingLexerWrapper(@NotNull Lexer delegate) {myDelegate = delegate;}

  @Override
  public @NotNull CharSequence getTokenSequence() {
    return myDelegate.getTokenSequence();
  }

  @Override
  public @NotNull String getTokenText() {
    return myDelegate.getTokenText();
  }

  @Override
  public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
    myDelegate.start(buffer, startOffset, endOffset, initialState);
    myLastValuesActual = false;
  }

  @Override
  public int getState() {
    return myLastValuesActual ? myLastState : myDelegate.getState();
  }

  @Override
  public @Nullable IElementType getTokenType() {
    if (!myLastValuesActual) {
      myLastTokenType = myDelegate.getTokenType();
      if (myLastTokenType != null) {
        myLastStartOffset = myDelegate.getTokenStart();
        myLastEndOffset = myDelegate.getTokenEnd();
        myLastState = myDelegate.getState();
      }
      myLastValuesActual = true;
    }
    return myLastTokenType;
  }

  @Override
  public int getTokenStart() {
    return myLastValuesActual ? myLastStartOffset : myDelegate.getTokenStart();
  }

  @Override
  public int getTokenEnd() {
    return myLastValuesActual ? myLastEndOffset : myDelegate.getTokenEnd();
  }

  @Override
  public void advance() {
    myDelegate.advance();

    int prevStart = 0;
    int prevEnd = 0;
    int prevState = 0;
    IElementType prevType = null;
    if (myLastValuesActual) {
      prevStart = myLastStartOffset;
      prevEnd = myLastEndOffset;
      prevState = myLastState;
      prevType = myLastTokenType;
    }

    myLastValuesActual = false;
    getTokenType(); // cache values

    if (prevType != null && myLastTokenType != null) {
      if (myLastStartOffset > myLastEndOffset) {
        throwException("Incorrect token offsets returned by lexer");
      }
      if (myLastStartOffset != prevEnd) {
        throwException("Discontinuous sequence of tokens is generated by lexer");
      }
      if (myLastEndOffset == myLastStartOffset && prevEnd == prevStart && myLastState == prevState && myLastTokenType == prevType) {
        throwException("Lexer is not progressing after calling advance()");
      }
    }
  }

  @Override
  public @NotNull LexerPosition getCurrentPosition() {
    return myDelegate.getCurrentPosition();
  }

  @Override
  public void restore(@NotNull LexerPosition position) {
    myDelegate.restore(position);
    myLastValuesActual = false;
  }

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

  @Override
  public int getBufferEnd() {
    return myDelegate.getBufferEnd();
  }

  private void throwException(@NotNull String message) {
    Class<? extends Lexer> lexerClass = myDelegate.getClass();
    boolean isFlexAdapter = lexerClass == FlexAdapter.class;
    throw PluginException.createByClass(message + ": " + (isFlexAdapter ? myDelegate.toString() : lexerClass.getName()),
                                        null,
                                        isFlexAdapter ? ((FlexAdapter)myDelegate).getFlex().getClass() : lexerClass);
  }
}
