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

KrDebugBottleneck.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2003 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 params = { ... }
params = params[ 1 ]

local kernel = params.kernel
local archiveTable = params.archiveTable

local newTransitQueue = assert( kernel.newTransitQueue )
local AgGetBreakOnThrow = assert( params.AgGetBreakOnThrow )
local AgSetBreakOnThrow = assert( params.AgSetBreakOnThrow )
local AgGetBreakOnHalt = assert( params.AgGetBreakOnHalt )
local AgSetBreakOnHalt = assert( params.AgSetBreakOnHalt )
local triggerUniverse = assert( params.triggerUniverse )
local fetchViaHttp = assert( params.fetchViaHttp )
local debuggerPort = assert( params.debuggerPort )
local JSON = assert( params.JSON )
local serviceCodeQueue = params.serviceCodeQueue

local shouldPullBreakpoints = params.shouldPullBreakpoints
local supportsBreakpoints = shouldPullBreakpoints ~= nil
local activeDebugger = params.activeDebugger
local getConfiguration = params.getConfiguration

local config = getConfiguration and getConfiguration()

local logger = kernel.logger "KrDebugBottleneck"
local cllogger = kernel.logger "KrDebugCodeLoading"


logger:infof( "Starting." )

--============================================================================--
-- @mark Constants
--==============================================================================

local kResults = 1
local kActiveDebuggerHalideUrl = 2
local kIdle = 3

--==============================================================================

local encoders = {
	raw = function( value )
		return tostring( value )
	end,
	lua = function( value )

		if type( value ) == 'string' then
			value = string.format( '"%s"', value )
		elseif type( value ) == 'table' then
			value = archiveTable( value )
		else
			value = tostring( value )
		end

		return string.format( "return %s", value )

	end,
	json = function( value )
		local success, str = pcall( JSON.encode, value )
		if success then
			return str
		else
			return JSON.encode( {
				message = "error",
				value = tostring(str)
			} )
		end
	end,
}

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

local function encodeResult( value, encoding )

	local encoder = encoders[ encoding ]
	if not encoder then
		encoder = encoders.lua
	end

	return encoder( value )

end

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

local debugHandlers = {}
local REQUIRED_NUMBER = {}
local REQUIRED_STRING = {}
local OPTIONAL = {}
local REQUIRED = {
	[ REQUIRED_NUMBER ] = 'number',
	[ REQUIRED_STRING ] = 'string',
}

local function parseHttpArguments( cmdName, sendArgs, args )
	local queuedParams = {}
	args = args or {}

	for x, default in pairs( sendArgs or {} ) do
		local value = args[ x ]
		if value == nil and default ~= OPTIONAL then
			value = default
		end
		if REQUIRED[ value ] then
			return nil,
				string.format( "Missing required %s parameter '%s' for command '%s'", REQUIRED[ value ], x, cmdName )

		end
		local coerceType

		if default == REQUIRED_NUMBER then
			coerceType = 'number'
		elseif default == REQUIRED_STRING then
			coerceType = 'string'
		else
			coerceType = type( default )
		end

		if coerceType == 'number' then
			value = tonumber( value )
			if type( value ) ~= 'number' then
				return nil, string.format( "The required %s parameter '%s' for command '%s' must be a valid number", REQUIRED[ value ], x, cmdName )

		end
		end

		queuedParams[ x ] = value
	end

	return true, queuedParams
end

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

local makeHaltedThreadMap = function( haltedThreads )
	local result = {}
	for index, thread in ipairs( haltedThreads ) do
		result[ thread.id ] = true
	end
	return result
end

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

local lookupThreadById = function( haltedThreads, id )

	local threadIndex = nil
	for index, thread in ipairs( haltedThreads ) do
		if thread.id == id then
			threadIndex = index
		end
	end
	if not threadIndex then
		threadIndex = 1
	end

	return threadIndex

end

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

local function makeHandler( param, sendArgs )

	debugHandlers[ '/' .. param ] =  function( args, debugQueue, state )
		local haltedThreads = state.haltedThreads

		local success, queuedParams = parseHttpArguments( param, sendArgs, args )

		if not success then
			return {
					message = 'error',
					value = queuedParams
			}
		end

		local threadIndex = lookupThreadById( haltedThreads, queuedParams.thread )

		if haltedThreads[ threadIndex ] == nil then
			return {
				message = 'error',
				code = 'nothalted',
				value = string.format( 'Can\'t get %s.  Thread %d is not currently halted.', param, queuedParams.thread )
			}
		end

		local resultQueue = newTransitQueue()

		logger:debugf( "Sending %s request to main universe", param )

		queuedParams.resultQueue = resultQueue
		queuedParams.message = param

		haltedThreads[ threadIndex ].queue:enqueue( queuedParams )

		logger:trace( "Waiting for response on resultQueue" )

		local result = resultQueue:dequeue( true, queuedParams.timeout )

		if result then
			logger:debugf( "Got %s off resultQueue", param )

			return {
				message = param,
				value = result.value
			}
		else
			logger:warnf( "Timed out receiving from resultQueue." )
			return {
				message = "error",
				value = "Timeout awaiting response"
			}
		end
	end

