# pulled from cfiggers/jayson

(defmacro- letv [bindings & body]
  ~(do ,;(seq [[k v] :in (partition 2 bindings)] ['var k v]) ,;body))

(defn- read-hex [n] 
    (scan-number (string "0x" n)))

(defn- check-utf-16 [capture]
  (let [u (read-hex capture)]
    (if (and (>= u 0xD800)
             (<= u 0xDBFF))
      capture 
      false)))

(def- utf-8->bytes 
  (peg/compile
   ~{:double-u-esc (/ (* "\\u" (cmt (<- 4) ,|(check-utf-16 $)) "\\u" (<- 4))
                      ,|(+ (blshift (- (read-hex $0) 0xD800) 10)
                           (- (read-hex $1) 0xDC00) 0x10000))
     :single-u-esc (/ (* "\\u" (<- 4)) ,|(read-hex $))
     :unicode-esc  (/ (+ :double-u-esc :single-u-esc)
                      ,|(string/from-bytes
                         ;(cond
                           (<= $ 0x7f) [$]
                           (<= $ 0x7ff)
                           [(bor (band (brshift $  6) 0x1F) 0xC0)
                            (bor (band (brshift $  0) 0x3F) 0x80)]
                           (<= $ 0xffff)
                           [(bor (band (brshift $ 12) 0x0F) 0xE0)
                            (bor (band (brshift $  6) 0x3F) 0x80)
                            (bor (band (brshift $  0) 0x3F) 0x80)]
                             # Otherwise
                           [(bor (band (brshift $ 18) 0x07) 0xF0)
                            (bor (band (brshift $ 12) 0x3F) 0x80)
                            (bor (band (brshift $  6) 0x3F) 0x80)
                            (bor (band (brshift $  0) 0x3F) 0x80)])))
     :escape       (/ (* "\\" (<- (set "avbnfrt\"\\/")))
                      ,|(get {"a" "\a" "v" "\v" "b" "\b"
                              "n" "\n" "f" "\f" "r" "\r"
                              "t" "\t"} $ $))
     :main         (+ (some (+ :unicode-esc :escape (<- 1))) -1)}))

(defn decode 
  ``
  Returns a janet object after parsing JSON. If `keywords` is truthy,
  string keys will be converted to keywords. If `nils` is truthy, `null`
  will become `nil` instead of the keyword `:json/null`.
  ``
  [json-source &opt keywords nils] 

  (def json-parser 
    {:null (if nils
             ~(/ (<- (+ "null" "Null")) nil)
             ~(/ (<- (+ "null" "Null")) :json/null))
     :bool-t ~(/ (<- (+ "true")) true)
     :bool-f ~(/ (<- (+ "false")) false)
     :number ~(/ (<- (* (? "-") :d+ (? (* "." :d+)))) ,|(scan-number $))
     :string ~(/ (* "\"" (<- (to (* (> -1 (not "\\")) "\"")))
                    (* (> -1 (not "\\")) "\""))
                 ,|(string/join (peg/match utf-8->bytes $))) 
     :array ~(/ (* "[" :s* (? (* :value (any (* :s* "," :value)))) "]") ,|(array ;$&))
     :key-value (if keywords
                  ~(* :s* (/ :string ,|(keyword $)) :s* ":" :value)
                  ~(* :s* :string :s* ":" :value))
     :object ~(/ (* "{" :s* (? (* :key-value (any (* :s* "," :key-value)))) "}")
                 ,|(from-pairs (partition 2 $&)))
     :value ~(* :s* (+ :null :bool-t :bool-f :number :string :array :object) :s*)
     :unmatched ~(/ (<- (to (+ :value -1))) ,|[:unmatched $])
     :main ~(some (+ :value "\n" :unmatched))})
  
  (first (peg/match (peg/compile json-parser) json-source)))

(def- bytes->utf-8
  (peg/compile
   ~{:four-byte  (/ (* (<- (range "\xf0\xff")) (<- 1) (<- 1) (<- 1))
                    ,|(bor (blshift (band (first $0) 0x07) 18)
                           (blshift (band (first $1) 0x3F) 12)
                           (blshift (band (first $2) 0x3F) 6)
                           (blshift (band (first $3) 0x3F) 0)))
     :three-byte (/ (* (<- (range "\xe0\xef")) (<- 1) (<- 1))
                    ,|(bor (blshift (band (first $0) 0x0F) 12)
                           (blshift (band (first $1) 0x3F) 6)
                           (blshift (band (first $2) 0x3F) 0)))
     :two-byte   (/ (* (<- (range "\x80\xdf")) (<- 1))
                    ,|(bor (blshift (band (first $0) 0x1F) 6)
                           (blshift (band (first $1) 0x3F) 0)))
     :multi-byte (/ (+ :two-byte :three-byte :four-byte)
                    ,|(if (< $ 0x10000) 
                        (string/format "\\u%04X" $)
                        (string/format "\\u%04X\\u%04X" 
                                       (+ (brshift (- $ 0x10000) 10) 0xD800) 
                                       (+ (band (- $ 0x10000) 0x3FF) 0xDC00))))
     :one-byte   (<- (range "\x20\x7f"))
     :0to31      (/ (<- (range "\0\x1F"))
                    ,|(or ({"\a" "\\u0007" "\b" "\\u0008"
                            "\t" "\\u0009" "\n" "\\u000A"
                            "\v" "\\u000B" "\f" "\\u000C"
                            "\r" "\\u000D"} $)
                          (string/format "\\u%04X" (first $))))
     :backslash  (/ (<- "\\") "\\\\")
     :quote      (/ (<- "\"") "\\\"")
     :main       (+ (some (+ :0to31 :backslash :quote :one-byte :multi-byte)) -1)}))

(defn- encodeone [x depth]
  (if (> depth 1024) (error "recurred too deeply"))
  (cond
    (= x :json/null) "null"
    (= x nil) "null"
    (bytes? x) (string "\"" (string/join (peg/match bytes->utf-8 x)) "\"")
    (indexed? x) (string "[" (string/join (map |(encodeone $ (inc depth)) x) ",") "]")
    (dictionary? x) (string "{" (string/join
                                 (seq [[k v] :in (pairs x)]
                                   (string "\"" (string/join (peg/match bytes->utf-8 k)) "\"" ":" (encodeone v (inc depth)))) ",") "}")
    (case (type x)
      :nil "null"
      :boolean (string x)
      :number (string x)
      (error "type not supported"))))

(defn encode 
  `` 
  Encodes a janet value in JSON (utf-8). If `buf` is provided, the formated 
  JSON is append to `buf` instead of a new buffer. Returns the modifed buffer.
  ``
  [x &opt buf]
  
  (letv [ret (encodeone x 0)]
        (if (and buf (buffer? buf))
          (buffer/push ret)
          (thaw ret))))
