-- Given array of diffs, return concatenated string:
-- * include common substrings
-- * exclude change substrings which have opposite op
-- * include change substrings which have argument op
--   with inverse highlight only if there is a common substring
function getCommonAndChangedSubstrings(diffs: Array<Diff>, op: number, hasCommonDiff: boolean): string
	return Array.reduce(diffs, function(reduced: string, diff: Diff): string
		if diff[1] == DIFF_EQUAL then
			return reduced .. diff[2]
		elseif diff[1] ~= op then
			return reduced
		elseif hasCommonDiff then
			return reduced .. INVERTED_COLOR(diff[2])
		else
			return reduced .. diff[2]
		end
	end, "")
end

function isLineDiffable(expected: any, received: any): boolean
	local expectedType = getType(expected)
	local receivedType = getType(received)

	if expectedType ~= receivedType then
		return false
	end

	if isPrimitive(expected) then
		-- Print generic line diff for strings only:
		-- * if neither string is empty
		-- * if either string has more than one line
		return (
				typeof(expected) == "string"
				and typeof(received) == "string"
				and #expected ~= 0
				and #received ~= 0
				and not not (string.find(expected, MULTILINE_REGEXP) or string.find(received, MULTILINE_REGEXP))
			)
	end

	if expectedType == "DateTime" or expectedType == "function" then
		return false
	end

	if expectedType == "table" and typeof(expected.asymmetricMatch) == "function" then
		return false
	end

	if receivedType == "table" and typeof(received.asymmetricMatch) == "function" then
		return false
	end

	return true
end

local MAX_DIFF_STRING_LENGTH = 20000

function printDiffOrStringify(
	expected: any,
	received: any,
	expectedLabel: string,
	receivedLabel: string,
	expand: boolean -- CLI options: true if '--expand' or false if '--no-expand'
): string
	if
		typeof(expected) == "string"
		and typeof(received) == "string"
		and #expected ~= 0
		and #received ~= 0
		and #expected <= MAX_DIFF_STRING_LENGTH
		and #received <= MAX_DIFF_STRING_LENGTH
		and expected ~= received
	then
		if string.find(expected, MULTILINE_REGEXP) or string.find(received, MULTILINE_REGEXP) then
			return diffStringsUnified(expected, received, {
				aAnnotation = expectedLabel,
				bAnnotation = receivedLabel,
				changeLineTrailingSpaceColor = chalk.bgYellow,
				commonLineTrailingSpaceColor = chalk.bgYellow,
				emptyFirstOrLastLinePlaceholder = utf8.char(8629),
				expand = expand,
				includeChangeCounts = true,
			})
		end

		local diffs = diffStringsRaw(expected, received, true)
		local hasCommonDiff = Array.some(diffs, function(diff)
			return diff[1] == DIFF_EQUAL
		end)

		local printLabel = getLabelPrinter(expectedLabel, receivedLabel)
		local expectedLine = printLabel(expectedLabel)
			.. printExpected(getCommonAndChangedSubstrings(diffs, DIFF_DELETE, hasCommonDiff))

		local receivedLine = printLabel(receivedLabel)
			.. printReceived(getCommonAndChangedSubstrings(diffs, DIFF_INSERT, hasCommonDiff))

		return expectedLine .. "\n" .. receivedLine
	end

	if isLineDiffable(expected, received) then
		local replaced = replaceMatchedToAsymmetricMatcher(
			deepCyclicCopyReplaceable(expected),
			deepCyclicCopyReplaceable(received),
			{},
			{}
		)
		local replacedExpected = replaced.replacedExpected
		local replacedReceived = replaced.replacedReceived
		local difference = diffDefault(replacedExpected, replacedReceived, {
			aAnnotation = expectedLabel,
			bAnnotation = receivedLabel,
			expand = expand,
			includeChangeCounts = true,
		})

		if
			typeof(difference) == "string"
			and string.find(difference, "%- " .. expectedLabel)
			and string.find(difference, "%+ " .. receivedLabel)
		then
			return difference
		end
	end

	local printLabel = getLabelPrinter(expectedLabel, receivedLabel)
	local expectedLine = printLabel(expectedLabel) .. printExpected(expected)
	local receivedLine
	if stringify(expected) == stringify(received) then
		receivedLine = printLabel(receivedLabel) .. "serializes to the same string"
	else
		receivedLine = printLabel(receivedLabel) .. printReceived(received)
	end

	return expectedLine .. "\n" .. receivedLine
end