-- see copyright notice in grldc.h

local debug = require( "debug" )
local net = require( "grldc.net" )
local socket = require( "grldc.socket" )
local utilities = require( "grldc.utilities" )

local assert = assert
local print = print
local type = type
local debug = debug
local xpcall = xpcall
local pcall = pcall
local tostring = tostring
local string = string
local table = table
local unpack = unpack
local error = error
local setmetatable = setmetatable
local getmetatable = getmetatable
local coroutine = coroutine
local pairs = pairs
local ipairs = ipairs
local loadstring = loadstring
local originalCoroutineCreate = coroutine.create
local globals = _G
local getfenv = getfenv
local setfenv = setfenv
local findfenv = getfenv

-- luabind functions
local class_info = class_info

if getfenv == nil then
	-- lua 5.2: no get/set environment
	findfenv = function( f )
		local idx = 1
		while true do
			local name, value = debug.getupvalue( f, idx )
			if name == nil then break end
			--print( "\""..name.."\"" )
			if name == "_ENV" then return value end
			idx = idx + 1
		end
		return nil
	end
	
	getfenv = function( f )
		local env = findfenv( f )
		if env == nil then
			error( "Can't find function environment" )
		end
		return env
	end
	
	assert( setfenv == nil )
	setfenv = function( f, env )
		local idx = 1
		while true do
			local name, value = debug.getupvalue( f, idx )
			if name == nil then break end
			--print( "\""..name.."\"" )
			if name == "_ENV" then
				debug.setupvalue( f, idx, env )
				return
			end
			idx = idx + 1
		end
		error( "Can't find function environment" )
	end
end

module( "grldc" )

local server = nil
local status = "running"

local hookActiveCount = 0

local callstack
local coroutines = {}
setmetatable( coroutines, { __mode = "k" } )

local commands = {}
local runningCommands = {} -- commands that can be issued even when the debugged code is running

local breakPoints = {}
local breakPointAliases = {}

internal_.init( breakPointAliases )

local values = {}
local proxyMeta = {}

local envMeta = {}

local function releaseValue( id )
	--print( "Releasing value with ID "..id )
	assert( values[id] ~= nil )
	values[id] = nil
end

local function splitValue( value )
	local t = type( value )
	if t == "nil" then
		return { type = t, short = tostring( value ) }
	elseif t == "number" then
		return value
	elseif t == "string" then
		local maxStringLength = 48
		if #value > maxStringLength then
			local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
			--print( "Created string value with ID "..id )
			values[id] = value
			return { type = t, short = "\""..string.sub( value, 1, maxStringLength-3 ).."\"...", id = id }
		else
			return value
		end
	elseif t == "boolean" then
		return value
	elseif t == "table" then
		local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
		--print( "Created table value with ID "..id )
		values[id] = value
		local res = { type = t, short = tostring( value ), id = id }
		if getmetatable( value ) == proxyMeta then
			res.type = "proxy"
			res.short = value.short
		end
		return res
	elseif t == "function" then
		local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
		--print( "Created function value with ID "..id )
		values[id] = value
		return { type = t, short = tostring( value ), id = id }
	elseif t == "thread" then
		return { type = t, short = tostring( value ) }
	elseif t == "userdata" then
		local m = getmetatable(value)
		if m and m.__luabind_class then
			local tostr = m.__tostring
			m.__tostring = nil -- temporarily disable tostring, so that we can get native lua info
			local ok, ptr = pcall(tostring,value)
			m.__tostring = tostr
			_, _, ptr = string.find( ptr, "userdata: (.+)" )
			local info = class_info( value )
			local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
			values[id] = value
			return { type = t, short = "[luabind] "..info.name..": "..ptr, id = id }
		end
		return { type = t, short = tostring(value) }
	end
end

