#lang racket/base

(provide (except-out (all-from-out racket/base) #%module-begin)
         module-begin/plain
         module-begin/doc)

(require (for-syntax racket/base syntax/boundmap racket/list
                     syntax/strip-context))

(begin-for-syntax
  (define first-id #f)
  (define main-id #f)
  (define (mapping-get mapping id)
    (free-identifier-mapping-get mapping id (lambda () '())))
  ;; maps a chunk identifier to its collected expressions
  (define chunks (make-free-identifier-mapping))
  ;; maps a chunk identifier to all identifiers that are used to define it
  (define chunk-groups (make-free-identifier-mapping))
  (define (get-chunk id) (mapping-get chunks id))
  (define (add-to-chunk! id exprs)
    (unless first-id (set! first-id id))
    (when (eq? (syntax-e id) '<*>) (set! main-id id))
    (free-identifier-mapping-put!
     chunk-groups id
     (cons id (mapping-get chunk-groups id)))
    (free-identifier-mapping-put!
     chunks id
     `(,@(mapping-get chunks id) ,@exprs))))

(define-syntax (tangle stx)
  (define chunk-mentions '())
  (unless first-id
    (raise-no-chunks-error))
  (define orig-stx (syntax-case stx () [(_ orig) #'orig]))
  (define (restore nstx d) (datum->syntax orig-stx d nstx nstx))
  (define (shift nstx) (replace-context orig-stx nstx))
  (define body
    (let ([main-id (or main-id first-id)])
      (restore
       main-id
       (let loop ([block (get-chunk main-id)])
         (append-map
          (lambda (expr)
            (if (identifier? expr)
                (let ([subs (get-chunk expr)])
                  (if (pair? subs)
                      (begin (set! chunk-mentions (cons expr chunk-mentions))
                             (loop subs))
                      (list (shift expr))))
                (let ([subs (syntax->list expr)])
                  (if subs
                      (list (restore expr (loop subs)))
                      (list (shift expr))))))
          block)))))                               
  (with-syntax ([(body ...) (strip-comments body)]
                ;; construct arrows manually
                [((b-use b-id) ...)
                 (append-map (lambda (m)
                               (map (lambda (u)
                                      (list (syntax-local-introduce m) 
                                            (syntax-local-introduce u)))
                                    (mapping-get chunk-groups m)))
                             chunk-mentions)])
    #`(begin body ... (let ([b-id (void)]) b-use) ...)))

(define-for-syntax (strip-comments body)
  (cond
   [(syntax? body)
    (define r (strip-comments (syntax-e body)))
    (if (eq? r (syntax-e body))
        body
        (datum->syntax body r body body))]
   [(pair? body)
    (define a (car body))
    (define ad (syntax-e a))
    (cond
     [(and (pair? ad)
           (memq (syntax-e (car ad))
                 '(code:comment
                   code:contract)))
      (strip-comments (cdr body))]
     [(eq? ad 'code:blank)
      (strip-comments (cdr body))]
     [(and (or (eq? ad 'code:hilite)
               (eq? ad 'code:quote))
           (let* ([d (cdr body)]
                  [dd (if (syntax? d)
                          (syntax-e d)
                          d)])
             (and (pair? dd)
                  (or (null? (cdr dd))
                      (and (syntax? (cdr dd))
                           (null? (syntax-e (cdr dd))))))))
      (define d (cdr body))
      (define r
        (strip-comments (car (if (syntax? d) (syntax-e d) d))))
      (if (eq? ad 'code:quote)
          `(quote ,r)
          r)]
     [(and (pair? ad)
           (eq? (syntax-e (car ad))
                'code:line))
      (strip-comments (append (cdr ad) (cdr body)))]
     [else (cons (strip-comments a)
                 (strip-comments (cdr body)))])]
   [else body]))
      
(define-for-syntax (extract-chunks exprs)
  (let loop ([exprs exprs])
    (syntax-case exprs ()
      [() (void)]
      [(expr . exprs)
       (syntax-case #'expr (define-syntax quote-syntax)
         [(define-values (lifted) (quote-syntax (a-chunk id body ...)))
          (eq? (syntax-e #'a-chunk) 'a-chunk)
          (begin
            (add-to-chunk! #'id (syntax->list #'(body ...)))
            (loop #'exprs))]
         [_ 
          (loop #'exprs)])])))

(define-for-syntax ((make-module-begin submod?) stx)
  (syntax-case stx ()
    [(_) (raise-no-chunks-error)]
    [(_ body0 . body)
     (let ([expanded 
            (expand `(,#'module scribble-lp-tmp-name scribble/private/lp
                                ,@(strip-context #'(body0 . body))))])
       (syntax-case expanded ()
         [(module name lang (mb . stuff))
          (begin (extract-chunks #'stuff)
                 #`(#%module-begin
                    (tangle body0)
                    ;; The `doc` submodule allows a `scribble/lp` module
                    ;; to be provided to `scribble`:
                    #,@(if submod?
                           (list
                            (let ([submod
                                   (strip-context
                                    #`(module doc scribble/doclang2
                                        (require scribble/manual
                                                 (only-in scribble/private/lp chunk CHUNK))
                                        (begin body0 . body)))])
                              (syntax-case submod ()
                                [(_ . rest)
                                 (datum->syntax submod (cons #'module* #'rest))])))
                           '())))]))]))

(define-syntax module-begin/plain (make-module-begin #f))
(define-syntax module-begin/doc (make-module-begin #t))

(define-for-syntax (raise-no-chunks-error)
  (raise-syntax-error 'scribble/lp "no chunks"))
