#lang racket/base

(require racket/math racket/flonum racket/contract racket/match
         "math.rkt"
         "contract.rkt" "contract-doc.rkt")

(provide (struct-out invertible-function)
         make-axis-transform
         id-transform
         log-transform
         cbrt-transform
         hand-drawn-transform)

(define-struct/contract invertible-function ([f (real? . -> . real?)] [finv (real? . -> . real?)])
  #:transparent)

;; Turns any total, surjective, monotone flonum op and its inverse into an axis transform
(define ((make-axis-transform flop flinv) x-min x-max)
  (let ([x-min  (exact->inexact x-min)]
        [x-max  (exact->inexact x-max)])
    (define fx-min (flop x-min))
    (define fx-scale (fl/ (fl- x-max x-min)
                          (fl- (flop x-max) fx-min)))
    (define (f x)
      (fl+ x-min (fl* (fl- (flop (exact->inexact x)) fx-min)
                      fx-scale)))
    (define (finv y)
      (flinv (fl+ fx-min (fl/ (fl- (exact->inexact y) x-min)
                              fx-scale))))
    (invertible-function f finv)))

;; ===================================================================================================
;; Specific axis transforms

(define ((flnewton-invert f f-diff f-inv-guess n) y)
  (let ([y  (exact->inexact y)])
    (let loop ([x  (f-inv-guess y)] [n n])
      (let/ec return
        (when (zero? n) (return x))
        
        (define dx (fl/ (fl- y (f x)) (f-diff x)))
        (when (zero? dx) (return x))
        
        (loop (fl- x dx) (sub1 n))))))

(define (sine-diag d)
  (let ([d  (exact->inexact d)])
    (λ (x) (let ([x  (exact->inexact x)])
             (fl+ x (fl* (fl/ 1.0 (fl* 4.0 d)) (flsin (fl* d x))))))))

(define (sine-diag-diff d)
  (let ([d  (exact->inexact d)])
    (λ (x) (let ([x  (exact->inexact x)])
             (fl- (fl/ (flcos (fl* d x)) 4.0) 1.0)))))

(define (sine-diag-inv d)
  (flnewton-invert (sine-diag d) (sine-diag-diff d) values 10))

(define cbrt
  (let ([e  (exact->inexact 1/3)])
    (λ (x) (let ([x  (exact->inexact x)])
             (fl* (sgn x) (expt (flabs x) e))))))

(define (cube x)
  (let ([x  (exact->inexact x)])
    (fl* x (fl* x x))))


(defproc (id-transform [x-min real?] [x-max real?]) invertible-function?
  (invertible-function values values))

(defproc (log-transform [x-min real?] [x-max real?]) invertible-function?
  (when ((exact->inexact x-min) . <= . 0)
    (raise-type-error 'log-transform "positive real" 0 x-min x-max))
  ((make-axis-transform fllog flexp) x-min x-max))

(define cbrt-trans (make-axis-transform cbrt cube))

(defproc (cbrt-transform [x-min real?] [x-max real?]) invertible-function?
  (cbrt-trans x-min x-max))

(defproc (hand-drawn-transform [freq (>/c 0)]) (real? real? . -> . invertible-function?)
  (λ (mn mx)
    (define d (/ freq (- mx mn)))
    ((make-axis-transform (sine-diag d) (sine-diag-inv d)) mn mx)))
