local trace = function(...)
--	print(string.format( ... ) )
end

local nolocals = function() end
local function markreg(r, locals, line )
	if r >= 0 and r < locals.length then
		trace( "markreg %d %d %d", r, #locals, locals.length or -1 )
		trace( "using %s on line %d", locals[r + 1].varname, line )
		locals[r + 1].read = locals[r + 1].read or 0 + 1
	end
end

local function isread( r, locals )
	return locals[r + 1] and locals[r + 1].read -- nil if never read
end
 
local function marka( cracked, locals, line  )
	markreg(cracked.A, locals, line )
end

local function isRegister(index)
	return index < 256 -- 256 and up are from constant pool
end

local function markb( cracked, locals, line  )
	if isRegister( cracked.B ) then
		markreg(cracked.B, locals, line )
	end
end

local function markc( cracked, locals, line  )
	if isRegister( cracked.C ) then
		markreg(cracked.C, locals, line )
	end
end

local function markbc( cracked, locals, line  )
	markb(cracked, locals, line )
	markc(cracked, locals, line )
end

-- for compound expressions used to provide a local an initial
-- value, sometimes the local's register is re-used during the
-- calculation which makes it falsely show up as having been
-- read even if it is never read afterward
local function markbcnota( cracked, locals, line, index  )
	local a = cracked.A
	local b = cracked.B
	local loc = locals[ a + 1 ]
	if not( a == b and loc and loc.startpc == index ) then
		markb(cracked, locals, line )
	end
	markc(cracked, locals, line )
end

-- SPAM [dtull 2009-04-06]: Review if special case is required when C == 0
local function markforcall( cracked, locals, line )
	local c = cracked.C
	local limit = cracked.A + c - 2
	for i = cracked.A, cracked.A + cracked.B - 1 do
		if c < 2 or isread( i, locals ) or i > limit then
			markreg( i, locals, line )
		end
	end
end

-- Notes on the register assignment behaviors of the various
-- instructions since I'll need to track writes in order to
-- avoid having the local table assignment logic be fooled
-- on re-assignment. See UnusedLocals unit test for further
-- clarification on why this matters. Items marked with an
-- x need a review for more complex analysis.
--
--	MOVE = "a",
--	LOADNIL = "a",
--	LOADK = "a",
--	LOADBOOL = "a",
--	GETGLOBAL = "a",
--	GETUPVAL = "a",
--	GETTABLE = "a",
--	ADD = "a",
--	SUB = "a",
--	MUL = "a",
--	DIV = "a",
--	MOD = "a",
--	POW = "a",
--	UNM = "a",
--	NOT = "a",
--	LEN = "a",
--	CONCAT = "a",
--	VARARG = "a...", -- hits a and some set after it
--	NEWTABLE = "a!", -- ! because the assignment precedes definition
--	CLOSURE = "a",
--
--	SETLIST = "x", -- needs SETTABLE ctable checks
--	CALL = "x",
--	RETURN = "x",
--	TAILCALL = "x",
--	SELF = "x",
--	EQ = "x",
--	LT = "x",
--	LE = "x",
--	TEST = "x",
--	TESTSET = "a?",
--
--	FORPREP = "x",
--	FORLOOP = "x",
--	TFORLOOP = "x",

-- for each instruction, mark locals as used
local markers = {
	LOADK = nolocals,
	LOADBOOL = nolocals,
	LOADNIL = nolocals,
	JMP = nolocals,
	GETGLOBAL = nolocals,

	-- CLOSURE is followed by a series of MOVE/GETUPVAL pseudo instructions
	-- which tend to take care of all the fields that would be touched
	CLOSURE = nolocals,
	GETUPVAL = nolocals,
	CLOSE = nolocals,
	
	-- in practice, these use extra slots, not local registers (that is,
	-- these are usually preceded by a series of MOVE instructions that
	-- touch the locals that are used as part of the loop setup)
	FORPREP = nolocals,
	FORLOOP = nolocals,
	TFORLOOP = nolocals,

	VARARG = nolocals, -- VARARG only assigns locals, it does not read
	SETLIST = function( cracked, locals, line )
		-- SPAM [dtull 2009-04-06]: if C is 0, next "instruction" is not really an instruction
		-- Other than that, in practice SETLIST intructions are preceded by NEWTABLE, LOADK, and
		-- MOVE instructions that fill the list so no variables are referenced by the SETLIST
		-- command itself only.
	end,

	RETURN = function( cracked, locals, line )
		if cracked.B > 1 then
			local max = cracked.A + cracked.B - 2
			for i = cracked.A, max do
				markreg(i, locals, line )
			end
		end
	end,
	CONCAT = function( cracked, locals, line )
		local min = cracked.B ~= cracked.A and cracked.B or cracked.B + 1
		for i = min, cracked.C do
			markreg(i, locals, line )
		end
	end,

	GETTABLE = function( cracked, locals, line )
		-- special case: mark b and c, but if a == b and b is otherwise unread,
		-- do not count this instructions use of b as a read (register re-use)
		if cracked.A ~= cracked.B or isread( cracked.B, locals ) then
			markb( cracked, locals, line )
		end
		markc( cracked, locals, line )
	end,
	SETTABLE = function( cracked, locals, line )
		local a = locals[ cracked.A + 1 ]
		if a and not a.ctable then
			-- when the table was created locally, don't count fields set into
			-- it as usages only count ways that the table escapes
			marka( cracked, locals, line )
		end

		markbc( cracked, locals, line )
	end,
	NEWTABLE = function( cracked, locals, line )
		local a = locals[ cracked.A + 1 ]
		if a then
			-- mark a as having been assigned a created table so that sets
			-- into the table are not alone considered uses of the local
			trace( "marking %s as a created table", a.varname )
		else
			-- create an anonymous entry for it.
			a = { }
			locals[ cracked.A + 1 ] = a
		end
		
		a.ctable = true
	end,
	
	CALL = markforcall,
	TAILCALL = markforcall,

	MOVE = markb,
	ADD = markbcnota,
	SUB = markbcnota,
	DIV = markbcnota,
	MUL = markbcnota,
	MOD = markbcnota,
	POW = markbcnota,
	UNM = markb,
	NOT = markb,
	LEN = markb,
	EQ = markbc,
	LT = markbc,
	LE = markbc,
	SETGLOBAL = marka,
	SETUPVAL = marka,
	SELF = markbc,
	TEST = marka,
	TESTSET = markb,

	-- SPAM [dtull 2009-04-06]:	revisit CLOSURE/GETUPVAL interaction
	-- GETUPVAL has different properties when it
	-- appears as part of a series of ops after a
	-- CLOSURE opcode
	closure = {
		GETUPVAL = markb
	}
}

local warn = {
	__index = function(_, k)
		return function()
			trace( "no local marker for opcode: %s", k )
		end
	end
}

setmetatable( markers, warn )

local function visibleLocals( locals, opcode )
	local result = { }
	for _, loc in ipairs( locals ) do
		if loc.startpc <= opcode and loc.endpc >= opcode then
			table.insert( result, loc )
		end
	end
	
	result.length = #result
	
	return result
end

local function visibleLocalsGenerator( locals )
	local result
	local changes = { }
	for _, loc in ipairs( locals ) do
		changes[ loc.startpc ] = true
		changes[ loc.endpc + 1 ] = true
	end

	return function( opcode )
		if not result or changes[ opcode ] then
			local newResult = visibleLocals( locals, opcode )
			
			-- if this scope change adds new locals and the current set of locals
			-- includes some temp registers, it may have been an inline table
			-- initialization, so copy over the ctable markers to indicate that
			-- the newly visible registers contain inline created tables
			if result and result.length < #result and result.length < #newResult then
				for i = result.length + 1, math.min( #newResult, #result ) do
					trace( "copying ctable status from temp to visible: %s", newResult[ i ].varname )
					-- it's quite possible for the locals table to be sparse
					-- in the section past the named locals (temp registers)
					if result[ i ] then
						newResult[ i ].ctable = result[ i ].ctable
					end
				end
			end
			
			result = newResult
		end
		
		return result
	end
end

local function markLoopLocals( cracked, locals )
	local mark = 0
	
	for _, loc in ipairs( locals ) do
		if mark > 0 then
			mark = mark - 1
			loc.loop = true
		elseif loc.varname == "(for step)" then
			-- FORLOOP: mark next variable
			local forloop = cracked[ loc.endpc ]
			mark = 1
		elseif loc.varname == "(for control)" then
			-- TFORLOOP: mark next C variables
			local tforloop = cracked[ loc.endpc - 1 ]
			mark = tforloop.C -- B is unused, so C is 2, not 3
		end
	end
end

local findUnusedLocals
findUnusedLocals = function( crackInstructions, countParams, toignore, fd, results )
	results = results or { }

	trace("= = = =")
	trace("locals: %d, fn defined on: ", #fd.locals, fd.lineDefined )
	table.foreach( fd.locals, function(i, t)
		trace("%d\t%s (%d-%d)", i, t.varname, t.startpc, t.endpc)
	end )
	trace("- - - -")
	
--	import "AgProfileSection".addDataPoint( "UnusedLocals.countInstrs", #fd.code )
--	import "AgProfileSection".addDataPoint( "UnusedLocals.countLocals", #fd.locals )

	local vlg = visibleLocalsGenerator( fd.locals )
	local cracked = crackInstructions( fd.code )
	for index, t in ipairs( cracked ) do
		markers[t.opcode](t, vlg( index), fd.lines[ index ], index )
		trace("- - ", index, t.opcode, unpack(t))
	end
	
	markLoopLocals( cracked, fd.locals )
	
	local paramslots = countParams( fd )
	
	for i, loc in ipairs( fd.locals ) do
		if not loc.read and not toignore[ loc.varname ] then
			local kind = i <= paramslots and "parameter" or loc.loop and "loop-local" or "local"
			-- parameters and locals which are declared but not assigned at the very beginning
			-- of the function body have no valid lineDefined (unassigned locals do not generate
			-- instructions so at the top of a function they have startpc = 0)
			local line = fd.lines[ loc.startpc ] or fd.lineDefined
			
			table.insert( results, { name = loc.varname, kind = kind, line = line } )
		end
	end
	
	for _, subf in ipairs( fd.constants.functions ) do
		findUnusedLocals( crackInstructions, countParams, toignore, subf, results )
	end
	
	return results
end

--import "AgDynamicInstrumentation".wrapVisibleFunctions("UnusedLocals", {
--	[ findUnusedLocals ] = true,
--})

return findUnusedLocals