end

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

makeHandler( 'stack', { level = 1, thread = 1 } )
makeHandler( 'code', { level = 1, thread = 1 } )
makeHandler( 'traceback', { thread = 1 } )
makeHandler( 'objectInfo', { marshal_value = REQUIRED_NUMBER, thread = 1 } )
makeHandler( 'runLua', { code = REQUIRED_STRING, level = 1, thread = 1, timeout = 5 } )

local function updateDefaultBreakOnThrow( newBreakOnThrow )
	local success, message
	logger:debugf( "Persisting AgDebugger.breakOnThrow : %s", newBreakOnThrow and "halt on throw" or "go right on by throw" )
	if config and config.update then
		success, message = config.update( function( cfg )

			-- TODO [bzibble 2016-09-28]: Disabling persistence of breakOnThrow when it is changed from the debugger.
			--
			-- Toggling the "Disable BreakOnThrow" button in the debugger will still work for the duration of that session, but
			-- it will now always clear state of this flag in the config file.
			--
			-- The checked state of this button in the debugger does not stay in sync with the state of the debuggee, so it is
			-- confusing and dangerous to persist this flag across sessions when it is changed via the debugger.
			--
			-- In the future, if the debugger is given the ability to keep the state of this button in sync with the debuggee's
			-- current state, this workaround will no longer be needed. You will always see the true state of this flag.
			--
			-- Note that this flag can still be added to the config file manually. It is still respsected if present.
			--
			--cfg.AgDebugger.breakOnThrow = newBreakOnThrow
			cfg.AgDebugger.breakOnThrow = nil

		end )

		if not success then
			logger:errorf( "Failed to write AgDebugger.breakOnThrow into kernel config: %s", message )
		end
	else
		logger:warnf( "Cannot persist AgDebugger.breakOnThrow because no config.update" )
	end
end

debugHandlers[ '/toggleBreakOnThrow' ] = function()
	local doit = not AgGetBreakOnThrow()
	logger:debugf( "Toggling break on throw to %s", doit )
	updateDefaultBreakOnThrow( doit )
	AgSetBreakOnThrow( doit )
	return {
		message = 'toggleBreakOnThrow',
		value = doit,
	}
end

debugHandlers[ '/toggleBreakOnHalt' ] = function()
	local doit = not AgGetBreakOnHalt()
	logger:debugf( "Toggling break on Halt to %s", doit )
	AgSetBreakOnHalt( doit )
	return {
		message = 'toggleBreakOnHalt',
		value = doit,
	}
end

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

local k404Handler = function( command, args )

	return {
		message = 'error',
		value = string.format( "Command '%s' not understood", command or "unknown" )
	}

end

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

local sContinueArgs = {
	thread = 1,
}

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

debugHandlers[ '/takeProfilingSample' ] = function( args, debugQueue, state )
	for _, t in pairs( state.allThreads ) do
		triggerUniverse( t.luaState )
	end

	return {
		message = 'takeProfilingSample',
		value = 'ok',
	}

end

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

debugHandlers[ '/listProfilerSamples' ] = function( args, debugQueue, state )

	local samples = state.profilerSamples
	state.profilerSamples = nil

	return {
		message = 'listProfilerSamples',
		value = samples or { },
	}

end

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

debugHandlers[ '/continue' ] = function( args, debugQueue, state )
	local haltedThreads = state.haltedThreads

	logger:tracef( "parseHttpArguments" )
	local success, queuedParams = parseHttpArguments( 'continue', sContinueArgs, args )

	logger:tracef( "parseHttpArguments done" )

	if not success then
		return {
				message = 'error',
				value = queuedParams
		}
	end


	logger:tracef( "lookupThreadById" )

	local threadIndex = lookupThreadById( haltedThreads, queuedParams.thread )

	local responseQueue = haltedThreads[ threadIndex ]
	responseQueue = responseQueue and responseQueue.queue

	if responseQueue == nil then
		return {
			message = 'error',
			code = 'nothalted',
			value = string.format( 'Can\'t continue.  Thread %d is not currently halted.', queuedParams.thread )
		}
	end

	logger:tracef( "define stepIntent: %s", args and args.step )

	local stepIntent = args.step or "Run"

	logger:tracef( "Step intent: %s", stepIntent )

	if stepIntent and responseQueue then

		logger:debugf( "Continuing thread %d (%s).", queuedParams.thread, stepIntent )

		responseQueue:enqueue {
			stepIntent = stepIntent,
			args = args.args
		}

		table.remove( haltedThreads, threadIndex )

	end


	return {
		message = 'continue',
		value = 'ok',
	}

end

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