local function getValue( id )
	local value = assert( values[id], "No value associated to ID "..tostring(id) )
	local t = type( value )
	local res
	if t == "table" and getmetatable( value ) == proxyMeta then
		res = {}
		for _, entry in ipairs( value ) do
			table.insert( res, { name = entry.name, value = splitValue( entry.value ) } )
		end
	elseif t == "table" then
		res = {}
		local meta = getmetatable( value )
		if meta ~= nil then
			table.insert( res, { name = "<metatable>", value = splitValue( meta ) } )
		end
		for k, v in pairs( value ) do
			local key = splitValue(k)
			local val = splitValue(v)
			if type( key ) == "table" and key.id ~= nil then -- the key is a complex value
				local proxy = { { name = "<key>", value = k }, { name = "<value>", value = v } }
				if type( val ) == "table" then
					proxy.short = val.short
				else
					if type( val ) == "string" then
						proxy.short = "\""..val.."\""
					else
						proxy.short = tostring( val )
					end
				end
				setmetatable( proxy, proxyMeta )
				proxy = splitValue( proxy )
				table.insert( res, { name = "["..key.short.."]", value = proxy } )
				releaseValue( key.id )
				if type( val ) == "table" and val.id ~= nil then
					releaseValue( val.id )
				end
			else
				local name
				if type( key ) == "table" then
					name = "["..key.short.."]"
				else
					local simpleKey = false
					if type( key ) == "string" then
						simpleKey = (string.find( key, "^[%a_][%a%d_]*$" ) ~= nil)
					end
					if simpleKey then
						name = key
					else
						local keyStr = tostring( key )
						if type( key ) == "string" then
							keyStr = "\""..keyStr.."\""
						end
						name = "["..keyStr.."]"
					end
				end
				table.insert( res, { name = name, value = val } )
			end
		end
	elseif t == "function" then
		res = {}
		local upvaluesProxy = {}
		setmetatable( upvaluesProxy, proxyMeta )
		local upIdx = 1
		while true do
			local upName, upValue = debug.getupvalue( value, upIdx )
			if upName == nil then break end
			table.insert( upvaluesProxy, { name = upIdx..": "..upName, value = upValue } )
			upIdx = upIdx + 1
		end
		local info = debug.getinfo( value, "S" )
		table.insert( res, { name = "<what>", value = splitValue( info.what ) } )
		if string.sub( info.source, 1, 1 ) == "@" then
			table.insert( res, { name = "<source>", value = splitValue( info.source.."("..info.linedefined..")" ) } )
		else
			table.insert( res, { name = "<source>", value = splitValue( info.source ) } )
		end
		table.insert( res, { name = "<environment>", value = splitValue( findfenv( value ) ) } )
		table.insert( res, { name = "<upvalues>", value = splitValue( upvaluesProxy ) } )
	elseif t == "string" then
		res = { { name = "<value>", value = value } }
	elseif t == "userdata" then
		local m = getmetatable( value )
		if m and m.__luabind_class then
			local info = class_info( value )
			local res = {}
			table.insert( res, { name = "<class methods>", value = splitValue( info.methods ) } )
			for _, attrName in pairs( info.attributes ) do
				table.insert( res, { name = attrName, value = splitValue( value[attrName] ) } )
			end
			return res
		else
			error( "Unknown value type: "..t.." (value = "..tostring( value )..")" )
		end
	else
		error( "Unknown value type: "..t.." (value = "..tostring( value )..")" )
	end
	if res[1] == nil then
		res[1] = { name = "<empty>" }
	end
	return res
end

local function checkClosed( f, ... )
	local results = { xpcall( f, function( msg ) if msg == "closed" then return msg end return debug.traceback( msg ) end ) }
	if not results[1] then
		if results[2] == "closed" then
			print( "Connection with debugger lost" )
			server = nil
		else
			error( results[2], 0 )
		end
		return ...
	end
	return unpack( results, 2 )
end

local function synchronize()
	print( "sending synchronization request..." )
	server:send( "synchronize" )
	print( "receiving breakpoints..." )
	local numBreakpoints = server:receive()
	print( tostring(numBreakpoints).." breakpoint(s)" )
	assert( numBreakpoints == 0 ) -- not yet implemented
	local breakOnConnection = server:receive()
	return breakOnConnection
end

function updateRunningRequests_()
	while true do
		if server == nil then break end
		local command = server:tryReceive( "running" )
		if command == nil then break end
		local func = runningCommands[command]
		assert( func ~= nil, "Unknown running command: "..tostring( command ) )
		local ok, msg = xpcall( func, debug.traceback )
		if not ok then
			print( "Error processing running command "..command..": "..tostring( msg ) )
		end
	end
end

function registerSourceFile_( fileName )
	local nsource = "@"..utilities.normalizePath( string.sub( fileName, 2 ) )
	s = breakPoints[nsource]
	if s == nil then
		s = {}
		breakPoints[nsource] = s
	end
	breakPointAliases[fileName] = s
	return s
