#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

# Included by the ``os`` module but a module in its own right for NimScript
# support.

when not declared(os):
  {.pragma: rtl.}
  import strutils

when defined(nimscript) or (defined(nimdoc) and not declared(os)):
  {.pragma: rtl.}
  {.push hint[ConvFromXtoItselfNotNeeded]:off.}

when not declared(getEnv) or defined(nimscript):
  type
    ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
                                              ## from an environment variable
    WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
                                              ## to an environment variable

    ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a write
                                              ## operation to the directory
                                              ## structure
    WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write
                                              ## operation to
                                              ## the directory structure

    OSErrorCode* = distinct int32 ## Specifies an OS Error Code.

  {.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
      FReadDir: ReadDirEffect,
      FWriteDir: WriteDirEffect,
      TOSErrorCode: OSErrorCode
  ].}
  const
    doslike = defined(windows) or defined(OS2) or defined(DOS)
      # DOS-like filesystem

  when defined(Nimdoc): # only for proper documentation:
    const
      CurDir* = '.'
        ## The constant string used by the operating system to refer to the
        ## current directory.
        ##
        ## For example: '.' for POSIX or ':' for the classic Macintosh.

      ParDir* = ".."
        ## The constant string used by the operating system to refer to the
        ## parent directory.
        ##
        ## For example: ".." for POSIX or "::" for the classic Macintosh.

      DirSep* = '/'
        ## The character used by the operating system to separate pathname
        ## components, for example, '/' for POSIX or ':' for the classic
        ## Macintosh.

      AltSep* = '/'
        ## An alternative character used by the operating system to separate
        ## pathname components, or the same as `DirSep` if only one separator
        ## character exists. This is set to '/' on Windows systems
        ## where `DirSep` is a backslash.

      PathSep* = ':'
        ## The character conventionally used by the operating system to separate
        ## search patch components (as in PATH), such as ':' for POSIX
        ## or ';' for Windows.

      FileSystemCaseSensitive* = true
        ## true if the file system is case sensitive, false otherwise. Used by
        ## `cmpPaths` to compare filenames properly.

      ExeExt* = ""
        ## The file extension of native executables. For example:
        ## "" for POSIX, "exe" on Windows.

      ScriptExt* = ""
        ## The file extension of a script file. For example: "" for POSIX,
        ## "bat" on Windows.

      DynlibFormat* = "lib$1.so"
        ## The format string to turn a filename into a `DLL`:idx: file (also
        ## called `shared object`:idx: on some operating systems).

  elif defined(macos):
    const
      CurDir* = ':'
      ParDir* = "::"
      DirSep* = ':'
      AltSep* = Dirsep
      PathSep* = ','
      FileSystemCaseSensitive* = false
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "$1.dylib"

    #  MacOS paths
    #  ===========
    #  MacOS directory separator is a colon ":" which is the only character not
    #  allowed in filenames.
    #
    #  A path containing no colon or which begins with a colon is a partial
    #  path.
    #  E.g. ":kalle:petter" ":kalle" "kalle"
    #
    #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
    #  When generating paths, one is safe if one ensures that all partial paths
    #  begin with a colon, and all full paths end with a colon.
    #  In full paths the first name (e g HD above) is the name of a mounted
    #  volume.
    #  These names are not unique, because, for instance, two diskettes with the
    #  same names could be inserted. This means that paths on MacOS are not
    #  waterproof. In case of equal names the first volume found will do.
    #  Two colons "::" are the relative path to the parent. Three is to the
    #  grandparent etc.
  elif doslike:
    const
      CurDir* = '.'
      ParDir* = ".."
      DirSep* = '\\' # seperator within paths
      AltSep* = '/'
      PathSep* = ';' # seperator between paths
      FileSystemCaseSensitive* = false
      ExeExt* = "exe"
      ScriptExt* = "bat"
      DynlibFormat* = "$1.dll"
  elif defined(PalmOS) or defined(MorphOS):
    const
      DirSep* = '/'
      AltSep* = Dirsep
      PathSep* = ';'
      ParDir* = ".."
      FileSystemCaseSensitive* = false
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "$1.prc"
  elif defined(RISCOS):
    const
      DirSep* = '.'
      AltSep* = '.'
      ParDir* = ".." # is this correct?
      PathSep* = ','
      FileSystemCaseSensitive* = true
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "lib$1.so"
  else: # UNIX-like operating system
    const
      CurDir* = '.'
      ParDir* = ".."
      DirSep* = '/'
      AltSep* = DirSep
      PathSep* = ':'
      FileSystemCaseSensitive* = true
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"

  const
    ExtSep* = '.'
      ## The character which separates the base filename from the extension;
      ## for example, the '.' in ``os.nim``.


  proc joinPath*(head, tail: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Joins two directory names to one.
    ##
    ## For example on Unix:
    ##
    ## .. code-block:: nim
    ##   joinPath("usr", "lib")
    ##
    ## results in:
    ##
    ## .. code-block:: nim
    ##   "usr/lib"
    ##
    ## If head is the empty string, tail is returned. If tail is the empty
    ## string, head is returned with a trailing path separator. If tail starts
    ## with a path separator it will be removed when concatenated to head. Other
    ## path separators not located on boundaries won't be modified. More
    ## examples on Unix:
    ##
    ## .. code-block:: nim
    ##   assert joinPath("usr", "") == "usr/"
    ##   assert joinPath("", "lib") == "lib"
    ##   assert joinPath("", "/lib") == "/lib"
    ##   assert joinPath("usr/", "/lib") == "usr/lib"
    if len(head) == 0:
      result = tail
    elif head[len(head)-1] in {DirSep, AltSep}:
      if tail[0] in {DirSep, AltSep}:
        result = head & substr(tail, 1)
      else:
        result = head & tail
    else:
      if 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)`, 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.
    result = parts[0]
    for i in 1..high(parts):
      result = joinPath(result, parts[i])

  proc `/` * (head, tail: string): string {.noSideEffect.} =
    ## The same as ``joinPath(head, tail)``
    ##
    ## Here are some examples for Unix:
    ##
    ## .. code-block:: nim
    ##   assert "usr" / "" == "usr/"
    ##   assert "" / "lib" == "lib"
    ##   assert "" / "/lib" == "/lib"
    ##   assert "usr/" / "/lib" == "usr/lib"
    return joinPath(head, tail)

  proc splitPath*(path: string): tuple[head, tail: string] {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Splits a directory into (head, tail), so that
    ## ``head / tail == path`` (except for edge cases like "/usr").
    ##
    ## Examples:
    ##
    ## .. code-block:: nim
    ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
    ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
    ##   splitPath("bin") -> ("", "bin")
    ##   splitPath("/bin") -> ("", "bin")
    ##   splitPath("") -> ("", "")
    var sepPos = -1
    for i in countdown(len(path)-1, 0):
      if path[i] in {DirSep, AltSep}:
        sepPos = i
        break
    if sepPos >= 0:
      result.head = substr(path, 0, sepPos-1)
      result.tail = substr(path, sepPos+1)
    else:
      result.head = ""
      result.tail = path

  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 often the same as the ``head`` result of ``splitPath``.
    ## If there is no parent, "" is returned.
    ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
    ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
    let sepPos = parentDirPos(path)
    if sepPos >= 0:
      result = substr(path, 0, sepPos-1)
    else:
      result = ""

  proc tailDir*(path: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Returns the tail part of `path`..
    ##
    ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
    ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
    ## | Example: ``tailDir("bin") == ""``.
    var q = 1
    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
    for i in 0..len(path)-q:
      if path[i] in {DirSep, AltSep}:
        return substr(path, i+1)
    result = ""

  proc isRootDir*(path: string): bool {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Checks whether a given `path` is a root directory
    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 set, the traversal will start from the file system root
    ## diretory. If `inclusive` is set, the original argument will be included
    ## in the traversal.
    ##
    ## Relative paths won't be expanded by this proc. Instead, it will traverse
    ## only the directories appearing in the relative path.
    if not fromRoot:
      var current = path
      if inclusive: yield path
      while true:
        if current.isRootDir: break
        current = current.parentDir
        yield current
    else:
      for i in countup(0, 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.
    let sepPos = parentDirPos(head)
    if sepPos >= 0:
      result = substr(head, 0, sepPos-1) / tail
    else:
      result = head / tail

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

  proc searchExtPos(s: string): int =
    # BUGFIX: do not search until 0! .DS_Store is no file extension!
    result = -1
    for i in countdown(len(s)-1, 1):
      if s[i] == ExtSep:
        result = i
        break
      elif s[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, filename, extension).
    ## `dir` does not end in `DirSep`.
    ## `extension` includes the leading dot.
    ##
    ## Example:
    ##
    ## .. code-block:: nim
    ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
    ##   assert dir == "usr/local"
    ##   assert name == "nimc"
    ##   assert ext == ".html"
    ##
    ## 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.
    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
      result = (path, "", "")
    else:
      var sepPos = -1
      var dotPos = path.len
      for i in countdown(len(path)-1, 0):
        if path[i] == ExtSep:
          if dotPos == path.len and i > 0 and
              path[i-1] notin {DirSep, AltSep}: dotPos = i
        elif path[i] in {DirSep, AltSep}:
          sepPos = i
          break
      result.dir = substr(path, 0, sepPos-1)
      result.name = substr(path, sepPos+1, dotPos-1)
      result.ext = substr(path, dotPos)

  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)``.
    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
      result = ""
    else:
      result = splitPath(path).tail


  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.)
    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.)
    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 iff pathA == pathB
    ## | < 0 iff pathA < pathB
    ## | > 0 iff pathA > pathB
    if FileSystemCaseSensitive:
      result = cmp(pathA, pathB)
    else:
      when defined(nimscript):
        result = cmpic(pathA, pathB)
      elif defined(nimdoc): discard
      else:
        result = cmpIgnoreCase(pathA, pathB)

  proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
    ## Checks whether a given `path` is absolute.
    ##
    ## On Windows, network paths are considered absolute too.
    when doslike:
      var len = len(path)
      result = (len > 0 and path[0] in {'/', '\\'}) or
               (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
    elif defined(macos):
      result = path.len > 0 and path[0] != ':'
    elif defined(RISCOS):
      result = path[0] == '$'
    elif defined(posix):
      result = path[0] == '/'

  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:
      var start: int
      if path[0] == '/':
        # an absolute path
        when doslike:
          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[1] == '/':
        # current directory
        result = $CurDir
        start = 2
      else:
        result = ""
        start = 0

      var i = start
      while i < len(path): # ../../../ --> ::::
        if 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 defined(nimdoc) and not declared(os):
  proc getEnv(x: string): string = discard
  proc existsFile(x: string): bool = discard

when declared(getEnv) or defined(nimscript):
  proc getHomeDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the home directory of the current user.
    ##
    ## This proc is wrapped by the expandTilde proc for the convenience of
    ## processing paths coming from user configuration files.
    when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
    else: return string(getEnv("HOME")) & "/"

  proc getConfigDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the config directory of the current user for applications.
    when defined(windows): return string(getEnv("APPDATA")) & "\\"
    else: return string(getEnv("HOME")) & "/.config/"

  proc getTempDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the temporary directory of the current user for applications to
    ## save temporary files in.
    when defined(windows): return string(getEnv("TEMP")) & "\\"
    else: return "/tmp/"

  proc expandTilde*(path: string): string {.
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Expands a path starting with ``~/`` to a full path.
    ##
    ## If `path` starts with the tilde character and is followed by `/` or `\\`
    ## this proc will return the reminder of the path appended to the result of
    ## the getHomeDir() proc, otherwise the input path will be returned without
    ## modification.
    ##
    ## The behaviour of this proc is the same on the Windows platform despite
    ## not having this convention. Example:
    ##
    ## .. code-block:: nim
    ##   let configFile = expandTilde("~" / "appname.cfg")
    ##   echo configFile
    ##   # --> C:\Users\amber\appname.cfg
    if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
      result = getHomeDir() / path.substr(2)
    else:
      result = path

  when not declared(split):
    iterator split(s: string, sep: char): string =
      var last = 0
      if len(s) > 0:
        while last <= len(s):
          var first = last
          while last < len(s) and s[last] != sep: inc(last)
          yield substr(s, first, last-1)
          inc(last)

  when not defined(windows) and declared(os):
    proc checkSymlink(path: string): bool =
      var rawInfo: Stat
      if lstat(path, rawInfo) < 0'i32: result = false
      else: result = S_ISLNK(rawInfo.st_mode)

  const
    ExeExts* = when defined(windows): ["exe", "cmd", "bat"] else: [""] ## \
      ## platform specific file extension for executables. On Windows
      ## ``["exe", "cmd", "bat"]``, on Posix ``[""]``.

  proc findExe*(exe: string, followSymlinks: bool = true;
                extensions=ExeExts): string {.
    tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
    ## Searches for `exe` in the current working directory and then
    ## in directories listed in the ``PATH`` environment variable.
    ## Returns "" if the `exe` cannot be found. `exe`
    ## is added the `ExeExts <#ExeExts>`_ file extensions if it has none.
    ## If the system supports symlinks it also resolves them until it
    ## meets the actual file. This behavior can be disabled if desired.
    for ext in extensions:
      result = addFileExt(exe, ext)
      if existsFile(result): return
    var path = string(getEnv("PATH"))
    for candidate in split(path, PathSep):
      when defined(windows):
        var x = (if candidate[0] == '"' and candidate[^1] == '"':
                  substr(candidate, 1, candidate.len-2) else: candidate) /
               exe
      else:
        var x = expandTilde(candidate) / exe
      for ext in extensions:
        var x = addFileExt(x, ext)
        if existsFile(x):
          when not defined(windows) and declared(os):
            while followSymlinks: # doubles as if here
              if x.checkSymlink:
                var r = newString(256)
                var len = readlink(x, r, 256)
                if len < 0:
                  raiseOSError(osLastError())
                if len > 256:
                  r = newString(len+1)
                  len = readlink(x, r, len)
                setLen(r, len)
                if isAbsolute(r):
                  x = r
                else:
                  x = parentDir(x) / r
              else:
                break
          return x
    result = ""

when defined(nimscript) or (defined(nimdoc) and not declared(os)):
  {.pop.} # hint[ConvFromXtoItselfNotNeeded]:off
