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

const {format} = require('util');

const colors = require('colors/safe');
const {diffLines, diffWords} = require('diff');

import type {
  AssertionLocation,
  ErrorAssertionResult,
  Suggestion,
} from './assertionTypes';

// Like str.trim(), but only trims blank lines at the beginning and end
function trim(str: string) {
  return str.replace(/^\s*\n/, '').replace(/\n\s*$/, '');
}

function getDiff(
  expected: string,
  actual: string,
  ignoreWhitespace: boolean = true,
): ?Array<string> {
  let isSame = true;
  let actualLine = '';
  let expectedLine = '';
  let messages: Array<string> = [];
  let parts = [];
  actual = trim(actual);
  expected = trim(expected);

  // Doing a word-by-word diff algorithm gets really slow for very large, very
  // different strings. While it's hard to estimate how different they are, it's
  // easy to tell when they're very large :)
  const numWords = actual.split(/\s+/).length + expected.split(/\s+/).length;
  if (numWords > 1000) {
    // diffLines trims every line when ignoreWhitespace is true. What I want is
    // for the diff algorithm to ignore whitespace but NOT trim whitespace. So
    // basically I need to look up the original line to get the correct
    // whitespace. Le sigh.
    const expectedLines = expected.split('\n');
    const actualLines = actual.split('\n');
    let aidx = 0,
      eidx = 0;
    for (const part of diffLines(actual, expected, {ignoreWhitespace})) {
      for (let value of part.value.split(/(\n)/)) {
        if (value != '') {
          if (value !== '\n') {
            if (part.added) {
              value = expectedLines[eidx];
            } else if (part.removed) {
              value = actualLines[aidx];
            } else {
              value = expectedLines[eidx] || actualLines[aidx];
            }
          } else {
            if (part.added) {
              eidx++;
            } else if (part.removed) {
              aidx++;
            } else {
              aidx++;
              eidx++;
            }
          }
          parts.push({
            added: part.added,
            removed: part.removed,
            value: value,
          });
        }
      }
    }
  } else {
    for (const part of diffWords(actual, expected)) {
      for (const value of part.value.split(/(\n)/)) {
        if (value != '') {
          const notWhitespace = !ignoreWhitespace || !value.match(/^\s+$/);
          parts.push({
            added: part.added && notWhitespace,
            removed: part.removed && notWhitespace,
            value,
          });
        }
      }
    }
  }

  const saveLine = () => {
    if (expectedLine == actualLine) {
      if (expectedLine == '') {
        return;
      }
      // Gotta make these line num with the +/- lines
      messages.push('  ' + expectedLine);
    } else {
      actualLine != '' && messages.push(colors.green('+ ') + actualLine);
      expectedLine != '' && messages.push(colors.red('- ') + expectedLine);
      isSame = false;
    }
    actualLine = expectedLine = '';
  };

  for (const part of parts) {
    if (part.value == '\n') {
      saveLine();
    } else {
      if (part.added) {
        expectedLine += colors.red(part.value);
      } else if (part.removed) {
        actualLine += colors.green(part.value);
      } else {
        expectedLine += colors.grey(part.value);
        actualLine += colors.grey(part.value);
      }
    }
  }
  saveLine();
  return isSame ? null : messages;
}

function simpleDiffAssertion(
  expected: string,
  actual: string,
  assertLoc: ?AssertionLocation,
  reason: ?string,
  diffSubject: string,
  suggestion: Suggestion,
  ignoreWhitespace: boolean = true,
): ErrorAssertionResult {
  const diffMessages = getDiff(expected, actual, ignoreWhitespace);
  if (diffMessages != null) {
    const locMessage =
      assertLoc == null
        ? []
        : [
            format(
              colors.white('%s line %d col %d'),
              assertLoc.filename,
              assertLoc.line,
              assertLoc.column,
            ),
          ];
    const reasonMessage =
      reason == null
        ? []
        : [format(colors.grey('Reason: ') + colors.red('%s'), reason)];
    const keyMessage = [
      format(
        colors.green('Actual %s (+)') +
          colors.grey(" didn't match ") +
          colors.red('expected %s (-)'),
        diffSubject,
        diffSubject,
      ),
    ];
    const messages = [].concat(
      locMessage,
      reasonMessage,
      keyMessage,
      diffMessages,
    );

    return {type: 'fail', messages, assertLoc, suggestion};
  }
  return {type: 'pass'};
}

module.exports = {
  default: simpleDiffAssertion,
};
