/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 * @format
 */

'use strict';

import type {Program} from 'flow-estree';
import type {ParserOptions} from './ParserOptions';
import type {FlowParserProgram} from './FlowParserDeserializer';
import type {FlowParserWASM as FlowParserWASMType} from './FlowParserWASM';

import FlowParserDeserializer from './FlowParserDeserializer';
import FlowParserWASMModule from './FlowParserWASM';
import {getModuleDocblock} from './getModuleDocblock';

type FlowParse = (
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
) => number;

type WireSourceLocation = {
  source?: ?string,
  start?: unknown,
  end?: unknown,
  rangeStart?: number,
  rangeEnd?: number,
  [string]: unknown,
};

type WireNode = {
  loc?: WireSourceLocation,
  range?: [number, number],
  [string]: unknown,
};

let FlowParserWASM: FlowParserWASMType;
let flowParse: FlowParse;
let flowParseResult_free: number => void;
let flowParseResult_getError: number => string;
let flowParseResult_getErrorLine: number => number;
let flowParseResult_getErrorColumn: number => number;
let flowParseResult_getProgramBuffer: number => number;
let flowParseResult_getPositionBuffer: number => number;
let flowParseResult_getPositionBufferSize: number => number;
let flowParseResult_getStringBuffer: number => number;

/**
 * Init the WASM wrapper code generated by `emscripten` for the
 * Flow Rust parser WASM binary.
 */
function initFlowParserWASM(): void {
  if (FlowParserWASM != null) {
    return;
  }

  FlowParserWASM = FlowParserWASMModule({
    quit(_status, toThrow) {
      throw toThrow;
    },
  });

  // The Flow Rust WASM binary exports an extended C ABI: the original
  // Hermes signature plus a (sourceFilename, sourceFilenameSize) pair that
  // lets the parser switch on file extension (.flow / .d.ts / .d.cts /
  // .d.mts) for ambient-context handling, plus an explicit
  // `enableDecorators` flag (Stage-1 decorator syntax) so fixtures that
  // disable decorators can exercise the unexpected-`@` error path, an
  // `assertOperator` flag to enable Flow's `expr!` non-null assertion,
  // `enableEnums` / `enableRecords` flags that mirror OCaml's
  // `default_parse_options` (both off by default; fixtures that use enum
  // or record syntax opt in via their `.options.json`), an `enableTypes`
  // flag that mirrors OCaml's `types` option (on by default; fixtures that
  // need to exercise the no-type-grammar path opt out via `types: false`),
  // a `sourceType` integer (0=unspecified, 1=script, 2=module), and a final
  // zero to keep Flow's legacy comment syntax disabled. The Rust parser does
  // docblock `@flow` pragma detection internally when
  // `enableTypesPragmaDetection` is set; the JS adapter `parse()` resolves
  // `flow: 'detect'` to that flag.
  flowParse = FlowParserWASM.cwrap('hermesParse', 'number', [
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
    'number',
  ]);

  flowParseResult_free = FlowParserWASM.cwrap(
    'hermesParseResult_free',
    'void',
    ['number'],
  );

  flowParseResult_getError = FlowParserWASM.cwrap(
    'hermesParseResult_getError',
    'string',
    ['number'],
  );

  flowParseResult_getErrorLine = FlowParserWASM.cwrap(
    'hermesParseResult_getErrorLine',
    'number',
    ['number'],
  );

  flowParseResult_getErrorColumn = FlowParserWASM.cwrap(
    'hermesParseResult_getErrorColumn',
    'number',
    ['number'],
  );

  flowParseResult_getProgramBuffer = FlowParserWASM.cwrap(
    'hermesParseResult_getProgramBuffer',
    'number',
    ['number'],
  );

  flowParseResult_getPositionBuffer = FlowParserWASM.cwrap(
    'hermesParseResult_getPositionBuffer',
    'number',
    ['number'],
  );

  flowParseResult_getPositionBufferSize = FlowParserWASM.cwrap(
    'hermesParseResult_getPositionBufferSize',
    'number',
    ['number'],
  );

  flowParseResult_getStringBuffer = FlowParserWASM.cwrap(
    'hermesParseResult_getStringBuffer',
    'number',
    ['number'],
  );
}

