#lang racket/unit

  (require "sig.rkt"
           mred/mred-sig
           racket/class
           "../preferences.rkt"
           string-constants
           file/convertible)
  
  (import mred^)
  (export (rename framework:number-snip/int^
                  [-snip-class% snip-class%]))
  (init-depend mred^)
  
  ;; make-repeating-decimal-snip : number boolean -> snip
  (define (make-repeating-decimal-snip number e-prefix?)
    (new number-snip%
      [number number]
      [decimal-prefix (if e-prefix? "#e" "")]))
  
  ;; make-fraction-snip : number boolean -> snip
  (define (make-fraction-snip number e-prefix?)
    (let ([n (new number-snip%
               [number number]
               [decimal-prefix (if e-prefix? "#e" "")])])
      (send n set-fraction-view (preferences:get 'framework:fraction-snip-style))
      n))
  
  (define (set-box/f! b v) (when (box? b) (set-box! b v)))
  (define bw? (< (get-display-depth) 3))
  
  (define -snip-class%
    (class snip-class%
      (define/override (read f)
        (let* ([number (string->number (bytes->string/utf-8 (send f get-bytes)))]
               [decimal-prefix (bytes->string/utf-8 (send f get-bytes))]
               [fraction-bytes (send f get-bytes)]
               [expansions (string->number (bytes->string/utf-8 (send f get-bytes)))]
               [fraction-view
                (cond
                  [(equal? #"#t" fraction-bytes) 'decimal]
                  [(equal? #"#f" fraction-bytes) 
                   (preferences:get 'framework:fraction-snip-style)]
                  [(equal? #"mixed" fraction-bytes) 'mixed]
                  [(equal? #"decimal" fraction-bytes) 'decimal]
                  [(equal? #"improper" fraction-bytes) 'improper])]
               [snip
                (new number-snip%
                  [number number]
                  [decimal-prefix decimal-prefix])])
          (send snip iterate (max 0 (- expansions 1))) ;; one iteration is automatic
          (send snip set-fraction-view fraction-view)
          snip))
      (super-new)))
  
  (define old-number-snipclass (new -snip-class%))
  (send old-number-snipclass set-version 3)
  (send old-number-snipclass set-classname "drscheme:number")
  (send (get-the-snip-class-list) add old-number-snipclass)
  
  (define number-snipclass (new -snip-class%))
  (send number-snipclass set-version 1)
  (send number-snipclass set-classname (format "~s" '(lib "number-snip.ss" "drscheme" "private")))
  (send (get-the-snip-class-list) add number-snipclass)
  
  (define arrow-cursor (make-object cursor% 'arrow))
  
  ;; cut-off : number
  ;; indicates how many digits to fetch for each click
  (define cut-off 25)

  (define-local-member-name draw-fraction)
  
  (define number-snip-convertible<%>
    (interface* ()
                ([prop:convertible
                  (λ (number-snip request default)
                    (case request
                      [(png-bytes) 
                       (define dc (make-object bitmap-dc% (make-bitmap 1 1)))
                       (define wb (box 0))
                       (define hb (box 0))
                       (send number-snip get-extent dc 0 0 wb hb #f #f #f #f)
                       (define bm (make-bitmap (inexact->exact (ceiling (unbox wb))) 
                                               (inexact->exact (ceiling (unbox hb)))))
                       (send dc set-bitmap bm)
                       (send number-snip draw-fraction dc 0 0)
                       (define bp (open-output-bytes))
                       (send bm save-file bp 'png)
                       (get-output-bytes bp)]
                      [(text)
                       (send number-snip get-text 0 1)]
                      [else default]))])))

  (define (get-number s) (send s get-number))

  (define (is-number-snip? x) (is-a? x number-snip%))
  (define number-snip%
    (class* snip% (readable-snip<%> number-snip-convertible<%>)
      ;; number : number
      ;; this is the number to show
      (init-field number)
      (define/public (get-number) number)
      
      ;; decimal-prefix : string
      ;; this prefix is shown on the string when it is viewed in
      ;; the decimal view
      (init-field [decimal-prefix ""])
      
      ;; fraction-view : (union 'decimal 'mixed 'improper)
      ;; this field holds the current view state
      (field [fraction-view 'decimal])
      
      ;; these fields are for the drawing code for decimal printing
      (field 
       ;; clickable-portion : (union #f string)
       [clickable-portion #f]
       ;; unbarred-portion : string
       [unbarred-portion ""]
       ;; barred-portion : (union #f string)
       [barred-portion #f])
      
      (field
       ;; wholes/frac : string
       ;; the whole-number portion of the number as a fraction
       [wholes/frac
        (cond
          [(= (floor number) 0) ""]
          [(= (ceiling number) 0) "-"]
          [(< number 0)
           (number->string (ceiling number))]
          [else
           (number->string (floor number))])])
      
      (field
       ;; wholes/dec : string
       ;; the whole-number portion of decimal expansion
       [wholes/dec
        (cond
          [(= (floor number) 0) "0"]
          [(= (ceiling number) 0) "-0"]
          [(< number 0)
           (number->string (ceiling number))]
          [else
           (number->string (floor number))])])
      
      ;; these fields are for the fractional printing view
      (field
       ;; nums : string
       ;; the numerator of the mixed fraction, as a string
       [nums (number->string (numerator (- (abs number) (floor (abs number)))))]
       
       ;; improper-nums : string
       ;; the numerator of the improper fraction, as a string
       [improper-nums (number->string (numerator (abs number)))]
       
       ;; mixed-prefix : string
       ;; a prefix on the front of the mixed number (indicates if negative)
       [improper-prefix (if (number . < . 0) "-" "")]
       
       ;; dens : string
       ;; the denominator, as a string
       [dens (number->string (denominator (- (abs number) (floor (abs number)))))])
      
      ;; these fields are for the decimal expansion calculation code
      (field
       [init-num (* 10 (numerator (- (abs number) (floor (abs number)))))]
       [den (denominator (- (abs number) (floor (abs number))))])
      
      ;; ht : number -o> (cons digit number)
      ;; this maps from divisors of the denominator to
      ;; digit and new divisor pairs. Use this
      ;; to read off the decimal expansion.
      (field
       [ht (make-hash)]
       [expansions 0])
      
      ;; this field holds the state of the current computation
      ;; of the numbers digits. If it is a number, it corresponds
      ;; to the next starting divisor in the iteration.
      ;; if it is #f, it means that the string of digits is
      ;; fully computed.
      (field [state init-num])
      
      ;; repeat : (union 'unk number #f)
      ;; this field correlates with `state'. If `state' is a number,
      ;; this field is 'unk. Otherwise, this is either a number of #f.
      ;; #f indicates no repeat.
      ;; a number indiates a repeat starting at `number' in `ht'.
      (field [repeat 'unk])
      
      ;; set-fraction-view : (union 'mixed 'improper 'decimal) -> void
      ;; sets the view based on the input
      (define/public (set-fraction-view b)
        (set! fraction-view b)
        (let ([admin (get-admin)])
          (when admin
            (send admin resized this #t))))
      
      ;; get-fraction-view : -> (union 'mixed 'improper 'decimal)
      ;; returns the current fraction view settings
      (define/public (get-fraction-view) fraction-view)
      
      ;; iterate : number -> void
      ;; computes the next sequence of digits (`n' times)
      ;; and update the strings for GUI drawing
      (define/public (iterate n)
        (let loop ([n n])
          (unless (zero? n)
            (expand-number)
            (loop (- n 1))))
        (update-drawing-fields))
      
      (inherit get-admin)
      
      ;; iterate/reflow : -> void
      ;; iterates the fraction and tells the administrator to redraw the numbers
      (define/private (iterate/reflow)
        (iterate 1)
        (let ([admin (get-admin)])
          (when admin
            (send admin resized this #t))))
      
      ;; one-step-division : number -> number number
      ;; given a numerator and denominator,
      ;; returns a digits and a new numerator to consider
      (define/private (one-step-division num)
        (cond
          [(num . < . den) (values 0 (* 10 num))]
          [else
           (let ([qu (quotient num den)])
             (values qu (* 10 (- num (* qu den)))))]))
      
      ;; expand-number : -> void
      ;; iterates until the numbers decimal expansion is completely computed,
      ;; or the number's decimal expansion terminates.
      (define/public (expand-number)
        (when state
          (set! expansions (+ expansions 1))
          (let loop ([num state]
                     [counter cut-off])
            (cond
              [(hash-has-key? ht num)
               (set! state #f)
               (set! repeat num)]
              [(zero? counter) 
               (set! state num)]
              [else
               (let-values ([(dig next-num) (one-step-division num)])
                 (if (zero? next-num)
                     (begin
                       (hash-set! ht num (cons dig #t))
                       (set! state #f)
                       (set! repeat #f))
                     (begin
                       (hash-set! ht num (cons dig next-num))
                       (loop next-num (- counter 1)))))]))))
      
      ;; update-drawing-fields : -> void
      (define/public (update-drawing-fields)
        (cond
          [(number? state) 
           (set! unbarred-portion
                 (string-append
                  decimal-prefix
                  wholes/dec
                  "."
                  (apply string-append (map number->string (extract-non-cycle)))))
           (set! barred-portion #f)
           (set! clickable-portion "...")]
          [(number? repeat)
           (set! unbarred-portion
                 (string-append
                  decimal-prefix
                  wholes/dec
                  "."
                  (apply string-append 
                         (map number->string (extract-non-cycle)))))
           (set! barred-portion (apply string-append (map number->string (extract-cycle))))
           (set! clickable-portion #f)]
          [else
           (set! unbarred-portion
                 (string-append
                  decimal-prefix
                  wholes/dec
                  "."
                  (apply string-append
                         (map number->string (extract-non-cycle)))))
           (set! barred-portion #f)
           (set! clickable-portion #f)]))
      
      ;; extract-cycle : -> (listof digit)
      ;; pre: (number? repeat)
      (define/private (extract-cycle)
        (let ([pr (hash-ref ht repeat)])
          (cons (car pr)
                (extract-helper (cdr pr)))))
      
      ;; extract-non-cycle : -> (listof digit)
      (define/private (extract-non-cycle) (extract-helper init-num))
      
      (define/private (extract-helper start)
        (let loop ([ind start])
          (cond
            [(equal? ind repeat) null]
            [else
             (let* ([iter (hash-ref ht ind)]
                    [dig (car iter)]
                    [next-num (cdr iter)])
               (cons dig
                     (if (hash-has-key? ht next-num)
                         (loop next-num)
                         null)))])))
      
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;;                                                                  ;;
      ;;                       snip infrastructure                        ;;
      ;;                                                                  ;;
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      
      
      (define/public (read-special file line col pos)
        number)
      
      (define/override get-text
        (case-lambda
          [(offset num) (get-text offset num #f)]
          [(offset num flattened?) 
           (case fraction-view
             [(mixed)
              (cond
                [(string=? wholes/frac "")
                 (string-append nums "/" dens)]
                [(string=? wholes/frac "-")
                 (string-append wholes/frac nums "/" dens)]
                [else
                 (string-append wholes/frac " " nums "/" dens)])]
             [(decimal)
              (string-append 
               unbarred-portion
               (or barred-portion "")
               (or clickable-portion ""))]
             [(improper) (string-append 
                          improper-prefix
                          improper-nums
                          "/"
                          dens)])]))
      
      (define/override (write f)
        (send f put (string->bytes/utf-8 (number->string number)))
        (send f put (string->bytes/utf-8 decimal-prefix))
        (send f put (string->bytes/utf-8 (format "~a" fraction-view)))
        (send f put (string->bytes/utf-8 (number->string expansions))))
      
      (define/override (copy)
        (let ([snip (new number-snip%
                      [number number]
                      [decimal-prefix decimal-prefix])])
          (send snip iterate (max 0 (- expansions 1))) ;; one iteration is automatic
          (send snip set-fraction-view fraction-view)
          snip))
      
      (inherit get-style)
      
      (define/override (get-extent dc x y wb hb descent space lspace rspace)
        (case fraction-view
          [(decimal)
           (get-decimal-extent dc x y wb hb descent space lspace rspace)]
          [(mixed)
           (get-mixed-fraction-extent dc x y wb hb descent space lspace rspace)]
          [(improper)
           (get-improper-fraction-extent dc x y wb hb descent space lspace rspace)]))
      
      (define/override (draw dc x y left top right bottom dx dy draw-caret)
        (define clr (send dc get-text-foreground))
        (define pen (send dc get-pen))
        (when (pair? draw-caret)
          (let ([fg-color (get-highlight-text-color)])
            (when fg-color
              (send dc set-pen fg-color 1 'solid)
              (send dc set-text-foreground fg-color)))) 
        (draw-fraction dc x y)
        (send dc set-text-foreground clr)
        (send dc set-pen pen))
      
      (define/public (draw-fraction dc x y)
        (case fraction-view
          [(mixed) (draw-mixed-fraction dc x y)]
          [(improper) (draw-improper-fraction dc x y)]
          [(decimal) (draw-decimals dc x y)]))
      
      (define/private (get-improper-fraction-extent dc x y w h descent space lspace rspace)
        (let* ([style (get-style)]
               [th (send style get-text-height dc)]
               [old-font (send dc get-font)])
          (send dc set-font (send style get-font))
          (let-values ([(nw nh nd na) (send dc get-text-extent improper-nums)]
                       [(dw dh dd da) (send dc get-text-extent dens)]
                       [(ww wh wd wa) (send dc get-text-extent improper-prefix)])
            (define frac-h (+ nh dh 1))
            (set-box/f! h (+ nh dh 1))
            (set-box/f! w (+ ww (max nw dw)))
            (set-box/f! descent (+ wd (/ (- frac-h wh) 2)))
            (set-box/f! space  (+ wa (/ (- frac-h wh) 2)))
            (set-box/f! lspace 0)
            (set-box/f! rspace 0))))
      
      (define/private (draw-improper-fraction dc x y)
        (let-values ([(nw nh nd na) (send dc get-text-extent improper-nums)]
                     [(dw dh dd da) (send dc get-text-extent dens)]
                     [(ww wh wd wa) (send dc get-text-extent improper-prefix)])
          (let ([frac-w (max nw dw)])
            (send dc draw-text improper-nums (+ x ww (- frac-w nw)) y)
            (send dc draw-text dens (+ x ww (- (/ dw 2)) (/ frac-w 2)) (+ y nh 1))
            (send dc draw-text improper-prefix x (+ y (/ nh 2)))
            (send dc draw-line
                  (+ x ww) (+ y dh)
                  (+ x ww (max nw dw) -1) (+ y dh)))))
      
      (define/private (get-mixed-fraction-extent dc x y w h descent space lspace rspace)
        (let* ([style (get-style)]
               [th (send style get-text-height dc)]
               [old-font (send dc get-font)])
          (send dc set-font (send style get-font))
          (let-values ([(nw nh nd na) (send dc get-text-extent nums)]
                       [(dw dh dd da) (send dc get-text-extent dens)]
                       [(ww wh wd wa) (send dc get-text-extent wholes/frac)])
            (define frac-h (+ nh dh 1))
            (set-box/f! h frac-h)
            (set-box/f! w (+ ww (max nw dw)))
            (set-box/f! descent (+ wd (/ (- frac-h wh) 2)))
            (set-box/f! space  (+ wa (/ (- frac-h wh) 2)))
            (set-box/f! lspace 0)
            (set-box/f! rspace 0))))
      
      (define/private (draw-mixed-fraction dc x y)
        (let-values ([(nw nh na nd) (send dc get-text-extent nums)]
                     [(dw dh da dd) (send dc get-text-extent dens)]
                     [(ww wh wa wd) (send dc get-text-extent wholes/frac)])
          (let ([frac-w (max nw dw)])
            (define frac-h (+ nh dh 1))
            (send dc draw-text nums (+ x ww (- frac-w nw)) y)
            (send dc draw-text dens (+ x ww (- (/ dw 2)) (/ frac-w 2)) (+ y nh 1))
            (send dc draw-text wholes/frac x (+ y (/ (- frac-h wh) 2)))
            (send dc draw-line
                  (+ x ww) (+ y dh)
                  (+ x ww (max nw dw) -1) (+ y dh)))))
      
      (define/private (get-decimal-extent dc x y wb hb descent space lspace rspace)
        (let ([font (send (get-style) get-font)])
          (let-values ([(w1 h1 d1 a1) (get-text-extent/f dc unbarred-portion font)]
                       [(w2 h2 d2 a2) (get-text-extent/f dc barred-portion font)]
                       [(w3 h3 d3 a3) (get-text-extent/f dc clickable-portion font)])
            (set-box/f! wb (+ w1 w2 w3))
            (set-box/f! hb (if barred-portion 
                               (+ h1 2)
                               h1))
            (set-box/f! descent d1)
            (set-box/f! space (if barred-portion
                                  (+ a1 2)
                                  a1))
            (set-box/f! lspace 0)
            (set-box/f! rspace 0))))
      
      (define/private (draw-decimals dc x y)
        (define (draw-digits digits x)
          (if digits
              (let-values ([(w h a d) (send dc get-text-extent digits)])
                (send dc draw-text digits x (if barred-portion (+ y 2) y))
                (+ x w))
              x))
        (let* ([unbarred-end (draw-digits unbarred-portion x)]
               [barred-end (draw-digits barred-portion unbarred-end)]
               [clickable-end (draw-digits clickable-portion barred-end)])
          (when barred-portion
            (send dc draw-line unbarred-end y (- barred-end 1) y))))
      
      (define/private (get-text-extent/f dc str font)
        (if str
            (let-values ([(w h d a) (send dc get-text-extent str font)])
              (values w h d a))
            (values 0 0 0 0)))
      
      (define/override (adjust-cursor dc x y editorx editory evt)
        (let ([sx (- (send evt get-x) x)]
              [sy (- (send evt get-y) y)])
          (if (in-clickable-portion? dc sx sy)
              arrow-cursor
              #f)))
      
      (define/override (on-event dc x y editor-x editor-y evt)
        (let ([sx (- (send evt get-x) x)]
              [sy (- (send evt get-y) y)])
          (cond
            [(send evt button-down? 'right)
             (let ([admin (get-admin)])
               (when admin
                 (let ([popup-menu (make-right-clickable-menu)])
                   (send admin popup-menu popup-menu this (+ sx 1) (+ sy 1)))))]
            [(send evt button-up? 'left)
             (when (in-clickable-portion? dc sx sy)
               (iterate/reflow))]
            [else (void)])))
      
      (define/private (make-right-clickable-menu)
        (let* ([menu (make-object popup-menu%)]
               [decimal-item
                (make-object checkable-menu-item% 
                  (string-constant show-decimal-expansion)
                  menu
                  (λ (x y) 
                    (set-fraction-view 'decimal)
                    (preferences:set 'framework:fraction-snip-style 'decimal)))]
               [mixed-fraction-item
                (make-object checkable-menu-item% 
                  (string-constant show-mixed-fraction-view)
                  menu
                  (λ (x y) 
                    (set-fraction-view 'mixed)
                    (preferences:set 'framework:fraction-snip-style 'mixed)))]
               [improper-fraction-item
                (make-object checkable-menu-item% 
                  (string-constant show-improper-fraction-view)
                  menu
                  (λ (x y) 
                    (set-fraction-view 'improper)
                    (preferences:set 'framework:fraction-snip-style 'improper)))])
          
          (case fraction-view
            [(decimal) (send decimal-item check #t)]
            [(mixed) (send mixed-fraction-item check #t)]
            [(improper) (send improper-fraction-item check #t)])
          
          (when (and (eq? fraction-view 'decimal)
                     clickable-portion)
            (make-object menu-item% 
              (string-constant show-more-decimal-places)
              menu
              (λ (x y)
                (iterate/reflow))))
          menu))
      
      (define/private (in-clickable-portion? dc sx sy)
        (and clickable-portion
             (let ([font (send (get-style) get-font)])
               (let-values ([(w1 h1 d1 a1) (get-text-extent/f dc unbarred-portion font)]
                            [(w2 h2 d2 a2) (get-text-extent/f dc barred-portion font)]
                            [(w3 h3 d3 a3) (get-text-extent/f dc clickable-portion font)])
                 (and (<= (+ w1 w2) sx (+ w1 w2 w3))
                      (<= 0 sy h3))))))
      
      (super-new)
      (inherit set-snipclass set-flags get-flags)
      (set-flags (cons 'handles-events (get-flags)))
      (set-snipclass number-snipclass)
      (iterate 1))) ;; calc first digits
