--[[
 General Lua Libraries for Lua 5.1, 5.2 & 5.3
 Copyright (C) 2002-2018 stdlib authors
]]
--[[--
 Additions to the core package module.

 The module table returned by `std.package` also contains all of the entries
 from the core `package` table.   An hygienic way to import this module, then, is
 simply to override core `package` locally:

      local package = require 'std.package'

 Manage `package.path` with normalization, duplicate removal,
 insertion & removal of elements and automatic folding of '/' and '?'
 onto `package.dirsep` and `package.pathmark`, for easy addition of
 new paths. For example, instead of all this:

      lib = std.io.catfile('.', 'lib', package.pathmark .. '.lua')
      paths = std.string.split(package.path, package.pathsep)
      for i, path in ipairs(paths) do
         -- ... lots of normalization code...
      end
      i = 1
      while i <= #paths do
         if paths[i] == lib then
            table.remove(paths, i)
         else
            i = i + 1
         end
      end
      table.insert(paths, 1, lib)
      package.path = table.concat(paths, package.pathsep)

 You can now write just:

      package.path = package.normalize('./lib/?.lua', package.path)

 @corelibrary std.package
]]


local _ = require 'std._base'

local argscheck = _.typecheck and _.typecheck.argscheck
local catfile = _.io.catfile
local escape_pattern = _.string.escape_pattern
local invert = _.table.invert
local split = _.string.split

_ = nil

local _ENV = require 'std.normalize' {
   'package',
   concat = 'table.concat',
   dirsep = 'package.dirsep',
   gsub = 'string.gsub',
   merge = 'table.merge',
   pathmark = 'package.pathmark',
   pathsep = 'package.pathsep',
   string_find = 'string.find',
   table_insert = 'table.insert',
   table_remove = 'table.remove',
}



--[[ =============== ]]--
--[[ Implementation. ]]--
--[[ =============== ]]--


--- Make named constants for `package.config`
-- (undocumented in 5.1; see luaconf.h for C equivalents).
-- @table package
-- @string dirsep directory separator
-- @string pathsep path separator
-- @string pathmark string that marks substitution points in a path template
-- @string execdir(Windows only) replaced by the executable's directory in a path
-- @string igmark Mark to ignore all before it when building `luaopen_` function name.


local function pathsub(path)
   return gsub(path, '%%?.', function(capture)
      if capture == '?' then
         return pathmark
      elseif capture == '/' then
         return dirsep
      else
         return gsub(capture, '^%%', '', 1)
      end
   end)
end


local function find(pathstrings, patt, init, plain)
   local paths = split(pathstrings, pathsep)
   if plain then
      patt = escape_pattern(patt)
   end
   init = init or 1
   if init < 0 then
      init = #paths - init
   end
   for i = init, #paths do
      if string_find(paths[i], patt) then
         return i, paths[i]
      end
   end
end


local function normalize(...)
   local i, paths, pathstrings = 1, {}, concat({...}, pathsep)
   for _, path in ipairs(split(pathstrings, pathsep)) do
      path = gsub(pathsub(path), catfile('^[^', ']'), catfile('.', '%0'))
      path = gsub(path, catfile('', '%.', ''), dirsep)
      path = gsub(path, catfile('', '%.$'), '')
      path = gsub(path, catfile('^%.', '%..', ''), catfile('..', ''))
      path = gsub(path, catfile('', '$'), '')

      -- Carefully remove redundant /foo/../ matches.
      repeat
         local again = false
         path = gsub(path, catfile('', '([^', ']+)', '%.%.', ''),
            function(dir1)
               if dir1 == '..' then   -- don't remove /../../
                  return catfile('', '..', '..', '')
               else
                  again = true
                  return dirsep
               end
            end)
         path = gsub(path, catfile('', '([^', ']+)', '%.%.$'),
               function(dir1)
                  if dir1 == '..' then -- don't remove /../..
                     return catfile('', '..', '..')
                  else
                     again = true
                     return ''
                  end
               end)
      until again == false

      -- Build an inverted table of elements to eliminate duplicates after
      -- normalization.
      if not paths[path] then
         paths[path], i = i, i + 1
      end
   end
   return concat(invert(paths), pathsep)