end

local function getinfo( thread, level, what )
	if type(level) ~= "function" then
		level = level + 1 -- do not count ourself
	end
	if thread == nil then
		thread = getmainthread()
	end
	return debug.getinfo( thread, level, what )
end

local function getlocal( thread, level, idx )
	level = level + 1 -- do not count ourself
	if thread == nil then
		thread = getmainthread()
	end
	return debug.getlocal( thread, level, idx )
end

local function setlocal( thread, level, idx, value )
	level = level + 1 -- do not count ourself
	if thread == nil then
		thread = getmainthread()
	end
	return debug.setlocal( thread, level, idx, value )
end

function getAppLevel_( thread, fromLevel )
	-- find where the application code starts in the callstack (we want to ignore grldc functions)
	local level = (fromLevel or 1) + 1
	local appLevel
	local grldcFunction = { [breakNow] = true, [connect] = true, [globals.coroutine.create] = true, [updateRunningRequests_] = true }
	--[[print( "GRLDC functions:" )
	for f in pairs( grldcFunction ) do
		print( "\t"..tostring(f).." ("..tostring(getinfo(thread,f,"nf").name)..")" )
	end
	print( "Current stack:" )]]
	while true do
		local info = getinfo( thread, level, "f" )
		if info == nil then break end
		--print( "\t"..level.." "..tostring(info.func).." ("..tostring(info.name)..")" )
		if grldcFunction[info.func] then appLevel = level end -- actual appLevel is level + 1, but we don't count ourself
		level = level + 1
	end
	return appLevel
end

local function getCallstack( thread )
	local appLevel = getAppLevel_( thread )
	if appLevel == nil then appLevel = 0 end

	local callstack = {}
	local level = appLevel
	while true do
		local info = getinfo( thread, level, "nSl" )
		if info == nil then break end
		level = level + 1
		local data =
		{
			name = info.name,
			namewhat = info.namewhat,
			what = info.what,
			source = info.source,
			line = info.currentline,
		}
		table.insert( callstack, data )
	end
	return callstack
end

local function setHook()
	hookActiveCount = hookActiveCount + 1
	if hookActiveCount == 1 then
		internal_.setHookActive( true )
	end
end

local function removeHook()
	hookActiveCount = hookActiveCount - 1
	if hookActiveCount == 0 then
		internal_.setHookActive( false )
	end
	--debug.sethook( nil )
end

function suspendHook()
	removeHook()
end

function resumeHook()
	setHook()
end

local function registerCoroutine( co )
	--debug.sethook( co, hook, "crl" )
	internal_.setHook( co )
	coroutines[co] = {}
end

globals.coroutine.create = function( f )
	local co = originalCoroutineCreate( f )
	registerCoroutine( co )
	return co
end

local function setBreakPoint( source, line, value )
	assert( string.sub( source, 1, 1 ) == "@" )
	local nsource = "@"..utilities.normalizePath( string.sub( source, 2 ) )
	assert( nsource == source, "Source must be normalized before setting a breakpoint, but source "..source.." is not normalized to "..nsource )
	print( "Setting breakpoint at "..source.."("..line..") to "..tostring( value ) )
	local s = breakPoints[source]
	if s == nil then s = {} breakPoints[source] = s end
	if value then
		s[line] = true
	else
		s[line] = nil
	end
end

function connect( address, port, name, maxRetry )
	local retryCount = maxRetry
    assert( name ~= nil )
    assert( server == nil, "Already connected" )
    print( "grldc: connecting to GRLD server..." )
    while true do
        local ok, msg = pcall( function()
            server = net.connect( address, port )
        end )
        if ok then break end
        if not ok and msg ~= "connection refused" then
            error( msg )
        end
        if maxRetry ~= nil then
            retryCount = retryCount - 1
            if retryCount < 0 then
				print( "grldc: can't connect to GRLD server after "..(maxRetry+1).." attempt(s) ; debugging disabled" )
                return false
            end
        end
    end
    print( "grldc module connected to the GRLD server" )
    checkClosed( function()
        print( "sending client name..." )
        server:send( name )
        print( "synchronizing with server..." )
		local breakOnConnection = synchronize()
		local co, mainthread = coroutine.running()
		assert( co == nil or mainthread, "Connection to the debugger must be done from the main thread" )
		if mainthread then
			-- lua 5.2: we can access the main thread directly
			getmainthread = function() return co end
		end
        print( "setting debug hook..." )
        internal_.setHook( getmainthread() )
        print( "hook set" )
        setHook()
        if breakOnConnection then
            breakNow()
        end
    end )
    return true
