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

UndeclaredGlobals.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2005 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

local qComplainAboutTopLevelReferences = true

local qAllowTopLevelGlobals = true

-- it is useful if this code can function even in the absence of substrate itself
local import = rawget( _G, "import" )
if import then
	local AgConfiguration = import "AgConfiguration"
	
	if AgConfiguration.linter.allowTopLevelGlobals ~= nil then
		qAllowTopLevelGlobals = AgConfiguration.linter.allowTopLevelGlobals
	end
end

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

-- These are all ways of getting the identifier "foo" in the list of declared global identifiers
-- declare "foo"
-- declare( "foo", "bar", "baz" )
-- require "foo"
-- require "foo.lua"

local function declaredGlobals( codes, constants, lines, badRequires )
	
	-- [scouten 2010-10-29]: If there's no import function, we probably are
	-- in the Wichita environment and not Lightroom. We use neither declare
	-- nor require in this fashion, so just bail out now.
	
	if not import then return {} end
	
	-- special case; declare is allowed to not need "declaring"
	local result = { }
	badRequires = badRequires or { }
	
	local globalDeclarationRegister = nil
	local isRequire = false
	local paramNames = { }
	
	for i, crackedInstruction in ipairs( codes ) do
		local op = crackedInstruction.opcode
		if op == "GETGLOBAL" and constants.scalars[ crackedInstruction.Bx + 1 ] == "declare" then
			globalDeclarationRegister = crackedInstruction.A 		-- are we starting a "declare" statement?
		end

		if op == "GETGLOBAL" and constants.scalars[ crackedInstruction.Bx + 1 ] == "require" then
			globalDeclarationRegister = crackedInstruction.A		-- are we starting a "require" statement?
			isRequire = true
		end

		if op == "LOADK" and globalDeclarationRegister then
			
			-- if the string has a ".lua" suffix, strip that off
			local theParamStr = constants.scalars[ crackedInstruction.Bx + 1 ]
			local first = string.find( theParamStr, "%.lua$" )
			if first ~= nil then
				-- policy decision: using require with .lua at the end is now deprecated, treat
				-- it as a distinct lint hit and do not consider it a valid global declaration
				if isRequire then
					table.insert( badRequires, {
						kind = "reqdotlua",
						line = ( lines and lines[ i ] ) or true,
						name = theParamStr
					} )
				end
			end
			
			-- remember identifier "foo" in the result table
			table.insert( paramNames, theParamStr )
		end
		
		if op == "CALL" and globalDeclarationRegister then
			-- very basic sanity check that this call is the function from GETGLOBAL (can still be fooled)
			if crackedInstruction.A == globalDeclarationRegister then
				for index, name in ipairs( paramNames ) do
					local usages = 0
					-- 0 means all results are retained, 1 means none are retained. The declare call has no results
					-- and for require only the first is used at all, so mark usages accordingly.
					if isRequire and crackedInstruction.C == 0 or ( index == 1 and crackedInstruction.C ~= 1 ) then
						usages = 1 -- result was effectively "used" immediately
					end
	
					-- The C argument to a CALL indicates how many of the results are retained
					result[ name ] = {
						usages = usages,
						line = ( lines and lines[ i ] ) or true,
					}
				end
			end
			
			globalDeclarationRegister = false		-- done with "declare" or "require" opcodes
			paramNames = { }
		end
	end
	return result, badRequires
end

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

local function globals( codes, constants, lines )
	local result = { }
	for opCodeIndex, crackedInstruction in ipairs( codes ) do
		local op = crackedInstruction.opcode
		if op == "GETGLOBAL" or op == "SETGLOBAL" then
			local indx = constants.scalars[ crackedInstruction.Bx + 1 ]
			if not result[ indx ] then
				result[ indx ] = ( lines and lines[ opCodeIndex ] ) or true
			end
		end
	end
	return result
end

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

local function assignedGlobalNames( code, constants )
	local result = { }
	
	for _, crackedInstruction in ipairs( code ) do
		local op = crackedInstruction.opcode
		
		if op == "SETGLOBAL" then
			result[ constants.scalars[ crackedInstruction.Bx + 1 ] ] = true
		end

	end
	return result
end

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

local function badGlobals( isDeclared, globals, declaredScopes )

	local result = { }
	
	local function inAScope( x, scopes )
		for _, v in ipairs( scopes ) do
			if v[x] then
				v[x].usages = v[x].usages + 1
				return true
			end
		end
		return false
	end
	
	for k, v in pairs( globals ) do
		if ( not inAScope( k, declaredScopes ) ) and not isDeclared( k )
				and not result[ k ] then
			result[ k ] = v
		end
	end
	return result

end

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

local function findUndeclaredGlobalsInFunction( isDeclared, crackInstructions, f, declarationScopes, level,
		badRequires, unusedGlobals )
	level = level or 1
	declarationScopes = declarationScopes or { }
	unusedGlobals = unusedGlobals or { }
	badRequires = badRequires or { }
	
	local result = { }

	local cracked = crackInstructions( f.code )
	
	-- table of tables: { { declaredGlobalsInTopLevel }, { declaredGlobalsInNextLevel}, etc }
	
	local declarationsAtThisLevel =  declaredGlobals( cracked, f.constants, f.lines, badRequires )
	table.insert( declarationScopes, declarationsAtThisLevel ) -- append declarationScope
	
	-- Policy decision: we don't want to warn about global refs at the "top" function level
	-- only gather data about nested function inside the top function.
	if level > 1 or qComplainAboutTopLevelReferences then
		result = badGlobals( isDeclared, globals( cracked, f.constants, f.lines ), declarationScopes )
	end
	
	for _, aFunc in ipairs( f.constants.functions ) do
		local res = findUndeclaredGlobalsInFunction( isDeclared, crackInstructions, aFunc, declarationScopes, level + 1,
			badRequires, unusedGlobals )
		for k, v in pairs( res ) do
			if not result[ k ] then
				result[ k ] = v
			end
		end
	end
	
	if level == 1 then
		local allowableNames
		if qAllowTopLevelGlobals then
			allowableNames = assignedGlobalNames( cracked, f.constants )
		else
			allowableNames = {}
		end
		
		for k, v in pairs( allowableNames ) do
			result[ k ] = nil
		end
	end
	
	for id, global in pairs( declarationScopes[ #declarationScopes ] ) do
		if global.usages == 0 and not string.match( id, "^C:" ) then
			table.insert( unusedGlobals, {
				kind = "unusedglobal",
				name = id,
				line = global.line,
			} )
		end
	end
	
	table.remove( declarationScopes ) -- remove last declarationScope

	return result, badRequires, unusedGlobals
end

return findUndeclaredGlobalsInFunction
