-- shapescape

-- somehow you need to detect whether the inputted argument is the path from require or a path from shell to execute a .scape file
-- i guess the best way to do that is to just check if the file extension is .scape
-- lol yea

-- also if the file extension IS .scape, that means the API path is actually shell.getRunningProgram()

local tArgs = {...}
local shapescape = {utils={},slide={},scape={},generic={}}

local scapeRef = {}
local slideRef = {}

local corFilterCache = {}

local doExecute
	
if type(tArgs[1]) == "string" and string.find(tArgs[1],"%.%w+$") and string.sub(tArgs[1],string.find(tArgs[1],"%.%w+$")) == ".scape" then
	-- execute file
	-- somehow
	doExecute = tArgs[1]
	shapescape.path = shell.getRunningProgram()
else
	shapescape.path = tArgs[2] or tArgs[1]
end

if shapescape.path then
	shapescape.path = "/"..fs.getDir(shapescape.path)
end

do -- imports
	local function doImport(t,k)
		local n
		for k,v in pairs(shapescape) do
			if v == t then
				n = k
				break
			end
		end
		if not n then
			error("Module not found",2)
		end
		shapescape[n] = require("/"..fs.combine(shapescape.path,n))
		if shapescape[n].init then
			shapescape[n].init(shapescape)
		end
		for k,v in pairs(shapescape[n]) do
			t[k] = v
		end
		return shapescape[n][k]
	end

	local function getFileType(filename)
		if string.find(filename,"%.%w+$") == nil then
			return ""
		else
			return string.sub(filename,string.find(filename,"%.%w+$"))
		end
	end



	local function getFileName(filename,ext)
		local f = filename
		if string.find(filename," ") then
			f = string.sub(filename,1,({string.find(filename," ")})[1]-1)
		end
		f = fs.getName(f)
		if not ext or getFileType(f) == ".llnk" then
			f = string.sub(f,1,string.len(f)-string.len(getFileType(f)))
		end
		return string.gsub(f,"_"," ")
	end

	local ls = fs.list(shapescape.path)
	for i,f in ipairs(ls) do
		if f ~= "init.lua" then
			local n = getFileName(f)
			shapescape[n] = {import=function() doImport(shapescape[n]) end}
			setmetatable(shapescape[n],{__index=doImport})
		end
	end
end



