/**
 * Portable file operations.
 */
module system/io/file
imports 
  system/io/-
  collection/list/-
  term/string

/**
 * Stream and file representations
 */
signature
  constructors
    Stream    : ImplDep -> Stream

    Path      : String -> FileLoc

    stdin     : FileLoc
    stdout    : FileLoc
    stderr    : FileLoc

/**
 * Abstractions for streams
 */
strategies

  /**
   * Opens a Stream associated to a FileLoc
   *
   * @type (FileLoc, String) -> Stream
   */
  open-stream = ?(_, _);
    (  Fst; stdio-stream
    <+ (?Path(<id>), id); fopen
    <+ (is-string,   id); fopen
    )

  /**
   * @type FileLoc -> Stream
   */
  stdio-stream = ?stdin() ; stdin-stream
  stdio-stream = ?stdout(); stdout-stream
  stdio-stream = ?stderr(); stderr-stream

  /**
   * Returns the Stream for stdin, stdout and stderr
   *
   * @type _ -> Stream
   */
  stdin-stream  = prim("SSL_stdin_stream") ; !Stream(<id>)
  stdout-stream = prim("SSL_stdout_stream"); !Stream(<id>)
  stderr-stream = prim("SSL_stderr_stream"); !Stream(<id>)

/**
 * Stream operations
 */
strategies

  /**
   * Opens the file whose name is the string pointed to by 
   * pathname and returns a Stream associated with this file.
   * For documentation on the mode string, see the man page of
   * fopen.
   *
   * fails if the file couldn't be opened. 
   *
   * @type String * String -> Stream
   */
  fopen =
    ?(pathname, mode); prim("SSL_fopen", pathname, mode); !Stream(<id>)

  /**
   * Dissociates the named stream from its underlying file or set of functions.
   * If the stream was being used for output, any buffered data is written 
   * first, using fflush(3).
   *
   * @type Stream -> ()
   */
  fclose =
    ?Stream(stream); prim("SSL_fclose", stream)
      <+ ?stream-old; prim("SSL_fclose", stream-old) // legacy

  /**
   * Forces a write of all user-space buffered data for the given Stream
   *
   * @type Stream -> Stream
   */
  fflush =
    ?Stream(stream); prim("SSL_fflush", stream); !Stream(<id>)
    <+ ?stream-old; prim("SSL_fflush", stream-old); !Stream(<id>) // legacy

/**
 * Character output to stream
 */
strategies

  /**
   * Writes a String to a Stream
   * @type (String, Stream) -> Stream
   */
  fputs =
    ?(str, Stream(stream)); prim("SSL_fputs", str, stream); !Stream(<id>)

  /**
   * Writes a String to stdout with a trailing newline and returns stdout.
   *
   * @type String -> Stream
   */
  puts =
    ?str; prim("SSL_puts", str); !Stream(<id>)

  /**
   * Writes a Char to a Stream
   *
   * @type (Char, Stream) -> Stream
   */
  fputc =
    ?(c, Stream(stream)); prim("SSL_fputc", c, stream); !Stream(<id>)

/**
 * Character input from stream
 */
strategies

  /**
   * fgetc() reads the next character from the stream and returns the 
   * ASCII code of the character as an Int. Fails on end of file or error.
   *
   * @type Stream -> Char
   */
  fgetc = 
    ?Stream(stream); prim("SSL_fgetc", stream)

/**
 * tmpnam
 */
strategies

  tmpnam =
    prim("SSL_tmpnam")

/**
 * Components of a pathname
 */
strategies

  /**
   * Returns directory portion of pathname in a POSIX compatible way.
   *
   * @inc   dirname
   * @type  String -> String
   */
  dirname =
    string-as-chars(
      /* (1) Strip trailing slashes */
      try(split-init-last; ?(<id>, '/'))
    ; /* (2) If string consists entirely of slash characters, string shall be
             set to a single slash character.  In this case, skip steps (3)
             through (8). */
    ( ?[]
      < !['/']
      + ( /* (3) If there are any trailing slash characters in string, they
                 shall be removed. */
          init
          /* (4) If there are no slash characters remaining in string, string
                 shall be set to a single period character.  In this case, skip
                 steps (5) through (8).

             (5) If there are any trailing nonslash characters in string,
                 they shall be removed. */

        ; repeat(split-init-last; (id, not(?'/')); Fst)
        ; (?[]
          < !['.']
          + ( /* (7) If there are any trailing slash characters in string, they
                   shall be removed. */
              repeat(split-init-last; ?(<id>, '/'))
            ; (?[] < !['/'] + id))))))

  /**
   * Succeeds if the input pathname is an absolute path.
   *
   * A pathname is absolute if it starts with a /.
   *
   * @type  String -> String
   * @since 0.9.4
   * @inc   is-abs-path
   */
  is-abspath =
    where(explode-string; ?['/' | _])

  /**
   * Succeeds if the input pathname is a relative path.
   *
   * A pathname is relative if it does not start with a /.
   *
   * @type  String -> String
   * @since 0.9.4
   */
  is-relpath =
    not(is-abspath)

  /**
   * Strips the directory from a pathname.
   *
   * @type  String -> String
   * @inc   base-filename-test
   */
  base-filename =
      <string-tokenize> (['/'], <id>)
    ; last
  
/**
 * Strategies for file extensions
 */
strategies

  /**
   * Drops the current extension and replaces it with the specified extension.
   *
   * @type   String -> String
   * @since  0.15
   * @inc    guarantee-extension
   */    
  guarantee-extension(|ext) =
    remove-extension
    ; <add-extension> (<id>, ext)

  /**
   * Checks if the file extension is ext.
   *
   * @param  The required extension (without . )
   * @type   String -> String
   * @since  0.15
   * @inc    has-extension
   */
  has-extension(|ext) =
    test-extension(?ext)
    
  /**
   * Tests if the file extension (everything after the . satisfies ext.
   *
   * @param  Test for the file extension. Is applied to the plain file extension (without the .)
   * @since  0.15
   * @inc    test-extension
   */
  test-extension(ext) =
    where(
      get-extension
      ; ext
    )

  /**
   * Adds an extension (second argument) to a path (first argument).
   *
   * @type (String, String) -> String
   */
  add-extension : 
    (name, ext) -> <concat-strings> [name, ".", ext]

  /**
   * Remove an extension from a file name.
   * Returns the input if the file does not have an extension.
   *
   * @inc  remove-extension
   * @type String -> String
   */
  remove-extension =
    if get-extension; explode-string; !['.' | <id>] => ext then
      explode-string
      ; at-suffix(?ext; ![])
      ; implode-string
    end

  /**
   * Get extension from filename
   *
   * @type   String -> String
   */
  get-extension =
    explode-string
    ; list-tokenize(|['.'])
    
    /**
     * There should be at least two elements in the result, otherwise 
     * there is not extension.
     */
    ; ?[_, _ | _]
    
    /**
     * The match of ?[base, ext] cannot be placed in at-suffix due to
     * the lack of unbinding of variables on backtracking.
     */
    ; at-suffix(?[_, _]; ?lastwo)
    ; !lastwo => [base, ext]
    ; where(
        /**
         * The n-th . part should not contain / \ :
         * in that case it is not a real extension
         */
        <not(fetch(?'/' + ?'\'))> ext
        
        /**
         * The (n-1)th part should not end with a / or \
         * in that case, part n is not a real extension.
         */
      ; <last; not(?'/' + '\')> base
      )
    ; <implode-string> ext