'use strict';

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

var _chalk = require('chalk');

var _chalk2 = _interopRequireDefault(_chalk);

var _diff = require('diff');

var _constants = require('./constants.js');

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

const DIFF_CONTEXT_DEFAULT = 5; // removed | added | equal

// Given diff digit, return array which consists of:
// if compared line is removed or added: corresponding original line
// if compared line is equal: original received and expected lines
/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 */

// Given chunk, return diff character.
const getDiffChar = chunk => (chunk.removed ? '-' : chunk.added ? '+' : ' ');

// Given diff character in line of hunk or computed from properties of chunk.
const getDiffDigit = char => (char === '-' ? -1 : char === '+' ? 1 : 0);

// Color for text of line.
const getColor = (digit, onlyIndentationChanged) => {
  if (digit === -1) {
    return _chalk2.default.green; // removed
  }
  if (digit === 1) {
    return _chalk2.default.red; // added
  }
  return onlyIndentationChanged ? _chalk2.default.cyan : _chalk2.default.dim;
};

// Do NOT color leading or trailing spaces if original lines are equal:

// Background color for leading or trailing spaces.
const getBgColor = (digit, onlyIndentationChanged) =>
  digit === 0 && !onlyIndentationChanged
    ? _chalk2.default.bgYellow
    : _chalk2.default.inverse;

// ONLY trailing if expected value is snapshot or multiline string.
const highlightTrailingSpaces = (line, bgColor) =>
  line.replace(/\s+$/, bgColor('$&'));

// BOTH leading AND trailing if expected value is data structure.
const highlightLeadingTrailingSpaces = (
  line,
  bgColor
  // If line consists of ALL spaces: highlight all of them.
) =>
  highlightTrailingSpaces(line, bgColor).replace(
    // If line has an ODD length of leading spaces: highlight only the LAST.
    /^(\s\s)*(\s)(?=[^\s])/,
    '$1' + bgColor('$2')
  );

const getAnnotation = options =>
  _chalk2.default.green(
    '- ' + ((options && options.aAnnotation) || 'Expected')
  ) +
  '\n' +
  _chalk2.default.red('+ ' + ((options && options.bAnnotation) || 'Received')) +
  '\n\n';

// Given string, return array of its lines.
const splitIntoLines = string => {
  const lines = string.split('\n');

  if (lines.length !== 0 && lines[lines.length - 1] === '') {
    lines.pop();
  }

  return lines;
};

// Given diff character and compared line, return original line with colors.
const formatLine = (char, lineCompared, getOriginal) => {
  const digit = getDiffDigit(char);

  if (getOriginal) {
    // Compared without indentation if expected value is data structure.
    const lineArray = getOriginal(digit);
    const lineOriginal = lineArray[0];
    const onlyIndentationChanged =
      digit === 0 && lineOriginal.length !== lineArray[1].length;

    return getColor(digit, onlyIndentationChanged)(
      char +
        ' ' +
        // Prepend indentation spaces from original to compared line.
        lineOriginal.slice(0, lineOriginal.length - lineCompared.length) +
        highlightLeadingTrailingSpaces(
          lineCompared,
          getBgColor(digit, onlyIndentationChanged)
        )
    );
  }

  // Format compared line when expected is snapshot or multiline string.
  return getColor(digit)(
    char + ' ' + highlightTrailingSpaces(lineCompared, getBgColor(digit))
  );
};

// Given original lines, return callback function
// which given diff digit, returns array.
const getterForChunks = original => {
  const linesExpected = splitIntoLines(original.a);
  const linesReceived = splitIntoLines(original.b);

  let iExpected = 0;
  let iReceived = 0;

  return digit => {
    if (digit === -1) {
      return [linesExpected[iExpected++]];
    }
    if (digit === 1) {
      return [linesReceived[iReceived++]];
    }
    // Because compared line is equal: original received and expected lines.
    return [linesReceived[iReceived++], linesExpected[iExpected++]];
  };
};

// jest --expand
const formatChunks = (a, b, original) => {
  const chunks = (0, _diff.diffLines)(a, b);
  if (chunks.every(chunk => !chunk.removed && !chunk.added)) {
    return null;
  }

  const getOriginal = original && getterForChunks(original);
  return chunks
    .reduce((lines, chunk) => {
      const char = getDiffChar(chunk);

      splitIntoLines(chunk.value).forEach(line => {
        lines.push(formatLine(char, line, getOriginal));
      });

      return lines;
    }, [])
    .join('\n');
};

// Only show patch marks ("@@ ... @@") if the diff is big.
// To determine this, we need to compare either the original string (a) to
// `hunk.oldLines` or a new string to `hunk.newLines`.
// If the `oldLinesCount` is greater than `hunk.oldLines`
// we can be sure that at least 1 line has been "hidden".
const shouldShowPatchMarks = (hunk, oldLinesCount) =>
  oldLinesCount > hunk.oldLines;

const createPatchMark = hunk => {
  const markOld = `-${hunk.oldStart},${hunk.oldLines}`;
  const markNew = `+${hunk.newStart},${hunk.newLines}`;
  return _chalk2.default.yellow(`@@ ${markOld} ${markNew} @@`);
};

// Given original lines, return callback function which given indexes for hunk,
// returns another callback function which given diff digit, returns array.
const getterForHunks = original => {
  const linesExpected = splitIntoLines(original.a);
  const linesReceived = splitIntoLines(original.b);

  return (iExpected, iReceived) => digit => {
    if (digit === -1) {
      return [linesExpected[iExpected++]];
    }
    if (digit === 1) {
      return [linesReceived[iReceived++]];
    }
    // Because compared line is equal: original received and expected lines.
    return [linesReceived[iReceived++], linesExpected[iExpected++]];
  };
};

// jest --no-expand
const formatHunks = (a, b, contextLines, original) => {
  const options = {
    context:
      typeof contextLines === 'number' && contextLines >= 0
        ? contextLines
        : DIFF_CONTEXT_DEFAULT
  };

  var _structuredPatch = (0, _diff.structuredPatch)(
    '',
    '',
    a,
    b,
    '',
    '',
    options
  );

  const hunks = _structuredPatch.hunks;

  if (hunks.length === 0) {
    return null;
  }

  const getter = original && getterForHunks(original);
  const oldLinesCount = (a.match(/\n/g) || []).length;
  return hunks
    .reduce((lines, hunk) => {
      if (shouldShowPatchMarks(hunk, oldLinesCount)) {
        lines.push(createPatchMark(hunk));
      }

      // Hunk properties are one-based but index args are zero-based.
      const getOriginal =
        getter && getter(hunk.oldStart - 1, hunk.newStart - 1);
      hunk.lines.forEach(line => {
        lines.push(formatLine(line[0], line.slice(1), getOriginal));
      });

      return lines;
    }, [])
    .join('\n');
};

function diffStrings(a, b, options, original) {
  // Because `formatHunks` and `formatChunks` ignore one trailing newline,
  // always append newline to strings:
  a += '\n';
  b += '\n';

  // `diff` uses the Myers LCS diff algorithm which runs in O(n+d^2) time
  // (where "d" is the edit distance) and can get very slow for large edit
  // distances. Mitigate the cost by switching to a lower-resolution diff
  // whenever linebreaks are involved.
  const result =
    options && options.expand === false
      ? formatHunks(a, b, options && options.contextLines, original)
      : formatChunks(a, b, original);

  return result === null
    ? _constants.NO_DIFF_MESSAGE
    : getAnnotation(options) + result;
}
