-- preamble: common routines

local JSON = require("JSON")

-- silence JSON parsing errors
function JSON:assert () end  -- luacheck: no unused args

local color = require('color')
local w = require('tables').wrap
local matchers = require('matchers')

---
 -- Queries config options value using 'npm config' call
 -- @param  {string}  config_entry  Config option name
 -- @return {string}  Config value for specific option or
 --   empty string in case of any error
---
local function get_npm_config_value (config_entry)
    assert(config_entry and type(config_entry) == "string" and #config_entry > 0,
        "get_npm_config_value: config_entry param should be non-empty string")

    local proc = io.popen("2>nul npm config get "..config_entry)
    if not proc then return "" end

    local value = proc:read()
    proc:close()

    return value or nil
end

local modules = matchers.create_dirs_matcher('node_modules/*')

local cache_location = nil
local cached_modules_matcher = nil
local function cached_modules(token)
    -- If we already have matcher then just return it
    if cached_modules_matcher then return cached_modules_matcher(token) end

    -- otherwise try to get cache location and return empty table if failed
    cache_location = cache_location or get_npm_config_value("cache")
    if not cache_location then return {} end

    -- Create a new matcher, save it in module's variable for further usage and return it
    cached_modules_matcher = matchers.create_dirs_matcher(cache_location..'/*')
    return cached_modules_matcher(token)
end

local globals_location = nil
local global_modules_matcher = nil
local function global_modules(token)
    -- If we already have matcher then just return it
    if global_modules_matcher then return global_modules_matcher(token) end

    -- If token starts with . or .. or has path delimiter then return empty
    -- result and do not create a matcher so only fs paths will be completed
    if (token:match('^%.(%.)?') or token:match('[%\\%/]+')) then return {} end

    -- otherwise try to get cache location and return empty table if failed
    globals_location = globals_location or get_npm_config_value("prefix")
    if not globals_location then return {} end

    -- Create a new matcher, save it in module's variable for further usage and return it
    global_modules_matcher = matchers.create_dirs_matcher(globals_location..'/node_modules/*')
    return global_modules_matcher(token)
end

-- Reads package.json in current directory and extracts all "script" commands defined
local function scripts(token) -- luacheck: no unused args

    -- Read package.json first
    local package_json = io.open('package.json')
    -- If there is no such file, then close handle and return
    if package_json == nil then return w() end

    -- Read the whole file contents
    local package_contents = package_json:read("*a")
    package_json:close()

    local package_scripts = JSON:decode(package_contents).scripts
    return w(package_scripts):keys()
end

local parser = clink.arg.new_parser

-- end preamble

local install_parser = parser({matchers.dirs},
        "--force",
        "-g", "--global",
        "--link",
        "--no-bin-links",
        "--no-optional",
        "--no-shrinkwrap",
        "--nodedir=/",
        "--production",
        "--save", "--save-dev", "--save-optional",
        "--tag"
        ):loop(1)

-- TODO: list only global modules with -g
local remove_parser = parser({modules}, "-g", "--global"):loop(1)

local search_parser = parser("--long")

local script_parser = parser({scripts})

local list_parser = parser(
    {modules},
    "--prod", "--production",
    "--dev", "--development",
    "--only"..parser({"dev", "prod"}),
    "--json",
    "--long",
    "--parseable",
    "--global", "-g",
    "--depth",
    "--link"
)

local npm_parser = parser({
    "add-user",
    "adduser",
    "apihelp",
    "audit"..parser({
        "fix"..parser("--force", "--package-lock-only", "--dry-run", "--production", "--only=dev"),
        "--json",
        "--parseable"
    }),
    "author",
    "bin",
    "bugs",
    "c",
    "cache"..parser({
        "add"..parser({matchers.dirs}),
        "clean"..parser({cached_modules}),
        "ls"
        }),
    "completion",
    "config",
    "ddp",
    "dedupe",
    "deprecate",
    "docs",
    "edit",
    "explore",
    "faq",
    "find" .. search_parser,
    "find-dupes",
    "get",
    "help",
    "help-search",
    "home",
    "info",
    "init",
    "install" .. install_parser,
    "issues",
    "la",
    "link"..parser({matchers.files, global_modules}),
    "list"..list_parser,
    "ll"..list_parser,
    "ln"..parser({matchers.files, global_modules}),
    "login",
    "ls"..list_parser,
    "outdated"..parser(
        "--json",
        "--long",
        "--parseable",
        "--global",
        "--depth"
    ),
    "owner",
    "pack",
    "prefix",
    "prune",
    "publish"..parser(
        "--tag",
        "--access"..parser({"public", "restricted"})
    ),
    "r",
    "rb",
    "rebuild",
    "rm" .. remove_parser,
    "remove" .. remove_parser,
    "repo",
    "restart",
    "root",
    "run"..script_parser,
    "run-script"..script_parser,
    "search" .. search_parser,
    "set",
    "show",
    "shrinkwrap",
    "star",
    "stars",
    "start",
    "stop",
    "submodule",
    "tag",
    "test",
    "un",
    "uninstall" .. remove_parser,
    "unlink",
    "unpublish",
    "unstar",
    "up"..parser({modules}),
    "update"..parser({modules}),
    "v",
    "version",
    "view",
    "whoami"
    },
    "-h", "--version"
)

clink.arg.register_parser("npm", npm_parser)

local function escape_percents(s)
    return s:gsub('%%', '%%%%')
end

local function npm_prompt_filter()
    -- Automatically disable this when a .clinkprompt custom prompt is active.
    local customprompt = clink.getclinkprompt and clink.getclinkprompt()
    if customprompt and customprompt ~= "" then return false end

    local package_file = io.open('package.json')
    if not package_file then return false end

    local package_data = package_file:read('*a')
    package_file:close()

    local package = JSON:decode(package_data)
    -- Bail out if package.json is malformed
    if not package then return false end
    -- Don't print package info when the package is private or both version and name are missing
    if package.private or (not package.name and not package.version) then return false end

    local package_name = package.name or "<no name>"
    local package_version = package.version and "@"..package.version or ""
    local package_string = color.color_text("("..package_name..package_version..")", color.YELLOW)
    clink.prompt.value = clink.prompt.value:gsub('{git}', '{git} '..escape_percents(package_string))

    return false
end

clink.prompt.register_filter(npm_prompt_filter, 40)
