#lang racket/base
(require framework
         slideshow/pict
         racket/runtime-path
         racket/gui/base
         (for-syntax racket/base)
         racket/class
         racket/promise
         "../pict.rkt"
         "../reduction-semantics.rkt")

(provide btest done show-bitmap-test-gui?)

(define show-bitmap-test-gui? (make-parameter #t))

(define just-save-failures? (getenv "PLTJUSTSAVEFAILURES"))

(define tests 0)
(define failed-tests 0)
(define failed-panels '())
(define (done)
  (if (zero? failed-tests)
      (printf "bitmap-test.rkt: ~a tests, all passed\n" tests)
      (eprintf "bitmap-test.rkt: ~a tests, ~a failed\n" tests failed-tests)))

(define-syntax (btest stx)
  (syntax-case stx ()
    [(_ test-exp bitmap-filename)
     #`(test/proc
        #,(syntax-line stx)
        (λ () test-exp)
        bitmap-filename)]))

(define-runtime-path bmps-dir (format "bmps-~a" (system-type)))

(define (test/proc line-number pict-thunk raw-bitmap-filename)
  (set! tests (+ tests 1))
  (let* ([pict (set-fonts/call pict-thunk)]
         [bitmap-filename 
          (build-path bmps-dir
                      raw-bitmap-filename)]
         [old-bitmap (if (file-exists? bitmap-filename)
                         (read-bitmap bitmap-filename)
                         (let* ([bm (make-bitmap 100 20)]
                                [bdc (make-object bitmap-dc% bm)])
                           (send bdc clear)
                           (send bdc draw-text "does not exist" 0 0)
                           (send bdc set-bitmap #f)
                           bm))]
         [new-bitmap ((if (eq? (system-type) 'unix)
                          make-bitmap
                          make-screen-bitmap)
                       (ceiling (inexact->exact (pict-width pict))) 
                       (ceiling (inexact->exact (pict-height pict))))]
         [bdc (make-object bitmap-dc% new-bitmap)])
    (send bdc clear)
    (draw-pict pict bdc 0 0)
    (send bdc set-bitmap #f)
    (unless (bitmaps-same? old-bitmap new-bitmap)
      (set! failed-tests (+ failed-tests 1))
      (cond
        [just-save-failures? 
         (eprintf "saving ~a\n" bitmap-filename)
         (void (send new-bitmap save-file bitmap-filename 'png))]
        [else
         (when (show-bitmap-test-gui?)
           (let ([failed-panel (make-failed-panel line-number bitmap-filename old-bitmap new-bitmap)])
             (set! failed-panels (append failed-panels (list failed-panel)))))]))))

(define (set-fonts/call thunk)
  (case (system-type)
    [(unix)
     (let ([rewrite-style 
            (λ (s)
              (let loop ([s s])
                (cond
                  [(pair? s) (cons (loop (car s)) (loop (cdr s)))]
                  [(eq? s 'roman) (verify-face "DejaVu Serif")]
                  [(eq? s 'swiss) (verify-face "DejaVu Sans")]
                  [else s])))])
       (parameterize ([label-style (rewrite-style (label-style))]
                      [literal-style (rewrite-style (literal-style))]
                      [metafunction-style (rewrite-style (metafunction-style))]
                      [non-terminal-style (rewrite-style (non-terminal-style))]
                      [non-terminal-subscript-style (rewrite-style (non-terminal-subscript-style))]
                      [non-terminal-superscript-style (rewrite-style (non-terminal-superscript-style))]
                      [default-style (rewrite-style (default-style))])
         (thunk)))]
    [else
     (thunk)]))

(define (verify-face face)
  (unless (member face (get-face-list))
    (error 'verify-face "unknown face: ~s" face))
  face)

(define (bitmaps-same? old-bitmap new-bitmap)
  (let ([w (send old-bitmap get-width)]
        [h (send old-bitmap get-height)])
    (and (= w (send new-bitmap get-width))
         (= h (send new-bitmap get-height))
         (let ([bytes1 (make-bytes (* w h 4))]
               [bytes2 (make-bytes (* w h 4))])
           (send old-bitmap get-argb-pixels 0 0 w h bytes1)
           (send new-bitmap get-argb-pixels 0 0 w h bytes2)
           (equal? bytes1 bytes2)))))

(define test-result-single-panel #f)
(define (get-test-result-single-panel)
  (cond
    [test-result-single-panel
     test-result-single-panel]
    [else
     (let ()
       (define f (new frame% [label "bitmap-test.rkt failures"]))
       (define lined (new vertical-panel% [parent f] [style '(border)]))
       (define sp (new panel:single% [parent lined]))
       (define current-index 0)
       (define hp (new horizontal-panel% [parent f] [alignment '(center center)]))
       (define prev 
         (new button% 
              [label "Prev"] 
              [parent hp]
              [callback
               (λ (x y)
                 (set! current-index (modulo (- current-index 1) (length failed-panels)))
                 (update-gui))]))
       (define next (new button% 
                         [label "Next"] 
                         [parent hp]
                         [callback
                          (λ (x y)
                            (set! current-index (modulo (+ current-index 1) (length failed-panels)))
                            (update-gui))]))
       (define (update-gui) 
         (send sp active-child (list-ref failed-panels current-index)))
       (set! test-result-single-panel sp)
       (send f show #t)
       sp)]))

(define (compute-diffs old-bitmap new-bitmap)
  (define ow (send old-bitmap get-width))
  (define nw (send new-bitmap get-width))
  (define oh (send old-bitmap get-height))
  (define nh (send new-bitmap get-height))
  (define w (max ow nw))
  (define h (max oh nh))
  (define old-bytes (make-bytes (* ow oh 4)))
  (define new-bytes (make-bytes (* nw nh 4)))
  (define diff-bytes (make-bytes (* w h 4) 255))
  (define number-of-different-pixels 0)
  (send old-bitmap get-argb-pixels 0 0 ow oh old-bytes)
  (send new-bitmap get-argb-pixels 0 0 nw nh new-bytes)
  (let loop ([x 0])
    (unless (= x w)
      (let loop ([y 0])
        (unless (= y h)
          (define diff-start (* 4 (+ (* y w) x)))
          (cond
            [(and (< x nw)
                  (< y nh)
                  (< x ow)
                  (< y oh))
             (define old-start (* 4 (+ (* y ow) x)))
             (define new-start (* 4 (+ (* y nw) x)))
             (define a (bytes-ref old-bytes old-start))
             (define r (bytes-ref old-bytes (+ old-start 1)))
             (define g (bytes-ref old-bytes (+ old-start 2)))
             (define b (bytes-ref old-bytes (+ old-start 3)))
             (cond
               [(and (= a (bytes-ref new-bytes new-start))
                     (= r (bytes-ref new-bytes (+ new-start 1)))
                     (= g (bytes-ref new-bytes (+ new-start 2)))
                     (= b (bytes-ref new-bytes (+ new-start 3))))
                (bytes-set! diff-bytes diff-start a)
                (bytes-set! diff-bytes (+ diff-start 1) r)
                (bytes-set! diff-bytes (+ diff-start 2) g)
                (bytes-set! diff-bytes (+ diff-start 3) b)]
               [else
                (set! number-of-different-pixels (+ number-of-different-pixels 1))
                ;; don't need to set diff-start or (+ diff-start 1) since
                ;; the bytes are initialized to 255
                (bytes-set! diff-bytes (+ diff-start 2) 0)
                (bytes-set! diff-bytes (+ diff-start 3) 0)])]
            [else
             (bytes-set! diff-bytes (+ diff-start 2) 0)
             (bytes-set! diff-bytes (+ diff-start 3) 0)])
          (loop (+ y 1))))
      (loop (+ x 1))))
  (define diff-bitmap (make-bitmap w h))
  (send diff-bitmap set-argb-pixels 0 0 w h diff-bytes)
  (values diff-bitmap number-of-different-pixels))

(define (make-failed-panel line-number filename old-bitmap new-bitmap)
  (define diff-bitmap 'unk)
  (define number-of-different-pixels #f)
  (define f (new vertical-panel% [parent (get-test-result-single-panel)]))
  (define msg (new message% [label (format "line ~a" line-number)] [parent f]))
  (define hp (new horizontal-panel% [parent f]))
  (define vp1 (new vertical-panel% [parent hp]))
  (define vp2 (new vertical-panel% [parent hp]))
  (define computing/differences-msg (new message% [label ""] [auto-resize #t] [parent f]))
  (define chk (new check-box% 
                   [label "Show diff"]
                   [parent f]
                   [callback
                    (λ (_1 _2)
                      (define (update-check)
                        (cond
                          [(send chk get-value)
                           (send right-hand set-label (force diff-bitmap))]
                          [else
                           (send right-hand set-label new-bitmap)]))
                      (cond
                        [(eq? diff-bitmap 'unk)
                         (send chk enable #f)
                         (send computing/differences-msg set-label "Computing diff ...")
                         (thread
                          (λ ()
                            (define-values (_diff-bitmap number-of-different-pixels) (compute-diffs old-bitmap new-bitmap))
                            (set! diff-bitmap _diff-bitmap)
                            (queue-callback
                             (λ ()
                               (send computing/differences-msg set-label (format "~a pixels different" number-of-different-pixels))
                               (send chk enable #t)
                               (update-check)))))]
                        [else
                         (update-check)]))]))
  (define btn (new button%
                   [parent f]
                   [label "Save"]
                   [callback
                    (λ (x y)
                      (send new-bitmap save-file filename 'png))]))
  (define left-label (new message% 
                          [parent vp1]
                          [label (format "Old ~ax~a" (send old-bitmap get-width) (send old-bitmap get-height))]))
  (define left-hand (new message%
                         [parent vp1]
                         [label old-bitmap]))
  (define right-label (new message% 
                           [parent vp2]
                           [label (format "New ~ax~a" (send new-bitmap get-width) (send new-bitmap get-height))]))
  (define right-hand (new message%
                          [parent vp2]
                          [label new-bitmap]))
  (send left-hand set-label old-bitmap)
  (send right-hand set-label new-bitmap)
  f)
