#lang debug racket/base
(require (for-syntax racket/base racket/syntax syntax/strip-context)
         racket/match
         racket/sequence
         racket/string)
(provide (all-defined-out))

(define (list->attrs . kvs)
  (for/list ([kv (in-slice 2 kvs)])
            kv))

(define (pica->pts prefix [suffix #false])
  ;; both pieces of measurement are either positive or negative
  ((if (negative? prefix) - +) (+ (* (abs prefix) 12) (or suffix 0))))
(define (cm->in x) (/ x 2.54))
(define (in->pts x) (* 72 x))
(define (mm->cm x) (/ x 10.0))

(define (parse-dimension x [em-resolution-attrs #false])
  (define pica-pat (regexp "^(p|pica)(s)?$"))
  (define (unit->converter-proc unit)
    (match unit
      [(regexp #rx"^(pt|point)(s)?$") values] ; points
      [(regexp pica-pat) pica->pts] ; pica (no pts)
      [(regexp #rx"^inch(es)?|in(s)?$") in->pts] ; inches
      [(regexp #rx"^cm(s)?$") (compose1 in->pts cm->in)] ; cm
      [(regexp #rx"^mm(s)?$") (compose1 in->pts cm->in mm->cm)] ; mm
      [(regexp #rx"^em(s)?$")
       #:when em-resolution-attrs
       ;; if we don't have attrs for resolving the em string, we ignore it
       (λ (num) (* (hash-ref em-resolution-attrs :font-size) num))]
      [_ #false]))
  (define parsed-thing
    (match x
      [#false #false]
      [(? number? num) num]
      [(? string? str)
       (match (regexp-match #px"^(-?[0-9\\.]+)\\s*([a-z]+)([0-9\\.]+)?$" (string-downcase str))
         [(list str
                (app string->number num)
                (app unit->converter-proc converter-proc)
                #false) ; prefix measurement (suffix is #false)
          #:when (and converter-proc num)
          (converter-proc num)]
         [(list str
                (app string->number prefix-num)
                (and (regexp pica-pat) unit)
                (app string->number suffix-num))
          #:when (and prefix-num suffix-num) ; prefix + suffix measurement (only pica + point)
          (pica->pts prefix-num suffix-num)]
         [_ str])]))
  (match parsed-thing
    [(and (? integer?) (? inexact?)) (inexact->exact parsed-thing)]
    [_ parsed-thing]))

(module+ test
  (require rackunit)
  (check-equal? (parse-dimension "Foobar") "Foobar")
  (check-equal? (parse-dimension "72plink") "72plink")
  (check-equal? (parse-dimension "7..2pt") "7..2pt")
  (check-equal? (parse-dimension "72pt") 72)
  (check-equal? (parse-dimension "72.5pts") 72.5)
  (check-equal? (parse-dimension "-72point") -72)
  (check-equal? (parse-dimension "-72.5points") -72.5)
  (check-equal? (parse-dimension "72.5points3") "72.5points3")
  (check-equal? (parse-dimension "2in") 144)
  (check-equal? (parse-dimension "-2ins") -144)
  (check-equal? (parse-dimension "2.5inch") 180)
  (check-equal? (parse-dimension "-2.5inches") -180)
  (check-equal? (parse-dimension "2.54cm") (parse-dimension "1in"))
  (check-equal? (parse-dimension "-2.54cms") (parse-dimension "-1in"))
  (check-equal? (parse-dimension "25.4mm") (parse-dimension "1in"))
  (check-equal? (parse-dimension "-25.4mms") (parse-dimension "-1in"))
  (check-equal? (parse-dimension "2.5em" (hash :font-size 12)) 30)
  (check-equal? (parse-dimension "-2.5em" (hash :font-size 12)) -30)
  (check-equal? (parse-dimension "25.4em") "25.4em")
  (check-equal? (parse-dimension "-25.4ems") "-25.4ems")
  (check-equal? (parse-dimension "6pica3") 75)
  (check-equal? (parse-dimension "6.5picas3.5") 81.5)
  (check-equal? (parse-dimension "12p") 144)
  (check-equal? (parse-dimension "-6p3") -75)
  (check-equal? (parse-dimension "-6.5ps3.5") -81.5))

(define (copy-block-attrs source-hash dest-hash)
  (define new-hash (make-hasheq))
  (for ([(k v) (in-hash dest-hash)])
       (hash-set! new-hash k v))
  (for ([(k v) (in-hash source-hash)]
        #:when (memq k block-attrs))
       (hash-set! new-hash k v))
  new-hash)

(define-syntax (define-attrs stx)
  (syntax-case stx ()
    [(_ (ATTR-NAME ...))
     (with-syntax ([(ATTR-ID ...) (for/list ([attr-id (in-list (syntax->list #'(ATTR-NAME ...)))])
                                            (format-id stx ":~a" (syntax-e attr-id)))])
       #'(begin
           (define ATTR-ID 'ATTR-NAME) ...))]
    [(_ ID (ATTR-NAME ...))
     (replace-context stx
                      #'(begin
                          (define ID (list 'ATTR-NAME ...))
                          (define-attrs (ATTR-NAME ...))))]))


#|
Naming guidelines
+ shorter is better
+ general to specific: border-color-left, not border-left-color or left-border-color
+ don't refer to specific output format, e.g. PDF or HTML
+ consistency with CSS style property names is OK if the concept is mostly the same, but usually it's not
+ default value for any missing attr is #false
+ measurement units are points by default

|#


(define-attrs (font-family
               font-path
               font-size
               font-color
               font-features
               font-features-adjust
               font-italic
               font-bold
               font-underline
               font-tracking
               font-baseline-shift
               font-case
                           
               bg
               link
               href
               line-height
               hyphenate
               list-index
               no-colbr
               no-pbr
               page-number
               doc-title

               output-path

               pdf-title
               pdf-subject
               pdf-author
               pdf-keywords

               break

               draw
               width
               height
               parent
               anchor-from
               anchor-from-parent
               anchor-to
               string
               color
               stroke
               x
               y
               x1 ;; x & y are the top-left pos of the bounding box.
               y1 ;; x1 & y1 are the starting points of a line
               x2
               y2))
  

(define-attrs block-attrs (display
                           ;; inset values increase the layout size of the quad.
                           ;; they are relative to the natural layout box.
                           inset-top
                           inset-bottom
                           inset-left
                           inset-right
                           ;; border-inset values do not increase the layout size of the quad.
                           ;; they are relative to the layout size of the quad, with inset values included.
                           ;; this is different from CSS, where margin + padding increase the size of the layout.
                           ;; one has to be dependent on the other, so a choice must be made.
                           ;; I find this approach more sensible because
                           ;; borders are a styling element, not a layout element.
                           ;; this means that changing the inset values will change the position of the border.
                           ;; but this is preferable to the CSS model, where moving the border changes the layout.
                           ;; principle: minimize the number of values that affect the layout,
                           ;; so it's easier to reason about programmatically.
                           border-inset-top
                           border-inset-bottom
                           border-inset-left
                           border-inset-right
                           border-width-left
                           border-width-right
                           border-width-top
                           border-width-bottom
                           border-color-left
                           border-color-right
                           border-color-top
                           border-color-bottom
                           background-color

                           block-clip ; whether box boundary clips its contents

                           space-after ; space after a block, like a paragraph
                           space-before
                           
                           column-count
                           column-gap
                      
                           keep-first-lines
                           keep-last-lines

                           keep-with-next

                           line-align

                           first-line-indent

                           line-wrap

                           image-file
                           image-object
                           image-alt
                           image-height
                           image-width

                           page-width
                           page-height
                           page-size ; e.g., "letter"
                           page-orientation ; only applies to page-size dimensions

                           page-number-start ; 1
                           page-side-start ; 'right

                           page-margin-top
                           page-margin-bottom
                           page-margin-left
                           page-margin-right
                           page-margin-gutter

                           footer-display
                           footer-text

                           ;; we want this distinct from anchor-parent
                           ;; because the two directives may overlap / contradict.
                           ;; for instance: repeat on every page,
                           ;; a quad that prints on the previous page on the second line.
                           repeat

                           draw-debug

                           fn-ref
                           fn-text
                           fn-text-start))

(define (takes-dimension-string? k)
  (and (memq k (list :page-width
                     :page-height
                     :page-margin-top
                     :page-margin-bottom
                     :page-margin-left
                     :page-margin-right
                     :page-margin-gutter
                     :column-gap
                     :inset-top
                     :inset-bottom
                     :inset-left
                     :inset-right
                     :border-inset-top
                     :border-inset-bottom
                     :border-inset-left
                     :border-inset-right
                     :border-width-left
                     :border-width-right
                     :border-width-top
                     :border-width-bottom
                     :space-before
                     :space-after
                     :image-height
                     :image-width
                     :font-size
                     :font-tracking
                     :font-baseline-shift
                     :line-height
                     :x
                     :y
                     :x1
                     :x2
                     :y1
                     :y2
                     :stroke
                     :width)) #true))

(define (has-case-sensitive-value? k)
  (and (memq k (list :pdf-title
                     :pdf-subject
                     :pdf-author
                     :pdf-keywords
                     :string
                     :link
                     :font-family
                     :footer-text
                     :output-path)) #true))

(define (takes-path? k)
  (and (memq k (list :image-file)) #true))
