;; clojure.pprint — a column-aware pretty printer and a Common Lisp compatible
;; cl-format. The writer accumulates into a StringBuilder; pprint/write/cl-format
;; bind *out* (this ns's own dynamic binding) to a pretty-writer over it and emit
;; the result through clojure.core/print, so with-out-str captures it.
;;
;; Native print is not involved: this ns defines its own print/pr/prn/println
;; that route through (-write *out* ...).
(ns clojure.pprint
  (:refer-clojure :exclude [deftype print println pr prn]))

;;======================================================================
;; Macros (must precede their first use)
;;======================================================================

(defmacro getf
  "Get the value of the field named by the keyword sym from *out*'s field atom."
  [sym]
  `(~sym (deref (.-fields ~'this))))

(defmacro setf
  "Set the value of field sym in *out*'s field atom."
  [sym new-val]
  `(swap! (.-fields ~'this) assoc ~sym ~new-val))

(defmacro deftype
  "Builds a defrecord plus make-X / X? helpers (a tagged record used as a
  pretty-printer buffer token)."
  [type-name & fields]
  (let [name-str (name type-name)
        fields (map (comp symbol name) fields)]
    `(do
       (defrecord ~type-name [~'type-tag ~@fields])
       (defn- ~(symbol (str "make-" name-str))
         ~(vec fields)
         (~(symbol (str "->" type-name)) ~(keyword name-str) ~@fields))
       (defn- ~(symbol (str name-str "?")) [x#] (= (:type-tag x#) ~(keyword name-str))))))

(defmacro pprint-logical-block
  "Execute body as a pretty-printing logical block, output to *out* which must be
  a pretty-printing writer. Options :prefix, :per-line-prefix and :suffix may
  precede the body."
  [& args]
  (let [[options body] (loop [body args acc []]
                         (if (#{:prefix :per-line-prefix :suffix} (first body))
                           (recur (drop 2 body) (concat acc (take 2 body)))
                           [(apply hash-map acc) body]))]
    `(do (if (clojure.pprint/level-exceeded)
           (-write clojure.core/*out* "#")
           (clojure.core/binding [clojure.pprint/*current-level* (inc clojure.pprint/*current-level*)
                                  clojure.pprint/*current-length* 0]
             (clojure.pprint/start-block clojure.core/*out*
                                        ~(:prefix options)
                                        ~(:per-line-prefix options)
                                        ~(:suffix options))
             ~@body
             (clojure.pprint/end-block clojure.core/*out*)))
         nil)))

(defmacro print-length-loop
  "A loop for pretty-printer dispatch functions. (Length limiting via
  *print-length* is enforced by write-out, not by truncating the loop.)"
  [bindings & body]
  `(loop ~bindings ~@body))

(defmacro formatter-out
  "Returns a function (fn [& args]) that runs the compiled format against *out*.
  format-in is a control string or a previously compiled format."
  [format-in]
  `(let [format-in# ~format-in
         cf# (if (string? format-in#) (clojure.pprint/cached-compile format-in#) format-in#)]
     (fn [& args#]
       (let [navigator# (clojure.pprint/init-navigator args#)]
         (clojure.pprint/execute-format cf# navigator#)))))

(defmacro formatter
  "Returns a function (fn [stream & args]) that runs the compiled format."
  [format-in]
  `(let [format-in# ~format-in
         cf# (if (string? format-in#) (clojure.pprint/cached-compile format-in#) format-in#)]
     (fn [stream# & args#]
       (let [navigator# (clojure.pprint/init-navigator args#)]
         (clojure.pprint/execute-format stream# cf# navigator#)))))

(defmacro with-pprint-dispatch
  "Execute body with the pretty-print dispatch function bound to function. A
  non-function dispatch (e.g. a keyword) is accepted and ignored."
  [function & body]
  `(clojure.core/binding [clojure.pprint/*print-pprint-dispatch* ~function]
     ~@body))

(defmacro pp
  "Pretty print the last REPL result. (jolt has no *1, so this is a no-op stub.)"
  [] `nil)

;;======================================================================
;; print fns that route through *out*
;;======================================================================

(defn- print [& more]
  (-write *out* (apply print-str more)))

(defn- println [& more]
  (apply print more)
  (-write *out* "\n"))

(defn- print-char [c]
  (-write *out* (condp = c
                  \backspace "\\backspace"
                  \space "\\space"
                  \tab "\\tab"
                  \newline "\\newline"
                  \formfeed "\\formfeed"
                  \return "\\return"
                  \" "\\\""
                  \\ "\\\\"
                  (str "\\" c))))

(defn- ^:dynamic pr [& more]
  (-write *out* (apply pr-str more)))

(defn- prn [& more]
  (apply pr more)
  (-write *out* "\n"))

;;======================================================================
;; utils
;;======================================================================

(defn- float? [n]
  (and (number? n)
       (not (integer? n))))

(defn- char-code [c]
  (cond
    (number? c) c
    (char? c) (int c)
    (and (string? c) (= (count c) 1)) (int (.charAt c 0))
    :else (throw (Exception. "Argument to char must be a character or number"))))

(defn- map-passing-context [func initial-context lis]
  (loop [context initial-context
         lis lis
         acc []]
    (if (empty? lis)
      [acc context]
      (let [this (first lis)
            remainder (next lis)
            [result new-context] (apply func [this context])]
        (recur new-context remainder (conj acc result))))))

(defn- consume [func initial-context]
  (loop [context initial-context
         acc []]
    (let [[result new-context] (apply func [context])]
      (if (not result)
        [acc new-context]
        (recur new-context (conj acc result))))))

(defn- unzip-map [m]
  [(into {} (for [[k [v1 v2]] m] [k v1]))
   (into {} (for [[k [v1 v2]] m] [k v2]))])

(defn- tuple-map [m v1]
  (into {} (for [[k v] m] [k [v v1]])))

(defn- rtrim [s c]
  (let [len (count s)]
    (if (and (pos? len) (= (nth s (dec (count s))) c))
      (loop [n (dec len)]
        (cond
          (neg? n) ""
          (not (= (nth s n) c)) (subs s 0 (inc n))
          true (recur (dec n))))
      s)))

(defn- ltrim [s c]
  (let [len (count s)]
    (if (and (pos? len) (= (nth s 0) c))
      (loop [n 0]
        (if (or (= n len) (not (= (nth s n) c)))
          (subs s n)
          (recur (inc n))))
      s)))

(defn- prefix-count [aseq val]
  (let [test (if (coll? val) (set val) #{val})]
    (loop [pos 0]
      (if (or (= pos (count aseq)) (not (test (nth aseq pos))))
        pos
        (recur (inc pos))))))

(defprotocol IPrettyFlush
  (-ppflush [pp]))

(defprotocol IPrettyWriter
  (-write [w x])
  (-pflush [w]))

;;======================================================================
;; column writer
;;======================================================================

(def ^:dynamic ^{:private true} *default-page-width* 72)

(defn- get-field [this sym]
  (sym (deref (.-fields this))))

(defn- set-field [this sym new-val]
  (swap! (.-fields this) assoc sym new-val))

(defn- get-column [this]
  (get-field this :cur))

(defn- get-line [this]
  (get-field this :line))

(defn- get-max-column [this]
  (get-field this :max))

(defn- set-max-column [this new-max]
  (set-field this :max new-max)
  nil)

(defn- get-writer [this]
  (get-field this :base))

(defn- c-write-char [this c]
  (if (= c \newline)
    (do
      (set-field this :cur 0)
      (set-field this :line (inc (get-field this :line))))
    (set-field this :cur (inc (get-field this :cur))))
  (-write (get-field this :base) c))

;; the base sink: accumulates into a StringBuilder. Columns are tracked by the
;; column-writer that wraps it; this just appends.
(defrecord StringBufferWriter [sb]
  IPrettyWriter
  (-write [_ x]
    (.append sb (if (char? x) (str x) x))
    nil)
  (-pflush [_] nil)
  IPrettyFlush
  (-ppflush [_] nil))

(defrecord ColumnWriter [fields]
  IPrettyWriter
  (-write [this x]
    (cond
      (string? x)
      (let [s x
            nl (.lastIndexOf s "\n")]
        (if (neg? nl)
          (set-field this :cur (+ (get-field this :cur) (count s)))
          (do
            (set-field this :cur (- (count s) nl 1))
            (set-field this :line (+ (get-field this :line)
                                     (count (filter #(= % \newline) s))))))
        (-write (get-field this :base) s))
      (or (char? x) (number? x))
      (c-write-char this x)))
  (-pflush [this] (-pflush (get-field this :base)))
  IPrettyFlush
  (-ppflush [_] nil))

(defn- column-writer
  ([writer] (column-writer writer *default-page-width*))
  ([writer max-columns]
   (->ColumnWriter (atom {:max max-columns :cur 0 :line 0 :base writer}))))

;;======================================================================
;; pretty writer
;;======================================================================

(declare ^{:arglists '([this])} get-miser-width)

(defrecord logical-block
  [parent section start-col indent
   done-nl intra-block-nl
   prefix per-line-prefix suffix
   logical-block-callback])

(defn- ancestor? [parent child]
  (loop [child (:parent child)]
    (cond
      (nil? child) false
      (identical? parent child) true
      :else (recur (:parent child)))))

(defn- buffer-length [l]
  (let [l (seq l)]
    (if l
      (- (:end-pos (last l)) (:start-pos (first l)))
      0)))

(deftype buffer-blob :data :trailing-white-space :start-pos :end-pos)
(deftype nl-t :type :logical-block :start-pos :end-pos)
(deftype start-block-t :logical-block :start-pos :end-pos)
(deftype end-block-t :logical-block :start-pos :end-pos)
(deftype indent-t :logical-block :relative-to :offset :start-pos :end-pos)

(def ^:private pp-newline (fn [] "\n"))

(declare emit-nl)

(defmulti ^{:private true} write-token (fn [this token] (:type-tag token)))

(defmethod write-token :start-block-t [this token]
  (when-let [cb (getf :logical-block-callback)] (cb :start))
  (let [lb (:logical-block token)]
    (when-let [prefix (:prefix lb)]
      (-write (getf :base) prefix))
    (let [col (get-column (getf :base))]
      (reset! (:start-col lb) col)
      (reset! (:indent lb) col))))

(defmethod write-token :end-block-t [this token]
  (when-let [cb (getf :logical-block-callback)] (cb :end))
  (when-let [suffix (:suffix (:logical-block token))]
    (-write (getf :base) suffix)))

(defmethod write-token :indent-t [this token]
  (let [lb (:logical-block token)]
    (reset! (:indent lb)
            (+ (:offset token)
               (condp = (:relative-to token)
                 :block @(:start-col lb)
                 :current (get-column (getf :base)))))))

(defmethod write-token :buffer-blob [this token]
  (-write (getf :base) (:data token)))

(defmethod write-token :nl-t [this token]
  (if (or (= (:type token) :mandatory)
          (and (not (= (:type token) :fill))
               @(:done-nl (:logical-block token))))
    (emit-nl this token)
    (if-let [tws (getf :trailing-white-space)]
      (-write (getf :base) tws)))
  (setf :trailing-white-space nil))

(defn- write-tokens [this tokens force-trailing-whitespace]
  (doseq [token tokens]
    (if-not (= (:type-tag token) :nl-t)
      (if-let [tws (getf :trailing-white-space)]
        (-write (getf :base) tws)))
    (write-token this token)
    (setf :trailing-white-space (:trailing-white-space token))
    (let [tws (getf :trailing-white-space)]
      (when (and force-trailing-whitespace tws)
        (-write (getf :base) tws)
        (setf :trailing-white-space nil)))))

(defn- tokens-fit? [this tokens]
  (let [maxcol (get-max-column (getf :base))]
    (or
      (nil? maxcol)
      (< (+ (get-column (getf :base)) (buffer-length tokens)) maxcol))))

(defn- linear-nl? [this lb section]
  (or @(:done-nl lb)
      (not (tokens-fit? this section))))

(defn- miser-nl? [this lb section]
  (let [miser-width (get-miser-width this)
        maxcol (get-max-column (getf :base))]
    (and miser-width maxcol
         (>= @(:start-col lb) (- maxcol miser-width))
         (linear-nl? this lb section))))

(defmulti ^{:private true} emit-nl? (fn [t _ _ _] (:type t)))

(defmethod emit-nl? :linear [newl this section _]
  (let [lb (:logical-block newl)]
    (linear-nl? this lb section)))

(defmethod emit-nl? :miser [newl this section _]
  (let [lb (:logical-block newl)]
    (miser-nl? this lb section)))

(defmethod emit-nl? :fill [newl this section subsection]
  (let [lb (:logical-block newl)]
    (or @(:intra-block-nl lb)
        (not (tokens-fit? this subsection))
        (miser-nl? this lb section))))

(defmethod emit-nl? :mandatory [_ _ _ _]
  true)

(defn- get-section [buffer]
  (let [nl (first buffer)
        lb (:logical-block nl)
        section (seq (take-while #(not (and (nl-t? %) (ancestor? (:logical-block %) lb)))
                                 (next buffer)))]
    [section (seq (drop (inc (count section)) buffer))]))

(defn- get-sub-section [buffer]
  (let [nl (first buffer)
        lb (:logical-block nl)
        section (seq (take-while #(let [nl-lb (:logical-block %)]
                                    (not (and (nl-t? %) (or (= nl-lb lb) (ancestor? nl-lb lb)))))
                                 (next buffer)))]
    section))

(defn- update-nl-state [lb]
  (reset! (:intra-block-nl lb) true)
  (reset! (:done-nl lb) true)
  (loop [lb (:parent lb)]
    (if lb
      (do (reset! (:done-nl lb) true)
          (reset! (:intra-block-nl lb) true)
          (recur (:parent lb))))))

(defn- emit-nl [this nl]
  (-write (getf :base) (pp-newline))
  (setf :trailing-white-space nil)
  (let [lb (:logical-block nl)
        prefix (:per-line-prefix lb)]
    (if prefix
      (-write (getf :base) prefix))
    (let [istr (apply str (repeat (- @(:indent lb) (count prefix)) \space))]
      (-write (getf :base) istr))
    (update-nl-state lb)))

(defn- split-at-newline [tokens]
  (let [pre (seq (take-while #(not (nl-t? %)) tokens))]
    [pre (seq (drop (count pre) tokens))]))

(defn- write-token-string [this tokens]
  (let [[a b] (split-at-newline tokens)]
    (if a (write-tokens this a false))
    (if b
      (let [[section remainder] (get-section b)
            newl (first b)]
        (let [do-nl (emit-nl? newl this section (get-sub-section b))
              result (if do-nl
                       (do
                         (emit-nl this newl)
                         (next b))
                       b)
              long-section (not (tokens-fit? this result))
              result (if long-section
                       (let [rem2 (write-token-string this section)]
                         (if (= rem2 section)
                           (do
                             (write-tokens this section false)
                             remainder)
                           (into [] (concat rem2 remainder))))
                       result)]
          result)))))

(defn- write-line [this]
  (loop [buffer (getf :buffer)]
    (setf :buffer (into [] buffer))
    (if (not (tokens-fit? this buffer))
      (let [new-buffer (write-token-string this buffer)]
        (if-not (identical? buffer new-buffer)
          (recur new-buffer))))))

(defn- add-to-buffer [this token]
  (setf :buffer (conj (getf :buffer) token))
  (if (not (tokens-fit? this (getf :buffer)))
    (write-line this)))

(defn- write-buffered-output [this]
  (write-line this)
  (if-let [buf (getf :buffer)]
    (do
      (write-tokens this buf true)
      (setf :buffer []))))

(defn- write-white-space [this]
  (when-let [tws (getf :trailing-white-space)]
    (-write (getf :base) tws)
    (setf :trailing-white-space nil)))

(defn- write-initial-lines [this s]
  (let [lines (clojure.string/split s #"\n" -1)]
    (if (= (count lines) 1)
      s
      (let [prefix (:per-line-prefix (first (getf :logical-blocks)))
            l (first lines)]
        (if (= :buffering (getf :mode))
          (let [oldpos (getf :pos)
                newpos (+ oldpos (count l))]
            (setf :pos newpos)
            (add-to-buffer this (make-buffer-blob l nil oldpos newpos))
            (write-buffered-output this))
          (do
            (write-white-space this)
            (-write (getf :base) l)))
        (-write (getf :base) "\n")
        (doseq [l (next (butlast lines))]
          (-write (getf :base) l)
          (-write (getf :base) (pp-newline))
          (if prefix
            (-write (getf :base) prefix)))
        (setf :buffering :writing)
        (last lines)))))

(defn- p-write-char [this c]
  (if (= (getf :mode) :writing)
    (do
      (write-white-space this)
      (-write (getf :base) c))
    (if (= c \newline)
      (write-initial-lines this "\n")
      (let [oldpos (getf :pos)
            newpos (inc oldpos)]
        (setf :pos newpos)
        (add-to-buffer this (make-buffer-blob (str c) nil oldpos newpos))))))

(defrecord PrettyWriter [fields]
  IPrettyWriter
  (-write [this x]
    (cond
      (string? x)
      (let [s0 (write-initial-lines this x)
            s (clojure.string/replace-first s0 #"\s+$" "")
            white-space (subs s0 (count s))
            mode (getf :mode)]
        (if (= mode :writing)
          (do
            (write-white-space this)
            (-write (getf :base) s)
            (setf :trailing-white-space white-space))
          (let [oldpos (getf :pos)
                newpos (+ oldpos (count s0))]
            (setf :pos newpos)
            (add-to-buffer this (make-buffer-blob s white-space oldpos newpos)))))
      (or (char? x) (number? x))
      (p-write-char this x)))
  (-pflush [this]
    (-ppflush this)
    (-pflush (getf :base)))
  IPrettyFlush
  (-ppflush [this]
    (if (= (getf :mode) :buffering)
      (do
        (write-tokens this (getf :buffer) true)
        (setf :buffer []))
      (write-white-space this))))

(defn- pretty-writer [writer max-columns miser-width]
  (let [lb (->logical-block nil nil (atom 0) (atom 0) (atom false) (atom false)
                            nil nil nil nil)]
    (->PrettyWriter
      (atom {:pretty-writer true
             :base (column-writer writer max-columns)
             :logical-blocks lb
             :sections nil
             :mode :writing
             :buffer []
             :buffer-block lb
             :buffer-level 1
             :miser-width miser-width
             :trailing-white-space nil
             :pos 0}))))

(defn- start-block
  [this prefix per-line-prefix suffix]
  (let [lb (->logical-block (getf :logical-blocks) nil (atom 0) (atom 0)
                            (atom false) (atom false)
                            prefix per-line-prefix suffix nil)]
    (setf :logical-blocks lb)
    (if (= (getf :mode) :writing)
      (do
        (write-white-space this)
        (when-let [cb (getf :logical-block-callback)] (cb :start))
        (if prefix
          (-write (getf :base) prefix))
        (let [col (get-column (getf :base))]
          (reset! (:start-col lb) col)
          (reset! (:indent lb) col)))
      (let [oldpos (getf :pos)
            newpos (+ oldpos (if prefix (count prefix) 0))]
        (setf :pos newpos)
        (add-to-buffer this (make-start-block-t lb oldpos newpos))))))

(defn- end-block [this]
  (let [lb (getf :logical-blocks)
        suffix (:suffix lb)]
    (if (= (getf :mode) :writing)
      (do
        (write-white-space this)
        (if suffix
          (-write (getf :base) suffix))
        (when-let [cb (getf :logical-block-callback)] (cb :end)))
      (let [oldpos (getf :pos)
            newpos (+ oldpos (if suffix (count suffix) 0))]
        (setf :pos newpos)
        (add-to-buffer this (make-end-block-t lb oldpos newpos))))
    (setf :logical-blocks (:parent lb))))

(defn- nl [this type]
  (setf :mode :buffering)
  (let [pos (getf :pos)]
    (add-to-buffer this (make-nl-t type (getf :logical-blocks) pos pos))))

(defn- indent [this relative-to offset]
  (let [lb (getf :logical-blocks)]
    (if (= (getf :mode) :writing)
      (do
        (write-white-space this)
        (reset! (:indent lb)
                (+ offset (condp = relative-to
                            :block @(:start-col lb)
                            :current (get-column (getf :base))))))
      (let [pos (getf :pos)]
        (add-to-buffer this (make-indent-t lb relative-to offset pos pos))))))

(defn- get-miser-width [this]
  (getf :miser-width))

;;======================================================================
;; pprint base
;;======================================================================

(def ^:dynamic *print-pretty* true)
(def ^:dynamic *print-pprint-dispatch* nil)
(def ^:dynamic *print-right-margin* 72)
(def ^:dynamic *print-miser-width* 40)
(def ^:dynamic ^{:private true} *print-lines* nil)
(def ^:dynamic ^{:private true} *print-circle* nil)
(def ^:dynamic ^{:private true} *print-shared* nil)
(def ^:dynamic *print-suppress-namespaces* nil)
(def ^:dynamic *print-radix* nil)
(def ^:dynamic *print-base* 10)
(def ^:dynamic ^{:private true} *current-level* 0)
(def ^:dynamic ^{:private true} *current-length* nil)

;; jolt has no bindable clojure.core/*print-length* / *print-level* vars; define
;; them here so the printer-control machinery can bind and read them.
(def ^:dynamic *print-length* nil)
(def ^:dynamic *print-level* nil)

(declare ^{:arglists '([n])} format-simple-number)

(defn- pretty-writer? [x]
  (and (instance? PrettyWriter x) (:pretty-writer (deref (.-fields x)))))

(defn- make-pretty-writer [base-writer right-margin miser-width]
  (pretty-writer base-writer right-margin miser-width))

(defmacro ^{:private true} with-pretty-writer [base-writer & body]
  `(let [base-writer# ~base-writer
         new-writer# (not (pretty-writer? base-writer#))]
     (clojure.core/binding [clojure.core/*out* (if new-writer#
                                                 (make-pretty-writer base-writer# *print-right-margin* *print-miser-width*)
                                                 base-writer#)]
       ;; route core print into this pretty-writer even under an outer
       ;; with-out-str; nested pr-str/print-str re-suppress so their captures work
       (clojure.core/__with-pprint-routing
         (fn []
           ~@body
           (-ppflush clojure.core/*out*))))))

(defn write-out
  "Write an object to *out* subject to the current bindings of the printer control
  variables. *out* must be a PrettyWriter when pretty printing is enabled."
  [object]
  (let [length-reached (and *current-length*
                            *print-length*
                            (>= *current-length* *print-length*))]
    (if-not *print-pretty*
      (pr object)
      (if length-reached
        (-write *out* "...")
        (do
          (if *current-length* (set! *current-length* (inc *current-length*)))
          (*print-pprint-dispatch* object))))
    length-reached))

(defn write
  "Write an object subject to the current bindings of the printer control
  variables. Returns the string result if :stream is nil, nil otherwise."
  [object & kw-args]
  (let [options (merge {:stream true} (apply hash-map kw-args))]
    (binding [clojure.pprint/*print-base* (get options :base clojure.pprint/*print-base*)
              clojure.pprint/*print-circle* (get options :circle clojure.pprint/*print-circle*)
              clojure.pprint/*print-length* (get options :length clojure.pprint/*print-length*)
              clojure.pprint/*print-level* (get options :level clojure.pprint/*print-level*)
              clojure.pprint/*print-lines* (get options :lines clojure.pprint/*print-lines*)
              clojure.pprint/*print-miser-width* (get options :miser-width clojure.pprint/*print-miser-width*)
              clojure.pprint/*print-pprint-dispatch* (get options :dispatch clojure.pprint/*print-pprint-dispatch*)
              clojure.pprint/*print-pretty* (get options :pretty clojure.pprint/*print-pretty*)
              clojure.pprint/*print-radix* (get options :radix clojure.pprint/*print-radix*)
              clojure.core/*print-readably* (get options :readably clojure.core/*print-readably*)
              clojure.pprint/*print-right-margin* (get options :right-margin clojure.pprint/*print-right-margin*)
              clojure.pprint/*print-suppress-namespaces* (get options :suppress-namespaces clojure.pprint/*print-suppress-namespaces*)]
      (let [sb (StringBuilder.)
            optval (if (contains? options :stream)
                     (:stream options)
                     true)
            base-writer (if (or (true? optval) (nil? optval))
                          (->StringBufferWriter sb)
                          optval)]
        (if *print-pretty*
          (with-pretty-writer base-writer
            (write-out object))
          (binding [*out* base-writer]
            (pr object)))
        (if (true? optval)
          (clojure.core/print (str sb)))
        (if (nil? optval)
          (str sb))))))

(defn pprint
  "Pretty print object. With one arg, prints to *out* (captured by with-out-str).
  The 2-arg form writes to the supplied pretty-writer."
  ([object]
   (let [sb (StringBuilder.)]
     (binding [*out* (->StringBufferWriter sb)]
       (pprint object *out*)
       (clojure.core/print (str sb)))))
  ([object writer]
   (with-pretty-writer writer
     (binding [*print-pretty* true]
       (write-out object))
     (if (not (= 0 (get-column *out*)))
       (-write *out* "\n")))))

(defn set-pprint-dispatch [function]
  (set! *print-pprint-dispatch* function)
  nil)

(defn- check-enumerated-arg [arg choices]
  (if-not (choices arg)
    (throw (Exception. (str "Bad argument: " arg ". It must be one of " choices)))))

(defn- level-exceeded []
  (and *print-level* (>= *current-level* *print-level*)))

(defn pprint-newline
  "Print a conditional newline (:linear :miser :fill or :mandatory) to *out*,
  which must be a pretty-printing writer."
  [kind]
  (check-enumerated-arg kind #{:linear :miser :fill :mandatory})
  (nl *out* kind))

(defn pprint-indent
  "Create an indent at this point in the pretty-printing stream. relative-to is
  :block or :current; n is an offset."
  [relative-to n]
  (check-enumerated-arg relative-to #{:block :current})
  (indent *out* relative-to n))

(defn pprint-tab
  [kind colnum colinc]
  (check-enumerated-arg kind #{:line :section :line-relative :section-relative})
  (throw (Exception. "pprint-tab is not yet implemented")))

;;======================================================================
;; cl-format
;;======================================================================

(declare ^{:arglists '([format-str])} compile-format)
(declare ^{:arglists '([stream format args] [format args])} execute-format)
(declare ^{:arglists '([s])} init-navigator)

(defn cl-format
  "A Common Lisp compatible format function. If writer is nil, returns the
  formatted string; if true, prints to *out*; otherwise writes to writer."
  [writer format-in & args]
  (let [compiled-format (if (string? format-in) (compile-format format-in) format-in)
        navigator (init-navigator args)]
    (execute-format writer compiled-format navigator)))

(def ^:dynamic ^{:private true} *format-str* nil)

(defn- format-error [message offset]
  (let [full-message (str message "\n" *format-str* "\n"
                          (apply str (repeat offset \space)) "^" "\n")]
    (throw (Exception. full-message))))

(defrecord ^{:private true} arg-navigator [seq rest pos])

(defn- init-navigator [s]
  (let [s (seq s)]
    (->arg-navigator s s 0)))

(defn- next-arg [navigator]
  (let [rst (:rest navigator)]
    (if rst
      [(first rst) (->arg-navigator (:seq navigator) (next rst) (inc (:pos navigator)))]
      (throw (Exception. "Not enough arguments for format definition")))))

(defn- next-arg-or-nil [navigator]
  (let [rst (:rest navigator)]
    (if rst
      [(first rst) (->arg-navigator (:seq navigator) (next rst) (inc (:pos navigator)))]
      [nil navigator])))

(defn- get-format-arg [navigator]
  (let [[raw-format navigator] (next-arg navigator)
        compiled-format (if (string? raw-format)
                          (compile-format raw-format)
                          raw-format)]
    [compiled-format navigator]))

(declare relative-reposition)

(defn- absolute-reposition [navigator position]
  (if (>= position (:pos navigator))
    (relative-reposition navigator (- (:pos navigator) position))
    (->arg-navigator (:seq navigator) (drop position (:seq navigator)) position)))

(defn- relative-reposition [navigator position]
  (let [newpos (+ (:pos navigator) position)]
    (if (neg? position)
      (absolute-reposition navigator newpos)
      (->arg-navigator (:seq navigator) (drop position (:rest navigator)) newpos))))

(defrecord ^{:private true} compiled-directive [func dirdef params offset])

(defn- realize-parameter [[param [raw-val offset]] navigator]
  (let [[real-param new-navigator]
        (cond
          (contains? #{:at :colon} param)
          [raw-val navigator]

          (= raw-val :parameter-from-args)
          (next-arg navigator)

          (= raw-val :remaining-arg-count)
          [(count (:rest navigator)) navigator]

          true
          [raw-val navigator])]
    [[param [real-param offset]] new-navigator]))

(defn- realize-parameter-list [parameter-map navigator]
  (let [[pairs new-navigator]
        (map-passing-context realize-parameter navigator parameter-map)]
    [(into {} pairs) new-navigator]))

(declare ^{:arglists '([base val])} opt-base-str)

(def ^{:private true}
  special-radix-markers {2 "#b" 8 "#o" 16 "#x"})

(defn- format-simple-number [n]
  (cond
    (integer? n) (if (= *print-base* 10)
                   (str n (if *print-radix* "."))
                   (str
                     (if *print-radix* (or (get special-radix-markers *print-base*) (str "#" *print-base* "r")))
                     (opt-base-str *print-base* n)))
    :else nil))

(defn- format-ascii [print-func params arg-navigator offsets]
  (let [[arg arg-navigator] (next-arg arg-navigator)
        base-output (or (format-simple-number arg) (print-func arg))
        base-width (count base-output)
        min-width (+ base-width (:minpad params))
        width (if (>= min-width (:mincol params))
                min-width
                (+ min-width
                   (* (+ (quot (- (:mincol params) min-width 1)
                               (:colinc params))
                         1)
                      (:colinc params))))
        chars (apply str (repeat (- width base-width) (:padchar params)))]
    (if (:at params)
      (print (str chars base-output))
      (print (str base-output chars)))
    arg-navigator))

(defn- integral? [x]
  (cond
    (integer? x) true
    (float? x) (= x (Math/floor x))
    :else false))

(defn- remainders [base val]
  (reverse
    (first
      (consume #(if (pos? %)
                  [(rem % base) (quot % base)]
                  [nil nil])
               val))))

(defn- base-str [base val]
  (if (zero? val)
    "0"
    (apply str
           (map
             #(if (< % 10) (char (+ (char-code \0) %)) (char (+ (char-code \a) (- % 10))))
             (remainders base val)))))

(defn- opt-base-str [base val]
  (base-str base val))

(defn- group-by* [unit lis]
  (reverse
    (first
      (consume (fn [x] [(seq (reverse (take unit x))) (seq (drop unit x))]) (reverse lis)))))

(defn- format-integer [base params arg-navigator offsets]
  (let [[arg arg-navigator] (next-arg arg-navigator)]
    (if (integral? arg)
      (let [neg (neg? arg)
            pos-arg (if neg (- arg) arg)
            raw-str (opt-base-str base pos-arg)
            group-str (if (:colon params)
                        (let [groups (map #(apply str %) (group-by* (:commainterval params) raw-str))
                              commas (repeat (count groups) (:commachar params))]
                          (apply str (next (interleave commas groups))))
                        raw-str)
            signed-str (cond
                         neg (str "-" group-str)
                         (:at params) (str "+" group-str)
                         true group-str)
            padded-str (if (< (count signed-str) (:mincol params))
                         (str (apply str (repeat (- (:mincol params) (count signed-str))
                                                 (:padchar params)))
                              signed-str)
                         signed-str)]
        (print padded-str))
      (format-ascii print-str {:mincol (:mincol params) :colinc 1 :minpad 0
                               :padchar (:padchar params) :at true}
                    (init-navigator [arg]) nil))
    arg-navigator))

;; Check to see if a result is an abort (~^) construct
(defn- abort? [context]
  (let [token (first context)]
    (or (= :up-arrow token) (= :colon-up-arrow token))))

(defn- execute-sub-format [format args base-args]
  (second
    (map-passing-context
      (fn [element context]
        (if (abort? context)
          [nil context]
          (let [[params args] (realize-parameter-list (:params element) context)
                [params offsets] (unzip-map params)
                params (assoc params :base-args base-args)]
            [nil (apply (:func element) [params args offsets])])))
      args
      format)))

;;----------------------------------------------------------------------
;; conditional ~[...~]
;;----------------------------------------------------------------------

(defn- choice-conditional [params arg-navigator offsets]
  (let [arg (:selector params)
        [arg navigator] (if arg [arg arg-navigator] (next-arg arg-navigator))
        clauses (:clauses params)
        clause (if (or (neg? arg) (>= arg (count clauses)))
                 (first (:else params))
                 (nth clauses arg))]
    (if clause
      (execute-sub-format clause navigator (:base-args params))
      navigator)))

(defn- boolean-conditional [params arg-navigator offsets]
  (let [[arg navigator] (next-arg arg-navigator)
        clauses (:clauses params)
        clause (if arg
                 (second clauses)
                 (first clauses))]
    (if clause
      (execute-sub-format clause navigator (:base-args params))
      navigator)))

(defn- check-arg-conditional [params arg-navigator offsets]
  (let [[arg navigator] (next-arg arg-navigator)
        clauses (:clauses params)
        clause (if arg (first clauses))]
    (if arg
      (if clause
        (execute-sub-format clause arg-navigator (:base-args params))
        arg-navigator)
      navigator)))

;;----------------------------------------------------------------------
;; iteration ~{...~}
;;----------------------------------------------------------------------

(defn- iterate-sublist [params navigator offsets]
  (let [max-count (:max-iterations params)
        param-clause (first (:clauses params))
        [clause navigator] (if (empty? param-clause)
                             (get-format-arg navigator)
                             [param-clause navigator])
        [arg-list navigator] (next-arg navigator)
        args (init-navigator arg-list)]
    (loop [count 0
           args args
           last-pos -1]
      (if (and (not max-count) (= (:pos args) last-pos) (> count 1))
        (throw (Exception. "~{ construct not consuming any arguments: Infinite loop!")))
      (if (or (and (empty? (:rest args))
                   (or (not (:colon (:right-params params))) (> count 0)))
              (and max-count (>= count max-count)))
        navigator
        (let [iter-result (execute-sub-format clause args (:base-args params))]
          (if (= :up-arrow (first iter-result))
            navigator
            (recur (inc count) iter-result (:pos args))))))))

(defn- iterate-list-of-sublists [params navigator offsets]
  (let [max-count (:max-iterations params)
        param-clause (first (:clauses params))
        [clause navigator] (if (empty? param-clause)
                             (get-format-arg navigator)
                             [param-clause navigator])
        [arg-list navigator] (next-arg navigator)]
    (loop [count 0
           arg-list arg-list]
      (if (or (and (empty? arg-list)
                   (or (not (:colon (:right-params params))) (> count 0)))
              (and max-count (>= count max-count)))
        navigator
        (let [iter-result (execute-sub-format
                            clause
                            (init-navigator (first arg-list))
                            (init-navigator (next arg-list)))]
          (if (= :colon-up-arrow (first iter-result))
            navigator
            (recur (inc count) (next arg-list))))))))

(defn- iterate-main-list [params navigator offsets]
  (let [max-count (:max-iterations params)
        param-clause (first (:clauses params))
        [clause navigator] (if (empty? param-clause)
                             (get-format-arg navigator)
                             [param-clause navigator])]
    (loop [count 0
           navigator navigator
           last-pos -1]
      (if (and (not max-count) (= (:pos navigator) last-pos) (> count 1))
        (throw (Exception. "~@{ construct not consuming any arguments: Infinite loop!")))
      (if (or (and (empty? (:rest navigator))
                   (or (not (:colon (:right-params params))) (> count 0)))
              (and max-count (>= count max-count)))
        navigator
        (let [iter-result (execute-sub-format clause navigator (:base-args params))]
          (if (= :up-arrow (first iter-result))
            (second iter-result)
            (recur
              (inc count) iter-result (:pos navigator))))))))

(defn- iterate-main-sublists [params navigator offsets]
  (let [max-count (:max-iterations params)
        param-clause (first (:clauses params))
        [clause navigator] (if (empty? param-clause)
                             (get-format-arg navigator)
                             [param-clause navigator])]
    (loop [count 0
           navigator navigator]
      (if (or (and (empty? (:rest navigator))
                   (or (not (:colon (:right-params params))) (> count 0)))
              (and max-count (>= count max-count)))
        navigator
        (let [[sublist navigator] (next-arg-or-nil navigator)
              iter-result (execute-sub-format clause (init-navigator sublist) navigator)]
          (if (= :colon-up-arrow (first iter-result))
            navigator
            (recur (inc count) navigator)))))))

;;----------------------------------------------------------------------
;; ~< ... ~> justification / logical block
;;----------------------------------------------------------------------

(declare ^{:arglists '([params navigator offsets])} format-logical-block)
(declare ^{:arglists '([params navigator offsets])} justify-clauses)

(defn- logical-block-or-justify [params navigator offsets]
  (if (:colon (:right-params params))
    (format-logical-block params navigator offsets)
    (justify-clauses params navigator offsets)))

(defn- render-clauses [clauses navigator base-navigator]
  (loop [clauses clauses
         acc []
         navigator navigator]
    (if (empty? clauses)
      [acc navigator]
      (let [clause (first clauses)
            [iter-result result-str] (let [sb (StringBuilder.)]
                                       (binding [*out* (->StringBufferWriter sb)]
                                         [(execute-sub-format clause navigator base-navigator)
                                          (str sb)]))]
        (if (= :up-arrow (first iter-result))
          [acc (second iter-result)]
          (recur (next clauses) (conj acc result-str) iter-result))))))

(defn- justify-clauses [params navigator offsets]
  (let [[[eol-str] new-navigator] (when-let [else (:else params)]
                                    (render-clauses else navigator (:base-args params)))
        navigator (or new-navigator navigator)
        [else-params new-navigator] (when-let [p (:else-params params)]
                                      (realize-parameter-list p navigator))
        navigator (or new-navigator navigator)
        min-remaining (or (first (:min-remaining else-params)) 0)
        max-columns (or (first (:max-columns else-params))
                        (get-max-column *out*))
        clauses (:clauses params)
        [strs navigator] (render-clauses clauses navigator (:base-args params))
        slots (max 1
                   (+ (dec (count strs)) (if (:colon params) 1 0) (if (:at params) 1 0)))
        chars (reduce + (map count strs))
        mincol (:mincol params)
        minpad (:minpad params)
        colinc (:colinc params)
        minout (+ chars (* slots minpad))
        result-columns (if (<= minout mincol)
                         mincol
                         (+ mincol (* colinc
                                      (+ 1 (quot (- minout mincol 1) colinc)))))
        total-pad (- result-columns chars)
        pad (max minpad (quot total-pad slots))
        extra-pad (- total-pad (* pad slots))
        pad-str (apply str (repeat pad (:padchar params)))]
    (if (and eol-str (> (+ (get-column (:base (deref (.-fields *out*)))) min-remaining result-columns)
                        max-columns))
      (print eol-str))
    (loop [slots slots
           extra-pad extra-pad
           strs strs
           pad-only (or (:colon params)
                        (and (= (count strs) 1) (not (:at params))))]
      (if (seq strs)
        (do
          (print (str (if (not pad-only) (first strs))
                      (if (or pad-only (next strs) (:at params)) pad-str)
                      (if (pos? extra-pad) (:padchar params))))
          (recur
            (dec slots)
            (dec extra-pad)
            (if pad-only strs (next strs))
            false))))
    navigator))

;;----------------------------------------------------------------------
;; ~T tabulation, ~& fresh-line, get-pretty-writer
;;----------------------------------------------------------------------

(defn get-pretty-writer
  "Returns writer wrapped in a pretty writer unless it already is one."
  [writer]
  (if (pretty-writer? writer)
    writer
    (pretty-writer writer *print-right-margin* *print-miser-width*)))

(defn fresh-line []
  (if (instance? PrettyWriter *out*)
    (if (not (= 0 (get-column (:base (deref (.-fields *out*))))))
      (prn))
    (prn)))

(defn- absolute-tabulation [params navigator offsets]
  (let [colnum (:colnum params)
        colinc (:colinc params)
        current (get-column (:base (deref (.-fields *out*))))
        space-count (cond
                      (< current colnum) (- colnum current)
                      (= colinc 0) 0
                      :else (- colinc (rem (- current colnum) colinc)))]
    (print (apply str (repeat space-count \space))))
  navigator)

(defn- relative-tabulation [params navigator offsets]
  (let [colrel (:colnum params)
        colinc (:colinc params)
        start-col (+ colrel (get-column (:base (deref (.-fields *out*)))))
        offset (if (pos? colinc) (rem start-col colinc) 0)
        space-count (+ colrel (if (= 0 offset) 0 (- colinc offset)))]
    (print (apply str (repeat space-count \space))))
  navigator)

;;----------------------------------------------------------------------
;; ~< ... ~:> logical block, ~_ newline, ~I indent
;;----------------------------------------------------------------------

(defn- format-logical-block [params navigator offsets]
  (let [clauses (:clauses params)
        clause-count (count clauses)
        prefix (cond
                 (> clause-count 1) (:string (:params (first (first clauses))))
                 (:colon params) "(")
        body (nth clauses (if (> clause-count 1) 1 0))
        suffix (cond
                 (> clause-count 2) (:string (:params (first (nth clauses 2))))
                 (:colon params) ")")
        [arg navigator] (next-arg navigator)]
    (pprint-logical-block :prefix prefix :suffix suffix
      (execute-sub-format
        body
        (init-navigator arg)
        (:base-args params)))
    navigator))

(defn- set-indent [params navigator offsets]
  (let [relative-to (if (:colon params) :current :block)]
    (pprint-indent relative-to (:n params))
    navigator))

(defn- conditional-newline [params navigator offsets]
  (let [kind (if (:colon params)
               (if (:at params) :mandatory :fill)
               (if (:at params) :miser :linear))]
    (pprint-newline kind)
    navigator))

;;----------------------------------------------------------------------
;; directive table
;;----------------------------------------------------------------------

(defmacro ^{:private true} defdirectives [& directives]
  (let [process (fn [[char params flags bracket-info & generator-fn]]
                  [char
                   {:directive char
                    :params `(array-map ~@params)
                    :flags flags
                    :bracket-info bracket-info
                    :generator-fn (concat '(fn [params offset]) generator-fn)}])]
    `(def ^{:private true}
       ~'directive-table (hash-map ~@(mapcat process directives)))))

(defdirectives
  (\A
    [:mincol [0 Long] :colinc [1 Long] :minpad [0 Long] :padchar [\space Character]]
    #{:at :colon :both} {}
    #(format-ascii print-str %1 %2 %3))

  (\S
    [:mincol [0 Long] :colinc [1 Long] :minpad [0 Long] :padchar [\space Character]]
    #{:at :colon :both} {}
    #(format-ascii pr-str %1 %2 %3))

  (\D
    [:mincol [0 Long] :padchar [\space Character] :commachar [\, Character]
     :commainterval [3 Long]]
    #{:at :colon :both} {}
    #(format-integer 10 %1 %2 %3))

  (\B
    [:mincol [0 Long] :padchar [\space Character] :commachar [\, Character]
     :commainterval [3 Long]]
    #{:at :colon :both} {}
    #(format-integer 2 %1 %2 %3))

  (\O
    [:mincol [0 Long] :padchar [\space Character] :commachar [\, Character]
     :commainterval [3 Long]]
    #{:at :colon :both} {}
    #(format-integer 8 %1 %2 %3))

  (\X
    [:mincol [0 Long] :padchar [\space Character] :commachar [\, Character]
     :commainterval [3 Long]]
    #{:at :colon :both} {}
    #(format-integer 16 %1 %2 %3))

  (\%
    [:count [1 Long]]
    #{} {}
    (fn [params arg-navigator offsets]
      (dotimes [i (:count params)]
        (prn))
      arg-navigator))

  (\&
    [:count [1 Long]]
    #{:pretty} {}
    (fn [params arg-navigator offsets]
      (let [cnt (:count params)]
        (if (pos? cnt) (fresh-line))
        (dotimes [i (dec cnt)]
          (prn)))
      arg-navigator))

  (\|
    [:count [1 Long]]
    #{} {}
    (fn [params arg-navigator offsets]
      (dotimes [i (:count params)]
        (print \formfeed))
      arg-navigator))

  (\~
    [:n [1 Long]]
    #{} {}
    (fn [params arg-navigator offsets]
      (let [n (:n params)]
        (print (apply str (repeat n \~)))
        arg-navigator)))

  (\newline
    []
    #{:colon :at} {}
    (fn [params arg-navigator offsets]
      (if (:at params)
        (prn))
      arg-navigator))

  (\T
    [:colnum [1 Long] :colinc [1 Long]]
    #{:at :pretty} {}
    (if (:at params)
      #(relative-tabulation %1 %2 %3)
      #(absolute-tabulation %1 %2 %3)))

  (\*
    [:n [1 Long]]
    #{:colon :at} {}
    (fn [params navigator offsets]
      (let [n (:n params)]
        (if (:at params)
          (absolute-reposition navigator n)
          (relative-reposition navigator (if (:colon params) (- n) n))))))

  (\?
    []
    #{:at} {}
    (if (:at params)
      (fn [params navigator offsets]
        (let [[subformat navigator] (get-format-arg navigator)]
          (execute-sub-format subformat navigator (:base-args params))))
      (fn [params navigator offsets]
        (let [[subformat navigator] (get-format-arg navigator)
              [subargs navigator] (next-arg navigator)
              sub-navigator (init-navigator subargs)]
          (execute-sub-format subformat sub-navigator (:base-args params))
          navigator))))

  (\) [] #{} {} nil)

  (\[
    [:selector [nil Long]]
    #{:colon :at} {:right \], :allows-separator true, :else :last}
    (cond
      (:colon params)
      boolean-conditional

      (:at params)
      check-arg-conditional

      true
      choice-conditional))

  (\; [:min-remaining [nil Long] :max-columns [nil Long]]
    #{:colon} {:separator true} nil)

  (\] [] #{} {} nil)

  (\{
    [:max-iterations [nil Long]]
    #{:colon :at :both} {:right \}, :allows-separator false}
    (cond
      (and (:at params) (:colon params))
      iterate-main-sublists

      (:colon params)
      iterate-list-of-sublists

      (:at params)
      iterate-main-list

      true
      iterate-sublist))

  (\} [] #{:colon} {} nil)

  (\<
    [:mincol [0 Long] :colinc [1 Long] :minpad [0 Long] :padchar [\space Character]]
    #{:colon :at :both :pretty} {:right \>, :allows-separator true, :else :first}
    logical-block-or-justify)

  (\> [] #{:colon} {} nil)

  (\^ [:arg1 [nil Long] :arg2 [nil Long] :arg3 [nil Long]]
    #{:colon} {}
    (fn [params navigator offsets]
      (let [arg1 (:arg1 params)
            arg2 (:arg2 params)
            arg3 (:arg3 params)
            exit (if (:colon params) :colon-up-arrow :up-arrow)]
        (cond
          (and arg1 arg2 arg3)
          (if (<= arg1 arg2 arg3) [exit navigator] navigator)

          (and arg1 arg2)
          (if (= arg1 arg2) [exit navigator] navigator)

          arg1
          (if (= arg1 0) [exit navigator] navigator)

          true
          (if (if (:colon params)
                (empty? (:rest (:base-args params)))
                (empty? (:rest navigator)))
            [exit navigator] navigator)))))

  (\W
    []
    #{:at :colon :both :pretty} {}
    (if (or (:at params) (:colon params))
      (let [bindings (concat
                       (if (:at params) [:level nil :length nil] [])
                       (if (:colon params) [:pretty true] []))]
        (fn [params navigator offsets]
          (let [[arg navigator] (next-arg navigator)]
            (if (apply write arg bindings)
              [:up-arrow navigator]
              navigator))))
      (fn [params navigator offsets]
        (let [[arg navigator] (next-arg navigator)]
          (if (write-out arg)
            [:up-arrow navigator]
            navigator)))))

  (\_
    []
    #{:at :colon :both} {}
    conditional-newline)

  (\I
    [:n [0 Long]]
    #{:colon} {}
    set-indent))

;;----------------------------------------------------------------------
;; compiling format strings
;;----------------------------------------------------------------------

(def ^{:private true}
  param-pattern #"^([vV]|#|('.)|([+-]?\d+)|(?=,))")

(def ^{:private true}
  special-params #{:parameter-from-args :remaining-arg-count})

(defn- extract-param [[s offset saw-comma]]
  ;; param-pattern is ^-anchored; re-find returns [whole g1 g2 g3] (groups nil)
  ;; or nil. The whole match (possibly empty, from the comma lookahead) is the
  ;; token; its length advances the cursor.
  (let [param (re-find param-pattern s)
        token-str (if (vector? param) (first param) param)]
    (if token-str
      (let [len (count token-str)
            remainder (subs s len)
            new-offset (+ offset len)]
        (if (not (= \, (nth remainder 0 nil)))
          [[token-str offset] [remainder new-offset false]]
          [[token-str offset] [(subs remainder 1) (inc new-offset) true]]))
      (if saw-comma
        (format-error "Badly formed parameters in format directive" offset)
        [nil [s offset]]))))

(defn- extract-params [s offset]
  (consume extract-param [s offset false]))

(defn- translate-param [[p offset]]
  [(cond
     (= (count p) 0) nil
     (and (= (count p) 1) (contains? #{\v \V} (nth p 0))) :parameter-from-args
     (and (= (count p) 1) (= \# (nth p 0))) :remaining-arg-count
     (and (= (count p) 2) (= \' (nth p 0))) (nth p 1)
     true (Long/parseLong p))
   offset])

(def ^{:private true} flag-defs {\: :colon, \@ :at})

(defn- extract-flags [s offset]
  (consume
    (fn [[s offset flags]]
      (if (empty? s)
        [nil [s offset flags]]
        (let [flag (get flag-defs (first s))]
          (if flag
            (if (contains? flags flag)
              (format-error
                (str "Flag \"" (first s) "\" appears more than once in a directive")
                offset)
              [true [(subs s 1) (inc offset) (assoc flags flag [true offset])]])
            [nil [s offset flags]]))))
    [s offset {}]))

(defn- check-flags [dirdef flags]
  (let [allowed (:flags dirdef)]
    (if (and (not (:at allowed)) (:at flags))
      (format-error (str "\"@\" is an illegal flag for format directive \"" (:directive dirdef) "\"")
                    (nth (:at flags) 1)))
    (if (and (not (:colon allowed)) (:colon flags))
      (format-error (str "\":\" is an illegal flag for format directive \"" (:directive dirdef) "\"")
                    (nth (:colon flags) 1)))
    (if (and (not (:both allowed)) (:at flags) (:colon flags))
      (format-error (str "Cannot combine \"@\" and \":\" flags for format directive \""
                         (:directive dirdef) "\"")
                    (min (nth (:colon flags) 1) (nth (:at flags) 1))))))

(defn- map-params [dirdef params flags offset]
  (check-flags dirdef flags)
  (if (> (count params) (count (:params dirdef)))
    (format-error
      (cl-format
        nil
        "Too many parameters for directive \"~C\": ~D~:* ~[were~;was~:;were~] specified but only ~D~:* ~[are~;is~:;are~] allowed"
        (:directive dirdef) (count params) (count (:params dirdef)))
      (second (first params))))
  (doall
    (map #(let [val (first %1)]
            (if (not (or (nil? val) (contains? special-params val)))
              nil))
         params (:params dirdef)))

  (merge
    (into {}
          (reverse (for [[name [default]] (:params dirdef)] [name [default offset]])))
    (reduce #(apply assoc %1 %2) {} (filter #(first (nth % 1)) (zipmap (keys (:params dirdef)) params)))
    flags))

(defn- compile-directive [s offset]
  (let [[raw-params [rest offset]] (extract-params s offset)
        [_ [rest offset flags]] (extract-flags rest offset)
        directive (first rest)
        dirdef (if directive
                 (or (get directive-table (first (clojure.string/upper-case (str directive))))
                     (get directive-table directive)))
        params (if dirdef (map-params dirdef (map translate-param raw-params) flags offset))]
    (if (not directive)
      (format-error "Format string ended in the middle of a directive" offset))
    (if (not dirdef)
      (format-error (str "Directive \"" directive "\" is undefined") offset))
    [(->compiled-directive ((:generator-fn dirdef) params offset) dirdef params offset)
     (let [remainder (subs rest 1)
           offset (inc offset)
           trim? (and (= \newline (:directive dirdef))
                      (not (:colon params)))
           trim-count (if trim? (prefix-count remainder [\space \tab]) 0)
           remainder (subs remainder trim-count)
           offset (+ offset trim-count)]
       [remainder offset])]))

(defn- compile-raw-string [s offset]
  (->compiled-directive (fn [_ a _] (print s) a) nil {:string s} offset))

(defn- right-bracket [this] (:right (:bracket-info (:dirdef this))))
(defn- separator? [this] (:separator (:bracket-info (:dirdef this))))
(defn- else-separator? [this]
  (and (:separator (:bracket-info (:dirdef this)))
       (:colon (:params this))))

(declare ^{:arglists '([bracket-info offset remainder])} collect-clauses)

(defn- process-bracket [this remainder]
  (let [[subex remainder] (collect-clauses (:bracket-info (:dirdef this))
                                           (:offset this) remainder)]
    [(->compiled-directive
       (:func this) (:dirdef this)
       (merge (:params this) (tuple-map subex (:offset this)))
       (:offset this))
     remainder]))

(defn- process-clause [bracket-info offset remainder]
  (consume
    (fn [remainder]
      (if (empty? remainder)
        (format-error "No closing bracket found." offset)
        (let [this (first remainder)
              remainder (next remainder)]
          (cond
            (right-bracket this)
            (process-bracket this remainder)

            (= (:right bracket-info) (:directive (:dirdef this)))
            [nil [:right-bracket (:params this) nil remainder]]

            (else-separator? this)
            [nil [:else nil (:params this) remainder]]

            (separator? this)
            [nil [:separator nil nil remainder]]

            true
            [this remainder]))))
    remainder))

(defn- collect-clauses [bracket-info offset remainder]
  (second
    (consume
      (fn [[clause-map saw-else remainder]]
        (let [[clause [type right-params else-params remainder]]
              (process-clause bracket-info offset remainder)]
          (cond
            (= type :right-bracket)
            [nil [(merge-with concat clause-map
                              {(if saw-else :else :clauses) [clause]
                               :right-params right-params})
                  remainder]]

            (= type :else)
            (cond
              (:else clause-map)
              (format-error "Two else clauses (\"~:;\") inside bracket construction." offset)

              (not (:else bracket-info))
              (format-error "An else clause (\"~:;\") is in a bracket type that doesn't support it."
                            offset)

              (and (= :first (:else bracket-info)) (seq (:clauses clause-map)))
              (format-error
                "The else clause (\"~:;\") is only allowed in the first position for this directive."
                offset)

              true
              (if (= :first (:else bracket-info))
                [true [(merge-with concat clause-map {:else [clause] :else-params else-params})
                       false remainder]]
                [true [(merge-with concat clause-map {:clauses [clause]})
                       true remainder]]))

            (= type :separator)
            (cond
              saw-else
              (format-error "A plain clause (with \"~;\") follows an else clause (\"~:;\") inside bracket construction." offset)

              (not (:allows-separator bracket-info))
              (format-error "A separator (\"~;\") is in a bracket type that doesn't support it."
                            offset)

              true
              [true [(merge-with concat clause-map {:clauses [clause]})
                     false remainder]]))))
      [{:clauses []} false remainder])))

(defn- process-nesting [format]
  (first
    (consume
      (fn [remainder]
        (let [this (first remainder)
              remainder (next remainder)
              bracket (:bracket-info (:dirdef this))]
          (if (:right bracket)
            (process-bracket this remainder)
            [this remainder])))
      format)))

(defn- compile-format [format-str]
  (binding [*format-str* format-str]
    (process-nesting
      (first
        (consume
          (fn [[s offset]]
            (if (empty? s)
              [nil s]
              (let [tilde (.indexOf s "~")]
                (cond
                  (neg? tilde) [(compile-raw-string s offset) ["" (+ offset (count s))]]
                  (zero? tilde) (compile-directive (subs s 1) (inc offset))
                  true
                  [(compile-raw-string (subs s 0 tilde) offset) [(subs s tilde) (+ tilde offset)]]))))
          [format-str 0])))))

(defn- needs-pretty [format]
  (loop [format format]
    (if (empty? format)
      false
      (if (or (:pretty (:flags (:dirdef (first format))))
              (some needs-pretty (first (:clauses (:params (first format)))))
              (some needs-pretty (first (:else (:params (first format))))))
        true
        (recur (next format))))))

(defn- execute-format
  ([stream format args]
   (let [sb (StringBuilder.)
         real-stream (if (or (not stream) (true? stream))
                       (->StringBufferWriter sb)
                       stream)
         wrapped-stream (if (and (needs-pretty format)
                                 (not (pretty-writer? real-stream)))
                          (get-pretty-writer real-stream)
                          real-stream)]
     (binding [*out* wrapped-stream]
       (try
         (execute-format format args)
         (finally
           (if-not (identical? real-stream wrapped-stream)
             (-pflush wrapped-stream))))
       (cond
         (not stream) (str sb)
         (true? stream) (clojure.core/print (str sb))
         :else nil))))
  ([format args]
   (map-passing-context
     (fn [element context]
       (if (abort? context)
         [nil context]
         (let [[params args] (realize-parameter-list
                               (:params element) context)
               [params offsets] (unzip-map params)
               params (assoc params :base-args args)]
           [nil (apply (:func element) [params args offsets])])))
     args
     format)
   nil))

(def ^{:private true} cached-compile (memoize compile-format))

;;======================================================================
;; dispatch
;;======================================================================

(def ^{:private true} reader-macros
  {'quote "'"
   'var "#'"
   'clojure.core/deref "@"
   'clojure.core/unquote "~"})

(defn- pprint-reader-macro [alis]
  (let [macro-char (reader-macros (first alis))]
    (when (and macro-char (= 2 (count alis)))
      (-write *out* macro-char)
      (write-out (second alis))
      true)))

;;; simple dispatch

(defn- pprint-simple-list [alis]
  (pprint-logical-block :prefix "(" :suffix ")"
    (print-length-loop [alis (seq alis)]
      (when alis
        (write-out (first alis))
        (when (next alis)
          (-write *out* " ")
          (pprint-newline :linear)
          (recur (next alis)))))))

(defn- pprint-list [alis]
  (if-not (pprint-reader-macro alis)
    (pprint-simple-list alis)))

(defn- pprint-vector [avec]
  (pprint-logical-block :prefix "[" :suffix "]"
    (print-length-loop [aseq (seq avec)]
      (when aseq
        (write-out (first aseq))
        (when (next aseq)
          (-write *out* " ")
          (pprint-newline :linear)
          (recur (next aseq)))))))

(def ^{:private true} pprint-array (formatter-out "~<[~;~@{~w~^, ~:_~}~;]~:>"))

(defn- pprint-map [amap]
  (pprint-logical-block :prefix "{" :suffix "}"
    (print-length-loop [aseq (seq amap)]
      (when aseq
        (pprint-logical-block
          (write-out (ffirst aseq))
          (-write *out* " ")
          (pprint-newline :linear)
          (set! *current-length* 0)
          (write-out (fnext (first aseq))))
        (when (next aseq)
          (-write *out* ", ")
          (pprint-newline :linear)
          (recur (next aseq)))))))

(defn- pprint-simple-default [obj]
  (-write *out* (pr-str obj)))

(def pprint-set (formatter-out "~<#{~;~@{~w~^ ~:_~}~;}~:>"))

(defn- type-dispatcher [obj]
  (cond
    (symbol? obj) :symbol
    (seq? obj) :list
    (map? obj) :map
    (vector? obj) :vector
    (set? obj) :set
    (nil? obj) nil
    :else :default))

;; simple-dispatch / code-dispatch are plain functions rather than multimethods:
;; a multimethod baked into the seed can't capture this namespace's load context,
;; and the printer never extends these tables externally.
(defn simple-dispatch
  "The pretty print dispatch function for simple data structure format."
  [obj]
  (case (type-dispatcher obj)
    :list (pprint-list obj)
    :vector (pprint-vector obj)
    :map (pprint-map obj)
    :set (pprint-set obj)
    nil (-write *out* (pr-str nil))
    (pprint-simple-default obj)))

;;; code dispatch

(declare ^{:arglists '([alis])} pprint-simple-code-list)

(defn- brackets [form]
  (if (vector? form)
    ["[" "]"]
    ["(" ")"]))

(defn- pprint-simple-code-list [alis]
  (pprint-logical-block :prefix "(" :suffix ")"
    (pprint-indent :block 1)
    (print-length-loop [alis (seq alis)]
      (when alis
        (write-out (first alis))
        (when (next alis)
          (-write *out* " ")
          (pprint-newline :linear)
          (recur (next alis)))))))

(defn- pprint-code-list [alis]
  (if-not (pprint-reader-macro alis)
    (pprint-simple-code-list alis)))

(defn- pprint-code-symbol [sym]
  (if *print-suppress-namespaces*
    (print (name sym))
    (pr sym)))

(defn code-dispatch
  "The pretty print dispatch function for pretty printing Clojure code."
  [obj]
  (case (type-dispatcher obj)
    :list (pprint-code-list obj)
    :symbol (pprint-code-symbol obj)
    :vector (pprint-vector obj)
    :map (pprint-map obj)
    :set (pprint-set obj)
    nil (pr obj)
    (pprint-simple-default obj)))

(alter-var-root (var *print-pprint-dispatch*) (constantly simple-dispatch))

;;======================================================================
;; print-table
;;======================================================================

(defn- add-padding [width s]
  (let [padding (max 0 (- width (count s)))]
    (apply str (clojure.string/join (repeat padding \space)) s)))

(defn print-table
  "Prints a collection of maps in a textual table."
  ([ks rows]
   (when (seq rows)
     (let [widths (map
                    (fn [k]
                      (apply max (count (str k)) (map #(count (str (get % k))) rows)))
                    ks)
           spacers (map #(apply str (repeat % "-")) widths)
           fmt-row (fn [leader divider trailer row]
                     (str leader
                          (apply str (interpose divider
                                                (for [[col width] (map vector (map #(get row %) ks) widths)]
                                                  (add-padding width (str col)))))
                          trailer))]
       (clojure.core/println)
       (clojure.core/println (fmt-row "| " " | " " |" (zipmap ks ks)))
       (clojure.core/println (fmt-row "|-" "-+-" "-|" (zipmap ks spacers)))
       (doseq [row rows]
         (clojure.core/println (fmt-row "| " " | " " |" row))))))
  ([rows] (print-table (keys (first rows)) rows)))

;;======================================================================
;; core print routing
;;======================================================================

;; Route clojure.core/print et al. into the active pretty-writer when *out* is
;; bound to one, matching JVM Clojure where core print honours *out*. Custom
;; dispatch fns (e.g. clojure.data.json's, which calls clojure.core/print and
;; (PrintWriter. *out*)) depend on this. Declines (nil) when *out* is anything
;; else, so the string falls through to the normal output seam.
(clojure.core/__set-pprint-write-hook!
  (fn [s]
    (let [o clojure.core/*out*]
      (when (instance? PrettyWriter o)
        (-write o s)
        true))))
