--[[----------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2022 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

------------------------------------------------------------------------------]]

local WFTableDiff = {}

local kAdded     = "added"
local kChanged   = "changed"
local kDeleted   = "deleted"


-- Sort diffNodes by decending depth, grouped by kind - abuses dictionary order
local function diffNodeSort(a, b)

	if a.depth == b.depth then
		if a.kind == b.kind then
			return a.key < b.key
		else
			return a.kind < b.kind
		end
	else
		return a.depth < b.depth
	end

end

local function diffNode_raw(kind, key, value, depth, path)

	local diffNode = {
		kind = kind,
		key = key,
		value = value,
		depth = depth,
		path = path,
	}

	setmetatable(diffNode, { __lt = diffNodeSort })

	return diffNode

end

local function diffNode_add(key, value, depth, path)
	return diffNode_raw( kAdded, key, value, depth, path )
end

local function diffNode_change(key, value, depth, path)
	return diffNode_raw( kChanged, key, value, depth, path )
end

local function diffNode_remove(key, depth, path)
	return diffNode_raw( kDeleted, key, nil, depth, path )
end

local function arrayMerge(t1,t2)
   for i=1,#t2 do
      t1[#t1+1] = t2[i]
   end
   return t1
end


--[[!

	\brief Determines what keys have been added | changed | deleted between two tables

	\param T1 (table) - First table to diff, in test cases this would be the expected value
	\param auxListToApply (table) - Second table to diff, in test cases this would be the actual value
	\param depth (int, optional) - used in the recursive case to keep track of how deep a change is
	\param path (string, optional) - used in the recursive case to keep track of paths to sub keys a la "root.node.leaf"

	\return <code>table</code> An array of "diffNodes", sorted by the length of their path

--]]
local function diff (T1, T2, depth, path)

	depth = depth or 0
	path = path or ''

	local diffNodes = {}
	local seenKey = {}

	-- Start by looking at the keys in T1

	for k,v in pairs(T1) do

		if type(T1[k]) == "function" or type(T1[k]) == "userdata" then

			-- TODO: Ignore functions and userdata - not sure we use these anywhere FIND OUT
			error("WFTableDiff only supports lua data types")

		elseif type( T1[k] ) == "table" and type( T2[k] ) == "table" then

			-- Drill down and diff sub tables

			local subDiff = diff(T1[k], T2[k], depth + 1, (path ~= "") and (path .. '.' .. k) or k )

			arrayMerge(diffNodes, next( subDiff ) and subDiff or {})

		elseif T2[k] == nil then

			-- if the T1 key is not present in T2, track it as deleted

			diffNodes[ #diffNodes + 1 ] = diffNode_remove(k, depth, path)

		elseif T2[k] ~= v then
			-- if the T1 key is different in T2, track it as updated

			diffNodes[ #diffNodes + 1 ] = diffNode_change(k, T2[k], depth, path)

		end

		seenKey[ path .. k ] = true

	end


	-- Check for additions in T2
	for k,v in pairs(T2) do

		if type(T2[k]) == "function" or type(T2[k]) == "userdata" then

			-- TODO: Ignore functions and userdata - not sure we use these anywhere FIND OUT
			error("WFTableDiff only supports lua data types")

		elseif seenKey[ path .. k ] then

			-- Changes have already been tracked for this key in the first pass

		elseif type(T1[k]) == "table" and type(T2[k]) == "table" then

			-- Drill down and diff sub tables
			local subDiff = diff(T2[k], T1[k], depth + 1, (path ~= "") and (path .. '.' .. k) or k )

			arrayMerge(diffNodes, next( subDiff ) and subDiff or {})

		elseif (T2[k] ~= T1[k]) and T1[k] == nil then

			-- if the T1 key is different in T2, track it as added

			diffNodes[ #diffNodes + 1 ] = diffNode_add(k, v, depth, path)

		end
	end


	table.sort(diffNodes)


	return diffNodes
end

WFTableDiff.diff = diff

------------------------------------------------------------------------------

--[[!

	\brief Formats a diff List for Humans

	\param diff (table) - List of diffNodes

	\return <code>string</code> A \n separated string of change statements

--]]
local function prettifyDiff( diff )

	local currentResults = {}

	for _, node in ipairs(diff) do

		local pretty
		local path

		if node.path == "" then

			path = ""

		else

			path = node.path .. "."

		end


		if node.kind == kAdded then

			pretty = string.format("ADDED Key: %s%s, Value: %s", path, node.key, node.value)

		elseif node.kind == kChanged then

			pretty = string.format("CHANGED Key: %s%s, Value: %s", path, node.key, node.value)

		elseif node.kind == kDeleted then

			pretty = string.format("DELETED Key: %s%s", path, node.key)

		end

		currentResults[ #currentResults + 1 ] = pretty

	end

	return table.concat(currentResults, '\n')

end

WFTableDiff.prettifyDiff = prettifyDiff

return WFTableDiff
