-- sandbox stuff

local function copy(v, p)
    if type(v) == "table" then
        local t = {}
        for key, val in pairs(v) do
            t[key] = copy(val)
        end
        if p then
            for key, val in pairs(p) do
                t[key] = copy(val)
            end
        end
        return t
    else
        return v
    end
end

local function nextDeadline()
    return computer.uptime() + 5
end

local bubbleYield = false
local timeout = nextDeadline()

local function tooLong()
    return computer.uptime() >= timeout
end

local tooLongWithoutYielding = "too long without yielding"

local function nextHeatUp()
    return computer.uptime() + math.random() * 2 + 0.1
end

local heatInc = nextHeatUp()

debug.sethook(function()
    if computer.uptime() >= heatInc then
        heatInc = nextHeatUp()
        computer.addHeat(math.random() * 3)
    end

    if tooLong() and not bubbleYield then
        bubbleYield = true
        error(tooLongWithoutYielding) -- here it is an actual string
    end
end, "c", 100000) -- no bogo mips, the check is cheap anyways

local function resume(co, val1, ...)
    while true do
        local t = {coroutine.resume(co, val1, ...)}
        if bubbleYield then -- yield was meaningless
            coroutine.yield() -- carry through
        else
            return table.unpack(t) -- yield the user cares about
        end
    end
end

local function yield()
    bubbleYield = true
    coroutine.yield()
end

local function ensureYields()
    if bubbleYield then
        coroutine.yield()
    end
end

local function checkArg(idx, v, ...)
    local bad = true
    local n = select("#", ...)
    for i=1,n do
        local t = select(i, ...)
        if type(v) == t then bad = false break end
    end
    if not bad then return end
    local msg = string.format("bad argument #%d (%s expected, got %s)", idx, table.concat({...}, " or "), type(v))
    error(msg, 3)
end

local libcomponent

local componentProxy = {
    __pairs = function(self)
        local method
        return function()
            method = next(self, method)
            if method then
                return method, self[method]
            end
        end
    end,
}

local componentCallback = {
    __call = function(self, ...)
        return libcomponent.invoke(self.address, self.name, ...)
    end,
    __tostring = function(self)
        return libcomponent.doc(self.address, self.name) or "undocumented"
    end
}

libcomponent = {
    invoke = function(addr, method, ...)
        checkArg(1, addr, "string")
        checkArg(2, method, "string")

        while true do
            local r = {pcall(component.invoke, addr, method, ...)}
            computer.clearError()

            -- in this situation, either the temperature is above 100 C and we throttle
            -- or the call budget has been filled and we dont care
            if computer.isOverheating() or computer.isOverworked() then
                local ok = pcall(yield)
                assert(ok, "component explicitly requested to be suspended")
            end

            if computer.getState() == states.blackout then
                -- oops, powerout
                local ok = pcall(yield)
                assert(ok, "blackout")
            end

            if computer.getState() == states.busy then
                -- busy gets to try again
                computer.setState(states.running)
            else
                if r[1] then
                    return table.unpack(r, 2)
                end
                return nil, r[2]
            end
        end
    end,
    list = function(filter, exact)
        checkArg(1, filter, "string", "nil")
        local t = component.list()
        local list = t
        if filter then
            list = {}
            for addr, kind in pairs(t) do
                if type(exact) == "boolean" and exact then
                    if kind == filter then
                        list[addr] = kind
                    end
                elseif rawequal(exact, "pattern") then
                    if string.match(kind, filter) then
                        list[addr] = kind
                    end
                else
                    if string.find(kind, filter, nil, true) then
                        list[addr] = kind
                    end
                end
            end
        end

        local key = nil
        return setmetatable(list, {
            __call = function()
                key = next(list, key)
                if key then
                    return key, list[key]
                end
            end,
        })
    end,
    methods = component.methods,
    fields = component.fields,
    doc = component.doc,
    slot = component.slot,
    type = component.type,
    proxy = function(addr)
        checkArg(1, addr, "string")
        if not component.type(addr) then return nil, "no such component" end
        local proxy = setmetatable({
            address = addr,
            type = component.type(addr),
            slot = component.slot(addr),
            fields = {},
        }, componentProxy)
        local methods = component.methods(addr)
        for method in pairs(methods) do
            proxy[method] = setmetatable({address = addr, name = method}, componentCallback)
        end
        return proxy
    end,
}

