;;; Arl Standard Library - IO Helpers

(module io
  (export read-line read-file read-lines
          write-file write-lines append-file file-exists?
          newline
          read-from-string write-string
          file-size file-modified-time file-delete
          directory-exists? directory-list directory-delete
          getenv setenv
          system-output exit)

  (import core :refer (r-call get))

  ;; Normalize content to character vector.
  (define _normalize-line-vector
    (lambda (lines)
      (if (is.null lines)
        (character 0)
        (if (is.call lines)
          (vapply (as.list lines) as.character (character 1))
          (if (is.list lines)
            (vapply lines as.character (character 1))
            (as.character lines))))))

  ;; Collapse content to string.
  (define _normalize-lines
    (lambda (content (sep "\n"))
      (define lines (_normalize-line-vector content))
      (if (<= (length lines) 1)
        (as.character lines)
        (paste lines :collapse sep))))

  ;;' @section Console I/O
  ;;' Functions for reading from standard input and writing to standard output.

  ;;' @description Read single line from stdin.
  ;;' @param prompt Prompt string to display before reading (default "")
  ;;' @examples
  ;;' (read-line)              ; waits for user input, returns string
  ;;' (read-line "Name? ")     ; prints prompt, then waits for input
  (define read-line
    (lambda ((prompt ""))
      (define con (getOption "arl.stdin"))
      (if (is.null con) (define con (stdin)) #nil)
      (if (nzchar prompt) (cat prompt) #nil)
      (r-call "readLines" (list :con con :n 1 :warn #f))))

  ;;' @seealso read
  (define read-from-string read)  ;; Alias for read

  ;;' @description Write string to output (alias for cat).
  ;;' @param x Value to write to standard output
  ;;' @examples
  ;;' (write-string "hello")  ; outputs "hello" with no newline
  ;;' @seealso display, newline
  (define write-string
    (lambda (x)
      (cat x)
      #nil))

  ;;' @description Output a newline.
  ;;' @seealso display, write-string
  (define newline
    (lambda ()
      (cat "\n")
      #nil))

  ;;' @section File I/O
  ;;' Functions for reading from and writing to files.

  ;;' @description Read entire file as string.
  ;;' @param path File path to read
  ;;' @param encoding Character encoding to use (default "UTF-8")
  ;;' @noeval
  ;;' @examples
  ;;' (read-file "data.txt")                  ; => file contents as string
  ;;' (read-file "data.txt" "latin1")         ; read with specific encoding
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list (r-call "c" (list "line1" "line2")) _tmp))
  ;;' (assert-equal "line1\nline2" (read-file _tmp))
  ;;' (r-call "unlink" (list _tmp))
  (define read-file
    (lambda (path (encoding "UTF-8"))
      (define con (file path :open "r" :encoding encoding))
      (define lines (readLines :con con :warn #f))
      (close con)
      (paste lines :collapse "\n")))

  ;;' @description Read file into list of lines.
  ;;' @param path File path to read
  ;;' @param encoding Character encoding to use (default "UTF-8")
  ;;' @noeval
  ;;' @examples
  ;;' (read-lines "data.txt")  ; => ("line1" "line2" "line3")
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list (r-call "c" (list "alpha" "beta" "gamma")) _tmp))
  ;;' (assert-equal (list "alpha" "beta" "gamma") (read-lines _tmp))
  ;;' (r-call "unlink" (list _tmp))
  (define read-lines
    (lambda (path (encoding "UTF-8"))
      (define con (file path :open "r" :encoding encoding))
      (define lines (readLines :con con :warn #f))
      (close con)
      (as.list lines)))

  ;;' @description Write string or lines to file.
  ;;' @param path File path to write to
  ;;' @param content String or list of lines to write
  ;;' @param sep Separator used to join lines (default "\n")
  ;;' @param encoding Character encoding to use (default "UTF-8")
  ;;' @noeval
  ;;' @examples
  ;;' (write-file "out.txt" "hello world")
  ;;' (write-file "out.txt" (list "line1" "line2"))
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (write-file _tmp "hello world")
  ;;' (assert-equal "hello world" (read-file _tmp))
  ;;' (r-call "unlink" (list _tmp))
  (define write-file
    (lambda (path content (sep "\n") (encoding "UTF-8"))
      (define text (_normalize-lines content sep))
      (define con (file path :open "w" :encoding encoding))
      (writeLines text :con con :useBytes #f)
      (close con)
      #t))

  ;;' @description Write list of lines to file.
  ;;' @param path File path to write to
  ;;' @param lines List of lines to write
  ;;' @param encoding Character encoding to use (default "UTF-8")
  ;;' @noeval
  ;;' @examples
  ;;' (write-lines "out.txt" (list "line1" "line2" "line3"))
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (write-lines _tmp (list "one" "two" "three"))
  ;;' (assert-equal (list "one" "two" "three") (read-lines _tmp))
  ;;' (r-call "unlink" (list _tmp))
  (define write-lines
    (lambda (path lines (encoding "UTF-8"))
      (define out (_normalize-line-vector lines))
      (define con (file path :open "w" :encoding encoding))
      (writeLines out :con con :useBytes #f)
      (close con)
      #t))

  ;;' @description Append content to file.
  ;;' @param path File path to append to
  ;;' @param content String or list of lines to append
  ;;' @param sep Separator used to join lines (default "\n")
  ;;' @param encoding Character encoding to use (default "UTF-8")
  ;;' @noeval
  ;;' @examples
  ;;' (append-file "log.txt" "new entry\n")
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (write-file _tmp "first")
  ;;' (append-file _tmp "second")
  ;;' (assert-true (string-contains? (read-file _tmp) "first"))
  ;;' (assert-true (string-contains? (read-file _tmp) "second"))
  ;;' (r-call "unlink" (list _tmp))
  (define append-file
    (lambda (path content (sep "\n") (encoding "UTF-8"))
      (define text (_normalize-lines content sep))
      (define con (file path :open "a" :encoding encoding))
      (writeLines text :con con :useBytes #f :sep "")
      (close con)
      #t))

  ;;' @description Return #t if file exists.
  ;;' @param path File path to check
  ;;' @noeval
  ;;' @examples
  ;;' (file-exists? "data.txt")    ; => #t or #f
  ;;' (file-exists? "nope.txt")    ; => #f
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list "" _tmp))
  ;;' (assert-true (file-exists? _tmp))
  ;;' (assert-false (file-exists? "/nonexistent/path/to/file.txt"))
  ;;' (r-call "unlink" (list _tmp))
  (define file-exists?
    (lambda (path)
      (isTRUE (file.exists path))))

  ;; Get R base system function for system-output
  (define _rsystem (get "system" :envir (baseenv)))

  ;;' @description Return size of file in bytes.
  ;;' @param path File path to query
  ;;' @noeval
  ;;' @examples
  ;;' (file-size "data.txt")       ; => 1024 (bytes)
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list "hello" _tmp))
  ;;' (assert-true (> (file-size _tmp) 0))
  ;;' (r-call "unlink" (list _tmp))
  (define file-size
    (lambda (path)
      (if (file.exists path)
        (r-call "[[" (list (r-call "file.info" (list path)) "size"))
        (stop (sprintf "file-size: file does not exist: %s" path)))))

  ;;' @description Return file modification time as numeric timestamp.
  ;;' @param path File path to query
  ;;' @noeval
  ;;' @examples
  ;;' (file-modified-time "data.txt")  ; => 1700000000 (numeric timestamp)
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list "" _tmp))
  ;;' (assert-true (> (file-modified-time _tmp) 0))
  ;;' (r-call "unlink" (list _tmp))
  (define file-modified-time
    (lambda (path)
      (if (file.exists path)
        (begin
          (define mtime (r-call "[[" (list (r-call "file.info" (list path)) "mtime")))
          (as.numeric mtime))
        (stop (sprintf "file-modified-time: file does not exist: %s" path)))))

  ;;' @description Delete file. Return #t on success.
  ;;' @param path File path to delete
  ;;' @noeval
  ;;' @examples
  ;;' (file-delete "temp.txt")     ; => #t
  ;;' @assert
  ;;' (define _tmp (r-call "tempfile" (list)))
  ;;' (r-call "writeLines" (list "" _tmp))
  ;;' (assert-true (file-exists? _tmp))
  ;;' (assert-true (file-delete _tmp))
  ;;' (assert-false (file-exists? _tmp))
  (define file-delete
    (lambda (path)
      (if (file.exists path)
        (begin
          (file.remove path)
          #t)
        (stop (sprintf "file-delete: file does not exist: %s" path)))))

  ;;' @section Directory Operations
  ;;' Functions for querying and manipulating directories.

  ;;' @description Return #t if directory exists.
  ;;' @param path Directory path to check
  ;;' @examples
  ;;' (directory-exists? "/tmp")         ; => #t
  ;;' (directory-exists? "/nonexistent") ; => #f
  ;;' @assert
  ;;' (assert-true (directory-exists? (r-call "tempdir" (list))))
  ;;' (assert-false (directory-exists? "/nonexistent/path/that/does/not/exist"))
  (define directory-exists?
    (lambda (path)
      (isTRUE (dir.exists path))))

  ;;' @description List directory contents as list of filenames.
  ;;' @param path Directory path to list
  ;;' @param full.names When #t, return full file paths instead of just names (default #f)
  ;;' @noeval
  ;;' @examples
  ;;' (directory-list ".")               ; => ("file1.txt" "file2.txt")
  ;;' (directory-list "." #t)            ; => ("./file1.txt" "./file2.txt")
  ;;' @assert
  ;;' (define _tmpdir (r-call "tempfile" (list "dir")))
  ;;' (r-call "dir.create" (list _tmpdir))
  ;;' (r-call "writeLines" (list "" (r-call "file.path" (list _tmpdir "a.txt"))))
  ;;' (r-call "writeLines" (list "" (r-call "file.path" (list _tmpdir "b.txt"))))
  ;;' (define _listing (directory-list _tmpdir))
  ;;' (assert-true (> (length _listing) 0))
  ;;' (assert-true (r-call "%in%" (list "a.txt" _listing)))
  ;;' (r-call "unlink" (list _tmpdir :recursive #t))
  (define directory-list
    (lambda (path (full.names #f))
      (if (dir.exists path)
        (as.list (list.files path :full.names full.names))
        (stop (sprintf "directory-list: directory does not exist: %s" path)))))

  ;;' @description Delete directory. Return #t on success.
  ;;' @param path Directory path to delete
  ;;' @param recursive When #t, delete contents recursively (default #t)
  ;;' @noeval
  ;;' @examples
  ;;' (directory-delete "/tmp/mydir")    ; => #t
  ;;' @assert
  ;;' (define _tmpdir (r-call "tempfile" (list "deldir")))
  ;;' (r-call "dir.create" (list _tmpdir))
  ;;' (assert-true (directory-exists? _tmpdir))
  ;;' (assert-true (directory-delete _tmpdir))
  ;;' (assert-false (directory-exists? _tmpdir))
  (define directory-delete
    (lambda (path (recursive #t))
      (if (dir.exists path)
        (begin
          (unlink path :recursive recursive)
          #t)
        (stop (sprintf "directory-delete: directory does not exist: %s" path)))))

  ;;' @section Environment and System
  ;;' Functions for environment variables, shell commands, and process control.

  ;;' @description Get environment variable value. Return #nil if not set.
  ;;' @param name Environment variable name
  ;;' @examples
  ;;' (getenv "HOME")           ; => "/home/user"
  ;;' (getenv "UNDEFINED_VAR")  ; => #nil
  ;;' @assert
  ;;' (assert-true (is.character (getenv "HOME")))
  ;;' (assert-true (is.null (getenv "ARL_UNDEFINED_TEST_VAR_XYZ")))
  ;;' @seealso setenv
  (define getenv
    (lambda (name)
      (define value (Sys.getenv name))
      (if (== value "")
        #nil
        value)))

  ;;' @description Set environment variable.
  ;;' @param name Environment variable name
  ;;' @param value Value to assign to the variable
  ;;' @examples
  ;;' (setenv "MY_VAR" "hello")  ; => #t
  ;;' @assert
  ;;' (setenv "ARL_TEST_SETENV_VAR" "test_value_123")
  ;;' (assert-equal "test_value_123" (getenv "ARL_TEST_SETENV_VAR"))
  ;;' @seealso getenv
  (define setenv
    (lambda (name value)
      ;; Create a named list and use do.call
      ;; First create a list with the value
      (define args-list (r-call "list" (list value)))
      ;; Set the name on the first element
      (define named-list (r-call "names<-" (list args-list (r-call "c" (list name)))))
      ;; Call Sys.setenv with the named list - pass as string "Sys.setenv"
      (r-call "do.call" (list "Sys.setenv" named-list))
      #t))

  ;;' @description Execute shell command and capture output as string.
  ;;' @param command Shell command string to execute
  ;;' @examples
  ;;' (system-output "whoami")   ; => "username"
  ;;' (system-output "echo hi")  ; => "hi"
  ;;' @assert
  ;;' (assert-equal "hi" (system-output "echo hi"))
  (define system-output
    (lambda (command)
      (paste (_rsystem command #t) :collapse "\n")))

  ;;' @description Exit program with status code.
  ;;' @param status Exit status code (default 0)
  ;;' @signature (exit status)
  (define exit
    (lambda ((status 0))
      (q :save "no" :status status))))
