#lang typed-scheme
(provide results run-all-tests)

#;(require "../list.scm"
         "../etc.rkt")
(require/typed "foldo.rkt"
  (apply-to-scheme-files
                ((Path -> (Listof (Listof (U #f (Listof (U Real #f))))))
                 Path
                 -> (Listof (U #f (Listof  (Listof ( U #f (Listof (U Real #f))))))))))

(define-type-alias top Any)
(define-type-alias str String)

(require/typed mzlib/file
  [filename-extension (Path -> (U #f Bytes))]
  [normalize-path (Path Path -> Path)]
  [explode-path (Path -> (Listof Path))])
(require/typed "patch.rkt" [srfi48::format (Port String String top * -> top)])
;; FIXME - prefix
#;(require/typed srfi48:format ( Port String String top * -> top) (prefix-in srfi48: (lib "48.rkt" "srfi")))
(require (lib "match.rkt")
         ;(lib "file.rkt")
         ;(lib "list.rkt")
         ;(lib "etc.rkt")
         (prefix-in srfi13: (lib "13.rkt" "srfi"))
         ;(prefix srfi48: (lib "48.rkt" "srfi"))
         )

(define-type-alias Sexpr Any)
(define-type-alias number Real)
(define-type-alias boolean Boolean)
(define-type-alias NumF (U number #f))
(define-type-alias NumFs (Listof NumF))
(define-type-alias NumFss (Listof NumFs))
(define-type-alias NumB (U boolean number))
;;C is either Sexpr or Listof Sepr
;;X = (Listof (U number #f)) - not needed as a parameter
(define-type-alias (Unit X C) ((C ->  X) -> (Path -> (Listof (U #f X)))))

;; ============================================================
;; CONFIG
(define: COLLECTS-PATH : (Parameter Path Path ) (make-parameter (build-path "/proj/scheme/plt/collects" #;"~/svn/plt/collects/")))
(define: PLANET-CODE-PATH : (Parameter Path Path) (make-parameter (build-path "~/Desktop/most-recent-archives/" #;"~/local/src/planet/most-recent-archives/")))

; collects-path : a path to the collects directory to compare
; planet-code-path : a path to the "other" code to compare (i.e. unpacked, most recent versions
;   of all planet packages)

;; ============================================================
;; STATS

;; t-test : (listof number) (listof number) -> number
;; computes t value for the given sequences. t-tests measure
;; the extent to which difference in mean between two sets of
;; _interval-valued_ samples (e.g. distances, times, weights, counts ...)
;; can be explained by chance. Generally speaking, higher absolute
;; values of t correspond to higher confidence that an observed difference
;; in mean cannot be explained by chance.
(define: (t-test [seqA : (Listof Real)] [seqB : (Listof Real)]) : Real
  (manual-t-test
   (avg seqA) (avg seqB)
   (variance seqA) (variance seqB)
   (length seqA) (length seqB)))

(define: (manual-t-test [avga : number] [avgb : number] [vara : number]
                        [varb : number] [cta : number] [ctb : number]) : number
  (/ (- avga avgb)
     (assert (sqrt (+ (/ vara cta) (/ varb ctb))) real?)))

;; chi-square : (listof [0,1]) (listof [0,1]) -> number
;; chi-square is a simple measure of the extent to which the
;; difference in the frequency of 0's and 1's in the first
;; sequence and their frequency in the second sequence can
;; be explained by chance. higher numbers means higher confidence
;; that they cannot.
(define: (chi-square [seqA : (Listof number)] [seqB : (Listof number)]) :  number
  (with-handlers ([exn:fail? (lambda: ([e : str]) +nan.0)])
    (let* ([ct-a (length seqA)]
           [ct-b (length seqB)]
           [total-subjects (+ ct-a ct-b)]
           [a-hits (apply + seqA)]
           [b-hits (apply + seqB)] ;; these assume that the data is coded as 1 = present, 0 = not present
           [a-misses (- ct-a a-hits)]
           [b-misses (- ct-b b-hits)]
           [table
            `((,a-hits ,b-hits)
              (,a-misses ,b-misses))]
           [expected (lambda: ([i : Integer] [j : Integer])
                              (/ (* (row-total i table) (col-total j table)) total-subjects))])
      (exact->inexact
       (table-sum
        (lambda: ([i : Integer] [j : Integer])
                 (/ (sqr (- (expected i j) (table-ref i j table))) (expected i j)))
        table)))))

;; ============================================================
;; UNITS OF MEASUREMENT IMPLEMENTATIONS

;; per-module : path ((listof expr) -> (number | #f)) -> (path -> (listof (number | #f)))  === Unit P
(pdefine: (X) (per-module [f : ((Listof Sexpr) ->  X )]) : (Path -> (cons (U #f X) '()))
          (lambda: ([path : Path])
                   (with-handlers ([exn:fail:read? (lambda: ([e : Void]) (list #f))])  ;; with handler
                     (let ([initial-sexp (with-input-from-file path read)])
                       (match initial-sexp
                         [`(module ,_ ,_  . , (? list? bodies)) ;; FIXME - use ... instead of .
                          (list (f bodies))]
                         [_ (list #f)])))))


;; per-module-top-level-expression : path (expr -> (number | #f)) -> (path -> (listof (number | #f)))
(define: (per-module-top-level-expression [f : (Sexpr -> (Listof NumF))] ) : ( Path -> (Listof (U #f (Listof NumF))))
  (let ([calc (per-module (lambda: ([exprs : (Listof Sexpr)]) (map f exprs)))])
    (lambda: ([p : Path]) (let* ([r  (calc p)]
                                 [carr (car r)]) ;;carr added
                            (if carr  carr
                                (list carr)))))) ;; list carr instead of r

;; ============================================================
;; BASIC CALCULATIONS
;; (for use with metric definitions below)

;; ----------------------------------------
;; depth

(define: (sexp-depth [sexp : Any]) : number
  (cond
    [(list? sexp) ;; (pair? sexp)
     (+ (max-sexp-depth sexp) 1)]
    [else 0]))

(define: (max-sexp-depth [losx : (Listof Any)]) : number
  (improper-foldr (lambda: ([t : Any] [r : number]) (max (sexp-depth t) r)) 0 losx))

(define: (avg-sexp-depth [sexps : (Listof Any)]) : number
  (cond
    [(null? sexps) 0]
    [else (avg (map sexp-depth sexps))]))

;; ----------------------------------------
;; setbang counts

(define-type-alias (IList e) (mu x (Un e '() (cons e x))))

;; count-setbangs/ilist : ((ilistof expr) -> number)
(define: (count-setbangs/ilist [exprs : (Listof Any)]) : number
  (apply + (imap count-setbangs/expr exprs)))

;; FIXME - changes having to do with match ...
(define: (count-setbangs/expr [expr : Any]) : number
  (match expr
    [`(,(? setbang?) . ,rest ) ;(,(? setbang?) ,rest ...)
     (if (list? rest)
         (+ 1 (count-setbangs/ilist rest))
         0)] ;; mostly occurs in syntax patterns
    [('quote _) 0]
    [('quasiquote _) 0] ; undercount potentially, but how many `,(set! ...)'s can there be?
    [`(,e1 . ,e2)
     (if (list? expr)
         (count-setbangs/ilist expr)
         (error " l" expr ))]   ;;FIXME - do something intelligent here
    [_ 0]))

;; setbang? : sexp -> boolean
(define: (setbang? [v : Any]) : Any
  (and (symbol? v)
       (regexp-match #rx"^set(-.*)?!" (symbol->string v))))

;; count-fns
(define: (count-fns-with-setbangs [exprs : (Listof Sexpr)]) : number
  (apply + (map (lambda: ([e : Sexpr]) (if (= (count-setbangs/expr e) 0) 0 1)) exprs)))
(define: (module-has-setbangs? [exprs : (Listof Sexpr)]) : Boolean
  (ormap expr-uses-setbangs? exprs))
(define: (expr-uses-setbangs? [expr : Sexpr]) : Boolean
  (not (= (count-setbangs/expr expr) 0)))

(define: (setbangs-per-1000-atoms [exprs : (Listof Any)]) : NumF
  (if (null? exprs)
      #f
      (let ([set!s (count-setbangs/ilist exprs)]
            [atoms (atoms exprs)])
        (* (/ set!s atoms) 1000.0))))

;; ----------------------------------------
;; contracts


(define: (uses-contracts [exprs : (Listof Sexpr)]) : Boolean
  (ormap (lambda: ([e : Sexpr])
                  (match e
                    [`(provide/contract . ,_) #t]
                    [_ #f]))
         exprs))

(define: (contracted-provides [exprs : (Listof Sexpr)]):  number
  (foldl
   (lambda: ([t : Sexpr] [r : number])
            (match t
              ;; FIXME match ...
              [`(provide/contract . ,p ) ;(provide/contract ,p ...)
               (if (list? p)
                   (+ (length p) r)
                   r)] ;; extra case added
              [_ r]))
   0
   exprs))

;; FIXME - same problem with match ...
(define: (uncontracted-provides [exprs : (Listof Sexpr)]) : number
  (foldl
   (lambda: ([t : Sexpr] [r : number])
            (match t
              [`(provide . ,p ) ;(provide ,p ...)
               (if (list? p)
                   (+ (length p) r)
                   r)]
              [_ r]))
   0
   exprs))

;; ----------------------------------------
;; macros

(define: (number-of-macro-definitions [expr : Sexpr]) : number
  (match expr
    [`(define-syntax ,_ ...) 1]
    [`(define-syntaxes (,s . ,r ). ,_ ) ;`(define-syntaxes (,s ...) ,_ ...)
     (if (and (list?  expr)(list? r))
         (length (cons s r));;s -> cadr expr
         (error "corrupted file"))]
    [`(define-syntax-set (,s . ,r) . ,_ ) ;(define-syntax-set (,s ...) ,_ ...)
     (if (and (list? expr) (list? r))
         (length (cons s r))
         (error "corrupted file"))]
    [_ 0]))

(define: (num-of-define-syntax [exprs : (Listof Sexpr)]) : number
  (foldl (lambda: ([t : Sexpr] [r : number]) (+ (number-of-macro-definitions t) r)) 0 exprs))

;; ----------------------------------------
;; expression size

(define: (atoms [sexp : Any]) : number
  (cond
    [(null? sexp) 0]
    [(not (pair? sexp)) 1]
    [else (+ (atoms (car sexp)) (atoms (cdr sexp)))]))

(define: (max-atoms [exprs : (Listof Sexpr)]) : NumF
  (let ([atom-counts (map atoms exprs)])
    (if (null? atom-counts)
        #f
        (apply max 0 atom-counts)))) ;; FIXME: expected at least 2 argument--->   0 added !!!

(define: (avg-atoms [exprs : (Listof Sexpr)]) : NumF
  (let ([atom-counts (map atoms exprs)])
    (if (null? atom-counts)
        #f
        (avg (map atoms exprs)))))

(define: (total-atoms [exprs : (Listof Sexpr)]) : number
  (apply + (map atoms exprs)))


;; ============================================================
;; METRIC DEFINITIONS
;; 'a 'b metric : (string * (listof sexp -> 'a option) * ((listof 'a) (listof 'a) -> 'b)
(define-typed-struct (b c d) metric ([analysis-unit : (Unit (Listof NumF) c)]
                                     [computation : (c -> d)]
                                     [>display : ((Listof d) (Listof d) -> b)]))
(define-type-alias Metric metric)
(define-type-alias Table (Listof (Listof Real)))
(define-type-alias Atom-display (cons Symbol (Listof Real)))

(define: (standard-display [name : Symbol]
                           [summarize : ((Listof number) -> number)]
                           [significance-test : ((Listof number)(Listof number) -> number)])
  : ((Listof NumF) (Listof NumF) -> Atom-display)
  ;; FIXME - use lambda instead of (define ((
  (lambda: ([seqA : (Listof NumF)] [seqB : (Listof NumF)])
           (let ([clean-seqA (nonfalses seqA)]
                 [clean-seqB (nonfalses seqB)])
             (list name (summarize clean-seqA) (summarize clean-seqB) (significance-test clean-seqA clean-seqB)))))

(pdefine: (c) (interval [u : (Unit (Listof NumF) c)]
                        [name : Symbol]
                        [compute : (c -> NumF)])
          : (Metric Atom-display c NumF)
          (make-metric u compute (standard-display name avg t-test)))

(pdefine: (c) (count [u : (Unit (Listof NumF) c)]
                     [name : Symbol]
                     [compute : (c -> Boolean)])
          : (Metric  Atom-display c NumF)
          (make-metric u (lambda: ([es : c]) #{(if (compute es) 1 0) :: NumF}) (standard-display name avg chi-square)))

(pdefine: (c) (combine-metrics [ms : (Listof (Metric Atom-display c NumF))])
          : (Metric (Listof Atom-display) c (Listof NumF))
          (let ([u (metric-analysis-unit (car ms))])
            ;; This test now redundant b/c of typechecking
            (unless (andmap (lambda: ([m : (Metric Atom-display c NumF) ]) (eq? u (metric-analysis-unit m))) ms)
              (error 'combine-metrics "all combined metrics must operate on the same unit of analysis"))

            (make-metric
             u
             (lambda: ([exprs : c] ) (map (lambda: ([m : (Metric Atom-display c NumF)]) ((metric-computation m) exprs)) ms))
             (lambda: ([seqA : (Listof (Listof NumF))] [seqB : (Listof (Listof NumF))])
                      (map (lambda: ([m : (Metric Atom-display c NumF)]
                                     [sA : (Listof NumF)]
                                     [sB : (Listof NumF)])
                                    ((metric->display m) sA sB)) ms (pivot seqA) (pivot seqB))))))

;; FIXME - should go in helper file
;; FIXME - (filter (lambda (x) x) l)
(pdefine: (X) (nonfalses [l : (Listof (U #f X))]) : (Listof X)
          (let: loop : (Listof X) ([lst :(Listof (U #f X)) l])
                (if (null? lst)
                    '()
                    (let ([x (car lst)])
                      (if x
                          (cons x (loop (cdr lst)))
                          (loop (cdr lst)))))))

(define: (avg [l : (Listof number)]) : number
  (/ (exact->inexact (apply + l)) (length l)))
(define: (avg* [l : (Listof number)]) : number
  (avg (nonfalses l)))

(require (for-syntax scheme/base))

(define-syntax (define-metrics stx)
  (syntax-case stx ()
    [(define-metrics all-metrics-id unit-of-analysis type (name kind fn) ...) ;;TYPE ADDED !!!!
     (with-syntax ([(kind-app ...) (for/list ([k (syntax->list #'(kind ...))]
                                              [n (syntax->list #'(name ...))]
                                              [f (syntax->list #'(fn ...))])
                                     (quasisyntax/loc k (#,k u '#,n #,f)))])
       (syntax/loc
           stx
         (begin
           (define: u : ((type -> (Listof NumF)) -> (Path -> (Listof (U #f(Listof NumF))))) unit-of-analysis )
           (define: name : (Metric Atom-display type NumF) kind-app) ...
           (define: all-metrics-id : (Metric (Listof Atom-display) type (Listof NumF)) (combine-metrics (list name ...))))))]))

(define-metrics module-metrics #{per-module @ (Listof NumF)} (Listof Sexpr)
  (maximum-sexp-depth        interval  max-sexp-depth)
  (average-sexp-depth        interval  avg-sexp-depth)
  (number-of-setbangs/mod    interval  count-setbangs/ilist)
  (number-of-exprs           #{interval @ (Listof Any)}  #{length @ Any})
  (uses-setbang?/mod         count     module-has-setbangs?)
  (uses-contracts?           count     uses-contracts)
  (number-of-contracts       interval  contracted-provides)
  (num-uncontracted-provides interval  uncontracted-provides)
  (number-of-macro-defs      interval  num-of-define-syntax)
  (maximum-num-atoms         interval  max-atoms)
  (average-num-atoms         interval  avg-atoms)
  (total-num-atoms/mod       interval  total-atoms)
  (set!s-per-1000-atoms      interval  setbangs-per-1000-atoms))

(define-metrics tl-expr-metrics per-module-top-level-expression  Sexpr
  (uses-setbang?/fn          count     expr-uses-setbangs?)
  (number-of-setbangs/fn     interval  count-setbangs/expr)
  (total-num-atoms/fn        interval  atoms))

(define: all-metrics : (Listof (U (Metric (Listof Atom-display) Sexpr (Listof NumF))
                                  (Metric (Listof Atom-display) (Listof Sexpr) (Listof NumF))))
  (list module-metrics tl-expr-metrics))

;; ============================================================
;; EXPERIMENT RUNNING

;; FIXME - everything in untyped file (foldo.rkt) b/c fold-files has terrible api
#;(define-syntax (define-excluder stx)

    (define (path->clause c)
      (syntax-case c ()
        [(item ...)
         #`[`(#,@(reverse (syntax-e #'(item ...))) ,_ (... ...)) #t]]
        [item
         #`[`(item) #t]]))

    (syntax-case stx ()
      [(_ name path ...)
       (with-syntax ([(match-clause ...) (map path->clause (syntax-e #'(path ...)))])
         #`(define: (name [p : Path]) : top
             (let* ([dirnames (map path->string (explode-path p))])
               (match (reverse dirnames) ; goofy backwards matching because ... matches greedily
                 match-clause ...
                 [_ #f]))))]))

#;(define-excluder default-excluder
    "compiled" ".svn" #;("collects" "drscheme") #;("collects" "framework"))

#;(define: exclude-directory? : (Parameter (Path -> Any)) (make-parameter default-excluder))

;; ----------------------------------------
;; apply-to-scheme-files: (path[file] -> X) path[directory] -> (listof X)
;; applies the given function to each .rkt or .ss or .scm file in the given
;; directory hierarchy; returns all results in a list
#;(define:  (apply-to-scheme-files [f : (Path -> (Listof(Listof(Listof NumF))))]
                                   [root : Path])
    : (Listof (Listof(Listof(Listof NumF)))) ;;FOLD-FILES

    (fold-files
     (lambda: ([path : Path] [kind : Symbol]
                             [acc : (Listof (Listof(Listof(Listof NumF))))])
              (case kind
                [(file)
                 (let ([extension (filename-extension path)])
                   (cond
                     [(not extension) #;acc (values acc #t)]
                     [(regexp-match #rx"(ss|scm)$" extension)
                      (let ([resl (f path)])
                        (if resl
                            #;(cons resl acc) (values (cons resl acc) #t) ;;values added
                            #;acc (values acc #t)))]
                     [else #;acc (values acc #t)]))]
                [(dir)
                 (let* ([p (normalize-path path root)])
                   (if ((exclude-directory?) p)
                       #; acc (values acc #f)
                       #;acc (values acc #t)))]         ;; values added
                [(link) #;acc (values acc #t)]
                [else (error "never happen")]))  ;;error added
     '()
     root
     #t)) ;;value added

(define-typed-struct (a b c) result ([metric : (Metric b c a)] [seqA : (Listof a)] [seqB : (Listof a)]))
(define-type-alias Result result)

;; get-sequences : (listof 'a metric) path -> (listof (listof 'a))

(pdefine: (b c) (get-sequences [metrics : (Listof (U (Metric  b c (Listof NumF))))]
                                 [path : Path])
          :  (Listof (Listof (Listof NumF)))
          (let* ([metric-fns ; : (Listof (Path -> (Listof (U #f(Listof NumF)))))
                  (map (lambda: ([m : (Metric  b c (Listof NumF))])
                                ((metric-analysis-unit m)
                                 (metric-computation m))) metrics)]
                 [#{result-seqs  : (Listof (U #f (Listof (Listof ( U #f (Listof NumF))))))}
                  (apply-to-scheme-files
                   (lambda: ([file : Path])
                            (map (lambda: ([fn : (Path -> (Listof (U #f (Listof NumF))))]) (fn file)) metric-fns)) path)])
            (map (lambda: ([l :  (Listof(Listof (Option (Listof NumF))))])
                          ;; FIXME - problem with inference and ordering
                          (nonfalses (apply append l)))
                 (pivot (nonfalses result-seqs)))))

;; compare* : (listof metric) -> (listof result)
(: compare* (All (b c)
                 ((Listof (Metric b c (Listof NumF)))
                  ->
                  (Listof (Result (Listof NumF) b c)))))
(define (compare* metrics)
  (let* ([seqAs (get-sequences metrics (COLLECTS-PATH))]
         [seqBs (get-sequences metrics (PLANET-CODE-PATH))])
    (map #{make-result @ (Listof NumF) b c} metrics seqAs seqBs)))

(pdefine: (a b c) (show [result : (Result a b c)]) : b
          ((metric->display (result-metric  result))
           (result-seqA result )
           (result-seqB result )))

(define: (a) (pretty-print-result [result : (Result (Listof NumF) (Listof Atom-display) a)]) : Void
  (for-each
   (lambda: ([l : (Listof Any)])
            (apply srfi48::format ;;sfri48:format
                   (current-output-port)
                   "~26F  | ~8,2F  | ~6,2F  | ~12,2F\n"
                   (format "~a" (car l))
                   (cdr l)))
   (list* '("test name" "collects" "planet" "significance")  ;;list instead of list*
          '("---------" "--------" "------" "------------")
          (show result ))))

;; applies only to the combined metric [or more generally to listof-answer results]
(pdefine: (a b c) (total [experiment-number : Integer] [result : (Result (Listof number) b c)]) : (Listof number)
          (define: (total/s [s : Table]) : number (apply + (list-ref (pivot s) experiment-number)))
          (list (total/s (result-seqA result)) (total/s (result-seqB result))))

;; ============================================================
;; UTILITY

(pdefine: (X Y) (imap [f : (X -> Y)] [il : (Listof X)]) : (Listof Y)
          (cond
            [(null? il) '()]
            [(not (pair? il)) (list (f il))]
            [else (cons (f (car il)) (imap f (cdr il)))]))

(pdefine: (X) (pivot [l : (Listof (Listof X))]): (Listof (Listof X))
          (cond
            [(null? l) '()]
            [else
             (let ([n (length (car l))])
               (build-list n (lambda: ([i : Integer]) (map (lambda: ([j : (Listof X)]) (list-ref j i)) l))))]))

(define: (sqr [x : Real]) : Real (* x x))
(define: (variance [xs : (Listof Real)]): Real
  (let ([avg (/ (apply + xs) (length xs))])
    (/ (apply + (map (lambda: ([x : number]) (sqr (- x avg))) xs))
       (sub1 (length xs)))))

(define: (table-ref [i : Integer] [j : Integer] [table : Table]): number
  (list-ref (list-ref table i) j))
(define: (row-total [i : Integer] [table : Table]) : number
  (apply + (list-ref table i)))
(define: (col-total [j : Integer] [table : Table]) : number
  (apply + (map (lambda: ([x : (Listof number)]) (list-ref x j)) table)))
(define: (table-sum [f : (Integer Integer -> Real)] [table : Table]) : number
  (let ([rows (length table)]
        [cols (length (car table))])
    (let loop ([i 0] [j 0] [#{sum : Real} 0])
          (cond
            [(>= j cols) sum]
            [(>= i rows) (loop 0 (add1 j) sum)]
            [else        (loop (add1 i) j (+ sum (f i j)))]))))

(pdefine: (Y) (improper-foldr [f : (Any Y -> Y)] [b : Y] [l : Any]) : Y
          (cond
            [(null? l) b]
            [(not (pair? l))
             (f l b)]
            [else
             (f (car l) (improper-foldr f b (cdr l)))]))

;; unused (and untypeable)
#;(define: (/* . [args : (Listof number)]) : number  ;;((number)) against (number) and USELESS
    (apply map (lambda: ([ns : number]) (apply / ns)) args))


;; ============================================================
;; MAIN ENTRY POINT

(define: results :
  #;Any
  ;; FIXME bug in typed scheme when this type is used

  (Listof (U (Result (Listof NumF) (Listof Atom-display) (Listof Sexpr))
             (Result (Listof NumF) (Listof Atom-display) Sexpr)))
  '())
; just in case i want to do some more analysis on the results afterwards,
; so i don't have to waste a minute if i forget to bind the return value to something
(define: (run-all-tests) : top
  (let*: ([rs1 :  (Listof (Result (Listof NumF) (Listof Atom-display) (Listof Any)))
               (#{compare* @ (Listof Atom-display) (Listof Any)}
                (list module-metrics))]
          [rs2 :  (Listof (Result (Listof NumF) (Listof Atom-display) Any))
               (#{compare* @ (Listof Atom-display) Any}
                (list tl-expr-metrics))])
         (let
             ([rs (append rs1 rs2)])
           (set! results rs)
           (for-each #{pretty-print-result @ (Listof Any)} rs1)
           (for-each #{pretty-print-result @ Any} rs2)
           rs)))

