--[[
______      ______
| __ || __ || __ |
||__||||__||||__||
| __ || __ || __ |
||__||||__||||__||
|    ||____||    |
     Metastack

Looking for documentation?
Check the reference manual!

]]

tArgs = {...}
if not tArgs[1] then error("No program given") end
tArgs[1] = shell.resolve(tArgs[1])

local text = ""
local stop = false

-- stats
local cycles = 0
local amountInstructions = 0
local size = 0
local currentStack = 1
-- i'm not sorry for this
local function parse(image)
	local tStacks = {}
	local i = 0
	for c in (image.."\n"):gmatch("(.-)\n") do
		local skip = 0
		for x = 1, #c do
			if skip > 0 then
				skip = skip - 1
			else
				tStacks[i] = tStacks[i] or setmetatable({}, {__index = function() return 0 end})
				if c:sub(x, x) == "\\" then -- escape a number value and add it literally. skip over the next 
					if c:sub(x+1, x+1) == "\\" then -- or escape a backslash
						tStacks[i][#tStacks[i]+1] = string.byte("\\")
						skip = 1
					else
						_, skip = c:sub(x+1):find("[+-]?%d*%.?%d*") -- find number
						if skip then -- slam and skip
							tStacks[i][#tStacks[i]+1] = tonumber(c:sub(x+1, x + skip)) or 0
						else -- can't escape this fucking shit yo!!!
							tStacks[i][#tStacks[i]+1] = string.byte(c, x, x)
							skip = 0
						end
					end
				else -- slam value into stack
					tStacks[i] = tStacks[i] or setmetatable({}, {__index = function() return 0 end})
					tStacks[i][#tStacks[i]+1] = string.byte(c, x, x)
				end
			end
		end
		tStacks[i] = tStacks[i] or setmetatable({}, {__index = function() return 0 end})
		setmetatable(tStacks[i], {__index = function() return 0 end})
		i = i + 1
	end
    return tStacks
end

local f, err = fs.open(tArgs[1], "r")
if err then error(err) end
-- command stack: every cycle pop 1 from the command stack and evaluate it.
-- input stack: read-only for input.
local stacks = parse(f.readAll())
stacks[-1] = {}
stacks[currentStack] = stacks[currentStack] or setmetatable({}, {__index = function() return 0 end})
f.close()
table.remove(tArgs, 1)

local noDraw = false
local noStatsExit = false

-- check for noDraw (\nd) or noStatsExit (\nse)
for k, v in ipairs(tArgs) do
	if v:sub(1, 1) == "\\" then
		if v:sub(2):lower() == "nd" then
			noDraw = true
			tArgs[k] = ""
		elseif v:sub(2):lower() == "nse" then
			noStatsExit = true
			tArgs[k] = ""
		end
	end
end
-- put arguments into input stack
for k, v in ipairs(tArgs) do
	if v:sub(1, 1) == "\\" then -- \ is also the escape character
		for c in v:sub(2):gfind(".") do
			table.insert(stacks[-1], 1, string.byte(c))
		end
		table.insert(stacks[-1], 1, -1)
	elseif tonumber(v) then
		table.insert(stacks[-1], 1, math.floor(tonumber(v)))
	elseif v ~= "" then
		for c in v:gfind(".") do
			table.insert(stacks[-1], 1, string.byte(c))
		end
		table.insert(stacks[-1], 1, -1)
	end
end
tArgs = nil

local function pop(amnt)
	local out = {}
	for i = 1, amnt do
		out[#out+1] = table.remove(stacks[currentStack], #stacks[currentStack]) or 0
	end
	return out
end
local function push(...)
	local input = {...}
	for i = 1, #input do
		table.insert(stacks[currentStack], input[i])
	end
end
local function commandPop(amnt)
	local out = {}
	for i = 1, amnt do
		out[#out+1] = table.remove(stacks[0], #stacks[0]) or 0
	end
	return out
end
local function commandPush(...)
	local input = {...}
	for i = 1, #input do
		table.insert(stacks[0], input[i])
	end
end
local function swapCommandAndStack(stackNum)
	local com = {}
	local stk = {}
	stacks[0] = stacks[0] or setmetatable({}, {__index = function() return 0 end})
	for k, v in pairs(stacks[0]) do
		com[k] = v
	end
	stacks[stackNum] = stacks[stackNum] or setmetatable({}, {__index = function() return 0 end})
	for k, v in pairs(stacks[stackNum]) do
		stk[k] = v
	end
	stacks[0] = stk
	stacks[stackNum] = com
end

local function draw()
	term.clear()
	term.setCursorPos(1, 1)
	function drawStack(given, text)
		term.setTextColor(colors.white)
		term.setBackgroundColor(colors.black)
		write(text)
		for k, v in ipairs(given) do
			if k == #given then 
				term.setBackgroundColor(colors.red)
			end
			if (v < 256) and (v > 0) and (v ~= 0) and (v ~= 9) and (v ~= 10) and (v ~= 13) and (v ~= 128) and (v ~= 160) then 
				write(string.char(v))
			else
				term.setTextColor(colors.blue)
				write("["..v.."]")
			end
			term.setTextColor(colors.white)
			term.setBackgroundColor(colors.black)
		end
		print()
	end
	for k, v in pairs(stacks) do
		_G.stacksdump = stacks
		_G.kdump = k
		_G.vdump = v
		drawStack(v, (k == 0 and "command " or (k == -1 and "input " or "")).."stack"..((k == 0 or k == -1) and "" or " "..k)..(k == currentStack and ">" or ":").." ")
	end
	print("out: "..text)
	term.write("cycles: "..cycles.." | size: "..amountInstructions.." | area: "..size)
end
local dictionary = {
	[11] = function() -- Seek from bottom and move to the top
		local n = pop(1)
		push(table.remove(stacks[currentStack], 1+n[1]))
	end,
	[12] = function() -- Seek from the top and move to the top
		local n = pop(1)
		push(table.remove(stacks[currentStack], #stacks[currentStack]-n[1]))
	end,
	[19] = function() local n = pop(1); push(n[1] == 0 and 0 or 1) end, -- not not
	[20] = function() push(#stacks[currentStack]) end, -- Push current stack length (if equal to 0 then eos)
	[33] = function() local n = pop(1); push(n[1] == 0 and 1 or 0) end, -- not
	[37] = function() -- modulo
		local n = pop(2)
		push(n[1] == 0 and 0 or n[2] % n[1]) 
	end,
	[42] = function() local n = pop(2); push(n[2] * n[1]) end, -- multiply
	[43] = function() local n = pop(2); push(n[2] + n[1]) end, -- add
	[44] = function() -- ask for input text (push character bytes into the input stack)
		local w, h = term.getSize()
		if not noDraw then term.setCursorPos(1, h) end
		term.clearLine()
		write("input txt: ")
		local given = read()
		local t = {}
		for c in given:gfind(".") do
			table.insert(t, string.byte(c))
		end
		table.insert(t, -1)
		for i = #t, 1, -1 do
			table.insert(stacks[-1], t[i])
		end
	end,
	[45] = function() local n = pop(2); push(n[2] - n[1]) end, -- subtract
	[46] = function() local n = pop(1); if (n[1] <= 255) and (n[1] >= 0) then text = text..string.char(n[1]) end end, -- write charred
	[47] = function() -- divide
		local n = pop(2)
		push(n[1] == 0 and 0 or n[2] / n[1])
	end,
	-- push number to stack
	[48] = function() push(0) end,
	[49] = function() push(1) end,
	[50] = function() push(2) end,
	[51] = function() push(3) end,
	[52] = function() push(4) end,
	[53] = function() push(5) end,
	[54] = function() push(6) end,
	[55] = function() push(7) end,
	[56] = function() push(8) end,
	[57] = function() push(9) end,
	[58] = function() text = text..pop(1)[1] end, -- write number
	[59] = function() -- ask for input number (push number into input stack)
		local w, h = term.getSize()
		if not noDraw then term.setCursorPos(1, h) end
		term.clearLine()
		write("input num: ")
		table.insert(stacks[-1], tonumber(read()) or 0)
	end,
	[60] = function() local n = pop(2); push(n[2] < n[1] and 1 or 0) end, -- lesser than
	[61] = function() local n = pop(2) push(n[2] == n[1] and 1 or 0) end, -- equals
	[62] = function() local n = pop(2); push(n[2] > n[1] and 1 or 0) end, -- greater than
	[63] = function() -- Command If (pop 3: a, b, and c. if a is truthy (not 0) then swap command stack and b, else swap command stack and c)
		local n = pop(3)
		if n[3] ~= 0 then
			swapCommandAndStack(n[2])
		else
			swapCommandAndStack(n[1])
		end
		stacks[currentStack] = stacks[currentStack] or setmetatable({}, {__index = function() return 0 end})
		stacks[0] = stacks[0] or setmetatable({}, {__index = function() return 0 end})
	end,
	[64] = function() -- Unconditional change current stack
		currentStack = pop(1)[1]
		stacks[currentStack] = stacks[currentStack] or setmetatable({}, {__index = function() return 0 end})
	end,
	[69] = function() -- super eval: pop 1 as n and then pop 'n' push n into command
		local n = pop(1)
		commandPush(table.unpack(pop(n[1])))
	end,
	[71] = function() -- grab
		local n = pop(2) 
		push(string.byte(stacks[n[1]][n[2]]))
	end,
	[73] = function() --Grab from input stack
		push(table.remove(stacks[-1], #stacks[-1]) or 0)
	end,
	[92] = function() pop(1) end, -- void
	[95] = function() push(math.floor(pop(1)[1])) end, -- floor
	[101] = function() -- eval: pop 1 and push it into command
		commandPush(pop(1)[1])
	end,
	[103] = function() -- grab synonym
		local n = pop(2) 
		push(string.byte(stacks[n[1]][n[2]]))
	end,
	[105] = function() -- input grab synonym
		push(table.remove(stacks[-1], #stacks[-1]) or 0)
	end,
	[182] = function() push(#stacks[currentStack]) end, -- Stack length synonym
	[191] = function() -- Stack if (pop 3: a, b, and c. if a is truthy (not 0) then change current stack to b, else change current stack to c.)
		local n = pop(3)
		if n[3] ~= 0 then
			currentStack = n[2]
		else
			currentStack = n[1]
		end
		stacks[currentStack] = stacks[currentStack] or setmetatable({}, {__index = function() return 0 end})
	end,
	[236] = function() -- clone (pop -> current)
		local n = pop(1)
		for k, v in pairs(stacks[n[1]]) do
			stacks[currentStack][k] = v
		end
	end,
	[237] = function() -- clone (current -> pop)
		local n = pop(1)
		for k, v in pairs(stacks[n[1]]) do
			stacks[currentStack][k] = v
		end
	end,
	[238] = function() -- recurse (clone to bottom of command)
		local n = pop(1)
		local t = {}
		stacks[n[1]] = stacks[n[1]] or setmetatable({}, {__index = function() return 0 end})
		for k, v in ipairs(stacks[n[1]]) do -- copy for safety
			t[k] = v
		end
		for k = #t, 1, -1 do
			table.insert(stacks[0], 1, t[k])
		end
	end,
	[239] = function() -- clone (a -> b)
		local n = pop(2)
		for k, v in pairs(stacks[n[1]]) do
			stacks[n[2]][k] = v
		end
	end,
	[247] = function() push(stacks[currentStack][#stacks[currentStack]]) end, -- dupe top
	[255] = function() stop = true end, -- exit
}
setmetatable(dictionary, {__index = function() return function() end end})
if not noDraw then draw() end

while true do
	if not noDraw then local _, key = os.pullEvent("key"); if key == keys.backspace then break end end
	if #stacks[0] == 0 then
		os.sleep()
		break -- empty command stack guarantees idling forever. so stop right now. but wait atleast one tick to show final state
	end
	local n = commandPop(1)
	if n[1] then dictionary[n[1]]() end
	if stop then break end -- ÿ
	cycles = cycles + 1
	local stacksize = 0
	for k, v in pairs(stacks) do 
		if k ~= 0 then stacksize = stacksize + #v end
	end
	if stacksize > size then
		size = stacksize
	end
	if #stacks[0] > amountInstructions then
		amountInstructions = #stacks[0]
	end
	if not noDraw then 
		draw() 
	end
end
if not (noStatsExit or noDraw) then
	term.clear()
	term.setCursorPos(1,1)
end
if not noStatsExit then
	print("cycles: "..cycles.." | size: "..amountInstructions.." | area: "..size.."\nprinting output:")
end
print(text)