end

local function breakNowImpl()
	assert( status == "running" )
	status = "break"
	internal_.setStepMode( 0, nil )
	server:send( "break" )
	--assert( server:receive() == "ack_break" )
	
	callstack = getCallstack( coroutine.running() )
	
	server:send( callstack[1].source )
	server:send( callstack[1].line )
	while status == "break" do
		--print( "waiting data..." )
		server:waitData()
		--print( "received data" )
		updateRunningRequests_()
		local command = server:tryReceive()
		if command ~= nil then
			assert( commands[command] ~= nil, "Received unknown command: "..tostring(command) )
			commands[command]()
		end
	end
	
	callstack = nil
end

local function getCoroutineId( co )
	if co == nil then
		return "main"
	else
		local _, _, id = string.find( tostring( co ), "thread: (.*)" )
		assert( id ~= nil )
		return id
	end
end

local function getCoroutineFromId( id )
	if id == "current" then
		return coroutine.running()
	else
		local co = nil
		if id ~= "main" then
			for c, info in pairs( coroutines ) do
				if coroutine.status( c ) ~= "dead" and id == getCoroutineId( c ) then
					co = c
					break
				end
			end
			if co == nil then
				return "no such coroutine"
			end
		end
		return co
	end
end

function commands.run()
	--server:send( "ack_run" )
	status = "running"
	--assert( stepMode == nil )
end

function commands.stepover()
	--server:send( "ack_stepover" )
	status = "running"
	internal_.setStepMode( 2, coroutine.running() or getmainthread() )
end

function commands.stepin()
	--server:send( "ack_stepin" )
	status = "running"
	internal_.setStepMode( 1, nil )
end

function commands.stepout()
	--server:send( "ack_stepout" )
	status = "running"
	internal_.setStepMode( 3, coroutine.running() or getmainthread() )
end

function commands.callstack()
	local thread = server:receive()
	if thread == "current" then
		server:send( callstack )
	else
		local co = getCoroutineFromId( thread )
		if type( co ) ~= "string" then
			server:send( getCallstack( co ) )
		else
			server:send( co )
		end
	end
end

function commands.coroutines()
	local res = {}
	for co, info in pairs( coroutines ) do
		if coroutine.status( co ) ~= "dead" then
			local id = getCoroutineId( co )
			table.insert( res, { id = id } )
		end
	end
	server:send( res )
end

function commands.currentthread()
	server:send( getCoroutineId( coroutine.running() ) )
end

function commands.breakpoints()
	server:send( breakPoints )
end

function commands.locals()
	local res = {}
	local thread = server:receive()
	local level = server:receive()
	local co = getCoroutineFromId( thread )
	if type( co ) ~= "string" then
		local idx = 1
		local appLevel = getAppLevel_( co, 1 )
		if appLevel == nil then
			appLevel = 0
		end
		level = level + appLevel - 1
		while true do
			local name, value = getlocal( co, level, idx )
			if name == nil then break end
			if name ~= "(*temporary)" then
				table.insert( res, { name = name, value = splitValue( value ) } )
			end
			idx = idx + 1
		end
		server:send( res )
	else
		server:send( "no such coroutine" )
	end
end

function commands.upvalues()
	local res = {}
	local thread = server:receive()
	local level = server:receive()
	local co = getCoroutineFromId( thread )
	if type( co ) ~= "string" then
		local idx = 1
		local appLevel = getAppLevel_( co, 1 )
		if appLevel == nil then
			appLevel = 0
		end
		level = level + appLevel - 1
		local info = getinfo( co, level, "f" )
		while true do
			local name, value = debug.getupvalue( info.func, idx )
			if name == nil then break end
			table.insert( res, { name = name, value = splitValue( value ) } )
			idx = idx + 1
		end
		server:send( res )
	else
		server:send( "no such coroutine" )
	end
end

