(define-module (toml builder)
  #:use-module (srfi srfi-1)
  #:use-module (json)
  #:use-module (srfi srfi-19)
  #:use-module (ice-9 textual-ports)
  #:use-module (ice-9 receive)
  #:use-module (ice-9 string-fun)
  #:export (scm->toml value? toml-build-value toml-build-array toml-build-string))

(define-syntax-rule (log-exprs exp ...) (begin (format (current-error-port) "~a: ~S\n" (quote exp) exp) ...))

;; we want to be able to dynamically bind this functin in test-decoder.scm
;; TODO fix duplicate
(define value?
  (make-parameter
   (lambda (expr) (not (list? expr)))))

(define (value-pair? scm)
  (and (string? (car scm))
       ((value?) (cdr scm))))

(define (table-or-array-table? scm)
  (or (not (value-pair? scm))
      (array-table? (cdr scm))))

(define (array-table? v)
  (and (vector? v) (not (any (value?) (vector->list v)))))

(define (array-table-pair? scm)
  (array-table? (cdr scm)))
;; (define scm->value
;;   (lambda))
(define (build-newline port newline?)
  (when newline?
    (newline port)))

(define (toml-build-array-table-header keys port)
  (put-string port "[[")
  (build-keys keys port)
  (put-string port "]]"))

(define* (toml-build-array-table scm port #:optional (current-table '()))
  (define header (car scm))
  (define keys (append current-table (list header)))
  (define entries (vector->list (cdr scm)))
  (let loop ((entries entries))
    (unless (null? entries)
      (toml-build-array-table-header keys port)
      (build-newline port #t)
      (toml-build (car entries) port keys)
      (loop (cdr entries)))))

(define* (build-object-pair p port #:optional (current-table '())#:key (newline? #t) (inline? #f))
  (if (array-table? (cdr p))
      (toml-build-array-table p port current-table)
      (build-keyval p port #:newline? newline? #:inline? inline?)))

(define* (build-keyval p port #:key (newline? #t) (inline? #f))
  (build-key (car p) port)
  (put-string port " = ")
  (toml-build (cdr p) port #:newline? newline? #:inline? inline?))

(define* (escape-control-characters s #:optional (literal-nl? #f))
  (define (char->unicode-hex c)
    (define hex-unpadded (number->string (char->integer c) 16))
    (define zeroes (make-string (- 4 (string-length hex-unpadded)) #\0))
    (string-append "\\u" zeroes hex-unpadded))
  (apply string-append (map (lambda (c)
                              (cond
                               ((and literal-nl? (eq? c #\newline))
                                "\n")
                               ((eq? 'Cc (char-general-category c))
                                (char->unicode-hex c))
                               (else (string c))))
                            (string->list s))))

(define (toml-build-string s port)
  (define quote-type (if (or
                          (string-contains s "\\")
                          (string-contains s "\"")) #\' #\"))
  (define surround (if (string-contains s "\n")
                       (make-string 3 quote-type)
                       (string quote-type)))
  ;; (define surround (make-string 1 quote-type))
  (put-string port surround)
  (put-string port (escape-control-characters s #t))
  (put-string port surround))

(define (toml-build-key-string s port)
  (define quote-type (if (or
                          (string-contains s "\\")
                          (string-contains s "\"")) #\' #\"))
  (define surround (make-string 1 quote-type))
  (put-string port surround)
  (put-string port (escape-control-characters s))
  (put-string port surround))

(define (build-keys lst port)
  ;; TODO unicode keys
  ;; (put-string port (string-join lst "."))
  ;; (toml-build-string (string-join lst ".") port))
  (let loop ((lst lst))
    (if (null? (cdr lst))
        (build-key (car lst) port)
        (begin
          (build-key (car lst) port)
          (put-string port ".")
          (loop (cdr lst))))))

(define (build-key s port)
  (toml-build-key-string s port))

(define (build-table scm port current-table)
  (define new-table (append current-table (list (car scm))))
  (put-string port "[")
  (build-keys new-table port)
  (put-string port "]")
  (newline port)
  (toml-build (cdr scm) port new-table))

(define (values-then-array-tables a b)
  (let ((av? (array-table? (cdr a)))
        (bv? (array-table? (cdr b))))
    (cond
     ((and av? bv?) #t)
     (av? #f)
     (bv? #t))))

(define (toml-build-tree scm port current-table)
  (define pairs scm)
  (unless (null? pairs)
    (receive (tables keyvals)
        (partition table-or-array-table? pairs)
      ;; (log-exprs "." tables keyvals ".")
      (for-each (lambda (kv)
                  (build-object-pair kv port current-table))
                keyvals)
      (receive (array-tables std-tables)
          (partition array-table-pair? tables)
        ;; (log-exprs "_" array-tables std-tables "_")
        (for-each (lambda (t)
                    (build-table t port current-table))
                  std-tables)
        (for-each (lambda (kv)
                    (build-object-pair kv port current-table))
                  array-tables)))))

(define (build-delimited lst port)
  (unless (null? lst)
    (let loop ((lst lst))
      (if (null? (cdr lst))
          (toml-build (car lst) port #:newline? #f #:inline? #t)
          (begin (toml-build (car lst) port #:newline? #f #:inline? #t)
                 (put-string port ", ")
                 (loop (cdr lst)))))))

(define (build-delimited-pairs lst port)
  (unless (null? lst)
    (let loop ((lst lst))
      (if (null? (cdr lst))
          (build-object-pair (car lst) port #:newline? #f #:inline? #t)
          (begin (build-object-pair (car lst) port #:newline? #f #:inline? #t)
                 (put-string port ", ")
                 (loop (cdr lst)))))))

(define (toml-build-inline-tree scm port)
  (define pairs scm)
  (put-string port "{")
  (unless (null? pairs)
    (build-delimited-pairs pairs port))
  ;; (build-delimited pairs port))
  (put-string port "}"))

;; (build-object-pair (car pairs) port)
;; (for-each (lambda (p)
;;             (build-object-pair p port))
;;           (cdr pairs))
;; (newline port))))

(define (toml-build-array v port)
  (put-string port "[")
  (build-delimited (vector->list v) port)
  (put-string port "]"))

;; (define x (make-date 0 0 1 1 1 1 0 0))
;; (date->toml-datetime x)

(define (date->toml-datetime date)
  (define format (string-append
                  "~Y-~m-~dT~H:~M:~S"
                  (if (< 0 (date-nanosecond date)) ".~N" "")
                  "~z"))
  (date->string date format))

(define toml-build-value
  (make-parameter
   (lambda*
       (scm port #:key (newline? #t) (inline? #f))
     (cond
      ;; ((eq? scm null) (toml-build-null port))
      ;; ((boolean? scm) (toml-build-boolean scm port))
      ;; ((symbol? scm) (toml-build-string (symbol->string scm) port))

      ;; TODO float (nan, inf)
      ((date? scm) (put-string port (date->toml-datetime scm)))
      ((vector? scm) (toml-build-array scm port))
      ((string? scm) (toml-build-string scm port))
      (else (display scm port)))
     (build-newline port newline?))))

(define* (toml-build scm port #:optional (current-table '())
                     #:key (newline? #t) (inline? #f))
  ;; (log-exprs scm)
  (cond
   ;; ((eq? scm null) (toml-build-null port))
   ;; ((boolean? scm) (toml-build-boolean scm port))
   ;; ((toml-number? scm) (toml-build-number scm port))
   ;; ((symbol? scm) (toml-build-string (symbol->string scm) port))
   ((null? scm)
    (and inline? (toml-build-inline-tree '() port)))
   (((value?) scm)
    ((toml-build-value) scm port #:newline? newline? #:inline? inline?))
   ((array-table? (and (list? scm) (cdr scm)))
    (toml-build-array-table scm port))
   ((or (pair? scm) (null? scm))
    (if inline?
        (toml-build-inline-tree scm port)
        (toml-build-tree scm port current-table))))

  (build-newline port newline?))
;; (else (throw 'toml-invalid scm))))

(define* (scm->toml scm
                    #:optional (port (current-output-port)))
  (toml-build scm port))

;; (scm->toml '(("a" . "b") ("c" . "d")))
;; (scm->toml '(("yo" ("a" . "b"))))
;; (scm->toml '(("hi"
;;               ("yo" ("a" . "b") ("c" . "d"))
;;               ;; ("e" . #("f" "b" (("a" . "b")))) TODO inline-tables
;;               ("e" . #("f" "b" "g")))
;;              ("g" . "p")))

;; '(("servers"
;;    ("beta" ("role" . "backend") ("ip" . "10.0.0.2"))
;;    ("alpha"
;;     ("role" . "frontend")
;;     ("ip" . "10.0.0.1")))
;;   ("database"
;;    ("temp_targets" ("case" . 72.0) ("cpu" . 79.5))
;;    ("data" . #(#("delta" "phi") #(3.14)))
;;    ("ports" . #(8000 8001 8002))
;;    ("enabled" . #t))
;;   ("owner"
;;    ("dob"
;;     . "date")
;;    ("name" . "Tom Preston-Werner"))
;;   ("title" . "TOML Example"))

;; '()


;; (define (values-first a b)
;;   (let ((av? ((value?) a))
;;         (bv? ((value?) b)))
;;    (cond
;;     ((and av? bv?) #t)
;;     (av? #t)
;;     (bv? #f))))
