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

'use strict';

const Generator = require('./Generator');
const SourceMap = require('source-map');

import type {BabelSourceMap} from '@babel/core';
import type {BabelSourceMapSegment} from '@babel/generator';

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

export type MetroSourceMapSegmentTuple =
  | SourceMappingWithName
  | SourceMapping
  | GeneratedCodeMapping;

type FBExtensions = {
  x_facebook_offsets: Array<number>,
  x_metro_module_paths: Array<string>,
};

export type IndexMapSection = {
  map: MetroSourceMap,
  offset: {line: number, column: number},
};

export type IndexMap = {
  file?: string,
  mappings?: void, // avoids SourceMap being a disjoint union
  sections: Array<IndexMapSection>,
  version: number,
};

export type FBIndexMap = IndexMap & FBExtensions;
export type MetroSourceMap = IndexMap | BabelSourceMap;
export type FBSourceMap = FBIndexMap | (BabelSourceMap & FBExtensions);

/**
 * 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>,
    +path: string,
    +source: string,
    +code: string,
  }>,
  offsetLines: number = 0,
): Generator {
  const generator = new Generator();
  let carryOver = offsetLines;

  for (var j = 0, o = modules.length; j < o; ++j) {
    var module = modules[j];
    var {code, map} = module;

    if (Array.isArray(map)) {
      addMappingsForFile(generator, map, module, carryOver);
    } else if (map != null) {
      throw new Error(
        `Unexpected module with full source map found: ${module.path}`,
      );
    }

    carryOver = carryOver + countLines(code);
  }

  return generator;
}

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

  new SourceMap.SourceMapConsumer(sourceMap).eachMapping(map => {
    rawMappings.push({
      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, mappings, module, carryOver) {
  generator.startFile(module.path, module.source);

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

  generator.endFile();
}

function addMapping(generator, mapping, carryOver) {
  const n = mapping.length;
  const line = mapping[0] + carryOver;
  // lines start at 1, columns start at 0
  const column = mapping[1];
  if (n === 2) {
    generator.addSimpleMapping(line, column);
  } else if (n === 4) {
    const sourceMap: SourceMapping = (mapping: any);

    generator.addSourceMapping(line, column, sourceMap[2], sourceMap[3]);
  } else if (n === 5) {
    const sourceMap: SourceMappingWithName = (mapping: any);

    generator.addNamedSourceMapping(
      line,
      column,
      sourceMap[2],
      sourceMap[3],
      sourceMap[4],
    );
  } else {
    throw new Error(`Invalid mapping: [${mapping.join(', ')}]`);
  }
}

function countLines(string) {
  return string.split('\n').length;
}

function createIndexMap(
  file: string,
  sections: Array<IndexMapSection>,
): IndexMap {
  return {
    version: 3,
    file,
    sections,
  };
}

module.exports = {
  createIndexMap,
  fromRawMappings,
  toBabelSegments,
  toSegmentTuple,
};