end


local function insert(pathstrings, ...)
   local paths = split(pathstrings, pathsep)
   table_insert(paths, ...)
   return normalize(unpack(paths, 1, len(paths)))
end


local function mappath(pathstrings, callback, ...)
   for _, path in ipairs(split(pathstrings, pathsep)) do
      local r = callback(path, ...)
      if r ~= nil then
         return r
      end
   end
end


local function remove(pathstrings, pos)
   local paths = split(pathstrings, pathsep)
   table_remove(paths, pos)
   return concat(paths, pathsep)
end



--[[ ================= ]]--
--[[ Public Interface. ]]--
--[[ ================= ]]--


local function X(decl, fn)
   return argscheck and argscheck('std.package.' .. decl, fn) or fn
end


local M = {
   --- Look for a path segment match of *patt* in *pathstrings*.
   -- @function find
   -- @string pathstrings `pathsep` delimited path elements
   -- @string patt a Lua pattern to search for in *pathstrings*
   -- @int[opt=1] init element(not byte index!) to start search at.
   --    Negative numbers begin counting backwards from the last element
   -- @bool[opt=false] plain unless false, treat *patt* as a plain
   --    string, not a pattern. Note that if *plain* is given, then *init*
   --    must be given as well.
   -- @return the matching element number(not byte index!) and full text
   --    of the matching element, if any; otherwise nil
   -- @usage
   --    i, s = find(package.path, '^[^' .. package.dirsep .. '/]')
   find = X('find(string, string, ?int, ?boolean|:plain)', find),

   --- Insert a new element into a `package.path` like string of paths.
   -- @function insert
   -- @string pathstrings a `package.path` like string
   -- @int[opt=n+1] pos element index at which to insert *value*, where `n` is
   --    the number of elements prior to insertion
   -- @string value new path element to insert
   -- @treturn string a new string with the new element inserted
   -- @usage
   --    package.path = insert(package.path, 1, install_dir .. '/?.lua')
   insert = X('insert(string, [int], string)', insert),

   --- Call a function with each element of a path string.
   -- @function mappath
   -- @string pathstrings a `package.path` like string
   -- @tparam mappathcb callback function to call for each element
   -- @param ... additional arguments passed to *callback*
   -- @return nil, or first non-nil returned by *callback*
   -- @usage
   --    mappath(package.path, searcherfn, transformfn)
   mappath = X('mappath(string, function, [any...])', mappath),

   --- Normalize a path list.
   -- Removing redundant `.` and `..` directories, and keep only the first
   -- instance of duplicate elements.   Each argument can contain any number
   -- of `pathsep` delimited elements; wherein characters are subject to
   -- `/` and `?` normalization, converting `/` to `dirsep` and `?` to
   -- `pathmark`(unless immediately preceded by a `%` character).
   -- @function normalize
   -- @param ... path elements
   -- @treturn string a single normalized `pathsep` delimited paths string
   -- @usage
   --    package.path = normalize(user_paths, sys_paths, package.path)
   normalize = X('normalize(string...)', normalize),

   --- Remove any element from a `package.path` like string of paths.
   -- @function remove
   -- @string pathstrings a `package.path` like string
   -- @int[opt=n] pos element index from which to remove an item, where `n`
   --    is the number of elements prior to removal
   -- @treturn string a new string with given element removed
   -- @usage
   --    package.path = remove(package.path)
   remove = X('remove(string, ?int)', remove),
}


return merge(package, M)


--- Types
-- @section Types

--- Function signature of a callback for @{mappath}.
-- @function mappathcb
-- @string element an element from a `pathsep` delimited string of
--    paths
-- @param ... additional arguments propagated from @{mappath}
-- @return non-nil to break, otherwise continue with the next element
