#lang typed/racket/base

(require racket/sequence
         racket/list
         "../../flonum.rkt"
         "../../base.rkt")

(provide (all-defined-out))

;; ===================================================================================================

(: lte?->lt? (All (A) ((A A -> Any) -> (A A -> Boolean))))
(define ((lte?->lt? lte?) x1 x2)
  (and (lte? x1 x2) (not (lte? x2 x1))))

(: find-near-pow2 (Real -> Nonnegative-Exact-Rational))
(define (find-near-pow2 x)
  (expt 2 (max -1073 (min 1023 (exact-round (/ (log (abs x)) (fllog 2.0)))))))

(: weights->list (Symbol (Sequenceof Real) -> (Listof Nonnegative-Real)))
(define (weights->list name w-seq)
  (for/list: : (Listof Nonnegative-Real) ([w w-seq])
    (cond [(w . >= . 0)  w]
          [else  (raise-argument-error name "(Sequenceof Nonnegative-Real)" w-seq)])))

(: weights->normalized-weights (Symbol (Sequenceof Real) -> (Listof Nonnegative-Flonum)))
(define (weights->normalized-weights name ws)
  (let ([ws  (weights->list name ws)])
    (when (empty? ws) (raise-argument-error name "nonempty (Sequenceof Real)" ws))
    (define max-w (find-near-pow2 (apply max ws)))
    (let ([ws  (map (λ: ([w : Real]) (/ w max-w)) ws)])
      (define total-w (sum ws))
      (map (λ: ([w : Real]) (assert (fl (/ w total-w)) nonnegative?)) ws))))

;; ===================================================================================================

(: check-lengths! (All (A B) (Symbol String A B Index Index -> Void)))
(define (check-lengths! name what xs ys m n)
  (unless (= m n) (error name "~a must be the same length; given ~e (length ~a) and ~e (length ~a)"
                         what xs m ys n)))

(: sequences->weighted-samples
   (All (A) (Symbol (Sequenceof A) (Sequenceof Real)
                    -> (Values (Listof A) (Listof Nonnegative-Real)))))
(define (sequences->weighted-samples name x-seq w-seq)
  (define xs (sequence->list x-seq))
  (define ws
    (for/list: : (Listof Nonnegative-Real) ([w w-seq])
      (cond [(w . >= . 0)  w]
            [else  (raise-argument-error name "(Sequenceof Nonnegative-Real)" 1 x-seq w-seq)])))
  (check-lengths! name "values and weights" xs ws (length xs) (length ws))
  (values xs ws))

(define nonnegative? (λ: ([x : Real]) (not (negative? x))))

(: sequences->normalized-weighted-samples
   (All (A) (Symbol (Sequenceof A) (Sequenceof Real)
                    -> (Values (Listof A) (Listof Positive-Flonum)))))
(define (sequences->normalized-weighted-samples name xs ws)
  (let-values ([(xs ws)  (sequences->weighted-samples name xs ws)])
    (when (empty? xs) (raise-argument-error name "nonempty (Sequenceof A)" 0 xs ws))
    (define max-w (find-near-pow2 (assert (apply max ws) nonnegative?)))
    (let ([ws  (map (λ: ([w : Nonnegative-Real]) (/ w max-w)) ws)])
      (define total-w (sum ws))
      (let loop ([xs  xs]
                 [ws  ws]
                 [#{new-xs : (Listof A)}  empty]
                 [#{new-ws : (Listof Positive-Flonum)}  empty])
        (cond [(or (empty? xs) (empty? ws))  (values (reverse new-xs) (reverse new-ws))]
              [else
               (define w (fl (/ (first ws) total-w)))
               (cond [(w . > . 0.0)
                      (loop (rest xs) (rest ws) (cons (first xs) new-xs) (cons w new-ws))]
                     [else
                      (loop (rest xs) (rest ws) new-xs new-ws)])])))))

(: sequence->normalized-weighted-samples
   (All (A) (Symbol (Sequenceof A) -> (Values (Listof A) (Listof Positive-Flonum)))))
(define (sequence->normalized-weighted-samples name xs)
  (let ([xs  (sequence->list xs)])
    (when (empty? xs) (raise-argument-error name "nonempty (Sequenceof A)" xs))
    (define n (length xs))
    (define w (assert (fl/ 1.0 (fl n)) positive?))
    (values xs (build-list n (λ (i) w)))))

(: sequence->vector (All (A) ((Sequenceof A) -> (Vectorof A))))
(define (sequence->vector vs)
  (for/vector: ([v vs]) : A v))

(: sequences->weighted-sample-vectors
   (All (A) (Symbol (Sequenceof A) (Sequenceof Real)
                    -> (Values (Vectorof A) (Vectorof Nonnegative-Real)))))
(define (sequences->weighted-sample-vectors name x-seq w-seq)
  (define xs (sequence->vector x-seq))
  (define ws
    (for/vector: ([w w-seq]) : Nonnegative-Real
      (cond [(w . >= . 0)  w]
            [else  (raise-argument-error name "(Sequenceof Nonnegative-Real)" 1 x-seq w-seq)])))
  (check-lengths! name "values and weights" xs ws (vector-length xs) (vector-length ws))
  (values xs ws))

;; ===================================================================================================

;; bias = #f   Return the central moment
;; bias = #t   Assume sum of weights is the count and correct for bias normally
;; bias = n    Assume n actual samples; correct for bias

(: get-bias-adjustment (Nonnegative-Real (U #t Real) Nonnegative-Real -> Positive-Real))
(define (get-bias-adjustment c bias mn)
  (define n (if (real? bias) bias c))
  (if (n . > . mn) n +nan.0))

(: adjust-variance (Nonnegative-Real Nonnegative-Real (U #t #f Real) -> Nonnegative-Real))
(define (adjust-variance m2 n bias)
  (cond [bias
         (let ([n  (get-bias-adjustment n bias 1)])
           (define c (max 0 (/ n (- n 1))))  ; max proves c >= 0
           (* m2 c))]
        [else  m2]))

(: adjust-covariance (Real Nonnegative-Real (U #t #f Real) -> Real))
(define (adjust-covariance m2 n bias)
  (cond [bias
         (let ([n  (get-bias-adjustment n bias 1)])
           (* m2 (/ n (- n 1))))]
        [else  m2]))

(: adjust-skewness (Real Nonnegative-Real (U #t #f Real) -> Real))
(define (adjust-skewness g n bias)
  (cond [bias
         (let ([n  (get-bias-adjustment n bias 2)])
           (fl (* g (/ (sqrt (max 0 (* n (- n 1)))) (- n 2)))))]
        [else  (fl g)]))

(: adjust-kurtosis (Nonnegative-Real Nonnegative-Real (U #t #f Real) -> Nonnegative-Real))
(define (adjust-kurtosis g n bias)
  (cond [bias
         (let ([n  (get-bias-adjustment n bias 3)])
           (define c (max 0 (/ (- n 1) (* (- n 2) (- n 3)))))  ; max proves c >= 0
           (* (+ (* (+ n 1) g) 6) c))]
        [else  g]))