debugHandlers[ '/continueAll' ] = function( args, debugQueue, state )
	local allThreads, haltedThreads = state.allThreads, state.haltedThreads
	local pausePending = state.pausePending

	local success, queuedParams = parseHttpArguments( 'continueAll', sContinueArgs, args )

	if not success then
		return {
				message = 'error',
				value = queuedParams
		}
	end

	if #haltedThreads == 0 and not next( state.pausePending ) then
		return {
			message = 'error',
			code = 'nothalted',
			value = string.format( 'Can\'t continue.  Thread %d is not currently halted.', queuedParams.thread )
		}
	end

	for thread, responseQueue in pairs( haltedThreads ) do

		responseQueue = responseQueue and responseQueue.queue
		local stepIntent = args.step or "Run"

		if stepIntent and responseQueue then

			logger:debugf( "Continuing thread %d (%s).", queuedParams.thread, stepIntent )
			responseQueue:enqueue {
				stepIntent = stepIntent
			}

		end

	end

	for thread, responseQueue in pairs( haltedThreads ) do
			haltedThreads[ thread ] = nil
	end

	local command = {
		message = "continue"
	}

	for _, thread in ipairs( allThreads ) do
		if pausePending[ thread.id ] then
			logger:tracef( "Setting continue request to thread id %d", thread.id )
			pausePending[ thread.id ] = nil
			thread.debuggeeQueue:enqueue( command )
			triggerUniverse( thread.luaState )
		end
	end

	return {
		message = 'continue',
		value = 'ok',
	}

end

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

debugHandlers[ '/kill' ] = function( args, debugQueue, state )
	os.exit( -99 )
end


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

local function baseDebuggerUrl( optHost, optPort, optService )
	local host = optHost or "127.0.0.1"
	local port = optPort or 19570
	local service = optService or "remote_debugger"

	return string.format( "http://%s:%d/%s/", host, port, service )
end

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

local function sendDebuggerActivationMessage( thread, state )
	local activateUrl
	if state.baseUrl then
		local port = activeDebugger and "none" or debuggerPort
		activateUrl = state.baseUrl .. "openDebugger?port=" .. port
	end

	thread.debuggeeQueue:enqueue {
		message = "activationParameter",
		param = activateUrl
	}
	triggerUniverse( thread.luaState )
end

local function updateBaseUrl( state, baseUrl, doNotPersist )
	state.baseUrl = baseUrl

	local success, message = nil, "Debugger URL not updated."

	if not doNotPersist and config and config.update then
		success, message = config.update( function( cfg )
			cfg.AgDebugger.halideUrl = baseUrl
		end )

		if not success then
			logger:errorf( "Failed to write AgDebugger.halideUrl into kernel config: %s", message )
		end

	end

	for _, thread in ipairs( state.allThreads ) do
		sendDebuggerActivationMessage( thread, state )
	end

	return success, message
end

debugHandlers[ '/detach' ] = function( args, debugQueue, state, ip )

	local oldBaseUrl = state.baseUrl
	local status, message = updateBaseUrl( state, nil )

	local result = {
		oldBaseUrl = oldBaseUrl,
		status = status,
		message = message
	}

	return {
		message = "detach",
		value = result
	}
end

debugHandlers[ '/attach' ] = function( args, debugQueue, state, ip )
	local oldBaseUrl = state.baseUrl
	local baseUrl = baseDebuggerUrl( ip, tonumber( args.port ), args.service )

	local status, message

	if state.baseUrl ~= baseUrl then
		status, message = updateBaseUrl( state, baseUrl, activeDebugger )
	else
		status, message = true, "Already attached."
	end

	return {
		message = "attach",
		value = {
			newBaseUrl = baseUrl,
			oldBaseUrl = oldBaseUrl,
			status = status,
			message = message
		}
	}
end

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

debugHandlers[ '/pause' ] = function( args, debugQueue, state )
	local haltedThreads, allThreads = state.haltedThreads, state.allThreads
	local pausePending = state.pausePending
	local haltedThreadMap = makeHaltedThreadMap( haltedThreads )

	local command = {
		message = "pause"
	}

	local count = 0
	for _, thread in ipairs( allThreads ) do

		local halted = haltedThreadMap[ thread.id ]
		if not halted and not pausePending[ thread.id ] then
			-- put on the queue for the periodic debugger hook to find
			logger:tracef( "Setting pause request to thread id %d", thread.id )
			pausePending[ thread.id ] = true
			thread.debuggeeQueue:enqueue( command )
			triggerUniverse( thread.luaState )
			count = count + 1
		end

	end

	return {
		message = "pause",
		value = count
	}
end

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

if supportsBreakpoints then
	debugHandlers[ '/listSetBreakpoints' ] = function( args, debugQueue, state )

		return {
			message = 'listSetBreakpoints',
			value = state.breakpoints,
		}

	end
end

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