function commands.evaluate()
	local expr = server:receive()
	local thread = server:receive()
	local level = server:receive()
	local co = getCoroutineFromId( thread )
	if type( co ) ~= "string" then
		if string.sub( expr, 1, 1 ) == "=" then
			expr = "return "..string.sub( expr, 2 )
		end
		local ok, results = pcall( function()
			local f = assert( loadstring( expr ) )
			
			local appLevel = getAppLevel_( co, 1 )
			if appLevel == nil then
				appLevel = 0
			end
			local orgLevel = level
			level = level + appLevel - 1
			local info = getinfo( co, level, "f" )
			
			local upvalues = {}
			local idx = 1
			while true do
				local name, value = debug.getupvalue( info.func, idx )
				if name == nil then break end
				upvalues[name] = idx
				idx = idx + 1
			end
			
			local locals = {}
			idx = 1
			while true do
				local name, value = getlocal( co, level, idx )
				if name == nil then break end
				locals[name] = idx
				idx = idx + 1
			end
			
			local env = setmetatable( { func = info.func, thread = co, level = orgLevel, locals = locals, upvalues = upvalues, environment = getfenv( info.func ) }, envMeta )
			setfenv( f, env )
			return { f() }
		end )
		if ok then
			local res = {}
			local lastResult = 0 -- TODO : check if there is a way to know the actual number of results, even if the last ones are nil values
			for idx, value in pairs( results ) do
				if idx > lastResult then lastResult = idx end
				res[idx] = { name = "result #"..tostring(idx), value = splitValue( value ) }
			end
			for idx = 1, lastResult - 1 do
				if res[idx] == nil then
					res[idx] = { name = "result #"..tostring(idx), value = splitValue( nil ) }
				end
			end
			if res[1] == nil then
				res[1] = { name = "<no result>" }
			end
			server:send( res )
		else
			server:send( { { name = "<error>", value = splitValue( results ) } } )
		end
	else
		server:send( { { name = "<error>", value = "no such coroutine" } } )
	end
end

envMeta.__index = function( self, key )
	if key == "__globals__" then
		return globals
	elseif key == "_G" then
		return self
	end
	
	local lv = self.locals[key]
	if lv ~= nil then
		local appLevel = getAppLevel_( self.thread, 1 )
		if appLevel == nil then
			appLevel = 0
		end
		level = self.level + appLevel - 1
		local k, v = getlocal( self.thread, level, lv )
		return v
	end
	
	local uv = self.upvalues[key]
	if uv ~= nil then
		local k, v = debug.getupvalue( self.func, uv )
		return v
	end
	
	return self.environment[key]
end

envMeta.__newindex = function( self, key, value )
	if key == "__globals__" then
		globals[key] = value
		return
	elseif key == "_G" then
		error( "Can't override _G when remotely evaluating an expression" )
	end
	
	local lv = self.locals[key]
	if lv ~= nil then
		local appLevel = getAppLevel_( self.thread, 1 )
		if appLevel == nil then
			appLevel = 0
		end
		level = self.level + appLevel - 1
		setlocal( self.thread, level, lv, value )
		return
	end
	
	local uv = self.upvalues[key]
	if uv ~= nil then
		debug.setupvalue( self.func, uv, value )
		return
	end
	
	self.environment[key] = value
end

function commands.getValue()
	local id = server:receive()
	server:send( getValue( id ) )
end

function runningCommands.releaseValue()
	local id = server:receive( "running" )
	releaseValue( id )
end

runningCommands["break"] = function()
	if status == "running" then
		breakNow()
	else
		print( "Break command ignored: already breaked" )
	end
end

runningCommands.setbreakpoint = function()
	local data = server:receive( "running" )
	setBreakPoint( data.source, data.line, data.value )
end

function breakNow()
	removeHook()
	print( "Breaking execution..." )
	while true do
		if server == nil then
			print( "Can't break execution: not connected to a debugger" )
			return
		end
		local ok, msg = xpcall( breakNowImpl,
			function( msg )
				if msg == "closed" then return msg end
				return debug.traceback( msg )
			end
		)
		if ok then
			break
		else
			if msg == "closed" then
				print( "Connection with debugger lost" )
				server = nil
				break
			else
				print( "Error during break: "..msg )
			end
		end
		socket.sleep( 0.1 )
		status = "running"
	end
	print( "Resuming execution..." )
	internal_.setStepDepth( 0 )
	setHook()
end
