include system/inclrtl
import std/private/since

import strutils, pathnorm
import std/oserrors

import oscommon
export ReadDirEffect, WriteDirEffect

when defined(nimPreviewSlimSystem):
  import std/[syncio, assertions, widestrs]

## .. importdoc:: osappdirs.nim, osdirs.nim, osseps.nim, os.nim

const weirdTarget = defined(nimscript) or defined(js)

when weirdTarget:
  discard
elif defined(windows):
  import winlean
elif defined(posix):
  import posix, system/ansi_c
else:
  {.error: "OS module not ported to your operating system!".}

when weirdTarget:
  {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
else:
  {.pragma: noWeirdTarget.}

when defined(nimscript):
  # for procs already defined in scriptconfig.nim
  template noNimJs(body): untyped = discard
elif defined(js):
  {.pragma: noNimJs, error: "this proc is not available on the js target".}
else:
  {.pragma: noNimJs.}


proc normalizePathAux(path: var string){.inline, raises: [], noSideEffect.}


import std/private/osseps
export osseps

proc absolutePathInternal(path: string): string {.gcsafe.}

proc normalizePathEnd*(path: var string, trailingSep = false) =
  ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on
  ## ``trailingSep``, and taking care of edge cases: it preservers whether
  ## a path is absolute or relative, and makes sure trailing sep is `DirSep`,
  ## not `AltSep`. Trailing `/.` are compressed, see examples.
  if path.len == 0: return
  var i = path.len
  while i >= 1:
    if path[i-1] in {DirSep, AltSep}: dec(i)
    elif path[i-1] == '.' and i >= 2 and path[i-2] in {DirSep, AltSep}: dec(i)
    else: break
  if trailingSep:
    # foo// => foo
    path.setLen(i)
    # foo => foo/
    path.add DirSep
  elif i > 0:
    # foo// => foo
    path.setLen(i)
  else:
    # // => / (empty case was already taken care of)
    path = $DirSep

proc normalizePathEnd*(path: string, trailingSep = false): string =
  ## outplace overload
  runnableExamples:
    when defined(posix):
      assert normalizePathEnd("/lib//.//", trailingSep = true) == "/lib/"
      assert normalizePathEnd("lib/./.", trailingSep = false) == "lib"
      assert normalizePathEnd(".//./.", trailingSep = false) == "."
      assert normalizePathEnd("", trailingSep = true) == "" # not / !
      assert normalizePathEnd("/", trailingSep = false) == "/" # not "" !
  result = path
  result.normalizePathEnd(trailingSep)

template endsWith(a: string, b: set[char]): bool =
  a.len > 0 and a[^1] in b

proc joinPathImpl(result: var string, state: var int, tail: string) =
  let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and result.endsWith({DirSep, AltSep})
  normalizePathEnd(result, trailingSep=false)
  addNormalizePath(tail, result, state, DirSep)
  normalizePathEnd(result, trailingSep=trailingSep)

proc joinPath*(head, tail: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Joins two directory names to one.
  ##
  ## returns normalized path concatenation of `head` and `tail`, preserving
  ## whether or not `tail` has a trailing slash (or, if tail if empty, whether
  ## head has one).
  ##
  ## See also:
  ## * `joinPath(parts: varargs[string]) proc`_
  ## * `/ proc`_
  ## * `splitPath proc`_
  ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
  ## * `uri./ proc <uri.html#/,Uri,string>`_
  runnableExamples:
    when defined(posix):
      assert joinPath("usr", "lib") == "usr/lib"
      assert joinPath("usr", "lib/") == "usr/lib/"
      assert joinPath("usr", "") == "usr"
      assert joinPath("usr/", "") == "usr/"
      assert joinPath("", "") == ""
      assert joinPath("", "lib") == "lib"
      assert joinPath("", "/lib") == "/lib"
      assert joinPath("usr/", "/lib") == "usr/lib"
      assert joinPath("usr/lib", "../bin") == "usr/bin"

  result = newStringOfCap(head.len + tail.len)
  var state = 0
  joinPathImpl(result, state, head)
  joinPathImpl(result, state, tail)
  when false:
    if len(head) == 0:
      result = tail
    elif head[len(head)-1] in {DirSep, AltSep}:
      if tail.len > 0 and tail[0] in {DirSep, AltSep}:
        result = head & substr(tail, 1)
      else:
        result = head & tail
    else:
      if tail.len > 0 and tail[0] in {DirSep, AltSep}:
        result = head & tail
      else:
        result = head & DirSep & tail

proc joinPath*(parts: varargs[string]): string {.noSideEffect,
  rtl, extern: "nos$1OpenArray".} =
  ## The same as `joinPath(head, tail) proc`_,
  ## but works with any number of directory parts.
  ##
  ## You need to pass at least one element or the proc
  ## will assert in debug builds and crash on release builds.
  ##
  ## See also:
  ## * `joinPath(head, tail) proc`_
  ## * `/ proc`_
  ## * `/../ proc`_
  ## * `splitPath proc`_
  runnableExamples:
    when defined(posix):
      assert joinPath("a") == "a"
      assert joinPath("a", "b", "c") == "a/b/c"
      assert joinPath("usr/lib", "../../var", "log") == "var/log"

  var estimatedLen = 0
  for p in parts: estimatedLen += p.len
  result = newStringOfCap(estimatedLen)
  var state = 0
  for i in 0..high(parts):
    joinPathImpl(result, state, parts[i])

proc `/`*(head, tail: string): string {.noSideEffect, inline.} =
  ## The same as `joinPath(head, tail) proc`_.
  ##
  ## See also:
  ## * `/../ proc`_
  ## * `joinPath(head, tail) proc`_
  ## * `joinPath(parts: varargs[string]) proc`_
  ## * `splitPath proc`_
  ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
  ## * `uri./ proc <uri.html#/,Uri,string>`_
  runnableExamples:
    when defined(posix):
      assert "usr" / "" == "usr"
      assert "" / "lib" == "lib"
      assert "" / "/lib" == "/lib"
      assert "usr/" / "/lib/" == "usr/lib/"
      assert "usr" / "lib" / "../bin" == "usr/bin"

  result = joinPath(head, tail)

when doslikeFileSystem:
  import std/private/ntpath

proc splitPath*(path: string): tuple[head, tail: string] {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Splits a directory into `(head, tail)` tuple, so that
  ## ``head / tail == path`` (except for edge cases like "/usr").
  ##
  ## See also:
  ## * `joinPath(head, tail) proc`_
  ## * `joinPath(parts: varargs[string]) proc`_
  ## * `/ proc`_
  ## * `/../ proc`_
  ## * `relativePath proc`_
  runnableExamples:
    assert splitPath("usr/local/bin") == ("usr/local", "bin")
    assert splitPath("usr/local/bin/") == ("usr/local/bin", "")
    assert splitPath("/bin/") == ("/bin", "")
    when (NimMajor, NimMinor) <= (1, 0):
      assert splitPath("/bin") == ("", "bin")
    else:
      assert splitPath("/bin") == ("/", "bin")
    assert splitPath("bin") == ("", "bin")
    assert splitPath("") == ("", "")

  when doslikeFileSystem:
    let (drive, splitpath) = splitDrive(path)
    let stop = drive.len
  else:
    const stop = 0

  var sepPos = -1
  for i in countdown(len(path)-1, stop):
    if path[i] in {DirSep, AltSep}:
      sepPos = i
      break
  if sepPos >= 0:
    result.head = substr(path, 0,
      when (NimMajor, NimMinor) <= (1, 0):
        sepPos-1
      else:
        if likely(sepPos >= 1): sepPos-1 else: 0
    )
    result.tail = substr(path, sepPos+1)
  else:
    when doslikeFileSystem:
      result.head = drive
      result.tail = splitpath
    else:
      result.head = ""
      result.tail = path

proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1", raises: [].} =
  ## Checks whether a given `path` is absolute.
  ##
  ## On Windows, network paths are considered absolute too.
  runnableExamples:
    assert not "".isAbsolute
    assert not ".".isAbsolute
    when defined(posix):
      assert "/".isAbsolute
      assert not "a/".isAbsolute
      assert "/a/".isAbsolute

  if len(path) == 0: return false

  when doslikeFileSystem:
    var len = len(path)
    result = (path[0] in {'/', '\\'}) or
              (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
  elif defined(macos):
    # according to https://perldoc.perl.org/File/Spec/Mac.html `:a` is a relative path
    result = path[0] != ':'
  elif defined(RISCOS):
    result = path[0] == '$'
  elif defined(posix) or defined(js):
    # `or defined(js)` wouldn't be needed pending https://github.com/nim-lang/Nim/issues/13469
    # This works around the problem for posix, but Windows is still broken with nim js -d:nodejs
    result = path[0] == '/'
  else:
    doAssert false # if ever hits here, adapt as needed

when FileSystemCaseSensitive:
  template `!=?`(a, b: char): bool = a != b
else:
  template `!=?`(a, b: char): bool = toLowerAscii(a) != toLowerAscii(b)

when doslikeFileSystem:
  proc isAbsFromCurrentDrive(path: string): bool {.noSideEffect, raises: [].} =
    ## An absolute path from the root of the current drive (e.g. "\foo")
    path.len > 0 and
    (path[0] == AltSep or
     (path[0] == DirSep and
      (path.len == 1 or path[1] notin {DirSep, AltSep, ':'})))

  proc sameRoot(path1, path2: string): bool {.noSideEffect, raises: [].} =
    ## Return true if path1 and path2 have a same root.
    ##
    ## Detail of Windows path formats:
    ## https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats

    assert(isAbsolute(path1))
    assert(isAbsolute(path2))

    if isAbsFromCurrentDrive(path1) and isAbsFromCurrentDrive(path2):
      result = true
    elif cmpIgnoreCase(splitDrive(path1).drive, splitDrive(path2).drive) == 0:
      result = true
    else:
      result = false

proc relativePath*(path, base: string, sep = DirSep): string {.
  rtl, extern: "nos$1".} =
  ## Converts `path` to a path relative to `base`.
  ##
  ## The `sep` (default: DirSep_) is used for the path normalizations,
  ## this can be useful to ensure the relative path only contains `'/'`
  ## so that it can be used for URL constructions.
  ##
  ## On Windows, if a root of `path` and a root of `base` are different,
  ## returns `path` as is because it is impossible to make a relative path.
  ## That means an absolute path can be returned.
  ##
  ## See also:
  ## * `splitPath proc`_
  ## * `parentDir proc`_
  ## * `tailDir proc`_
  runnableExamples:
    assert relativePath("/Users/me/bar/z.nim", "/Users/other/bad", '/') == "../../me/bar/z.nim"
    assert relativePath("/Users/me/bar/z.nim", "/Users/other", '/') == "../me/bar/z.nim"
    when not doslikeFileSystem: # On Windows, UNC-paths start with `//`
      assert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
    assert relativePath("/Users/me/bar/z.nim", "/Users/me", '/') == "bar/z.nim"
    assert relativePath("", "/users/moo", '/') == ""
    assert relativePath("foo", ".", '/') == "foo"
    assert relativePath("foo", "foo", '/') == "."

  if path.len == 0: return ""
  var base = if base == ".": "" else: base
  var path = path
  path.normalizePathAux
  base.normalizePathAux
  let a1 = isAbsolute(path)
  let a2 = isAbsolute(base)
  if a1 and not a2:
    base = absolutePathInternal(base)
  elif a2 and not a1:
    path = absolutePathInternal(path)

  when doslikeFileSystem:
    if isAbsolute(path) and isAbsolute(base):
      if not sameRoot(path, base):
        return path

  var f = default PathIter
  var b = default PathIter
  var ff = (0, -1)
  var bb = (0, -1) # (int, int)
  result = newStringOfCap(path.len)
  # skip the common prefix:
  while f.hasNext(path) and b.hasNext(base):
    ff = next(f, path)
    bb = next(b, base)
    let diff = ff[1] - ff[0]
    if diff != bb[1] - bb[0]: break
    var same = true
    for i in 0..diff:
      if path[i + ff[0]] !=? base[i + bb[0]]:
        same = false
        break
    if not same: break
    ff = (0, -1)
    bb = (0, -1)
  #  for i in 0..diff:
  #    result.add base[i + bb[0]]

  # /foo/bar/xxx/ -- base
  # /foo/bar/baz  -- path path
  #   ../baz
  # every directory that is in 'base', needs to add '..'
  while true:
    if bb[1] >= bb[0]:
      if result.len > 0 and result[^1] != sep:
        result.add sep
      result.add ".."
    if not b.hasNext(base): break
    bb = b.next(base)

  # add the rest of 'path':
  while true:
    if ff[1] >= ff[0]:
      if result.len > 0 and result[^1] != sep:
        result.add sep
      for i in 0..ff[1] - ff[0]:
        result.add path[i + ff[0]]
    if not f.hasNext(path): break
    ff = f.next(path)

  when not defined(nimOldRelativePathBehavior):
    if result.len == 0: result.add "."

proc isRelativeTo*(path: string, base: string): bool {.since: (1, 1).} =
  ## Returns true if `path` is relative to `base`.
  runnableExamples:
    doAssert isRelativeTo("./foo//bar", "foo")
    doAssert isRelativeTo("foo/bar", ".")
    doAssert isRelativeTo("/foo/bar.nim", "/foo/bar.nim")
    doAssert not isRelativeTo("foo/bar.nims", "foo/bar.nim")
  let path = path.normalizePath
  let base = base.normalizePath
  let ret = relativePath(path, base)
  result = path.len > 0 and not ret.startsWith ".."

proc parentDirPos(path: string): int =
  var q = 1
  if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
  for i in countdown(len(path)-q, 0):
    if path[i] in {DirSep, AltSep}: return i
  result = -1

proc parentDir*(path: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Returns the parent directory of `path`.
  ##
  ## This is similar to ``splitPath(path).head`` when ``path`` doesn't end
  ## in a dir separator, but also takes care of path normalizations.
  ## The remainder can be obtained with `lastPathPart(path) proc`_.
  ##
  ## See also:
  ## * `relativePath proc`_
  ## * `splitPath proc`_
  ## * `tailDir proc`_
  ## * `parentDirs iterator`_
  runnableExamples:
    assert parentDir("") == ""
    when defined(posix):
      assert parentDir("/usr/local/bin") == "/usr/local"
      assert parentDir("foo/bar//") == "foo"
      assert parentDir("//foo//bar//.") == "/foo"
      assert parentDir("./foo") == "."
      assert parentDir("/./foo//./") == "/"
      assert parentDir("a//./") == "."
      assert parentDir("a/b/c/..") == "a"
  result = pathnorm.normalizePath(path)
  when doslikeFileSystem:
    let (drive, splitpath) = splitDrive(result)
    result = splitpath
  var sepPos = parentDirPos(result)
  if sepPos >= 0:
    result = substr(result, 0, sepPos)
    normalizePathEnd(result)
  elif result == ".." or result == "." or result.len == 0 or result[^1] in {DirSep, AltSep}:
    # `.` => `..` and .. => `../..`(etc) would be a sensible alternative
    # `/` => `/` (as done with splitFile) would be a sensible alternative
    result = ""
  else:
    result = "."
  when doslikeFileSystem:
    if result.len == 0:
      discard
    elif drive.len > 0 and result.len == 1 and result[0] in {DirSep, AltSep}:
      result = drive
    else:
      result = drive & result

proc tailDir*(path: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Returns the tail part of `path`.
  ##
  ## See also:
  ## * `relativePath proc`_
  ## * `splitPath proc`_
  ## * `parentDir proc`_
  runnableExamples:
    assert tailDir("/bin") == "bin"
    assert tailDir("bin") == ""
    assert tailDir("bin/") == ""
    assert tailDir("/usr/local/bin") == "usr/local/bin"
    assert tailDir("//usr//local//bin//") == "usr//local//bin//"
    assert tailDir("./usr/local/bin") == "usr/local/bin"
    assert tailDir("usr/local/bin") == "local/bin"

  var i = 0
  when doslikeFileSystem:
    let (drive, splitpath) = path.splitDrive
    if drive != "":
      return splitpath.strip(chars = {DirSep, AltSep}, trailing = false)
  while i < len(path):
    if path[i] in {DirSep, AltSep}:
      while i < len(path) and path[i] in {DirSep, AltSep}: inc i
      return substr(path, i)
    inc i
  result = ""

proc isRootDir*(path: string): bool {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Checks whether a given `path` is a root directory.
  runnableExamples:
    assert isRootDir("")
    assert isRootDir(".")
    assert isRootDir("/")
    assert isRootDir("a")
    assert not isRootDir("/a")
    assert not isRootDir("a/b/c")

  when doslikeFileSystem:
    if splitDrive(path).path == "":
      return true
  result = parentDirPos(path) < 0

iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
  ## Walks over all parent directories of a given `path`.
  ##
  ## If `fromRoot` is true (default: false), the traversal will start from
  ## the file system root directory.
  ## If `inclusive` is true (default), the original argument will be included
  ## in the traversal.
  ##
  ## Relative paths won't be expanded by this iterator. Instead, it will traverse
  ## only the directories appearing in the relative path.
  ##
  ## See also:
  ## * `parentDir proc`_
  ##
  runnableExamples:
    let g = "a/b/c"

    for p in g.parentDirs:
      echo p
      # a/b/c
      # a/b
      # a

    for p in g.parentDirs(fromRoot=true):
      echo p
      # a/
      # a/b/
      # a/b/c

    for p in g.parentDirs(inclusive=false):
      echo p
      # a/b
      # a

  if not fromRoot:
    var current = path
    if inclusive: yield path
    while true:
      if current.isRootDir: break
      current = current.parentDir
      yield current
  else:
    when doslikeFileSystem:
      let start = path.splitDrive.drive.len
    else:
      const start = 0
    for i in countup(start, path.len - 2): # ignore the last /
      # deal with non-normalized paths such as /foo//bar//baz
      if path[i] in {DirSep, AltSep} and
          (i == 0 or path[i-1] notin {DirSep, AltSep}):
        yield path.substr(0, i)

    if inclusive: yield path

proc `/../`*(head, tail: string): string {.noSideEffect.} =
  ## The same as ``parentDir(head) / tail``, unless there is no parent
  ## directory. Then ``head / tail`` is performed instead.
  ##
  ## See also:
  ## * `/ proc`_
  ## * `parentDir proc`_
  runnableExamples:
    when defined(posix):
      assert "a/b/c" /../ "d/e" == "a/b/d/e"
      assert "a" /../ "d/e" == "a/d/e"

  when doslikeFileSystem:
    let (drive, head) = splitDrive(head)
  let sepPos = parentDirPos(head)
  if sepPos >= 0:
    result = substr(head, 0, sepPos-1) / tail
  else:
    result = head / tail
  when doslikeFileSystem:
    result = drive / result

proc normExt(ext: string): string =
  if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
  else: result = ExtSep & ext

proc searchExtPos*(path: string): int =
  ## Returns index of the `'.'` char in `path` if it signifies the beginning
  ## of the file extension. Returns -1 otherwise.
  ##
  ## See also:
  ## * `splitFile proc`_
  ## * `extractFilename proc`_
  ## * `lastPathPart proc`_
  ## * `changeFileExt proc`_
  ## * `addFileExt proc`_
  runnableExamples:
    assert searchExtPos("a/b/c") == -1
    assert searchExtPos("c.nim") == 1
    assert searchExtPos("a/b/c.nim") == 5
    assert searchExtPos("a.b.c.nim") == 5

  # BUGFIX: do not search until 0! .DS_Store is no file extension!
  result = -1
  for i in countdown(len(path)-1, 1):
    if path[i] == ExtSep:
      result = i
      break
    elif path[i] in {DirSep, AltSep}:
      break # do not skip over path

proc splitFile*(path: string): tuple[dir, name, ext: string] {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Splits a filename into `(dir, name, extension)` tuple.
  ##
  ## `dir` does not end in DirSep_ unless it's `/`.
  ## `extension` includes the leading dot.
  ##
  ## If `path` has no extension, `ext` is the empty string.
  ## If `path` has no directory component, `dir` is the empty string.
  ## If `path` has no filename component, `name` and `ext` are empty strings.
  ##
  ## See also:
  ## * `searchExtPos proc`_
  ## * `extractFilename proc`_
  ## * `lastPathPart proc`_
  ## * `changeFileExt proc`_
  ## * `addFileExt proc`_
  runnableExamples:
    var (dir, name, ext) = splitFile("usr/local/nimc.html")
    assert dir == "usr/local"
    assert name == "nimc"
    assert ext == ".html"
    (dir, name, ext) = splitFile("/usr/local/os")
    assert dir == "/usr/local"
    assert name == "os"
    assert ext == ""
    (dir, name, ext) = splitFile("/usr/local/")
    assert dir == "/usr/local"
    assert name == ""
    assert ext == ""
    (dir, name, ext) = splitFile("/tmp.txt")
    assert dir == "/"
    assert name == "tmp"
    assert ext == ".txt"

  var namePos = 0
  var dotPos = 0
  when doslikeFileSystem:
    let (drive, _) = splitDrive(path)
    let stop = len(drive)
    result.dir = drive
  else:
    const stop = 0
  for i in countdown(len(path) - 1, stop):
    if path[i] in {DirSep, AltSep} or i == 0:
      if path[i] in {DirSep, AltSep}:
        result.dir = substr(path, 0, if likely(i >= 1): i - 1 else: 0)
        namePos = i + 1
      if dotPos > i:
        result.name = substr(path, namePos, dotPos - 1)
        result.ext = substr(path, dotPos)
      else:
        result.name = substr(path, namePos)
      break
    elif path[i] == ExtSep and i > 0 and i < len(path) - 1 and
         path[i - 1] notin {DirSep, AltSep} and
         path[i + 1] != ExtSep and dotPos == 0:
      dotPos = i

proc extractFilename*(path: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Extracts the filename of a given `path`.
  ##
  ## This is the same as ``name & ext`` from `splitFile(path) proc`_.
  ##
  ## See also:
  ## * `searchExtPos proc`_
  ## * `splitFile proc`_
  ## * `lastPathPart proc`_
  ## * `changeFileExt proc`_
  ## * `addFileExt proc`_
  runnableExamples:
    assert extractFilename("foo/bar/") == ""
    assert extractFilename("foo/bar") == "bar"
    assert extractFilename("foo/bar.baz") == "bar.baz"

  if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
    result = ""
  else:
    result = splitPath(path).tail

proc lastPathPart*(path: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Like `extractFilename proc`_, but ignores
  ## trailing dir separator; aka: `baseName`:idx: in some other languages.
  ##
  ## See also:
  ## * `searchExtPos proc`_
  ## * `splitFile proc`_
  ## * `extractFilename proc`_
  ## * `changeFileExt proc`_
  ## * `addFileExt proc`_
  runnableExamples:
    assert lastPathPart("foo/bar/") == "bar"
    assert lastPathPart("foo/bar") == "bar"

  let path = path.normalizePathEnd(trailingSep = false)
  result = extractFilename(path)

proc changeFileExt*(filename, ext: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Changes the file extension to `ext`.
  ##
  ## If the `filename` has no extension, `ext` will be added.
  ## If `ext` == "" then any extension is removed.
  ##
  ## `Ext` should be given without the leading `'.'`, because some
  ## filesystems may use a different character. (Although I know
  ## of none such beast.)
  ##
  ## See also:
  ## * `searchExtPos proc`_
  ## * `splitFile proc`_
  ## * `extractFilename proc`_
  ## * `lastPathPart proc`_
  ## * `addFileExt proc`_
  runnableExamples:
    assert changeFileExt("foo.bar", "baz") == "foo.baz"
    assert changeFileExt("foo.bar", "") == "foo"
    assert changeFileExt("foo", "baz") == "foo.baz"

  var extPos = searchExtPos(filename)
  if extPos < 0: result = filename & normExt(ext)
  else: result = substr(filename, 0, extPos-1) & normExt(ext)

proc addFileExt*(filename, ext: string): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Adds the file extension `ext` to `filename`, unless
  ## `filename` already has an extension.
  ##
  ## `Ext` should be given without the leading `'.'`, because some
  ## filesystems may use a different character.
  ## (Although I know of none such beast.)
  ##
  ## See also:
  ## * `searchExtPos proc`_
  ## * `splitFile proc`_
  ## * `extractFilename proc`_
  ## * `lastPathPart proc`_
  ## * `changeFileExt proc`_
  runnableExamples:
    assert addFileExt("foo.bar", "baz") == "foo.bar"
    assert addFileExt("foo.bar", "") == "foo.bar"
    assert addFileExt("foo", "baz") == "foo.baz"

  var extPos = searchExtPos(filename)
  if extPos < 0: result = filename & normExt(ext)
  else: result = filename

proc cmpPaths*(pathA, pathB: string): int {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Compares two paths.
  ##
  ## On a case-sensitive filesystem this is done
  ## case-sensitively otherwise case-insensitively. Returns:
  ##
  ## | 0 if pathA == pathB
  ## | < 0 if pathA < pathB
  ## | > 0 if pathA > pathB
  runnableExamples:
    when defined(macosx):
      assert cmpPaths("foo", "Foo") == 0
    elif defined(posix):
      assert cmpPaths("foo", "Foo") > 0

  let a = normalizePath(pathA)
  let b = normalizePath(pathB)
  if FileSystemCaseSensitive:
    result = cmp(a, b)
  else:
    when defined(nimscript):
      result = cmpic(a, b)
    elif defined(nimdoc): discard
    else:
      result = cmpIgnoreCase(a, b)

proc unixToNativePath*(path: string, drive=""): string {.
  noSideEffect, rtl, extern: "nos$1".} =
  ## Converts an UNIX-like path to a native one.
  ##
  ## On an UNIX system this does nothing. Else it converts
  ## `'/'`, `'.'`, `'..'` to the appropriate things.
  ##
  ## On systems with a concept of "drives", `drive` is used to determine
  ## which drive label to use during absolute path conversion.
  ## `drive` defaults to the drive of the current working directory, and is
  ## ignored on systems that do not have a concept of "drives".
  when defined(unix):
    result = path
  else:
    if path.len == 0: return ""

    var start: int
    if path[0] == '/':
      # an absolute path
      when doslikeFileSystem:
        if drive != "":
          result = drive & ":" & DirSep
        else:
          result = $DirSep
      elif defined(macos):
        result = "" # must not start with ':'
      else:
        result = $DirSep
      start = 1
    elif path[0] == '.' and (path.len == 1 or path[1] == '/'):
      # current directory
      result = $CurDir
      start = when doslikeFileSystem: 1 else: 2
    else:
      result = ""
      start = 0

    var i = start
    while i < len(path): # ../../../ --> ::::
      if i+2 < path.len and path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
        # parent directory
        when defined(macos):
          if result[high(result)] == ':':
            add result, ':'
          else:
            add result, ParDir
        else:
          add result, ParDir & DirSep
        inc(i, 3)
      elif path[i] == '/':
        add result, DirSep
        inc(i)
      else:
        add result, path[i]
        inc(i)


when not defined(nimscript):
  proc getCurrentDir*(): string {.rtl, extern: "nos$1", tags: [].} =
    ## Returns the `current working directory`:idx: i.e. where the built
    ## binary is run.
    ##
    ## So the path returned by this proc is determined at run time.
    ##
    ## See also:
    ## * `getHomeDir proc`_
    ## * `getConfigDir proc`_
    ## * `getTempDir proc`_
    ## * `setCurrentDir proc`_
    ## * `currentSourcePath template <system.html#currentSourcePath.t>`_
    ## * `getProjectPath proc <macros.html#getProjectPath>`_
    when defined(nodejs):
      var ret: cstring
      {.emit: "`ret` = process.cwd();".}
      return $ret
    elif defined(js):
      doAssert false, "use -d:nodejs to have `getCurrentDir` defined"
    elif defined(windows):
      var bufsize = MAX_PATH.int32
      var res = newWideCString("", bufsize)
      while true:
        var L = getCurrentDirectoryW(bufsize, res)
        if L == 0'i32:
          raiseOSError(osLastError())
        elif L > bufsize:
          res = newWideCString("", L)
          bufsize = L
        else:
          result = res$L
          break
    else:
      var bufsize = 1024 # should be enough
      result = newString(bufsize)
      while true:
        if getcwd(result.cstring, bufsize) != nil:
          setLen(result, c_strlen(result.cstring))
          break
        else:
          let err = osLastError()
          if err.int32 == ERANGE:
            bufsize = bufsize shl 1
            doAssert(bufsize >= 0)
            result = newString(bufsize)
          else:
            raiseOSError(osLastError())

proc absolutePath*(path: string, root = getCurrentDir()): string =
  ## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
  ## default: current directory).
  ## If `path` is absolute, return it, ignoring `root`.
  ##
  ## See also:
  ## * `normalizedPath proc`_
  ## * `normalizePath proc`_
  runnableExamples:
    assert absolutePath("a") == getCurrentDir() / "a"

  if isAbsolute(path): path
  else:
    if not root.isAbsolute:
      raise newException(ValueError, "The specified root is not absolute: " & root)
    joinPath(root, path)

proc absolutePathInternal(path: string): string =
  absolutePath(path, getCurrentDir())


proc normalizePath*(path: var string) {.rtl, extern: "nos$1", tags: [].} =
  ## Normalize a path.
  ##
  ## Consecutive directory separators are collapsed, including an initial double slash.
  ##
  ## On relative paths, double dot (`..`) sequences are collapsed if possible.
  ## On absolute paths they are always collapsed.
  ##
  ## .. warning:: URL-encoded and Unicode attempts at directory traversal are not detected.
  ##   Triple dot is not handled.
  ##
  ## See also:
  ## * `absolutePath proc`_
  ## * `normalizedPath proc`_ for outplace version
  ## * `normalizeExe proc`_
  runnableExamples:
    when defined(posix):
      var a = "a///b//..//c///d"
      a.normalizePath()
      assert a == "a/c/d"

  path = pathnorm.normalizePath(path)
  when false:
    let isAbs = isAbsolute(path)
    var stack: seq[string] = @[]
    for p in split(path, {DirSep}):
      case p
      of "", ".":
        continue
      of "..":
        if stack.len == 0:
          if isAbs:
            discard  # collapse all double dots on absoluta paths
          else:
            stack.add(p)
        elif stack[^1] == "..":
          stack.add(p)
        else:
          discard stack.pop()
      else:
        stack.add(p)

    if isAbs:
      path = DirSep & join(stack, $DirSep)
    elif stack.len > 0:
      path = join(stack, $DirSep)
    else:
      path = "."

proc normalizePathAux(path: var string) = normalizePath(path)

proc normalizedPath*(path: string): string {.rtl, extern: "nos$1", tags: [].} =
  ## Returns a normalized path for the current OS.
  ##
  ## See also:
  ## * `absolutePath proc`_
  ## * `normalizePath proc`_ for the in-place version
  runnableExamples:
    when defined(posix):
      assert normalizedPath("a///b//..//c///d") == "a/c/d"
  result = pathnorm.normalizePath(path)

proc normalizeExe*(file: var string) {.since: (1, 3, 5).} =
  ## on posix, prepends `./` if `file` doesn't contain `/` and is not `"", ".", ".."`.
  runnableExamples:
    import std/sugar
    when defined(posix):
      doAssert "foo".dup(normalizeExe) == "./foo"
      doAssert "foo/../bar".dup(normalizeExe) == "foo/../bar"
    doAssert "".dup(normalizeExe) == ""
  when defined(posix):
    if file.len > 0 and DirSep notin file and file != "." and file != "..":
      file = "./" & file

proc sameFile*(path1, path2: string): bool {.rtl, extern: "nos$1",
  tags: [ReadDirEffect], noWeirdTarget.} =
  ## Returns true if both pathname arguments refer to the same physical
  ## file or directory.
  ##
  ## Raises `OSError` if any of the files does not
  ## exist or information about it can not be obtained.
  ##
  ## This proc will return true if given two alternative hard-linked or
  ## sym-linked paths to the same file or directory.
  ##
  ## See also:
  ## * `sameFileContent proc`_
  when defined(windows):
    var success = true
    var f1 = openHandle(path1)
    var f2 = openHandle(path2)

    var lastErr: OSErrorCode
    if f1 != INVALID_HANDLE_VALUE and f2 != INVALID_HANDLE_VALUE:
      var fi1, fi2: BY_HANDLE_FILE_INFORMATION

      if getFileInformationByHandle(f1, addr(fi1)) != 0 and
         getFileInformationByHandle(f2, addr(fi2)) != 0:
        result = fi1.dwVolumeSerialNumber == fi2.dwVolumeSerialNumber and
                 fi1.nFileIndexHigh == fi2.nFileIndexHigh and
                 fi1.nFileIndexLow == fi2.nFileIndexLow
      else:
        lastErr = osLastError()
        success = false
    else:
      lastErr = osLastError()
      success = false

    discard closeHandle(f1)
    discard closeHandle(f2)

    if not success: raiseOSError(lastErr, $(path1, path2))
  else:
    var a, b: Stat
    if stat(path1, a) < 0'i32 or stat(path2, b) < 0'i32:
      raiseOSError(osLastError(), $(path1, path2))
    else:
      result = a.st_dev == b.st_dev and a.st_ino == b.st_ino
