;; This program assembles beastie.xhtml and friends from beastie.md,
;; ../release-notes.md and the docstrings in the various modules.
;;
;; The program requires that the file build/examples-manifest exists, containing
;; a list of (list "foo.scm" "blah.txt"), where the second string is
;; the name of a file, within dist/examples, to which the first-named
;; file has been copied.
;;
;; This file is part of Beastie <https://purl.org/nxg/dist/beastie>
;; SPDX-FileCopyrightText: 2024 Norman Gray <https://nxg.me.uk>
;; SPDX-License-Identifier: BSD-2-Clause


(module 'markdown 'xexpr)

(define (Usage)
  (eprintf "Usage: ~a [-l foo.css] outdir~%" (car *command-line*))
  (exit 1))

;; track dependencies
;; ... not currently needed
;; (define *file-deps* '())
;; (define (dependency-add! fn)
;;   (set! *file-deps* (cons fn *file-deps*)))

(define local-stylesheet-file #f)
(define *outdir*
  (receive (cmd opts args)
      (getopt '((#\l stylesheet
                 "Output XHTML with a local version of the stylesheet"
                 (set! local-stylesheet-file stylesheet))))
    (if (= (length args) 1)
        (car args)
        (Usage))))

(define stylesheet-contents
  (and local-stylesheet-file
       (with-input-from-file local-stylesheet-file
         (λ ()
           (let loop ((res '()))
             (let ((line (read-line)))
               (if (eof-object? line)
                   (string-join res "\n")
                   (loop (cons line res)))))))))

(define signature
  `(div ((class "signature"))
                 (a ((href "https://nxg.me.uk")) "Norman")
                 (br)
                 ,(*beastie* 'date)))

(define (xexpr-write/webpage! body metadata)
  ;; BODY should be a list of xexprs
  (let* ((annotations
          (and (list? metadata)
               (metadata/type metadata 'annotation)))
         (title
          (cond
           ((and (not annotations)
                 (string? metadata))
            metadata)
           ((not annotations) "Dummy title")
           ((list? annotations)
            (let loop ((ann annotations))
              ;; search for '("title" "the title...")
              (cond ((null? ann) "Dummy title")
                    ((string=? (caar ann) "title")
                     (cadar ann))
                    (else (loop (cdr ann))))))
           (else (beastie-error "Unexpected metadata: ~s" metadata)))))
    (display "<!DOCTYPE html\n  PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n")
    (xexpr-write/xml!
     `(html ((xmlns "http://www.w3.org/1999/xhtml")
             (xmlns:dcterms "http://purl.org/dc/terms/"))
            (head
             (title ,title)
             ,(if stylesheet-contents
                  `(style ((type "text/css")) ,stylesheet-contents)
                  '(link ((type "text/css")
                    (rel "stylesheet")
                    (href "https://nxg.me.uk/style/base.css"))))
             (link ((rev "author")
                    (rel "dcterms:creator"))))
            (body
             (h1 ((property "dcterms:title")) ,title)
             ,@body)))))

;; Given (module-name/symbol description/string)
;; produce a list of
;; (module-name/symbol description/string (listof function-name/string))
;; where the functions are those exposed/provided by the given module
(define (extract-docstrings modules)
  (map (λ (m)
         (let ((name (car m))
               (description (cadr m)))
           (let ((provides
                  ;; get-builtin-module/name* produces a list
                  ;; where the second element is a LET?
                  ;; Assemble from this a list of functions as strings,
                  ;; omitting any functions whose names include #\*
                  (filter (λ (fn/string)
                            (not (string-index fn/string #\*)))
                          (map (λ (n+f)
                                 (symbol->string (car n+f)))
                               (cadr (get-builtin-module/name* name))))))
             ;(eprintf "module ~s: provides ~s~%" name provides)
             (list name
                   description
                   provides))))
       modules))

(define (make-docpage/module! module-name summary docfile)
  (let ((provides
         ;; get-builtin-module/name* produces a list where
         ;; the second element is a LET? of the functions provided by
         ;; the module
         (cadr (get-builtin-module/name* module-name)))
        (md-filename (sprintf "~a.md" docfile))
        (xhtml-filename (sprintf "~a/~a.xhtml" *outdir* docfile)))

    (make-docpage/module*! provides
                           summary
                           md-filename
                           xhtml-filename)))

;; Given a markdown input file, and a LET? of the symbols provided by
;; a module, write out a webpage which is the parse of the Markdown,
;; plus a list of functions and their documentation, extracted from
;; the LET.
(define (make-docpage/module*! module/let summary md-filename xhtml-filename)
  (eprintf "make-docpage/module: ~a [~a] -> ~a~%" md-filename summary xhtml-filename)

  (define-values (md md-metadata)
    (parse-markdown-file/metadata md-filename))

  ;(dependency-add! md-filename)

  (let ((funcs-with-doc
         ;; produce a list of
         ;;    (function/string xexpr...)
         ;; with one item for each member of MODULE/LET,
         ;; which is a LET? of the symbols provided by a module
         (sort!
          (filter values
                  (map (λ (fn)
                         (let* ((name (car fn))
                                (name/string (symbol->string name)))
                           (and (not (string-index name/string #\*)) ;skip functions with * in name
                                `(,name/string
                                  (h3 (a ((name ,(sprintf "fn-~a" name)))
                                         ,name/string))
                                  ,(cond ((eval `(documentation ,name) module/let)
                                          => (λ (md)
                                               (if (string=? md "")
                                                   (begin
                                                     (eprintf "docpage/module ~s: doc for ~s empty~%"
                                                              name name)
                                                     '(p "[documentation empty]"))
                                                   (parse-markdown-string md))))
                                         (else
                                          '(p "[no documentation available]")))))))
                       module/let))
          (λ (a b)
            (string<? (car a) (car b))))))

  (with-output-to-file xhtml-filename
    (λ ()
      (xexpr-write/webpage!
       `((p ,summary)
         ,md
         (h2 "Functions")
         (p "Index:")
         (ul . ,(map (λ (fn)
                       `(li (a ((href ,(sprintf "#fn-~a" (car fn))))
                               ,(car fn))))
                     funcs-with-doc))
         ,@(apply append (map cdr funcs-with-doc))
         (div ((class "signature"))
              (a ((href "https://nxg.me.uk")) "Norman")
              (br)
              ,(*beastie* 'date)))
       md-metadata)))))

(define *modules*
  '((authors "Author-string parsing and formatting")
    (aux "Functions for reading and handling .aux files")
    (bibtex "Parsing and manipulating .bib databases")
    (bst "The implementation of the .bst language")
    (json "A basic JSON parser")
    (klipspringer "Parser generator")
    (markdown "A basic Markdown parser")
    (subtex "Expanding simple TeX commands")
    (unicode "Basic Unicode support")
    (utils "Various utility functions")
    (xexpr "Writing and searching x-expressions")))

;; The following is the list of core functions to be documented
;; (we don't 'provide' these, since they're always visible)
;;
;; Don't include *beastie* here, (a) because it gets omitted (by
;; make-docpage/module*!) because it has a '*' in its name, and (b)
;; because extracting its documentation is slightly different from
;; other functions, which results in make-docpage/module*! failing to
;; find it.
(define *core-functions*
  '(printf sprintf eprintf
           print-warning print-info print-trace
           regexp regexp? regexp-match-positions regexp-match-positions/multi
           regexp-match regexp-match/multi regexp-match?))

(define module-docs
  (extract-docstrings *modules*))

(define all-functions/list
  ;; (("function-name" . (span (a ((href ...))  ))) ....)
  (map cdr
       (sort! (apply append
                     (map (λ (m)
                            (let ((module-name (car m))
                                  (funcs (caddr m)))
                              (map (λ (fn)
                                     (cons fn
                                           `(span (a ((href ,(sprintf "~a.xhtml#fn-~a" module-name fn)))
                                                     ,fn)
                                                  ,(sprintf " (~a)" module-name))))
                                   funcs)))
                          (cons `(core "Core functions"
                                       ,(map symbol->string
                                             #;(λ (f)
                                               (list (symbol->string f)))
                                             *core-functions*))
                                module-docs)))
              (λ (a b)
                (string<? (car a) (car b))))))

(define (tweak-top-elements els)
  ;; els is (div (p "xxx") (h2 "xxx") ... )
  ;; turn h1 -> h2 and h2 -> h3, and add a ToC

  (define (hn->content+name hn)
    (let ((child1 (and (not (null? (cdr hn)))
                       (cadr hn))))
      (if (and (list? child1)
               (eqv? (car child1) 'a))
          (receive (a-gi a-atts a-content) ;child1 is (a ((name "foo")) "Content"...)
              (xexpr-disassemble child1)
            (cond ((assq 'name a-atts)
                   => (λ (n)            ;(name "foo")
                        (values a-content (cadr n))))
                  (else                 ;surprising...
                   (values a-content #f))))
          (values (cdr hn) #f))))

  (define (content->anchor content)
    (object->string (string->hash (sprintf "~a" content)) :display))

  `((div ((class "topsidebar"))
         (ul
          . ,(map (λ (h1)               ;(h1 "Content"...) or (h1 (a ((name "foo")) "Content"...))
                    (receive (content name)
                        (hn->content+name h1)
                      `(li
                        (a ((href ,(sprintf "#~a" (or name (content->anchor content)))))
                           . ,content))))
                  (xexpr-path-search '(h1) els))))
    . ,(map (λ (el)
              ;; go through all of the top-level elements in this (div...),
              ;; turning h1->h2 and h2->h3,
              ;; and creating an href target if it's not present already
              (let ((hn
                     (and (list? el)    ;always true in practice?
                          (not (null? el)) ;null only if (div () ...)
                          (case (car el)
                            ((h1) 'h2)  ;new GI
                            ((h2) 'h3)
                            (else #f)))))
                (if hn
                    (receive (content name)
                        (hn->content+name el)
                      `(,hn
                        (a ((name ,(or name (content->anchor content))))
                           . ,content)))
                    el)))
            (cdr els))))

;; write a list of functions
(let ((doc-full-body
       (tweak-top-elements
        `((p "The code within beastie is organised into a number of internal modules.")
          (h2 (a ((name "functions")) "All procedures, by module"))
          (p "Invoke a module "
             (code "foo")
             " using "
             (code "(module 'foo)")
             " or "
             (code "(module 'foo 'bar...)")
             #""".  Additionally, there are some utility functions available in all modules,
               and these are described in """
             (a ((href "core.xhtml")) "core functions")
             ". ")
          (div ((style "moduletoc"))
               (ul . ,(map (λ (module)
                             (let ((module-name (car module))
                                   (description (cadr module))
                                   #;(funcs (caddr module)))
                               `(li (a ((href ,(sprintf "~a.xhtml" module-name)))
                                       ,(symbol->string module-name))
                                    ": " ,description)))
                           module-docs)))
          (p "Within a REPL, " (code "(help foo)")
             " will give brief documentation on the function " (code "foo") ".")
          (p "Some functions have optional arguments denoted by " (code "[foo]")
             ".  Some functions have keyword arguments denoted by " (code ":foo") ".")
          (p "There are some examples of use in "
             (a ((href "examples/index.xhtml")) "the examples directory") ".")
          (p (em "Note") ": while beastie still has a version number less than 1.0, everything here should be regarded as more or less provisional.")

          (h2 "All procedures, alphabetically")

          ;; This version looks agreeably intense, but isn't hugely readable
          ;;(p . ,(intersperse ", " all-functions/list))

          ;; a table, though the columns aren't forced to the same
          ;; size, and this depends on the width of the screen
          ,(let* ((ncols 3)
                  (nrows (ceiling (/ (length all-functions/list) ncols)))
                  (cols (let morecols ((l all-functions/list)
                                       (res '()))
                          (if (null? l)
                              (reverse res)
                              (receive (front back)
                                  (if (<= nrows (length l))
                                      (split-at l nrows)
                                      (values l '()))
                                (morecols back (cons front res)))))))
             (define (maybe-car l)
               (if (null? l)
                   ""
                   (car l)))
             (define (maybe-cdr l)
               (if (null? l)
                   '()
                   (cdr l)))
             (let morerows ((cc cols)
                            (rows '()))
               (if (null? (car cc))
                   `(table . ,(reverse rows))
                   (let ((one-row (map maybe-car cc)))
                     (morerows (map maybe-cdr cc)
                               (cons `(tr
                                       . ,(map (λ (item)
                                                 `(td ,item))
                                               one-row))
                                     rows))))))

          ;; (h2 "DELETE?")
          ;; (p "The following is nice, but may be better in separate files.")
          ;; ,@(map (λ (module)
          ;;           (let ((module-name (car module))
          ;;                 (funcs (caddr module)))
          ;;             `(div (h3
          ;;                    (a ((name ,(sprintf "module-~a" module-name)))
          ;;                       "Module "
          ;;                       (code "'" ,(symbol->string module-name))))
          ;;                   (ul . ,(map (λ (f+doc)
          ;;                                 (let ((fname (car f+doc)))
          ;;                                   `(li (a ((href ,(sprintf "#f-~a-~a" module-name fname)))
          ;;                                           ,fname))))
          ;;                               funcs))
          ;;                   . ,(map (λ (f+doc)
          ;;                             (let ((fname (car f+doc))
          ;;                                   (fdoc  (cdr f+doc)))
          ;;                               `(div (h4
          ;;                                      (a ((name ,(sprintf "f-~a-~a" module-name fname)))
          ;;                                         ,(sprintf "Function ~a" fname)))
          ;;                                     ,fdoc)))
          ;;                           funcs))))
          ;;        functions)
          ,signature))))

  (with-output-to-file (sprintf "~a/procedures.xhtml" *outdir*)
    (λ ()
      (xexpr-write/webpage! doc-full-body "Supported procedures, by module"))))

;; Create index.xhtml from webpage.md and releasenotes.md
;; Create standard .xhtml from .md
(for-each (λ (in-out)
            (let ((in-file  (car in-out))
                  (out-file (cdr in-out)))
              ;(dependency-add! in-file)
              (receive (content metadata)
                  (parse-markdown-file/metadata in-file)
                (let ((page-body
                       (tweak-top-elements
                        `(,@content
                          ,signature))))
                  (with-output-to-file (sprintf "~a/~a" *outdir* out-file)
                    (λ ()
                      (xexpr-write/webpage! page-body metadata)))))))
          '(("vs-bibtex.md" . "vs-bibtex.xhtml")
            ("scheme.md"    . "scheme.xhtml")
            ("beastie.md"   . "beastie.xhtml")))

;; other files, with various special cases
;;
;; The webpage pulls in the contents of ../release-notes.md, as well
(receive (webpage-core webpage-metadata)
    (parse-markdown-file/metadata "webpage.md")
  ;(dependency-add! "webpage.md")
  (let* ((release-notes
          (parse-markdown-file "../release-notes.md"))
         (webpage-body
          (tweak-top-elements
           `(,@webpage-core

             (h1 "Release notes")
             ,@(cdr release-notes)

             ,signature))))

    (with-output-to-file (sprintf "~a/index.xhtml" *outdir*)
      (λ ()
        (xexpr-write/webpage! webpage-body webpage-metadata)))))

;; Create examples/index.xhtml from examples.md
;; and the contents of build/examples-manifest
(with-output-to-file (sprintf "~a/examples/index.xhtml" *outdir*)
  (λ ()
    (let ((the-examples (call-with-input-file "build/examples-manifest" read))
          (examples-md (parse-markdown-file "examples.md")))
      ;(dependency-add! "examples.md")
      (xexpr-write/webpage!
       `(,examples-md
         (ul . ,(map (λ (ex)
                       `(li (a ((href ,(cadr ex))) ,(car ex))))
                     the-examples))
         ,signature)
       "Examples of use"))))

;; Create the per-module documentation, from the docstrings within the program.
(for-each (λ (mod)
            (let ((modname (car mod))
                  (summary (cadr mod)))
              (make-docpage/module! modname
                                    summary
                                    (symbol->string modname))))
          *modules*)

;; Create core.xhtml, in the same way as the per-module documentation
(make-docpage/module*! (apply inlet     ;construct a temporary LET
                              (apply append
                                     (map (λ (f)
                                            `(,f ,(eval f)))
                                          *core-functions*)))
                       "Functions not in any module"
                       "core.md"
                       (sprintf "~a/core.xhtml" *outdir*))

;; (with-output-to-file "depend.mk"
;;   (λ ()
;;     (printf "~a/beastie.xhtml:" *outdir*)
;;     (for-each (λ (fn)
;;                 (printf " ~a" fn))
;;               *file-deps*)
;;     (printf "~%")))
