import { C, INTERNAL, LogCallback, ParseCallback, Range, SIZE_OF_INT, SIZE_OF_RANGE, setModule } from './constants';
import { Language } from './language';
import { marshalRange, unmarshalRange } from './marshal';
import { checkModule, initializeBinding } from './bindings';
import { Tree } from './tree';

/**
 * Options for parsing
 *
 * The `includedRanges` property is an array of {@link Range} objects that
 * represent the ranges of text that the parser should include when parsing.
 *
 * The `progressCallback` property is a function that is called periodically
 * during parsing to check whether parsing should be cancelled.
 *
 * See {@link Parser#parse} for more information.
 */
export interface ParseOptions {
  /**
   * An array of {@link Range} objects that
   * represent the ranges of text that the parser should include when parsing.
   *
   * This sets the ranges of text that the parser should include when parsing.
   * By default, the parser will always include entire documents. This
   * function allows you to parse only a *portion* of a document but
   * still return a syntax tree whose ranges match up with the document
   * as a whole. You can also pass multiple disjoint ranges.
   * If `ranges` is empty, then the entire document will be parsed.
   * Otherwise, the given ranges must be ordered from earliest to latest
   * in the document, and they must not overlap. That is, the following
   * must hold for all `i` < `length - 1`:
   * ```text
   *     ranges[i].end_byte <= ranges[i + 1].start_byte
   * ```
   */
  includedRanges?: Range[];

  /**
   * A function that is called periodically during parsing to check
   * whether parsing should be cancelled. If the progress callback returns
   * `true`, then parsing will be cancelled. You can also use this to instrument
   * parsing and check where the parser is at in the document. The progress callback
   * takes a single argument, which is a {@link ParseState} representing the current
   * state of the parser.
   */
  progressCallback?: (state: ParseState) => void;
}

/**
 * A stateful object that is passed into the progress callback {@link ParseOptions#progressCallback}
 * to provide the current state of the parser.
 */
export interface ParseState {
  /** The byte offset in the document that the parser is at. */
  currentOffset: number;

  /** Indicates whether the parser has encountered an error during parsing. */
  hasError: boolean;
}

/**
 * @internal
 *
 * Global variable for transferring data across the FFI boundary
 */
export let TRANSFER_BUFFER: number;

/**
 * The latest ABI version that is supported by the current version of the
 * library.
 *
 * When Languages are generated by the Tree-sitter CLI, they are
 * assigned an ABI version number that corresponds to the current CLI version.
 * The Tree-sitter library is generally backwards-compatible with languages
 * generated using older CLI versions, but is not forwards-compatible.
 */
export let LANGUAGE_VERSION: number;

/**
 * The earliest ABI version that is supported by the current version of the
 * library.
 */
export let MIN_COMPATIBLE_VERSION: number;

/**
 * A stateful object that is used to produce a {@link Tree} based on some
 * source code.
 */
export class Parser {
  /** @internal */
  private [0] = 0; // Internal handle for WASM

  /** @internal */
  private [1] = 0; // Internal handle for WASM

  /** @internal */
  private logCallback: LogCallback | null = null;

  /** The parser's current language. */
  language: Language | null = null;

  /**
   * This must always be called before creating a Parser.
   *
   * You can optionally pass in options to configure the WASM module, the most common
   * one being `locateFile` to help the module find the `.wasm` file.
   */
  static async init(moduleOptions?: EmscriptenModule) {
    setModule(await initializeBinding(moduleOptions));
    TRANSFER_BUFFER = C._ts_init();
    LANGUAGE_VERSION = C.getValue(TRANSFER_BUFFER, 'i32');
    MIN_COMPATIBLE_VERSION = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
  }

  /**
   * Create a new parser.
   */
  constructor() {
    this.initialize();
  }

  /** @internal */
  initialize() {
    if (!checkModule()) {
      throw new Error("cannot construct a Parser before calling `init()`");
    }
    C._ts_parser_new_wasm();
    this[0] = C.getValue(TRANSFER_BUFFER, 'i32');
    this[1] = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
  }

  /** Delete the parser, freeing its resources. */
  delete() {
    C._ts_parser_delete(this[0]);
    C._free(this[1]);
    this[0] = 0;
    this[1] = 0;
  }

  /**
   * Set the language that the parser should use for parsing.
   *
   * If the language was not successfully assigned, an error will be thrown.
   * This happens if the language was generated with an incompatible
   * version of the Tree-sitter CLI. Check the language's version using
   * {@link Language#version} and compare it to this library's
   * {@link LANGUAGE_VERSION} and {@link MIN_COMPATIBLE_VERSION} constants.
   */
  setLanguage(language: Language | null): this {
    let address: number;
    if (!language) {
      address = 0;
      this.language = null;
    } else if (language.constructor === Language) {
      address = language[0];
      const version = C._ts_language_version(address);
      if (version < MIN_COMPATIBLE_VERSION || LANGUAGE_VERSION < version) {
        throw new Error(
          `Incompatible language version ${version}. ` +
          `Compatibility range ${MIN_COMPATIBLE_VERSION} through ${LANGUAGE_VERSION}.`
        );
      }
      this.language = language;
    } else {
      throw new Error('Argument must be a Language');
    }

    C._ts_parser_set_language(this[0], address);
    return this;
  }

