import { ArrayDiff } from '../../diffs/forArrays/ArrayDiff';
import { compareArrays } from '../forArrays/compareArrays';
import { intersperse } from '../../utils/intersperse';
import { InvalidOperation } from '../../errors';
import { maximumStringLengthForPreciseDiff } from '../../constants/maximumStringLengthForPreciveDiff';
import { unequalCharCost } from '../../constants/costs';
import {
  AdditionDiffSegment as AdditionStringDiffSegment,
  OmissionDiffSegment as OmissionStringDiffSegment
} from '../../diffs/forStrings/StringDiffSegment';
import { equalDiff, EqualDiff, isEqualDiff } from '../../diffs/EqualDiff';
import {
  isAdditionDiffSegment as isAdditionArrayDiffSegment,
  isChangeDiffSegment as isChangeArrayDiffSegment,
  isEqualDiffSegment as isEqualArrayDiffSegment,
  isOmissionDiffSegment as isOmissionArrayDiffSegment
} from '../../diffs/forArrays/ArrayDiffSegment';
import { isStringDiff, stringDiff, StringDiff } from '../../diffs/forStrings/StringDiff';

const convertArrayDiffToStringDiff = function (arrayDiff: ArrayDiff<string>): StringDiff {
  const convertedStringDiff: StringDiff = stringDiff({
    segments: [],
    cost: 0
  });

  for (const arrayDiffSegment of arrayDiff.segments) {
    if (isEqualArrayDiffSegment(arrayDiffSegment)) {
      convertedStringDiff.segments.push({
        equal: arrayDiffSegment.equal.join(''),
        cost: arrayDiffSegment.cost
      });
    }
    if (isChangeArrayDiffSegment(arrayDiffSegment)) {
      const replaceSegment = {
        replace: '',
        replaceWith: '',
        cost: 0
      };

      for (const change of arrayDiffSegment.change) {
        if (!isStringDiff(change)) {
          throw new InvalidOperation('Tried to convert ArrayDiff with sub-diff to StringDiff.');
        }
        replaceSegment.replace += (change.segments[0] as AdditionStringDiffSegment).addition;
        replaceSegment.replaceWith += (change.segments[1] as OmissionStringDiffSegment).omission;
        replaceSegment.cost += change.cost;
      }
      convertedStringDiff.segments.push(replaceSegment);
    }
    if (isOmissionArrayDiffSegment(arrayDiffSegment)) {
      convertedStringDiff.segments.push({
        omission: arrayDiffSegment.omission.join(''),
        cost: arrayDiffSegment.cost
      });
    }
    if (isAdditionArrayDiffSegment(arrayDiffSegment)) {
      convertedStringDiff.segments.push({
        addition: arrayDiffSegment.addition.join(''),
        cost: arrayDiffSegment.cost
      });
    }
    convertedStringDiff.cost += arrayDiffSegment.cost;
  }

  return convertedStringDiff;
};

const compareStrings = function (
  actual: string,
  expected: string
): StringDiff | EqualDiff {
  if (actual === expected) {
    return equalDiff({
      value: actual
    });
  }

  let actualExploded = [ ...actual ];

  if (actualExploded.length > maximumStringLengthForPreciseDiff) {
    actualExploded = intersperse(actual.split(' '), ' ');
  }
  if (actualExploded.length > maximumStringLengthForPreciseDiff) {
    actualExploded = intersperse(actual.split('\n'), '\n');
  }

  let expectedExploded = [ ...expected ];

  if (expectedExploded.length > maximumStringLengthForPreciseDiff) {
    expectedExploded = intersperse(expected.split(' '), ' ');
  }
  if (expectedExploded.length > maximumStringLengthForPreciseDiff) {
    expectedExploded = intersperse(expected.split('\n'), '\n');
  }

  if (actualExploded.length <= 1 && expectedExploded.length <= 1) {
    return stringDiff({
      segments: [
        { addition: actual, cost: unequalCharCost / 2 },
        { omission: expected, cost: unequalCharCost / 2 }
      ],
      cost: unequalCharCost
    });
  }

  const result = compareArrays(actualExploded, expectedExploded);

  if (isEqualDiff(result)) {
    return equalDiff({
      value: actual
    });
  }

  return convertArrayDiffToStringDiff(result);
};

export {
  compareStrings
};
