/**
 * 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-local
 * @format
 * @oncall react_native
 */

'use strict';

import type {IConsumer} from './Consumer/types.flow';
import type {BabelSourceMapSegment} from '@babel/generator';

const {BundleBuilder, createIndexMap} = require('./BundleBuilder');
const composeSourceMaps = require('./composeSourceMaps');
const Consumer = require('./Consumer');
// We need to export this for `metro-symbolicate`
const normalizeSourcePath = require('./Consumer/normalizeSourcePath');
const {
  functionMapBabelPlugin,
  generateFunctionMap,
} = require('./generateFunctionMap');
const Generator = require('./Generator');
// $FlowFixMe[untyped-import] - source-map
const SourceMap = require('source-map');

export type {IConsumer};

type GeneratedCodeMapping = [number, number];
type SourceMapping = [number, number, number, number];
type SourceMappingWithName = [number, number, number, number, string];

export type MetroSourceMapSegmentTuple =
  | SourceMappingWithName
  | SourceMapping
  | GeneratedCodeMapping;

export type HermesFunctionOffsets = {[number]: $ReadOnlyArray<number>, ...};

export type FBSourcesArray = $ReadOnlyArray<?FBSourceMetadata>;
export type FBSourceMetadata = [?FBSourceFunctionMap];
export type FBSourceFunctionMap = {
  +names: $ReadOnlyArray<string>,
  +mappings: string,
};

export type FBSegmentMap = {[id: string]: MixedSourceMap, ...};

export type BasicSourceMap = {
  +file?: string,
  +mappings: string,
  +names: Array<string>,
  +sourceRoot?: string,
  +sources: Array<string>,
  +sourcesContent?: Array<?string>,
  +version: number,
  +x_facebook_offsets?: Array<number>,
  +x_metro_module_paths?: Array<string>,
  +x_facebook_sources?: FBSourcesArray,
  +x_facebook_segments?: FBSegmentMap,
  +x_hermes_function_offsets?: HermesFunctionOffsets,
  +x_google_ignoreList?: Array<number>,
};

export type IndexMapSection = {
  map: IndexMap | BasicSourceMap,
  offset: {
    line: number,
    column: number,
    ...
  },
  ...
};

export type IndexMap = {
  +file?: string,
  +mappings?: void, // avoids SourceMap being a disjoint union
  +sourcesContent?: void,
  +sections: Array<IndexMapSection>,
  +version: number,
  +x_facebook_offsets?: Array<number>,
  +x_metro_module_paths?: Array<string>,
  +x_facebook_sources?: void,
  +x_facebook_segments?: FBSegmentMap,
  +x_hermes_function_offsets?: HermesFunctionOffsets,
  +x_google_ignoreList?: void,
};

export type MixedSourceMap = IndexMap | BasicSourceMap;

type SourceMapConsumerMapping = {
  generatedLine: number,
  generatedColumn: number,
  originalLine: ?number,
  originalColumn: ?number,
  source: ?string,
  name: ?string,
};

function fromRawMappingsImpl(
  isBlocking: boolean,
  onDone: Generator => void,
  modules: $ReadOnlyArray<{
    +map: ?Array<MetroSourceMapSegmentTuple>,
    +functionMap: ?FBSourceFunctionMap,
    +path: string,
    +source: string,
    +code: string,
    +isIgnored: boolean,
    +lineCount?: number,
  }>,
  offsetLines: number,
): void {
  const modulesToProcess = modules.slice();
  const generator = new Generator();
  let carryOver = offsetLines;

  function processNextModule() {
    if (modulesToProcess.length === 0) {
      return true;
    }

    const mod = modulesToProcess.shift();
    const {code, map} = mod;
    if (Array.isArray(map)) {
      addMappingsForFile(generator, map, mod, carryOver);
    } else if (map != null) {
      throw new Error(
        `Unexpected module with full source map found: ${mod.path}`,
      );
    }
    carryOver = carryOver + countLines(code);
    return false;
  }

  function workLoop() {
    const time = process.hrtime();
    while (true) {
      const isDone = processNextModule();
      if (isDone) {
        onDone(generator);
        break;
      }
      if (!isBlocking) {
        // Keep the loop running but try to avoid blocking
        // for too long because this is not in a worker yet.
        const diff = process.hrtime(time);
        const NS_IN_MS = 1000000;
        if (diff[1] > 50 * NS_IN_MS) {
          // We've blocked for more than 50ms.
          // This code currently runs on the main thread,
          // so let's give Metro an opportunity to handle requests.
          setImmediate(workLoop);
          break;
        }
      }
    }
  }

  workLoop();
}

