#lang scheme/base

;; FIXME: newline decoding

(require rnrs/enums-6
         rnrs/conditions-6
         r6rs/private/io-conds
         r6rs/private/readtable
         r6rs/private/exns
         scheme/port
         scheme/pretty
         scheme/promise)

(provide (all-from-out r6rs/private/io-conds)
         file-options
         buffer-mode buffer-mode?
         latin-1-codec utf-8-codec utf-16-codec
         eol-style native-eol-style
         &i/o-decoding make-i/o-decoding-error i/o-decoding-error?
         &i/o-encoding make-i/o-encoding-error i/o-encoding-error? i/o-encoding-error-char
         error-handling-mode
         (rename-out [r6rs:make-transcoder make-transcoder])
         transcoder-codec
         transcoder-eol-style
         transcoder-error-handling-mode
         native-transcoder
         bytevector->string
         string->bytevector
         eof-object
         eof-object?
         port?
         port-transcoder
         textual-port?
         binary-port?
         transcoded-port
         port-has-port-position?
         port-position
         port-has-set-port-position!?
         set-port-position!
         close-port
         call-with-port
         input-port?
         port-eof?
         open-file-input-port
         open-bytevector-input-port
         open-string-input-port
         standard-input-port
         (rename-out [r6rs:current-input-port current-input-port])
         make-custom-binary-input-port
         make-custom-textual-input-port
         get-u8
         lookahead-u8
         get-bytevector-n
         get-bytevector-n!
         get-bytevector-some
         get-bytevector-all
         get-char
         lookahead-char
         get-string-n
         get-string-n!
         get-string-all
         get-line
         get-datum
         output-port?
         flush-output-port
         output-port-buffer-mode
         open-file-output-port
         open-bytevector-output-port
         call-with-bytevector-output-port
         open-string-output-port
         call-with-string-output-port
         standard-output-port
         standard-error-port
         (rename-out [r6rs:current-output-port current-output-port]
                     [r6rs:current-error-port current-error-port])
         make-custom-binary-output-port
         make-custom-textual-output-port
         put-u8
         put-bytevector
         put-char
         put-string
         put-datum
         open-file-input/output-port
         make-custom-binary-input/output-port
         make-custom-textual-input/output-port

         ;; Non-standard
         r6rs-port->port)

;; ----------------------------------------

(define-enumeration -file-option (no-create no-fail no-truncate)
  file-options)

(define-enumeration buffer-mode (none line block)
  -buffer-modes)

(define (buffer-mode? m)
  (enum-set-member? m (-buffer-modes none line block)))

(define-enumeration eol-style (lf cr crlf nel crnel ls none)
  -eol-styles)

(define-struct codec (enc))
(define latin-1 (make-codec "latin1"))
(define utf-8 (make-codec "utf-8"))
(define utf-16 (make-codec "utf-16"))

(define (latin-1-codec) latin-1)
(define (utf-8-codec) utf-8)
(define (utf-16-codec) utf-16)

