#lang racket/base

(require "decode.rkt"
         "core.rkt"
         "manual-struct.rkt"
         "decode-struct.rkt"
         "html-properties.rkt"
         "tag.rkt"
         "private/tag.rkt"
         racket/list
         racket/class
         racket/contract/base
         racket/contract/combinator
         (for-syntax racket/base))

(provide (all-from-out "tag.rkt"))

;; ----------------------------------------

(module configure-runtime racket/base
  (require scribble/base/lang/configure-runtime))

(define-syntax-rule (title-like-contract)
  (->* ()
       (#:tag (or/c #f string? (listof string?))
              #:tag-prefix (or/c #f string? module-path?)
              #:style (or/c style? string? symbol? (listof symbol?) #f))
       #:rest (listof pre-content?)
       part-start?))

(provide/contract
 [title (->* ()
             (#:tag (or/c #f string? (listof string?))
                    #:tag-prefix (or/c #f string? module-path?)
                    #:style (or/c style? string? symbol? (listof symbol?) #f)
                    #:version (or/c string? #f)
                    #:date (or/c string? #f))
             #:rest (listof pre-content?)
             title-decl?)]
 [section (title-like-contract)]
 [subsection (title-like-contract)]
 [subsubsection (title-like-contract)]
 [subsubsub*section  (->* ()
                          (#:tag (or/c #f string? (listof string?)))
                          #:rest (listof pre-content?)
                          block?)])
(provide include-section)

(define (title #:tag [tag #f] #:tag-prefix [prefix #f] #:style [style plain]
               #:version [version #f] #:date [date #f]
               . str)
  (let ([content (decode-content str)])
    (make-title-decl (prefix->string prefix)
                     (convert-tag tag content)
                     version
                     (let ([s (convert-part-style 'title style)])
                       (if date
                           (make-style (style-name s)
                                       (cons (make-document-date date)
                                             (style-properties s)))
                           s))
                     content)))

(define (section #:tag [tag #f] #:tag-prefix [prefix #f] #:style [style plain]
                 . str)
  (let ([content (decode-content str)])
    (make-part-start 0 (prefix->string prefix)
                     (convert-tag tag content)
                     (convert-part-style 'section style)
                     content)))

(define (subsection #:tag [tag #f] #:tag-prefix [prefix #f] #:style [style plain]
                    . str)
  (let ([content (decode-content str)])
    (make-part-start 1
                     (prefix->string prefix)
                     (convert-tag tag content)
                     (convert-part-style 'subsection style)
                     content)))

(define (subsubsection #:tag [tag #f] #:tag-prefix [prefix #f]
                       #:style [style plain] . str)
  (let ([content (decode-content str)])
    (make-part-start 2
                     (prefix->string prefix)
                     (convert-tag tag content)
                     (convert-part-style 'subsubsection style)
                     content)))

(define (subsubsub*section #:tag [tag #f] . str)
  (let ([content (decode-content str)])
    (make-paragraph plain 
                    (list
                     (make-element "SSubSubSubSection"
                                   (if tag
                                       (make-target-element #f content `(part ,tag))
                                       content))))))

(define-syntax (include-section stx)
  (syntax-case stx ()
    [(_ mod)
     (with-syntax ([doc-from-mod (datum->syntax #'mod 'doc)])
       (unless (module-path? (syntax->datum #'mod))
         (raise-syntax-error #f
                             "not a module path"
                             stx
                             #'mod))
       #'(begin
           (require (only-in mod [doc-from-mod doc]))
           doc))]))

;; ----------------------------------------

(provide/contract 
 [author (->* (content?) () #:rest (listof content?) block?)]
 [author+email (->* (content? string?) (#:obfuscate? any/c) element?)])

(define (author . auths)
  (make-paragraph 
   (make-style 'author null)
   (let ([nl (make-element 'newline '("\n"))])
     (case (length auths)
       [(1) auths]
       [(2) (list (car auths) nl "and " (cadr auths))]
       [else (let ([r (reverse auths)])
               (append (add-between (reverse (cdr r))
                                    (make-element #f (list "," nl)))
                       (list "," nl "and " (car r))))]))))

(define (author+email name email #:obfuscate? [obfuscate? #f])
  (make-element #f
                (list
                 name
                 " <" 
                 (if obfuscate?
                     (regexp-replace* #rx"[.]"
                                      (regexp-replace* #rx"@" email " at ")
                                      " dot ")
                     (hyperlink (string-append "mailto:" email) email))
                 ">")))

;; ----------------------------------------

(define (item? x) (an-item? x))

(define items/c
  (flat-rec-contract
   items/c
   item?
   block?
   (listof items/c)
   (spliceof items/c)))

(provide items/c)

(provide/contract 
 [itemlist (->* () 
                (#:style (or/c style? string? symbol? #f)) 
                #:rest (listof items/c)
                itemization?)]
 [item (->* () 
            () 
            #:rest (listof pre-flow?)
            item?)])
(provide/contract
 [item? (any/c . -> . boolean?)])

(define (itemlist #:style [style plain] . items)
  (let ([flows (let loop ([items items])
                 (cond
                  [(null? items) null]
                  [(item? (car items)) (cons (an-item-flow (car items))
                                             (loop (cdr items)))]
                  [(block? (car items)) (cons (list (car items))
                                              (loop (cdr items)))]
                  [(splice? (car items))
                   (loop (append (splice-run (car items))
                                 (cdr items)))]
                  [else
                   (loop (append (car items) (cdr items)))]))])
    (make-itemization (convert-block-style style) flows)))

(define-struct an-item (flow))

(define (item . str)
  (make-an-item (decode-flow str)))

;; ----------------------------------------

(provide ._ .__ ~ ?- -~-)

(define ._ (make-element (make-style "Sendabbrev" null) "."))
(define .__ (make-element (make-style "Sendsentence" null) "."))
(define ~ "\uA0")
(define ?- "\uAD")
(define -~- "\u2011")

;; ----------------------------------------

(define elem-like-contract
  (->* () () #:rest (listof pre-content?) element?))

(provide/contract
 [linebreak (-> element?)]
 [nonbreaking elem-like-contract]
 [hspace (-> exact-nonnegative-integer? element?)]
 [elem (->* ()
            (#:style element-style?)
            #:rest (listof pre-content?)
            element?)]
 [italic elem-like-contract]
 [bold elem-like-contract]
 [smaller elem-like-contract]
 [larger elem-like-contract]
 [emph elem-like-contract]
 [tt elem-like-contract]
 [subscript elem-like-contract]
 [superscript elem-like-contract]

 [literal (->* (string?) () #:rest (listof string?) element?)]

 [image (->* ((or/c path-string? (cons/c 'collects (listof bytes?))))
             (#:scale real?
                      #:suffixes (listof (and/c string? #rx"^[.]"))
                      #:style element-style?)
             #:rest (listof content?)
             image-element?)])

(define hspace-cache (make-vector 100 #f))

(define (hspace n)
  (if (n . < . (vector-length hspace-cache))
      (or (vector-ref hspace-cache n)
          (let ([h (make-element 'hspace (list (make-string n #\space)))])
            (vector-set! hspace-cache n h)
            h))
      (make-element 'hspace (list (make-string n #\space)))))

(define (linebreak)
  (make-element 'newline '("\n")))

(define (nonbreaking . str)
  (make-element 'no-break (decode-content str)))

(define (elem #:style [style plain] . str)
  (make-element style (decode-content str)))

(define (italic . str)
  (make-element 'italic (decode-content str)))

(define (bold . str)
  (make-element 'bold (decode-content str)))

(define (smaller . str)
  (make-element 'smaller (decode-content str)))

(define (larger . str)
  (make-element 'larger (decode-content str)))

(define (emph . str)
  (make-element 'emph (decode-content str)))

(define (tt . str)
  (let* ([l (decode-content str)]
         [l (let ([m (and (pair? l)
                          (string? (car l))
                          (regexp-match-positions #rx"^ +" (car l)))])
              (if m
                (list* (hspace (- (cdar m) (caar m)))
                       (substring (car l) (cdar m))
                       (cdr l))
                l))])
    (if (andmap string? l)
      (make-element 'tt l)
      (make-element #f (map (lambda (s)
                              (if (or (string? s) (symbol? s))
                                (make-element 'tt (list s))
                                s))
                            l)))))

(define (span-class classname . str)
  (make-element classname (decode-content str)))

(define (subscript . str)
  (make-element 'subscript (decode-content str)))

(define (superscript . str)
  (make-element 'superscript (decode-content str)))

(define (literal s . strs)
  (let ([s (apply string-append s strs)])
    (make-element #f s)))

(define (image #:scale [scale 1.0] 
               filename-relative-to-source
               #:suffixes [suffixes null]
               #:style [style #f]
               . alt)
  (make-image-element style
                      (decode-content alt)
                      filename-relative-to-source
                      suffixes
                      scale))

;; ----------------------------------------

(define (cell-spec/c c)
  (define rc
    (recursive-contract (or/c c
                              empty
                              (cons/c rc rc))))
  rc)

(provide/contract
 [para (->* ()
            (#:style (or/c style? string? symbol? #f ))
            #:rest (listof pre-content?)
            paragraph?)]
 [nested (->* ()
              (#:style (or/c style? string? symbol? #f ))
              #:rest (listof pre-flow?)
              nested-flow?)]
 [compound (->* ()
                (#:style (or/c style? string? symbol? #f ))
                #:rest (listof pre-flow?)
                compound-paragraph?)]
 [tabular (->* ((listof (listof (or/c 'cont block? content?))))
               (#:style (or/c style? string? symbol? #f)
                #:sep (or/c content? block? #f)
                #:column-properties (listof any/c)
                #:row-properties (listof any/c)
                #:cell-properties (listof (listof any/c))
                #:sep-properties (or/c list? #f))
               table?)])

(define (convert-block-style style)
  (cond
   [(style? style) style]
   [(or (string? style) (symbol? style)) (make-style style null)]
   [else plain]))

(define (nested #:style [style #f] . c)
  (make-nested-flow (convert-block-style style)
                    (decode-flow c)))

(define (para #:style [style #f] . c)
  (make-paragraph (convert-block-style style)
                  (decode-content c)))

(define (compound #:style [style #f] . c)
  (make-compound-paragraph (convert-block-style style)
                           (decode-flow c)))

(define (tabular #:style [style #f]
                 #:sep [sep #f]
                 #:sep-properties [sep-props #f]
                 #:column-properties [column-properties null]
                 #:row-properties [row-properties null]
                 #:cell-properties [cell-properties null]
                 cells)
  (define (nth-str pos)
    (case (modulo pos 10)
      [(1) "st"]
      [(2) "nd"]
      [(3) "rd"]
      [else "th"]))
  (unless (null? cells)
    (let ([n (length (car cells))])
      (for ([row (in-list (cdr cells))]
            [pos (in-naturals 2)])
        (unless (= n (length row))
          (raise-mismatch-error
           'tabular
           (format "bad length (~a does not match first row's length ~a) for ~a~a row: "
                   (length row)
                   n
                   pos
                   (nth-str pos))
           row)))))
  (for ([row (in-list cells)]
        [pos (in-naturals 1)])
    (when (and (pair? row) (eq? (car row) 'cont))
      (raise-mismatch-error
       'tabular
       (format "~a~a row starts with 'cont: " pos (nth-str pos))
       row)))
  (make-table (let ([s (convert-block-style style)])
                (define n-orig-cols (if (null? cells)
                                        0
                                        (length (car cells))))
                (define n-cols (if sep
                                   (max 0 (sub1 (* n-orig-cols 2)))
                                   n-orig-cols))
                (define n-rows (length cells))
                (unless (null? cells)
                  (when ((length column-properties) . > . n-orig-cols)
                    (raise-mismatch-error
                     'tabular
                     "column properties list is too long: "
                     column-properties)))
                (when ((length row-properties) . > . n-rows)
                  (raise-mismatch-error
                   'tabular
                   "row properties list is too long: "
                   row-properties))
                (when ((length cell-properties) . > . n-rows)
                  (raise-mismatch-error
                   'tabular
                   "cell properties list is too long: "
                   cell-properties))
                (unless (null? cells)
                  (for ([row (in-list cell-properties)])
                    (when ((length row) . > . n-orig-cols)
                      (raise-mismatch-error
                       'tabular
                       "row list within cell properties list is too long: "
                       row))))
                ;; Expand given column and cell properties lists to match
                ;; the dimensions of the given `cells` by duplicating
                ;; the last element of a list as needed (and ignoring
                ;; extra elements):
                (define (make-full-column-properties column-properties)
                  (let loop ([column-properties column-properties]
                             [n 0]
                             [prev null])
                    (cond
                     [(= n n-cols) null]
                     [(null? column-properties)
                      (if (or (zero? n) (not sep))
                          (cons prev (loop null (add1 n) prev))
                          (list* (or sep-props prev) prev (loop null (+ n 2) prev)))]
                     [else
                      (define (to-list v) (if (list? v) v (list v)))
                      (define props (to-list (car column-properties)))
                      (define rest (loop (cdr column-properties)
                                         (if (or (zero? n) (not sep))
                                             (add1 n)
                                             (+ n 2))
                                         props))
                      (if (or (zero? n) (not sep))
                          (cons props rest)
                          (list* (or sep-props prev) props rest))])))
                (define full-column-properties
                  (make-full-column-properties column-properties))
                (define (make-full-cell-properties cell-properties)
                  (let loop ([cell-properties cell-properties]
                             [n 0]
                             [prev (make-list n-cols null)])
                    (cond
                     [(= n n-rows) null]
                     [(null? cell-properties)
                      (cons prev (loop null (add1 n) prev))]
                     [else
                      (define props (make-full-column-properties (car cell-properties)))
                      (cons props
                            (loop (cdr cell-properties)
                                  (add1 n)
                                  props))])))
                (define full-cell-properties
                  (for/list ([c-row (in-list (make-full-cell-properties cell-properties))]
                             [r-row (in-list (make-full-cell-properties (map list row-properties)))])
                    (for/list ([c (in-list c-row)]
                               [r (in-list r-row)])
                      (append c r))))
                (define all-cell-properties
                  (and (or (pair? row-properties)
                           (pair? cell-properties))
                       (if (null? column-properties)
                           full-cell-properties
                           (for/list ([row (in-list full-cell-properties)])
                             (for/list ([cell (in-list row)]
                                        [col (in-list full-column-properties)])
                               (append cell col))))))
                (define all-column-properties
                  (and (pair? column-properties)
                       full-column-properties))
                ;; Will werge `cell-properties` and `column-properties` into
                ;; `s`. Start by finding any existing `table-columns`
                ;; and `table-cells` properties with the right number of
                ;; styles:
                (define props (style-properties s))
                (define tc (and all-column-properties
                                (let ([tc (ormap (lambda (v) (and (table-columns? v) v))
                                                 props)])
                                  (if (and tc
                                           (= (length (table-columns-styles tc))
                                              n-cols))
                                      tc
                                      #f))))
                (define tl (and all-cell-properties
                                (let ([tl (ormap (lambda (v) (and (table-cells? v) v))
                                                 props)])
                                  (if (and tl
                                           (= (length (table-cells-styless tl))
                                              n-rows)
                                           (andmap (lambda (cl)
                                                     (= (length cl) n-cols))
                                                   (table-cells-styless tl)))
                                      tl
                                      #f))))
                ;; Merge:
                (define (cons-maybe v l) (if v (cons v l) l))
                (make-style (style-name s)
                            (cons-maybe
                             (and all-column-properties
                                  (table-columns
                                   (if tc
                                       (for/list ([ps (in-list all-column-properties)]
                                                  [cs (in-list (table-columns-styles tc))])
                                         (make-style (style-name cs)
                                                     (append ps (style-properties cs))))
                                       (for/list ([ps (in-list all-column-properties)])
                                         (make-style #f ps)))))
                             (cons-maybe
                              (and all-cell-properties
                                   (table-cells
                                    (if tl
                                        (for/list ([pss (in-list all-cell-properties)]
                                                   [css (in-list (table-cells-styless tl))])
                                          (for/list ([ps (in-list pss)]
                                                     [cs (in-list css)])
                                            (make-style (style-name cs)
                                                        (append ps (style-properties cs)))))
                                        (for/list ([pss (in-list all-cell-properties)])
                                          (for/list ([ps (in-list pss)])
                                            (make-style #f ps))))))
                              (remq tc (remq tl props))))))
              ;; Process cells:
              (map (lambda (row)
                     (define (cvt cell)
                       (cond
                        [(eq? cell 'cont) cell]
                        [(block? cell) cell]
                        [else (make-paragraph plain cell)]))
                     (define l (map cvt row))
                     (if sep
                         (add-between/cont l (cvt sep))
                         l))
                   cells)))

;; Like `add-between`, but change `sep` to 'cont when
;; adding before a 'cont:
(define (add-between/cont l sep)
  (cond
   [(null? l) null]
   [(null? (cdr l)) l]
   [else
    (list* (car l)
           (if (eq? 'cont (cadr l)) 'cont sep)
           (add-between/cont (cdr l) sep))]))

;; ----------------------------------------

(provide
 (contract-out
  [elemtag (->* ((or/c taglet? generated-tag?))
                ()
                #:rest (listof pre-content?)
                element?)]
  [elemref (->* ((or/c taglet? generated-tag?))
                (#:underline? any/c)
                #:rest (listof pre-content?)
                element?)]
  [secref (->* (string?)
               (#:doc (or/c #f module-path?)
                #:tag-prefixes (or/c #f (listof string?))
                #:underline? any/c
                #:link-render-style (or/c #f link-render-style?))
               element?)]
  [Secref (->* (string?)
               (#:doc (or/c #f module-path?)
                #:tag-prefixes (or/c #f (listof string?))
                #:underline? any/c
                #:link-render-style (or/c #f link-render-style?))
               element?)]
  [seclink (->* (string?)
                (#:doc (or/c #f module-path?)
                 #:tag-prefixes (or/c #f (listof string?))
                 #:underline? any/c
                 #:indirect? any/c)
                #:rest (listof pre-content?)
                element?)]
  [other-doc (->* (module-path?)
                  (#:underline? any/c
                   #:indirect (or/c #f content?))
                  element?)]))

(define (elemtag t . body)
  (make-target-element #f (decode-content body) `(elem ,t)))
(define (elemref #:underline? [u? #t] t . body)
  (make-link-element (if u? #f "plainlink") (decode-content body) `(elem ,t)))

(define (secref s #:underline? [u? #t] #:doc [doc #f] #:tag-prefixes [prefix #f]
                #:link-render-style [link-style #f])
  (make-link-element (let ([name (if u? #f "plainlink")])
                       (if link-style
                           (style name (list link-style))
                           name))
                     null
                     (make-section-tag s #:doc doc #:tag-prefixes prefix)))
(define (Secref s #:underline? [u? #t] #:doc [doc #f] #:tag-prefixes [prefix #f]
                #:link-render-style [link-style #f])
  (let ([le (secref s #:underline? u? #:doc doc #:tag-prefixes prefix
                    #:link-render-style link-style)])
    (make-link-element
     (let ([es (or (element-style le) plain)])
       (style (style-name es) (cons 'uppercase (style-properties es))))
     (element-content le)
     (link-element-tag le))))

(define normal-indirect (style #f '(indirect-link)))
(define plain-indirect (style "plainlink" '(indirect-link)))

(define (seclink tag 
                 #:doc [doc #f] 
                 #:underline? [u? #t] 
                 #:tag-prefixes [prefix #f]
                 #:indirect? [indirect? #f]
                 . s)
  (make-link-element (if indirect?
                         (if u?
                             normal-indirect
                             plain-indirect)
                         (if u? 
                             #f 
                             "plainlink"))
                     (decode-content s)
                     `(part ,(doc-prefix doc prefix tag))))

(define (other-doc doc 
                   #:underline? [u? #t]
                   #:indirect [indirect #f])
  (if indirect
      (seclink "top" #:doc doc #:underline? u? #:indirect? #t
               (list "the " indirect " documentation"))
      (secref "top" #:doc doc #:underline? u?)))

;; ----------------------------------------

(provide/contract
 [hyperlink (->* ((or/c string? path?))
                 (#:underline? any/c
                               #:style element-style?)
                 #:rest (listof pre-content?)
                 element?)]
 [url (-> string? element?)]
 [margin-note (->* () (#:left? any/c) #:rest (listof pre-flow?) block?)]
 [margin-note* (->* () (#:left? any/c) #:rest (listof pre-content?) element?)]
 [centered (->* () () #:rest (listof pre-flow?) block?)]
 [verbatim (->* (content?) (#:indent exact-nonnegative-integer?) #:rest (listof content?) block?)])

(define (centered . s)
  (make-nested-flow (make-style "SCentered" null) (decode-flow s)))

(define (hyperlink url
                   #:underline? [underline? #t]
                   #:style [style (if underline? #f "plainlink")]
                   . str)
  (make-element (make-style (if (style? style)
                                (style-name style)
                                style)
                            (cons (make-target-url url)
                                  (if (style? style)
                                      (style-properties style)
                                      null)))
                (decode-content str)))

(define (url str)
  (hyperlink str (make-element 'url str)))

(define (margin-note #:left? [left? #f] . c)
  (make-nested-flow
   (make-style (if left? "refparaleft" "refpara")
               '(command never-indents))
   (list
    (make-nested-flow
     (make-style (if left? "refcolumnleft" "refcolumn")
                 null)
     (list
      (make-nested-flow
       (make-style "refcontent" null)
       (decode-flow c)))))))

(define (margin-note* #:left? [left? #f] . c)
  (make-element
   (make-style (if left? "refelemleft" "refelem") null)
   (make-element
    (make-style (if left? "refcolumnleft" "refcolumn") null)
    (make-element
     (make-style "refcontent" null)
     (decode-content c)))))

(define (verbatim #:indent [i 0] s . more)
  (define lines 
    ;; Break input into a list of lists, where each inner
    ;; list is a single line. Break lines on "\n" in the
    ;; input strings, while non-string content is treated
    ;; as an element within a line.
    (let loop ([l (cons s more)] [strs null])
      (cond
       [(null? l) (if (null? strs)
                      null
                      (map
                       list
                       (regexp-split
                        #rx"\n"
                        (apply string-append (reverse strs)))))]
       [(string? (car l))
        (loop (cdr l) (cons (car l) strs))]
       [else
        (define post-lines (loop (cdr l) null))
        (define pre-lines (loop null strs))
        (define-values (post-line rest-lines)
          (if (null? post-lines)
              (values null null)
              (values (car post-lines) (cdr post-lines))))
        (define-values (first-lines pre-line)
          (if (null? pre-lines)
              (values null null)
              (values (drop-right pre-lines 1)
                      (last pre-lines))))
        (append first-lines
                (list (append pre-line (list (car l)) post-line))
                rest-lines)])))
  (define (str->elts str)
    ;; Convert a single string in a line to typewriter font,
    ;; and also convert multiple adjacent spaces to `hspace` so
    ;; that the space is preserved exactly:
    (let ([spaces (regexp-match-positions #rx"(?:^| ) +" str)])
      (if spaces
        (list* (make-element 'tt (substring str 0 (caar spaces)))
               (hspace (- (cdar spaces) (caar spaces)))
               (str->elts (substring str (cdar spaces))))
        (list (make-element 'tt (list str))))))
  (define (strs->elts line)
    ;; Convert strings in the line:
    (apply append (map (lambda (e) 
                         (if (string? e) 
                             (str->elts e) 
                             (list e)))
                       line)))
  (define indent
    ;; Add indentation to a line:
    (if (zero? i)
      values
      (let ([hs (hspace i)]) (lambda (line) (cons hs line)))))
  (define (make-nonempty l)
    ;; If a line has no content, then add a single space:
    (if (let loop ([l l])
          (cond
           [(null? l) #t]
           [(equal? "" l) #t]
           [(list? l) (andmap loop l)]
           [(element? l) (loop (element-content l))]
           [(multiarg-element? l) (loop (multiarg-element-contents l))]
           [else #f]))
        (list l (hspace 1))
        l))
  (define (make-line line)
    ;; Convert a list of line elements --- a mixture of strings
    ;; and non-strings --- to a paragraph for the line:
    (let* ([line (indent (strs->elts line))])
      (list (make-paragraph omitable-style (make-nonempty line)))))
  (make-table (make-style "SVerbatim" null) (map make-line lines)))

(define omitable-style (make-style 'omitable null))

;; ----------------------------------------

; XXX unknown contract
(provide get-index-entries)
(provide/contract
 [index-block (-> delayed-block?)]
 [index (((or/c string? (listof string?))) ()  #:rest (listof pre-content?) . ->* . index-element?)]
 [index* (((listof string?) (listof any/c)) ()  #:rest (listof pre-content?) . ->* . index-element?)] ; XXX first any/c wrong in docs 
 [as-index (() () #:rest (listof pre-content?) . ->* . index-element?)]
 [section-index (() () #:rest (listof string?) . ->* . part-index-decl?)]
 [index-section (() (#:tag (or/c #f string?)) . ->* . part?)])

(define (section-index . elems)
  (make-part-index-decl (map content->string elems) elems))

(define (record-index word-seq element-seq tag content)
  (make-index-element #f
                      (list (make-target-element #f content `(idx ,tag)))
                      `(idx ,tag)
                      word-seq
                      element-seq
                      #f))

(define (index* word-seq content-seq . s)
  (let ([key (make-generated-tag)])
    (record-index (map clean-up-index-string word-seq)
                  content-seq key (decode-content s))))

(define (index word-seq . s)
  (let ([word-seq (if (string? word-seq) (list word-seq) word-seq)])
    (apply index* word-seq word-seq s)))

(define (as-index . s)
  (let ([key (make-generated-tag)]
        [content (decode-content s)])
    (record-index
     (list (clean-up-index-string (content->string content)))
     (if (= 1 (length content)) content (list (make-element #f content)))
     key
     content)))

(define (index-section #:title [title "Index"] #:tag [tag #f])
  (make-part #f
             `((part ,(or tag "doc-index")))
             (list title)
             (make-style 'index '(unnumbered))
             null
             (list (index-block))
             null))

;; returns an ordered list of (list tag (text ...) (element ...) index-desc)
(define (get-index-entries sec ri)
  (define (compare-lists xs ys <?)
    (let loop ([xs xs] [ys ys])
      (cond [(and (null? xs) (null? ys)) '=]
            [(null? xs) '<]
            [(null? ys) '>]
            [(<? (car xs) (car ys)) '<]
            [(<? (car ys) (car xs)) '>]
            [else (loop (cdr ys) (cdr xs))])))
  ;; string-ci<? as a major key, and string<? next, so "Foo" precedes "foo"
  ;; (define (string*<? s1 s2)
  ;;   (or (string-ci<? s1 s2)
  ;;       (and (not (string-ci<? s2 s1)) (string<? s1 s2))))
  (define (get-desc entry)
    (let ([desc (cadddr entry)])
      (cond [(exported-index-desc? desc)
             (cons 'libs (map (lambda (l)
                                (format "~s" l))
                              (exported-index-desc-from-libs desc)))]
            [(module-path-index-desc? desc) '(mod)]
            [(part-index-desc? desc) '(part)]
            [(delayed-index-desc? desc) '(delayed)]
            [else '(#f)])))
  ;; parts first, then modules, then bindings, delayed means it's not
  ;; the last round, and #f means no desc
  (define desc-order '(part mod libs delayed #f))
  ;; this defines an imposed ordering for module names
  (define lib-order '(#rx"^racket(?:/|$)" #rx"^r.rs(?:/|$)" #rx"^lang(?:/|$)"))
  (define (lib<? lib1 lib2)
    (define (lib-level lib)
      (let loop ([i 0] [rxs lib-order])
        (if (or (null? rxs) (regexp-match? (car rxs) lib))
          i (loop (add1 i) (cdr rxs)))))
    (let ([l1 (lib-level lib1)] [l2 (lib-level lib2)])
      (if (= l1 l2) (string<? lib1 lib2) (< l1 l2))))
  (define (compare-desc e1 e2)
    (let* ([d1 (get-desc e1)] [d2 (get-desc e2)]
           [t1 (car d1)]      [t2 (car d2)])
      (cond [(memq t2 (cdr (memq t1 desc-order))) '<]
            [(memq t1 (cdr (memq t2 desc-order))) '>]
            [else (case t1 ; equal to t2
                    [(part) '=] ; will just compare tags
                    [(mod)  '=] ; the text fields are the names of the modules
                    [(libs) (compare-lists (cdr d1) (cdr d2) lib<?)]
                    [(delayed) '>] ; dosn't matter, will run again
                    [(#f) '=])])))
  (define (entry<? e1 e2)
    (let ([text1 (cadr e1)] [text2 (cadr e2)])
      (case (compare-lists text1 text2 string-ci<?)
        [(<) #t] [(>) #f]
        [else (case (compare-desc e1 e2)
                [(<) #t] [(>) #f]
                [else (case (compare-lists text1 text2 string<?)
                        [(<) #t] [(>) #f]
                        [else
                         ;; (error 'get-index-entries
                         ;;        ;; when this happens, revise this code so
                         ;;        ;; ordering will always be deterministic
                         ;;        "internal error -- unordered entries: ~e ~e"
                         ;;        e1 e2)
                         ;; Instead, just compare the tags
                         (string<? (format "~a" (car e1))
                                   (format "~a" (car e2)))])])])))
  (define l null)
  (hash-for-each
   (let ([parent (collected-info-parent (part-collected-info sec ri))])
     (if parent
       (collected-info-info (part-collected-info parent ri))
       (let ([ci (resolve-info-ci ri)])
         ;; Force all xref info:
         ((collect-info-ext-demand ci) #f ci)
         (collect-info-ext-ht ci))))
   (lambda (k v)
     (when (and (pair? k) (eq? 'index-entry (car k)))
       (let ([v (if (known-doc? v) (known-doc-v v) v)])
         (set! l (cons (cons (cadr k) v) l))))))
  (sort l entry<?))

(define (index-block)
  (define alpha (string->list "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
  (define (rows . rows)
    (make-table (make-style 'index null)
                (map (lambda (row)
                       (list (make-paragraph plain row)))
                     rows)))
  (define contents
    (lambda (renderer sec ri)
      (define l (get-index-entries sec ri))
      (define manual-newlines? (send renderer index-manual-newlines?))
      (define alpha-starts (make-hasheq))
      (define alpha-row
        (let loop ([i l] [alpha alpha])
          (define (add-letter let l)
            (list* (make-element "nonavigation" (list (string let))) " " l))
          (cond [(null? alpha) null]
                [(null? i) (add-letter (car alpha) (loop i (cdr alpha)))]
                [else
                 (let* ([strs (cadr (car i))]
                        [letter (if (or (null? strs) (string=? "" (car strs)))
                                  #f
                                  (char-upcase (string-ref (car strs) 0)))])
                   (cond [(not letter) (loop (cdr i) alpha)]
                         [(char-ci>? letter (car alpha))
                          (add-letter (car alpha) (loop i (cdr alpha)))]
                         [(char-ci=? letter (car alpha))
                          (hash-set! alpha-starts (car i) letter)
                          (list* (make-element
                                  (make-style #f (list (make-target-url (format "#alpha:~a" letter))))
                                  (list (string (car alpha))))
                                 " "
                                 (loop (cdr i) (cdr alpha)))]
                         [else (loop (cdr i) alpha)]))])))
      (define body
        (let ([br (if manual-newlines? (make-element 'newline '("\n")) "")])
          (map (lambda (i)
                 (let ([e (make-link-element
                           "indexlink"
                           `(,@(add-between (caddr i) ", ") ,br)
                           (car i))])
                   (cond [(hash-ref alpha-starts i #f)
                          => (lambda (let)
                               (make-element
                                (make-style #f (list
                                                (make-url-anchor
                                                 (format "alpha:~a" (char-upcase let)))))
                                (list e)))]
                         [else e])))
               l)))
      (if manual-newlines?
        (rows alpha-row '(nbsp) body)
        (apply rows alpha-row '(nbsp) (map list body)))))
  (make-delayed-block contents))

;; ----------------------------------------

(provide/contract
 [table-of-contents (-> delayed-block?)]
 [local-table-of-contents (() 
                           (#:style (or/c style? string? symbol? (listof symbol?) #f))
                           . ->* . delayed-block?)])

(define (table-of-contents)
  (make-delayed-block
   (lambda (renderer part ri)
     (send renderer table-of-contents part ri))))

(define (local-table-of-contents #:style [style plain])
  (make-delayed-block
   (lambda (renderer part ri)
     (send renderer local-table-of-contents part ri style))))
