"use strict";

/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
/**
 * @template T
 * @typedef {import("./index.js").MinimizerOptions<T>} MinimizerOptions
 */

const VLQ_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/**
 * Encode a single integer as Base64 VLQ as used by the source-map spec.
 * @param {number} value integer to encode
 * @returns {string} encoded VLQ characters
 */
/* eslint-disable prefer-destructuring, no-eq-null, eqeqeq */
/**
 * @param {number} value integer to encode
 * @returns {string} encoded VLQ characters
 */
function encodeVlq(value) {
  let vlq = value < 0 ? -value << 1 | 1 : value << 1;
  let out = "";
  do {
    let digit = vlq & 0b11111;
    vlq >>>= 5;
    if (vlq > 0) {
      digit |= 0b100000;
    }
    out += VLQ_BASE64[digit];
  } while (vlq > 0);
  return out;
}

/**
 * Encode decoded source-map mappings (per-line arrays of segments) back into
 * the spec's `mappings` string.
 * @param {number[][][]} decoded mappings as nested arrays of segments
 * @returns {string} encoded `mappings` field
 */
function encodeMappings(decoded) {
  let result = "";
  let prevSourceIdx = 0;
  let prevOriginalLine = 0;
  let prevOriginalColumn = 0;
  let prevNameIdx = 0;
  for (let line = 0; line < decoded.length; line++) {
    if (line > 0) {
      result += ";";
    }
    let prevGeneratedColumn = 0;
    const segments = decoded[line];
    for (let i = 0; i < segments.length; i++) {
      if (i > 0) {
        result += ",";
      }
      const seg = segments[i];
      result += encodeVlq(seg[0] - prevGeneratedColumn);
      prevGeneratedColumn = seg[0];
      if (seg.length >= 4) {
        result += encodeVlq(seg[1] - prevSourceIdx);
        prevSourceIdx = seg[1];
        result += encodeVlq(seg[2] - prevOriginalLine);
        prevOriginalLine = seg[2];
        result += encodeVlq(seg[3] - prevOriginalColumn);
        prevOriginalColumn = seg[3];
        if (seg.length >= 5) {
          result += encodeVlq(seg[4] - prevNameIdx);
          prevNameIdx = seg[4];
        }
      }
    }
  }
  return result;
}

/**
 * Compose a freshly-produced source map with the input source map fed to
 * the minimizer. `currentMap` represents `name → step-output` and
 * `prevMap` represents `original → name`; the result represents
 * `original → step-output`.
 *
 * TODO: replace with a webpack-sources helper once one is exposed —
 * `SourceMapSource` already composes one level via `innerSourceMap`,
 * see https://github.com/webpack/webpack-sources for the proposal to
 * expose it as a public `composeSourceMaps` (or n-step `SourceMapSource`).
 * @param {RawSourceMap | undefined} currentMap map produced by the minimizer
 * @param {RawSourceMap | undefined} prevMap input source map fed to the minimizer
 * @param {string} name name of the asset that the current map points to
 * @returns {RawSourceMap | undefined} composed map
 */
