local platform  = require 'bee.platform'
local files     = require 'files'
local furi      = require 'file-uri'
local workspace = require "workspace"
local config    = require 'config'
local scope     = require 'workspace.scope'
local util      = require 'utility'

---@class require-path
local m = {}

---@class require-manager
---@field scp scope
---@field nameMap table<string, string>
---@field visibleCache table<string, require-manager.visibleResult[]>
local mt = {}
mt.__index = mt

---@alias require-manager.visibleResult { searcher: string, name: string }

---@param scp scope
---@return require-manager
local function createRequireManager(scp)
    return setmetatable({
        scp = scp,
        nameMap = {},
        visibleCache = {},
    }, mt)
end

--- `aaa/bbb/ccc.lua` 与 `?.lua` 将返回 `aaa.bbb.cccc`
---@param path string
---@param searcher string
---@return string?
function mt:getRequireNameByPath(path, searcher)
    local separator    = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
    local stemPath     = path
                        : gsub('%.[^%.]+$', '')
                        : gsub('[/\\%.]+', separator)
    local stemSearcher = searcher
                        : gsub('%.[^%.]+$', '')
                        : gsub('[/\\]+', separator)
    local start        = stemSearcher:match '()%?' or 1
    if stemPath:sub(1, start - 1) ~= stemSearcher:sub(1, start - 1) then
        return nil
    end
    for pos = #stemPath, start, -1 do
        local word = stemPath:sub(start, pos)
        local newSearcher = stemSearcher:gsub('%?', (word:gsub('%%', '%%%%')))
        if newSearcher == stemPath then
            return word
        end
    end
    return nil
end

---@param path string
---@return require-manager.visibleResult[]
function mt:getRequireResultByPath(path)
    local uri = furi.encode(path)
    local searchers   = config.get(self.scp.uri, 'Lua.runtime.path')
    local strict      = config.get(self.scp.uri, 'Lua.runtime.pathStrict')
    local libUri      = files.getLibraryUri(self.scp.uri, uri)
    local libraryPath = libUri and furi.decode(libUri)
    local result = {}
    for _, searcher in ipairs(searchers) do
        local isAbsolute = searcher:match '^[/\\]'
                        or searcher:match '^%a+%:'
        searcher = workspace.normalize(searcher)
        if searcher:sub(1, 1) == '.' then
            strict = true
        end
        local cutedPath = path
        local currentPath = path
        local head
        local pos = 1
        if not isAbsolute then
            if libraryPath then
                currentPath = currentPath:sub(#libraryPath + 2)
            else
                currentPath = workspace.getRelativePath(uri)
            end
        end

        -- handle `../?.lua`
        local parentCount = 0
        for _ = 1, 1000 do
            if searcher:match '^%.%.[/\\]' then
                parentCount = parentCount + 1
                searcher = searcher:sub(4)
            else
                break
            end
        end
        if parentCount > 0 then
            local parentPath = libraryPath
                            or (self.scp.uri and furi.decode(self.scp.uri))
            if parentPath then
                local tail
                for _ = 1, parentCount do
                    parentPath, tail = parentPath:match '^(.+)[/\\]([^/\\]*)$'
                    currentPath = tail .. '/' .. currentPath
                end
            end
        end

        repeat
            cutedPath = currentPath:sub(pos)
            head = currentPath:sub(1, pos - 1)
            pos = currentPath:match('[/\\]+()', pos)
            if platform.OS == 'Windows' then
                searcher = searcher :gsub('[/\\]+', '\\')
            else
                searcher = searcher :gsub('[/\\]+', '/')
            end
            local name = self:getRequireNameByPath(cutedPath, searcher)
            if name then
                local mySearcher = searcher
                if head then
                    mySearcher = head .. searcher
                end
                result[#result+1] = {
                    name     = name,
                    searcher = mySearcher,
                }
            end
        until not pos or strict
    end
    return result
end

---@param name string
function mt:addName(name)
    local separator = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
    local fsname = name:gsub('%' .. separator, '/')
    self.nameMap[fsname] = name
end

---@return require-manager.visibleResult[]
function mt:getVisiblePath(path)
    local uri = furi.encode(path)
    if  not self.scp:isChildUri(uri)
    and not self.scp:isLinkedUri(uri) then
        return {}
    end
    path = workspace.normalize(path)
    local result = self.visibleCache[path]
    if not result then
        result = self:getRequireResultByPath(path)
        self.visibleCache[path] = result
    end
    return result
end

--- 查找符合指定require name的所有uri
---@param suri uri
---@param name string
---@return uri[]
---@return table<uri, string>?
function mt:findUrisByRequireName(suri, name)
    if type(name) ~= 'string' then
        return {}
    end
    local searchers   = config.get(self.scp.uri, 'Lua.runtime.path')
    local strict      = config.get(self.scp.uri, 'Lua.runtime.pathStrict')
    local separator   = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
    local path        = name:gsub('%' .. separator, '/')
    local results     = {}
    local searcherMap = {}

    for _, searcher in ipairs(searchers) do
        local fspath = searcher:gsub('%?', path)
        if self.scp.uri then
            local fullPath = workspace.getAbsolutePath(self.scp.uri, fspath)
            local fullUri  = furi.encode(fullPath)
            if  files.exists(fullUri)
            and fullUri ~= suri then
                results[#results+1] = fullUri
                searcherMap[fullUri] = searcher
            end
        end
        if not strict then
            local tail = '/' .. furi.encode(fspath):gsub('^file:[/]*', '')
            for uri in files.eachFile(self.scp.uri) do
                if  not searcherMap[uri]
                and suri ~= uri
                and util.stringEndWith(uri, tail) then
                    results[#results+1] = uri
                    local parentUri = files.getLibraryUri(self.scp.uri, uri) or self.scp.uri
                    if parentUri == nil or parentUri == '' then
                        parentUri = furi.encode ''
                    end
                    local relative  = uri:sub(#parentUri + 1):sub(1, - #tail)
                    searcherMap[uri] = workspace.normalize(relative .. searcher)
                end
            end
        end
    end

    for uri in files.eachDll() do
        local opens = files.getDllOpens(uri) or {}
        for _, open in ipairs(opens) do
            if open == path then
                results[#results+1] = uri
            end
        end
    end

    return results, searcherMap
end

---@param uri uri
---@param path string
---@return require-manager.visibleResult[]
function m.getVisiblePath(uri, path)
    local scp = scope.getScope(uri)
    ---@type require-manager
    local mgr = scp:get 'requireManager'
             or scp:set('requireManager', createRequireManager(scp))
    return mgr:getVisiblePath(path)
end

---@param uri uri
---@param name string
function m.findUrisByRequireName(uri, name)
    local scp = scope.getScope(uri)
    ---@type require-manager
    local mgr = scp:get 'requireManager'
             or scp:set('requireManager', createRequireManager(scp))
    return mgr:findUrisByRequireName(uri, name)
end

files.watch(function (ev, uri)
    if ev == 'create' or ev == 'delete' then
        for _, scp in ipairs(workspace.folders) do
            scp:set('requireManager', nil)
        end
        scope.fallback:set('requireManager', nil)
    end
end)

config.watch(function (uri, key, value, oldValue)
    if key == 'Lua.completion.requireSeparator'
    or key == 'Lua.runtime.path'
    or key == 'Lua.runtime.pathStrict' then
        local scp = scope.getScope(uri)
        scp:set('requireManager', nil)
    end
end)

return m
