--- This provides a pure Lua implementation of the builtin @{require} function
-- and @{package} library.
--
-- Generally you do not need to use this module - it is injected into the
-- every program's environment. However, it may be useful when building a
-- custom shell or when running programs yourself.
--
-- @module cc.require
-- @usage Construct the package and require function, and insert them into a
-- custom environment.
--
--     local r = require "cc.require"
--     local env = setmetatable({}, { __index = _ENV })
--     env.require, env.package = r.make(env, "/")

local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")
local expect = expect.expect

local function preload(package)
    return function(name)
        if package.preload[name] then
            return package.preload[name]
        else
            return nil, "no field package.preload['" .. name .. "']"
        end
    end
end

local function from_file(package, env, dir)
    return function(name)
        local fname = string.gsub(name, "%.", "/")
        local sError = ""
        for pattern in string.gmatch(package.path, "[^;]+") do
            local sPath = string.gsub(pattern, "%?", fname)
            if sPath:sub(1, 1) ~= "/" then
                sPath = fs.combine(dir, sPath)
            end
            if fs.exists(sPath) and not fs.isDir(sPath) then
                local fnFile, sError = loadfile(sPath, nil, env)
                if fnFile then
                    return fnFile, sPath
                else
                    return nil, sError
                end
            else
                if #sError > 0 then
                    sError = sError .. "\n  "
                end
                sError = sError .. "no file '" .. sPath .. "'"
            end
        end
        return nil, sError
    end
end

local function make_require(package)
    local sentinel = {}
    return function(name)
        expect(1, name, "string")

        if package.loaded[name] == sentinel then
            error("loop or previous error loading module '" .. name .. "'", 0)
        end

        if package.loaded[name] then
            return package.loaded[name]
        end

        local sError = "module '" .. name .. "' not found:"
        for _, searcher in ipairs(package.loaders) do
            local loader = table.pack(searcher(name))
            if loader[1] then
                package.loaded[name] = sentinel
                local result = loader[1](name, table.unpack(loader, 2, loader.n))
                if result == nil then result = true end

                package.loaded[name] = result
                return result
            else
                sError = sError .. "\n  " .. loader[2]
            end
        end
        error(sError, 2)
    end
end

--- Build an implementation of Lua's @{package} library, and a @{require}
-- function to load modules within it.
--
-- @tparam table env The environment to load packages into.
-- @tparam string dir The directory that relative packages are loaded from.
-- @treturn function The new @{require} function.
-- @treturn table The new @{package} library.
local function make_package(env, dir)
    expect(1, env, "table")
    expect(2, dir, "string")

    local package = {}
    package.loaded = {
        _G = _G,
        bit32 = bit32,
        coroutine = coroutine,
        math = math,
        package = package,
        string = string,
        table = table,
    }
    package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua"
    if turtle then
        package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua"
    elseif commands then
        package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua"
    end
    package.config = "/\n;\n?\n!\n-"
    package.preload = {}
    package.loaders = { preload(package), from_file(package, env, dir) }

    return make_require(package), package
end

return { make = make_package }