debugHandlers[ '/listThreads' ] = function( args, debugQueue, state )
	local haltedThreads, allThreads = state.haltedThreads, state.allThreads

	local success, queuedParams = parseHttpArguments( 'listThreads', {}, args )

	if not success then
		return {
				message = 'error',
				value = queuedParams
		}
	end

	local haltedIds = makeHaltedThreadMap( haltedThreads )

	local result = {}

	for i, thread in ipairs( allThreads ) do
		local halted = haltedIds[ thread.id ]
		table.insert( result, {
			name = thread.name,
			halted = not not halted,
			pausing = state.pausePending[ thread.id ],
			id = thread.id,
		} )
	end

	table.sort( result, function( a, b )
		return a.id < b.id
	end )

	return {
		message = 'listThreads',
		value = result,
	}

end

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

local sRunMainThreadCodeArgs = {
	code = REQUIRED_STRING,
	universe = OPTIONAL,
	timeout = OPTIONAL
}

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

local function sendCodeToUniverse( runner, queuedParams, thread, resultQueue, optTimeout )

	if optTimeout and optTimeout <= 0 then
		return nil, string.format( "Timeout must be positive: %s", tostring( optTimeout ) )
	end

	if not thread.codeQueue then
		return nil, string.format( "No code queue for: %s", thread.name )
	end

	thread.codeQueue:enqueue {
		message = 'runMainThreadCode',
		code = queuedParams.code,
		queue = resultQueue,
	}

	-- SPAM [dtull 2012-05-10]: This is a bit awkward in that if this operation were to fail, it
	-- would leave a stray item in the code queue. This provides another reason to rework this logic
	-- so these are just messages on the usual debug/debuggee queues...
	if not runner( thread.name ) then
		return nil, string.format( "Code runner failed for: %s", thread.name )
	end

	-- SPAM [dtull 2012-05-08]: What would be better here would
	-- be to drain the debug queue and notice a halt explicitly,
	-- but that's a bigger overhaul than I want to perform now.
	local result = resultQueue:dequeue( true, optTimeout or 5.0 )

	if not result then
		return nil, string.format( "Code runner timed out for: %s", thread.name )
	end

	return result
end

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

local function universeWithName( universeName, state )
	for _, thread in ipairs( state.allThreads ) do
		if thread.name == universeName then
			return thread
		end
	end
end

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

debugHandlers[ '/runMainThreadCode' ] = function( args, debugQueue, state )
	local mainThreadCodeRunner = state.mainThreadCodeRunner

	local success, queuedParams = parseHttpArguments( 'runMainThreadCode', sRunMainThreadCodeArgs, args )

	if not success then
		return {
				message = 'error',
				value = queuedParams
		}
	end

	local resultQueue = newTransitQueue()

	local result, message

	if not mainThreadCodeRunner then
		local thread

		if queuedParams.universe then
			thread = universeWithName( queuedParams.universe, state )
		else
			thread = state.allThreads[ 1 ]
		end

		if thread then
			local runner = thread and thread.codeRunner or serviceCodeQueue

			result, message = sendCodeToUniverse( runner, queuedParams, thread, resultQueue,
				tonumber( queuedParams.timeout ) )
		elseif queuedParams.universe then
			local name = tostring( queuedParams.universe )
			result, message = nil, string.format( "Universe not found: %s", name )
		else
			result, message = nil, string.format( "No universe for code execution." )
		end
	else
		-- SPAM [dtull 2013-09-10]: Deprecated (here only for backward compatibility).

		mainThreadCodeRunner.queue:enqueue {
			message = 'runMainThreadCode',
			code = queuedParams.code,
			queue = resultQueue,
		}

		mainThreadCodeRunner.action:trigger()

		result = resultQueue:dequeue( true, queuedParams.timeout or 5.0 )
	end

	return {
		message = result and 'runMainThreadCode' or 'error',
		value = result and result or message or 'Unknown error!'
	}

end

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

local sSetClearBreakpointArgs = {
	chunkName = REQUIRED_STRING,
	set = REQUIRED_NUMBER,
	lineNumber = REQUIRED_NUMBER,
	optThreadId = OPTIONAL,
	code = OPTIONAL,
}

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

local function matches( bpParams, bp )
	return bpParams.chunkName == bp.chunkName and bpParams.lineNumber == bp.lineNumber
end

