import { TraceMap, presortedDecodedMap, decodedMappings } from './trace-mapping';
import {
  COLUMN,
  SOURCES_INDEX,
  SOURCE_LINE,
  SOURCE_COLUMN,
  NAMES_INDEX,
} from './sourcemap-segment';

import type {
  Section,
  SectionedSourceMap,
  DecodedSourceMap,
  SectionedSourceMapInput,
} from './types';
import type { SourceMapSegment } from './sourcemap-segment';

type AnyMap = {
  new (map: SectionedSourceMapInput, mapUrl?: string | null): TraceMap;
  (map: SectionedSourceMapInput, mapUrl?: string | null): TraceMap;
};

export const AnyMap: AnyMap = function (map, mapUrl) {
  const parsed =
    typeof map === 'string' ? (JSON.parse(map) as Exclude<SectionedSourceMapInput, string>) : map;

  if (!('sections' in parsed)) return new TraceMap(parsed, mapUrl);

  const mappings: SourceMapSegment[][] = [];
  const sources: string[] = [];
  const sourcesContent: (string | null)[] = [];
  const names: string[] = [];

  recurse(parsed, mapUrl, mappings, sources, sourcesContent, names, 0, 0, Infinity, Infinity);

  const joined: DecodedSourceMap = {
    version: 3,
    file: parsed.file,
    names,
    sources,
    sourcesContent,
    mappings,
  };

  return presortedDecodedMap(joined);
} as AnyMap;

function recurse(
  input: SectionedSourceMap,
  mapUrl: string | null | undefined,
  mappings: SourceMapSegment[][],
  sources: string[],
  sourcesContent: (string | null)[],
  names: string[],
  lineOffset: number,
  columnOffset: number,
  stopLine: number,
  stopColumn: number,
) {
  const { sections } = input;
  for (let i = 0; i < sections.length; i++) {
    const { map, offset } = sections[i];

    let sl = stopLine;
    let sc = stopColumn;
    if (i + 1 < sections.length) {
      const nextOffset = sections[i + 1].offset;
      sl = Math.min(stopLine, lineOffset + nextOffset.line);

      if (sl === stopLine) {
        sc = Math.min(stopColumn, columnOffset + nextOffset.column);
      } else if (sl < stopLine) {
        sc = columnOffset + nextOffset.column;
      }
    }

    addSection(
      map,
      mapUrl,
      mappings,
      sources,
      sourcesContent,
      names,
      lineOffset + offset.line,
      columnOffset + offset.column,
      sl,
      sc,
    );
  }
}

function addSection(
  input: Section['map'],
  mapUrl: string | null | undefined,
  mappings: SourceMapSegment[][],
  sources: string[],
  sourcesContent: (string | null)[],
  names: string[],
  lineOffset: number,
  columnOffset: number,
  stopLine: number,
  stopColumn: number,
) {
  if ('sections' in input) return recurse(...(arguments as unknown as Parameters<typeof recurse>));

  const map = new TraceMap(input, mapUrl);
  const sourcesOffset = sources.length;
  const namesOffset = names.length;
  const decoded = decodedMappings(map);
  const { resolvedSources, sourcesContent: contents } = map;

  append(sources, resolvedSources);
  append(names, map.names);
  if (contents) append(sourcesContent, contents);
  else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null);

  for (let i = 0; i < decoded.length; i++) {
    const lineI = lineOffset + i;

    // We can only add so many lines before we step into the range that the next section's map
    // controls. When we get to the last line, then we'll start checking the segments to see if
    // they've crossed into the column range. But it may not have any columns that overstep, so we
    // still need to check that we don't overstep lines, too.
    if (lineI > stopLine) return;

    // The out line may already exist in mappings (if we're continuing the line started by a
    // previous section). Or, we may have jumped ahead several lines to start this section.
    const out = getLine(mappings, lineI);
    // On the 0th loop, the section's column offset shifts us forward. On all other lines (since the
    // map can be multiple lines), it doesn't.
    const cOffset = i === 0 ? columnOffset : 0;

    const line = decoded[i];
    for (let j = 0; j < line.length; j++) {
      const seg = line[j];
      const column = cOffset + seg[COLUMN];

      // If this segment steps into the column range that the next section's map controls, we need
      // to stop early.
      if (lineI === stopLine && column >= stopColumn) return;

      if (seg.length === 1) {
        out.push([column]);
        continue;
      }

      const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX];
      const sourceLine = seg[SOURCE_LINE];
      const sourceColumn = seg[SOURCE_COLUMN];
      out.push(
        seg.length === 4
          ? [column, sourcesIndex, sourceLine, sourceColumn]
          : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]],
      );
    }
  }
}

function append<T>(arr: T[], other: T[]) {
  for (let i = 0; i < other.length; i++) arr.push(other[i]);
}

function getLine<T>(arr: T[][], index: number): T[] {
  for (let i = arr.length; i <= index; i++) arr[i] = [];
  return arr[index];
}
