
#lang scheme/base
(require scheme/list
         scheme/class
         scheme/gui
         drscheme/arrow
         framework/framework
         "../util/notify.ss")

(provide text:hover<%>
         text:hover-identifier<%>
         text:mouse-drawings<%>
         text:arrows<%>

         text:hover-mixin
         text:hover-identifier-mixin
         text:mouse-drawings-mixin
         text:tacking-mixin
         text:arrows-mixin)

(define arrow-brush
  (send the-brush-list find-or-create-brush "white" 'solid))
(define (tacked-arrow-brush color)
  (send the-brush-list find-or-create-brush color 'solid))

(define billboard-brush
  (send the-brush-list find-or-create-brush "white" 'solid))

(define white (send the-color-database find-color "white"))

;; A Drawing is (make-drawing number number (??? -> void) boolean boolean)
(define-struct drawing (start end draw visible? tacked?) #:mutable)

(define-struct idloc (start end id))

(define (mean x y)
  (/ (+ x y) 2))

(define-syntax with-saved-pen&brush
  (syntax-rules ()
    [(with-saved-pen&brush dc . body)
     (save-pen&brush dc (lambda () . body))]))

(define (save-pen&brush dc thunk)
  (let ([old-pen (send dc get-pen)]
        [old-brush (send dc get-brush)])
    (begin0 (thunk)
      (send dc set-pen old-pen)
      (send dc set-brush old-brush))))

(define-syntax with-saved-text-config
  (syntax-rules ()
    [(with-saved-text-config dc . body)
     (save-text-config dc (lambda () . body))]))

(define (save-text-config dc thunk)
  (let ([old-font (send dc get-font)]
        [old-color (send dc get-text-foreground)]
        [old-background (send dc get-text-background)]
        [old-mode (send dc get-text-mode)])
    (begin0 (thunk)
            (send dc set-font old-font)
            (send dc set-text-foreground old-color)
            (send dc set-text-background old-background)
            (send dc set-text-mode old-mode))))

(define text:hover<%>
  (interface (text:basic<%>)
    update-hover-position))

(define text:hover-identifier<%>
  (interface ()
    get-hovered-identifier
    set-hovered-identifier
    listen-hovered-identifier))

(define text:mouse-drawings<%>
  (interface (text:basic<%>)
    add-mouse-drawing
    for-each-drawing
    delete-all-drawings))

(define text:arrows<%>
  (interface (text:mouse-drawings<%>)
    add-arrow
    add-question-arrow
    add-billboard))

(define text:hover-mixin
  (mixin (text:basic<%>) (text:hover<%>)
    (inherit dc-location-to-editor-location
             find-position)

    (define/override (on-default-event ev)
      (define gx (send ev get-x))
      (define gy (send ev get-y))
      (define-values (x y) (dc-location-to-editor-location gx gy))
      (define pos (find-position x y))
      (super on-default-event ev)
      (case (send ev get-event-type)
        ((enter motion leave)
         (update-hover-position pos))))

    (define/public (update-hover-position pos)
      (void))

    (super-new)))

(define text:hover-identifier-mixin
  (mixin (text:hover<%>) (text:hover-identifier<%>)
    (field/notify hovered-identifier (new notify-box% (value #f)))

    (define idlocs null)

    (define/public (add-identifier-location start end id)
      (set! idlocs (cons (make-idloc start end id) idlocs)))

    (define/public (delete-all-identifier-locations)
      (set! idlocs null)
      (set-hovered-identifier #f))

    (define/override (update-hover-position pos)
      (super update-hover-position pos)
      (let search ([idlocs idlocs])
        (cond [(null? idlocs) (set-hovered-identifier #f)]
              [(and (<= (idloc-start (car idlocs)) pos)
                    (< pos (idloc-end (car idlocs))))
               (set-hovered-identifier (idloc-id (car idlocs)))]
              [else (search (cdr idlocs))])))
    (super-new)))

(define text:mouse-drawings-mixin
  (mixin (text:hover<%>) (text:mouse-drawings<%>)
    (inherit dc-location-to-editor-location
             find-position
             invalidate-bitmap-cache)

    ;; list of Drawings
    (field [drawings-list null])

    (define/public add-mouse-drawing
      (case-lambda
        [(start end draw)
         (add-mouse-drawing start end draw (box #f))]
        [(start end draw tack-box)
         (set! drawings-list 
               (cons (make-drawing start end draw #f tack-box)
                     drawings-list))]))

    (define/public (delete-all-drawings)
      (set! drawings-list null))

    (define/public-final (for-each-drawing f)
      (for-each f drawings-list))

    (define/override (on-paint before? dc left top right bottom dx dy draw-caret)
      (super on-paint before? dc left top right bottom dx dy draw-caret)
      (unless before?
        (for-each-drawing
         (lambda (d)
           (when (or (drawing-visible? d) (unbox (drawing-tacked? d)))
             ((drawing-draw d) this dc left top right bottom dx dy))))))

    (define/override (update-hover-position pos)
      (super update-hover-position pos)
      (let ([changed? (update-visible-drawings pos)])
        (when changed? (invalidate-bitmap-cache 0.0 0.0 +inf.0 +inf.0))))

    (define/private (update-visible-drawings pos)
      (let ([changed? #f])
        (for-each-drawing
         (lambda (d)
           (let ([vis? (<= (drawing-start d) pos (drawing-end d))])
             (unless (eqv? vis? (drawing-visible? d))
               (set-drawing-visible?! d vis?)
               (set! changed? #t)))))
        changed?))

    (super-new)))

(define text:tacking-mixin
  (mixin (text:basic<%> text:mouse-drawings<%>) ()
    (inherit get-canvas
             for-each-drawing)
    (inherit-field drawings-list)
    (super-new)

    (define/override (on-event ev)
      (case (send ev get-event-type)
        ((right-down)
         (if (ormap (lambda (d) (drawing-visible? d)) drawings-list)
             (send (get-canvas) popup-menu
                   (make-tack/untack-menu)
                   (send ev get-x)
                   (send ev get-y))
             (super on-event ev)))
        (else
         (super on-event ev))))

    (define/private (make-tack/untack-menu)
      (define menu (new popup-menu%))
      (new menu-item% (label "Tack")
           (parent menu)
           (callback
            (lambda _ (tack))))
      (new menu-item% (label "Untack")
           (parent menu)
           (callback
            (lambda _ (untack))))
      menu)

    (define/private (tack)
      (for-each-drawing
       (lambda (d)
         (when (drawing-visible? d)
           (set-box! (drawing-tacked? d) #t)))))
    (define/private (untack)
      (for-each-drawing
       (lambda (d)
         (when (drawing-visible? d)
           (set-box! (drawing-tacked? d) #f)))))))

(define text:arrows-mixin
  (mixin (text:mouse-drawings<%>) (text:arrows<%>)
    (inherit position-location
             add-mouse-drawing
             find-wordbreak)

    (define/public (add-arrow from1 from2 to1 to2 color)
      (internal-add-arrow from1 from2 to1 to2 color #f))

    (define/public (add-question-arrow from1 from2 to1 to2 color)
      (internal-add-arrow from1 from2 to1 to2 color #t))

    (define/public (add-billboard pos1 pos2 str color-name)
      (define color (send the-color-database find-color color-name))
      (let ([draw 
             (lambda (text dc left top right bottom dx dy)
               (let-values ([(x y) (range->mean-loc pos1 pos1)]
                            [(fw fh _d _v) (send dc get-text-extent "y")])
                 (with-saved-pen&brush dc
                   (with-saved-text-config dc
                     (send* dc
                       (set-pen color 1 'solid)
                       (set-brush billboard-brush)
                       (set-text-mode 'solid)
                       (set-font (billboard-font dc))
                       (set-text-foreground color))
                     (let-values ([(w h d v) (send dc get-text-extent str)]
                                  [(adj-y) fh]
                                  [(mini) _d])
                       (send* dc
                         (draw-rounded-rectangle
                          (+ x dx)
                          (+ y dy adj-y)
                          (+ w mini mini)
                          (+ h mini mini))
                         (draw-text str (+ x dx mini) (+ y dy mini adj-y))))))))])
        (add-mouse-drawing pos1 pos2 draw)))

    (define/private (internal-add-arrow from1 from2 to1 to2 color-name question?)
      (define color (send the-color-database find-color color-name))
      (define tack-box (box #f))
      (unless (and (= from1 to1) (= from2 to2))
        (let ([draw 
               (lambda (text dc left top right bottom dx dy)
                 (let-values ([(startx starty) (range->mean-loc from1 from2)]
                              [(endx endy) (range->mean-loc to1 to2)]
                              [(fw fh _d _v) (send dc get-text-extent "x")])
                   (with-saved-pen&brush dc
                     (with-saved-text-config dc
                       (send dc set-pen color 1 'solid)
                       (send dc set-brush
                             (if (unbox tack-box)
                                 (tacked-arrow-brush color)
                                 arrow-brush))
                       (draw-arrow dc startx
                                   (+ starty (/ fh 2))
                                   endx
                                   (+ endy (/ fh 2))
                                   dx dy)
                       (send dc set-text-mode 'transparent)
                       (when question?
                         (send dc set-font (?-font dc))
                         (send dc set-text-foreground color)
                         (send dc draw-text "?" 
                               (+ endx dx fw)
                               (- endy dy fh)))))))])
          (add-mouse-drawing from1 from2 draw tack-box)
          (add-mouse-drawing to1 to2 draw tack-box))))

    (define/private (position->location p)
      (define xbox (box 0.0))
      (define ybox (box 0.0))
      (position-location p xbox ybox)
      (values (unbox xbox) (unbox ybox)))

    (define/private (?-font dc)
      (let ([size (send (send dc get-font) get-point-size)])
        (send the-font-list find-or-create-font size 'default 'normal 'bold)))

    (define/private (billboard-font dc)
      (let ([size (send (send dc get-font) get-point-size)])
        (send the-font-list find-or-create-font size 'default 'normal)))

    (define/private (range->mean-loc pos1 pos2)
      (let*-values ([(loc1x loc1y) (position->location pos1)]
                    [(loc2x loc2y) (position->location pos2)]
                    [(locx) (mean loc1x loc2x)]
                    [(locy) (mean loc1y loc2y)])
        (values locx locy)))

    (super-new)))

(define text:mouse-drawings%
  (text:mouse-drawings-mixin
   (text:hover-mixin
    text:standard-style-list%)))

(define text:arrows%
  (text:arrows-mixin
   (text:tacking-mixin
    text:mouse-drawings%)))