/**
 * Creates a source map from modules with "raw mappings", i.e. an array of
 * tuples with either 2, 4, or 5 elements:
 * generated line, generated column, source line, source line, symbol name.
 * Accepts an `offsetLines` argument in case modules' code is to be offset in
 * the resulting bundle, e.g. by some prefix code.
 */
function fromRawMappings(
  modules: $ReadOnlyArray<{
    +map: ?Array<MetroSourceMapSegmentTuple>,
    +functionMap: ?FBSourceFunctionMap,
    +path: string,
    +source: string,
    +code: string,
    +isIgnored: boolean,
    +lineCount?: number,
  }>,
  offsetLines: number = 0,
): Generator {
  let generator: void | Generator;
  fromRawMappingsImpl(
    true,
    g => {
      generator = g;
    },
    modules,
    offsetLines,
  );
  if (generator == null) {
    throw new Error('Expected fromRawMappingsImpl() to finish synchronously.');
  }
  return generator;
}

async function fromRawMappingsNonBlocking(
  modules: $ReadOnlyArray<{
    +map: ?Array<MetroSourceMapSegmentTuple>,
    +functionMap: ?FBSourceFunctionMap,
    +path: string,
    +source: string,
    +code: string,
    +isIgnored: boolean,
    +lineCount?: number,
  }>,
  offsetLines: number = 0,
): Promise<Generator> {
  return new Promise(resolve => {
    fromRawMappingsImpl(false, resolve, modules, offsetLines);
  });
}

/**
 * Transforms a standard source map object into a Raw Mappings object, to be
 * used across the bundler.
 */
function toBabelSegments(
  sourceMap: BasicSourceMap,
): Array<BabelSourceMapSegment> {
  const rawMappings: Array<BabelSourceMapSegment> = [];

  new SourceMap.SourceMapConsumer(sourceMap).eachMapping(
    (map: SourceMapConsumerMapping) => {
      rawMappings.push(
        map.originalLine == null || map.originalColumn == null
          ? {
              generated: {
                line: map.generatedLine,
                column: map.generatedColumn,
              },
              source: map.source,
              name: map.name,
            }
          : {
              generated: {
                line: map.generatedLine,
                column: map.generatedColumn,
              },
              original: {
                line: map.originalLine,
                column: map.originalColumn,
              },
              source: map.source,
              name: map.name,
            },
      );
    },
  );

  return rawMappings;
}

function toSegmentTuple(
  mapping: BabelSourceMapSegment,
): MetroSourceMapSegmentTuple {
  const {column, line} = mapping.generated;
  const {name, original} = mapping;

  if (original == null) {
    return [line, column];
  }

  if (typeof name !== 'string') {
    return [line, column, original.line, original.column];
  }

  return [line, column, original.line, original.column, name];
}

function addMappingsForFile(
  generator: Generator,
  mappings: Array<MetroSourceMapSegmentTuple>,
  module: {
    +code: string,
    +functionMap: ?FBSourceFunctionMap,
    +map: ?Array<MetroSourceMapSegmentTuple>,
    +path: string,
    +source: string,
    +isIgnored: boolean,
    +lineCount?: number,
  },
  carryOver: number,
) {
  generator.startFile(module.path, module.source, module.functionMap, {
    addToIgnoreList: module.isIgnored,
  });

  for (let i = 0, n = mappings.length; i < n; ++i) {
    addMapping(generator, mappings[i], carryOver);
  }

  generator.endFile();
}

function addMapping(
  generator: Generator,
  mapping: MetroSourceMapSegmentTuple,
  carryOver: number,
) {
  const line = mapping[0] + carryOver;
  // lines start at 1, columns start at 0
  const column = mapping[1];
  switch (mapping.length) {
    case 2:
      generator.addSimpleMapping(line, column);
      return;
    case 4:
      generator.addSourceMapping(line, column, mapping[2], mapping[3]);
      return;
    case 5:
      generator.addNamedSourceMapping(
        line,
        column,
        mapping[2],
        mapping[3],
        mapping[4],
      );
      return;
  }
  throw new Error(`Invalid mapping: [${mapping.join(', ')}]`);
}

const newline = /\r\n?|\n|\u2028|\u2029/g;

const countLines = (string: string): number =>
  (string.match(newline) || []).length + 1;

module.exports = {
  BundleBuilder,
  composeSourceMaps,
  Consumer,
  createIndexMap,
  generateFunctionMap,
  fromRawMappings,
  fromRawMappingsNonBlocking,
  functionMapBabelPlugin,
  normalizeSourcePath,
  toBabelSegments,
  toSegmentTuple,
};