  /**
   * Parse a slice of UTF8 text.
   *
   * @param {string | ParseCallback} callback - The UTF8-encoded text to parse or a callback function.
   *
   * @param {Tree | null} [oldTree] - A previous syntax tree parsed from the same document. If the text of the
   *   document has changed since `oldTree` was created, then you must edit `oldTree` to match
   *   the new text using {@link Tree#edit}.
   *
   * @param {ParseOptions} [options] - Options for parsing the text.
   *  This can be used to set the included ranges, or a progress callback.
   *
   * @returns {Tree | null} A {@link Tree} if parsing succeeded, or `null` if:
   *  - The parser has not yet had a language assigned with {@link Parser#setLanguage}.
   *  - The progress callback returned true.
   */
  parse(
    callback: string | ParseCallback,
    oldTree?: Tree | null,
    options?: ParseOptions,
  ): Tree | null {
    if (typeof callback === 'string') {
      C.currentParseCallback = (index: number) => callback.slice(index);
    } else if (typeof callback === 'function') {
      C.currentParseCallback = callback;
    } else {
      throw new Error('Argument must be a string or a function');
    }

    if (options?.progressCallback) {
      C.currentProgressCallback = options.progressCallback;
    } else {
      C.currentProgressCallback = null;
    }

    if (this.logCallback) {
      C.currentLogCallback = this.logCallback;
      C._ts_parser_enable_logger_wasm(this[0], 1);
    } else {
      C.currentLogCallback = null;
      C._ts_parser_enable_logger_wasm(this[0], 0);
    }

    let rangeCount = 0;
    let rangeAddress = 0;
    if (options?.includedRanges) {
      rangeCount = options.includedRanges.length;
      rangeAddress = C._calloc(rangeCount, SIZE_OF_RANGE);
      let address = rangeAddress;
      for (let i = 0; i < rangeCount; i++) {
        marshalRange(address, options.includedRanges[i]);
        address += SIZE_OF_RANGE;
      }
    }

    const treeAddress = C._ts_parser_parse_wasm(
      this[0],
      this[1],
      oldTree ? oldTree[0] : 0,
      rangeAddress,
      rangeCount
    );

    if (!treeAddress) {
      C.currentParseCallback = null;
      C.currentLogCallback = null;
      C.currentProgressCallback = null;
      return null;
    }

    if (!this.language) {
      throw new Error('Parser must have a language to parse');
    }

    const result = new Tree(INTERNAL, treeAddress, this.language, C.currentParseCallback);
    C.currentParseCallback = null;
    C.currentLogCallback = null;
    C.currentProgressCallback = null;
    return result;
  }

  /**
   * Instruct the parser to start the next parse from the beginning.
   *
   * If the parser previously failed because of a timeout, cancellation,
   * or callback, then by default, it will resume where it left off on the
   * next call to {@link Parser#parse} or other parsing functions.
   * If you don't want to resume, and instead intend to use this parser to
   * parse some other document, you must call `reset` first.
   */
  reset(): void {
    C._ts_parser_reset(this[0]);
  }

  /** Get the ranges of text that the parser will include when parsing. */
  getIncludedRanges(): Range[] {
    C._ts_parser_included_ranges_wasm(this[0]);
    const count = C.getValue(TRANSFER_BUFFER, 'i32');
    const buffer = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
    const result = new Array<Range>(count);

    if (count > 0) {
      let address = buffer;
      for (let i = 0; i < count; i++) {
        result[i] = unmarshalRange(address);
        address += SIZE_OF_RANGE;
      }
      C._free(buffer);
    }

    return result;
  }

  /**
   * @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse}
   *
   * Get the duration in microseconds that parsing is allowed to take.
   *
   * This is set via {@link Parser#setTimeoutMicros}.
   */
  getTimeoutMicros(): number {
    return C._ts_parser_timeout_micros(this[0]);
  }

  /**
   * @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse}
   *
   * Set the maximum duration in microseconds that parsing should be allowed
   * to take before halting.
   *
   * If parsing takes longer than this, it will halt early, returning `null`.
   * See {@link Parser#parse} for more information.
   */
  setTimeoutMicros(timeout: number): void {
    C._ts_parser_set_timeout_micros(this[0], 0, timeout);
  }

  /** Set the logging callback that a parser should use during parsing. */
  setLogger(callback: LogCallback | boolean | null): this {
    if (!callback) {
      this.logCallback = null;
    } else if (typeof callback !== 'function') {
      throw new Error('Logger callback must be a function');
    } else {
      this.logCallback = callback;
    }
    return this;
  }

  /** Get the parser's current logger. */
  getLogger(): LogCallback | null {
    return this.logCallback;
  }
}