-- SPAM [dtull 2012-04-18]: This logic is partially duplicated with the logic in KrDebugLoop.lua
-- that updates the registry breakpoint table for the debugged universes.
local function updateBreakpointTable( isSet, bpParams, breakpoints )
	local matched = { }
	for i, bp in ipairs( breakpoints ) do
		if matches( bpParams, bp ) then
			matched[ #matched + 1 ] = i
		end
	end

	if isSet and #matched == 0 then
		breakpoints[ #breakpoints + 1 ] = bpParams
		logger:tracef( "Inserted Breakpoint (%d): %s:%d", #breakpoints, bpParams.chunkName, bpParams.lineNumber )
	elseif isSet then
		if #matched > 1 then
			logger:warnf( "Multiple matches for breakpoint: %s:%d", bpParams.chunkName, bpParams.lineNumber )
		end

		for _, m in ipairs( matched ) do
			breakpoints[ m ] = bpParams
			logger:tracef( "Updating Breakpoint (%d): %s:%d", m, bpParams.chunkName, bpParams.lineNumber )
		end
	else
		for i = #matched, 1, -1 do
			local m = matched[ i ]
			table.remove( breakpoints, m )
			logger:tracef( "Removing Breakpoint (%d): %s:%d", m, bpParams.chunkName, bpParams.lineNumber )
		end
	end
end


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

local function sendSetupMessages( state, thread )
	table.insert( state.allThreads, thread )

	logger:debugf( "Sending setup messages to thread %d (%s)", thread.id, thread.name )

	if supportsBreakpoints then
		thread.debuggeeQueue:enqueue {
			message = "initialBreakpoints",
			breakpoints = state.breakpoints
		}
	end

	if state.baseUrl then
		sendDebuggerActivationMessage( thread, state )
	end
end

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

local function processCodeloadingOption( state, codeLoading )
	if type( codeLoading ) == "table" then
		state.codeLoading = true

		for name, value in pairs( codeLoading ) do
			if type( value ) == "string" then
				-- this chunk will have a seeded value
				state.loadedChunks[ name ] = value
			elseif value == true then
				-- this chunk will loaded on demand
				state.loadedChunks[ name ] = true
			end

			cllogger:debugf( "Seeding code loader: %s -> %s", name, type( value ) )
		end

		-- if we're provided a table of codeLoading seed values,
		-- disable loading for anything not in that list
		setmetatable( state.loadedChunks, {
			__index = function( _, key )
				return false
			end
		} )

		return true
	elseif type( codeLoading ) == "boolean" then
		state.codeLoading = codeLoading

		return true
	end

	cllogger:debugf( "Code loading %s enabled.", state.codeLoading and "is" or "is not")
end

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

local function processAdditionalDirectives( state, bpTable )
	processCodeloadingOption( state, bpTable.codeLoading )
end

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

local function installBreakpoints( state, bpTable )
	assert( not state.breakpoints, "breakpoints should only be installed once" )

	state.breakpoints = { }

	for _, bp in ipairs( bpTable ) do
		updateBreakpointTable( true, bp, state.breakpoints )
	end

	-- while the API is still called "getBreakpoints", it is evolving toward
	-- a general getConfiguration call
	processAdditionalDirectives( state, bpTable )

	-- finish the setup of any threads which have been started but not yet
	-- sent the initial set of breakpoints
	for _, thread in ipairs( state.pendingThreads ) do
		sendSetupMessages( state, thread )
	end

	state.pendingThreads = nil
end

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

debugHandlers[ '/initialBreakpoints' ] = function( args, debugQueue, state )

	if not args or type( args ) ~= "table" then
		return {
			message = "error",
			value = "Malformed request: breakpoints table missing"
		}
	elseif state.breakpoints then
		return {
			message = "error",
			value = "Breakpoints already set."
		}
	elseif supportsBreakpoints then
		installBreakpoints( state, args )

		return {
			message = "initialBreakpoints",
			value = "ok"
		}
	else
		return {
			message = "error",
			value = "breakpoints not supported by this debuggee"
		}
	end
end

debugHandlers[ '/updateCodeLoading' ] = function( args, debugQueue, state )
	local success = processCodeloadingOption( state, args )

	return {
		message = "updateCodeLoading",
		value = success and "ok" or "malformed request"
	}
end

if supportsBreakpoints then
	-- In the dedicated debugger thread in lightroom we receive the http request
	-- that specifies we should set or clear a breakpoint.  Here we forward it to
	-- the all known universes.
	debugHandlers[ '/setClearBreakpoint' ] = function( args, debugQueue, state )
		local allThreads = state.allThreads
		local haltedThreads = state.haltedThreads

		local success, queuedParams = parseHttpArguments( 'setClearBreakpoint', sSetClearBreakpointArgs, args )

		if not success then
			return {
					message = 'error',
					value = queuedParams
			}
		end

		local set = queuedParams.set == 1

		local resultQueue = newTransitQueue()

		logger:debugf( "setClearBreakpoint %s : %d", queuedParams.chunkName, queuedParams.lineNumber )
		local command = {
			message = 'setClearBreakpoint',
			set = set,
			chunkName = queuedParams.chunkName,
			lineNumber = queuedParams.lineNumber,
			code = queuedParams.code,
			queue = resultQueue,
		}

		updateBreakpointTable( set, queuedParams, state.breakpoints )

		local pending = 0
		local haltedThreadMap = makeHaltedThreadMap( haltedThreads )

		for _, thread in ipairs( allThreads ) do

			local halted = haltedThreadMap[ thread.id ]
			if not halted then
				-- put on the queue for the periodic debugger hook to find
				logger:tracef( "Setting pending breakpoint for thread id %d", thread.id )
				thread.debuggeeQueue:enqueue( command )
				triggerUniverse( thread.luaState )
			end
			pending = pending + 1

		end

		for _, thread in ipairs( haltedThreads ) do

			logger:tracef( "Setting immediate breakpoint for thread %d", thread.id )
				-- we can add breakpoints to halted threads immediately
			local responseQueue = thread.queue
			responseQueue:enqueue( command )
			pending = pending + 1

		end

		local result
		for i = 1, pending do

			local nextResult = resultQueue:dequeue( true, 1.5 / pending )
			logger:tracef( "Setting breakpoint in running thread got result: %s", nextResult )
			if nextResult then
				pending = pending - 1
			end
			result = result or nextResult

		end

		logger:tracef( "Debugger thread got success message: %s", result )

		return {
			message = "setClearBreakpoint",
			value = {
				success = result,
				numThreads = #allThreads,
				numPending = pending,
			}
		}

	end
end

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

local function processHttpCommand( state, command, args, ip )

	logger:debugf( "Looking for handler for: %s", command )
	local handler = debugHandlers[ command ]
	local debugQueue = state.debugQueue

	if handler then
		logger:debugf("Invoking command %s w/ %s", command, tostring( args ) )
		return handler( args, debugQueue, state, ip )
	else
		return k404Handler( command, args )
	end

end

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

local function postHalideUrlMessage( state, url )
	state.debugQueue:enqueue {
		message = "halideUrl",
		url = url
	}
end

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

local function shouldLoadCode( state, chunkName )
	if not state.codeLoading then
		return false
	end

	if not state.baseUrl then
		return false
	end

	local existing = state.loadedChunks[ chunkName ]

	if type( existing ) == "string" then
		return false, existing
	end

	-- if existing is nil or true, return true, but
	-- a false value should disable loading
	return existing == nil or existing

end

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

local function createCodeRunnerFromMessage( message )
	if not message.id then
		return nil, "No universe specified."
	elseif type( message.action ) ~= "userdata" then
		return nil, "Expected an action object."
	elseif type( message.method ) ~= "string" then
		return nil, "Expected a method name."
	elseif type( message.action[ message.method ] ) ~= "function" then
		return nil, "Expected method to name a function."
	else
		local action = message.action
		local method = message.method
		return message.id, function()
			local trigger = action[ method ]
			trigger( action )
			return true
		end
	end
end

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

local function processMessage(
		state,
		message )

	local haltedThreads = state.haltedThreads
	local mainThreadCodeRunner = state.mainThreadCodeRunner
	local allThreads = state.allThreads

	if message.message == 'shutdown' then

		for _, thread in ipairs( haltedThreads ) do
			thread.queue:enqueue {
				stepIntent = "Run",
			}
		end

		if mainThreadCodeRunner then
			mainThreadCodeRunner.queue:enqueue( 'shutdown' )
			mainThreadCodeRunner.action:trigger()
		end

		return false

	elseif message.message == 'halideUrl' then

		logger:debugf( "Setting url to find Halide: %s", message.url )

		coroutine.yield( kActiveDebuggerHalideUrl, message.url )

	elseif message.message == 'mainThreadCodeRunner' then

		state.mainThreadCodeRunner = message.mainThreadCodeRunner

	elseif message.message == 'mainThreadCodeAction' then

		local id, runnerOrMessage = createCodeRunnerFromMessage( message )

		if id and runnerOrMessage then
			local threadIndex = lookupThreadById( state.allThreads, id )
			local thread = state.allThreads[ threadIndex ]
			if thread then
				thread.codeRunner = runnerOrMessage
			else
				logger:warn( "Failed to find thread in action message by id." )
			end
		else
			logger:warnf( "Invalid code action message: %s", runnerOrMessage )
		end

	elseif message.message == 'halt' then

		logger:debug( "halt received.  Waiting for debugger." )

		local nextQueue = message.responseQueue

		if not nextQueue then
			logger:error( "No response queue!" )
		else
			logger:trace( "Got response queue" )
		end

		if state.pausePending[ message.id ] then
			-- technically, this might not be the pause message, but clear this out anyway
			state.pausePending[ message.id ] = nil
		end

		table.insert( haltedThreads, {
			queue = nextQueue,
			id = message.id,
		} )

--		activateRemoteLuaDebugger( message.reason )

	elseif message.message == 'newThread' then

		logger:debugf( "Adding thread %d to pool (%s)", message.id, message.name )

		local thread = {
			name = message.name,
			debuggeeQueue = message.debuggeeQueue,
			codeQueue = message.codeQueue,
			id = message.id,
			luaState = message.luaState
		}

		if state.breakpoints then
			sendSetupMessages( state, thread )
		else
			-- set the thread aside until we have pulled an initial
			-- set of breakpoints to send
			table.insert( state.pendingThreads, thread )
		end

	elseif message.message == 'threadTerminate' then

		logger:debugf( "Removing thread %d from pool", message.id )
		for index, thread in ipairs( allThreads ) do
			if thread.id == message.id then
				table.remove( allThreads, index )
				thread.debuggeeQueue:enqueue {
					message = "threadTerminateAccept",
				}
			end
		end

	elseif message.message == 'loadChunk' then

		local shouldLoad, existing = shouldLoadCode( state, message.chunkName )
		if not shouldLoad and existing then -- we've got code for this chunk already
			message.responseQueue:enqueue {
				message = "loadChunk",
				chunkName = message.chunkName,
				code = existing
			}
		elseif shouldLoad then -- side load this one; here's the URL to use...
			-- universes may make overlapping requests for the same chunk, but
			-- that's fairly harmless and this involves less locking
			message.responseQueue:enqueue {
				message = "loadChunk",
				chunkName = message.chunkName,
				loadParameter = string.format( "%s%s/%s", state.baseUrl, "loadChunk", message.chunkName )
			}
		else -- do not side load this chunk
			message.responseQueue:enqueue {
				message = "loadChunk",
				chunkName = message.chunkName,
			}
		end

	elseif message.message == 'loadedChunk' then

		state.loadedChunks[ message.chunkName ] = message.code

	elseif message.message == 'profileSample' then

		local samples = state.profilerSamples
		if not samples then
			samples = { }
			state.profilerSamples = samples
		end

		samples[ #samples + 1 ] = message

	else

		logger:errorf( "Unknown debugger message: %s", message.message )

	end

	return true
end

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

local function drainDebugQueue(	state )

	logger:trace( "Drain debugQueue" )
	local message = state.debugQueue:dequeue( false )
	while message do
		logger:trace( "processMessage..." )
		if not processMessage(
			state,
			message ) then

			return false
		end

		message = state.debugQueue:dequeue( false )
	end

	return true

end

--==============================================================================

local function decodeQueryToTable( q )

	if not q then
		return q
	end

	local t = {}

	string.gsub( q, "([^&=]+)=([^&=]+)", function( k, v )
		-- fix + type of URI encoding
		v = string.replace( v, '+', ' ' )
		-- fix %20 type of URI encoding
		v = string.gsub( v, '%%(%x%x)', function( hex ) return string.char( tonumber( hex, 16 ) ) end )

		logger:debugf( "Query args: %s=%s", k, v )

		t[ k ] = v
	end )

	return t

end

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

local function decodeDataToTable( d )

	if type( d ) == 'string' then
		d = d .. " return s"
		d = loadstring( d, "debug query" )
	end
	if type( d ) == 'function' then
		setfenv( d, {} )
	end
	while type( d ) == 'function' do
		d = d()
	end

	return d

end


--==============================================================================

local function onePass( state, command, query, data, ip )

	local args = {}
	local request_id
	if data then
		args = decodeDataToTable( data )
	end
	if query then
		local qargs = decodeQueryToTable( query )
		for k, v in pairs( qargs ) do
			args[ k ] = v
		end
	end
	if command == true and type( args ) == 'table' and args.message then
		command = "/" .. args.message
		request_id = args.request_id
		args = args.args
	end

	-- compatibility with older Halide versions (and robustness in general)
	-- requires that we don't wait forever for this specific message before
	-- allowing the waiting universes to proceed
	if not state.breakpoints and command ~= "/initialBreakpoints" then
		logger:debug( "Halide did not provide initial breakpoints first, falling back." )
		installBreakpoints( state, { } )
	end

	if type( command ) ~= 'string' then
		logger:debugf( "No command received" )
		return true
	end

	logger:tracef( "Received command: %s (request id %s) from %s", command, request_id, ip )

	local filetype = 'lua'
	local _, _, nucmd, nufiletype = string.find( command, "(.+)%.([^.]+)" )
	if nucmd then
		command = nucmd
		filetype = nufiletype
	end

	local result, filetypeOverride, mime = processHttpCommand( state, command, args, ip )
	if filetypeOverride then
		filetype = filetypeOverride
	end

	if request_id then
		result = result or { value = result }
		result.request_id = request_id
	end

	result = encodeResult( result, filetype )

	if mime == nil and string.find( command, "%.txt" ) then
		mime = "text/plain"
	end

	if filetype == "json" then
		mime = "application/json"
	end

	mime = mime or "text/x-lua"

	logger:tracef( "Sending result: %s", result )

	local addlHeaders = string.format( "%s\r\nAccess-Control-Allow-Origin: *", mime )

	return result, addlHeaders
end

--==============================================================================

-- State:
local state = {
	debugQueue = params.debugQueue,
	haltedThreads = {},
	pausePending = {},
	allThreads = {},
	pendingThreads = {},
	loadedChunks = {},
	codeLoading = false
}

logger:debugf( "Init done." )

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

local nextRequest = coroutine.yield

-- Support for "active" clients which connect back to Halide, rather than
-- waiting for Halide to find them.
if activeDebugger then
	nextRequest = function( result, mime )
		local message = kResults
		if result == true then
			message = kIdle
		end
		logger:debugf( "yielding from KrDebugBottleneck: %s %s %s", message, result, mime )
		local data, stripHttpHeaders, extraCommand = coroutine.yield( message, result, mime )
		-- active clients poll, so there may not always be a message from Halide
		if data then
			if stripHttpHeaders then
				local _, _, lenStr = string.find( data, "Length: (%d+)" )
				if lenStr then
					local len = tonumber( lenStr )
					logger:tracef( "Removing HTTP headers using Content-Length: %d", len )
					data = string.sub( data, -len )
				end
			end
			return true, "", data
		elseif data == false then
			-- SPAM [dtull 2012-06-20]: this is only currently supported by the WinRT
			-- active debugger, but then WinRT is the only platform that uses that code
			-- path by default
			return false, "", "error"
		elseif extraCommand == "shutdown" then
			return data, stripHttpHeaders, extraCommand
		end
	end
end

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

local function breakpointUrl( optBaseUrl )

	if type( optBaseUrl ) ~= "string" then
		optBaseUrl = baseDebuggerUrl()
	end

	local port = tonumber( debuggerPort ) and debuggerPort or "none"

	return string.format( "%sgetBreakpoints?port=%s", optBaseUrl, port )

end

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

local function initBreakpoints( state, optBaseUrl )

	local bpUrl = breakpointUrl( optBaseUrl )
	logger:debugf( "Attempting to pull breakpoints from: %s", bpUrl )

	local points = fetchViaHttp( bpUrl )
	local success = not not points

	if not shouldPullBreakpoints then
		-- Pulling breakpoints doubles as a way to inform our preferred
		-- instance of Halide of our port and IP address, so this extra
		-- flag is used to suppress application of the breakpoints
		points = nil
	end

	local bpTable = decodeDataToTable( points )

	logger:debugf( "Breakpoints pulled: %d", points and #points or -1 )

	if type( bpTable ) == "table" then
		installBreakpoints( state, bpTable )
	else
		installBreakpoints( state, { } )

		if points == nil then
			logger:debug( "Breakpoints unavailable or not pulled." )
		else
			logger:debugf( "Unexpected breakpoint data: %s", tostring( points ) )
		end
	end


	return success
end

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

local function activeDebuggerUrl( optBaseUrl )

	if type( optBaseUrl ) ~= "string" then
		optBaseUrl = baseDebuggerUrl()
	end

	return string.format( "%sdebugger_call", optBaseUrl )

end

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

-- Request data:'
-- Don't use nextRequest() for this first call, as the first yield
-- is handled by AgDebugger.c, not the activeDebugger polling loop.

local optBaseUrl = config and config.config.AgDebugger.halideUrl

if activeDebugger then
	postHalideUrlMessage( state, activeDebuggerUrl( optBaseUrl ) )
else
	local halideAvailable

	halideAvailable = initBreakpoints( state, optBaseUrl )

	if halideAvailable then
		updateBaseUrl( state, optBaseUrl or baseDebuggerUrl(), true )
	end
end

local function initBreakOnThrowToSavedValue()
	if not config or not config.config then
		return
	end

	local configBreakOnThrow = config.config.AgDebugger.breakOnThrow
	if type( configBreakOnThrow ) == 'boolean' then
		logger:tracef( "config.AgDebugger.breakOnThrow is %s", configBreakOnThrow and "stop so you can see errors" or "don't stop for errors" )
		AgSetBreakOnThrow( configBreakOnThrow )
	end
end

local function attemptShutdown()
	local messages = { }

	if #state.allThreads > 0 then
		messages[ #messages + 1 ] = string.format( "Shutting down with %d universes still open!", #state.allThreads )
		for _, thread in ipairs( state.allThreads ) do
			messages[ #messages + 1 ] = string.format( "Open thread: %s (%d)", thread.name, thread.id )
		end

		logger:warn( table.concat( messages, "\n" ) )
		return false
	else
		return true
	end
end

initBreakOnThrowToSavedValue()

local url, query, data, ip = coroutine.yield()

while true do

	logger:debugf( "Got url: %s query: %s and data %s from %s", url, query, data, ip )

	if not url then
		if data == 'shutdown' then
			if attemptShutdown() then
				break
			end
		elseif data == "error" then
			logger:warn( "Error trying to contact Halide." )

			if activeDebugger and not state.breakpoints then
				logger:debug( "Initializing blank breakpoints since Halide is unavailable." )
				installBreakpoints( state, { } )
			end
		end
	end

	if not drainDebugQueue( state ) then
		break
	end

	local result, headers
	if url then
		result, headers = onePass( state, url, query, data, ip )
		if not result then
			break
		end
	end

	url, query, data, ip = nextRequest( result, headers )

end

logger:infof( "Shutdown" )
