-- Given array of diffs, return string:
-- * include common substrings
-- * exclude change substrings which have opposite op
-- * include change substrings which have argument op
--   with change color only if there is a common substring
local function joinDiffs(
	-- ROBLOX deviation: changed Array<Diff> to Array<any>
	diffs: Array<any>,
	op: number,
	hasCommon: boolean
): string
	return Array.reduce(
		diffs,
		-- ROBLOX deviation: Diff changed to any
		function(reduced: string, diff: Diff): string
			local retval = reduced
			if diff[1] == DIFF_EQUAL then
				retval = reduced .. diff[2]
			else
				if diff[1] == op then
					if hasCommon then
						retval = retval .. INVERTED_COLOR(diff[2])
					else
						retval = retval .. diff[2]
					end
				end
			end
			return retval
		end,
		""
	)
end

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

	if isPrimitive(received) then
		return typeof(received) == "string"
	end

	-- ROBLOX deviation: type changed from "date" to "DateTime"
	if receivedType == "DateTime" or receivedType == "function" or receivedType == "regexp" then
		return false
	end

	if instanceof(received, Error) then
		return false
	end

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

	return true
end

local function printExpected(val: any): string
	return EXPECTED_COLOR(minify(val))
end

local function printReceived(val: any): string
	return RECEIVED_COLOR(minify(val))
end

local function printPropertiesAndReceived(
	properties: any,
	received: any,
	expand: boolean -- CLI options: true if `--expand` or false if `--no-expand`
): string
	local aAnnotation = "Expected properties"
	local bAnnotation = "Received value"

	if isLineDiffable(properties) and isLineDiffable(received) then
		return diffLinesUnified(
			serialize(properties):split("\n"),
			serialize(getObjectSubset(received, properties)):split("\n"),
			{
				aAnnotation = aAnnotation,
				aColor = EXPECTED_COLOR,
				bAnnotation = bAnnotation,
				bColor = RECEIVED_COLOR,
				-- ROBLOX FIXME: our types and implementation are seriously misaligned
				changeLineTrailingSpaceColor = chalk.bgYellow :: any,
				commonLineTrailingSpaceColor = chalk.bgYellow :: any,
				emptyFirstOrLastLinePlaceholder = utf8.char(8629), -- U+21B5
				expand = expand,
				includeChangeCounts = true,
			}
		)
	end

	local printLabel = getLabelPrinter(aAnnotation, bAnnotation)
	return printLabel(aAnnotation)
		.. printExpected(properties)
		.. "\n"
		.. printLabel(bAnnotation)
		.. printReceived(received)
end

local MAX_DIFF_STRING_LENGTH = 20000

local function printSnapshotAndReceived(
	a: string, -- snapshot without extra line breaks
	b: string, -- received serialized but without extra line breaks
	received: any,
	expand: boolean -- CLI options: true if `--expand` or false if `--no-expand`
): string
	local aAnnotation = "Snapshot"
	local bAnnotation = "Received"
	local aColor = aSnapshotColor
	local bColor = bReceivedColor
	local options = {
		aAnnotation = aAnnotation,
		aColor = aColor,
		bAnnotation = bAnnotation,
		bColor = bColor,
		-- ROBLOX FIXME: our types and implementation are seriously misaligned
		changeLineTrailingSpaceColor = noColor :: any,
		commonLineTrailingSpaceColor = chalk.bgYellow :: any,
		emptyFirstOrLastLinePlaceholder = utf8.char(8629), -- U+21B5
		expand = expand,
		includeChangeCounts = true,
	}

	if typeof(received) == "string" then
		if #a >= 2 and String.startsWith(a, '"') and String.endsWith(a, '"') and b == prettyFormat(received) then
			-- If snapshot looks like default serialization of a string
			-- and received is string which has default serialization.
			if not a:find("\n") and not b:find("\n") then
				-- If neither string is multiline,
				-- display as labels and quoted strings.
				local aQuoted = a
				local bQuoted = b

				if #a - 2 <= MAX_DIFF_STRING_LENGTH and #b - 2 <= MAX_DIFF_STRING_LENGTH then
					local diffs = diffStringsRaw(string.sub(a, 2, -2), string.sub(b, 2, -2), true)
					local hasCommon = Array.some(diffs, function(diff)
						return diff[1] == DIFF_EQUAL
					end)
					aQuoted = '"' .. joinDiffs(diffs, DIFF_DELETE, hasCommon) .. '"'
					bQuoted = '"' .. joinDiffs(diffs, DIFF_INSERT, hasCommon) .. '"'
				end

				local printLabel = getLabelPrinter(aAnnotation, bAnnotation)
				return printLabel(aAnnotation) .. aColor(aQuoted) .. "\n" .. printLabel(bAnnotation) .. bColor(bQuoted)
			end

			-- Else either string is multiline, so display as unquoted strings.
			a = deserializeString(a) -- hypothetical expected string
			b = received -- not serialized
		end

		-- Else expected had custom serialization or was not a string
		-- or received has custom serialization.
		if #a <= MAX_DIFF_STRING_LENGTH and #b <= MAX_DIFF_STRING_LENGTH then
			return diffStringsUnified(a, b, options)
		else
			return diffLinesUnified(a:split("\n"), b:split("\n"), options)
		end
	end

	if isLineDiffable(received) then
		local aLines2 = a:split("\n")
		local bLines2 = b:split("\n")

		-- Fall through to fix a regression for custom serializers
		-- like jest-snapshot-serializer-raw that ignore the indent option.
		local b0 = serialize(received, 0)
		if b0 ~= b then
			local aLines0 = dedentLines(aLines2)

			if aLines0 ~= nil then
				-- Compare lines without indentation.
				local bLines0 = b0:split("\n")

				return diffLinesUnified2(aLines2, bLines2, aLines0, bLines0, options)
			end
		end

		-- Fall back because:
		-- * props include a multiline string
		-- * text has more than one adjacent line
		-- * markup does not close
		return diffLinesUnified(aLines2, bLines2, options)
	end

	local printLabel = getLabelPrinter(aAnnotation, bAnnotation)
	return printLabel(aAnnotation) .. aColor(a) .. "\n" .. printLabel(bAnnotation) .. bColor(b)
end

return {
	getSnapshotColorForChalkInstance = getSnapshotColorForChalkInstance,
	getReceivedColorForChalkInstance = getReceivedColorForChalkInstance,
	aSnapshotColor = aSnapshotColor,
	bReceivedColor = bReceivedColor,
	noColor = noColor,
	HINT_ARG = HINT_ARG,
	SNAPSHOT_ARG = SNAPSHOT_ARG,
	PROPERTIES_ARG = PROPERTIES_ARG,
	matcherHintFromConfig = matcherHintFromConfig,
	printExpected = printExpected,
	printReceived = printReceived,
	printPropertiesAndReceived = printPropertiesAndReceived,
	printSnapshotAndReceived = printSnapshotAndReceived,
}