;;;; fmt-block.scm -- columnar formatting
;;
;; Copyright (c) 2006-2011 Alex Shinn.  All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Columnar formatting
;;
;; A line-oriented formatter.  Takes a list of
;;   (line-fmt1 gen-fmt1 line-fmt2 gen-fmt2 ...)
;; and formats each of the gen-fmt1 formats as columns, printed
;; side-by-side, each line allowing post-processing done by line-fmt1
;; (just use dsp if you want to display the lines verbatim).

;; Continuations come to the rescue to make this work properly,
;; letting us weave the output between different columns without
;; needing to build up intermediate strings.

(define (fmt-columns . ls)
  (lambda (orig-st)
    (call-with-current-continuation
     (lambda (return)
       (define (infinite? x)
         (and (pair? x) (pair? (cdr x)) (pair? (cddr x)) (caddr x)))
       (let ((q1 '())
             (q2 '())
             (remaining (length (remove infinite? ls))))
         (define (enq! proc) (set! q2 (cons proc q2)))
         (define (deq!) (let ((proc (car q1))) (set! q1 (cdr q1)) proc))
         (define (line-init!) (set! q1 (reverse q2)) (set! q2 '()))
         (define (line-done?) (null? q1))
         (define line-buf '())
         (define line-non-empty? #f)
         (define (write-column fmt str finite?)
           (set! line-buf (cons (cons fmt str) line-buf))
           (if finite? (set! line-non-empty? #t)))
         (define (write-line)
           (cond
            (line-non-empty?
             (for-each
              (lambda (x) (set! orig-st (((car x) (cdr x)) orig-st)))
              (reverse line-buf))
             (set! orig-st (nl orig-st))))
           (set! line-buf '())
           (set! line-non-empty? #f)
           (line-init!))
         (define (next cont)
           (enq! cont)
           (cond
            ((line-done?) 
             (write-line)
             (if (not (positive? remaining)) (finish) ((deq!) #f)))
            (else ((deq!) #f))))
         (define (finish)
           (write-line)
           (return orig-st))
         (define (make-empty-col fmt)
           (define (blank *ignored*)
             (write-column fmt "" #f)
             (next blank))    ; infinite loop, next terminates for us
           blank)
         (define (make-col st fmt gen finite?)
           (let ((acc '()))            ; buffer incomplete lines
             (lambda (*ignored*)
               (define (output* str st)
                 (let lp ((i 0))
                   (let ((nli (string-index str #\newline i)))
                     (cond
                      (nli
                       (let ((line
                              (string-concatenate-reverse
                               (cons (substring/shared str i nli) acc))))
                         (set! acc '())
                         (write-column fmt line finite?)
                         (call-with-current-continuation next) 
                         (lp (+ nli 1))))
                      (else
                       (set! acc (cons (substring/shared str i) acc))))))
                 ;; update - don't output or the string port will fill up
                 (fmt-update str st))
               ;; gen threads through it's own state, ignore result
               (gen (fmt-set-writer! (copy-fmt-state st) output*))
               ;; reduce # of remaining finite columns
               (set! remaining (- remaining 1))
               ;; write any remaining accumulated output
               (if (pair? acc)
                   (let ((s (string-concatenate-reverse acc)))
                     (write-column fmt s (and finite? (not (equal? s ""))))))
               ;; (maybe) loop with an empty column in place
               (if (not (positive? remaining))
                   (finish)
                   (next (make-empty-col fmt))))))
         ;; queue up the initial formatters
         (for-each
          (lambda (col)
            (let ((st (fmt-set-port! (copy-fmt-state orig-st)
                                     (open-output-string))))
              (enq! (make-col st (car col) (dsp (cadr col))
                              (not (infinite? col))))))
          ls)
         (line-init!)
         ;; start
         ((deq!) #f))))))

(define (columnar . ls)
  (define (proportional-width? w)
    (and (number? w)
         (or (< 0 w 1)
             (and (inexact? w) (= w 1.0)))))
  (define (whitespace-pad? st)
    (char-whitespace? (or (fmt-pad-char st) #\space)))
  (define (build-column ls)
    (let-optionals* ls ((fixed-width #f)
                        (width #f)
                        (last? #t)
                        (tail '())
                        (gen #f)
                        (prefix '())
                        (align 'left)
                        (infinite? #f))
      (define (scale-width st)
        (max 1 (inexact->exact
                (truncate (* width (- (fmt-width st) fixed-width))))))
      (define (padder)
        (if (proportional-width? width)
            (case align
              ((right)
               (lambda (str) (lambda (st) ((pad/left (scale-width st) str) st))))
              ((center)
               (lambda (str) (lambda (st) ((pad/both (scale-width st) str) st))))
              (else
               (lambda (str) (lambda (st) ((pad/right (scale-width st) str) st)))))
            (case align
              ((right) (lambda (str) (pad/left width str)))
              ((center) (lambda (str) (pad/both width str)))
              (else (lambda (str) (pad/right width str))))))
      (define (affix x)
        (cond
         ((pair? tail)
          (lambda (str)
            (cat (string-concatenate prefix)
                 (x str)
                 (string-concatenate tail))))
         ((pair? prefix)
          (lambda (str) (cat (string-concatenate prefix) (x str))))
         (else x)))
      (list
       ;; line formatter
       (affix
        (let ((pad (padder)))
          (if (and last? (not (pair? tail)) (eq? align 'left))
              (lambda (str)
                (lambda (st)
                  (((if (whitespace-pad? st) dsp pad) str) st)))
              pad)))
       ;; generator
       (if (proportional-width? width)
           (lambda (st) ((with-width (scale-width st) gen) st))
           (with-width width gen))
       infinite?
       )))
  (define (adjust-widths ls border-width)
    (let* ((fixed-ls
            (filter (lambda (x) (and (number? (car x)) (>= (car x) 1))) ls))
           (fixed-total (fold + border-width (map car fixed-ls)))
           (scaled-ls (filter (lambda (x) (proportional-width? (car x))) ls))
           (denom (- (length ls) (+ (length fixed-ls) (length scaled-ls))))
           (rest (if (zero? denom)
                     0
                     (exact->inexact
                      (/ (- 1 (fold + 0 (map car scaled-ls))) denom)))))
      (if (negative? rest)
          (error 'columnar "fractional widths must sum to less than 1"
                 (map car scaled-ls)))
      (map
       (lambda (col)
         (cons fixed-total
               (if (not (number? (car col))) (cons rest (cdr col)) col)))
       ls)))
  (define (finish ls border-width)
    (apply fmt-columns
           (map build-column (adjust-widths (reverse ls) border-width))))
  (let lp ((ls ls) (strs '()) (align 'left) (infinite? #f)
           (width #t) (border-width 0) (res '()))
    (cond
     ((null? ls)
      (if (pair? strs)
          (finish (cons (cons (caar res)
                              (cons #t (cons (append (reverse strs)
                                                     (caddar res))
                                             (cdddar res))))
                        (cdr res))
                  border-width)
          (finish (cons (cons (caar res) (cons #t (cddar res))) (cdr res))
                  border-width)))
     ((string? (car ls))
      (if (string-index (car ls) #\newline)
          (error 'columnar "column string literals can't contain newlines")
          (lp (cdr ls) (cons (car ls) strs) align infinite?
              width (+ border-width (string-length (car ls))) res)))
     ((number? (car ls))
      (lp (cdr ls) strs align infinite? (car ls) border-width res))
     ((eq? (car ls) 'infinite)
      (lp (cdr ls) strs align #t width border-width res))
     ((symbol? (car ls))
      (lp (cdr ls) strs (car ls) infinite? width border-width res))
     ((procedure? (car ls))
      (lp (cdr ls) '() 'left #f #t border-width
          (cons (list width #f '() (car ls) (reverse strs) align infinite?)
                res)))
     (else
      (error 'columnar "invalid column" (car ls))))))

(define (max-line-width string-width str)
  (let lp ((i 0) (hi 0))
    (let ((j (string-index str #\newline i)))
      (if j
          (lp (+ j 1) (max hi (string-width (substring str i j))))
          (max hi (string-width (substring str i (string-length str))))))))

(define (pad-finite st proc width)
  (let* ((str ((fmt-to-string proc) (copy-fmt-state st)))
         (w (max-line-width (or (fmt-string-width st) string-length) str)))
    (list (cat str)
          (if (and (integer? width) (exact? width))
              (max width w)
              w))))

(define (tabular . ls)
  (lambda (st)
    (let lp ((ls ls) (infinite? #f) (width #t) (res '()))
      (cond
       ((null? ls)
        ((apply columnar (reverse res)) st))
       ((number? (car ls))
        (lp (cdr ls) infinite? (car ls) res))
       ((eq? 'infinite (car ls))
        (lp (cdr ls) #t width (cons (car ls) res)))
       ((procedure? (car ls))
        (if infinite?
            (if width
                (lp (cdr ls) #f #t (cons (car ls) (cons width res)))
                (lp (cdr ls) #f #t (cons (car ls) res)))
            (let ((gen+width (pad-finite st (car ls) width)))
              (lp (cdr ls) #f #t (append gen+width res)))))
       (else
        (lp (cdr ls) infinite? width (cons (car ls) res)))))))

;; break lines only, don't fmt-join short lines or justify
(define (fold-lines . ls)
  (lambda (st)
    (define output (fmt-writer st))
    (define (kons-in-line str st)
      (let ((len ((or (fmt-string-width st) string-length) str))
            (space (- (fmt-width st) (fmt-col st))))
        (cond
          ((or (<= len space) (not (positive? space)))
           (output str st))
          (else
           (kons-in-line
            (substring/shared str space len)
            (output nl-str
                    (output (substring/shared str 0 space) st)))))))
    ((fmt-let
      'writer
      (lambda (str st)
        (let lp ((str str) (st st))
          (let ((nli (string-index str #\newline)))
            (cond
              ((not nli)
               (kons-in-line str st))
              (else
               (lp (substring/shared str (+ nli 1))
                   (output nl-str
                           (kons-in-line
                            (substring/shared str 0 nli)
                            st))))))))
      (apply-cat ls))
     st)))

(define (wrap-fold-words seq knil max-width get-width line . o)
  (let* ((last-line (if (pair? o) (car o) line))
         (vec (if (list? seq) (list->vector seq) seq))
         (len (vector-length vec))
         (len-1 (- len 1))
         (breaks (make-vector len #f))
         (penalties (make-vector len #f))
         (widths
          (list->vector
           (map get-width (if (list? seq) seq (vector->list vec))))))
    (define (largest-fit i)
      (let lp ((j (+ i 1)) (width (vector-ref widths i)))
        (let ((width (+ width 1 (vector-ref widths j))))
          (cond
            ((>= width max-width) (- j 1))
            ((>= j len-1) len-1)
            (else (lp (+ j 1) width))))))
    (define (min-penalty! i)
      (cond
        ((>= i len-1) 0)
        ((vector-ref penalties i))
        (else
         (vector-set! penalties i (expt (+ max-width 1) 3))
         (vector-set! breaks i i)
         (let ((k (largest-fit i)))
           (let lp ((j i) (width 0))
             (if (<= j k)
                 (let* ((width (+ width (vector-ref widths j)))
                        (break-penalty
                         (+ (max 0 (expt (- max-width (+ width (- j i))) 3))
                            (min-penalty! (+ j 1)))))
                   (cond
                     ((< break-penalty (vector-ref penalties i))
                      (vector-set! breaks i j)
                      (vector-set! penalties i break-penalty)))
                   (lp (+ j 1) width)))))
         (if (>= (vector-ref breaks i) len-1)
             (vector-set! penalties i 0))
         (vector-ref penalties i))))
    (define (sub-list i j)
      (let lp ((i i) (res '()))
        (if (> i j)
            (reverse res)
            (lp (+ i 1) (cons (vector-ref vec i) res)))))
    (cond
     ((zero? len)
      ;; degenerate case
      (last-line '() knil))
     (else
      ;; compute optimum breaks
      (vector-set! breaks len-1 len-1)
      (vector-set! penalties len-1 0)
      (min-penalty! 0)
      ;; fold
      (let lp ((i 0) (acc knil))
        (let ((break (vector-ref breaks i)))
          (if (>= break len-1)
              (last-line (sub-list i len-1) acc)
              (lp (+ break 1) (line (sub-list i break) acc)))))))))

;; XXXX don't split, traverse the string manually and keep track of
;; sentence endings so we can insert two spaces
(define (wrap-fold str . o)
  (apply wrap-fold-words (string-tokenize str) o))

(define (wrap-lines . ls)
  (define (print-line ls st)
    (nl ((fmt-join dsp ls " ") st)))
  (define buffer '())
  (lambda (st)
    ((fmt-let
      'writer
      (lambda (str st) (set! buffer (cons str buffer)) st)
      (apply-cat ls))
     st)
    (wrap-fold (string-concatenate-reverse buffer)
               st (fmt-width st)
               (or (fmt-string-width st) string-length)
               print-line)))

(define (justify . ls)
  (lambda (st)
    (let ((width (fmt-width st))
          (string-width (or (fmt-string-width st) string-length))
          (output (fmt-writer st))
          (buffer '()))
      (define (justify-line ls st)
        (if (null? ls)
            (nl st)
            (let* ((sum (fold (lambda (s n) (+ n (string-width s))) 0 ls))
                   (len (length ls))
                   (diff (max 0 (- width sum)))
                   (sep (make-string (if (= len 1)
                                         0
                                         (quotient diff (- len 1)))
                                     #\space))
                   (rem (if (= len 1)
                            diff
                            (remainder diff (- len 1)))))
              (output
               (call-with-output-string
                 (lambda (p)
                   (display (car ls) p)
                   (let lp ((ls (cdr ls)) (i 1))
                     (cond
                       ((pair? ls)
                        (display sep p)
                        (if (<= i rem) (write-char #\space p))
                        (display (car ls) p)
                        (lp (cdr ls) (+ i 1)))))
                   (newline p)))
               st))))
      (define (justify-last ls st)
        (nl ((fmt-join dsp ls " ") st)))
      ((fmt-let
        'writer
        (lambda (str st) (set! buffer (cons str buffer)) st)
        (apply-cat ls))
       st)
      (wrap-fold (string-concatenate-reverse buffer)
                 st width string-width justify-line justify-last))))

(define (fmt-file path)
  (lambda (st)
    (call-with-input-file path
      (lambda (p)
        (let lp ((st st))
          (let ((line (read-line p)))
            (if (eof-object? line)
                st
                (lp (nl ((dsp line) st))))))))))

(define (line-numbers . o)
  (let ((start (if (pair? o) (car o) 1)))
    (fmt-join/range dsp start #f nl-str)))

