
--- Common fs operations implemented with third-party tools.
local tools = {}

local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local cfg = require("luarocks.core.cfg")

local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end })

local dir_stack = {}

do
   local tool_cache = {}

   local tool_options = {
      downloader = {
         desc = "downloader",
         { var = "WGET", name = "wget" },
         { var = "CURL", name = "curl" },
      },
      md5checker = {
         desc = "MD5 checker",
         { var = "MD5SUM", name = "md5sum" },
         { var = "OPENSSL", name = "openssl", cmdarg = "md5" },
         { var = "MD5", name = "md5" },
      },
   }

   function tools.which_tool(tooltype)
      local tool = tool_cache[tooltype]
      local names = {}
      if not tool then
         for _, opt in ipairs(tool_options[tooltype]) do
            table.insert(names, opt.name)
            if fs.is_tool_available(vars[opt.var], opt.name) then
               tool = opt
               tool_cache[tooltype] = opt
               break
            end
         end
      end
      if not tool then
         local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names]
         return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system"
      end
      return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "")
   end
end

local current_dir_with_cache
do
   local cache_pwd

   current_dir_with_cache = function()
      local current = cache_pwd
      if not current then
         local pipe = io.popen(fs.quiet_stderr(vars.PWD))
         current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "")
         pipe:close()
         cache_pwd = current
      end
      for _, directory in ipairs(dir_stack) do
         current = fs.absolute_name(directory, current)
      end
      return current, cache_pwd
   end

   --- Obtain current directory.
   -- Uses the module's internal directory stack.
   -- @return string: the absolute pathname of the current directory.
   function tools.current_dir()
      return (current_dir_with_cache()) -- drop second return
   end
end

--- Change the current directory.
-- Uses the module's internal directory stack. This does not have exact
-- semantics of chdir, as it does not handle errors the same way,
-- but works well for our purposes for now.
-- @param directory string: The directory to switch to.
-- @return boolean or (nil, string): true if successful, (nil, error message) if failed.
function tools.change_dir(directory)
   assert(type(directory) == "string")
   if fs.is_dir(directory) then
      table.insert(dir_stack, directory)
      return true
   end
   return nil, "directory not found: "..directory
end

--- Change directory to root.
-- Allows leaving a directory (e.g. for deleting it) in
-- a crossplatform way.
function tools.change_dir_to_root()
   local curr_dir = fs.current_dir()
   if not curr_dir or not fs.is_dir(curr_dir) then
      return false
   end
   table.insert(dir_stack, "/")
   return true
end

--- Change working directory to the previous in the directory stack.
function tools.pop_dir()
   local directory = table.remove(dir_stack)
   return directory ~= nil
end

--- Run the given command.
-- The command is executed in the current directory in the directory stack.
-- @param cmd string: No quoting/escaping is applied to the command.
-- @return boolean: true if command succeeds (status code 0), false
-- otherwise.
function tools.execute_string(cmd)
   local current, cache_pwd = current_dir_with_cache()
   if not current then return false end
   if current ~= cache_pwd then
      cmd = fs.command_at(current, cmd)
   end
   local code = os.execute(cmd)
   if code == 0 or code == true then
      return true
   else
      return false
   end
end

--- Internal implementation function for fs.dir.
-- Yields a filename on each iteration.
-- @param at string: directory to list
-- @return nil
function tools.dir_iterator(at)
   local pipe = io.popen(fs.command_at(at, vars.LS, true))
   for file in pipe:lines() do
      if file ~= "." and file ~= ".." then
         coroutine.yield(file)
      end
   end
   pipe:close()
end

--- Download a remote file.
-- @param url string: URL to be fetched.
-- @param filename string or nil: this function attempts to detect the
-- resulting local filename of the remote file as the basename of the URL;
-- if that is not correct (due to a redirection, for example), the local
-- filename can be given explicitly as this second argument.
-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to
-- re-downloading the file.
-- @return (string, string, string): filename on success,
-- false and the error message and code on failure.
function tools.use_downloader(url, filename, cache)
   assert(type(url) == "string")
   assert(type(filename) == "string" or not filename)

   filename = fs.absolute_name(filename or dir.base_name(url))

   local downloader, err = fs.which_tool("downloader")
   if not downloader then
      return nil, err, "downloader"
   end

   local ok = false
   if downloader == "wget" then
      local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet "
      if cfg.connection_timeout and cfg.connection_timeout > 0 then
        wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 "
      end
      if cache then
         -- --timestamping is incompatible with --output-document,
         -- but that's not a problem for our use cases.
         fs.delete(filename .. ".unixtime")
         fs.change_dir(dir.dir_name(filename))
         ok = fs.execute_quiet(wget_cmd.." --timestamping ", url)
         fs.pop_dir()
      elseif filename then
         ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url)
      else
         ok = fs.execute_quiet(wget_cmd, url)
      end
   elseif downloader == "curl" then
      local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" "
      if cfg.connection_timeout and cfg.connection_timeout > 0 then
        curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." "
      end
      if cache then
         curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" "
      end
      ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename)))
   end
   if ok then
      return filename
   else
      os.remove(filename)
      return nil, "failed downloading " .. url, "network"
   end
end

--- Get the MD5 checksum for a file.
-- @param file string: The file to be computed.
-- @return string: The MD5 checksum or nil + message
function tools.get_md5(file)
   local ok, md5checker = fs.which_tool("md5checker")
   if not ok then
      return false, md5checker
   end

   local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file)))
   local computed = pipe:read("*l")
   pipe:close()
   if computed then
      computed = computed:match("("..("%x"):rep(32)..")")
   end
   if computed then
      return computed
   else
      return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file))
   end
end

return tools
