#lang racket/base
(require racket/contract
         redex/reduction-semantics
         redex/pict
         slideshow/pict
         racket/list)

;; TO DO:
;; - chained binary operators
;;   eg (+ 1 2 3) => "1 + 2 + 3"
;; - helper for thin spaces around operators/functions ?
;; - rewriters for more conventions (eg subst, env, judgments, ...)

(provide
 (contract-out
  ;; Using and controlling rewriters
  [with-rewriters
   (-> (-> any) any)]
  [current-atomic-rewriters
   (parameter/c (plistof symbol? atomic-rewriter/c))]
  [current-compound-rewriters
   (parameter/c (plistof symbol? compound-rewriter/c))]
  [current-unquote-rewriters
   (parameter/c (plistof (-> lw? any/c) (-> lw? lw?)))]
  [add-atomic-rewriters!
   (->* [] [] #:rest (plistof symbol? atomic-rewriter/c)
        void?)]
  [add-compound-rewriters!
   (->* [] [] #:rest (plistof symbol? compound-rewriter/c)
        void?)]
  [add-unquote-rewriters!
   (->* [] [] #:rest (plistof (-> lw? any/c) (-> lw? lw?))
        void?)]
  ;; Rewriter constructors
  [only-first-rw
   (-> compound-rewriter/c)]
  [binary-rw
   (->* [content/c]
        [#:parenthesize-arg (or/c #t #f (listof symbol?) (-> lw? any/c))
         #:parenthesize-left (or/c #t #f (listof symbol?) (-> lw? any/c))
         #:parenthesize-right (or/c #t #f (listof symbol?) (-> lw? any/c))]
        compound-rewriter/c)]
  [prefix-rw
   (-> content/c
       compound-rewriter/c)]
  [postfix-rw
   (-> content/c
       compound-rewriter/c)]
  [function-rw
   (-> content/c
       compound-rewriter/c)]
  [splice-rw
   (-> compound-rewriter/c)]
  [constant-rw
   (-> content/c
       compound-rewriter/c)]
  [bracket-rw
   (->* [(or/c 'round 'square 'curly 'angle (list/c (or/c string? pict?) (or/c string? pict?)))]
        [#:comma? any/c]
        compound-rewriter/c)]
  [set-cons-rw
   (-> compound-rewriter/c)]
  ;; Contracts
  [compound-rewriter/c
   contract?]
  [plistof
   (-> contract? contract? contract?)]
  #|
  [content/c
   contract?]
  |#))

(define (plistof key/c val/c)
  (letrec ([ctc
            (recursive-contract
             (or/c '()
                   (cons/c key/c (cons/c val/c ctc))))])
    ctc))

(define content/c
  (or/c string? pict? lw? (-> (or/c string? pict? lw?))))

(define atomic-rewriter/c
  (or/c string? pict? (-> (or/c string? pict?))))

(define compound-rewriter/c
  (-> (listof lw?)
      (listof (or/c string? pict? lw?))))

;; ============================================================

(define current-atomic-rewriters (make-parameter null))
(define current-compound-rewriters (make-parameter null))
(define current-unquote-rewriters (make-parameter null))

(define (add-atomic-rewriters! . args)
  (push-rewriters! 'add-atomic-rewriters! current-atomic-rewriters args))
(define (add-compound-rewriters! . args)
  (push-rewriters! 'add-compound-rewrites! current-compound-rewriters args))
(define (add-unquote-rewriters! . args)
  (push-rewriters! 'add-unquote-rewrites! current-unquote-rewriters args))

(define (with-rewriters thunk)
  (with-atomic-rewriter*
   (lambda ()
     (with-compound-rewriter*
      (lambda ()
        (with-unquote-rewriter*
         (lambda ()
           (thunk))
         (reverse (current-unquote-rewriters))))
      (reverse (current-compound-rewriters))))
   (reverse (current-atomic-rewriters))))

;; ============================================================

(define (push-rewriters! who param args)
  (unless (even? (length args))
    (error who "expected even number of arguments, got: ~e" args))
  (param (append args (param))))

;; Note: l is reversed, so rewriter comes first, symbol/pred comes second.
(define (with-atomic-rewriter* thunk l)
  (if (null? l)
      (thunk)
      (with-atomic-rewriter
       (cadr l)
       (let ([rw (car l)])
         (cond [(string? rw)
                (lambda () (text rw (default-style) (default-font-size)))]
               [(pict? rw)
                (lambda () rw)]
               [else rw]))
       (with-atomic-rewriter* thunk (cddr l)))))
(define (with-compound-rewriter* thunk l)
  (if (null? l)
      (thunk)
      (with-compound-rewriter
       (cadr l) (car l)
       (with-compound-rewriter* thunk (cddr l)))))
(define (with-unquote-rewriter* thunk l)
  (with-unquote-rewriter
   (lambda (lw)
     (let loop ([l l])
       (cond [(null? l)
              lw]
             [else
              (let ([pred (cadr l)]
                    [tx (car l)])
                (cond [(pred lw) (tx lw)]
                      [else (loop (cddr l))]))])))
   (thunk)))

;; ============================================================

(define (only-first-rw)
  (compound-rw 'only-first-rw
    (lambda (lws)
      (let ([arg-lw (list-ref lws 2)])
        (list arg-lw)))))

(define (binary-rw op
                   #:parenthesize-arg [parenthesize-arg #f]
                   #:parenthesize-left [parenthesize-left parenthesize-arg]
                   #:parenthesize-right [parenthesize-right parenthesize-arg])
  (compound-rw 'binary-rw
    (lambda (lws)
      (let ([left (list-ref lws 2)]
            [right (list-ref lws 3)])
        ;; (list left (just-after op left) (between "" left right) right)
        ;; (list left (just-after op left) "" right)
        (append (maybe-parenthesize left parenthesize-left)
                (list (between (->string/pict op) left right))
                (maybe-parenthesize right parenthesize-right))))
    2 2))

(define (prefix-rw pre
                   #:parenthesize-arg [parenthesize-arg #f])
  (compound-rw 'prefix-rw
    (lambda (lws)
      (let ([arg-lw (list-ref lws 2)])
        (append (list (just-before (->string/pict pre) arg-lw))
                (maybe-parenthesize arg-lw parenthesize-arg))))
    1 1))

(define (postfix-rw post
                    #:parenthesize-arg [parenthesize-arg #f])
  (compound-rw 'postfix-rw
    (lambda (lws)
      (let ([arg-lw (list-ref lws 2)])
        (append (maybe-parenthesize arg-lw parenthesize-arg)
                (list (just-after (->string/pict post) arg-lw)))))
    1 1))

(define (function-rw name)
  (compound-rw 'function-rw
    (lambda (lws)
      (list* (re-lw (->string/pict name) (list-ref lws 1))
             (between "(" (list-ref lws 1) (list-ref lws 2))
             (comma-ize (cddr lws) #t)))))

(define (bracket-rw brackets #:tall? [tall? #f] #:comma? [comma? #t])
  (let-values ([(left-bracket right-bracket)
                (cond [(symbol? brackets)
                       (case brackets
                         ((round) (values "(" ")"))
                         ((square) (values "[" "]"))
                         ((curly) (values "{" "}"))
                         ((angle) (values "〈" "〉"))
                         ;; FIXME: more
                         (else (error 'bracket-rw "unknown bracket kind: ~e" brackets)))]
                      [(list? brackets)
                       (values (first brackets) (second brackets))])])
    (compound-rw 'bracket-rw
      (lambda (lws)
        (let ([lwA (first lws)]
              [lwZ (last lws)]
              [elems (drop-right (drop lws 2) 1)])
          (append (list (re-lw (if tall? (taller left-bracket) left-bracket) lwA)
                        "")
                  (if comma? (comma-ize elems #f) elems)
                  (list (re-lw (if tall? (taller right-bracket) right-bracket) lwZ))))))))

(define (splice-rw)
  (compound-rw 'splice-rw
    (lambda (lws)
      (drop-right (drop lws 2) 1))))

(define (constant-rw s)
  (compound-rw 'constant-rw
    (lambda (lws)
      (list (re-lw s (first lws))))))

(define (set-cons-rw)
  (compound-rw 'set-cons-rw
    (lambda (lws)
      (list (just-before "{" (list-ref lws 2))
            (list-ref lws 2)
            (just-after "}∪" (list-ref lws 2))
            (between "" (list-ref lws 2) (list-ref lws 3))
            (list-ref lws 3)))))                                  

;; ============================================================

;; Content = (U string symbol pict (listof (U 'spring lw)))

;; between : Content lw lw -> lw
;; Makes an lw with given content and location between given lws.
(define (between s a b)
  (build-lw s
            (lw-line a)
            0
            (+ (lw-column a) (lw-column-span a))
            (max 0 (- (lw-column b)
                      (+ (lw-column a) (lw-column-span a))))))

;; re-lw : Content lw -> lw
;; Makes an lw with locations of old lw and new content.
(define (re-lw new-e lw)
  (build-lw new-e
            (lw-line lw) (lw-line-span lw)
            (lw-column lw) (lw-column-span lw)))

;; refit : (listof lw) (listof lw) -> (listof lw)
;; Add empty lws ("") around new based on locations of old lws (??)
;; Soaks up logical space ????
(define (refit orig new)
  (append (list (between "" (just-before "" (car orig)) (car new)))
          new
          (list (between "" (just-after "" (last new)) (just-after "" (last orig))))))

(define (compound-rw who proc [min-args 0] [max-args +inf.0]
                     #:refit? [refit? #t])
  ((if refit? refit-rw values)
   (lambda (lws)
     (unless (>= (length lws) 3)
       (error who "expected list of at least 3 lws, got: ~e" lws))
     (let ([lwA (first lws)]
           [lwB (second lws)]
           [lwZ (last lws)])
       (unless (member (lw-e lwA) '("(" "[" "{"))
         (error who "expected first lw to contain open-paren, got: ~e" lwA))
       (unless (symbol? (lw-e lwB))
         (error who "expected second lw to contain symbol, got: ~e" lwB))
       (unless (member (lw-e lwZ) '(")" "]" "}"))
         (error who "expected last lw to contain close-paren, got: ~e" lwZ))
       (let ([args (- (length lws) 3)])
         (unless (<= min-args args max-args)
           (if (= min-args max-args)
               (error who "expected ~s argument(s), got ~s: ~e"
                      min-args args lws)
               (error who "expected between ~s and ~s arguments, got ~s: ~e"
                      min-args max-args args lws))))
       (proc lws)))))

(define ((refit-rw proc) lws)
  (refit lws (proc lws)))

(define (taller s)
  (define p
    (cond [(string? s) (text s (default-style) (default-font-size))]
          [(pict? s) s]))
  (define h (pict-height p))
  (drop-below-ascent (scale (launder p) 1 1.3) (* 0.1 h)))

(define (comma-ize lws contains-close-paren?)
  (let loop ([lws lws])
    (cond [(and contains-close-paren?
                (or (null? (cdr lws))
                    (null? (cddr lws))))
           lws]
          [(and (not contains-close-paren?)
                (or (null? lws)
                    (null? (cdr lws))))
           lws]
          [(positive? (lw-line-span (car lws)))
           ;; a line break?
           (cons (car lws) (loop (cdr lws)))]
          [else (list*
                 (car lws)
                 (between ", " (car lws) (cadr lws))
                 (loop (cdr lws)))])))

;; ->string/pict : (U string pict (-> (U string pict)) -> (U string pict)
(define (->string/pict c)
  (cond [(pict? c) c]
        [(string? c) c] ;; ((current-text) c (default-style) (default-font-size))
        [else (c)]))

;; maybe-parenthesize : lw _ -> (nonempty-listof lw)
(define (maybe-parenthesize arg paren-spec)
  (if (parenthesize? arg paren-spec)
      (parenthesize-lws (list arg))
      (list arg)))

;; parenthesize? : lw (U #t #f (listof symbol) (-> lw boolean)) -> boolean
(define (parenthesize? arg parenthesize-arg)
  (cond [(boolean? parenthesize-arg) parenthesize-arg]
        [(list? parenthesize-arg)
         (let ([contents (lw-e arg)])
           (and (not (lw-unq? arg))
                (not (lw-metafunction? arg))
                (list? contents)
                (>= (length contents) 3)
                (member (lw-e (first contents)) '("(" "[" "{"))
                (member (lw-e (last contents)) '(")" "]" "}"))
                (member (lw-e (second contents)) parenthesize-arg)
                #t))]
        [(procedure? parenthesize-arg)
         (parenthesize-arg arg)]))

;; parenthesize-lws : (nonempty-listof lw) -> (nonempty-listof lw)
(define (parenthesize-lws lws)
  (let ([lwA (first lws)]
        [lwZ (last lws)])
    (append (list (just-before "(" lwA))
            lws
            (list (just-after ")" lwZ)))))
