-- Copyright 2007-2020 Mitchell mitchell.att.foicica.com. See LICENSE.

--[[ This comment is for LuaDoc.
---
-- Extends the `lfs` library to find files in directories and determine absolute
-- file paths.
module('lfs')]]

---
-- The filter table containing common binary file extensions and version control
-- directories to exclude when iterating over files and directories using
-- `dir_foreach`.
-- @see dir_foreach
-- @class table
-- @name default_filter
lfs.default_filter = {
  -- File extensions to exclude.
  '!.a', '!.bmp', '!.bz2', '!.class', '!.dll', '!.exe', '!.gif', '!.gz',
  '!.jar', '!.jpeg', '!.jpg', '!.o', '!.pdf', '!.png', '!.so', '!.tar', '!.tgz',
  '!.tif', '!.tiff', '!.xz', '!.zip',
  -- Directories to exclude.
  '!/%.bzr$', '!/%.git$', '!/%.hg$', '!/%.svn$', '!/node_modules$',
}

---
-- Iterates over all files and sub-directories (up to *n* levels deep) in
-- directory *dir*, calling function *f* with each file found.
-- String or list *filter* determines which files to pass through to *f*, with
-- the default filter being `lfs.default_filter`. A filter consists of Lua
-- patterns that match file and directory paths to include or exclude. Exclusive
-- patterns begin with a '!'. If no inclusive patterns are given, any path is
-- initially considered. As a convenience, file extensions can be specified
-- literally instead of as a Lua pattern (e.g. '.lua' vs. '%.lua$'), and '/'
-- also matches the Windows directory separator ('[/\\]' is not needed).
-- @param dir The directory path to iterate over.
-- @param f Function to call with each full file path found. If *f* returns
--   `false` explicitly, iteration ceases.
-- @param filter Optional filter for files and directories to include and
--   exclude. The default value is `lfs.default_filter`.
-- @param n Optional maximum number of directory levels to descend into.
--   The default value is `nil`, which indicates no limit.
-- @param include_dirs Optional flag indicating whether or not to call *f* with
--   directory names too. Directory names are passed with a trailing '/' or '\',
--   depending on the current platform.
--   The default value is `false`.
-- @param level Utility value indicating the directory level this function is
--   at. This value is used and set internally, and should not be set otherwise.
-- @see filter
-- @name dir_foreach
function lfs.dir_foreach(dir, f, filter, n, include_dirs, level)
  if not level then
    -- Convert filter to a table from nil or string arguments.
    if not filter then filter = lfs.default_filter end
    if type(filter) == 'string' then filter = {filter} end
    -- Process the given filter into something that can match files more easily
    -- and/or quickly. For example, convert '.ext' shorthand to '%.ext$',
    -- substitute '/' with '[/\\]', and enable hash lookup for file extensions
    -- to include or exclude.
    local processed_filter = {
      consider_any = true,
      exts = setmetatable({}, {__index = function() return true end})
    }
    for i = 1, #filter do
      local patt = filter[i]
      patt = patt:gsub('^(!?)%%?%.([^.]+)$', '%1%%.%2$') -- '.lua' to '%.lua$'
      patt = patt:gsub('/([^\\])', '[/\\]%1') -- '/' to '[/\\]'
      local include = not patt:find('^!')
      local ext = patt:match('^!?%%.([^.]+)%$$')
      if ext then
        processed_filter.exts[ext] = include
        if include then setmetatable(processed_filter.exts, nil) end
      else
        if include then processed_filter.consider_any = false end
        processed_filter[#processed_filter + 1] = patt
      end
    end
    filter = processed_filter
  end
  local dir_sep, lfs_attributes = not WIN32 and '/' or '\\', lfs.attributes
  for basename in lfs.dir(dir) do
    if basename:find('^%.%.?$') then goto continue end -- ignore . and ..
    local filename = dir..(dir ~= '/' and dir_sep or '')..basename
    local mode = lfs_attributes(filename, 'mode')
    if mode ~= 'directory' and mode ~= 'file' then goto continue end
    local include
    if mode == 'file' then
      local ext = filename:match('[^.]+$')
      if ext and not filter.exts[ext] then goto continue end
      include = filter.consider_any or ext ~= nil
    elseif mode == 'directory' then
      include = filter.consider_any
    end
    for i = 1, #filter do
      local patt = filter[i]
      -- Treat exclusive patterns as logical AND.
      if patt:find('^!') and filename:find(patt:sub(2)) then goto continue end
      -- Treat inclusive patterns as logical OR.
      include = include or (not patt:find('^!') and filename:find(patt))
    end
    if include and mode == 'directory' then
      if include_dirs and f(filename..dir_sep) == false then return end
      if not n or (level or 0) < n then
        local halt = lfs.dir_foreach(filename, f, filter, n, include_dirs,
                                     (level or 0) + 1) == false
        if halt then return false end
      end
    elseif include and mode == 'file' then
      if f(filename) == false then return false end
    end
    ::continue::
  end
end

---
-- Returns the absolute path to string *filename*.
-- *prefix* or `lfs.currentdir()` is prepended to a relative filename. The
-- returned path is not guaranteed to exist.
-- @param filename The relative or absolute path to a file.
-- @param prefix Optional prefix path prepended to a relative filename.
-- @return string absolute path
-- @name abspath
function lfs.abspath(filename, prefix)
  if WIN32 then filename = filename:gsub('/', '\\') end
  if not filename:find(not WIN32 and '^/' or '^%a:[/\\]') and
     not (WIN32 and filename:find('^\\\\')) then
    prefix = prefix or lfs.currentdir()
    filename = prefix..(not WIN32 and '/' or '\\')..filename
  end
  filename = filename:gsub('%f[^/\\]%.[/\\]', '') -- clean up './'
  while filename:find('[^/\\]+[/\\]%.%.[/\\]') do
    filename = filename:gsub('[^/\\]+[/\\]%.%.[/\\]', '', 1) -- clean up '../'
  end
  return filename
end
