local sideNames = rs.getSides()
local sensorSides = {}
local sideSelection, targetSelection, targetOffset, detailOffset = 1, 1, 1, 1
local detailLines, targetNameMenuTable
local graphing, graphSide, graphTarget, graphMatch, graphInstance = false

os.loadAPI("ocs/apis/graph")
os.loadAPI("ocs/apis/sensor")

local function checkSensors()
	for _,side in ipairs(sideNames) do
		if peripheral.getType(side) == "sensor" then
			sensorSides[side] = true
		else
			sensorSides[side] = false
		end
	end
end

local function writeEntry(menuTable, index, cursorPos)
	if cursorPos == index then
		term.setBackgroundColor(term.isColor() and colors.blue or colors.white)
		term.setTextColor(term.isColor() and colors.white or colors.black)
		term.write(string.sub(menuTable[index], 1, 16))
		term.setBackgroundColor(colors.black)
		term.setTextColor(colors.white)
	else
		term.write(string.sub(menuTable[index], 1, 16))
	end
end

local function toLines(currTable, linesTable, trackingTable, depth)
	for k,v in pairs(currTable) do
		if type(v) == "table" then
			table.insert(linesTable, string.rep(" ", depth)..tostring(k)..":")
			if trackingTable[v] then
				table.insert(linesTable, string.rep(" ", depth + 1).."<Cyclic Reference: "..trackingTable[v]..">")
			else
				trackingTable[v] = #linesTable
				toLines(v, linesTable, trackingTable, depth + 1)
			end
		else
			table.insert(linesTable, string.rep(" ", depth)..tostring(k).."> "..tostring(v))
		end
	end
end

local function drawDividerDown(startY)
	local w, h = term.getSize()
	for i=startY, h do
		term.setCursorPos(17, i)
		term.write("|")
	end
end

