#lang racket

(require scribble/decode
         (except-in scribble/struct
                    element?)
         (only-in scribble/core
                  content?
                  element?
                  make-style
                  make-table-columns)
         )

(provide (contract-out
          [BNF (-> (cons/c (or/c block? content?)
                           (non-empty-listof (or/c block? content?)))
                   ...
                   table?)]
          [BNF-etc element?]
          ;; operate on content
          [BNF-seq (-> content? ...
                       (or/c element? ""))]
          [BNF-seq-lines (-> (listof content?) ...
                             block?)]
          [BNF-alt (-> content? ...
                       element?)]
          [BNF-alt/close (-> content? ...
                             element?)]
          ;; operate on pre-content
          [BNF-group (-> pre-content? ...
                         element?)]
          [nonterm (-> pre-content? ...
                       element?)]
          [optional (-> pre-content? ...
                        element?)]
          [kleenestar (-> pre-content? ...
                          element?)]
          [kleeneplus (-> pre-content? ...
                          element?)]
          [kleenerange (-> any/c any/c pre-content? ...
                           element?)]
          ))


(define spacer (make-element 'hspace (list " ")))
(define equals (make-element 'tt (list spacer "::=" spacer)))
(define alt (make-element 'tt (list spacer spacer "|" spacer spacer)))

(define (as-flow i) (make-flow (list (if (block? i)
                                         i
                                         (make-paragraph (list i))))))


(define baseline (make-style #f '(baseline)))

(define (BNF . defns)
  (make-table
   (make-style #f
               (list
                (make-table-columns
                 (list baseline baseline baseline baseline))))
   (apply
    append
    (map (match-lambda
           [(cons lhs (cons rhs0 more-rhs))
            (cons
             (list (as-flow spacer) (as-flow lhs) (as-flow equals) (as-flow rhs0))
             (map (lambda (i)
                    (list (as-flow spacer) (as-flow " ") (as-flow alt) (as-flow i)))
                  more-rhs))])
         defns))))

;; interleave : (listof content?) element? -> element?
(define (interleave l spacer)
  (make-element #f (cons (car l)
                         (apply append
                                (map (lambda (i)
                                       (list spacer i))
                                     (cdr l))))))

(define (BNF-seq . l)
  (if (null? l)
      ""
      (interleave l spacer)))

(define (BNF-seq-lines . l)
  (make-table #f (map (lambda (row) (list (as-flow (apply BNF-seq row))))
                      l)))

(define (BNF-alt . l)
  (interleave l alt))

(define (BNF-alt/close . l)
  (interleave l (make-element 'roman " | ")))

(define BNF-etc (make-element 'roman "..."))

(define (nonterm . s)
  (make-element 'roman (append (list 'lang)
                               (list (make-element 'italic (decode-content s)))
                               (list 'rang))))

(define (optional . s)
  (make-element #f (append (list (make-element 'roman "["))
                           (decode-content s)
                           (list (make-element 'roman "]")))))

(define (BNF-group . s)
  (make-element #f (append (list (make-element 'roman "{"))
                           (list (apply BNF-seq (decode-content s)))
                           (list (make-element 'roman "}")))))

(define (kleenestar . s)
  (make-element #f (append (decode-content s) (list (make-element 'roman "*")))))

(define (kleeneplus . s)
  (make-element #f (append (decode-content s) (list (make-element 'superscript (list "+"))))))

(define (kleenerange a b . s)
  (make-element #f (append (decode-content s)
                           (list (make-element 'roman
                                               (make-element 'superscript
                                                             (list (format "{~a,~a}" a b))))))))
