;;>| Utility Parsing Combinators
;;>
;;> Additional high-level combinators implemented using the aforementioned
;;> [basic combinators][basic section]. The provided high-level combinators,
;;> return more useful parser error messages then the basic ones.
;;>
;;> [basic section]: #section-basic-parsing-combinators

;;> Parser which always fails with the given error message.

(define (parse-fail msg)
  (lambda (source index sk fk)
    (fk source index msg)))

;;> Bind a constant `value` to a given `parser`.
;;> That is, always return this value if the parser succeeds.

(define (parse-bind value parser)
  (parse-map
    parser
    (lambda (x) value)))

;;> Run a parser `f`, which *must* return a list, and convert
;;> its return value to a string using the `list->string` procedure.

(define (parse-as-string parser)
  (parse-map
    parser
    list->string))

;;> Parse one or more digits (i.e. `0-9`), interpret them as a
;;> decimal number, and return this number.

(define parse-digits
  (parse-with-failure-reason
    (parse-map
      (parse-token char-set:digit)
      string->number)
    "expected digits"))

;;> Parse an ASCII lowercase character (i.e. `a-z`).

(define parse-lowercase
  (parse-with-failure-reason
    (parse-char char-set:lower-case)
    "expected lowercase character"))

;;> Attempt parsing using the given parser `f`, if it fails return a default value `def`.

(define (parse-default f def)
  (parse-map
    (parse-optional f)
    (lambda (x)
      (if x x def))))

;;> Parse a newline character.

(define parse-newline
  (parse-with-failure-reason
    (parse-char #\newline)
    "expected newline"))

;;> Parse a single blank character (i.e. horizontal whitespace).

(define parse-blank
  (parse-with-failure-reason
    (parse-char char-set:blank)
    "expected whitespace"))

;;> Parse one or more blank characters.

(define parse-blanks+
  (parse-with-failure-reason
    (parse-token char-set:blank)
    "expected whitespaces"))

;;> Parse zero or more blank characters.

(define parse-blanks
  (parse-optional parse-blanks+))

;;> Invokes parser `f` between the parsers `lhs` and `rhs`.

(define (parse-between lhs f rhs)
  (parse-map
    (parse-seq lhs f rhs)
    cadr))

;;> Returns the result of parser `f` but allows preceding its input
;;> with a backslash character to escape it in the parsed input format.

(define (parse-esc f)
  (parse-map
    (parse-seq
      (parse-char #\\)
      f)
    cadr))

;;> Parse an `alist` mapping chars to values which should be returned for each char.

(define (parse-alist alist)
  (apply
    parse-or
    (map
      (lambda (x)
        (parse-map
          (parse-char (car x))
          (lambda (_)
            (cdr x))))
      alist)))

;; Utility procedure for parsing ed(1) BRE addresses.

(define (%parse-regex-lit ch end)
  (parse-with-failure-reason
    (parse-atomic
      (parse-as-string
        (parse-between
          (parse-char ch)
          (parse-repeat (parse-or
                          (parse-esc (parse-char ch))
                          (parse-char (char-set-complement (char-set ch #\newline)))))
          end)))
    "expected regex"))

;;> Parse a regex literal which is enclosed by the character `ch`.

(define (parse-regex-lit ch)
  (%parse-regex-lit
    ch
    (parse-char ch)))

;;> Parse a regex literal which starts with character `ch` and is
;;> terminated by the same character or the end of line.

(define (parse-regex-lit* ch)
  (%parse-regex-lit
    ch
    (parse-or
      (parse-char ch)
      parse-end-of-line)))

;;> Invoke given parser and strip trailing blanks (if any).

(define (parse-strip-blanks parser)
  (parse-map
    (parse-seq
      parser
      parse-blanks)
    car))

;;> Like [parse-seq](#parse-seq) but skip blanks *before* each parser.

(define (parse-blanks-seq . o)
  (define (%parse-blanks-seq lst)
    (parse-seq-list
      (apply append
             (zip (make-list
                        (length lst)
                        (parse-ignore parse-blanks))
                  lst))))

  (%parse-blanks-seq o))

;;> Parse a single line (excluding the terminating newline character).

(define parse-line
  (parse-atomic
    (parse-or
      (parse-bind "" parse-newline) ;; empty line
      (parse-map
        (parse-seq
          (parse-token (lambda (x) (not (char=? x #\newline))))
          ;; XXX: parse-end-of-line does _not_ consume the end-of-file.
          ;; This is crucial for parse-input-mode to work correctly.
          (parse-or parse-newline parse-end-of-line))
        car))))

;;> Feed the result of the parser `ctx` to a single argument procedure `f`.
;;> The procedure must then return a new parser which is executed
;;> afterwards on the same index as `ctx`.

(define (parse-with-context ctx f)
  (lambda (source index sk fk)
    (let* ((yield (lambda (r s i fk) r))
           (value (call-with-parse ctx source index yield)))
      (if value ;; default fk returns #f on error
        ((f value) source index sk fk)
        (fk source index "context parser failed")))))
