#lang racket/base
(require racket/class
         racket/gui/base
         racket/system
         "graph.rkt")

(provide dot-positioning find-dot
         dot-label neato-label neato-hier-label neato-ipsep-label)
(define dot-label "dot")
(define neato-label "neato")
(define neato-hier-label "neato – hier")
(define neato-ipsep-label "neato – ipsep")

;; the code that finds the dot executable is the same as
;; pkg/contract-profile/dot.rkt; please check the other 
;; place if you find a bug here. the only reason it isn't
;; shared is dependencies and lack of an obvious common
;; place in between to put it.

;; these paths are explicitly checked (when find-executable-path
;; fails) because starting drracket from the finder (or the dock) 
;; under mac os x generally does not get the path right.
(define dot-paths 
  '("/usr/bin"
    "/bin"
    "/usr/local/bin"
    "/opt/local/bin/"))

(define dot.exe (if (eq? (system-type) 'windows) "dot.exe" "dot"))
(define neato.exe (if (eq? (system-type) 'windows) "neato.exe" "neato"))

(define (find-dot [neato? #f])
  (with-handlers ([(lambda (e) ; may not have permission
                     (and (exn:fail? e)
                          (regexp-match "access denied" (exn-message e))))
                   (λ (x) #f)])
    (define dp (find-executable-path dot.exe))
    (define np (find-executable-path neato.exe))
    (cond
      [(and dp np)
       (if neato? np dp)]
      [else
       (ormap (λ (x) (and (file-exists? (build-path x dot.exe))
                          (file-exists? (build-path x neato.exe))
                          (build-path x (if neato? neato.exe dot.exe))))
              dot-paths)])))

(define (dot-positioning pb [option dot-label] [overlap? #f])
  (define dot-path (find-dot (regexp-match #rx"neato" option)))
  (when dot-path
    (define info (snip-info pb))
    (define-values (positions max-y) (run-dot dot-path info option overlap?))
    (send pb begin-edit-sequence)
    (for ([position-line (in-list positions)])
      (define id (list-ref position-line 0))
      (define x (list-ref position-line 1))
      (define y (list-ref position-line 2))
      (define snip (car (hash-ref info id)))
      (send pb move-to snip x (- max-y y)))
    (send pb invalidate-bitmap-cache)
    (send pb end-edit-sequence)))

;; with-snips : pasteboard% 
;;           -> hash-table[snip -> (list i number[width] number[height] (listof number))]
(define (snip-info pb)
  (let ([num-ht (make-hasheq)]
        [children-ht (make-hasheq)])
    (let loop ([snip (send pb find-first-snip)]
               [i 0])
      (when snip
        (when (is-a? snip graph-snip<%>)
          (hash-set! num-ht snip i))
        (loop (send snip next) (+ i 1))))
    (let ([lb (box 0)]
          [tb (box 0)]
          [rb (box 0)]
          [bb (box 0)])
      (hash-for-each
       num-ht
       (λ (snip num)
         (send pb get-snip-location snip lb tb #f)
         (send pb get-snip-location snip rb bb #t)
         (hash-set! children-ht
                    (hash-ref num-ht snip)
                    (list 
                     snip
                     (- (unbox rb) (unbox lb))
                     (- (unbox bb) (unbox tb))
                     (filter
                      values
                      (map (λ (c) (hash-ref num-ht c #f))
                           (send snip get-children)))))))
      children-ht)))

;; run-dot : hash-table[snip -> (list i (listof number))] string -> void
(define (run-dot dot-path ht option overlap?)
  (define info (sort (hash-map ht (λ (k v) (cons k v)))
                     (λ (x y) (< (car x) (car y)))))
  (let-values ([(in1 out1) (make-pipe)]
               [(in2 out2) (make-pipe)])
    (thread
     (λ ()
       (parameterize ([current-output-port out1])
         (graph->dot info option overlap?))
       (close-output-port out1)))
    (thread
     (λ ()
       (parameterize ([current-input-port in1]
                      [current-output-port out2])
         (system (format "~a -Tplain" (path->string dot-path))))
       (close-output-port out2)
       (close-input-port in1)))
    (parse-plain in2)))

;; graph->dot (listof (list snip number (listof number)) -> void
;; prints out the dot input file based on `g'
(define (graph->dot g option overlap?)
  (printf "digraph {\n")
  (cond
    [(equal? option dot-label)
     (printf "  rankdir=\"~a\"\n" (if overlap? "LR" "TB"))]
    [(equal? option neato-label)
     (printf "  overlap=\"~a\"\n" (if overlap? "true" "false"))
     (printf "  splines=\"true\"\n")]
    [(equal? option neato-hier-label)
     (printf "  overlap=\"~a\"\n" (if overlap? "true" "false"))
     (printf "  mode=\"hier\"\n")
     (printf "  splines=\"true\"\n")]
    [(equal? option neato-ipsep-label)
     (printf "  mode=\"ipsep\"\n")
     (printf "  splines=\"true\"\n")
     (printf "  overlap=\"~a\"\n" (if overlap? "true" "ipsep"))])
  (for-each
   (λ (line)
     (let* ([snip (list-ref line 1)]
            [id (list-ref line 0)]
            [w (list-ref line 2)]
            [h (list-ref line 3)]
            [children (list-ref line 4)])
       (printf "  ~a [width=~a height=~a shape=box label=\"\"]\n" 
               (num->id id) 
               (format-number (pixels->inches w))
               (format-number (pixels->inches h)))
       (for-each
        (λ (child)
          (printf "  ~a -> ~a\n" (num->id id) (num->id child)))
        children)))
   g)
  (printf "}\n"))

(define (num->id n) (format "s~a" n))
(define (id->num s) (string->number (substring s 1 (string-length s))))

(define (format-number n)
  (let ([candidate (number->string (exact->inexact n))])
    (cond
      [(regexp-match #rx"^([0-9]*)\\.([0-9]*)$" candidate)
       =>
       (λ (m)
         (let ([prefix (list-ref m 1)]
               [suffix (list-ref m 2)])
           (if (< (string-length suffix) 5)
               candidate
               (string-append prefix "." (substring suffix 0 4)))))]
      [else
       candidate])))
  
(define (parse-plain port)
  
  (define max-y 0)
  
  (define positions '())
  
  (define (main)
    (let ([graph-line (read-line port)])
      (let loop ()
        (let ([line (read-line port)])
          (cond
            [(regexp-match #rx"^node" line)
             (join (parse-node line)
                   (loop))]
            [(regexp-match #rx"^edge" line)
             (join (parse-edge draw-edges possibly-update-max-y line)
                   (loop))]
            [(regexp-match #rx"stop" line)
             void]
            [else
             (error 'mrlib/private/dot "didn't recognize line:\n  ~s" line)])))))
  
  (define (possibly-update-max-y y)
    (set! max-y (max y max-y)))
  
  (define (join p1 p2)
    (cond
      [(eq? p1 void) p2]
      [(eq? p2 void) p1]
      [else 
       (λ (dc left top right bottom dx dy) 
         (p1 dc left top right bottom dx dy)
         (p2 dc left top right bottom dx dy))]))

  (define (parse-node line) 
    (let*-values ([(node line) (chomp line)]
                  [(id line) (chomp line)]
                  [(raw-x line) (chomp line)]
                  [(raw-y line) (chomp line)]
                  [(raw-w line) (chomp line)]
                  [(raw-h line) (chomp line)]
                  [(label line) (chomp line)]
                  [(style line) (chomp line)]
                  [(raw-shape line) (chomp line)]
                  [(color line) (chomp line)]
                  [(fillcolor line) (chomp line)])
      
      (define x (inches->pixels (string->number raw-x)))
      (define y (inches->pixels (string->number raw-y)))
      (define w (inches->pixels (string->number raw-w)))
      (define h (inches->pixels (string->number raw-h)))
      (define shape (string->symbol raw-shape))
      
      (set! positions (cons (list (id->num id) 
                                  (- x (/ w 2)) 
                                  (+ y (/ h 2)))
                            positions))
      
      (set! max-y (max (+ y h) max-y))
      void))
  
  ;; chomp : string -> (values string (union #f string))
  ;; returns the first word at the beginning of the string
  ;; and the remainder of the string (or #f is there is no more)
  (define (chomp s)
    (let ([s (regexp-replace #rx"^ *" s "")])
      (cond
        [(equal? s "")
         (values "" #f)]
        [else
         (case (string-ref s 0)
           [(#\") (let ([m (regexp-match #rx"^\"([^\"]*)\"(.*)$" s)])
                    (values (list-ref m 1)
                            (list-ref m 2)))]
           [else
            (cond
              [(regexp-match #rx"^([^ ]*) (.*)$" s)
               =>
               (λ (m)
                 (values (list-ref m 1)
                         (list-ref m 2)))]
              [(regexp-match #rx"^([^ ]*)$" s)
               =>
               (λ (m)
                 (values (list-ref m 1)
                         #f))]
              [else
               (error 'mrlib/private/dot "~s" s)])])])))
            
  (define (draw-edges dc dx dy raw-points)
    (let ([points (map (λ (x) (list (inches->pixels (car x))
                                    (inches->pixels (list-ref x 1))))
                       raw-points)])
      (send dc set-pen "blue" 1 'solid)
      (send dc set-brush "black" 'transparent)
      (let ([path (new dc-path%)])
        (send path move-to 
              (car (car points))
              (- max-y (cadr (car points))))
        (let loop ([points (cdr points)])
          (cond
            [(null? points) (void)]
            [else (let ([p1 (list-ref points 0)]
                        [p2 (list-ref points 1)]
                        [p3 (list-ref points 2)])
                    (send path curve-to 
                          (list-ref p1 0) (- max-y (list-ref p1 1))
                          (list-ref p2 0) (- max-y (list-ref p2 1))
                          (list-ref p3 0) (- max-y (list-ref p3 1)))
                    (loop (cdddr points)))]))
        (send dc draw-path path dx dy))))
  (main)
  (values positions
          max-y))

(define (parse-edge draw-edges possibly-update-max-y line)
  (define (give-up)
    (error 'mrlib/private/dot "could not parse edge line:\n  ~s\n" line))
  (define m (regexp-match #rx"edge ([^ ]+) ([^ ]+) ([0-9]+) (.*)$" line))
  (unless m (give-up))
  (define from (list-ref m 1))
  (define to (list-ref m 2))
  (define point-count (string->number (list-ref m 3)))
  (define rest (list-ref m 4))
  (define points 
    (let loop ([pts point-count]
               [rest rest])
      (cond
        [(zero? pts) '()]
        [else
         (define m (regexp-match #rx"^([-0-9e.]+) ([-0-9e.]+) (.*)$" rest))
         (unless m (give-up))
         (define x (string->number (list-ref m 1)))
         (define y (string->number (list-ref m 2)))
         (unless x (give-up))
         (unless y (give-up))
         (possibly-update-max-y y)
         (cons (list x y)
               (loop (- pts 1)
                     (list-ref m 3)))])))
  (λ (dc left top right bottom dx dy)
    (draw-edges dc dx dy points)))

(module+ test
  (parse-edge
   void
   void
   (string-append
    "edge s0 s2 13 1.4555 4.7222 1.1487 4.6131 0.81639 4.4528 0.56944 4.2222 0.15175 3.8322"
    " 3.7007e-17 3.6409 0 3.0694 0 3.0694 0 3.0694 0 2.0972 0 1.1469 1.2205 0.56102 1.7932 0.33843"
    " solid black")))

(define (pixels->inches x) (/ x 72))
(define (inches->pixels x) (* x 72))
