(define-module (sph documentation))

(use-modules (ice-9 peg) (sph)
  ((sph alist) #:select (alist-q alist-bind)) (sph module binding-info)
  ((sph lang indent-syntax) #:select (denoted-tree->indent-tree indent-tree->denoted-tree))
  (sph lang parser type-signature) ((sph lang scm-format format) #:select (format-docstring))
  ((sph list) #:select (contains? any->list fold-multiple))
  ((sph module) #:select (module-find module-exports))
  ((sph tree) #:select (denoted-tree-minimise-depth)) ((rnrs sorting) #:select (list-sort))
  ((sph string) #:select (string-equal? any->string-pretty-print)) (srfi srfi-1) (srfi srfi-2))

(export default-format-arguments display-module-information-short
  doc-bindings docstring->lines
  docstring-split-signature format-module-documentation
  output-format-indent output-format-signature
  output-format-markdown output-format-list
  lines->docstring module-description
  module-find-one-information module-find-one-information-sorted
  sort-module-information sph-documentation-description)

(define sph-documentation-description
  "extract and display documentation (bindings, arguments and docstrings) from modules")

(define indent-string (string #\space #\space))
(define (replace-underscore a state) (if (string-equal? "_" a) (number->string (+ 10 state) 32) a))

(define output-format-markdown
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (string-append "# " (symbol->string (bi-name bi))
        "\n"
        (docstring-split-signature (bi-documentation bi) #f
          (l (signature docstring)
            (string-append
              (if (or signature (not (string-null? formatted-arguments)))
                (string-append "## signature\n"
                  (if (string-null? formatted-arguments) formatted-arguments
                    (string-append formatted-arguments "\n"))
                  (if signature (string-append signature "\n") ""))
                "")
              (if docstring
                (string-append "## " (lines->docstring (string-split docstring #\newline) "")) ""))))
        "## type\n" (symbol->string (bi-type bi)) "\n"))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n"))))

(define output-format-indent
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (string-append (symbol->string (bi-name bi)) "\n"
        (docstring-split-signature (bi-documentation bi)
          (string-append indent-string indent-string)
          (l (signature docstring)
            (string-append
              (if (or signature (not (string-null? formatted-arguments)))
                (string-append indent-string "signature\n"
                  indent-string indent-string
                  (if (string-null? formatted-arguments) formatted-arguments
                    (string-append formatted-arguments "\n"))
                  (if signature (string-append indent-string indent-string signature "\n") ""))
                "")
              (if docstring (lines->docstring (string-split docstring #\newline) indent-string) ""))))
        indent-string "type: " (symbol->string (bi-type bi))))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n" (q suffix)))))

(define output-format-signature
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (docstring-split-signature (bi-documentation bi) ""
        (l (signature text-lines)
          (let
            ( (arguments-string formatted-arguments)
              (docstring
                (string-join (remove string-null? (if text-lines (any->list text-lines) null))
                  "\n  " (q prefix))))
            (string-append (symbol->string (bi-name bi))
              (if (contains? (q (procedure syntax)) (bi-type bi))
                (string-append " :: " arguments-string) ""))))))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n" (q suffix)))))

(define output-format-list
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments)
      (pair (symbol->string (bi-name bi))
        (append
          (docstring-split-signature (bi-documentation bi) ""
            (l (signature text)
              (append
                (if signature
                  (list
                    (pair (q signature)
                      (if (string-null? formatted-arguments) (list signature)
                        (list formatted-arguments signature))))
                  (if (string-null? formatted-arguments) (list)
                    (list (list (q signature) formatted-arguments))))
                (if (or (not text) (string-null? text)) null (list (list (q description) text))))))
          (list (list (q type) (bi-type bi))))))
    format-module-documentation (l (module-name md) (any->string-pretty-print md))))

(define (docstring-format a) "string -> string"
  "drop doublequotes of formatted string literal"
  (if (string-null? a) a
    (let* ((a (format-docstring a #f indent-string 0)) (a (substring a 1 (- (string-length a) 1))))
      (if (string-null? a) ""
        (string-join
          (map
            (l (line)
              "remove one eventual extra space from the beginning of lines when the docstring
               is formatted to offset the doublequote of the string literal"
              (let (index (string-skip line #\space))
                (if (and index (odd? index)) (string-drop line 1) line)))
            (string-split a #\newline))
          "\n")))))

(define (docstring-split-signature a line-prefix c)
  "string string procedure:{string:type-signatures string:rest-of-docstring} -> any
   if a string starts with a type-signature, split string at the end of it"
  (if a
    (let (signature (match-pattern peg-type-signature a))
      (if signature
        (c
          (parsed-type-signature->string (type-signature-simplify-tree (peg:tree signature))
            line-prefix)
          (docstring-format (string-trim (string-drop a (peg:end signature)))))
        (c #f (docstring-format a))))
    (c #f #f)))

(define (lines->docstring a indent-string) "list (string ...) -> string"
  (let (a (remove string-null? (if a (any->list a) null)))
    (if (null? a) ""
      (string-append indent-string "description"
        (string-join a (string-append "\n" indent-string indent-string) (q prefix)) "\n"))))

(define (list-replace-underscores& a state c) "call (c list-with-replacements updated-state)"
  (apply c
    ( (l (a) (pair (reverse (first a)) (tail a)))
      (fold-multiple
        (l (a result state) (list (pair (replace-underscore a state) result) (+ 1 state))) a
        (list) state))))

(define (replace-underscores& required optional c)
  (list-replace-underscores& (map symbol->string required) 0
    (l (required state)
      (list-replace-underscores& (map symbol->string optional) state
        (l (optional state) (c required optional state))))))

(define (default-format-arguments arguments type)
  "pair/list:alist symbol:\"procedure\"/\"syntax\"/\"variable\" -> string
   formats argumets in sph type-signature notation format.
   arguments as retrieved by \"module-binding-info\", which uses (ice-9 session) \"procedure-arguments\""
  "syntax arguments can be pairs"
  (if
    (and (eqv? (q procedure) type) (list? arguments)
      (not (null? arguments)) (pair? (first arguments)))
    (alist-bind arguments (optional rest required keyword allow-other-keys?)
      (replace-underscores& required optional
        (l (required optional state)
          (let
            ( (optional-string
                (if (or (not optional) (null? optional)) ""
                  (string-append "[" (string-join optional " ") "]")))
              (required-string
                (if (or (not required) (null? required)) "" (string-join required " ")))
              (keyword-string
                (if (or (not keyword) (null? keyword)) ""
                  (string-drop-right (string-drop (simple-format #f "~S" (map first keyword)) 1) 1)))
              (rest-string
                (if (or (not rest) (null? rest)) ""
                  (string-append (replace-underscore (symbol->string rest) state) " ..."))))
            (let
              (signature-string
                (string-append
                  (string-join
                    (filter (l (a) (not (string-null? a)))
                      (list required-string keyword-string optional-string rest-string))
                    " ")
                  " ->"))
              signature-string)))))
    (if (equal? (q syntax) type)
      (let ((r (simple-format #f "~S" arguments)))
        (string-append (substring r 1 (- (string-length r) 1))))
      (if (equal? (q variable) type)
        (if arguments (call-with-output-string (l (port) (display arguments port))) "") ""))))

(define (module-description name)
  "(symbol ...) -> false/string
   get the module description from an exported variable with a specific name:
     (a b c) -> a-b-c-description"
  (and-let*
    ( (a
        (false-if-exception
          (module-ref (resolve-module name)
            (string->symbol
              (string-append (string-join (map symbol->string name) "-") "-description"))))))
    "remove excess indentation"
    (let (a-tree (indent-tree->denoted-tree a))
      (denoted-tree->indent-tree
        (if (>= 1 (length a-tree)) a-tree
          (pair (first a-tree) (denoted-tree-minimise-depth (tail a-tree))))))))

(define (sort-module-information a)
  (let
    (b
      (map
        (l (a)
          (alist-bind a (name)
            (pair (if name (apply string-append (map symbol->string name)) "") a)))
        a))
    (map tail (list-sort (l (a b) (string<? (first a) (first b))) b))))

(define (module-find-one-information search-paths . module-find-one-arguments)
  (map
    (l (a) (alist-q name (first a) full-path (tail a) description (module-description (first a))))
    (apply append
      (filter-map (l (a) (apply module-find a module-find-one-arguments)) (any->list search-paths)))))

(define (module-find-one-information-sorted search-paths . module-find-arguments)
  (sort-module-information (apply module-find-one-information search-paths module-find-arguments)))

(define* (display-module-information-short a #:optional markdown?)
  (let
    ( (get-first-line
        (l (a)
          (let (index (string-index a #\newline))
            (if index (string-trim-right (string-take a index) #\.) a)))))
    (each
      (l (a)
        (alist-bind a (name description)
          (if markdown? (display "* ")) (display name)
          (if description (begin (display " - ") (display (get-first-line description)))))
        (newline))
      a)))

(define* (doc-bindings libraries #:optional (pair pair))
  "(list ...) -> ((symbol:name . list:library-name) ...)
   get a list of all bindings and the library name they belong to for all specified library names"
  (append-map
    (l (library)
      (or
        (false-if-exception
          (map (l (export) (pair export library)) (module-exports (resolve-interface library))))
        null))
    libraries))

(define* (format-module-documentation module-name #:optional (format-config output-format-indent))
  "(symbol ...) [list] ->
   return a string for the documentation found in a module (binding names, arguments and docstrings).
   for just retrieving module documentation as scheme data consider (sph module binding-info).
   example:
     (format-module-documentation (quote (rnrs sorting)))"
  (alist-bind format-config (format-arguments format-binding-info format-module-documentation)
    (format-module-documentation module-name
      (map
        (l (binding-info)
          (format-binding-info binding-info
            (format-arguments (bi-arguments binding-info) (bi-type binding-info))))
        (sort-module-binding-info (module-binding-info module-name))))))

(define-module (sph documentation))

(use-modules (ice-9 peg) (sph)
  ((sph alist) #:select (alist-q alist-bind)) (sph module binding-info)
  ((sph lang indent-syntax) #:select (denoted-tree->indent-tree indent-tree->denoted-tree))
  (sph lang parser type-signature) ((sph lang scm-format format) #:select (format-docstring))
  ((sph list) #:select (contains? any->list fold-multiple))
  ((sph module) #:select (module-find module-exports))
  ((sph tree) #:select (denoted-tree-minimise-depth)) ((rnrs sorting) #:select (list-sort))
  ((sph string) #:select (string-equal? any->string-pretty-print)) (srfi srfi-1) (srfi srfi-2))

(export default-format-arguments display-module-information-short
  doc-bindings docstring->lines
  docstring-split-signature format-module-documentation
  output-format-indent output-format-signature
  output-format-markdown output-format-list
  lines->docstring module-description
  module-find-one-information module-find-one-information-sorted
  sort-module-information sph-documentation-description)

(define sph-documentation-description
  "extract and display documentation (bindings, arguments and docstrings) from modules")

(define indent-string (string #\space #\space))
(define (replace-underscore a state) (if (string-equal? "_" a) (number->string (+ 10 state) 32) a))

(define output-format-markdown
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (string-append "# " (symbol->string (bi-name bi))
        "\n"
        (docstring-split-signature (bi-documentation bi) #f
          (l (signature docstring)
            (string-append
              (if (or signature (not (string-null? formatted-arguments)))
                (string-append "## signature\n"
                  (if (string-null? formatted-arguments) formatted-arguments
                    (string-append formatted-arguments "\n"))
                  (if signature (string-append signature "\n") ""))
                "")
              (if docstring
                (string-append "## " (lines->docstring (string-split docstring #\newline) "")) ""))))
        "## type\n" (symbol->string (bi-type bi)) "\n"))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n"))))

(define output-format-indent
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (string-append (symbol->string (bi-name bi)) "\n"
        (docstring-split-signature (bi-documentation bi)
          (string-append indent-string indent-string)
          (l (signature docstring)
            (string-append
              (if (or signature (not (string-null? formatted-arguments)))
                (string-append indent-string "signature\n"
                  indent-string indent-string
                  (if (string-null? formatted-arguments) formatted-arguments
                    (string-append formatted-arguments "\n"))
                  (if signature (string-append indent-string indent-string signature "\n") ""))
                "")
              (if docstring (lines->docstring (string-split docstring #\newline) indent-string) ""))))
        indent-string "type: " (symbol->string (bi-type bi))))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n" (q suffix)))))

(define output-format-signature
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments) "vector:record string -> string"
      (docstring-split-signature (bi-documentation bi) ""
        (l (signature text-lines)
          (let
            ( (arguments-string formatted-arguments)
              (docstring
                (string-join (remove string-null? (if text-lines (any->list text-lines) null))
                  "\n  " (q prefix))))
            (string-append (symbol->string (bi-name bi))
              (if (contains? (q (procedure syntax)) (bi-type bi))
                (string-append " :: " arguments-string) ""))))))
    format-module-documentation
    (l (module-name md) "any (string ...) -> string" (string-join md "\n" (q suffix)))))

(define output-format-list
  (alist-q format-arguments default-format-arguments
    format-binding-info
    (l (bi formatted-arguments)
      (pair (symbol->string (bi-name bi))
        (append
          (docstring-split-signature (bi-documentation bi) ""
            (l (signature text)
              (append
                (if signature
                  (list
                    (pair (q signature)
                      (if (string-null? formatted-arguments) (list signature)
                        (list formatted-arguments signature))))
                  (if (string-null? formatted-arguments) (list)
                    (list (list (q signature) formatted-arguments))))
                (if (or (not text) (string-null? text)) null (list (list (q description) text))))))
          (list (list (q type) (bi-type bi))))))
    format-module-documentation (l (module-name md) (any->string-pretty-print md))))

(define (docstring-format a) "string -> string"
  "drop doublequotes of formatted string literal"
  (if (string-null? a) a
    (let* ((a (format-docstring a #f indent-string 0)) (a (substring a 1 (- (string-length a) 1))))
      (if (string-null? a) ""
        (string-join
          (map
            (l (line)
              "remove one eventual extra space from the beginning of lines when the docstring
               is formatted to offset the doublequote of the string literal"
              (let (index (string-skip line #\space))
                (if (and index (odd? index)) (string-drop line 1) line)))
            (string-split a #\newline))
          "\n")))))

(define (docstring-split-signature a line-prefix c)
  "string string procedure:{string:type-signatures string:rest-of-docstring} -> any
   if a string starts with a type-signature, split string at the end of it"
  (if a
    (let (signature (match-pattern peg-type-signature a))
      (if signature
        (c
          (parsed-type-signature->string (type-signature-simplify-tree (peg:tree signature))
            line-prefix)
          (docstring-format (string-trim (string-drop a (peg:end signature)))))
        (c #f (docstring-format a))))
    (c #f #f)))

(define (lines->docstring a indent-string) "list (string ...) -> string"
  (let (a (remove string-null? (if a (any->list a) null)))
    (if (null? a) ""
      (string-append indent-string "description"
        (string-join a (string-append "\n" indent-string indent-string) (q prefix)) "\n"))))

(define (list-replace-underscores& a state c) "call (c list-with-replacements updated-state)"
  (apply c
    ( (l (a) (pair (reverse (first a)) (tail a)))
      (fold-multiple
        (l (a result state) (list (pair (replace-underscore a state) result) (+ 1 state))) a
        (list) state))))

(define (replace-underscores& required optional c)
  (list-replace-underscores& (map symbol->string required) 0
    (l (required state)
      (list-replace-underscores& (map symbol->string optional) state
        (l (optional state) (c required optional state))))))

(define (default-format-arguments arguments type)
  "pair/list:alist symbol:\"procedure\"/\"syntax\"/\"variable\" -> string
   formats argumets in sph type-signature notation format.
   arguments as retrieved by \"module-binding-info\", which uses (ice-9 session) \"procedure-arguments\""
  "syntax arguments can be pairs"
  (if
    (and (eqv? (q procedure) type) (list? arguments)
      (not (null? arguments)) (pair? (first arguments)))
    (alist-bind arguments (optional rest required keyword allow-other-keys?)
      (replace-underscores& required optional
        (l (required optional state)
          (let
            ( (optional-string
                (if (or (not optional) (null? optional)) ""
                  (string-append "[" (string-join optional " ") "]")))
              (required-string
                (if (or (not required) (null? required)) "" (string-join required " ")))
              (keyword-string
                (if (or (not keyword) (null? keyword)) ""
                  (string-drop-right (string-drop (simple-format #f "~S" (map first keyword)) 1) 1)))
              (rest-string
                (if (or (not rest) (null? rest)) ""
                  (string-append (replace-underscore (symbol->string rest) state) " ..."))))
            (let
              (signature-string
                (string-append
                  (string-join
                    (filter (l (a) (not (string-null? a)))
                      (list required-string keyword-string optional-string rest-string))
                    " ")
                  " ->"))
              signature-string)))))
    (if (equal? (q syntax) type)
      (let ((r (simple-format #f "~S" arguments)))
        (string-append (substring r 1 (- (string-length r) 1))))
      (if (equal? (q variable) type)
        (if arguments (call-with-output-string (l (port) (display arguments port))) "") ""))))

(define (module-description name)
  "(symbol ...) -> false/string
   get the module description from an exported variable with a specific name:
     (a b c) -> a-b-c-description"
  (and-let*
    ( (a
        (false-if-exception
          (module-ref (resolve-module name)
            (string->symbol
              (string-append (string-join (map symbol->string name) "-") "-description"))))))
    "remove excess indentation"
    (let (a-tree (indent-tree->denoted-tree a))
      (denoted-tree->indent-tree
        (if (>= 1 (length a-tree)) a-tree
          (pair (first a-tree) (denoted-tree-minimise-depth (tail a-tree))))))))

(define (sort-module-information a)
  (let
    (b
      (map
        (l (a)
          (alist-bind a (name)
            (pair (if name (apply string-append (map symbol->string name)) "") a)))
        a))
    (map tail (list-sort (l (a b) (string<? (first a) (first b))) b))))

(define (module-find-one-information search-paths . module-find-one-arguments)
  (map
    (l (a) (alist-q name (first a) full-path (tail a) description (module-description (first a))))
    (apply append
      (filter-map (l (a) (apply module-find a module-find-one-arguments)) (any->list search-paths)))))

(define (module-find-one-information-sorted search-paths . module-find-arguments)
  (sort-module-information (apply module-find-one-information search-paths module-find-arguments)))

(define* (display-module-information-short a #:optional markdown?)
  (let
    ( (get-first-line
        (l (a)
          (let (index (string-index a #\newline))
            (if index (string-trim-right (string-take a index) #\.) a)))))
    (each
      (l (a)
        (alist-bind a (name description)
          (if markdown? (display "* ")) (display name)
          (if description (begin (display " - ") (display (get-first-line description)))))
        (newline))
      a)))

(define* (doc-bindings libraries #:optional (pair pair))
  "(list ...) -> ((symbol:name . list:library-name) ...)
   get a list of all bindings and the library name they belong to for all specified library names"
  (append-map
    (l (library)
      (or
        (false-if-exception
          (map (l (export) (pair export library)) (module-exports (resolve-interface library))))
        null))
    libraries))

(define* (format-module-documentation module-name #:optional (format-config output-format-indent))
  "(symbol ...) [list] ->
   return a string for the documentation found in a module (binding names, arguments and docstrings).
   for just retrieving module documentation as scheme data consider (sph module binding-info).
   example:
     (format-module-documentation (quote (rnrs sorting)))"
  (alist-bind format-config (format-arguments format-binding-info format-module-documentation)
    (format-module-documentation module-name
      (map
        (l (binding-info)
          (format-binding-info binding-info
            (format-arguments (bi-arguments binding-info) (bi-type binding-info))))
        (sort-module-binding-info (module-binding-info module-name))))))
