/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import * as diff from 'diff-sequences'
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff } from './cleanupSemantic'
import {
  joinAlignedDiffsExpand,
  joinAlignedDiffsNoExpand,
} from './joinAlignedDiffs'
import { normalizeDiffOptions } from './normalizeDiffOptions'
import type { DiffOptions, DiffOptionsNormalized } from './types'

function isEmptyString(lines: Array<string>) {
  return lines.length === 1 && lines[0].length === 0
}

interface ChangeCounts {
  a: number
  b: number
}

function countChanges(diffs: Array<Diff>): ChangeCounts {
  let a = 0
  let b = 0

  diffs.forEach((diff) => {
    switch (diff[0]) {
      case DIFF_DELETE:
        a += 1
        break

      case DIFF_INSERT:
        b += 1
        break
    }
  })

  return { a, b }
}

function printAnnotation({
  aAnnotation,
  aColor,
  aIndicator,
  bAnnotation,
  bColor,
  bIndicator,
  includeChangeCounts,
  omitAnnotationLines,
}: DiffOptionsNormalized, changeCounts: ChangeCounts): string {
  if (omitAnnotationLines)
    return ''

  let aRest = ''
  let bRest = ''

  if (includeChangeCounts) {
    const aCount = String(changeCounts.a)
    const bCount = String(changeCounts.b)

    // Padding right aligns the ends of the annotations.
    const baAnnotationLengthDiff = bAnnotation.length - aAnnotation.length
    const aAnnotationPadding = ' '.repeat(Math.max(0, baAnnotationLengthDiff))
    const bAnnotationPadding = ' '.repeat(Math.max(0, -baAnnotationLengthDiff))

    // Padding left aligns the ends of the counts.
    const baCountLengthDiff = bCount.length - aCount.length
    const aCountPadding = ' '.repeat(Math.max(0, baCountLengthDiff))
    const bCountPadding = ' '.repeat(Math.max(0, -baCountLengthDiff))

    aRest = `${aAnnotationPadding}  ${aIndicator} ${aCountPadding}${aCount}`
    bRest = `${bAnnotationPadding}  ${bIndicator} ${bCountPadding}${bCount}`
  }

  const a = `${aIndicator} ${aAnnotation}${aRest}`
  const b = `${bIndicator} ${bAnnotation}${bRest}`
  return `${aColor(a)}\n${bColor(b)}\n\n`
}

export function printDiffLines(diffs: Array<Diff>, truncated: boolean, options: DiffOptionsNormalized): string {
  return printAnnotation(options, countChanges(diffs))
    + (options.expand ? joinAlignedDiffsExpand(diffs, options) : joinAlignedDiffsNoExpand(diffs, options))
    + (truncated ? options.truncateAnnotationColor(`\n${options.truncateAnnotation}`) : '')
}

// Compare two arrays of strings line-by-line. Format as comparison lines.
export function diffLinesUnified(aLines: Array<string>, bLines: Array<string>, options?: DiffOptions): string {
  const normalizedOptions = normalizeDiffOptions(options)
  const [diffs, truncated] = diffLinesRaw(
    isEmptyString(aLines) ? [] : aLines,
    isEmptyString(bLines) ? [] : bLines,
    normalizedOptions,
  )
  return printDiffLines(
    diffs,
    truncated,
    normalizedOptions,
  )
}

// Given two pairs of arrays of strings:
// Compare the pair of comparison arrays line-by-line.
// Format the corresponding lines in the pair of displayable arrays.
export function diffLinesUnified2(aLinesDisplay: Array<string>, bLinesDisplay: Array<string>, aLinesCompare: Array<string>, bLinesCompare: Array<string>, options?: DiffOptions): string {
  if (isEmptyString(aLinesDisplay) && isEmptyString(aLinesCompare)) {
    aLinesDisplay = []
    aLinesCompare = []
  }
  if (isEmptyString(bLinesDisplay) && isEmptyString(bLinesCompare)) {
    bLinesDisplay = []
    bLinesCompare = []
  }

  if (
    aLinesDisplay.length !== aLinesCompare.length
    || bLinesDisplay.length !== bLinesCompare.length
  ) {
    // Fall back to diff of display lines.
    return diffLinesUnified(aLinesDisplay, bLinesDisplay, options)
  }

  const [diffs, truncated] = diffLinesRaw(aLinesCompare, bLinesCompare, options)

  // Replace comparison lines with displayable lines.
  let aIndex = 0
  let bIndex = 0
  diffs.forEach((diff: Diff) => {
    switch (diff[0]) {
      case DIFF_DELETE:
        diff[1] = aLinesDisplay[aIndex]
        aIndex += 1
        break

      case DIFF_INSERT:
        diff[1] = bLinesDisplay[bIndex]
        bIndex += 1
        break

      default:
        diff[1] = bLinesDisplay[bIndex]
        aIndex += 1
        bIndex += 1
    }
  })

  return printDiffLines(diffs, truncated, normalizeDiffOptions(options))
}

// Compare two arrays of strings line-by-line.
export function diffLinesRaw(aLines: Array<string>, bLines: Array<string>, options?: DiffOptions): [Array<Diff>, boolean] {
  const truncate = options?.truncateThreshold ?? false
  const truncateThreshold = Math.max(Math.floor(options?.truncateThreshold ?? 0), 0)
  const aLength = truncate ? Math.min(aLines.length, truncateThreshold) : aLines.length
  const bLength = truncate ? Math.min(bLines.length, truncateThreshold) : bLines.length
  const truncated = aLength !== aLines.length || bLength !== bLines.length

  const isCommon = (aIndex: number, bIndex: number) => aLines[aIndex] === bLines[bIndex]

  const diffs: Array<Diff> = []
  let aIndex = 0
  let bIndex = 0

  const foundSubsequence = (
    nCommon: number,
    aCommon: number,
    bCommon: number,
  ) => {
    for (; aIndex !== aCommon; aIndex += 1)
      diffs.push(new Diff(DIFF_DELETE, aLines[aIndex]))

    for (; bIndex !== bCommon; bIndex += 1)
      diffs.push(new Diff(DIFF_INSERT, bLines[bIndex]))

    for (; nCommon !== 0; nCommon -= 1, aIndex += 1, bIndex += 1)
      diffs.push(new Diff(DIFF_EQUAL, bLines[bIndex]))
  }

  // @ts-expect-error wrong bundling
  const diffSequences = diff.default.default || diff.default

  diffSequences(aLength, bLength, isCommon, foundSubsequence)

  // After the last common subsequence, push remaining change items.
  for (; aIndex !== aLength; aIndex += 1)
    diffs.push(new Diff(DIFF_DELETE, aLines[aIndex]))

  for (; bIndex !== bLength; bIndex += 1)
    diffs.push(new Diff(DIFF_INSERT, bLines[bIndex]))

  return [diffs, truncated]
}
