import { StreamLanguage } from '@codemirror/language';

type BibtexState = {
  expectingEntryOpen: boolean;
  expectingEntryKey: boolean;
  expectingValue: boolean;
  inQuotedValue: boolean;
  bracedValueDepth: number;
};

function escapedQuoteAhead(text: string): boolean {
  let slashes = 0;
  for (let index = text.length - 2; index >= 0 && text[index] === '\\'; index -= 1) slashes += 1;
  return slashes % 2 === 1;
}

/**
 * Lightweight BibTeX/BibLaTeX mode for Learnitive Source editing.
 *
 * The parser is deliberately lexical rather than schema-specific so custom
 * entry types/fields keep highlighting instead of being rejected. It handles
 * entry types, citation keys, field names, quoted/braced values, TeX commands
 * inside values, comments, numbers and punctuation.
 */
export const bibtexLanguage = StreamLanguage.define<BibtexState>({
  startState() {
    return {
      expectingEntryOpen: false,
      expectingEntryKey: false,
      expectingValue: false,
      inQuotedValue: false,
      bracedValueDepth: 0,
    };
  },
  token(stream, state): string | null {
    if (stream.eatSpace()) return null;

    if (state.inQuotedValue) {
      if (stream.match(/^\\[A-Za-z@]+\*?/u)) return 'tagName';
      if (stream.match(/^\\./u)) return 'escape';
      const start = stream.pos;
      while (!stream.eol()) {
        const character = stream.next();
        if (character === '"' && !escapedQuoteAhead(stream.string.slice(start, stream.pos))) {
          state.inQuotedValue = false;
          break;
        }
        if (character === '\\') stream.next();
      }
      return 'string';
    }

    if (state.bracedValueDepth > 0) {
      if (stream.match(/^\\[A-Za-z@]+\*?/u)) return 'tagName';
      if (stream.match(/^\\./u)) return 'escape';
      if (stream.peek() === '{') {
        stream.next();
        state.bracedValueDepth += 1;
        return 'bracket';
      }
      if (stream.peek() === '}') {
        stream.next();
        state.bracedValueDepth -= 1;
        return 'bracket';
      }
      if (stream.match('%')) {
        stream.skipToEnd();
        return 'comment';
      }
      while (!stream.eol() && !/[{}\\%]/u.test(stream.peek() ?? '')) stream.next();
      if (stream.pos === stream.start) stream.next();
      return 'string';
    }

    if (stream.match('%')) {
      stream.skipToEnd();
      return 'comment';
    }

    if (stream.match(/^@[A-Za-z][A-Za-z0-9_-]*/u)) {
      state.expectingEntryOpen = true;
      state.expectingEntryKey = false;
      state.expectingValue = false;
      return 'keyword';
    }

    if (state.expectingEntryOpen && /^[{(]$/u.test(stream.peek() ?? '')) {
      stream.next();
      state.expectingEntryOpen = false;
      state.expectingEntryKey = true;
      return 'bracket';
    }

    if (state.expectingEntryKey) {
      if (stream.match(/^[^\s,{}()]+/u)) return 'variableName';
      if (stream.peek() === ',') {
        stream.next();
        state.expectingEntryKey = false;
        return 'punctuation';
      }
    }

    if (stream.match(/^[A-Za-z][A-Za-z0-9_:-]*(?=\s*=)/u)) return 'propertyName';

    if (stream.peek() === '=') {
      stream.next();
      state.expectingValue = true;
      return 'operator';
    }

    if (state.expectingValue && stream.peek() === '"') {
      stream.next();
      state.expectingValue = false;
      state.inQuotedValue = true;
      return 'string';
    }

    if (state.expectingValue && stream.peek() === '{') {
      stream.next();
      state.expectingValue = false;
      state.bracedValueDepth = 1;
      return 'bracket';
    }

    if (state.expectingValue && stream.match(/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)/u)) {
      state.expectingValue = false;
      return 'number';
    }

    if (stream.match(/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)/u)) return 'number';
    if (stream.match(/^#[^\s,{}()]*/u)) return 'operator';
    if (stream.match(/^[=,#]/u)) return 'punctuation';
    if (stream.match(/^[{}()]/u)) return 'bracket';
    if (stream.match(/^\\[A-Za-z@]+\*?/u)) return 'tagName';
    if (stream.match(/^\\./u)) return 'escape';

    stream.next();
    return null;
  },
});
