local f=_G local A=string.sub local Z=os.getenv local C=os.tmpname local O=package local b,I,h=table.insert,table.concat,table.remove local L=require("\112\108\046\117\116\105\108\115")local W,G=L.assert_string,L.raise local o,R=f.pcall(f.require,"\108\102\115")if not o then error("\112\108\046\112\097\116\104 \114\101\113\117\105\114\101\115 \076\117\097\070\105\108\101\083\121\115\116\101\109")end local m=R.attributes local q=R.currentdir local l=R.symlinkattributes local F={}local function j(f,A,Z,C)local O=("\037\115 \102\097\105\108\101\100"):format(tostring(f))if A~=nil then O=O..(" \102\111\114 \'\037\115\'"):format(tostring(A))end O=O..("\058 \037\115"):format(tostring(Z))if C~=nil then O=O..(" \040\099\111\100\101 \037\115\041"):format(tostring(C))end return O end F.dir=R.dir F.mkdir=function(f)local A,Z,C=R.mkdir(f)if not A then return A,j("\109\107\100\105\114",f,Z,C),C end return A,Z,C end F.rmdir=function(f)local A,Z,C=R.rmdir(f)if not A then return A,j("\114\109\100\105\114",f,Z,C),C end return A,Z,C end F.attrib=function(f,A)local Z,C,O=m(f,A)if not Z then return Z,j("\097\116\116\114\105\098",f,C,O),O end return Z,C,O end F.currentdir=function()local f,A,Z=q()if not f then return f,j("\099\117\114\114\101\110\116\100\105\114",nil,A,Z),Z end return f,A,Z end F.link_attrib=function(f,A)local Z,C,O=l(f,A)if not Z then return Z,j("\108\105\110\107\095\097\116\116\114\105\098",f,C,O),O end return Z,C,O end F.chdir=function(f)local A,Z,C=R.chdir(f)if not A then return A,j("\099\104\100\105\114",f,Z,C),C end return A,Z,C end function F.isdir(f)W(1,f)return m(f,"\109\111\100\101")=="\100\105\114\101\099\116\111\114\121"end function F.isfile(f)W(1,f)return m(f,"\109\111\100\101")=="\102\105\108\101"end function F.islink(f)W(1,f)if l then return l(f,"\109\111\100\101")=="\108\105\110\107"else return false end end function F.getsize(f)W(1,f)return m(f,"\115\105\122\101")end function F.exists(f)W(1,f)return m(f,"\109\111\100\101")~=nil and f end function F.getatime(f)W(1,f)return m(f,"\097\099\099\101\115\115")end function F.getmtime(f)W(1,f)return m(f,"\109\111\100\105\102\105\099\097\116\105\111\110")end function F.getctime(f)W(1,f)return F.attrib(f,"\099\104\097\110\103\101")end local function P(f,Z)return A(f,Z,Z)end F.is_windows=L.is_windows local d,i,e if F.is_windows then F.sep="\092"i="\047"F.dirsep="\059"e={["\047"]=true;["\092"]=true}else F.sep="\047"F.dirsep="\058"e={["\047"]=true}end d=F.sep function F.splitpath(f)W(1,f)local Z=#f local C=P(f,Z)while Z>0 and(C~=d and C~=i)do Z=Z-1 C=P(f,Z)end if Z==0 then return"",f else return A(f,1,Z-1),A(f,Z+1)end end function F.abspath(f,A)W(1,f)if A then W(2,A)end local Z=A~=nil if not Z and not q()then return f end f=f:gsub("\091\092\047\093\036","")A=A or q()if not F.isabs(f)then f=F.join(A,f)elseif F.is_windows and(not Z and(P(f,2)~="\058"and P(f,2)~="\092"))then f=A:sub(1,2)..f end return F.normpath(f)end function F.splitext(f)W(1,f)local Z=#f local C=P(f,Z)while Z>0 and C~="\046"do if e[C]then return f,""end Z=Z-1 C=P(f,Z)end if Z==0 then return f,""else return A(f,1,Z-1),A(f,Z)end end function F.dirname(f)W(1,f)local A=F.splitpath(f)return A end function F.basename(f)W(1,f)local A,Z=F.splitpath(f)return Z end function F.extension(f)W(1,f)local A,Z=F.splitext(f)return Z end function F.isabs(f)W(1,f)if F.is_windows and P(f,2)=="\058"then return e[P(f,3)]~=nil end return e[P(f,1)]~=nil end function F.join(f,A,...)W(1,f)W(2,A)if select("\035",...)>0 then local Z=F.join(f,A)local C={...}for f=1,#C,1 do W(f,C[f])Z=F.join(Z,C[f])end return Z end if F.isabs(A)then return A end local Z=P(f,#f)if Z~=F.sep and(Z~=i and Z~="")then f=f..F.sep end return f..A end function F.normcase(f)W(1,f)if F.is_windows then return(f:gsub("\047","\092")):lower()else return f end end function F.normpath(f)W(1,f)local A=""if F.is_windows then if f:match("\094\092\092")then A="\092\092"f=f:sub(3)elseif e[P(f,1)]then A="\092"f=f:sub(2)elseif P(f,2)=="\058"then A=f:sub(1,2)f=f:sub(3)if e[P(f,1)]then A=A.."\092"f=f:sub(2)end end f=f:gsub("\047","\092")else if f:match("\094\047\047")and P(f,3)~="\047"then A="\047\047"f=f:sub(3)elseif P(f,1)=="\047"then A="\047"f=f:match("\094\047\042\040\046\042\041\036")end end local Z={}for f in f:gmatch("\091\094"..(d.."\093\043"))do if f=="\046\046"then if#Z~=0 and Z[#Z]~="\046\046"then h(Z)else b(Z,f)end elseif f~="\046"then b(Z,f)end end f=A..I(Z,d)if f==""then f="\046"end return f end function F.relpath(f,A)W(1,f)if A then W(2,A)end local Z,C,O=L.split,math.min,table.insert f=F.abspath(f,A)A=A or q()local b if F.is_windows then f=f:gsub("\047","\092")A=A:gsub("\047","\092")b=function(f)return f:lower()end else b=function(f)return f end end local I,h=Z(A,d),Z(f,d)local G=C(#I,#h)if F.is_windows and(G>0 and(P(h[1],2)=="\058"and h[1]~=I[1]))then return f end local o=G+1 for f=1,G,1 do if b(I[f])~=b(h[f])then o=f break end end local R={}for f=1,(#I-o)+1,1 do R[f]="\046\046"end if o<=#h then for f=o,#h,1 do O(R,h[f])end end return table.concat(R,d)end function F.expanduser(f)W(1,f)if P(f,1)=="~"then local C=Z("\072\079\077\069")if not C then C=Z("\085\083\069\082\080\082\079\070\073\076\069")or Z("\072\079\077\069\068\082\073\086\069")..Z("\072\079\077\069\080\065\084\072")end return C..A(f,2)else return f end end function F.tmpname()local f=C()if F.is_windows and not f:find("\058")then f=Z("\084\069\077\080")..f end return f end function F.common_prefix(f,A)W(1,f)W(2,A)if#f>#A then A,f=f,A end local Z if F.is_windows then f=f:gsub("\047","\092")A=A:gsub("\047","\092")Z=function(f)return f:lower()end else Z=function(f)return f end end for C=1,#f,1 do if Z(P(f,C))~=Z(P(A,C))then local A=f:sub(1,C-1)if P(f,C-1)~=d then A=F.dirname(A)end return A end end if P(A,#f+1)~=d then f=F.dirname(f)end return f end function F.package_path(f)W(1,f)local A,Z,C A,Z=O.searchpath(f,O.path)if A then return A,true end A,C=O.searchpath(f,O.cpath)if A then return A,false end return G("\099\097\110\110\111\116 \102\105\110\100 \109\111\100\117\108\101 \111\110 \112\097\116\104\n"..(Z..("\n"..C)))end return F
--- Path manipulation and file queries.
--
-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}.
--
-- NOTE: the functions assume the paths being dealt with to originate
-- from the OS the application is running on. Windows drive letters are not
-- to be used when running on a Unix system for example. The one exception
-- is Windows paths to allow both forward and backward slashes (since Lua
-- also accepts those)
--
-- Dependencies: `pl.utils`, `lfs`
-- @module pl.path

