-- :Minify:--
local apis = ({...})[1]
local BOOT_DRIVE_PATH = apis.BOOT_DRIVE_PATH or "/$"
local fs = apis.fs
local native = apis.peripheral
local peripheral = {}
local sides = {"top", "bottom", "left", "right", "front", "back"}

function peripheral.getNames()
    local results = {}
    for n = 1, #sides do
        local side = sides[n]
        if native.isPresent(side) then
            table.insert(results, side)
            if native.hasType(side, "peripheral_hub") then
                local remote = native.call(side, "getNamesRemote")
                for _, name in ipairs(remote) do
                    table.insert(results, name)
                end
            end
        end
    end
    return results
end

function peripheral.isPresent(name)
    if native.isPresent(name) then
        return true
    end

    for n = 1, #sides do
        local side = sides[n]
        if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
            return true
        end
    end
    return false
end

function peripheral.getType(peripheral)
    if type(peripheral) == "string" then
        if native.isPresent(peripheral) then
            return native.getType(peripheral)
        end
        for n = 1, #sides do
            local side = sides[n]
            if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
                return native.call(side, "getTypeRemote", peripheral)
            end
        end
        return nil
    else
        local mt = getmetatable(peripheral)
        if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then
            error("bad argument #1 (table is not a peripheral)", 2)
        end
        return table.unpack(mt.types)
    end
end

function peripheral.hasType(peripheral, peripheral_type)
    if type(peripheral) == "string" then
        if native.isPresent(peripheral) then
            return native.hasType(peripheral, peripheral_type)
        end
        for n = 1, #sides do
            local side = sides[n]
            if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
                return native.call(side, "hasTypeRemote", peripheral, peripheral_type)
            end
        end
        return nil
    else
        local mt = getmetatable(peripheral)
        if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then
            error("bad argument #1 (table is not a peripheral)", 2)
        end
        return mt.types[peripheral_type] ~= nil
    end
end

function peripheral.getMethods(name)
    if native.isPresent(name) then
        return native.getMethods(name)
    end
    for n = 1, #sides do
        local side = sides[n]
        if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
            return native.call(side, "getMethodsRemote", name)
        end
    end
    return nil
end

function peripheral.getName(peripheral)
    local mt = getmetatable(peripheral)
    if not mt or mt.__name ~= "peripheral" or type(mt.name) ~= "string" then
        error("bad argument #1 (table is not a peripheral)", 2)
    end
    return mt.name
end

function peripheral.call(name, method, ...)
    if native.isPresent(name) then
        return native.call(name, method, ...)
    end

    for n = 1, #sides do
        local side = sides[n]
        if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
            return native.call(side, "callRemote", name, method, ...)
        end
    end
    return nil
end

function peripheral.wrap(name)
    local methods = peripheral.getMethods(name)
    if not methods then
        return nil
    end

    local types = { peripheral.getType(name) }
    for i = 1, #types do types[types[i]] = true end
    local result = setmetatable({}, {
        __name = "peripheral",
        name = name,
        type = types[1],
        types = types,
    })
    for _, method in ipairs(methods) do
        result[method] = function(...)
            return peripheral.call(name, method, ...)
        end
    end
    return result
end

function peripheral.find(ty, filter)
    local results = {}
    for _, name in ipairs(peripheral.getNames()) do
        if peripheral.hasType(name, ty) then
            local wrapped = peripheral.wrap(name)
            if filter == nil or filter(name, wrapped) then
                table.insert(results, wrapped)
            end
        end
    end
    return table.unpack(results)
end

local disks = {}
local internal = {}

local function norm(path)
    if not path or path == "" then return "/" end
    return fs.combine("/", path)
end

local function createDisk(id, basePath, readonly, periph)
    basePath = norm(basePath)

    local disk = {address = id, isReadOnly = function() return readonly end}

    function disk:spaceUsed()
        return fs.getCapacity(basePath) - fs.getFreeSpace(basePath)
    end

    function disk:spaceTotal() return fs.getCapacity(basePath) end

    function disk:list(path)
        local p = fs.combine(basePath, path)
        if not fs.exists(p) or not fs.isDir(p) then
            return nil, "not directory"
        end
        return fs.list(p)
    end

    function disk:fileExists(path)
        local p = fs.combine(basePath, path)
        return fs.exists(p) and not fs.isDir(p)
    end

    function disk:directoryExists(path)
        local p = fs.combine(basePath, path)
        return fs.exists(p) and fs.isDir(p)
    end

    function disk:type(path)
        local p = fs.combine(basePath, path)
        if not fs.exists(p) then
            return nil
        elseif fs.isDir(p) then
            return "directory"
        else
            return "file"
        end
    end

    function disk:makeDirectory(path)
        local p = fs.combine(basePath, path)
        fs.makeDir(p)
        return true
    end

    function disk:remove(path)
        local p = fs.combine(basePath, path)
        if fs.exists(p) then fs.delete(p) end
        return true
    end

    function disk:setLabel(label) periph.setLabel(label) end

    function disk:getLabel(label) return periph.getLabel() end

    function disk:attributes(path)
        local p = fs.combine(basePath, path)
        return fs.attributes(p)
    end

    function disk:open(path, mode)
        local p = fs.combine(basePath, path)
        return fs.open(p, mode)
    end

    return disk
end

internal["$"] = createDisk("$", BOOT_DRIVE_PATH, false, {
    setLabel = function(label)
        local h = fs.open("/.label", "w")
        h.write(label)
        h.close()
    end,
    getLabel = function()
        local h = fs.open("/.label", "r")
        if not h then return "$" end
        local label = h.readAll()
        h.close()
        return label
    end
})

local function refresh()
    for id, _ in pairs(disks) do
        if not peripheral.getType(id) then disks[id] = nil end
    end

    for _, disk in ipairs({peripheral.find("drive")}) do
        if disk.isDiskPresent() then
            disks[tostring(disk.getDiskID())]=createDisk("cctdisk"..tostring(disk.getDiskID()), disk.getMountPath(), false, fs)
        end
    end
end

local function iter()
    refresh()
    local combined = {}

    for id, obj in pairs(internal) do combined[id] = obj end
    for id, obj in pairs(disks) do combined[id] = obj end

    return pairs(combined)
end

return {refresh = refresh, list = iter}