local function redraw()
	w, h = term.getSize()
	--pre-fetch sensor targets and detailed target information.
	local targetNames = nil
	detailLines = {}
	checkSensors()
	if sensorSides[sideNames[sideSelection]] then
		targetNames = sensor.call(sideNames[sideSelection], "getSensorName") and sensor.call(sideNames[sideSelection], "getTargets")
		targetNameMenuTable = {}
		if targetNames then
			for k,v in pairs(targetNames) do
				table.insert(targetNameMenuTable, k)
			end
			table.sort(targetNameMenuTable)
			if #targetNameMenuTable > 0 then
				toLines(sensor.call(sideNames[sideSelection], "getTargetDetails", targetNameMenuTable[targetSelection]), detailLines, {}, 0)
			end
		end
	end
	--now draw the screen.
	term.clear()
	term.setCursorPos(1, 1)
	term.write("=Sensor Info Viewer="..string.rep("=", w - 20))
	term.setCursorPos(1, 2)
	for n,side in ipairs(sideNames) do
		if n == sideSelection then
			term.setBackgroundColor(term.isColor() and colors.blue or colors.white)
			term.setTextColor(term.isColor() and colors.white or colors.black)
			term.write(side)
			term.setBackgroundColor(colors.black)
			term.setTextColor(colors.white)
			term.write(" ")
		else
			term.write(side.." ")
		end
	end
	term.setCursorPos(1, 3)
	term.write("-Targets--------+-Info-"..string.rep("-", w - 23))

	if targetNames then
		--make sure we have valid targets, even if we have a valid sensor.
		if #targetNameMenuTable > 0 then
			term.setCursorPos(1, 4)
			if targetOffset > 1 then
				term.write("/\\")
			else
				writeEntry(targetNameMenuTable, 1, targetSelection)	
			end
			--h-5 to leave room for top and bottom entries.
			for i=1, math.min(h - 5, #targetNameMenuTable - 1) do
				term.setCursorPos(1, i + 4)
				writeEntry(targetNameMenuTable, targetOffset + i, targetSelection)
			end
			if #targetNameMenuTable >= h then
				term.setCursorPos(1, h)
				if #targetNameMenuTable > targetOffset + h - 4 then
					term.write("\\/")
				else
					writeEntry(targetNameMenuTable, #targetNameMenuTable, targetSelection)
				end
			end

			--detailed info.
			for i=1, math.min(h - 3, #detailLines - ((detailOffset - 1) * (h - 3))) do
				term.setCursorPos(17, i + 3)
				term.write("|"..string.sub(detailLines[(detailOffset - 1) * (h - 3) + i], 1, w - 17))
			end
			local currX, currY = term.getCursorPos()
			drawDividerDown(currY + 1)
		else
			term.setCursorPos(1, 4)
			term.write("No targets found|")
			drawDividerDown(5)
		end
	else
		if peripheral.getType(sideNames[sideSelection]) == "sensor" then
			term.setCursorPos(1, 4)
			term.write("No sensor card  |")
			drawDividerDown(5)
		else
			term.setCursorPos(1, 4)
			term.write("No sensor found |")
			drawDividerDown(5)
		end
	end
	term.setCursorPos(1, h)
end

local function findGraphMatch(currTable, target, matchCount, trackingTable)
	for k, v in pairs(currTable) do
		if type(v) == "table" then
			if trackingTable[v] then return false end
			trackingTable[v] = true
			ret, path, count = findGraphMatch(v, target, matchCount, trackingTable)
			if ret and path then
				return ret, tostring(k).."-"..path
			elseif count then
				matchCount = count
			end
		elseif type(v) == "number" then
			matchCount = matchCount + 1
			if matchCount == target then return v, tostring(k) end
		end
	end
	return false, nil, matchCount
end

local function createGraph(targetNum)
	local monSide = ""
	for k, side in ipairs(rs.getSides()) do
		if peripheral.getType(side) == "monitor" then
			monSide = side
			break
		end
	end
	if monSide and targetNum >= 1 and findGraphMatch(sensor.call(graphSide, "getTargetDetails", graphTarget), targetNum, 0, {}) then
		graphMatch = targetNum
		local val, name = findGraphMatch(sensor.call(graphSide, "getTargetDetails", graphTarget), targetNum, 0, {})
		local updateFunc = function() return (findGraphMatch(sensor.call(graphSide, "getTargetDetails", graphTarget), graphMatch, 0, {})) end
		graphInst = graph.new(peripheral.wrap(monSide), updateFunc, name)
		return graphInst
	end
end

while true do
	redraw()
	local e, p1 = os.pullEvent()
	if e == "key" then
		local w, h = term.getSize()
		if p1 == 203 then
			--left, selects previous side
			if sideSelection > 1 then
				sideSelection = sideSelection - 1
				targetSelection = 1
				targetOffset = 1
				detailOffset = 1
				detailLines = nil
			end
		elseif p1 == 205 then
			--right, selects next side
			if sideSelection < 6 then
				sideSelection = sideSelection + 1
				targetSelection = 1
				targetOffset = 1
				detailOffset = 1
				detailLines = nil
			end
		elseif p1 == 200 then
			--up, selects previous target, adjusting offset if necessary.
			if targetSelection > 1 then
				if targetSelection - targetOffset + 1 == 2 and targetOffset > 1 then
					targetOffset = targetOffset - 1
				end
				targetSelection = targetSelection - 1
				detailOffset = 1
				detailLines = nil
			end
		elseif p1 == 208 then
			--down, selects next target, adjusting offset if necessary.
			if targetNameMenuTable and targetSelection < #targetNameMenuTable then
				if targetSelection - targetOffset + 1 == h - 4 and targetSelection ~= #targetNameMenuTable - 1 then
					targetOffset = targetOffset + 1
				end
				targetSelection = targetSelection + 1
				detailOffset = 1
				detailLines = nil
			end
		elseif p1 == 201 then
			--pgup, moves detail
			if detailOffset > 1 then
				detailOffset = detailOffset - 1
			end
		elseif p1 == 209 then
			--pgdown, moves detail
			local w, h = term.getSize()
			if detailLines and detailOffset < math.ceil(#detailLines / (h - 3)) then
				detailOffset = detailOffset + 1
			end
		--and now, since redraw() will eat char events with the change to sensor.call:
		elseif p1 == 31 then --s
			if detailLines then
				local fileHandle = io.open("sensorDetailed-"..sideNames[sideSelection].."-"..targetNameMenuTable[targetSelection], "w")
				if fileHandle then
					for k, v in ipairs(detailLines) do
						fileHandle:write(v.."\n")
					end
					fileHandle:close()
				end
			end
		elseif p1 == 16 then --q
			sleep(0)
			return
		elseif p1 == 34 then --g
			if graph then
				graphing = not graphing
				if not graphing then
					graphSide = nil
					graphTarget = nil
					graphUpdate = nil
				else
					graphSide = sideNames[sideSelection]
					graphTarget = targetNameMenuTable[targetSelection]
					graphInstance = createGraph(1)
					graphUpdate = os.startTimer(0.5)
				end
			end
		elseif p1 == 49 then --n
			if graphing then
				local newGraph = createGraph(graphMatch + 1)
				if newGraph then
					graphInstance = newGraph
					graphUpdate = os.startTimer(0.5)
				end
			end
		elseif p1 == 25 then --p
			if graphing then
				local newGraph = createGraph(graphMatch - 1)
				if newGraph then
					graphInstance = newGraph
					graphUpdate = os.startTimer(0.5)
				end
			end
		end
	elseif e == "timer" then
		if p1 == graphUpdate then
			graphInstance:draw()
			graphUpdate = os.startTimer(0.5)
		end
	end
end