-- imports and locals
local _G = _G
local sub = string.sub
local getenv = os.getenv
local tmpnam = os.tmpname
local package = package
local append, concat, remove = table.insert, table.concat, table.remove
local utils = require 'pl.utils'
local assert_string,raise = utils.assert_string,utils.raise

local res,lfs = _G.pcall(_G.require,'lfs')
if not res then
    error("pl.path requires LuaFileSystem")
end

local attrib = lfs.attributes
local currentdir = lfs.currentdir
local link_attrib = lfs.symlinkattributes

local path = {}

local function err_func(name, param, err, code)
  local ret = ("%s failed"):format(tostring(name))
  if param ~= nil then
    ret = ret .. (" for '%s'"):format(tostring(param))
  end
  ret = ret .. (": %s"):format(tostring(err))
  if code ~= nil then
    ret = ret .. (" (code %s)"):format(tostring(code))
  end
  return ret
end

--- Lua iterator over the entries of a given directory.
-- Implicit link to [`luafilesystem.dir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function dir
path.dir = lfs.dir

--- Creates a directory.
-- Implicit link to [`luafilesystem.mkdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function mkdir
path.mkdir = function(d)
  local ok, err, code = lfs.mkdir(d)
  if not ok then
    return ok, err_func("mkdir", d, err, code), code
  end
  return ok, err, code
end

--- Removes a directory.
-- Implicit link to [`luafilesystem.rmdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function rmdir
path.rmdir = function(d)
  local ok, err, code = lfs.rmdir(d)
  if not ok then
    return ok, err_func("rmdir", d, err, code), code
  end
  return ok, err, code
end

--- Gets attributes.
-- Implicit link to [`luafilesystem.attributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function attrib
path.attrib = function(d, r)
  local ok, err, code = attrib(d, r)
  if not ok then
    return ok, err_func("attrib", d, err, code), code
  end
  return ok, err, code
end

--- Get the working directory.
-- Implicit link to [`luafilesystem.currentdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function currentdir
path.currentdir = function()
  local ok, err, code = currentdir()
  if not ok then
    return ok, err_func("currentdir", nil, err, code), code
  end
  return ok, err, code
end

--- Gets symlink attributes.
-- Implicit link to [`luafilesystem.symlinkattributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function link_attrib
path.link_attrib = function(d, r)
  local ok, err, code = link_attrib(d, r)
  if not ok then
    return ok, err_func("link_attrib", d, err, code), code
  end
  return ok, err, code
end

--- Changes the working directory.
-- On Windows, if a drive is specified, it also changes the current drive. If
-- only specifying the drive, it will only switch drive, but not modify the path.
-- Implicit link to [`luafilesystem.chdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
-- @function chdir
path.chdir = function(d)
  local ok, err, code = lfs.chdir(d)
  if not ok then
    return ok, err_func("chdir", d, err, code), code
  end
  return ok, err, code
end

--- is this a directory?
-- @string P A file path
function path.isdir(P)
    assert_string(1,P)
    return attrib(P,'mode') == 'directory'
end

--- is this a file?
-- @string P A file path
function path.isfile(P)
    assert_string(1,P)
    return attrib(P,'mode') == 'file'
end

-- is this a symbolic link?
-- @string P A file path
function path.islink(P)
    assert_string(1,P)
    if link_attrib then
        return link_attrib(P,'mode')=='link'
    else
        return false
    end
end

--- return size of a file.
-- @string P A file path
function path.getsize(P)
    assert_string(1,P)
    return attrib(P,'size')
end

--- does a path exist?
-- @string P A file path
-- @return the file path if it exists (either as file, directory, socket, etc), nil otherwise
function path.exists(P)
    assert_string(1,P)
    return attrib(P,'mode') ~= nil and P
end

--- Return the time of last access as the number of seconds since the epoch.
-- @string P A file path
function path.getatime(P)
    assert_string(1,P)
    return attrib(P,'access')
end

--- Return the time of last modification as the number of seconds since the epoch.
-- @string P A file path
function path.getmtime(P)
    assert_string(1,P)
    return attrib(P,'modification')
end

---Return the system's ctime as the number of seconds since the epoch.
-- @string P A file path
function path.getctime(P)
    assert_string(1,P)
    return path.attrib(P,'change')
end


local function at(s,i)
    return sub(s,i,i)
end

path.is_windows = utils.is_windows

local sep, other_sep, seps
-- constant sep is the directory separator for this platform.
-- constant dirsep is the separator in the PATH environment variable
if path.is_windows then
    path.sep = '\\'; other_sep = '/'
    path.dirsep = ';'
    seps = { ['/'] = true, ['\\'] = true }
else
    path.sep = '/'
    path.dirsep = ':'
    seps = { ['/'] = true }
end
sep = path.sep

--- are we running Windows?
-- @class field
-- @name path.is_windows

--- path separator for this platform.
-- @class field
-- @name path.sep

--- separator for PATH for this platform
-- @class field
-- @name path.dirsep

--- given a path, return the directory part and a file part.
-- if there's no directory part, the first value will be empty
-- @string P A file path
-- @return directory part
-- @return file part
-- @usage
-- local dir, file = path.splitpath("some/dir/myfile.txt")
-- assert(dir == "some/dir")
-- assert(file == "myfile.txt")
--
-- local dir, file = path.splitpath("some/dir/")
-- assert(dir == "some/dir")
-- assert(file == "")
--
-- local dir, file = path.splitpath("some_dir")
-- assert(dir == "")
-- assert(file == "some_dir")
function path.splitpath(P)
    assert_string(1,P)
    local i = #P
    local ch = at(P,i)
    while i > 0 and ch ~= sep and ch ~= other_sep do
        i = i - 1
        ch = at(P,i)
    end
    if i == 0 then
        return '',P
    else
        return sub(P,1,i-1), sub(P,i+1)
    end
end

--- return an absolute path.
-- @string P A file path
-- @string[opt] pwd optional start path to use (default is current dir)
function path.abspath(P,pwd)
    assert_string(1,P)
    if pwd then assert_string(2,pwd) end
    local use_pwd = pwd ~= nil
    if not use_pwd and not currentdir() then return P end
    P = P:gsub('[\\/]$','')
    pwd = pwd or currentdir()
    if not path.isabs(P) then
        P = path.join(pwd,P)
    elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then
        P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
    end
    return path.normpath(P)
end

--- given a path, return the root part and the extension part.
-- if there's no extension part, the second value will be empty
-- @string P A file path
-- @treturn string root part (everything upto the "."", maybe empty)
-- @treturn string extension part (including the ".", maybe empty)
-- @usage
-- local file_path, ext = path.splitext("/bonzo/dog_stuff/cat.txt")
-- assert(file_path == "/bonzo/dog_stuff/cat")
-- assert(ext == ".txt")
--
-- local file_path, ext = path.splitext("")
-- assert(file_path == "")
-- assert(ext == "")
function path.splitext(P)
    assert_string(1,P)
    local i = #P
    local ch = at(P,i)
    while i > 0 and ch ~= '.' do
        if seps[ch] then
            return P,''
        end
        i = i - 1
        ch = at(P,i)
    end
    if i == 0 then
        return P,''
    else
        return sub(P,1,i-1),sub(P,i)
    end
end

--- return the directory part of a path
-- @string P A file path
-- @treturn string everything before the last dir-separator
-- @see splitpath
-- @usage
-- path.dirname("/some/path/file.txt")   -- "/some/path"
-- path.dirname("file.txt")              -- "" (empty string)
function path.dirname(P)
    assert_string(1,P)
    local p1 = path.splitpath(P)
    return p1
end

--- return the file part of a path
-- @string P A file path
-- @treturn string
-- @see splitpath
-- @usage
-- path.basename("/some/path/file.txt")  -- "file.txt"
-- path.basename("/some/path/file/")     -- "" (empty string)
function path.basename(P)
    assert_string(1,P)
    local _,p2 = path.splitpath(P)
    return p2
end

--- get the extension part of a path.
-- @string P A file path
-- @treturn string
-- @see splitext
-- @usage
-- path.extension("/some/path/file.txt") -- ".txt"
-- path.extension("/some/path/file_txt") -- "" (empty string)
function path.extension(P)
    assert_string(1,P)
    local _,p2 = path.splitext(P)
    return p2
end

--- is this an absolute path?
-- @string P A file path
-- @usage
-- path.isabs("hello/path")    -- false
-- path.isabs("/hello/path")   -- true
-- -- Windows;
-- path.isabs("hello\path")    -- false
-- path.isabs("\hello\path")   -- true
-- path.isabs("C:\hello\path") -- true
-- path.isabs("C:hello\path")  -- false
function path.isabs(P)
    assert_string(1,P)
    if path.is_windows and at(P,2) == ":" then
        return seps[at(P,3)] ~= nil
    end
    return seps[at(P,1)] ~= nil
end

--- return the path resulting from combining the individual paths.
-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
-- empty elements (except the last) will be ignored.
-- @string p1 A file path
-- @string p2 A file path
-- @string ... more file paths
-- @treturn string the combined path
-- @usage
-- path.join("/first","second","third")   -- "/first/second/third"
-- path.join("first","second/third")      -- "first/second/third"
-- path.join("/first","/second","third")  -- "/second/third"
function path.join(p1,p2,...)
    assert_string(1,p1)
    assert_string(2,p2)
    if select('#',...) > 0 then
        local p = path.join(p1,p2)
        local args = {...}
        for i = 1,#args do
            assert_string(i,args[i])
            p = path.join(p,args[i])
        end
        return p
    end
    if path.isabs(p2) then return p2 end
    local endc = at(p1,#p1)
    if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
        p1 = p1..path.sep
    end
    return p1..p2
end

--- normalize the case of a pathname. On Unix, this returns the path unchanged,
-- for Windows it converts;
--
-- * the path to lowercase
-- * forward slashes to backward slashes
-- @string P A file path
-- @usage path.normcase("/Some/Path/File.txt")
-- -- Windows: "\some\path\file.txt"
-- -- Others : "/Some/Path/File.txt"
function path.normcase(P)
    assert_string(1,P)
    if path.is_windows then
        return P:gsub('/','\\'):lower()
    else
        return P
    end
end

--- normalize a path name.
-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`.
--
-- An empty path results in '.'.
-- @string P a file path
function path.normpath(P)
    assert_string(1,P)
    -- Split path into anchor and relative path.
    local anchor = ''
    if path.is_windows then
        if P:match '^\\\\' then -- UNC
            anchor = '\\\\'
            P = P:sub(3)
        elseif seps[at(P, 1)] then
            anchor = '\\'
            P = P:sub(2)
        elseif at(P, 2) == ':' then
            anchor = P:sub(1, 2)
            P = P:sub(3)
            if seps[at(P, 1)] then
                anchor = anchor..'\\'
                P = P:sub(2)
            end
        end
        P = P:gsub('/','\\')
    else
        -- According to POSIX, in path start '//' and '/' are distinct,
        -- but '///+' is equivalent to '/'.
        if P:match '^//' and at(P, 3) ~= '/' then
            anchor = '//'
            P = P:sub(3)
        elseif at(P, 1) == '/' then
            anchor = '/'
            P = P:match '^/*(.*)$'
        end
    end
    local parts = {}
    for part in P:gmatch('[^'..sep..']+') do
        if part == '..' then
            if #parts ~= 0 and parts[#parts] ~= '..' then
                remove(parts)
            else
                append(parts, part)
            end
        elseif part ~= '.' then
            append(parts, part)
        end
    end
    P = anchor..concat(parts, sep)
    if P == '' then P = '.' end
    return P
end

--- relative path from current directory or optional start point
-- @string P a path
-- @string[opt] start optional start point (default current directory)
function path.relpath (P,start)
    assert_string(1,P)
    if start then assert_string(2,start) end
    local split,min,append = utils.split, math.min, table.insert
    P = path.abspath(P,start)
    start = start or currentdir()
    local compare
    if path.is_windows then
        P = P:gsub("/","\\")
        start = start:gsub("/","\\")
        compare = function(v) return v:lower() end
    else
        compare = function(v) return v end
    end
    local startl, Pl = split(start,sep), split(P,sep)
    local n = min(#startl,#Pl)
    if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then
        return P
    end
    local k = n+1 -- default value if this loop doesn't bail out!
    for i = 1,n do
        if compare(startl[i]) ~= compare(Pl[i]) then
            k = i
            break
        end
    end
    local rell = {}
    for i = 1, #startl-k+1 do rell[i] = '..' end
    if k <= #Pl then
        for i = k,#Pl do append(rell,Pl[i]) end
    end
    return table.concat(rell,sep)
end


--- Replace a starting '~' with the user's home directory.
-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
-- @string P A file path
function path.expanduser(P)
    assert_string(1,P)
    if at(P,1) == '~' then
        local home = getenv('HOME')
        if not home then -- has to be Windows
            home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
        end
        return home..sub(P,2)
    else
        return P
    end
end


---Return a suitable full path to a new temporary file name.
-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows)
function path.tmpname ()
    local res = tmpnam()
    -- On Windows if Lua is compiled using MSVC14 os.tmpname
    -- already returns an absolute path within TEMP env variable directory,
    -- no need to prepend it.
    if path.is_windows and not res:find(':') then
        res = getenv('TEMP')..res
    end
    return res
end

--- return the largest common prefix path of two paths.
-- @string path1 a file path
-- @string path2 a file path
-- @return the common prefix (Windows: separators will be normalized, casing will be original)
function path.common_prefix (path1,path2)
    assert_string(1,path1)
    assert_string(2,path2)
    -- get them in order!
    if #path1 > #path2 then path2,path1 = path1,path2 end
    local compare
    if path.is_windows then
        path1 = path1:gsub("/", "\\")
        path2 = path2:gsub("/", "\\")
        compare = function(v) return v:lower() end
    else
        compare = function(v) return v end
    end
    for i = 1,#path1 do
        if compare(at(path1,i)) ~= compare(at(path2,i)) then
            local cp = path1:sub(1,i-1)
            if at(path1,i-1) ~= sep then
                cp = path.dirname(cp)
            end
            return cp
        end
    end
    if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
    return path1
    --return ''
end

--- return the full path where a particular Lua module would be found.
-- Both package.path and package.cpath is searched, so the result may
-- either be a Lua file or a shared library.
-- @string mod name of the module
-- @return on success: path of module, lua or binary
-- @return on error: nil, error string listing paths tried
function path.package_path(mod)
    assert_string(1,mod)
    local res, err1, err2
    res, err1 = package.searchpath(mod,package.path)
    if res then return res,true end
    res, err2 = package.searchpath(mod,package.cpath)
    if res then return res,false end
    return raise ('cannot find module on path\n' .. err1 .. "\n" .. err2)
end


---- finis -----
return path