-- [[ UTILITIES API ]]
local utils = shapescape.utils
utils.wordwrap = function(txt,tWidth)
	local lines = {}
	for line in txt:gmatch("([^\n]*)\n?") do
		table.insert(lines,"")
		for word in line:gmatch("%S*%s?") do
			local width
			if type(tWidth) == "table" then
				if not tWidth[#lines] then
					lines[#lines] = nil
					return lines
				else
					width = tWidth[#lines]
				end
			else
				width = tWidth
			end
			if #lines[#lines]+#word > width and #(lines[#lines]:gsub(" ","")) > 0 then
				lines[#lines+1] = ""
			end
			if #lines[#lines]+#word > width then
				local tWord = word
				while #lines[#lines]+#tWord > width do
					lines[#lines] = tWord:sub(1,width)
					table.insert(lines,"")
					tWord = tWord:sub(width+1)
				end
				lines[#lines] = tWord
			else
				lines[#lines] = lines[#lines]..word
			end
		end
	end
	if lines[#lines] == "" then
		lines[#lines] = nil
	end
	if txt:sub(#txt) == "\n" then
		table.insert(lines,"")
	end
	while type(tWidth) == "table" and #lines > #tWidth do
		lines[#lines] = nil
	end
	return lines
end

local animationTbl = {}
utils.renderImage = function(spr,x,y,format,transparency)
	local format
	local img = spr
	local cterm = term.current()
	if not format then
		if type(spr) == "string" then
			format = "nfp"
		elseif type(spr) == "table" then
			format = "bImg"
			if type(spr[1][1]) == "table" then
				if #spr == 1 or not spr.animated then
					spr = spr[1]
				else
					if not animationTbl[spr] then
						animationTbl[spr] = {cFrame=0,oTime=os.epoch("utc")/1000}
					else
						local cFrame = animationTbl[spr].cFrame+0.0001
						local dur = spr[math.ceil(cFrame)].duration or spr.secondsPerFrame or 0.05
						local oTime = animationTbl[spr].oTime
						local cTime = os.epoch("utc")/1000
						local delta = (cTime-oTime)
						animationTbl[spr].cFrame = (cFrame + delta/dur) % #spr
						animationTbl[spr].oTime = cTime
					end
					spr = spr[math.ceil(animationTbl[spr].cFrame+0.0001)]
				end
			end
		end
	end
	if format == "bImg" or format == "lImg" then
		if not spr[1] or not spr[1][1] or type(spr[1][1]) ~= "string" then
			error("Unrecognized format",2)
		end
		local sW,sH = #spr[1][1],#spr
		local w,h = term.getSize()
		for l=1,#spr do
			--[[if not y then
				term.setCursorPos(math.ceil(w/2)-math.floor(sW/2),(math.ceil(h/2)-math.floor(sH/2)+(l-1)))
			else
				term.setCursorPos(x,y+(l-1))
			end]]
			local line
			if y then
				line = y+(l-1)
			end
			local cX,cY = x or math.ceil(w/2)-math.floor(sW/2), line or (math.ceil(h/2)-math.floor(sH/2)+(l-1))
			term.setCursorPos(cX,cY)
			local bl = {}
			bl[1] = spr[l][1]
			if transparency then
				bl[2] = spr[l][2]
				bl[3] = spr[l][3]
				-- thing
				local line
				if cY >= 1 and cY <= h then
					line = {cterm.getLine(cY)}
					for t=1,3 do
						line[t] = line[t]:sub(cX,cX+(#spr[l][1]-1))
					end
				else
					line = {string.rep("f",#spr[l][1])}
					line[2] = line[1]
					line[3] = line[2]
				end
				if #line[2] < #spr[l][1] then
					line[1] = line[1]..string.rep(" ",#spr[l][1]-#line[1])
					line[2] = line[2]..string.rep("f",#spr[l][1]-#line[2])
					line[3] = line[3]..string.rep("f",#spr[l][1]-#line[3])
				end
				local start,final = 0,0
				while true do
					start,final = bl[2]:gsub(" ","T"):find("T")
					if start then
						bl[2] = bl[2]:sub(1,start-1)..line[3]:sub(start,final)..bl[2]:sub(final+1,#bl[2])
					else
						break
					end
				end
				while true do
					start,final = bl[3]:gsub(" ","T"):find("T")
					if start then
						if bl[1]:sub(start,final) == " " or bl[1]:sub(start,final) == "\128" then
							bl[1] = bl[1]:sub(1,start-1)..line[1]:sub(start,final)..bl[1]:sub(final+1,#bl[1])
							bl[2] = bl[2]:sub(1,start-1)..line[2]:sub(start,final)..bl[2]:sub(final+1,#bl[2])
						end
						bl[3] = bl[3]:sub(1,start-1)..line[3]:sub(start,final)..bl[3]:sub(final+1,#bl[3])
					else
						break
					end
				end
			else
				bl[2] = string.gsub(spr[l][2]:gsub(" ","T"),"T",utils.toBlit(term.getBackgroundColor()))
				bl[3] = string.gsub(spr[l][3]:gsub(" ","T"),"T",utils.toBlit(term.getBackgroundColor()))
			end
			term.blit(unpack(bl))
		end
	elseif format == "nfp" or format == "nfg" then
		local b,e = string.find(spr,"\n")
		local sW,sH
		local w,h = term.getSize()
		local lines,sW = getLines(spr)
		sH = #lines
		for l=1,sH do
			local line
			if y then
				line = y+(l-1)
			end
			term.setCursorPos(x or math.ceil(w/2)-math.floor(sW/2), line or math.ceil(h/2)-math.floor(sH/2)+(l-1))
			for lX=1,sW do
				local ch = lines[l]:sub(lX,lX)
				if ch ~= " " and ch ~= "" then
					term.blit(" ","f",ch)
				else
					local cx,cy = term.getCursorPos()
					term.setCursorPos(cx+1,cy)
				end
			end
		end
	end
end

utils.instantiate = function(orig)
	local function deepCopy(o, seen) -- so that "seen" doesn't appear in autocomplete
		seen = seen or {}
		if seen[o] then
			return seen[o]
		end
		local copy
		if type(o) == 'table' then
			copy = {}
			seen[o] = copy
			for k,v in pairs(o) do
				copy[deepCopy(k, seen)] = deepCopy(v, seen)
			end
			setmetatable(copy, deepCopy(getmetatable(o), seen))
		else
			copy = o
		end
		return copy
	end
	return deepCopy(orig)
end

local to_colors, to_blit = {}, {}
for i = 1, 16 do
	to_blit[2^(i-1)] = ("0123456789abcdef"):sub(i, i)
	to_colors[("0123456789abcdef"):sub(i, i)] = 2^(i-1)
end

utils.toColor = function(theblit)
	return to_colors[theblit] or nil
end

utils.toBlit = function(thecolor)
	return to_blit[thecolor] or nil
end

utils.fread = function(filepath)
	local fread = fs.open(filepath,"r")
	local thing = fread.readAll()
	fread.close()
	return thing
end

utils.locateEntry = function(tbl,value)
	for k,v in pairs(tbl) do
		if v == value then
			return k
		end
	end
end

utils.genStatelessIterator = function(...)
	local tbls = {...}
	local function iter(tbl)
		local temp = {}
		for i=#tbls,1 do
			for k,v in pairs(tbls[i]) do
				temp[k] = v
			end
		end
		return next,temp
	end
	return iter
end

-- [[ LOCAL API ]]
shapescape.debug = {scapeRef=scapeRef,slideRef=slideRef}
if not lOS.pDebug then
	lOS.pDebug = {}
end
if not lOS.pDebug.scapes then
	lOS.pDebug.scapes = {}
end
shapescape.debug.scapes = lOS.pDebug.scapes
local dLocal = shapescape.debug

local function runListener(listeners,scape)
	for k=#listeners,1,-1 do
		local func = listeners[k]
		local ok,err = pcall(func,unpack(scape.args))
		if not ok then
			if not scape.blacklist then
				scape.blacklist = {}
			end
			if scape.blacklist[func] then
				table.remove(listeners,k)
			else
				scape.blacklist[func] = true
				scape:warn(err)
			end
		end
	end
end
dLocal.runListener = runListener

local pointerCache = {}
dLocal.pointerCache = {}
local function runPointer(pointerValue,scape,self)
	local pointer = self.pointers[pointerValue]
	local ret
	if type(pointer) == "string" or pointer.type == "formula" then
		local formula
		if type(pointer) == "string" then
			formula = pointer
		else
			formula = pointer.content
		end
		local env = {
			self = self,
			-- load shapescape api or something too
			math = math,
		}
		env.width,env.height = scape.slides[scape.currentSlide]:getSize()
		setmetatable(env,{__index=scape.variables})
		local id = self.name or self.id
		local func,err = load("return "..formula,"@"..id.."."..pointerValue, nil, env)
		if not func then
			func,err = load(formula,"@"..id.."."..pointerValue, nil, env)
		end
		if not func then
			scape:warn(err)
		else
			local ok,output = pcall(func)
			if ok then
				if pointerValue == "text" and type(output) == "string" then
					output = {content=output, color=self.textColor or colors.white}
				elseif (pointerValue == "fill" or pointerValue == "border") and type(output) == "number" then
					output = {color=output}
				end
				ret = output
			else
				scape:warn(output)
				ret = 0
			end
		end
	elseif pointer.type == "variable" then
		-- parse dots
		ret = scape.variables[pointer.content]
	end
	if not pointerCache[self] then
		pointerCache[self] = {}
	end
	if pointerCache[self][pointerValue] ~= ret then
		pointerCache[self][pointerValue] = ret
		self:markAsChanged()
	end
	return ret
end
dLocal.runPointer = runPointer

local function deleted(t,k)
	if k == "deleted" or k == "destroyed" then
		return t
	else
		error("This object no longer exists.",2)
	end
end
dLocal.deleted = deleted

local function captureCursor()
	local cursor = {}
	cursor.x,cursor.y = term.getCursorPos()
	cursor.color = term.getTextColor()
	cursor.blink = term.getCursorBlink()
	return cursor
end

local function restoreCursor(cursor)
	term.setCursorPos(cursor.x,cursor.y)
	term.setTextColor(cursor.color)
	term.setCursorBlink(cursor.blink)
end

local function resumeCoroutines(self,...)
	local ev = {...}
	local scape = self:getScape()
	for i=#self.coroutines,1,-1 do
		local cor = self.coroutines[i]
		if coroutine.status(cor) == "dead" then
			table.remove(self.coroutines,i)
		elseif not (corFilterCache[cor] and ev[1] ~= corFilterCache[cor]) then
			local oterm = term.current()
			if self.window then
				term.redirect(self.window)
			end
			local ok, filter = coroutine.resume(cor,...)
			if not ok then
				scape:warn(filter)
				table.remove(self.coroutines,i)
			else
				corFilterCache[cor] = filter
			end
			term.redirect(oterm)
		end
	end
end
dLocal.resumeCoroutines = resumeCoroutines

local function fixChildren(o)
	for i,obj in ipairs(o.children) do
		scapeRef[obj] = scapeRef[o]
		slideRef[obj] = slideRef[o]
		if obj.children then
			fixChildren(obj)
		end
	end
end

local changeCache = {}
local renderCache = {}
dLocal.renderCache = renderCache
dLocal.changeCache = changeCache

-- [[ GENERIC API ]]
local generic = shapescape.generic

generic.getAbsolutePosition = function(self,includePadding)
	local x1,y1,x2,y2 = self.x1,self.y1,self.x2,self.y2
	local o = self
	while o.parent do
		--local p = o.parent.padding or 0
		--local pL,pR,pT,pB = (o.parent.paddingLeft or p), (o.parent.paddingRight or p), (o.parent.paddingTop or p), (o.parent.paddingBottom or p)
		local off = 0
		if o.parent.border then
			off = 1
		end
		x1 = x1+o.parent.x1-1+off
		x2 = x2+o.parent.x1-1+off
		y1 = y1+o.parent.y1-1+off
		y2 = y2+o.parent.y1-1+off
		o = o.parent
	end
	if includePadding then
		local p = self.padding or 0
		local pL,pR,pT,pB = (self.paddingLeft or p), (self.paddingRight or p), (self.paddingTop or p), (self.paddingBottom or p)
		x1, y1, x2, y2 = x1-pL, y1-pT, x2+pR, y2+pB
	end
	return x1,y1,x2,y2
end

generic.render = function(self, baseX, baseY, full)
	
	-- for variable parameters use custom objects using metamethods; indexing self with that field would return the value of the variable it refers to
	-- the metamethods would be applied on the parent of the parameter and be defined during object initialization
	
	local oCursor = captureCursor()

	baseX = baseX or 1
	baseY = baseY or 1
	
	local off = 0
	if self.border then
		off = 1
	end

	local p = self.padding or 0
	local pL,pR,pT,pB = (self.paddingLeft or p), (self.paddingRight or p), (self.paddingTop or p), (self.paddingBottom or p)
	
	local x1, y1, x2, y2 = self.x1+baseX-1-pL, self.y1+baseY-1-pT, self.x2+baseX-1+pR, self.y2+baseY-1+pB
	
	if self.type == "circle" then
		-- different rendering
		
	elseif self.border or self.fill or self.text then -- literally all other rendering
	
		
		local mode = "fill"
		if not self.fill then
			mode = "transparent"
		end
		-- no lUtils allowed
		-- center, top, bottom, left, right, corTL, corTR, corBL, corBR
		local p
		local t = " "
		local fg = " "
		local bg = " "
		if self.border and self.border.color then
			fg = utils.toBlit(self.border.color)
		elseif self.fill and self.fill.color and self.fill.color ~= 0 then
			fg = utils.toBlit(self.fill.color)
		end
		if self.fill and self.fill.color and self.fill.color ~= 0 and not (self.window and self.active) then
			bg = utils.toBlit(self.fill.color)
		end
		--print("fg="..fg.."\nbg="..bg)
		--os.sleep(0.5)
		local pc = {" ",bg,bg}
		if self.border then
			local weight = self.border.weight or 1
			local dist = self.border.padding or 2
			local ctbl = {
				[1] = { -- weight
					[0] = { -- distance
						top = {"\143",t,fg},
						bottom = {"\131",fg,t},
						left = {"\149",t,fg},
						right = {"\149",fg,t},
						corTL = {"\159",t,fg},
						corTR = {"\144",fg,t},
						corBL = {"\130",fg,t},
						corBR = {"\129",fg,t},
					},
					[1] = {
						top = {"\140",fg,bg},
						bottom = {"\140",fg,bg},
						left = {"\149",fg,bg},
						right = {"\149",bg,fg},
						corTL = {"\156",fg,bg},
						corTR = {"\147",bg,fg},
						corBL = {"\141",fg,bg},
						corBR = {"\142",fg,bg},
					},
					[2] = {
						top = {"\131",fg,bg},
						bottom = {"\143",bg,fg},
						left = {"\149",fg,bg},
						right = {"\149",bg,fg},
						corTL = {"\151",fg,bg},
						corTR = {"\148",bg,fg},
						corBL = {"\138",bg,fg},
						corBR = {"\133",bg,fg},
					}, --maybe make more efficient in future using the strings "bg" and "fg" so grid doesn't have to regenerate on color change
				}
			}
			p = utils.instantiate(ctbl[weight][dist])
			p.center = pc
			
		else
			p = {center=pc, top=pc, bottom=pc, left=pc, right=pc, corTL=pc, corTR=pc, corBL=pc, corBR=pc}
		end
		local txt
		local lines = {}
		local txtlines = {}
		if self.text and self.text.color and self.text.content then
			txt = self.text.content or self.textContent
			-- if self.fill.image or self.fill.pattern then replace center lines with that etc etc
			
			-- add handling for blit lines
			txtlines = utils.wordwrap(txt, (x2-pR-off)-(x1+pL+off)+1)
		end
		
		
		--_G.debuglines = {}
		local winW,winH
		if self.window then
			winW,winH = self.window.getSize()
		end
		for y=y1,y2 do
			local winLine
			local absY = y-y1+1 - off-pT
			if self.window then
				if absY < 1 then
					winLine = {self.window.getLine(1)}
				elseif absY <= winH then
					winLine = {self.window.getLine(absY)}
				else
					winLine = {self.window.getLine(winH)}
				end
			end
			local txtline = txtlines[y-y1+1-pT-off]
			term.setCursorPos(x1,y)
			local line = {{},{},{}}
			--table.insert(debuglines,line)
			local function addPix(pix,amount)
				amount = amount or 1
				for i=1,amount do
					table.insert(line[1],pix[1])
					table.insert(line[2],pix[2])
					table.insert(line[3],pix[3])
				end
			end
			if not self.border then
				addPix(p.center,x2-x1+1)
			elseif y == y1 then
				addPix(p.corTL)
				addPix(p.top,x2-x1-1)
				addPix(p.corTR)
			elseif y == y2 then
				addPix(p.corBL)
				addPix(p.bottom,x2-x1-1)
				addPix(p.corBR)
			else
				addPix(p.left)
				addPix(p.center,x2-x1-1)
				addPix(p.right)
			end
			local wLine
			if self.window then
				wLine = {
					string.rep(" ",off+pL)..winLine[1]..string.rep(" ",off+pR),
					string.rep(winLine[2]:sub(1,1),off+pL)..winLine[2]..string.rep(winLine[2]:sub(winW,winW),off+pR),
					string.rep(winLine[3]:sub(1,1),off+pL)..winLine[3]..string.rep(winLine[3]:sub(winW,winW),off+pR),
				}
			elseif full then
				local tw,th = term.getSize()
				if y >= 1 and y <= th then
					wLine = {term.current().getLine(y)}
				else
					wLine = {"","",""}
				end
			end
			--_G.mydebugwline = wLine
			if wLine then
				for c=1,#line[1] do
					local p = c+x1-1
					if self.window then
						p = c
					end
					local c1,c2,c3 = wLine[1]:sub(p,p),wLine[2]:sub(p,p),wLine[3]:sub(p,p)
					if c1 == "" then c1 = " " end
					if c2 == "" then c2 = "f" end
					if c3 == "" then c3 = "f" end
					if line[2][c] == " " and line[3][c] == " " then
						line[1][c] = c1
						line[2][c] = c2
						line[3][c] = c3
					elseif line[2][c] == " " then
						line[2][c] = c3
					elseif line[3][c] == " " then
						line[3][c] = c3
					end
				end
			end
			for l=1,3 do
				line[l] = table.concat(line[l])
			end
			if txtline then
				line[1] = line[1]:sub(1,off+pL)..txtline..line[1]:sub(off+pL+#txtline+1)
				--[[for s=off+pL+1,off+pL+#txtline do
					local c = s-(off+pL)
					line[1][s] = txtline:sub(c,c)]]
					
				line[2] = line[2]:sub(1,off+pL)..string.rep(utils.toBlit(self.text.color or self.textColor),#txtline)..line[2]:sub(off+pL+#txtline+1)
			end
			if full then
				term.blit(unpack(line))
			else
				table.insert(lines,line)
			end
		end
		if not full then
			renderCache[self] = lines
		end
		--[[if self.fill and self.fill.image then -- temporary, make it stretch n shit and put it in the line fill
			if self.fill.color then
				term.setBackgroundColor(self.fill.color)
			end
			utils.renderImage(self.fill.image,x1+pL+off,y1+pT+off,nil,(not self.fill.color))
		end]]
		
		
	end
	if self.window and self.window.getCursorBlink() then
		local x,y = self:getAbsolutePosition()
		local col = self.window.getTextColor()
		local cX,cY = self.window.getCursorPos()
		term.setCursorPos(x+cX+off-1,y+cY+off-1)
		term.setCursorBlink(true)
		term.setTextColor(col)
	else
		restoreCursor(oCursor)
	end
	if self.children then
		-- add base coords AND self coords (u become new parent but m aybe u already have parent)
		for k,v in ipairs(self.children) do
			if generic.isVisible(v) then
				generic.render(v,x1+pL+off, y1+pT+off, full) --x1 and y1 are already base plus self stoopid
			end
		end
	end
end

generic.update = function(self, ...)
	if not self.active then
		if not scapeRef[slideRef[self]] then
			error("No scape attached",2)
		end
		self:initialize()
	end
	if self.window then
		local sw,sh = self:getSize()
		local ww,wh = self.window.getSize()
		if sw ~= ww or sh ~= wh then
			self:resize(sw,sh)
		end
	end
	local fEv = "self.mouse"
	if not self:getScape() then
		fEv = "mouse"
	end
	local e = {...}
	local runCor = true
	local runChildren = true
	if e[1]:find(fEv) == 1 then
		--e[3] = e[3]-self.x1+1
		--e[4] = e[4]-self.y1+1
		-- completely wrong, get absolute position
		if e[3] and e[4] then
			local x1,y1,x2,y2 = self:getAbsolutePosition()
			local off = 0
			if self.border then
				off = 1
			end
			e[3] = e[3]-(x1+off)+1
			e[4] = e[4]-(y1+off)+1 -- test
		end
		local cE = table.pack(table.unpack(e))
		cE[1] = cE[1]:gsub("self.","")
		if self.coroutines then
			resumeCoroutines(self,unpack(cE))
		end
		runCor = false
		runChildren = false
	elseif e[1]:find("mouse") == 1 then
		runCor = false
	end
	local oterm = term.current()
	if self.window then
		term.redirect(self.window)
	end
	--[=[if self.eventListeners[e[1]] then
		runListener(self.eventListeners[e[1]],self:getScape())
	end]=]
	local sTbl = {}
	for i=1,#e do
		sTbl[i] = tostring(e[i])
		local l = table.concat(sTbl,".",1,i)
		if self.eventListeners[l] then
			runListener(self.eventListeners[l],self:getScape())
		end
	end
	if self.coroutines and runCor then
		resumeCoroutines(self,unpack(e))
	end
	if not self.destroyed then
		if self.children and runChildren then
			for i,child in ipairs(self.children) do
				child:update(...)
			end
		end
	end
	term.redirect(oterm)
end

local privRef = {}

generic.initialize = function(self) -- provide the scape table to it for variables
	local slide = slideRef[self]
	local scape = scapeRef[slide]
	if not scape then
		error("No scape attached",2)
	end
	-- NOTE: CLASSES MUST NOT BE INITIALIZED, ONLY THEIR INSTANCES
	-- except they should, just using custom class initialize
	-- oh yeah i didnt make classes yet hm
	-- figure out how to handle variables
	-- maybe a table in an object with names of properties with variable values and then this table isn't looked at until initialize is ran and the properties are replaced with custom variable shit stuff
	-- load the scripts into functions
	-- etc
	-- do everything that would make it not serializable
	
	-- add scape log to log errors and shit and warnings
	-- generate env
	if not self.parent or not self.parent.env then
		self.env = {
			shell = scape.env.shell,
			multishell = scape.env.multishell,
			package = scape.env.package,
			require = scape.env.require,
			LevelOS = scape.env.LevelOS,
			shapescape = scape.env.shapescape, --no. add as __index.
			_SCAPE = scape,
			_SLIDE = slide,
		}
	else
		self.env = {}
		for k,v in pairs(self.parent.env) do
			self.env[k] = v
		end
	end
	-- make local sahpesacpe api
	self.env._ENV = self.env
	self.env.self = self
	setmetatable(self.env,{__index=scape.variables,__newindex=scape.variables}) -- scape.variables index is _G
	if self.type == "window" then
		local p = self.padding or 0
		local pL,pR,pT,pB = (self.paddingLeft or p), (self.paddingRight or p), (self.paddingTop or p), (self.paddingBottom or p)
		local off = 0
		if self.border then
			off = 1
		end
		local w,h = (self.x2-off)-(self.x1+off)+1, (self.y2-off)-(self.y1+off)+1
		self.window = window.create(term.current(),self.x1+off,self.y1+off,w,h,false)
		if self.fill and self.fill.color then
			self.window.setBackgroundColor(self.fill.color)
			self.window.clear()
			self.window.setCursorPos(1,1)
		end
		if self.textColor then
			self.window.setTextColor(self.textColor)
		end
	end

	for i,e in pairs(self.eventListeners) do
		if type(e) ~= "table" then
			e = {e}
		end
		local newE = {}
		for k,v in ipairs(e) do
			local asset
			if type(v) == "number" then
				asset = scape:getAssetByID(v)
			elseif type(v) == "string" then
				asset = scape:getAssetByName(v)
			else
				asset = v
			end
			id, name = asset.id, asset.name
			if asset then
				local func,err = load(asset.content,"@"..(asset.name or asset.id),nil,self.env)
				if not func then
					scape:warn(err)
				else
					table.insert(newE,func)
				end
				-- put erorr if not loaded correctly idk
			end
		end
		self.eventListeners[i] = newE
	end
	-- generate coroutines, run once with scape.args
	
	for k,v in pairs(self.pointers) do
		self[k] = nil
	end
	local oldindex = getmetatable(self).__index
	local private = {}
	privRef[self] = private
	for k,v in pairs(self) do
		private[k] = v
		self[k] = nil
	end
	local mt = {
		__index=function(tbl,k)
			if private[k] then
				return private[k]
			elseif self.pointers[k] then
				return runPointer(k,scape,self)
			elseif k == "width" then
				return self.x2-self.x1+1
			elseif k == "height" then
				return self.y2-self.y1+1
			elseif k == "x" then
				return self.x1
			elseif k == "y" then
				return self.y1
			else
				return oldindex[k]
			end
		end,
		__newindex=function(tbl,k,v)
			if tbl[k] ~= v then
				if k == "width" then
					self:resize(v,self.height)
				elseif k == "height" then
					self:resize(self.width,v)
				elseif k == "x" then
					self:move(v,self.y1)
				elseif k == "y" then
					self:move(self.x1,v)
				elseif k == "x1" then
					self:reposition(v)
				elseif k == "y1" then
					self:reposition(self.x1,v)
				elseif k == "x2" then
					self:reposition(self.x1,self.y1,v,self.y2)
				elseif k == "y2" then
					self:reposition(self.x1,self.y1,self.x2,v)
				else
					private[k] = v
					self:markAsChanged()
				end
			end
		end
	}
	if self.type == "group" then
		mt.__pairs = utils.genStatelessIterator(private, group, generic)
	else
		mt.__pairs = utils.genStatelessIterator(private, generic)
	end
	setmetatable(self, mt)
	self.active = true

	if self.children then
		for k,v in ipairs(self.children) do
			if v.name then
				if not self[v.name] then
					self[v.name] = v
				end
				if not self.env[v.name] then
					self.env[v.name] = v
				end
			end
			v:initialize()
		end
	end

	if self.coroutines then
		local args = scape.args or {}
		for k=#self.coroutines,1,-1 do
			local v = self.coroutines[k]
			local asset = scape.assets[v]
			if asset then
				local func,err = load(asset.content,"@"..(asset.name or asset.id),nil,self.env)
				if not func then
					scape:warn(err)
					table.remove(self.coroutines,k)
				else
					self.coroutines[k] = coroutine.create(func)
				end
			end
		end
		resumeCoroutines(self,unpack(args))
	end

	if self.eventListeners["initialize"] then
		runListener(self.eventListeners["initialize"],scape)
	end

	return true
end

generic.reposition = function(self,x1,y1,x2,y2)
	-- take care of aligning n shit idk
	x1,y1,x2,y2 = x1 or self.x1, y1 or self.y1, x2 or self.x2, y2 or self.y2
	if self.active then
		if not self.pointers.x1 then
			privRef[self].x1 = x1
		end
		if not self.pointers.y1 then
			privRef[self].y1 = y1
		end
		if not self.pointers.x2 then
			privRef[self].x2 = x2
		end
		if not self.pointers.y2 then
			privRef[self].y2 = y2
		end
		--self:markAsChanged()
		renderCache[self:getSlide()] = nil
	else
		self.x1,self.y1,self.x2,self.y2 = x1,y1,x2,y2
	end

	if self.window then-- position dont matter shit, just size (window is invisible) thats wrong motherfucker it absolutely does
		local p = self.padding or 0
		local pL,pR,pT,pB = (self.paddingLeft or p), (self.paddingRight or p), (self.paddingTop or p), (self.paddingBottom or p)
		local off = 0
		if self.border then
			off = 1
		end
		local w,h = (self.x2-off)-(self.x1+off)+1, (self.y2-off)-(self.y1+off)+1
		local x,y = self:getAbsolutePosition()
		self.window.reposition(x+off,y+off,w,h)
	end
end

generic.move = function(self,x,y)
	x = x or self.x1
	y = y or self.y1
	local offX,offY = x-self.x1,y-self.y1
	self:reposition(x,y,self.x2+offX,self.y2+offY)
end

generic.resize = function(self,width,height)
	self:reposition(self.x1,self.y1,((width and (self.x1+width-1)) or self.x2),((height and (self.y1+height-1)) or self.y2))
end

generic.duplicate = function(self,properties)
	if self.active then
		error("Cannot duplicate active objects",2)
	end
	return self:getSlide():addObject(utils.instantiate(self))
end

generic.destroy = function(self,full)
	if full == nil then
		full = true
	end
	if self.parent then
		for k,v in pairs(self.parent.children) do
			if v == self then
				table.remove(self.parent.children,k)
				break
			end
		end
		for k,v in pairs(self.parent) do
			if v == self then
				self.parent[k] = nil
			end
		end
		local sl = self:getSlide()
	elseif not self.isClass then
		local sl = self:getSlide()
		for k,v in pairs(sl.objects) do
			if v == self then
				table.remove(sl.objects,k)
			end
		end
		for id,obj in ipairs(sl.objects) do
			obj.id = id -- so ID is not static hm
			-- then again it doesn't really need to be because string IDs *are* static
		end
	end
	if full then
		for k,v in pairs(self) do
			self[k] = nil
		end
		setmetatable(self,{__index=deleted,__newindex=deleted})
	end
	if slideRef[self] then
		renderCache[slideRef[self]] = nil
		slideRef[self] = nil
	end
end

generic.markAsChanged = function(self)
	local slide = self:getSlide()
	if not slide then
		return false
	else
		if not changeCache[slide] then
			changeCache[slide] = {}
		end
		if changeCache[self] then
			for k,v in pairs(changeCache[self]) do
				if k ~= "old" and self[k] ~= v then
					changeCache[slide][self] = 2
					break
				end
			end
		end
		local x1,y1,x2,y2 = self:getAbsolutePosition(true)
		changeCache[self] = {x1=x1,y1=y1,x2=x2,y2=y2,old=changeCache[self]}
		changeCache[slide][self] = changeCache[slide][self] or 1
		if self.children then
			for k,v in pairs(self.children) do
				v:markAsChanged()
			end
		end
	end
end

generic.getSize = function(self)
	return self.x2-self.x1+1,self.y2-self.y1+1
end

generic.getPosition = function(self)
	return self.x1,self.y1
end

generic.getBackgroundColor = function(self)
	if not self.fill then
		return 0
	else
		return self.fill.color or 0
	end
end

generic.getBorderColor = function(self)
	if not self.border then
		return 0
	else
		return self.border.color or 0
	end
end

generic.getTextColor = function(self)
	if not self.text then
		return 0
	else
		return self.text.color or 0
	end
end

generic.getText = function(self)
	if not self.text then
		return nil
	else
		return self.text.content
	end
end

generic.setBackgroundColor = function(self,color)
	if color == nil or color == 0 then
		self.fill = nil
	else
		if not self.fill then self.fill = {} end
		self.fill.color = color
	end
	self:markAsChanged()
end

generic.setTextColor = function(self,color)
	if not self.text then self.text = {content=""} end
	self.text.color = color
	self:markAsChanged()
end

generic.setText = function(self,content,color)
	if not self.text then self.text = {} end
	self.text.color = color or self.text.color or self.txtcolor or self.textColor or colors.black
	self.text.content = content
	self:markAsChanged()
end

generic.setBorderColor = function(self,color)
	if color == nil or color == 0 then
		self.border = nil
	else
		if not self.border then self.border = {weight=1,padding=2} end
		self.border.color = color
	end
	self:markAsChanged()
end

generic.setPointer = function(self, key, value)
	if not self.pointers then self.pointers = {} end
	if self.active then
		privRef[self][key] = nil
	end
	self.pointers[key] = value
end

generic.removePointer = function(self, key)
	if self.active then
		self[key] = self[key]
	end
	self.pointers[key] = nil
end

generic.addListener = function(self, sEvent, sAsset)
	local scape = scapeRef[self]
	if not self.eventListeners then
		self.eventListeners = {}
	end
	if sEvent == "coroutine" then
		if not self.coroutines then
			self.coroutines = {}
		end
	elseif not self.eventListeners[sEvent] then
		self.eventListeners[sEvent] = {}
	end
	if self.active and (type(sAsset) == "string" or type(sAsset) == "number") and scape then
		local asset
		if type(sAsset) == "string" then
			asset = scape:getAssetByName(sAsset)
		else
			asset = scape:getAssetByID(sAsset)
		end
		if asset then
			local func,err = load(asset.content,"@"..(asset.name or asset.id),nil,self.env)
			if not func then
				scape:warn(err)
			elseif sEvent == "coroutine" then
				local args = scape.args or {}
				local k = #self.coroutines+1
				self.coroutines[k] = coroutine.create(func)
				local oterm = term.current()
				if self.window then
					term.redirect(self.window)
				end
				local ok,filter = coroutine.resume(self.coroutines[k],unpack(args))
				if not ok then
					scape:warn(filter)
					table.remove(self.coroutines,k)
				else
					corFilterCache[self.coroutines[k]] = filter
				end
				term.redirect(oterm)
			else
				table.insert(self.eventListeners[sEvent],func)
			end
		end
	elseif self.active and sEvent == "coroutine" then
		local func = sAsset
		local args = scape.args or {}
		local k = #self.coroutines+1
		self.coroutines[k] = coroutine.create(func)
		local oterm = term.current()
		if self.window then
			term.redirect(self.window)
		end
		local ok,filter = coroutine.resume(self.coroutines[k],unpack(args))
		if not ok then
			scape:warn(filter)
			table.remove(self.coroutines,k)
		else
			corFilterCache[self.coroutines[k]] = filter
		end
		term.redirect(oterm)
	elseif sEvent == "coroutine" then
		table.insert(self.coroutines,sAsset)
	else
		table.insert(self.eventListeners[sEvent],sAsset)
	end
end

generic.getSlide = function(self)
	return slideRef[self]
end

generic.getScape = function(self)
	return scapeRef[self]
end

generic.isInside = function(self,x,y)
	local x1,y1,x2,y2 = self:getAbsolutePosition(true)

	if x >= x1 and y >= y1 and x <= x2 and y <= y2 then
		return true
	end
end

generic.arrange = function(self,newPositionID)
	local slide = self:getSlide()
	if not slide then
		error("No slide attached to object",2)
	end
	local parent = slide.objects
	if self.parent then
		parent = self.parent.children
	end
	if newPositionID < 0 then
		newPositionID = #parent+newPositionID+2
	end
	newPositionID = math.min(#parent+1,newPositionID)
	table.remove(parent,self.id)
	table.insert(parent,newPositionID,self)
	for id,iObj in ipairs(parent) do
		iObj.id = id
	end
end

generic.moveToFront = function(self)
	self:arrange(-1)
end

generic.moveToBack = function(self)
	self:arrange(1)
end

generic.moveForward = function(self)
	self:arrange(self.id+1)
end

generic.moveBackward = function(self)
	self:arrange(math.max(1,self.id-1))
end

-- [[ GROUP API ]]
shapescape.group = {}
local group = shapescape.group
setmetatable(group, {__index=generic})

group.resetSize = function(self)
	local off = 0
	if self.border then
		off = 1
	end
	local baseX,baseY = self.x1+off,self.y1+off
	self.x1 = self.children[1].x1+baseX-1-off
	self.y1 = self.children[1].y1+baseY-1-off
	self.x2 = self.children[1].x2+baseX-1+off
	self.y2 = self.children[1].y2+baseY-1+off
	for i=2,#self.children do
		local o = self.children[i]
		self.x1 = math.min(self.x1,o.x1+baseX-1-off)
		self.y1 = math.min(self.y1,o.y1+baseY-1-off)
		self.x2 = math.max(self.x2,o.x2+baseX-1+off)
		self.y2 = math.max(self.y2,o.y2+baseY-1+off)
	end
	local nBaseX,nBaseY = self.x1+off,self.y1+off
	local offX,offY = nBaseX-baseX,nBaseY-baseY
	for i=1,#self.children do
		self.children[i]:move(self.children[i].x1-offX,self.children[i].y1-offY)
	end
end

group.addObject = function(self,obj)
	local off = 0
	if self.border then
		off = 1
	end
	local xOff,yOff,_,_ = self:getAbsolutePosition()
	xOff = xOff+off-1
	yOff = yOff+off-1
	obj.x1,obj.y1,obj.x2,obj.y2 = obj.x1-xOff,obj.y1-yOff,obj.x2-xOff,obj.y2-yOff
	obj.parent = self
	shapescape.loadObject(obj)
	scapeRef[obj] = scapeRef[self]
	slideRef[obj] = slideRef[self]
	if obj.children then
		fixChildren(obj)
	end
	table.insert(self.children,obj)
	obj.id = #self.children
	self:resetSize()
	if self.active then
		obj:initialize()
	end
	self:resetSize()
	local sl = self:getSlide()
	if sl then
		renderCache[sl] = nil
	end
end

local invisibleObjects = {}
generic.setVisible = function(self,visibility)
	if not visibility then
		invisibleObjects[self] = true
	else
		invisibleObjects[self] = false
	end
end

generic.isVisible = function(self)
	return not invisibleObjects[self]
end

-- [[ SLIDE API ]]
local slide = shapescape.slide

slide.initialize = function(self)
	self:setVisible(true)
	local scape = scapeRef[self]
	self.window = window.create(term.current(), 1, 1, self.width, self.height, self:isVisible())
	self:render(true)
	for i=1,#self.objects do
		local obj = self.objects[i]
		obj:initialize(scape)
		if obj.name and not scape.variables[obj.name] then
			scape.variables[obj.name] = obj
		end
	end
	self.active = true
end

slide.getScape = function(self)
	return scapeRef[self]
end

slide.getSize = function(self)
	return self.width,self.height
end

slide.resize = function(self, width, height)
	self.width, self.height = width, height
	if self.window then
		local x,y = self.window.getPosition()
		self.window.reposition(x,y,width,height)
	end
end

slide.loadObject = function(self,obj)
	shapescape.loadObject(obj)
	scapeRef[obj] = scapeRef[self]
	slideRef[obj] = self
	if obj.children then
		fixChildren(obj)
	end
	renderCache[self] = nil
	return obj
end

slide.addObject = function(self,obj)
	obj.id = #self.objects+1
	table.insert(self.objects,obj)
	return self:loadObject(obj)
end

slide.createObject = function(self,oType,x1,y1,x2,y2,properties)
	return self:addObject(shapescape.createObject(oType,x1,y1,x2,y2,properties))
end

slide.createRectangle = function(self,x1,y1,x2,y2,fill,border,properties)
	return self:addObject(shapescape.createRectangle(x1,y1,x2,y2,fill,border,properties))
end

slide.createWindow = function(self,x1,y1,x2,y2,fill,textColor,border,properties)
	return self:addObject(shapescape.createWindow(x1,y1,x2,y2,fill,textColor,border,properties))
end

slide.createGroup = function(self,objects,properties)
	return self:addObject(shapescape.createGroup(objects,properties))
end

slide.isVisible = function(self)
	return self.visible
end

slide.setVisible = function(self,visibility)
	self.visible = not not visibility
	if self.window then
		self.window.setVisible(self.visible)
	end
end

slide.getBackgroundColor = function(self)
	if not self.background then
		return 0
	else
		return self.background.color or 0
	end
end

slide.setBackgroundColor = function(self,color)
	if not self.background then self.background = {} end
	self.background.color = color
end

slide.genCache = function(self,objs,doDebug)
	if not objs then
		renderCache[self] = {}
		objs = self.objects
	end
	local c = renderCache[self]
	for i=1,#objs do
		local o = objs[i]
		if o:isVisible() then
			local x1,y1,x2,y2 = o:getAbsolutePosition(true)
			for x=x1,x2 do
				if not c[x] then
					c[x] = {}
				end
				local cx = c[x]
				for y=y1,y2 do
					if not cx[y] then
						cx[y] = {o}
					else
						table.insert(cx[y],1,o)
					end
				end
			end
			if doDebug then
				o:render(1,1,true)
				os.sleep(1)
				term.clear()
				os.sleep(1)
			end
			if o.children then
				self:genCache(o.children, doDebug)
			end
		end
	end
end

slide.render = function(self,full)
	local oterm = term.current()
	if self.window then
		term.redirect(self.window)
	end
	local function get(pix,x,y)
		local fgn = 1
		local o = pix[fgn]
		local ox,oy = o:getAbsolutePosition(true)
		local fgp = renderCache[o] and renderCache[o][y-oy+1] and renderCache[o][y-oy+1][2]:sub(x-ox+1,x-ox+1) or " "
		while fgp == " " do
			fgn = fgn+1
			if fgn > #pix then
				fgp = utils.toBlit(term.getBackgroundColor())
				break
			end
			local o = pix[fgn]
			ox,oy = o:getAbsolutePosition(true)
			fgp = renderCache[o] and renderCache[o][y-oy+1] and renderCache[o][y-oy+1][2]:sub(x-ox+1,x-ox+1) or " "
		end
		local bgn = 1
		local o = pix[bgn]
		local ox,oy = o:getAbsolutePosition(true)
		local bgp = renderCache[o] and renderCache[o][y-oy+1] and renderCache[o][y-oy+1][3]:sub(x-ox+1,x-ox+1) or " "
		while bgp == " " do
			bgn = bgn+1
			if bgn > #pix then
				bgp = utils.toBlit(term.getTextColor())
				break
			end
			local o = pix[bgn]
			ox,oy = o:getAbsolutePosition(true)
			bgp = renderCache[o] and renderCache[o][y-oy+1] and renderCache[o][y-oy+1][3]:sub(x-ox+1,x-ox+1) or " "
		end
		local txtn = math.min(fgn,bgn)
		if txtn > #pix then
			txtp = " "
		else
			local o = pix[txtn]
			local ox,oy = o:getAbsolutePosition(true)
			txtp = renderCache[pix[txtn]] and renderCache[pix[txtn]][y-oy+1][1]:sub(x-ox+1,x-ox+1) or " "
		end
		if #txtp ~= #fgp or #txtp ~= #bgp or #fgp ~= #bgp then
			_G.x,_G.y,_G.txtp,_G.fgp,_G.bgp,_G.pix = x,y,txtp,fgp,bgp,pix
			error("wtf")
		end
		--table.insert(debuglogshit,{txtp=txtp,fgp=fgp,bgp=bgp,pix=pix,x=x,y=y})
		return txtp,fgp,bgp
	end
	if self.background then
		if self.background.color then
			term.setBackgroundColor(self.background.color)
		elseif self.background.image then
			-- idk
		end
	end
	local bg = utils.toBlit(term.getBackgroundColor())
	if full or not renderCache[self] then
		term.clear()
		for i,o in ipairs(self.objects) do
			if not renderCache[o] then
				o:render()
			end
		end
		if not renderCache[self] then
			self:genCache()
		end
		
		local w,h = self:getSize()
		local c = renderCache[self]

		_G.debugLines = {}
		for y=1,h do
			term.setCursorPos(1,y)
			local line = {{},{},{}}
			for x=1,w do
				if c[x] and c[x][y] then
					local cxy = c[x][y]
					l1,l2,l3 = get(cxy,x,y)
					_G.debugPix = {l1,l2,l3}
					table.insert(line[1],l1)
					table.insert(line[2],l2)
					table.insert(line[3],l3)
					--table.insert(debuglogshit,{l1,l2,l3})
				else
					table.insert(line[1]," ")
					table.insert(line[2],bg)
					table.insert(line[3],bg)
					--table.insert(debuglogshit,{" ",bg,bg})
				end
			end
			-- THIS LINE INTERRUPTS YOU MOTHERFUCKER WORK PLEASE
			_G.moredebugshit = {line[1],line[2],line[3]}
			table.insert(debugLines,{table.concat(line[1]),table.concat(line[2]),table.concat(line[3])})
			term.blit(table.concat(line[1]),table.concat(line[2]),table.concat(line[3]))
		end
	elseif changeCache[self] then
		--[[for k,v in pairs(changeCache[self]) do
			if v == 2 then
				renderCache[self] = nil
				self:genCache()
				self:render(true)
				break
			end
		end]]
		if self.debugMode and not self.i then
			self.i = 0
		end
		local c = renderCache[self]
		_G.debugRenderList = {}
		if not changeCache.previous then
			changeCache.previous = {}
		end
		for o,t in pairs(changeCache[self]) do
			if not o.destroyed then
				o:render()
				table.insert(debugRenderList,o)
				local x1,y1,x2,y2 = o:getAbsolutePosition(true)
				local cur = {x1=x1,y1=y1,x2=x2,y2=y2}
				local changePos = false
				if changeCache.previous[o] then
					local p = changeCache.previous[o]
					for k,v in pairs(p) do
						if v ~= cur[k] then
							-- render previous position
							changePos = true
							for y=p.y1,p.y2 do
								local line = {{},{},{}}
								for x=p.x1,p.x2 do
									if c[x] and c[x][y] then
										local cxy = c[x][y]
										table.remove(cxy,utils.locateEntry(cxy,o))
										if #cxy == 0 then
											c[x][y] = nil
										end
									end
									if c[x] and c[x][y] then
										if self.debugMode then
											term.setCursorPos(x,y)
											term.setBackgroundColor(2^self.i)
											term.write(" ")
										else
											local cxy = c[x][y]
											l1,l2,l3 = get(cxy,x,y)
											_G.debugPix = {l1,l2,l3}
											table.insert(line[1],l1)
											table.insert(line[2],l2)
											table.insert(line[3],l3)
										end
									else
										if self.debugMode then
											term.setCursorPos(x,y)
											term.setBackgroundColor(2^self.i)
											term.setTextColor(2^((self.i+1)%16))
											term.write("0")
										else
											table.insert(line[1]," ")
											table.insert(line[2],bg)
											table.insert(line[3],bg)
										end
									end
								end
								if not self.debugMode then
									term.setCursorPos(p.x1,y)
									term.blit(table.concat(line[1]),table.concat(line[2]),table.concat(line[3]))
								end
							end
							break
						end
					end
				end
				changeCache.previous[o] = cur
				for y=y1,y2 do
					local line = {{},{},{}}
					for x=x1,x2 do
						if changePos then
							if not c[x] then
								c[x] = {}
							end
							if not c[x][y] then
								c[x][y] = {}
							end
							local cxy = c[x][y]
							table.insert(cxy,o)
							-- compare equal parent levels but FUCK how do I do that
							-- OH I KNOW
							-- if parent and parent does NOT equal go up a level
							-- if not parent or parent equals then bigger ID wins
							-- wait fuck im dumb, what if b IS the parent
							local function isOnTop(a, b)
								if a.parent == b.parent then
									return a.id > b.id
								elseif a.parent == b then
									return true
								elseif a == b.parent then
									return false
								else
									if a.parent then
										if b.parent then
											local r = isOnTop(a.parent, b.parent)
											if r ~= nil then
												return r
											end
										end
										local r = isOnTop(a.parent, b)
										if r ~= nil then
											return r
										end
									end
									if b.parent then
										local r = isOnTop(a, b.parent)
										if r ~= nil then
											return r
										end
									end
								end
							end
							table.sort(cxy,function(a,b) return isOnTop(a,b) end) -- bruh
						end
						if c[x] and c[x][y] then
							if self.debugMode then
								term.setCursorPos(x,y)
								term.setBackgroundColor(2^self.i)
								term.write(" ")
							else
								local cxy = c[x][y]
								l1,l2,l3 = get(cxy,x,y)
								_G.debugPix = {l1,l2,l3,cxy=cxy}
								table.insert(line[1],l1)
								table.insert(line[2],l2)
								table.insert(line[3],l3)
							end
						else
							if self.debugMode then
								term.setCursorPos(x,y)
								term.setBackgroundColor(2^self.i)
								term.setTextColor(2^((self.i+1)%16))
								term.write("0")
							else
								table.insert(line[1]," ")
								table.insert(line[2],bg)
								table.insert(line[3],bg)
							end
						end
					end
					if not self.debugMode then
						term.setCursorPos(x1,y)
						term.blit(table.concat(line[1]),table.concat(line[2]),table.concat(line[3]))
					end
				end
			end
		end
		if self.debugMode and #debugRenderList > 0 then
			self.i = (self.i+1)%16
		end
		changeCache[self] = {}
	end
	term.redirect(oterm)
end

slide.destroy = function(self)
	local scape = self:getScape()
	for k,v in pairs(scape.slides) do
		if v == self then
			table.remove(scape.slides,k)
		end
	end
	for k,v in ipairs(scape.slides) do
		v.id = k
	end
	for k,v in pairs(self) do
		self[k] = nil
	end
	setmetatable(self,{__index=deleted,__newindex=deleted})
end


-- [[ SCAPE API ]]
local scape = shapescape.scape

scape.initialize = function(self,env,args)
	table.insert(shapescape.debug.scapes,self)
	self.env = env or _ENV
	self.env.shapescape = {
		getEvent = function()
			return unpack(self.event)
		end,
		getSlide = function()
			return self.slides[self.currentSlide]
		end,
		setSlide = function(slideID)
			-- if string search actually not necessary
			if self.slides[slideID] then
				self.currentSlide = self.slides[slideID].id
				os.pullEvent()
			end
		end,
		getSlides = function()
			return self.slides
		end,
		getScape = function()
			return self
		end,
		exit = function(...)
			self.returnValues = table.pack(...)
			self.status = "dead"
		end,
	}
	for k,v in pairs(shapescape) do
		self.env.shapescape[k] = v
	end
	if not self.variables then
		self.variables = {}
	end
	setmetatable(self.variables,{__index=_G})
	self.status = "running"
	self.log = {}
	self.args = args or {}
	self.currentSlide = 1
	self.active = true
	--_G.debugSelf = self
	for s=1,#self.slides do
		-- dont initialize slides until update
		local sl = self.slides[s]
		if sl.name and not self.slides[sl.name] then
			self.slides[sl.name] = sl
		end
	end
	for c=1,#self.classes do
		self.classes[c]:initialize()
	end
end

scape.loadSlide = function(self,tSlide)
	local w,h = term.getSize()
	width = tSlide.width or w
	height = tSlide.height or h
	setmetatable(tSlide,{__index=slide})
	scapeRef[tSlide] = self
	for o=1,#tSlide.objects do
		local obj = tSlide.objects[o]
		tSlide:loadObject(obj)
	end
	return tSlide
end

scape.newSlide = function(self,width,height,backgroundColor)
	local w,h = term.getSize()
	width = width or w
	height = height or h
	local sl = {width=width,height=height,visible=true,objects={},id=#self.slides+1}
	if backgroundColor then
		sl.background = {color=backgroundColor}
	end
	--sl.window = window.create(term.current(),1,1,width,height,false)
	table.insert(self.slides,sl)
	return self:loadSlide(sl)
end

scape.removeSlide = function(self, slide)
	if type(slide) ~= "table" then
		for k,v in pairs(self.slides) do
			if k == slide then
				slide = v
				break
			end
		end
		if type(slide) ~= "table" then
			return false
		end
	end
	for k,v in pairs(self.slides) do
		if v == slide then
			table.remove(self.slides,k)
		end
	end
	return true
end

scape.setSlide = function(self,nSlide)
	if type(nSlide) == "table" and nSlide.id then
		self.currentSlide = nSlide.id
	elseif type(nSlide) == "number" then
		self.currentSlide = nSlide
	else
		error("bad argument #2 to 'scape.setSlide' (expected slide, got "..type(nSlide)..")",2)
	end
end

scape.update = function(self, ...)
	-- i dont feel like this anymore
	-- maybe instead of just mouse_click, make it self.mouse_click or <name>.mouse_click
	-- and then mouse_click will execute on every mouse click, no matter where was clicked
	local e = table.pack(...)
	self.event = e
	local s = self.slides[self.currentSlide] -- idiot
	if not s.active then
		s:initialize()
	end
	for o=#s.objects,1,-1 do
		if not s.objects[o].active then
			s.objects[o]:initialize()
		end
		s.objects[o]:update(...)
	end
	local secEvent
	-- NOTE: transparent objects with children let mouse clicks through when click doesn't collide with any child
	local function checkCollisions(objs,prefix)
		local clickedObj
		for o=#objs,1,-1 do
			if objs[o]:isVisible() and objs[o]:isInside(e[3],e[4]) then
				if objs[o].children then
					local pre
					if objs[o].name and prefix then
						pre = prefix..objs[o].name.."."
					end
					clickedObj = checkCollisions(objs[o].children,pre)
				end
				if not (objs[o].children and objs[o]:getBackgroundColor() == 0 and not clickedObj) then -- only passes through if a child wasn't clicked
					local newEvent = utils.instantiate(e)
					newEvent[1] = "self."..e[1]
					objs[o]:update(unpack(newEvent)) -- will NOT update parent, meaning parent should NEVER pass self.mouse events to children
					if not clickedObj then
						clickedObj = objs[o]
						if objs[o].name and prefix then
							secEvent = utils.instantiate(e)
							secEvent[1] = (prefix or "")..objs[o].name.."."..e[1]
						end
					end
					return clickedObj
				end
			end
		end
	end

	if e[1]:find("mouse") == 1 and e[3] and e[4] then
		checkCollisions(s.objects,"")
	end
	if secEvent then
		for o=1,#s.objects do
			s.objects[o]:update(unpack(secEvent))
		end
	end
end

scape.render = function(self)
	term.setCursorBlink(false)
	self.slides[self.currentSlide]:render()
	if self.log then
		local cursor = captureCursor()
		local warns = {}
		for l=#self.log,1,-1 do
			local entry = self.log[l]
			if os.epoch("utc") <= entry.time+5000 then
				table.insert(warns,entry)
			else
				break
			end
		end
		local warnCols = {
			["WARNING"] = colors.orange,
			["ERROR"] = colors.red,
			["MSG"] = colors.lime,
		}
		local y = 2
		local w,h = term.getSize()
		for i,warn in ipairs(warns) do
			term.setCursorPos(2,y)
			local prefix = "["..warn.type.."] "
			local lines = utils.wordwrap(prefix..tostring(warn.text),w-4)
			width = 0
			for l=1,#lines do
				width = math.max(width,#lines[l]+2)
			end
			term.setBackgroundColor(colors.gray)
			term.write(string.rep(" ",width))
			term.setCursorPos(2,y)
			term.setTextColor(warnCols[warn.type])
			term.write("\149")
			term.setTextColor(colors.white)
			term.write(prefix)
			term.setTextColor(warnCols[warn.type])
			term.write(lines[1]:sub(#prefix+1).." ")
			y = y+1
			for l=2,#lines do
				term.setCursorPos(2,y)
				term.write("\149"..lines[l]..string.rep(" ",width-#lines[l]-3))
				y = y+1
			end
			y = y+1
		end
		restoreCursor(cursor)
	end
end

scape.run = function(self,...)
	local w,h = term.getSize()
	for s=1,#self.slides do
		self.slides[s]:resize(w,h)
	end
	if not self.active then
		self:initialize(_ENV, table.pack(...))
	end
	self:render()
	while self.status ~= "dead" do
		local e = table.pack(os.pullEvent())
		if e[1] == "term_resize" then
			local w,h = term.getSize()
			for s=1,#self.slides do
				self.slides[s]:resize(w,h)
			end
		end
		self:update(table.unpack(e,1,e.n))
		self:render()
	end
	return unpack(self.returnValues or {})
end

scape.createSlide = function(self,objects,width,height)
	sl = self:newSlide(width,height)
	for o=1,#objects do
		local obj = objects[o]
		sl:createObject(obj.type,obj.x1,obj.y1,obj.x2,obj.y2,obj)
	end
end

scape.addScript = function(self, sCode, sName)
	self.assets[self.lastAssetID+1] = {content=sCode,name=sName,id=self.lastAssetID+1}
	self.lastAssetID = self.lastAssetID+1
end

scape.getAssetByName = function(self, sName)
	for k,v in pairs(self.assets) do
		if v.name == sName then
			return v
		end
	end
end

scape.getAssetByID = function(self, nID)
	return self.assets[nID]
end

scape.getAPI = function(self)
	return shapescape
end

scape.message = function(self,message,type)
	type = type or "MSG"
	local types = {["MSG"]=true,["WARNING"]=true,["ERROR"]=true}
	if not types[type] then
		error("Invalid type '"..type.."'",2)
	end
	for e=#self.log,1,-1 do
		if os.epoch("utc") > self.log[e].time+5000 then
			break
		elseif self.log[e].text == message and self.log[e].type == type then
			table.remove(self.log,e)
		end
	end
	table.insert(self.log,{time=os.epoch("local"),text=message,type=type})
end

scape.warn = function(self,warning)
	self:message(warning,"WARNING")
end

-- [[ GLOBAL API ]]
shapescape.loadScape = function(tScape)
	local sc
	if type(tScape) == "table" then
		sc = tScape
	elseif type(tScape) == "string" and fs.exists(tScape) then
		local content = utils.fread(tScape)
		sc = textutils.unserialize(content)
	end
	setmetatable(sc,{__index=scape})
	for s=1,#sc.slides do
		sc:loadSlide(sc.slides[s])
	end
	local function classChildren(obj)
		for i,o in ipairs(obj.children) do
			scapeRef[o] = scapeRef[obj]
			if o.children then
				classChildren(o)
			end
		end
	end
	if not sc.classes then
		if sc.templates then
			sc.classes = sc.templates
			sc.templates = nil
		else
			sc.classes = {}
		end
	end
	for c=1,#sc.classes do
		scapeRef[sc.classes[c]] = sc
		if sc.classes[c].children then
			classChildren(sc.classes[c])
		end
		shapescape.class.load(sc.classes[c])
	end
	return sc
end

shapescape.createScape = function(slides,assets,classes)
	local sc = {slides=slides or {},assets=assets or {},classes=classes or {},variables={}} -- maybe width/height or something idk
	-- ooh metatables
	sc.lastAssetID = 0
	for k,v in pairs(sc.assets) do
		sc.lastAssetID = math.max(sc.lastAssetID,v.id+1)
	end
	return shapescape.loadScape(sc)
end

shapescape.loadObject = function(obj)
	if type(obj.fill) == "number" then
		if obj.fill == 0 then
			obj.fill = nil
		else
			obj.fill = {color=obj.fill}
		end
	end
	if type(obj.border) == "number" then
		if obj.border == 0 then
			obj.border = nil
		else
			obj.border = {color=obj.border,weight=1,padding=2}
		end
	end
	if type(obj.text) == "number" then
		if obj.text == 0 then
			obj.text = nil
		else
			obj.text = {color=obj.text,content=""}
		end
	elseif type(obj.text) == "string" then
		obj.text = {color=obj.txtcolor or obj.textColor or colors.black,content=obj.text}
	end
	if not obj.eventListeners then
		obj.eventListeners = {}
	end
	if not obj.pointers then
		obj.pointers = {}
	end
	if obj.type == "group" then
		setmetatable(obj,{__index=group})
	else
		setmetatable(obj,{__index=generic})
	end
	if obj.children then
		for i,child in ipairs(obj.children) do
			shapescape.loadObject(child)
			child.parent = obj
			child.id = i
		end
	end
	local x1,y1,x2,y2 = obj:getAbsolutePosition()
	changeCache[obj] = {x1=x1,y1=y1,x2=x2,y2=y2}
	return obj
end

shapescape.createObject = function(oType,x1,y1,x2,y2,properties)
	properties = properties or {}
	local obj = {x1=x1,y1=y1,x2=x2,y2=y2,type=oType}
	for k,v in pairs(properties) do
		obj[k] = v
	end
	return shapescape.loadObject(obj)
end

shapescape.createRectangle = function(x1,y1,x2,y2,fill,border,properties)
	properties = properties or {}
	properties.fill = fill
	properties.border = border
	return shapescape.createObject("rect",x1,y1,x2,y2,properties)
end

shapescape.createWindow = function(x1,y1,x2,y2,fill,textColor,border,properties)
	properties = properties or {}
	properties.fill = fill
	properties.border = border
	properties.textColor = textColor
	return shapescape.createObject("window",x1,y1,x2,y2,properties)
end

shapescape.createGroup = function(objects,properties)
	properties = properties or {}
	properties.children = objects
	if not objects or not objects[1] then return end
	local x1,y1,x2,y2 = objects[1].x1,objects[1].y1,objects[1].x2,objects[1].y2
	for i=2,#objects do
		--print(x1,y1,x2,y2)
		local o = objects[i]
		x1 = math.min(x1,o.x1)
		y1 = math.min(y1,o.y1)
		x2 = math.max(x2,o.x2)
		y2 = math.max(y2,o.y2)
		--print("after object: "..o.x1..","..o.y1..","..o.x2..","..o.y2..":",x1,y1,x2,y2)
		--os.sleep(0.5)
	end
	--os.sleep(2)
	local obj = shapescape.createObject("group",x1,y1,x2,y2,properties)
	setmetatable(obj,{__index=group})
	for i,o in ipairs(objects) do
		o.x1,o.y1,o.x2,o.y2 = o.x1-x1+1,o.y1-y1+1,o.x2-x1+1,o.y2-y1+1
		o.parent = obj
		o.id = i
	end
	return obj
end

--return {utils=utils,generic=generic,scape=scape,createObject=createObject,createRectangle=createRectangle,createGroup=createGroup}
return shapescape