#lang racket/base

(provide emit
         (struct-out vertical-mode)
         (struct-out separated)
         (struct-out grouped))

(require racket/match)
(require (only-in racket/string string-trim))
(require (only-in racket/pretty pretty-print-columns))
(require (only-in racket/port call-with-output-string))

;; mode ∈ {'never, 'variable, 'always, 'extra-newline}
;;
;; Things that don't know how to write themselves vertically always write horizontally.
;; Otherwise, mode is relevant:
;;
;;  - 'never: always horizontally
;;  - 'variable: horizontally, unless it doesn't fit, in which case vertically
;;  - 'always: always vertically
;;  - 'extra-newline: as 'normal, but with extra newlines separating items
;;
(struct vertical-mode (mode item) #:transparent)

(struct separated (items separator terminator) #:transparent)
(struct grouped (items separator terminator opener closer) #:transparent)

(struct emitter (port indentation) #:transparent)

(define (emit-seq op vs sep ter cl e indent mode)
  (define p (emitter-port e))
  (when op (display op p))
  (let loop ((vs vs) (need-sep #f))
    (match vs
      ['()
       (when need-sep (display ter p))
       (when cl (display cl p))]
      [(cons v vs)
       (when need-sep (display sep p))
       (emit* v e indent mode)
       (loop vs #t)])))

(define (emit-vertical op vs sep ter cl e indent mode)
  (define inner-indent (if op (string-append indent (emitter-indentation e)) indent))
  (let ((sep (string-append (trim-delimiter-right sep) inner-indent))
        (ter (trim-delimiter-right ter))
        (op (and op (string-append (trim-delimiter-right op) inner-indent)))
        (cl (and cl (string-append indent (trim-delimiter-left cl)))))
    (emit-seq op vs sep ter cl e inner-indent mode)))

(define (trim-delimiter-right s)
  (string-trim #:left? #f s #px"[ \t\f\v]+"))

(define (trim-delimiter-left s)
  (string-trim #:right? #f s #px"[ \t\f\v]+"))

(define (try-horizontal op vs sep ter cl e indent mode)
  (call-with-output-string
   (lambda (p)
     (emit-seq op vs sep ter cl (emitter p (emitter-indentation e)) indent 'never))))

(define (emit-separated op vs sep ter cl e indent mode)
  (match mode
    ['never (emit-seq op vs sep ter cl e indent mode)]
    ['variable (let ((s (try-horizontal op vs sep ter cl e indent mode)))
                 (if (> (string-length s) (pretty-print-columns))
                     (emit-vertical op vs sep ter cl e indent mode)
                     (display s (emitter-port e))))]
    ['always (emit-vertical op vs sep ter cl e indent mode)]
    ['extra-newline (let ((sep (string-append sep "\n")))
                      (emit-vertical op vs sep ter cl e indent mode))]))

(define (emit* v e indent mode)
  (define (w v) (emit* v e indent mode))
  (match v
    [(? string? s) (display s (emitter-port e))]
    [(? list? vs) (map w vs)]
    [(separated vs sep ter) (emit-separated #f vs sep ter #f e indent mode)]
    [(grouped vs sep ter op cl) (emit-separated op vs sep ter cl e indent mode)]
    [(vertical-mode new-mode v) (emit* v e indent new-mode)]))

(define (emit v [o (current-output-port)]
              #:mode [initial-mode 'variable]
              #:indentation [indentation0 "    "]
              #:initial-indent [initial-indent "\n"])
  ;; (local-require racket/pretty) (pretty-write v)
  (define indentation (if (number? indentation0) (make-string indentation0 #\space) indentation0))
  (emit* v (emitter o indentation) initial-indent initial-mode))
