#lang racket/base
(require racket/class
         racket/gui/base
         racket/promise
         data/interval-map
         framework
         unstable/class-iop
         "pretty-printer.rkt"
         "interfaces.rkt"
         "prefs.rkt"
         "util.rkt")
(provide print-syntax-to-editor
         code-style)

(define-syntax-rule (uninterruptible e ...)
  ;; (coarsely) prevent breaks within editor operations
  (parameterize-break #f (begin e ...))
  #|
  (parameterize-break #f
    (let ([ta (now)])
      (begin0 (begin e ...)
        (let ([tb (now)])
          (eprintf "****\n")
          (pretty-write '(begin e ...) (current-error-port))
          (eprintf "  -- ~s ms\n\n" (- tb ta))))))
  |#)

(define (now) (current-inexact-milliseconds))

;; FIXME: assumes text never moves

;; print-syntax-to-editor : syntax text controller<%> config number number
;;                       -> display<%>
;; Note: must call display<%>::refresh to finish styling.
(define (print-syntax-to-editor stx text controller config columns
                                [insertion-point (send text last-position)])
  (define output-port (open-output-string/count-lines))
  (define range
    (pretty-print-syntax stx output-port 
                         (send/i controller controller<%> get-primary-partition)
                         (length (send/i config config<%> get-colors))
                         (send/i config config<%> get-suffix-option)
                         (send config get-pretty-styles)
                         columns
                         (send config get-pretty-abbrev?)))
  (define output-string (get-output-string output-port))
  (define output-length (sub1 (string-length output-string))) ;; skip final newline
  (fixup-parentheses output-string range)
  (with-unlock text
    (uninterruptible
     (send text insert output-length output-string insertion-point))
    (new display%
         (text text)
         (controller controller)
         (config config)
         (range range)
         (start-position insertion-point)
         (end-position (+ insertion-point output-length)))))

;; display%
;; Note: must call refresh method to finish styling.
(define display%
  (class* object% (display<%>)
    (init-field/i [controller controller<%>]
                  [config config<%>]
                  [range range<%>])
    (init-field text
                start-position
                end-position)

    (define base-style
      (code-style text (send/i config config<%> get-syntax-font-size)))

    ;; on-next-refresh : (listof (cons stx style-delta))
    ;; Styles to be applied on next refresh only. (eg, underline)
    (define on-next-refresh null)

    ;; extra-styles : hash[stx => (listof style-delta)]
    ;; Styles to be re-applied on every refresh.
    (define extra-styles (make-hasheq))

    ;; to-undo-styles : (listof (cons nat nat))
    ;; Ranges to unbold or unhighlight when selection changes.
    ;; FIXME: ought to be managed by text:region-data (to auto-update ranges)
    ;;   until then, positions are relative
    (define to-undo-styles null)

    ;; initialize : -> void
    (define/private (initialize)
      (uninterruptible
       (send text change-style base-style start-position end-position #f))
      (uninterruptible (apply-primary-partition-styles))
      (uninterruptible (add-clickbacks)))

    ;; add-clickbacks : -> void
    (define/private (add-clickbacks)
      (define mapping (send text get-region-mapping 'syntax))
      (define lazy-interval-map-init
        (delay
          (uninterruptible
           (for ([range (send/i range range<%> all-ranges)])
             (let ([stx (range-obj range)]
                   [start (range-start range)]
                   [end (range-end range)])
               (interval-map-set! mapping (+ start-position start) (+ start-position end) stx))))))
      (define (the-callback position)
        (force lazy-interval-map-init)
        (send/i controller selection-manager<%> set-selected-syntax
                (interval-map-ref mapping position #f)))
      (send text set-clickregion start-position end-position the-callback)
      (send text set-clickregion start-position end-position the-callback 'right-down))

    ;; refresh : -> void
    ;; Clears all highlighting and reapplies all non-foreground styles.
    (define/public (refresh)
      (with-unlock text
        (uninterruptible
         (let ([undo-select/highlight-d (get-undo-select/highlight-d)])
           (for ([r (in-list to-undo-styles)])
             (send text change-style undo-select/highlight-d
                   (relative->text-position (car r))
                   (relative->text-position (cdr r)))))
         (set! to-undo-styles null))
        (uninterruptible
         (for ([stx+delta (in-list on-next-refresh)])
           (for ([r (in-list (send/i range range<%> get-ranges (car stx+delta)))])
             (restyle-range r (cdr stx+delta) #f)))
         (set! on-next-refresh null))
        (uninterruptible
         (apply-extra-styles))
        (let ([selected-syntax
               (send/i controller selection-manager<%>
                       get-selected-syntax)])
          (uninterruptible
           (apply-secondary-relation-styles selected-syntax))
          (uninterruptible
           (apply-selection-styles selected-syntax)))))

    ;; get-range : -> range<%>
    (define/public (get-range) range)

    ;; get-start-position : -> number
    (define/public (get-start-position) start-position)

    ;; get-end-position : -> number
    (define/public (get-end-position) end-position)

    ;; highlight-syntaxes : (list-of syntax) string -> void
    (define/public (highlight-syntaxes stxs hi-color)
      (let ([delta (highlight-style-delta hi-color)])
        (for ([stx (in-list stxs)])
          (hash-set! extra-styles stx
                     (cons delta (hash-ref extra-styles stx null))))))

    ;; underline-syntaxes : (listof syntax) -> void
    (define/public (underline-syntaxes stxs)
      (for ([stx (in-list stxs)])
        (set! on-next-refresh
              (cons (cons stx underline-d) on-next-refresh))))

    ;; Primary styles
    ;; (Done once on initialization, never repeated)

    ;; apply-primary-partition-styles : -> void
    ;; Changes the foreground color according to the primary partition.
    ;; Only called once, when the syntax is first drawn.
    (define/private (apply-primary-partition-styles)
      (define style-list (send text get-style-list))
      (define (color-style color)
        (let ([delta (new style-delta%)])
          (send delta set-delta-foreground color)
          (send style-list find-or-create-style base-style delta)))
      (define color-styles
        (list->vector
         (map color-style
              (map translate-color
                   (send/i config config<%> get-colors)))))
      (define overflow-style (color-style (translate-color "darkgray")))
      (define color-partition
        (send/i controller mark-manager<%> get-primary-partition))
      (define offset start-position)
      ;; Optimization: don't call change-style when new style = old style
      (let tr*loop ([trs (send/i range range<%> get-treeranges)] [old-style #f])
        (for ([tr trs])
          (define stx (treerange-obj tr))
          (define start (treerange-start tr))
          (define end (treerange-end tr))
          (define subs (treerange-subs tr))
          (define new-style
            (primary-style stx color-partition color-styles overflow-style))
          (unless (eq? old-style new-style)
            (send text change-style new-style (+ offset start) (+ offset end) #f))
          (tr*loop subs new-style)))
      (void))

    ;; primary-style : syntax partition (vector-of style-delta%) style-delta%
    ;;               -> style-delta%
    (define/private (primary-style stx partition color-vector overflow)
      (let ([n (send/i partition partition<%> get-partition stx)])
        (cond [(< n (vector-length color-vector))
               (vector-ref color-vector n)]
              [else
               overflow])))

    ;; Secondary Styling
    ;; May change in response to user actions

    ;; apply-extra-styles : -> void
    ;; Applies externally-added styles (such as highlighting)
    (define/private (apply-extra-styles)
      (for ([(stx deltas) (in-hash extra-styles)])
        (for ([r (in-list (send/i range range<%> get-ranges stx))])
          (for ([delta (in-list deltas)])
            (restyle-range r delta #t)))))

    ;; apply-selection-styles : syntax -> void
    ;; Styles subterms eq to the selected syntax
    (define/private (apply-selection-styles selected-syntax)
      (for ([r (in-list (send/i range range<%> get-ranges selected-syntax))])
        (restyle-range r select-d #t)))

    ;; apply-secondary-relation-styles : selected-syntax -> void
    ;; If the selected syntax is an identifier, then styles all identifiers
    ;; in the relation with it.
    (define/private (apply-secondary-relation-styles selected-syntax)
      (when (identifier? selected-syntax)
        (let* ([name+relation
                (send/i controller secondary-relation<%>
                        get-identifier=?)]
               [relation (and name+relation (cdr name+relation))]
               [secondary-highlight-d (get-secondary-highlight-d)])
          (when relation
            (for ([id (in-list (send/i range range<%> get-identifier-list))])
              (when (relation selected-syntax id)
                (for ([r (in-list (send/i range range<%> get-ranges id))])
                  (restyle-range r secondary-highlight-d #t))))))))

    ;; restyle-range : (cons num num) style-delta% boolean -> void
    (define/private (restyle-range r style need-undo?)
      (when need-undo? (set! to-undo-styles (cons r to-undo-styles)))
      (send text change-style style
            (relative->text-position (car r))
            (relative->text-position (cdr r))))

    ;; relative->text-position : number -> number
    (define/private (relative->text-position pos)
      (+ pos start-position))

    ;; Initialize
    (super-new)
    (send/i controller controller<%> add-syntax-display this)
    (initialize)))

;; fixup-parentheses : string range -> void
(define (fixup-parentheses string range)
  (for ([r (send/i range range<%> all-ranges)])
    (let ([stx (range-obj r)]
          [start (range-start r)]
          [end (range-end r)])
      (when (and (syntax? stx) (pair? (syntax-e stx)))
        (case (syntax-property stx 'paren-shape)
          ((#\[)
           (string-set! string start #\[)
           (string-set! string (sub1 end) #\]))
          ((#\{) 
           (string-set! string start #\{)
           (string-set! string (sub1 end) #\})))))))

(define (open-output-string/count-lines)
  (let ([os (open-output-string)])
    (port-count-lines! os)
    os))

;; code-style : text<%> number/#f -> style<%>
(define (code-style text font-size)
  (let* ([style-list (send text get-style-list)]
         [style (send style-list find-named-style (editor:get-default-color-style-name))])
    (if font-size
        (send style-list find-or-create-style
              style
              (make-object style-delta% 'change-size font-size))
        style)))

;; anchor-snip%
(define anchor-snip%
  (class snip%
    (define/override (copy)
      (make-object string-snip% ""))
    (super-instantiate ())))

;; Color translation

;; translate-color : color-string -> color%
(define (translate-color color-string)
  (let ([c (make-object color% color-string)])
    (if (pref:invert-colors?)
        (let-values ([(r* g* b*)
                      (lightness-invert (send c red) (send c green) (send c blue))])
          #|
          (printf "translate: ~s -> ~s\n"
                  (list (send c red) (send c green) (send c blue))
                  (list r* g* b*))
          |#
          (make-object color% r* g* b*))
        c)))

;; lightness-invert : uint8 uint8 uint8 -> (values uint8 uint8 uint8)
(define (lightness-invert r g b)
  (define (c x)
    (/ (exact->inexact x) 255.0))
  (define (d x)
    (inexact->exact (round (* x 255))))
  (let-values ([(r g b) (lightness-invert* (c r) (c g) (c b))])
    (values (d r) (d g) (d b))))

(define (lightness-invert* R G B)
  (let-values ([(Hp Sl L) (rgb->hsl* R G B)])
    (hsl*->rgb Hp Sl (- 1.0 L))))

(define (rgb->hsl* R G B)
  (define M (max R G B))
  (define m (min R G B))
  (define C (- M m))
  (define Hp
    (cond [(zero? C)
           ;; Undefined, but use 0
           0.0]
          [(= M R)
           (realmod* (/ (- G B) C) 6)]
          [(= M G)
           (+ (/ (- B R) C) 2)]
          [(= M B)
           (+ (/ (- R G) C) 4)]))
  (define L (* 0.5 (+ M m)))
  (define Sl
    (cond [(zero? C) 0.0]
          [(>= L 0.5) (/ C (* 2 L))]
          [else (/ C (- 2 (* 2 L)))]))
  
  (values Hp Sl L))

(define (hsl*->rgb Hp Sl L)
  (define C
    (cond [(>= L 0.5) (* 2 L Sl)]
          [else (* (- 2 (* 2 L)) Sl)]))
  (define X (* C (- 1 (abs (- (realmod Hp 2) 1)))))
  (define-values (R1 G1 B1)
    (cond [(< Hp 1) (values C X 0)]
          [(< Hp 2) (values X C 0)]
          [(< Hp 3) (values 0 C X)]
          [(< Hp 4) (values 0 X C)]
          [(< Hp 5) (values X 0 C)]
          [(< Hp 6) (values C 0 X)]))
  (define m (- L (* 0.5 C)))
  (values (+ R1 m) (+ G1 m) (+ B1 m)))

;; realmod : real integer -> real
;; Adjusts a real number to [0, base]
(define (realmod x base)
  (define xint (ceiling x))
  (define m (modulo xint base))
  (realmod* (- m (- xint x)) base))

;; realmod* : real real -> real
;; Adjusts a number in [-base, base] to [0,base]
;; Not a real mod, but faintly reminiscent.
(define (realmod* x base)
  (if (negative? x)
      (+ x base)
      x))

;; Styles

(define select-d
  (make-object style-delta% 'change-weight 'bold))

(define underline-d
  (make-object style-delta% 'change-underline #t))

(define (highlight-style-delta raw-color #:translate-color? [translate-color? #t])
  (let ([sd (new style-delta%)]
        [color (if translate-color? (translate-color raw-color) raw-color)])
    (send sd set-delta-background color)
    sd))

(define (mk-2-constant-style bow-color [wob-color (translate-color bow-color)])
  (let ([wob-version (highlight-style-delta wob-color #:translate-color? #f)]
        [bow-version (highlight-style-delta bow-color #:translate-color? #f)])
    (λ ()
      (if (pref:invert-colors?)
          wob-version
          bow-version))))

(define get-secondary-highlight-d
  (mk-2-constant-style "yellow" "darkgoldenrod"))

#|
(define undo-select-d
  (make-object style-delta% 'change-weight 'normal))
(define get-undo-highlight-d
  (mk-2-constant-style "white" "black"))
|#

(define (get-undo-select/highlight-d)
  (let ([sd (make-object style-delta% 'change-weight 'normal)]
        [bg (if (pref:invert-colors?) "black" "white")])
    (send sd set-delta-background bg)
    sd))
