;; io.scm -- various input/output utilities
;; Copyright (c) 2010-2012 Alex Shinn.  All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; utilities

(define eof
  (call-with-input-string " "
    (lambda (in) (read-char in) (read-char in))))

;; Copy whole characters from the given cursor positions.
;; Return the src cursor position of the next unwritten char,
;; which may be before `to' if the char would overflow.
;; Now provided as a primitive from (chibi ast).
;; (define (string-cursor-copy! dst start src from to)
;;   (let lp ((i from)
;;            (j (string-cursor->index dst start)))
;;     (let ((i2 (string-cursor-next src i)))
;;       (cond ((> i2 to) i)
;;             (else
;;              (string-set! dst j (string-cursor-ref src i))
;;              (lp i2 (+ j 1)))))))

(define (utf8->string vec . o)
  (if (pair? o)
      (let ((start (car o))
            (end (if (pair? (cdr o)) (cadr o) (bytevector-length vec))))
        (utf8->string (subbytes vec start end)))
      (string-copy (utf8->string! vec))))

(define (string->utf8 str . o)
  (if (pair? o)
      (let ((start (car o))
            (end (if (pair? (cdr o)) (cadr o) (string-length str))))
        (string->utf8 (substring str start end)))
      (%string->utf8 str)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; reading and writing

;; Display \var{str} to the given output port, defaulting to
;; \scheme{(current-output-port)}, followed by a newline.

(define (write-line str . o)
  (let ((out (if (pair? o) (car o) (current-output-port))))
    (display str out)
    (newline out)))

;;> \procedure{(write-string str [out [start [end]]])}

;;> Writes the characters from \var{start} to \var{end} of string
;;> \var{str} to output port \var{out}, where \var{start} defaults
;;> to 0 and \var{end} defaults to \scheme{(string-length \var{str})}.

(define (write-string str . o)
  (let ((out (if (pair? o) (car o) (current-output-port)))
        (o (if (pair? o) (cdr o) o)))
    (if (pair? o)
        (let ((start (car o))
              (end (if (pair? (cdr o)) (cadr o) (string-length str))))
          (cond-expand
           (string-streams
            (if (zero? start)
                (%write-string str end out)
                (display (substring str start end) out)))
           (else
            (display (substring str start end) out))))
        (display str out))))

;;> \procedure{(read-line [in [n]])}

;;> Read a line from the input port \var{in}, defaulting to
;;> \scheme{(current-input-port)}, and return the result as
;;> a string not including the newline.  Reads at most \var{n}
;;> characters, defaulting to 8192.

(cond-expand
 ((not string-streams)
  (define (%read-line n in)
    (let ((out (open-output-string)))
      (let lp ((i 0))
        (let ((ch (peek-char in)))
          (cond
           ((eof-object? ch)
            (let ((res (get-output-string out)))
              (and (not (equal? res "")) res)))
           ((eqv? ch #\newline)
            (read-char in)
            (get-output-string out))
           ((eqv? ch #\return)
            (read-char in)
            (if (eqv? #\newline (peek-char in))
                (read-char in))
            (get-output-string out))
           ((>= i n)
            (get-output-string out))
           (else
            (write-char (read-char in) out)
            (lp (+ i 1))))))))))

(define (read-line . o)
  (let ((in (if (pair? o) (car o) (current-input-port)))
        (n (if (and (pair? o) (pair? (cdr o))) (car (cdr o)) 8192)))
    (let ((res (%read-line n in)))
      (cond-expand
       (string-streams
        (port-line-set! in (+ 1 (port-line in)))))
      (if (not res)
          eof
          (let ((len (string-length res)))
            (cond
             ((and (> len 0) (eqv? #\newline (string-ref res (- len 1))))
              (if (and (> len 1) (eqv? #\return (string-ref res (- len 2))))
                  (substring res 0 (- len 2))
                  (substring res 0 (- len 1))))
             ((and (> len 0) (eqv? #\return (string-ref res (- len 1))))
              (substring res 0 (- len 1)))
             (else
              res)))))))

;;> \procedure{(read-string n [in])}

;;> Reads \var{n} characters from input-port \var{in},
;;> defaulting to \scheme{(current-input-port)}, and
;;> returns the result as a string.  Returns \scheme{""}
;;> if \var{n} is zero.  May return a string with fewer
;;> than \var{n} characters if the end of file is reached,
;;> or the eof-object if no characters are available.

(cond-expand
 ((not string-streams)
  (define (%read-string n in)
    (let ((out (open-output-string)))
      (let lp ((i 0))
        (cond ((or (= i n) (eof-object? (peek-char in)))
               (list i (get-output-string out)))
              (else (write-char (read-char in) out) (lp (+ i 1)))))))))

(define (read-string n . o)
  (if (zero? n)
      ""
      (let ((in (if (pair? o) (car o) (current-input-port))))
        (let ((res (%read-string n in)))
          (cond
           ((if (pair? res) (= 0 (car res)) #t)
            eof)
           (else
            (port-line-set! in (+ (string-count-chars #\newline (cadr res) 0)
                                  (port-line in)))
            (cadr res)))))))

;;> \procedure{(read-string! str n [in])}

;;> Reads \var{n} characters from port \var{in}, which
;;> defaults to \scheme{(current-input-port)}, and writes
;;> them into the string \var{str} starting at index 0.
;;> Returns the number of characters read.
;;> An error is signalled if the length of \var{str} is smaller
;;> than \var{n}.

(cond-expand
 ((not string-streams)
  (define (%read-string! str n in)
    (let lp ((i 0))
      (cond ((or (= i n) (eof-object? (peek-char in))) i)
            (else (string-set! str i (read-char in)) (lp (+ i 1))))))))

(define (read-string! str n . o)
  (if (> n (string-length str))
      (error "string to small to read chars" str n))
  (let* ((in (if (pair? o) (car o) (current-input-port)))
         (res (%read-string! str n in)))
    (port-line-set! in (+ (string-count-chars #\newline str 0 n) (port-line in)))
    res))

;;> Sends the entire contents of a file or input port to an output port.

(define (send-file fd-port-or-filename . o)
  (let* ((in (if (string? fd-port-or-filename)
                 (open-input-file fd-port-or-filename)
                 fd-port-or-filename))
         (out (if (pair? o) (car o) (current-output-port)))
         (fd (if (port? in) (port-fileno in) in))
         (sock (if (port? out) (port-fileno out) out)))
    (define (copy-bytes)
      (let ((b (read-u8 in)))
        (cond ((not (eof-object? b))
               (write-u8 b out)
               (copy-bytes)))))
    (if (and fd sock (is-a-socket? sock))
        (let lp ((start 0))
          (let ((res (%send-file fd sock start)))
            (cond
             ((not res) (copy-bytes))
             ((not (zero? res)) (lp (+ start res))))))
        (copy-bytes))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; higher order port operations

;;> The fundamental port iterator.

(define (port-fold kons knil . o)
  (let ((read (if (pair? o) (car o) read))
        (in (if (and (pair? o) (pair? (cdr o)))
                (car (cdr o))
                (current-input-port))))
    (let lp ((acc knil))
      (let ((x (read in)))
        (if (eof-object? x) acc (lp (kons x acc)))))))

(define (port-fold-right kons knil . o)
  (let ((read (if (pair? o) (car o) read))
        (in (if (and (pair? o) (pair? (cdr o)))
                (car (cdr o))
                (current-input-port))))
    (let lp ()
      (let ((x (read in)))
        (if (eof-object? x) knil (kons x (lp)))))))

(define (port-map fn . o)
  (reverse (apply port-fold (lambda (x ls) (cons (fn x) ls)) '() o)))

(define (port->list read in)
  (port-map (lambda (x) x) read in))

(define (port->sexp-list in)
  (port->list read in))

(define (port->string-list in)
  (port->list read-line in))

(define (port->string in)
  (string-concatenate (port->list (lambda (in) (read-string 1024 in)) in)))

(define (port->bytevector in)
  (let ((out (open-output-bytevector)))
    (do ((c (read-u8 in) (read-u8 in)))
        ((eof-object? c) (get-output-bytevector out))
      (write-u8 c out))))

(define (file->string path)
  (call-with-input-file path port->string))

(define (file->bytevector path)
  (call-with-input-file path port->bytevector))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; custom port utilities

;;> \var{read} is a procedure of three arguments:
;;> \scheme{(lambda (str start end) ...)} which should fill \var{str} from
;;> \var{start} to \var{end} with bytes, returning the actual number
;;> of bytes filled.

(define (make-custom-input-port read . o)
  (let ((seek (and (pair? o) (car o)))
        (close (and (pair? o) (pair? (cdr o)) (car (cdr o)))))
    (%make-custom-input-port read seek close)))

;;> \var{write} is a procedure of three arguments:
;;> \scheme{(lambda (str start end) ...)} which should write the bytes of
;;> \var{str} from \var{start} to \var{end}, returning the actual
;;> number of bytes written.

(define (make-custom-output-port write . o)
  (let ((seek (and (pair? o) (car o)))
        (close (and (pair? o) (pair? (cdr o)) (car (cdr o)))))
    (%make-custom-output-port write seek close)))

;;> Similar to \scheme{make-custom-input-port} but returns a binary
;;> port, and \var{read} receives a bytevector to fill instead of a
;;> string.

(define (make-custom-binary-input-port read . o)
  (let ((seek (and (pair? o) (car o)))
        (close (and (pair? o) (pair? (cdr o)) (car (cdr o)))))
    (%make-custom-binary-input-port read seek close)))

;;> Similar to \scheme{make-custom-output-port} but returns a binary
;;> port, and \var{write} receives data from a bytevector instead of a
;;> string.

(define (make-custom-binary-output-port write . o)
  (let ((seek (and (pair? o) (car o)))
        (close (and (pair? o) (pair? (cdr o)) (car (cdr o)))))
    (%make-custom-binary-output-port write seek close)))

;;> A simple /dev/null port which accepts and does nothing with any
;;> data written to it.

(define (make-null-output-port)
  (make-custom-output-port (lambda (str start end) 0)))

;;> A port to broadcast everything written to it to multiple output
;;> ports.

(define (make-broadcast-port . ports)
  (make-custom-output-port
   (lambda (str start end)
     (let ((str (if (zero? start) str (substring str start)))
           (n (- end start)))
       (for-each (lambda (p) (%write-string str n p)) ports)
       n))))

;;> An output port which runs all output (in arbitrary string chunks)
;;> through the \var{filter} procedure.

(define (make-filtered-output-port filter out)
  (make-custom-output-port
   (lambda (str start end)
     (let* ((len (string-length str))
            (s1 (if (and (zero? start) (= end len)) str (substring str start end)))
            (s2 (filter s1)))
       (if (string? s2)
           (%write-string s2 (string-length s2) out))))))

;;> An input port which acts as all of the \var{ports} concatenated
;;> together in order.

(define (make-concatenated-port . ports)
  (make-custom-input-port
   (lambda (str start end)
     (if (null? ports)
         0
         (let ((str (if (zero? start) str (substring str start)))
               (n (- end start)))
           (let lp ((i (read-string! str n (car ports))))
             (cond
              ((>= i n)
               i)
              (else
               (set! ports (cdr ports))
               (cond
                ((null? ports)
                 i)
                (else
                 (let* ((s (read-string (- n i) (car ports)))
                        (len (if (string? s) (string-length s) 0)))
                   (if (and (string? str) (> len 0))
                       (string-cursor-copy! str i s 0 len))
                   (lp (+ i len)))))))))))))

;;> A /dev/null input port which always returns \scheme{eof-object}.

(define (make-null-input-port)
  (make-concatenated-port))

;;> A utility to represent a port generated in chunks by the thunk
;;> \var{generator}, which should return a single string representing
;;> the next input to buffer, or \scheme{#f} when there is no more
;;> input.

(define (make-generated-input-port generator)
  (let ((buf "")
        (len 0)
        (offset 0))
    (make-custom-input-port
     (lambda (str start end)
       (let ((n (- end start)))
         (cond
          ((>= (- len offset) n)
           ;; buf contains enough to fill str
           (let* ((offset2
                   (string-cursor-copy! str start buf offset (+ offset n)))
                  (end2 (+ (- offset2 offset) start)))
             (set! offset offset2)
             end2))
          (else
           ;; copy the rest of buf into str
           (string-cursor-copy! str start buf offset len)
           ;; i is the position to copy into str, from start to end
           (let lp ((i (+ start (- len offset))))
             (set! buf (generator))
             (cond
              ((not (string? buf))
               (set! buf "")
               (set! len 0)
               (set! offset 0)
               i)
              (else
               (set! len (string-size buf))
               (cond
                ((>= len (- n i))
                 (let* ((offset2 (string-cursor-copy! str i buf 0 (- n i)))
                        (end2 (+ i offset2)))
                   (set! offset offset2)
                   end2))
                (else
                 (let ((offset2 (string-cursor-copy! str i buf 0 len)))
                   (lp (+ i offset2)))))))))))))))

(define (%bytevector-copy! to at from start end) ; simplified
  (do ((i at (+ i 1)) (j start (+ j 1)))
      ((>= j end))
    (bytevector-u8-set! to i (bytevector-u8-ref from j))))

;;> Equivalent to \scheme{make-generated-input-port}, but produces a
;;> binary port, and \var{generator} should return a bytevector or
;;> \scheme{#f} when there is no more input.

(define (make-generated-binary-input-port generator)
  (let ((buf #u8())
        (len 0)
        (offset 0))
    (make-custom-binary-input-port
     (lambda (bv start end)
       (let ((n (- end start)))
         (cond
          ((>= (- len offset) n)
           (%bytevector-copy! bv start buf offset (+ offset n))
           (set! offset (+ offset n))
           (+ start n))
          (else
           (%bytevector-copy! bv start buf offset len)
           (let lp ((i (+ start (- len offset))))
             (set! buf (generator))
             (set! offset 0)
             (cond
              ((not (bytevector? buf))
               (set! buf #u8())
               (set! len 0)
               i)
              (else
               (set! len (bytevector-length buf))
               (cond
                ((>= len (- n i))
                 (%bytevector-copy! bv i buf 0 (- n i))
                 (set! offset (- n i))
                 n)
                (else
                 (%bytevector-copy! bv i buf 0 len)
                 (lp (+ i len))))))))))))))

;;> An input port which runs all input (in arbitrary string chunks)
;;> through the \var{filter} procedure.

(define (make-filtered-input-port filter in)
  (make-generated-input-port
   (lambda ()
     (let ((res (read-string 1024 in)))
       (if (string? res) (filter res) res)))))