// Copy a string into the WASM heap and null-terminate
function copyToHeap(buffer: Buffer, addr: number): void {
  FlowParserWASM.HEAP8.set(buffer, addr);
  FlowParserWASM.HEAP8[addr + buffer.length] = 0;
}

// Coerce any option value to the 0/1 expected by the C ABI. Anything other
// than the literal boolean `true` is treated as `false` so callers can't
// accidentally pass `undefined`/`null`/strings and get NaN at the cwrap
// boundary.
function flag(v: unknown): 0 | 1 {
  return v === true ? 1 : 0;
}

function parse(source: string, options: ParserOptions): Program {
  initFlowParserWASM();

  const sourceBuffer = Buffer.from(source, 'utf8');
  // Plumb the source filename into WASM so the parser can inspect file
  // extensions (.flow / .d.ts / .d.cts / .d.mts) for ambient context. When
  // no filename is provided we pass NULL/0 — the parser falls back to its
  // non-ambient defaults.
  const filename =
    typeof options.sourceFilename === 'string' ? options.sourceFilename : '';
  const filenameBuffer =
    filename.length > 0 ? Buffer.from(filename, 'utf8') : null;

  // All wasm allocations live inside a single try/finally so a throw from
  // any malloc, copyToHeap, or the parse call itself frees everything we
  // managed to allocate. Earlier versions allocated and partially populated
  // before entering the try, so a throw from copyToHeap leaked the wasm
  // address back to the heap.
  let sourceAddr = 0;
  let filenameAddr = 0;
  let filenameLen = 0;
  let parseResult = 0;
  try {
    sourceAddr = FlowParserWASM._malloc(sourceBuffer.length + 1);
    if (!sourceAddr) {
      throw new Error('Parser out of memory');
    }
    if (filenameBuffer != null) {
      filenameAddr = FlowParserWASM._malloc(filenameBuffer.length + 1);
      if (!filenameAddr) {
        throw new Error('Parser out of memory');
      }
      copyToHeap(filenameBuffer, filenameAddr);
      filenameLen = filenameBuffer.length + 1;
    }
    copyToHeap(sourceBuffer, sourceAddr);

    // `enableTypes` mirrors OCaml's `types` option, which defaults to true.
    // Fixtures opt out via `types: false` to exercise the no-type-grammar
    // path (e.g. ts_syntax fixtures testing that `as`/`satisfies` are not
    // parsed when types are disabled).
    const enableTypes = options.enableTypes === false ? 0 : 1;
    // `sourceType` is sent to Rust as an integer code:
    //   0 = unspecified / parser default
    //   1 = 'script'
    //   2 = 'module'
    // All three map to the same parse on the Rust side: flow_parser has no
    // script/module gate, top-level imports/exports are always accepted,
    // and top-level `await` is always rejected (matching OCaml flow_parser,
    // which also pins `allow_await:false` at the top level). The integer
    // round-trips for diagnostics; semantics are identical.
    const sourceTypeCode =
      options.sourceType === 'script'
        ? 1
        : options.sourceType === 'module'
          ? 2
          : 0;
    parseResult = flowParse(
      sourceAddr,
      sourceBuffer.length + 1,
      filenameAddr,
      filenameLen,
      flag(options.enableExperimentalComponentSyntax),
      flag(options.enableExperimentalFlowMatchSyntax),
      // Decorators default to disabled — OCaml's `default_parse_options`
      // sets `esproposal_decorators = false`. Opt in via
      // `enableExperimentalDecorators: true`.
      flag(options.enableExperimentalDecorators),
      flag(options.tokens),
      flag(options.allowReturnOutsideFunction),
      // `assertOperator` enables Flow's `expr!` non-null assertion.
      flag(options.assertOperator),
      // `enableEnums` / `enableRecords` mirror OCaml's `default_parse_options`
      // (both default to false; fixtures opt in via their `.options.json`).
      flag(options.enableEnums),
      flag(options.enableRecords),
      enableTypes,
      sourceTypeCode,
      // When set, the Rust parser scans the docblock for `@flow`. Files with
      // the pragma parse all Flow syntax; files without it still parse
      // unambiguous Flow syntax such as `import type`.
      // The JS public `parse()` in src/index.js sets this when the caller
      // requests `flow: 'detect'`; raw direct callers of `FlowParser.parse`
      // (e.g. the wasm fixture runner) leave it off and supply
      // `enableTypes` directly.
      flag(options.enableTypesPragmaDetection),
      0,
    );

    // The Rust parser always returns a (possibly partial) AST plus an
    // error list, mirroring the OCaml behaviour. Errors are surfaced as
    // `program.errors` by the deserializer; only fall back to throwing if
    // we got back no program buffer at all (e.g. invalid UTF-8).
    const programBuffer = flowParseResult_getProgramBuffer(parseResult);
    if (!programBuffer) {
      const err = flowParseResult_getError(parseResult);
      const syntaxError = new SyntaxError(err || 'unknown parse error');
      // $FlowExpectedError[prop-missing] SyntaxError loc is a parser extension.
      syntaxError.loc = {
        line: flowParseResult_getErrorLine(parseResult),
        column: flowParseResult_getErrorColumn(parseResult),
      };
      throw syntaxError;
    }

    const deserializer = new FlowParserDeserializer(
      programBuffer,
      flowParseResult_getPositionBuffer(parseResult),
      flowParseResult_getPositionBufferSize(parseResult),
      flowParseResult_getStringBuffer(parseResult),
      FlowParserWASM,
      options,
    );
    const ast = deserializer.deserialize();
    if (ast.type !== 'Program') {
      throw new Error(`Expected Program, got ${ast.type}`);
    }

    // Wire→ESTree loc/range normalization. The wire format carries
    // `rangeStart` / `rangeEnd` on the loc object; ESTree consumers expect a
    // `range: [start, end]` array on the *node* itself, plus a clean
    // `loc: { source, start, end }`. This walk is the parser-level analog of
    // upstream HermesToESTreeAdapter.transform()'s mandatory range conversion
    // (HermesToESTreeAdapter.js:39 — `node.range = [loc.rangeStart, loc.rangeEnd]`).
    // Lives here (not in src/index.js) so callers that bypass the public
    // hermes-parser-compatibility wrapper (e.g. the wasm fixture runner that
    // tests raw Flow ESTree parity with the OCaml parser) still get the
    // canonical ESTree loc/range shape.
    //
    // Track visited nodes so a malformed AST with a cycle doesn't blow the
    // stack; track visited locs separately because two nodes can share a loc
    // reference and we must read `rangeStart`/`rangeEnd` before deleting them.
    const sourceFilename =
      typeof options.sourceFilename === 'string'
        ? options.sourceFilename
        : null;
    const visitedNodes: WeakSet<WireNode> = new WeakSet();
    const locRanges: WeakMap<WireSourceLocation, [number, number]> =
      new WeakMap();
    function fixLocs(node: unknown): void {
      if (node == null || typeof node !== 'object') {
        return;
      }
      // $FlowFixMe[incompatible-variance] Dynamic ESTree serializer object.
      // $FlowFixMe[incompatible-type] Dynamic ESTree serializer object.
      const wireNode: WireNode = node;
      if (visitedNodes.has(wireNode)) {
        return;
      }
      visitedNodes.add(wireNode);
      if (wireNode.loc != null) {
        const loc = wireNode.loc;
        let range = locRanges.get(loc);
        if (range == null) {
          const rangeStart = loc.rangeStart;
          const rangeEnd = loc.rangeEnd;
          if (rangeStart == null || rangeEnd == null) {
            throw new Error('Expected serialized source range');
          }
          loc.source = sourceFilename;
          range = [rangeStart, rangeEnd];
          locRanges.set(loc, range);
          delete loc.rangeStart;
          delete loc.rangeEnd;
        }
        wireNode.range = range;
      }
      for (const key of Object.keys(wireNode)) {
        const val = wireNode[key];
        if (Array.isArray(val)) {
          for (const child of val) {
            fixLocs(child);
          }
        } else {
          fixLocs(val);
        }
      }
    }
    fixLocs(ast);

    // First-error-throw lives at the wasm boundary so the public
    // src/index.js parse() stays close to upstream. Direct callers (e.g.
    // the wasm fixture runner that wants raw ast.errors instead of a
    // throw) opt out via `throwOnParseErrors: false`. Format mirrors
    // HermesASTAdapter.formatError — `${message} (${line}:${column})`.
    // `allowReturnOutsideFunction` is honored in the Rust parser via
    // ParseOptions; invalid RegExp flags are no longer surfaced as parse
    // errors at the Rust source — both filters that used to live here
    // are gone.
    if (options.throwOnParseErrors !== false) {
      const errors = ast.errors;
      if (errors != null && errors.length > 0) {
        const first = errors[0];
        const start = first.loc.start;
        if (start == null) {
          throw new Error('Expected serialized parser error start location');
        }
        const line = start.line;
        const column = start.column;
        const syntaxError = new SyntaxError(
          `${first.message} (${line}:${column})`,
        );
        // $FlowExpectedError[prop-missing] SyntaxError loc is a parser extension.
        syntaxError.loc = {line, column};
        throw syntaxError;
      }
    }

    // Category C - G (shit.md): module-vs-script detection mirroring
    // HermesASTAdapter.setModuleSourceType. Walks the top-level program body
    // for import/export forms; a single value-kind import or export is enough
    // to classify the program as a module. Type-only imports and re-exports
    // keep the program in script mode. We check only the top-level body
    // (matching upstream) because nested imports are not legal at runtime.
    // When the caller pinned `sourceType: 'script'` or `'module'` we honor
    // it verbatim; `'unambiguous'` was already deleted from `options` in
    // src/index.js's getOptions so it falls through to detection.
    if (options.sourceType === 'script' || options.sourceType === 'module') {
      ast.sourceType = options.sourceType;
    } else {
      ast.sourceType = detectSourceType(ast);
    }

    // Category C - I (shit.md): attach the module docblock derived from the
    // leading block comment. Mirrors HermesToESTreeAdapter.mapProgram.
    ast.docblock = getModuleDocblock(ast);

    return asProgram(ast);
  } finally {
    if (parseResult !== 0) {
      flowParseResult_free(parseResult);
    }
    if (sourceAddr !== 0) {
      FlowParserWASM._free(sourceAddr);
    }
    if (filenameAddr !== 0) {
      FlowParserWASM._free(filenameAddr);
    }
  }
}

function asProgram(ast: FlowParserProgram): Program {
  // FlowParserProgram is the deserialized wire shape. `fixLocs` above mutates
  // that shape into the public ESTree Program shape, including loc/range
  // fields that Flow cannot prove from the serializer protocol.
  const program: $FlowFixMe = ast;
  return program;
}

function detectSourceType(program: FlowParserProgram): 'script' | 'module' {
  if (!Array.isArray(program.body)) {
    return 'script';
  }
  for (const stmt of program.body) {
    if (stmt == null) {
      continue;
    }
    switch (stmt.type) {
      case 'ImportDeclaration':
        if (stmt.importKind === 'value' || stmt.importKind == null) {
          return 'module';
        }
        break;
      case 'ExportDefaultDeclaration':
        return 'module';
      case 'ExportNamedDeclaration':
      case 'ExportAllDeclaration':
        if (stmt.exportKind === 'value' || stmt.exportKind == null) {
          return 'module';
        }
        break;
    }
  }
  return 'script';
}

export {parse};