function composeSourceMaps(currentMap, prevMap, name) {
  if (!currentMap || !prevMap) {
    return currentMap;
  }

  // Custom minimizers may return the map as a JSON string (e.g. terser's
  // default output). `TraceMap` accepts both shapes, but we still hand
  // back the original `currentMap` (string preserved) when the previous
  // map can't be combined.
  const {
    TraceMap,
    decodedMappings,
    originalPositionFor,
    sourceContentFor
  } = require("@jridgewell/trace-mapping");
  const current = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
  /** @type {unknown} */currentMap);
  const previous = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
  /** @type {unknown} */prevMap);

  /** @type {string[]} */
  const sources = [];
  /** @type {(string | null)[]} */
  const sourcesContent = [];
  /** @type {string[]} */
  const names = [];
  /** @type {Map<string, number>} */
  const sourceIdx = new Map();
  /** @type {Map<string, number>} */
  const nameIdx = new Map();

  /**
   * @param {string | null | undefined} source source identifier
   * @param {string | undefined} content source content (when available)
   * @returns {number} index assigned in the composed map
   */
  const getSourceIdx = (source, content) => {
    const key = source || "";
    let idx = sourceIdx.get(key);
    if (typeof idx === "undefined") {
      idx = sources.length;
      sources.push(key);
      sourcesContent.push(typeof content === "string" ? content : null);
      sourceIdx.set(key, idx);
    } else if (typeof content === "string" && sourcesContent[idx] === null) {
      sourcesContent[idx] = content;
    }
    return idx;
  };

  /**
   * @param {string | null | undefined} value name
   * @returns {number} index assigned in the composed map
   */
  const getNameIdx = value => {
    if (typeof value !== "string") {
      return -1;
    }
    let idx = nameIdx.get(value);
    if (typeof idx === "undefined") {
      idx = names.length;
      names.push(value);
      nameIdx.set(value, idx);
    }
    return idx;
  };
  const decoded = decodedMappings(current);
  const currentSources = current.sources.map(
  /**
   * @param {string | null} source source from current map
   * @returns {string} normalized source string
   */
  source => source || "");
  const currentNames = current.names;

  /** @type {number[][][]} */
  const composed = [];
  for (let line = 0; line < decoded.length; line++) {
    /** @type {number[][]} */
    const newSegments = [];
    for (const rawSeg of decoded[line]) {
      const seg = /** @type {number[]} */rawSeg;

      // Single-element segment is just a generated column with no source info
      if (seg.length < 4) {
        newSegments.push([seg[0]]);
        continue;
      }
      const sourceName = currentSources[seg[1]];
      const origLine = /** @type {number} */seg[2];
      const origCol = /** @type {number} */seg[3];
      const segName = seg.length >= 5 ? currentNames[seg[4]] : (/** @type {string | null} */null);

      // When the segment points back at our intermediate `name`, look up
      // the original position in the previous map and emit a mapping that
      // points all the way back. Otherwise keep the segment as-is.
      if (sourceName === name) {
        const orig = originalPositionFor(previous, {
          line: origLine + 1,
          column: origCol
        });
        if (typeof orig.source !== "string" || orig.line == null || orig.column == null) {
          continue;
        }
        const content = sourceContentFor(previous, orig.source) || undefined;
        const newSrcIdx = getSourceIdx(orig.source, content);
        const finalName = typeof orig.name === "string" && orig.name ? orig.name : segName;
        if (typeof finalName === "string") {
          newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column, getNameIdx(finalName)]);
        } else {
          newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column]);
        }
      } else {
        const content = sourceContentFor(current, sourceName) || undefined;
        const newSrcIdx = getSourceIdx(sourceName, content);
        if (typeof segName === "string") {
          newSegments.push([seg[0], newSrcIdx, origLine, origCol, getNameIdx(segName)]);
        } else {
          newSegments.push([seg[0], newSrcIdx, origLine, origCol]);
        }
      }
    }
    composed.push(newSegments);
  }
  const result = /** @type {RawSourceMap} */

  /** @type {unknown} */{
    version: 3,
    sources,
    names,
    mappings: encodeMappings(composed)
  };
  if (currentMap.file) {
    result.file = currentMap.file;
  }
  if (sourcesContent.some(value => typeof value === "string")) {
    result.sourcesContent = /** @type {string[]} */
    /** @type {unknown} */sourcesContent;
  }
  return result;
}
/* eslint-enable prefer-destructuring, no-eq-null, eqeqeq */

/**
 * @template T
 * @param {import("./index.js").InternalOptions<T>} options options
 * @returns {Promise<MinimizedResult>} minified result
 */
async function minify(options) {
  const {
    name,
    input,
    inputSourceMap,
    extractComments,
    module,
    ecma
  } = options;
  const {
    implementation,
    options: minimizerOptions
  } = options.minimizer;
  const implementations = Array.isArray(implementation) ? implementation : [implementation];

  /** @type {string | undefined} */
  let lastCode;
  /** @type {RawSourceMap | undefined} */
  let lastMap;
  /** @type {(Error | string)[]} */
  const warnings = [];
  /** @type {(Error | string)[]} */
  const errors = [];
  /** @type {string[]} */
  const extractedComments = [];
  for (let i = 0; i < implementations.length; i++) {
    const currentImplementation = /** @type {import("./index.js").BasicMinimizerImplementation<T> & import("./index.js").MinimizeFunctionHelpers} */
    implementations[i];
    const baseOptions = /** @type {import("./index.js").MinimizerOptions<T> & { module?: boolean, ecma?: number | string }} */

    Array.isArray(minimizerOptions) ? minimizerOptions[i] || {} : minimizerOptions || {};
    const currentInput = typeof lastCode === "string" ? lastCode : input;
    const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap;

    // Overlay `module` and `ecma` without mutating the caller's options so
    // a single options object can be reused safely across assets.
    const currentOptions = /** @type {import("./index.js").MinimizerOptions<T>} */
    {
      ...baseOptions,
      module: baseOptions.module || module,
      ecma: baseOptions.ecma || ecma
    };
    const result = await currentImplementation({
      [name]: currentInput
    }, currentMap, currentOptions, extractComments);
    if (result.warnings && result.warnings.length > 0) {
      warnings.push(...result.warnings);
    }
    if (result.errors && result.errors.length > 0) {
      errors.push(...result.errors);
    }
    if (result.extractedComments && result.extractedComments.length > 0) {
      extractedComments.push(...result.extractedComments);
    }
    if (typeof result.code === "string") {
      lastCode = result.code;
      // The minimizer's output map is `name → step-output`. Chain it with
      // the previous accumulated map so that across an array of minimizers
      // the final map points back to the original sources.
      lastMap = composeSourceMaps(result.map, currentMap, name);
    }
  }
  return {
    code: lastCode,
    map: lastMap,
    warnings,
    errors,
    extractedComments
  };
}

/**
 * @param {string} options options
 * @returns {Promise<MinimizedResult>} minified result
 */
async function transform(options) {
  // 'use strict' => this === undefined (Clean Scope)
  // Safer for possible security issues, albeit not critical at all here

  const evaluatedOptions =
  /**
   * @template T
   * @type {import("./index.js").InternalOptions<T>}
   */

  // eslint-disable-next-line no-new-func
  new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`) // eslint-disable-next-line n/exports-style
  (exports, require, module, __filename, __dirname);
  return minify(evaluatedOptions);
}
module.exports = {
  minify,
  transform
};