"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = mergeSourceMap;

const sourceMap = require("source-map");

function mergeSourceMap(inputMap, map) {
  const input = buildMappingData(inputMap);
  const output = buildMappingData(map);
  const mergedGenerator = new sourceMap.SourceMapGenerator();

  for (const {
    source
  } of input.sources) {
    if (typeof source.content === "string") {
      mergedGenerator.setSourceContent(source.path, source.content);
    }
  }

  if (output.sources.length === 1) {
    const defaultSource = output.sources[0];
    const insertedMappings = new Map();
    eachInputGeneratedRange(input, (generated, original, source) => {
      eachOverlappingGeneratedOutputRange(defaultSource, generated, item => {
        const key = makeMappingKey(item);
        if (insertedMappings.has(key)) return;
        insertedMappings.set(key, item);
        mergedGenerator.addMapping({
          source: source.path,
          original: {
            line: original.line,
            column: original.columnStart
          },
          generated: {
            line: item.line,
            column: item.columnStart
          },
          name: original.name
        });
      });
    });

    for (const item of insertedMappings.values()) {
      if (item.columnEnd === Infinity) {
        continue;
      }

      const clearItem = {
        line: item.line,
        columnStart: item.columnEnd
      };
      const key = makeMappingKey(clearItem);

      if (insertedMappings.has(key)) {
        continue;
      }

      mergedGenerator.addMapping({
        generated: {
          line: clearItem.line,
          column: clearItem.columnStart
        }
      });
    }
  }

  const result = mergedGenerator.toJSON();

  if (typeof input.sourceRoot === "string") {
    result.sourceRoot = input.sourceRoot;
  }

  return result;
}

function makeMappingKey(item) {
  return `${item.line}/${item.columnStart}`;
}

function eachOverlappingGeneratedOutputRange(outputFile, inputGeneratedRange, callback) {
  const overlappingOriginal = filterApplicableOriginalRanges(outputFile, inputGeneratedRange);

  for (const {
    generated
  } of overlappingOriginal) {
    for (const item of generated) {
      callback(item);
    }
  }
}

function filterApplicableOriginalRanges({
  mappings
}, {
  line,
  columnStart,
  columnEnd
}) {
  return filterSortedArray(mappings, ({
    original: outOriginal
  }) => {
    if (line > outOriginal.line) return -1;
    if (line < outOriginal.line) return 1;
    if (columnStart >= outOriginal.columnEnd) return -1;
    if (columnEnd <= outOriginal.columnStart) return 1;
    return 0;
  });
}

function eachInputGeneratedRange(map, callback) {
  for (const {
    source,
    mappings
  } of map.sources) {
    for (const {
      original,
      generated
    } of mappings) {
      for (const item of generated) {
        callback(item, original, source);
      }
    }
  }
}

function buildMappingData(map) {
  const consumer = new sourceMap.SourceMapConsumer(Object.assign({}, map, {
    sourceRoot: null
  }));
  const sources = new Map();
  const mappings = new Map();
  let last = null;
  consumer.computeColumnSpans();
  consumer.eachMapping(m => {
    if (m.originalLine === null) return;
    let source = sources.get(m.source);

    if (!source) {
      source = {
        path: m.source,
        content: consumer.sourceContentFor(m.source, true)
      };
      sources.set(m.source, source);
    }

    let sourceData = mappings.get(source);

    if (!sourceData) {
      sourceData = {
        source,
        mappings: []
      };
      mappings.set(source, sourceData);
    }

    const obj = {
      line: m.originalLine,
      columnStart: m.originalColumn,
      columnEnd: Infinity,
      name: m.name
    };

    if (last && last.source === source && last.mapping.line === m.originalLine) {
      last.mapping.columnEnd = m.originalColumn;
    }

    last = {
      source,
      mapping: obj
    };
    sourceData.mappings.push({
      original: obj,
      generated: consumer.allGeneratedPositionsFor({
        source: m.source,
        line: m.originalLine,
        column: m.originalColumn
      }).map(item => ({
        line: item.line,
        columnStart: item.column,
        columnEnd: item.lastColumn + 1
      }))
    });
  }, null, sourceMap.SourceMapConsumer.ORIGINAL_ORDER);
  return {
    file: map.file,
    sourceRoot: map.sourceRoot,
    sources: Array.from(mappings.values())
  };
}

function findInsertionLocation(array, callback) {
  let left = 0;
  let right = array.length;

  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    const item = array[mid];
    const result = callback(item);

    if (result === 0) {
      left = mid;
      break;
    }

    if (result >= 0) {
      right = mid;
    } else {
      left = mid + 1;
    }
  }

  let i = left;

  if (i < array.length) {
    while (i >= 0 && callback(array[i]) >= 0) {
      i--;
    }

    return i + 1;
  }

  return i;
}

function filterSortedArray(array, callback) {
  const start = findInsertionLocation(array, callback);
  const results = [];

  for (let i = start; i < array.length && callback(array[i]) === 0; i++) {
    results.push(array[i]);
  }

  return results;
}