/*
 * 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
 */

import {getHookNamesMappingFromAST} from './astUtils';
import {encode, decode} from 'sourcemap-codec';
import {File} from '@babel/types';

export type HookMap = {|
  names: $ReadOnlyArray<string>,
  mappings: HookMapMappings,
|};

export type EncodedHookMap = {|
  names: $ReadOnlyArray<string>,
  mappings: string,
|};

// See generateHookMap below for more details on formatting
export type HookMapEntry = [
  number, // 1-indexed line number
  number, // 0-indexed column number
  number, // 0-indexed index into names array
  number, // TODO: filler number to support reusing encoding from `sourcemap-codec` (see TODO below)
];
export type HookMapLine = HookMapEntry[];
export type HookMapMappings = HookMapLine[];

/**
 * Given a parsed source code AST, returns a "Hook Map", which is a
 * mapping which maps locations in the source code to their to their
 * corresponding Hook name, if there is a relevant Hook name for that
 * location (see getHookNamesMappingFromAST for details on the
 * representation of the mapping).
 *
 * The format of the Hook Map follows a similar format as the `name`
 * and `mappings` fields in the Source Map spec, where `names` is an
 * array of strings, and `mappings` contains segments lines, columns,
 * and indices into the `names` array.
 *
 * E.g.:
 *   {
 *     names: ["<no-hook>", "state"],
 *     mappings: [
 *       [ -> line 1
 *         [1, 0, 0],  -> line, col, name index
 *       ],
 *       [ -> line 2
 *         [2, 5, 1],  -> line, col, name index
 *         [2, 15, 0],  -> line, col, name index
 *       ],
 *     ],
 *   }
 */
export function generateHookMap(sourceAST: File): HookMap {
  const hookNamesMapping = getHookNamesMappingFromAST(sourceAST);
  const namesMap: Map<string, number> = new Map();
  const names = [];
  const mappings = [];

  let currentLine = null;
  hookNamesMapping.forEach(({name, start}) => {
    let nameIndex = namesMap.get(name);
    if (nameIndex == null) {
      names.push(name);
      nameIndex = names.length - 1;
      namesMap.set(name, nameIndex);
    }

    // TODO: We add a -1 at the end of the entry so we can later
    // encode/decode the mappings by reusing the encode/decode functions
    // from the `sourcemap-codec` library. This library expects segments
    // of specific sizes (i.e. of size 4) in order to encode them correctly.
    // In the future, when we implement our own encoding, we will not
    // need this restriction and can remove the -1 at the end.
    const entry = [start.line, start.column, nameIndex, -1];

    if (currentLine !== start.line) {
      currentLine = start.line;
      mappings.push([entry]);
    } else {
      const current = mappings[mappings.length - 1];
      current.push(entry);
    }
  });

  return {names, mappings};
}

/**
 * Returns encoded version of a Hook Map that is returned
 * by generateHookMap.
 *
 * **NOTE:**
 * TODO: To encode the `mappings` in the Hook Map, we
 * reuse the encode function from the `sourcemap-codec`
 * library, which means that we are restricted to only
 * encoding segments of specific sizes.
 * Inside generateHookMap we make sure to build segments
 * of size 4.
 * In the future, when we implement our own encoding, we will not
 * need this restriction and can remove the -1 at the end.
 */
export function generateEncodedHookMap(sourceAST: File): EncodedHookMap {
  const hookMap = generateHookMap(sourceAST);
  const encoded = encode(hookMap.mappings);
  return {
    names: hookMap.names,
    mappings: encoded,
  };
}

export function decodeHookMap(encodedHookMap: EncodedHookMap): HookMap {
  return {
    names: encodedHookMap.names,
    mappings: decode(encodedHookMap.mappings),
  };
}