local libcomputer = {
    isRobot = function()
        return libcomponent.list("robot", true) ~= nil
    end,
    address = computer.address,
    tmpAddress = computer.tmpAddress,
    usedMemory = computer.usedMemory,
    freeMemory = computer.freeMemory,
    totalMemory = computer.totalMemory,
    uptime = computer.uptime,
    energy = computer.energy,
    maxEnergy = computer.maxEnergy,
    users = computer.users,
    -- these 2 are not actually implemented
    -- TODO: implement them
    addUser = computer.addUser,
    removeUser = computer.removeUser,

    shutdown = function(reboot)
        computer.setState(reboot and states.REPEAT or states.closing)
        yield()
    end,
    pushSignal = computer.pushSignal,
    pullSignal = function(timeout)
        local deadline = computer.uptime() + (type(timeout) == "number" and timeout or math.huge)

        repeat
            yield() -- give executor a chance to give us stuff
            local s = table.pack(computer.popSignal())
            if s.n > 0 then
                return table.unpack(s)
            end
        until computer.uptime() >= deadline
    end,
    beep = computer.beep,
    getDeviceInfo = function()
        return {} -- yup
    end,
    getProgramLocations = function()
        return {} -- yup
    end,

    getArchitectures = computer.getArchitectures,
    getArchitecture = computer.getArchitecture,
    setArchitecture = function(...)
        computer.setArchitecture(...) -- also sets state to SWITCH
        yield()
    end,
    getTemperature = computer.getTemperature,
}