(define (native-eol-style)
  (if (eq? (system-type) 'windows)
      'crlf
      'lf))

(define-condition-type &i/o-decoding &i/o-port
  make-i/o-decoding-error i/o-decoding-error?)

(define-condition-type &i/o-encoding &i/o-port
  make-i/o-encoding-error i/o-encoding-error?
  (char i/o-encoding-error-char))

(define-enumeration error-handling-mode (ignore raise replace)
  -handling-modes)

(define-struct transcoder (codec eol-style error-handling-mode))

(define (r6rs:make-transcoder codec 
                              [eol-style (native-eol-style)]
                              [handling-mode 'replace])
  (unless (codec? codec)
    (raise-type-error 'make-transcoder "codec" codec))
  (unless (enum-set-member? eol-style (-eol-styles lf cr crlf nel crnel ls none))
    (raise-type-error 'make-transcoder "'lf, 'cr, 'crlf, 'nel, 'crnel, 'ls, or 'none" eol-style))
  (unless (enum-set-member? handling-mode (-handling-modes ignore raise replace))
    (raise-type-error 'make-transcoder "'ignore, 'raise, or 'replace" eol-style))
  (make-transcoder codec eol-style handling-mode))

(define utf8-transcoder 
  (make-transcoder utf-8 'none '?))

(define (native-transcoder)
  utf8-transcoder)

(define (eof-object) eof)

;; ----------------------------------------

(define (make-disconnectable-input-port port close?)
  (define disconnected? #f)
  (define (check-disconnect)
    (when disconnected?
      (error 'read-byte "cannot read for transcoded binary port")))
  (values
   (make-input-port
    (object-name port)
    (lambda (bytes)
      (check-disconnect)
      (let ([n (read-bytes-avail!* bytes port)])
        (if (eq? n 0)
            (wrap-evt port (lambda (v) 0))
            n)))
    (lambda (bytes skip evt)
      (check-disconnect)
      (let ([n (peek-bytes-avail! bytes skip evt port)])
        (if (eq? n 0)
            (wrap-evt port (lambda (v) 0))
            n)))
    (lambda ()
      (unless disconnected?
        (when close?
          (close-input-port port))))
    (and (port-provides-progress-evts? port)
         (lambda ()
           (check-disconnect)
           (port-progress-evt port)))
    (and (port-provides-progress-evts? port)
         (lambda (k evt done)
           (port-commit-peeked k evt done port)))
    (lambda ()
      (check-disconnect)
      (port-next-location port))
    (lambda ()
      (check-disconnect)
      (port-count-lines! port))
    1
    (case-lambda
     [() (check-disconnect) (file-stream-buffer-mode port)]
     [(mode) (check-disconnect) (file-stream-buffer-mode port 
                                                         (if (eq? mode 'line) 'block mode))]))
   (lambda ()
     (set! disconnected? #t)
     port)))

(define (make-disconnectable-output-port port close?)
  (define disconnected? #f)
  (define (check-disconnect)
    (when disconnected?
      (error 'read-byte "cannot read for transcoded binary port")))
  (values
   (make-output-port
    (object-name port)
    port
    (lambda (bytes start end can-buffer/block? enable-breaks?)
      (check-disconnect)
      (if (= start end)
          (begin
            (flush-output port)
            0)
          (cond
           [enable-breaks?
            (parameterize-break #t (write-bytes (subbytes bytes start end) port))]
           [can-buffer/block?
            (write-bytes (subbytes bytes start end) port)]
           [else
            (write-bytes-avail* (subbytes bytes start end) port)])))
    (lambda ()
      (unless disconnected?
        (when close?
          (close-output-port port))))
    (and (port-writes-special? port)
         (lambda (v can-buffer/block? enable-breaks?)
           (check-disconnect)
           (cond
            [enable-breaks?
             (parameterize-break #t (write-special v port))]
            [can-buffer/block?
             (write-special v port)]
            [else
             (write-special-avail* v port)])))
    (and (port-writes-atomic? port)
         (lambda (bytes start end)
           (check-disconnect)
           (write-bytes-avail-evt bytes port start end)))
    (and (port-writes-special? port)
         (port-writes-atomic? port)
         (lambda (v)
           (check-disconnect)
           (write-special-evt v port)))
    (lambda ()
      (check-disconnect)
      (port-next-location port))
    (lambda ()
      (check-disconnect)
      (port-count-lines! port))
    1
    (case-lambda
     [() (check-disconnect) (file-stream-buffer-mode port)]
     [(mode) (check-disconnect) (file-stream-buffer-mode port mode)]))
   (lambda ()
     (set! disconnected? #t)
     port)))

;; For merging two kinds of ports:
(define-struct dual-port (in out)
  #:property prop:input-port 0
  #:property prop:output-port 1)

;; R6RS functions that generate binary ports wrap them with `binary-...-port'
;; structures, so that the binary ports can be "closed" by `transcoded-port'.
(define-struct binary-input-port (port disconnect get-pos set-pos!)
  #:property prop:input-port 0)
(define-struct binary-output-port (port disconnect get-pos set-pos!)
  #:property prop:output-port 0)
(define-struct (binary-input/output-port binary-input-port) (out-port out-disconnect)
  #:property prop:output-port 0)

;; Textual ports are transcoded
(define-struct textual-input-port (port transcoder)
  #:property prop:input-port 0)
(define-struct textual-output-port (port transcoder)
  #:property prop:output-port 0)
(define-struct (textual-input/output-port textual-input-port) (out-port)
  #:property prop:output-port 0)

(define (port-transcoder port)
  (cond
   [(dual-port? port) (port-transcoder (dual-port-in port))]
   [(textual-input-port? port) (textual-input-port-transcoder port)]
   [(textual-output-port? port) (textual-output-port-transcoder port)]
   [(input-port? port) #f]
   [(output-port? port) #f]
   [else (raise-type-error 'port-transcoder "port" port)]))

(define (textual-port? v)
  (if (port? v)
      (or (textual-input-port? v)
          (textual-output-port? v)
          (and (dual-port? v) 
               (textual-port? (dual-port-in v))))
      (raise-type-error 'textual-port? "port" v)))

(define (binary-port? v)
  (if (port? v)
      (not (or (textual-input-port? v)
               (textual-output-port? v)
               (and (dual-port? v) 
                    (textual-port? (dual-port-in v)))))
      (raise-type-error 'binary-port? "port" v)))

(define (wrap-binary-input-port p get-pos set-pos! close?)
  (let-values ([(p disconnect) (make-disconnectable-input-port p close?)])
    (make-binary-input-port p disconnect get-pos set-pos!)))

(define (wrap-binary-output-port p get-pos set-pos! close?)
  (let-values ([(p disconnect) (make-disconnectable-output-port p close?)])
    (make-binary-output-port p disconnect get-pos set-pos!)))

(define (wrap-binary-input/output-port p get-pos set-pos! close?)
  (let-values ([(p disconnect) (make-disconnectable-input-port p #t)]
               [(out-p out-disconnect) (make-disconnectable-output-port p #t)])
    (make-binary-input/output-port p disconnect get-pos set-pos!
                                   out-p out-disconnect)))

(define (no-op-transcoder? t)
  (or (eq? t utf8-transcoder)
      (and (eq? utf-8 (transcoder-codec t))
           (eq? (transcoder-eol-style t) 'none)
           (eq? 'replace (transcoder-error-handling-mode t)))))

(define (transcode-input p t)
  (let ([p (if (binary-input-port? p)
               ((binary-input-port-disconnect p))
               p)])
    (if (no-op-transcoder? t)
        p
        (letrec ([self
                  (reencode-input-port p 
                                       (codec-enc (transcoder-codec t))
                                       (case (transcoder-error-handling-mode t)
                                         [(raise) #f]
                                         [(ignore) #""]
                                         [(replace) (string->bytes/utf-8 "\uFFFD")])
                                       #t
                                       (object-name p)
                                       (not (eq? (transcoder-eol-style t) 'none))
                                       (lambda (msg port)
                                         (raise
                                          (condition
                                           (make-message-condition 
                                            (format "~a: ~e" msg port))
                                           (make-i/o-decoding-error
                                            self)))))])
          self))))

(define (transcode-output p t)
  (let ([p (cond
            [(binary-output-port? p)
             ((binary-output-port-disconnect p))]
            [(binary-input/output-port? p)
             ((binary-input/output-port-out-disconnect p))]
            [else p])])
    (if (no-op-transcoder? t)
        p
        (letrec ([self
                  (reencode-output-port p 
                                        (codec-enc (transcoder-codec t))
                                        (case (transcoder-error-handling-mode t)
                                          [(raise) #f]
                                          [(ignore) #""]
                                          [(replace) (string->bytes/utf-8 "\uFFFD")])
                                        #t
                                        (object-name p)
                                        (case (transcoder-eol-style t)
                                          [(lf none) #f]
                                          [(cr) #"\r"]
                                          [(crlf) #"\r\n"]
                                          [(nel) (string->bytes/utf-8 "\u85")]
                                          [(crnel) (string->bytes/utf-8 "\r\u85")]
                                          [(ls) (string->bytes/utf-8 "\u2028")]
                                          [else (error 'transcoded-port "unknown eol style: ~e" 
                                                       (transcoder-eol-style t))])
                                        (lambda (msg port)
                                          (raise
                                           (condition
                                            (make-message-condition 
                                             (format "~a: ~e" msg port))
                                            (make-i/o-encoding-error
                                             self
                                             #\?)))))])
          self))))

(define (transcoded-port p t)
  (unless (and (port? p)
               (binary-port? p))
    (raise-type-error 'transcoded-port "binary port" p))
  (unless (transcoder? t)
    (raise-type-error 'transcoded-port "transcoder" t))
  (cond
   [(and (input-port? p) (output-port? p))
    (make-textual-input/output-port (transcode-input p t)
                                    t 
                                    (transcode-output p t))]
   [(input-port? p)
    (make-textual-input-port (transcode-input p t) t)]
   [(output-port? p)
    (make-textual-output-port (transcode-output p t) t)]))

(define (port-has-port-position? p)
  (unless (port? p)
    (raise-type-error 'port-has-port-position? "port" p))
  (cond
   [(binary-input-port? p)
    (and (binary-input-port-get-pos p) #t)]
   [(binary-output-port? p)
    (and (binary-output-port-get-pos p) #t)]
   [(textual-input-port? p)
    (port-has-port-position? (textual-input-port-port p))]
   [(textual-output-port? p)
    (port-has-port-position? (textual-output-port-port p))]
   [(dual-port? p)
    (port-has-port-position? (dual-port-in p))]
   [else #t]))

(define (port-position p)
  (cond 
   [(binary-input-port? p)
    ((binary-input-port-get-pos p))]
   [(binary-output-port? p)
    ((binary-output-port-get-pos p))]
   [(textual-input-port? p)
    (port-position (textual-input-port-port p))]
   [(textual-output-port? p)
    (port-position (textual-output-port-port p))]
   [(dual-port? p)
    (port-position (dual-port-in p))]
   [else (file-position p)]))

(define (port-has-set-port-position!? p)
  (unless (port? p)
    (raise-type-error 'port-has-port-set-position!? "port" p))
  (cond 
   [(binary-input-port? p)
    (and (binary-input-port-set-pos! p) #t)]
   [(binary-output-port? p)
    (and (binary-output-port-set-pos! p) #t)]
   [(textual-input-port? p)
    (port-has-set-port-position!? (textual-input-port-port p))]
   [(textual-output-port? p)
    (port-has-set-port-position!? (textual-output-port-port p))]
   [(dual-port? p)
    (port-has-set-port-position!? (dual-port-in p))]
   [else
    ;; we could also allow string ports here
    (file-stream-port? p)]))

(define (set-port-position! p pos)
  (unless (and (port? p)
               (port-has-set-port-position!? p))
    (raise-type-error 'set-port-position! "port with settable position" p))
  (cond 
   [(binary-input-port? p)
    ((binary-input-port-set-pos! p) pos)]
   [(binary-output-port? p)
    ((binary-output-port-set-pos! p) pos)]
   [(textual-input-port? p)
    (set-port-position! (textual-input-port-port p) pos)]
   [(textual-output-port? p)
    (set-port-position! (textual-output-port-port p) pos)]
   [(dual-port? p)
    (set-port-position! (dual-port-in p) pos)]
   [else
    (file-position p pos)]))

(define (call-with-port port proc)
  (unless (port? port)
    (raise-type-error 'call-with-port "port" port))
  (begin0
   (proc port)
   (close-port port)))

(define (close-port port)
  (when (input-port? port)
    (close-input-port port))
  (when (output-port? port)
    (close-output-port port)))

;; ----------------------------------------

(define (port-eof? p)
  (eof-object? (peek-byte p)))

(define (open-file-input-port filename 
                              [options (file-options)]
                              [buffer-mode 'block]
                              [maybe-transcoder #f])
  (unless (enum-set=? (enum-set-universe options)
                      (enum-set-universe (file-options)))
    (raise-type-error 'open-file-input-port "file-options enum set" options))
  (unless (enum-set-member? buffer-mode (-buffer-modes none line block))
    (raise-type-error 'open-file-input-port "'none, 'line, or 'block" buffer-mode))
  (when maybe-transcoder
    (unless (transcoder? maybe-transcoder)
      (raise-type-error 'open-file-input-port "transcoder or #f" maybe-transcoder)))
  (let ([p (open-input-file filename)])
    (file-stream-buffer-mode p (if (eq? buffer-mode 'line)
                                   'block
                                   buffer-mode))
    (if maybe-transcoder
        (transcoded-port p maybe-transcoder)
        (wrap-binary-input-port p 
                                (lambda () (file-position p)) 
                                (lambda (pos) (file-position p pos))
                                #t))))

(define (open-bytevector-input-port bytes [maybe-transcoder #f])
  (unless (bytes? bytes)
    (raise-type-error 'open-bytevector-input-port "bytevector" bytes))
  (when maybe-transcoder
    (unless (transcoder? maybe-transcoder)
      (raise-type-error 'open-bytevector-input-port "transcoder or #f" maybe-transcoder)))
  (let ([p (open-input-bytes bytes)])
    (if maybe-transcoder
        (transcoded-port p maybe-transcoder)
        (wrap-binary-input-port p
                                (lambda () (file-position p)) 
                                (lambda (pos) (file-position p pos))
                                #t))))

(define (open-string-input-port str)
  (unless (string? str)
    (raise-type-error 'open-bytevector-input-port "string" str))
  (let ([p (open-input-string str)])
    (transcoded-port
     (wrap-binary-input-port p
                             (lambda () (file-position p)) 
                             (lambda (pos) (file-position p pos))
                             #t)
     utf8-transcoder)))

(define standard-input-port
  (let ([p (current-input-port)])
    (lambda ()
      (wrap-binary-input-port p
                              (lambda () (file-position p)) 
                              (lambda (pos) (file-position p pos))
                              #f))))

(define input-ports (make-weak-hasheq))

(define (r6rs:current-input-port)
  (let ([p (current-input-port)])
    (cond
     [(textual-port? p) p]
     [(hash-ref input-ports p #f)
      => ephemeron-value]
     [else
      (let ([p2 (transcoded-port p utf8-transcoder)])
        (hash-set! input-ports p (make-ephemeron p p2))
        p2)])))

(define (make-custom-binary-input-port id read! get-position set-position! close)
  (let* ([peeked 0]
         [p (make-input-port/read-to-peek
             id
             (lambda (bytes)
               (let ([v (read! bytes 0 (bytes-length bytes))])
                 (set! peeked (+ peeked v))
                 (if (zero? v)
                     eof
                     v)))
             #f
             (or close void)
             #f void 1
             #f #f
             (lambda (consumed-n)
               (unless (eof-object? consumed-n)
                 (set! peeked (- consumed-n 1)))))])
    (wrap-binary-input-port p
                            (and get-position
                                 (lambda ()
                                   (let ([v (get-position)])
                                     (- v peeked))))
                            (and set-position!
                                 (lambda (pos)
                                   ;; flush peeked
                                   (let loop ()
                                     (unless (zero? peeked)
                                       (read-byte-or-special p)
                                       (loop)))
                                   ;; set position
                                   (set-position! pos)))
                            #t)))


(define (make-custom-textual-input-port id read! get-position set-position! close)
  (make-textual-input-port
   (make-custom-binary-input-port
    id 
    (let-values ([(in out) (make-pipe)])
      (lambda (bstr offset len)
        (let loop ()
          (let ([n (read-bytes-avail!* bstr in offset len)])
            (if (zero? n)
                (let ([str (make-string (bytes-length bstr))])
                  (let ([len (read! str 0 (bytes-length bstr))])
                    (if (zero? len)
                        0
                        (begin
                          (write-string (substring str 0 len) out)
                          (loop)))))
                n)))))
    get-position
    set-position!
    (or close void))
   #f))

;; ----------------------------------------

(define (get-u8 p)
  (unless (binary-port? p)
    (raise-type-error 'get-u8 "binary port" p))
  (read-byte p))

(define (lookahead-u8 p)
  (unless (binary-port? p)
    (raise-type-error 'lookahead-u8 "binary port" p))
  (peek-byte p 0))

(define (get-bytevector-n p cnt)
  (unless (binary-port? p)
    (raise-type-error 'get-bytevector-n "binary port" p))
  (read-bytes cnt p))

(define (get-bytevector-n! p bytes start end)
  (unless (binary-port? p)
    (raise-type-error 'get-bytevector-n! "binary port" p))
  (read-bytes! bytes p start end))

(define (get-bytevector-some p)
  (unless (binary-port? p)
    (raise-type-error 'get-bytevector-some "binary port" p))
  (let ([bytes (make-bytes 4096)])
    (let ([n (read-bytes-avail! bytes p)])
      (if (eof-object? n)
          n
          (subbytes bytes 0 n)))))

(define (get-bytevector-all p)
  (unless (binary-port? p)
    (raise-type-error 'get-bytevector-all "binary port" p))
  (let ([p2 (open-output-bytes)])
    (copy-port p p2)
    (get-output-bytes p2 #t)))

;; ----------------------------------------

(define (get-char p)
  (unless (textual-port? p)
    (raise-type-error 'get-char "textual port" p))
  (read-char p))

(define (lookahead-char p)
  (unless (textual-port? p)
    (raise-type-error 'lookahead-char "textual port" p))
  (peek-char p))

(define (get-string-n p cnt)
  (unless (textual-port? p)
    (raise-type-error 'get-string-n "textual port" p))
  (read-string cnt p))

(define (get-string-n! p str start end)
  (unless (textual-port? p)
    (raise-type-error 'get-string-n! "textual port" p))
  (read-string! str p start end))

(define (get-string-all p)
  (unless (textual-port? p)
    (raise-type-error 'get-string-all "textual port" p))
  (let ([p2 (open-output-bytes)])
    (copy-port p p2)
    (get-output-string p2)))

(define (get-line p)
  (unless (textual-port? p)
    (raise-type-error 'get-line "textual port" p))
  (read-line p 'linefeed))

(define (get-datum p)
  (unless (textual-port? p)
    (raise-type-error 'get-datum "textual port" p))
  (let loop ([v (with-r6rs-reader-parameters (lambda () (read p)))])
    (cond
     [(pair? v) (mcons (loop (car v))
                       (loop (cdr v)))]
     [(vector? v) (list->vector
                   (map loop (vector->list v)))]
     [else v])))

;; ----------------------------------------

(define (flush-output-port p)
  (flush-output p))

(define (output-port-buffer-mode p)
  (file-stream-buffer-mode p))

(define (do-open-file-output-port who
                                  filename 
                                  options
                                  buffer-mode
                                  maybe-transcoder
                                  open-output-file
                                  file-position
                                  wrap-binary-port)
  (unless (enum-set=? (enum-set-universe options)
                      (enum-set-universe (file-options)))
    (raise-type-error who "file-options enum set" options))
  (unless (enum-set-member? buffer-mode (-buffer-modes none line block))
    (raise-type-error who "'none, 'line, or 'block" buffer-mode))
  (when maybe-transcoder
    (unless (transcoder? maybe-transcoder)
      (raise-type-error who "transcoder or #f" maybe-transcoder)))
  (let ([exists-mode (cond
                      [(or (enum-set=? options (file-options no-create no-fail no-truncate))
                           (enum-set=? options (file-options no-create no-truncate)))
                       'update]
                      [(enum-set=? options (file-options no-fail no-truncate))
                       'can-update]
                      [(enum-set-member? 'no-create options) ; no-create, no-create + no-fail
                       'must-truncate]
                      [(enum-set-member? 'no-fail options) ; no-fail
                       'truncate]
                      [else ; no-truncate, <empty>
                       'error])])
    (let ([p (with-handlers ([exn:fail:filesystem?
                              (lambda (exn)
                                (if (and (or (eq? exists-mode 'update)
                                             (eq? exists-mode 'must-truncate))
                                         (not (file-exists? filename)))
                                    (raise
                                     (make-exn:fail:filesystem:exists-not
                                      (exn-message exn)
                                      (exn-continuation-marks exn)
                                      filename))
                                    (raise exn)))])
               (open-output-file filename #:exists exists-mode))])
      (file-stream-buffer-mode p buffer-mode)
      (if maybe-transcoder
          (transcoded-port p maybe-transcoder)
          (wrap-binary-port p 
                            (and file-position (lambda () (file-position p)))
                            (and file-position (lambda (pos) (file-position p pos)))
                            #t)))))

(define (open-file-output-port filename 
                               [options (file-options)]
                               [buffer-mode 'block]
                               [maybe-transcoder #f])
  (do-open-file-output-port 'open-file-output-port
                            filename 
                            options
                            buffer-mode
                            maybe-transcoder
                            open-output-file
                            file-position
                            wrap-binary-output-port))

(define (open-bytevector-output-port [maybe-transcoder #f])
  (when maybe-transcoder
    (unless (transcoder? maybe-transcoder)
      (raise-type-error 'open-bytevector-output-port "transcoder or #f" maybe-transcoder)))
  (let* ([p (open-output-bytes)]
         [p2 (if maybe-transcoder
                 (transcoded-port p maybe-transcoder)
                 (wrap-binary-output-port p
                                          (lambda () (file-position p)) 
                                          (lambda (pos) (file-position p pos))
                                          #t))])
    (values
     p2
     (lambda () 
       (unless (port-closed? p2)
         (flush-output p2))
       (get-output-bytes p #t)))))

(define (call-with-bytevector-output-port proc [maybe-transcoder #f])
  (let-values ([(p get) (open-bytevector-output-port maybe-transcoder)])
    (proc p)
    (close-output-port p)
    (get)))

(define (open-string-output-port)
  (let ([p (open-output-string)])
    (values
     (transcoded-port p utf8-transcoder)
     (lambda ()
       (bytes->string/utf-8 (get-output-bytes p #t))))))

(define (call-with-string-output-port proc)
  (let-values ([(p get) (open-string-output-port)])
    (proc p)
    (close-output-port p)
    (get)))

(define standard-output-port
  (let ([p (current-output-port)])
    (lambda ()
      (wrap-binary-output-port p
                               (lambda () (file-position p)) 
                               (lambda (pos) (file-position p pos))
                               #f))))


(define standard-error-port
  (let ([p (current-error-port)])
    (lambda ()
      (wrap-binary-output-port p
                               (lambda () (file-position p)) 
                               (lambda (pos) (file-position p pos))
                               #f))))

(define output-ports (make-weak-hasheq))

(define (r6rs:current-output-port)
  (convert-output-port (current-output-port)))

(define (r6rs:current-error-port)
  (convert-output-port (current-error-port)))

(define (convert-output-port p)
  (cond
   [(textual-port? p) p]
   [(hash-ref output-ports p #f)
    => ephemeron-value]
   [else
    (let ([p2 (transcoded-port p utf8-transcoder)])
      (hash-set! output-ports p (make-ephemeron p p2))
      p2)]))

(define (make-custom-binary-output-port id write! get-position set-position! close)
  (wrap-binary-output-port
   (make-output-port
    id
    always-evt ;; assuming that it never blocks!
    (lambda (bytes start end can-block/buffer? enable-break?)
      (if (= start end)
          0
          (write! bytes start end)))
    (or close void)
    #f
    #f
    #f
    #f
    void
    1
    #f)
   get-position
   set-position!
   #t))
   
(define (make-custom-textual-output-port id write! get-position set-position! close)
  (make-textual-output-port
   (wrap-binary-output-port
    (make-output-port
     id
     always-evt ;; assuming that it never blocks!
     (let-values ([(in out) (make-pipe)]
                  [(c) #f]
                  [(cvt-buffer) #f]
                  [(buffer) #f])
       (lambda (bytes start end can-block/buffer? enable-break?)
         (let ([direct? (zero? (pipe-content-length in))])
           (if (and direct?
                    (bytes-utf-8-length bytes #f start end))
               ;; No old bytes saved, and bytes to write form a complete
               ;; UTF-8 encoding, so we can write directly:
               (let* ([s (bytes->string/utf-8 bytes #f start end)]
                      [len (string-length s)])
                 (when (positive? len)
                   (write! s 0 len)))
               ;; Partial or need to use existing bytes, so use pipe
               (begin
                 (write-bytes bytes out start end)
                 (unless buffer
                   (set! c (bytes-open-converter "UTF-8-permissive" "UTF-8"))
                   (set! buffer (make-bytes 4096))
                   (set! cvt-buffer (make-bytes 4096)))
                 (let loop ()
                   (let ([n (peek-bytes-avail!* buffer 0 in)])
                     (let ([more? ((pipe-content-length in) . > . n)])
                       (let-values ([(amt used status) (bytes-convert c buffer 0 n cvt-buffer)])
                         (when (positive? amt)
                           (read-bytes! buffer in 0 amt)
                           (let* ([s (bytes->string/utf-8 buffer #f 0 amt)]
                                  [len (string-length s)])
                             (when (positive? len)
                               (write! s 0 (string-length s)))))
                         (when (eq? status 'error)
                           ;; Discard an erroneous byte
                           (read-byte in))
                         ;; Loop 
                         (unless (and (eq? status 'complete)
                                      (not more?))
                           (loop)))))))))
         (- end start)))
     (or close void)
     #f
     #f
     #f
     #f
     void
     1
     #f)
    get-position
    set-position!
    #t)
   #f))

;; ----------------------------------------

(define (put-u8 port b)
  (unless (binary-port? port)
    (raise-type-error 'put-u8 "binary port" port))
  (write-byte b port))

(define (put-bytevector port bytes [start 0] [count (- (bytes-length bytes) start)])
  (unless (binary-port? port)
    (raise-type-error 'put-bytevector "binary port" port))
  (write-bytes bytes port start (+ start count)))

(define (put-char port ch)
  (unless (textual-port? port)
    (raise-type-error 'put-u8 "textual port" port))
  (write-char ch port))

(define (put-string port str [start 0] [count (- (string-length str) start)])
  (unless (textual-port? port)
    (raise-type-error 'put-string "textual port" port))
  (write-string (substring str start (+ start count)) port))

(define (put-datum port v)
  (unless (textual-port? port)
    (raise-type-error 'put-datum "textual port" port))
  (parameterize ([print-mpair-curly-braces #f]
                 [pretty-print-columns 'infinity]
                 [pretty-print-size-hook
                  (lambda (v write? p)
                    (cond
                     [(symbol? v)
                      (let ([s (symbol->string v)])
                        (and (not (regexp-match (force rx:id) s))
                             (for/fold ([len 0])
                                 ([c (in-string s)]
                                  [pos (in-naturals)])
                               (+ len 
                                  (if (or (char-alphabetic? c)
                                          (and (char-numeric? c)
                                               (positive? pos)))
                                      1
                                      (+ 3 (string-length 
                                            (number->string (char->integer c) 16))))))))]
                     [(string? v) 
                      (and (for/or ([c (in-string v)])
                                   (not (or (char-graphic? c)
                                            (char-blank? c))))
                           (for/fold ([w 2])
                               ([c (in-string v)])
                             (cond
                              [(eq? c #\") 2]
                              [(eq? c #\\) 2]
                              [(char-graphic? c) 1]
                              [(char-blank? c) 1]
                              [(eq? c #\newline) 2]
                              [(eq? c #\return) 2]
                              [else 9])))]
                     [(char? v)
                      (case v
                        [(#\u7) 7] ; #\alarm
                        [(#\u1B) 5] ; #\esc
                        [(#\u7F) 8] ; #\delete
                        [else (and (not (char-graphic? v))
                                   (+ 3
                                      (if ((char->integer v) . < . #x10000)
                                          4
                                          6)))])]
                     [(bytes? v) (+ 5
                                    (sub1 (bytes-length v))
                                    (for/fold ([len 0])
                                        ([b (in-bytes v)])
                                      (+ len (cond
                                              [(b . < . 10) 1]
                                              [(b . < . 100) 2]
                                              [else 3]))))]
                     [else #f]))]
                 [pretty-print-print-hook
                  (lambda (v write? p)
                    (cond
                     [(symbol? v)
                      (for ([c (in-string (symbol->string v))]
                            [pos (in-naturals)])
                        (if (or (char-alphabetic? c)
                                (and (char-numeric? c)
                                     (positive? pos)))
                            (display c p)
                            (begin
                              (display "\\x" p)
                              (display (number->string (char->integer c) 16) p)
                              (display ";" p))))]
                     [(string? v) 
                      (write-char #\" p)
                      (for ([c (in-string v)])
                        (cond
                         [(eq? c #\") (display "\\\"" p)]
                         [(eq? c #\\) (display "\\\\" p)]
                         [(char-graphic? c) (write-char c p)]
                         [(char-blank? c) (write-char c p)]
                         [(eq? c #\newline) (display "\\n" p)]
                         [(eq? c #\return) (display "\\r" p)]
                         [else
                          (display "\\x" p)
                          (let ([s (format "00000~x" (char->integer c))])
                            (display (substring s (- (string-length s) 6)) p)
                            (write-char #\; p))]))
                      (write-char #\" p)]
                     [(char? v)
                      (case v
                        [(#\u7) (display "#\\alarm" p)]
                        [(#\u1B) (display "#\\esc" p)]
                        [(#\u7F) (display "#\\delete" p)]
                        [else 
                         (display "#\\x" p)
                         (let ([n (number->string (char->integer v) 16)])
                           (display (make-string
                                     (- (if ((string-length n) . <= . 4)
                                            4
                                            6)
                                        (string-length n))
                                     #\0)
                                    p)
                           (display n p))])]
                     [(bytes? v)
                      (display "#vu8(" p)
                      (if (zero? (bytes-length v))
                          (display ")" p)
                          (begin
                            (display (bytes-ref v 0) p)
                            (for ([b (in-bytes v)]
                                  [i (in-naturals)])
                              (unless (zero? i)
                                (display " " p)
                                (display b p)))
                            (display ")" p)))]))])
    (pretty-write v port)))

;; ----------------------------------------

(define (open-file-input/output-port filename 
                                     [options (file-options)]
                                     [buffer-mode 'block]
                                     [maybe-transcoder #f])
  (do-open-file-output-port 'open-file-input/output-port
                            filename 
                            options
                            (if (eq? buffer-mode 'line)
                                'block
                                buffer-mode)
                            maybe-transcoder
                            (lambda (name #:exists mode)
                              (let-values ([(in out) (open-input-output-file name #:exists mode)])
                                (file-stream-buffer-mode out buffer-mode)
                                (make-dual-port in out)))
                            ;; Input and output buffering make `file-position' iffy.
                            (if (eq? buffer-mode 'none)
                                (case-lambda
                                 [(p) (file-position (dual-port-in p))]
                                 [(p pos) 
                                  (flush-output p)
                                  (file-position (dual-port-in p) pos)])
                                #f)
                            wrap-binary-input/output-port))

(define (make-make-custom-input/output-port
         make-custom-input-port
         make-custom-output-port)
  (lambda (id read! write! get-pos set-pos! close)
    (let* ([closed-one? #f]
           [close (and close
                       (lambda ()
                         (if closed-one?
                             (close)
                             (set! closed-one? #t))))])
      (let ([in (make-custom-input-port id read! get-pos set-pos! close)]
            [out (make-custom-output-port id write! get-pos set-pos! close)])
        (make-dual-port in out)))))

(define make-custom-binary-input/output-port
  (make-make-custom-input/output-port
   make-custom-binary-input-port
   make-custom-binary-output-port))

(define make-custom-textual-input/output-port
  (make-make-custom-input/output-port
   make-custom-textual-input-port
   make-custom-textual-output-port))

(define (bytevector->string bv t)
  (unless (transcoder? t)
    (raise-type-error 'bytevector->string "transcoder" t))
  (let ([p #f])
    (dynamic-wind
     (lambda () 
       (set! p (open-bytevector-input-port bv t)))
     (lambda ()
       (apply
        string-append
        (let loop ()
          (let ([s (get-string-n p 4096)])
            (if (eof-object? s)
                null
                (cons s (loop)))))))
     (lambda () (close-input-port p)))))
                   
(define (string->bytevector s t)
  (unless (transcoder? t)
    (raise-type-error 'string->bytevector "transcoder" t))
  (let ([p #f]
        [result #f])
    (dynamic-wind
     (lambda () 
       (set!-values (p result) (open-bytevector-output-port t)))
     (lambda ()
       (put-string p s)
       (result))
     (lambda () (close-output-port p)))))

;; ----------------------------------------

(define (r6rs-port->port p)
  (cond [(binary-input-port? p)
         ((binary-input-port-disconnect p))]
        [(binary-output-port? p)
         ((binary-output-port-disconnect p))]
        [else
         (raise-type-error 'r6rs-port->port "binary input or output port" p)]))
