#lang racket/base
(require "rg.rkt" 
         "jdg-gen.rkt"
         "error.rkt"
         "reduction-semantics.rkt"
         "struct.rkt"
         "term.rkt"
         "matcher.rkt"
         "judgment-form.rkt"
         "search.rkt"
         "term-fn.rkt"
         "pat-unify.rkt"
         racket/contract
         racket/match
         racket/pretty
         (for-syntax racket/base
                     syntax/stx
                     setup/path-to-relative
                     "rewrite-side-conditions.rkt"
                     "term-fn.rkt"
                     "keyword-macros.rkt"))

(define-for-syntax (metafunc name)
  (and (identifier? name)
       (let ([tf (syntax-local-value name (λ () #f))])
         (and (term-fn? tf) (term-fn-get-id tf)))))

(define-for-syntax (metafunc/err name stx)
  (let ([m (metafunc name)])
    (if m m (raise-syntax-error #f "not a metafunction" stx name))))

(define-for-syntax (term-generator lang pattern what)
  (with-syntax ([pattern pattern])
    #`((compile #,lang '#,what) `pattern)))

(define (make-generator raw-generators form-name)
  (λ (size #:attempt-num [attempt-num 1] #:retries [retries default-retries])
    (define (check-arg arg)
      (unless (natural-number/c arg)
        (raise-argument-error form-name "natural number" arg)))
    (check-arg size)
    (check-arg attempt-num)
    (check-arg retries)
    (let-values ([(term _) ((match raw-generators
                              [(list g) g]
                              [_ (pick-from-list raw-generators)])
                            size attempt-num retries)])
      term)))

(define-for-syntax (show-message stx)
  (syntax-case stx ()
    [(what . _)
     (identifier? #'what)
     (with-syntax ([loc (if (and (path? (syntax-source stx))
                                 (syntax-line stx))
                            (format "~a:~a"
                                    (path->relative-string/library (syntax-source stx)) 
                                    (syntax-line stx))
                            #f)])
       #`(λ (msg)
           (fprintf 
            (current-output-port)
            "~a: ~a~a"
            'what (if loc (string-append loc "\n") "") msg)))]))

(define-for-syntax attempts-keyword
  (list '#:attempts #'(default-check-attempts)
        (list #'natural-number/c "#:attempts argument")))
(define-for-syntax source-keyword
  (list '#:source #f))
(define-for-syntax retries-keyword
  (list '#:retries #'default-retries 
        (list #'natural-number/c "#:retries argument")))
(define-for-syntax print?-keyword
  (list '#:print? #t))
(define-for-syntax attempt-size-keyword
  (list '#:attempt-size #'default-attempt-size 
        (list #'attempt-size/c "#:attempt-size argument")))
(define-for-syntax (prepare-keyword lists?)
  (list '#:prepare #f 
        (list (if lists? #'(-> list? list?) #'(-> any/c any/c)) 
              "#:prepare argument")))

(define-syntax (redex-check stx)
  (syntax-case stx ()
    [(form lang pat property . kw-args)
     (with-syntax ([(pattern (name ...) (name/ellipses ...))
                    (rewrite-side-conditions/check-errs 
                     (language-id-nts #'lang 'redex-check)
                     'redex-check #t #'pat)]
                   [show (show-message stx)])
     (let-values ([(attempts-stx source-stx retries-stx print?-stx size-stx fix-stx)
                   (apply values
                          (parse-kw-args (list attempts-keyword
                                               source-keyword
                                               retries-keyword
                                               print?-keyword
                                               attempt-size-keyword
                                               (prepare-keyword #f))
                                         (syntax kw-args)
                                         stx
                                         (syntax-e #'form)))])
         (with-syntax ([property (syntax
                                  (bind-prop
                                   (λ (bindings)
                                     (term-let ([name/ellipses (lookup-binding bindings 'name)] ...)
                                               property))))])
           (quasisyntax/loc stx
             (let ([att #,attempts-stx]
                   [ret #,retries-stx]
                   [print? #,print?-stx]
                   [fix #,fix-stx]
                   [term-match (λ (generated)
                                 (cond [(test-match lang pat generated) => values]
                                       [else (redex-error 'redex-check "~s does not match ~s" generated 'pat)]))])
               (parameterize ([attempt->size #,size-stx])
               #,(if source-stx
                     #`(let-values ([(metafunc/red-rel num-cases) 
                                     #,(cond [(metafunc source-stx)
                                              => (λ (x) #`(values #,x (length (metafunc-proc-cases #,x))))]
                                             [else
                                              #`(let ([r #,(apply-contract #'reduction-relation? source-stx 
                                                                           "#:source argument" (syntax-e #'form))])
                                                  (values r (length (reduction-relation-make-procs r))))])])
                         (check-lhs-pats
                          lang
                          metafunc/red-rel
                          property
                          (max 1 (floor (/ att num-cases)))
                          ret
                          'redex-check
                          (and print? show)
                          fix
                          #:term-match term-match))
                     #`(check-one
                        #,(term-generator #'lang #'pattern 'redex-check)
                        property att ret (and print? show) fix (and fix term-match)))))))))]))

(define (format-attempts a)
  (format "~a attempt~a" a (if (= 1 a) "" "s")))

(define (check-one generator property attempts retries show term-fix term-match) 
  (let ([c (check generator property attempts retries show 
                  #:term-fix term-fix
                  #:term-match term-match)])
    (if (counterexample? c)
        (unless show c) ; check printed it
        (if show
            (show (format "no counterexamples in ~a\n"
                          (format-attempts attempts)))
            #t))))

(define-struct (exn:fail:redex:test exn:fail:redex) (source term))
(define-struct counterexample (term) #:transparent)

(define-struct term-prop (pred))
(define-struct bind-prop (pred))

(define (check generator property attempts retries show
               #:source [source #f]
               #:term-fix [term-fix #f]
               #:term-match [term-match #f]
               #:skip-term? [skip-term? (λ (x) #f)])
  (let loop ([remaining attempts])
    (if (zero? remaining)
        #t
        (let ([attempt (add1 (- attempts remaining))])
          (let-values ([(term bindings) (generator ((attempt->size) attempt) attempt retries)]
                       [(handler) 
                        (λ (action term)
                          (λ (exn)
                            (let ([msg (format "~a ~s raises an exception" action term)])
                              (when show (show (format "~a\n" msg)))
                              (raise 
                               (if show
                                   exn
                                   (make-exn:fail:redex:test
                                    (format "~a:\n~a" msg (exn-message exn))
                                    (current-continuation-marks)
                                    exn
                                    term))))))])
            (let ([term (with-handlers ([exn:fail? (handler "fixing" term)])
                          (if term-fix (term-fix term) term))])
              (cond
                [(skip-term? term) (loop (- remaining 1))]
                [else
                 (if (if term-match
                         (let ([bindings (make-bindings 
                                          (match-bindings
                                           (pick-from-list (term-match term))))])
                           (with-handlers ([exn:fail? (handler "checking" term)])
                             (match property
                               [(term-prop pred) (pred term)]
                               [(bind-prop pred) (pred bindings)])))
                         (with-handlers ([exn:fail? (handler "checking" term)])
                           (match (cons property term-fix)
                             [(cons (term-prop pred) _) (pred term)]
                             [(cons (bind-prop pred) #f) (pred bindings)])))
                     (loop (sub1 remaining))
                     (begin
                       (when show
                         (show
                          (format "counterexample found after ~a~a:\n"
                                  (format-attempts attempt)
                                  (if source (format " with ~a" source) "")))
                         (pretty-write term (current-output-port)))
                       (make-counterexample term)))])))))))

(define (check-lhs-pats lang mf/rr prop attempts retries what show term-fix
                        #:term-match [term-match #f])
  (define lang-gen (compile lang what))
  (define-values (pats srcs skip-term?)
    (cond [(metafunc-proc? mf/rr)
           (values (map (λ (case) ((metafunc-case-lhs+ case) lang)) 
                        (metafunc-proc-cases mf/rr))
                   (metafunction-srcs mf/rr)
                   (compose not (metafunc-proc-in-dom? mf/rr)))]
          [(reduction-relation? mf/rr)
           (values (map (λ (rwp) ((rewrite-proc-lhs rwp) lang)) (reduction-relation-make-procs mf/rr))
                   (reduction-relation-srcs mf/rr)
                   (let ([pat (compile-pattern (reduction-relation-lang mf/rr)
                                               (reduction-relation-domain-pat mf/rr)
                                               #f)])
                     (λ (x) (not (match-pattern? pat x)))))]))
  
  (let loop ([pats pats] [srcs srcs])
    (if (and (null? pats) (null? srcs))
        (if show
            (show
             (format "no counterexamples in ~a (with each clause)\n"
                     (format-attempts attempts)))
            #t)
        (let ([c (with-handlers ([exn:fail:redex:generation-failure?
                                  ; Produce an error message that blames the LHS as a whole.
                                  (λ (_)
                                    (raise-gen-fail what (format "LHS of ~a" (car srcs)) retries))])
                   (check
                    (lang-gen (car pats))
                    prop
                    attempts
                    retries
                    show
                    #:skip-term? skip-term?
                    #:source (car srcs)
                    #:term-match term-match
                    #:term-fix term-fix))])
          (if (counterexample? c)
              (unless show c)
              (loop (cdr pats) (cdr srcs)))))))

(define-syntax (check-metafunction stx)
  (syntax-case stx ()
    [(form name property . kw-args)
     (let-values ([(attempts retries print? size fix)
                   (apply values
                          (parse-kw-args (list attempts-keyword
                                               retries-keyword
                                               print?-keyword
                                               attempt-size-keyword
                                               (prepare-keyword #t))
                                         (syntax kw-args)
                                         stx
                                         (syntax-e #'form)))]
                  [(m) (metafunc/err #'name stx)])
       (quasisyntax/loc stx
         (parameterize ([attempt->size #,size])
           (let ([att #,attempts]
                 [ret #,retries]
                 [fix #,fix])
             (check-lhs-pats 
              (metafunc-proc-lang #,m)
              #,m
              (term-prop #,(apply-contract #'(-> (listof any/c) any) #'property #f (syntax-e #'form)))
              att
              ret
              'check-metafunction
              (and #,print? #,(show-message stx))
              fix)))))]))

(define (reduction-relation-srcs r)
  (map (λ (proc) (or (rewrite-proc-name proc)
                     (format "clause at ~a" (rewrite-proc-lhs-src proc))))
       (reduction-relation-make-procs r)))

(define (metafunction-srcs m)
  (map (λ (x) (format "clause at ~a" (metafunc-case-src-loc x)))
       (metafunc-proc-cases m)))

(define-syntax (check-reduction-relation stx)
  (syntax-case stx ()
    [(form relation property . kw-args)
     (let-values ([(attempts retries print? size fix)
                   (apply values
                          (parse-kw-args (list attempts-keyword
                                               retries-keyword
                                               print?-keyword
                                               attempt-size-keyword
                                               (prepare-keyword #f))
                                         (syntax kw-args)
                                         stx
                                         (syntax-e #'form)))])
       (quasisyntax/loc stx
         (parameterize ([attempt->size #,size])
           (let ([att #,attempts]
                 [ret #,retries]
                 [rel #,(apply-contract #'reduction-relation? #'relation #f (syntax-e #'form))]
                 [fix #,fix])
             (check-lhs-pats
              (reduction-relation-lang rel)
              rel
              (term-prop #,(apply-contract #'(-> any/c any) #'property #f (syntax-e #'form)))
              att
              ret
              'check-reduction-relation
              (and #,print? #,(show-message stx))
              fix)))))]))

(define-syntax (generate-term stx)
  (syntax-case stx ()
    [(form-name args ...)
     #`(#%expression (generate-term/real form-name args ...))]))

(define-syntax (generate-term/real stx)
  (let ([l (cdr (syntax->list stx))])
    (when (list? l)
      (for ([x (in-list l)])
        (define k (syntax-e x))
        (when (keyword? k)
          (unless (member k '(#:satisfying #:source #:attempt-num #:retries))
            (raise-syntax-error 'generate-term "unknown keyword" stx x))))))
  (syntax-case stx ()
    [(_ orig-name language #:satisfying (jf/mf-id . args) . rest)
     (cond
       [(metafunc #'jf/mf-id)
        (let ()
          (define (signal-error whatever)
            (when (stx-pair? whatever)
              (define cr (syntax-e (stx-car whatever)))
              (when (keyword? cr)
                (raise-syntax-error 'generate-term 
                                    "#:satisfying does not yet support additional keyword arguments"
                                    stx 
                                    (stx-car whatever))))
            (raise-syntax-error 'generate-term
                                "expected a metafunction result and a size"
                                stx))
          (let ([body-code 
                 (λ (res size)
                   #`(generate-mf-pat language (jf/mf-id . args) #,res #,size))])
            (syntax-case #'rest (=)
              [(= res) 
               #`(λ (size) 
                   #,(body-code #'res #'size))]
              [(= res size)
               (body-code #'res #'size)]
              [(x . y)
               (or (not (identifier? #'x))
                   (not (free-identifier=? #'= #'x)))
               (raise-syntax-error 'generate-term
                                   "expected to find ="
                                   stx
                                   #'x)]
              [whatever
               (signal-error #'whatever)])))]
       [(judgment-form-id? #'jf/mf-id)
        (syntax-case #'rest ()
          [() 
           #`(λ (size) 
               (generate-jf-pat language (jf/mf-id . args) size))]
          [(size)
           #'(generate-jf-pat language (jf/mf-id . args) size)]
          [(x . y) (raise-syntax-error 'generate-term 
                                       "#:satisfying does not yet support additional keyword arguments"
                                       stx #'x)])]
       [else
        (raise-syntax-error 'generate-term 
                            "expected either a metafunction or a judgment-form identifier"
                            stx
                            #'jf/mf-id)])]
    [(_ orig-name actual-stx ...)
     (let ()
       (define form-name (syntax-e #'orig-name))
       (define-values (raw-generators args)
         (syntax-case #'(actual-stx ...) ()
           [(#:source src . rest)
            (values 
             (cond [(metafunc #'src) 
                    => (λ (f)
                         #`(let* ([f #,f]
                                  [L (metafunc-proc-lang f)]
                                  [compile-pat (compile L '#,form-name)]
                                  [cases (metafunc-proc-cases f)])
                             (check-cases 'src cases)
                             (map (λ (c) (compile-pat ((metafunc-case-lhs+ c) L))) 
                                  cases)))]
                   [else
                    #`(let* ([r #,(apply-contract #'reduction-relation?  #'src "#:source argument" form-name)]
                             [L (reduction-relation-lang r)]
                             [compile-pat (compile L '#,form-name)])
                        (map (λ (p) (compile-pat ((rewrite-proc-lhs p) L)))
                             (reduction-relation-make-procs r)))])
             #'rest)]
           [(lang pat . rest)
            (with-syntax ([(pattern (vars ...) (vars/ellipses ...)) 
                           (rewrite-side-conditions/check-errs 
                            (language-id-nts #'lang form-name)
                            form-name #t #'pat)])
              (values #`(list #,(term-generator #'lang #'pattern form-name))
                      #'rest))]))
       (define generator-syntax
         #`(make-generator #,raw-generators '#,form-name))
       (syntax-case args ()
         [()
          generator-syntax]
         [(size . kw-args)
          (quasisyntax/loc stx
            (#,generator-syntax size . kw-args))]))]))

(define (check-cases name cases)
  (when (null? cases)
    (raise-gen-fail 'generate-term (format "from ~a metafunction (it has no clauses)" name) 1)))

(define-syntax (generate-mf-pat stx)
  (syntax-case stx ()
    [(g-m-p lang-id (mf-name . lhs-pats) rhs-pat size)
     #`(parameterize ([unsupported-pat-err-name 'generate-term])
        ((make-redex-generator lang-id (mf-name . lhs-pats) = rhs-pat size)))]))

(define-syntax (generate-jf-pat stx)
  (syntax-case stx ()
    [(g-j-p lang-id (jf-name . pat-raw) size)
     #`(parameterize ([unsupported-pat-err-name 'generate-term])
         ((make-redex-generator lang-id (jf-name . pat-raw) size)))]))

(define-syntax (redex-generator stx)
  (syntax-case stx ()
    [(form-name args ...)
     #`(#%expression (make-redex-generator args ...))]))

(define-syntax (make-redex-generator stx)
  (syntax-case stx ()
    [(_ lang-id (jf/mf-id . args) . rest)
     (cond
       [(judgment-form-id? #'jf/mf-id)
        (syntax-case #'rest ()
          [(size)
           (let* ([j-f (lookup-judgment-form-id #'jf/mf-id)]
                  [clauses (judgment-form-gen-clauses j-f)]
                  [nts (definition-nts #'lang-id stx 'redex-generator)]
                  [relation? (judgment-form-relation? j-f)]
                  [args-stx (if relation?
                                (syntax/loc #'args (args))
                                #'args)]) 
             (with-syntax ([(pat (names ...) (names/ellipses ...))
                            (rewrite-side-conditions/check-errs nts 'redex-generator #t args-stx)])
               (if relation?
                   #`(let ([gen-proc (make-jf-gen/proc 'jf/mf-id #,clauses lang-id 'pat size)])
                       (λ ()
                         (match (gen-proc)
                           [`(,jf-name (,trms (... ...)))
                            `(,jf-name ,@trms)]
                           [#f #f])))
                   #`(make-jf-gen/proc 'jf/mf-id #,clauses lang-id 'pat size))))]
          [_
           (raise-syntax-error 'redex-generator 
                               "expected an integer depth bound"
                               stx
                               #'rest)])]
       [(metafunc #'jf/mf-id)
        (syntax-case #'rest ()
          [(= res size)
           (and (identifier? #'=)
                (equal? '= (syntax->datum #'=)))
           (let ()
             (define mf (syntax-local-value #'jf/mf-id (λ () #f)))
             (define nts (definition-nts #'lang-id stx 'redex-generator))
             (with-syntax ([(lhs-pat (lhs-names ...) (lhs-names/ellipses ...))
                            (rewrite-side-conditions/check-errs nts (syntax-e #'g-m-p) #t #'args)]
                           [(rhs-pat (rhs-names ...) (rhs-names/ellipses ...))
                            (rewrite-side-conditions/check-errs nts (syntax-e #'g-m-p) #t #'res)]
                           [mf-id (term-fn-get-id mf)])
               #`(make-mf-gen/proc 'mf-id mf-id lang-id 'lhs-pat 'rhs-pat size)))]
          [_
           (raise-syntax-error 'redex-generator 
                               "expected \"=\" followed by a result pattern and an integer depth bound"
                               stx
                               #'rest)])]
       [else
        (raise-syntax-error 'redex-generator
                            "expected either a metafunction or a judgment-form identifier"
                            stx
                            #'jf/mf-id)])]
    [(_ not-lang-id . rest)
     (not (identifier? #'not-lang-id))
     (raise-syntax-error 'redex-generator
                            "expected an identifier in the language position"
                            stx
                            #'not-lang-id)]))

(define (make-jf-gen/proc jf-id mk-clauses lang pat size)
  (define gen (search/next (mk-clauses) pat size lang))
  (define (termify search-res)
    (cond
      [(not-failed? search-res)
       (define exp (pat->term lang (p*e-p search-res) (p*e-e search-res)))
       (and (not-failed? exp)
            (cons jf-id exp))]
      [else #f]))
  (λ ()
    (parameterize ([current-logger generation-logger])
      (termify (gen)))))

(define (make-mf-gen/proc fn metafunc-proc lang lhs-pat rhs-pat size)
  (define gen (search/next ((metafunc-proc-gen-clauses metafunc-proc))
                           `(list ,lhs-pat ,rhs-pat)
                           size
                           lang))
  (define (termify res)
    (and (not-failed? res)
         (match res
           [(p*e lhs+rhs env)
            (define lhs+rhs-term (pat->term lang lhs+rhs env))
            (and (not-failed? lhs+rhs-term)
                 (match lhs+rhs-term
                   [(list lhs-term rhs-term)
                    `((,fn ,@lhs-term) = ,rhs-term)]))])))
  (λ ()
    (parameterize ([current-logger generation-logger])
      (termify (gen)))))
  
(provide redex-check
         generate-term
         check-reduction-relation
         check-metafunction
         enable-gen-trace!
         disable-gen-trace!
         last-gen-trace
         get-most-recent-trace
         update-gen-trace!
         exn:fail:redex:generation-failure?
         redex-generator
         (struct-out counterexample)
         (struct-out exn:fail:redex:test))