local sandbox
sandbox = {
    assert = assert,
    error = error,
    getmetatable = function(t)
        if type(t) == "string" then -- HUGE security problem
            return nil -- fixed
        end
        return getmetatable(t)
    end,
    ipairs = ipairs,
    load = function(ld, source, _, env) -- mode is ignored as bytecode is just fully illegal for now
        return load(ld, source, "t", env or sandbox)
    end,
    next = next,
    pairs = pairs,
    pcall = function(...)
        if tooLong() then
            yield()
            return false, tooLongWithoutYielding
        end
        local t = {pcall(...)}
        ensureYields() -- if it took too long, this will make it yield
        return table.unpack(t)
    end,
    rawequal = rawequal,
    rawget = rawget,
    rawlen = rawlen,
    rawset = rawset,
    select = select,
    setmetatable = function(t, mt)
        if type(mt) ~= "table" then
            return setmetatable(t, mt)
        end
        -- we do mutate the metatable but this field shouldn't exist anyways
        mt.__gc = nil
        return setmetatable(t, mt)
    end,
    tonumber = tonumber,
    tostring = tostring,
    type = type,
    _VERSION = _VERSION,
    xpcall = function(f, msgh, ...)
        checkArg(1, f, "function")
        checkArg(2, msgh, "function")

        -- to prevent infinite loops we simply terminate the error handler if it took too long.
        local function errorCapture(ff, ...)
            --ensureYields() -- you can't yield in xpcall...
            -- Immediately dont care
            if tooLong() then
                return nil, tooLongWithoutYielding
            end
            -- This would mean you shutdown in the errorCapture.
            -- In vanilla OC, that does nothing.
            -- In here, it returns a suspended error and then yields eventually.
            if bubbleYield then
                return nil, "suspended"
            end

            return xpcall(ff, function(...)
                if tooLong() then
                    return tooLongWithoutYielding
                else
                    return select(2, errorCapture(msgh, ...))
                end
            end, ...)
        end

        local t = {errorCapture(f, ...)}
        pcall(ensureYields) -- it can fail if we are doing xpcall in xpcall.
        return table.unpack(t)
    end,

    coroutine = {
        create = coroutine.create,
        resume = resume,
        running = coroutine.running,
        status = coroutine.status,
        yield = coroutine.yield,
        wrap = function(f)
            -- uses the correct resume
            local co = coroutine.create(f)
            return function(...)
                local result = {resume(co, ...)}
                if result[1] then
                    return table.unpack(result, 2)
                else
                    error(result[2], 0)
                end
            end
        end,
        isyieldable = coroutine.isyieldable,
    },

    string = copy(string),
    table = copy(table),
    math = copy(math, {
        -- patch table
        atan2 = math.atan,
        ldexp = function(a, e) return a * (2.0 ^ e) end,
        pow = function(a, b) return a ^ b end,
    }),

    os = {
        clock = os.clock,
        date = os.date,
        difftime = function(t2, t1) return t2 - t1 end, -- thanks UNIX
        time = function(t)
            checkArg(1, t, "table", "nil")
            return os.time(t)
        end,
    },

    debug = {
        getinfo = function(...)
          local result = debug.getinfo(...)
          if result then
            return {
              source = result.source,
              short_src = result.short_src,
              linedefined = result.linedefined,
              lastlinedefined = result.lastlinedefined,
              what = result.what,
              currentline = result.currentline,
              nups = result.nups,
              nparams = result.nparams,
              isvararg = result.isvararg,
              name = result.name,
              namewhat = result.namewhat,
              istailcall = result.istailcall,
              -- believe it or not, this IS NOT safe.
              -- They may use this to re-call machine.lua which would reset the hook and timeout.
              -- TODO: make this safe.
              --func = result.func,
            }
          end
        end,
        traceback = debug.traceback,
        -- we only allow the first return, aka not the value.
        -- Otherwise, some dumb shmuck could do nasty stuff.
        -- TODO: make them not need this.
        getlocal = function(...) return (debug.getlocal(...)) end,
        getupvalue = function(...) return (debug.getupvalue(...)) end,
    },

    utf8 = copy(utf8),
    unicode = copy(unicode, {
        wtrunc = function (str,space)
			space = space - 1
			return str:sub(1,(space >= utf8.len(str)) and (#str) or (utf8.offset(str,space+1)-1))
		end,
        isWide = function(s) return unicode.wlen(s) > unicode.len(s) end,
        upper = string.upper,
        lower = string.lower,
        --[[
        sub = function (str,a,b)
			if not b then b = utf8.len(str) end
			if not a then a = 1 end
			-- a = math.max(a,1)
			
			if a < 0 then
				-- negative
				
				a = utf8.len(str) + a + 1
			end
			
			if b < 0 then
				b = utf8.len(str) + b + 1
			end
			
			if a > b then return "" end
			
			if b >= utf8.len(str) then b = #str else b = utf8.offset(str,b+1)-1 end
			
			if a > utf8.len(str) then return "" end
			a = utf8.offset(str,a)
			
			return str:sub(a,b)
			-- return str:sub(a, b)
		end,
        ]]
    }),
    checkArg = checkArg,
    component = libcomponent,
    computer = libcomputer,
    debugprint = print,
}
sandbox._G = sandbox

local function bootstrap()
    local eeprom = libcomponent.list("eeprom")()
    assert(eeprom, "no eeprom")

    local code = libcomponent.invoke(eeprom, "get")
    assert(code and #code > 0, "empty eeprom")

    return assert(load(code, "=bios", "t", sandbox))
end

coroutine.yield() -- startup delay

local f = bootstrap()
local co = coroutine.create(f)

while true do
    timeout = nextDeadline()
    bubbleYield = false

    local ok, err = coroutine.resume(co)

    if not ok then
        error(debug.traceback(co, err), 0)
    elseif coroutine.status(co) == "dead" then
        error("computer halted", 0)
    else
        coroutine.yield()
    end
end
