/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { OffsetRange } from '../../../core/offsetRange.js';
import { IDiffAlgorithm, SequenceDiff, ISequence, ITimeout, InfiniteTimeout, DiffAlgorithmResult } from './diffAlgorithm.js';
import { Array2D } from '../utils.js';

/**
 * A O(MN) diffing algorithm that supports a score function.
 * The algorithm can be improved by processing the 2d array diagonally.
*/
export class DynamicProgrammingDiffing implements IDiffAlgorithm {
	compute(sequence1: ISequence, sequence2: ISequence, timeout: ITimeout = InfiniteTimeout.instance, equalityScore?: (offset1: number, offset2: number) => number): DiffAlgorithmResult {
		if (sequence1.length === 0 || sequence2.length === 0) {
			return DiffAlgorithmResult.trivial(sequence1, sequence2);
		}

		/**
		 * lcsLengths.get(i, j): Length of the longest common subsequence of sequence1.substring(0, i + 1) and sequence2.substring(0, j + 1).
		 */
		const lcsLengths = new Array2D<number>(sequence1.length, sequence2.length);
		const directions = new Array2D<number>(sequence1.length, sequence2.length);
		const lengths = new Array2D<number>(sequence1.length, sequence2.length);

		// ==== Initializing lcsLengths ====
		for (let s1 = 0; s1 < sequence1.length; s1++) {
			for (let s2 = 0; s2 < sequence2.length; s2++) {
				if (!timeout.isValid()) {
					return DiffAlgorithmResult.trivialTimedOut(sequence1, sequence2);
				}

				const horizontalLen = s1 === 0 ? 0 : lcsLengths.get(s1 - 1, s2);
				const verticalLen = s2 === 0 ? 0 : lcsLengths.get(s1, s2 - 1);

				let extendedSeqScore: number;
				if (sequence1.getElement(s1) === sequence2.getElement(s2)) {
					if (s1 === 0 || s2 === 0) {
						extendedSeqScore = 0;
					} else {
						extendedSeqScore = lcsLengths.get(s1 - 1, s2 - 1);
					}
					if (s1 > 0 && s2 > 0 && directions.get(s1 - 1, s2 - 1) === 3) {
						// Prefer consecutive diagonals
						extendedSeqScore += lengths.get(s1 - 1, s2 - 1);
					}
					extendedSeqScore += (equalityScore ? equalityScore(s1, s2) : 1);
				} else {
					extendedSeqScore = -1;
				}

				const newValue = Math.max(horizontalLen, verticalLen, extendedSeqScore);

				if (newValue === extendedSeqScore) {
					// Prefer diagonals
					const prevLen = s1 > 0 && s2 > 0 ? lengths.get(s1 - 1, s2 - 1) : 0;
					lengths.set(s1, s2, prevLen + 1);
					directions.set(s1, s2, 3);
				} else if (newValue === horizontalLen) {
					lengths.set(s1, s2, 0);
					directions.set(s1, s2, 1);
				} else if (newValue === verticalLen) {
					lengths.set(s1, s2, 0);
					directions.set(s1, s2, 2);
				}

				lcsLengths.set(s1, s2, newValue);
			}
		}

		// ==== Backtracking ====
		const result: SequenceDiff[] = [];
		let lastAligningPosS1: number = sequence1.length;
		let lastAligningPosS2: number = sequence2.length;

		function reportDecreasingAligningPositions(s1: number, s2: number): void {
			if (s1 + 1 !== lastAligningPosS1 || s2 + 1 !== lastAligningPosS2) {
				result.push(new SequenceDiff(
					new OffsetRange(s1 + 1, lastAligningPosS1),
					new OffsetRange(s2 + 1, lastAligningPosS2),
				));
			}
			lastAligningPosS1 = s1;
			lastAligningPosS2 = s2;
		}

		let s1 = sequence1.length - 1;
		let s2 = sequence2.length - 1;
		while (s1 >= 0 && s2 >= 0) {
			if (directions.get(s1, s2) === 3) {
				reportDecreasingAligningPositions(s1, s2);
				s1--;
				s2--;
			} else {
				if (directions.get(s1, s2) === 1) {
					s1--;
				} else {
					s2--;
				}
			}
		}
		reportDecreasingAligningPositions(-1, -1);
		result.reverse();
		return new DiffAlgorithmResult(result, false);
	}
}
