#lang scheme/base

(provide define-struct/contract
         define/contract
         with-contract)

(require (for-syntax scheme/base
                     scheme/list
                     scheme/struct-info
                     syntax/define
                     syntax/kerncase
                     (prefix-in a: "private/helpers.ss"))
         scheme/splicing
         "private/arrow.ss"
         "private/base.ss"
         "private/guts.ss")

;; These are useful for all below.

(define-syntax (verify-contract stx)
  (syntax-case stx ()
    [(_ name x) (a:known-good-contract? #'x) #'x]
    [(_ name x) #'(coerce-contract name x)]))

;; id->contract-src-info : identifier -> syntax
;; constructs the last argument to the -contract, given an identifier
(define-for-syntax (id->contract-src-info id)
  #`(list (make-srcloc #,id
                       #,(syntax-line id)
                       #,(syntax-column id)
                       #,(syntax-position id)
                       #,(syntax-span id))
          #,(format "~s" (syntax->datum id))))



;                                                                                                    
;                                                                                                    
;                                                                                                    
;       ;           ;;; ;                     ;                                                      
;       ;          ;                          ;                                                      
;       ;          ;                         ;                           ;                       ;   
;    ;; ;    ;;;  ;;;;  ;   ; ;;     ;;;     ;     ;;;    ;;;    ; ;;   ;;;;  ; ;  ;;;     ;;;  ;;;; 
;   ;  ;;   ;   ;  ;    ;   ;;  ;   ;   ;    ;    ;   ;  ;   ;   ;;  ;   ;    ;;  ;   ;   ;   ;  ;   
;  ;    ;  ;    ;  ;    ;   ;   ;  ;    ;    ;   ;      ;     ;  ;   ;   ;    ;       ;  ;       ;   
;  ;    ;  ;;;;;;  ;    ;   ;   ;  ;;;;;;   ;    ;      ;     ;  ;   ;   ;    ;    ;;;;  ;       ;   
;  ;    ;  ;       ;    ;   ;   ;  ;        ;    ;      ;     ;  ;   ;   ;    ;   ;   ;  ;       ;   
;   ;  ;;   ;      ;    ;   ;   ;   ;       ;     ;   ;  ;   ;   ;   ;   ;    ;   ;   ;   ;   ;  ;   
;    ;; ;    ;;;;  ;    ;   ;   ;    ;;;;  ;       ;;;    ;;;    ;   ;    ;;  ;    ;;;;;   ;;;    ;; 
;                                          ;                                                         
;                                          ;                                                         
;                                                                                                    

;; (define/contract id contract expr)
;; defines `id' with `contract'; initially binding
;; it to the result of `expr'.  These variables may not be set!'d.
(define-syntax (define/contract define-stx)
  (when (eq? (syntax-local-context) 'expression)
    (raise-syntax-error 'define/contract
                        "used in expression context"
                        define-stx))
  (syntax-case define-stx ()
    [(_ name)
     (raise-syntax-error 'define/contract
                         "no contract or body"
                         define-stx)]
    [(_ name contract-expr)
     (raise-syntax-error 'define/contract
                         "expected a contract expression and a definition body, but found only one expression"
                         define-stx)]
    [(_ name+arg-list contract #:freevars args . body)
     (identifier? #'args)
     (raise-syntax-error 'define/contract
                         "expected list of identifier/contract pairs"
                         #'args)]
    [(_ name+arg-list contract #:freevars (arg ...) #:freevar x c . body)
     (syntax/loc define-stx
       (define/contract name+arg-list contract #:freevars (arg ... [x c]) . body))]
    [(_ name+arg-list contract #:freevar x c . body)
     (syntax/loc define-stx
       (define/contract name+arg-list contract #:freevars () #:freevar x c . body))]
    [(_ name+arg-list contract #:freevars args body0 body ...)
     (begin
       (when (and (identifier? #'name+arg-list)
                  (not (null? (syntax->list #'(body ...)))))
         (raise-syntax-error 'define/contract
                             "multiple expressions after identifier and contract"
                             #'(body ...)))
       (let-values ([(name body-expr)
                     (if (identifier? #'name+arg-list)
                         (values #'name+arg-list #'body0)
                         (normalize-definition
                          (datum->syntax #'define-stx (list* 'define/contract #'name+arg-list
                                                             #'body0 #'(body ...)))
                          #'lambda #t #t))])
         (with-syntax ([name name]
                       [body-expr body-expr]
                       [type (if (identifier? #'name+arg-list) 'definition 'function)])
           (syntax/loc define-stx
             (with-contract #:type type name
               ([name contract])
               #:freevars args
               (define name body-expr))))))]
    [(_ name+arg-list contract body0 body ...)
     (syntax/loc define-stx
       (define/contract name+arg-list contract #:freevars () body0 body ...))]))

(define-syntax (define-struct/contract stx)
  (define-struct field-info (stx ctc [mutable? #:mutable] auto?))
  (define-struct s-info (auto-value-stx transparent? def-stxs? def-vals?))

  (define syntax-error
    (lambda v
      (apply raise-syntax-error 'define-struct/contract v)))
  
  (define (build-struct-names name field-infos)
    (let ([name-str (symbol->string (syntax-case name ()
                                      [id (identifier? #'id)
                                          (syntax-e #'id)]
                                      [(sub super) 
                                       (syntax-e #'sub)]))])
      (list* 
       (syntax-case name ()
         [id (identifier? #'id) #'id]
         [(sub super) #'sub])
       (datum->syntax 
        name
        (string->symbol
         (string-append "struct:" name-str)))
       (datum->syntax 
        name
        (string->symbol
         (string-append "make-" name-str)))
       (datum->syntax 
        name
        (string->symbol
         (string-append name-str "?")))           
       (apply append
              (for/list ([finfo field-infos])
                (let ([field-str (symbol->string (syntax-e (field-info-stx finfo)))])
                  (cons (datum->syntax 
                         name
                         (string->symbol
                          (string-append name-str "-" field-str)))
                        (if (field-info-mutable? finfo)
                            (list (datum->syntax 
                                   name
                                   (string->symbol
                                    (string-append "set-" name-str "-" field-str "!"))))
                            null))))))))
  
  (define (build-contracts stx pred field-infos)
    (list* (syntax/loc stx any/c)
           (syntax/loc stx any/c)
           (apply append
                  (for/list ([finfo field-infos])
                    (let ([field-ctc (field-info-ctc finfo)])
                      (cons (quasisyntax/loc stx
                              (-> #,pred #,field-ctc))
                            (if (field-info-mutable? finfo)
                                (list 
                                 (quasisyntax/loc stx
                                   (-> #,pred #,field-ctc void?)))
                                null)))))))
  
  (define (check-field f ctc)
    (let ([p-list (syntax->list f)])
      (if p-list
          (begin 
            (when (null? p-list)
              (syntax-error "expected struct field" f))
            (unless (identifier? (car p-list))
              (syntax-error "expected identifier" f))
            (let loop ([rest (cdr p-list)]
                       [mutable? #f]
                       [auto? #f])
              (if (null? rest) 
                  (make-field-info (car p-list) ctc mutable? auto?)
                  (let ([elem (syntax-e (car rest))])
                    (if (keyword? elem)
                        (cond
                          [(eq? elem '#:mutable)
                           (begin (when mutable?
                                    (syntax-error "redundant #:mutable"
                                                  (car rest)))
                                  (loop (cdr rest) #t auto?))]
                          [(eq? elem '#:auto)
                           (begin (when auto?
                                    (syntax-error "redundant #:mutable"
                                                  (car rest)))
                                  (loop (cdr rest) mutable? #t))]
                          [else (syntax-error "expected #:mutable or #:auto"
                                              (car rest))])
                        (syntax-error "expected #:mutable or #:auto"
                                      (car rest)))))))
          (if (identifier? f)
              (make-field-info f ctc #f #f)
              (syntax-error "expected struct field" f)))))
  (define (check-kwds kwd-list field-infos)
    (let loop ([kwds kwd-list]
               [auto-value-stx #f]
               [mutable? #f]
               [transparent? #f]
               [def-stxs? #t]
               [def-vals? #t])
      (if (null? kwds)
          (make-s-info auto-value-stx transparent? def-stxs? def-vals?)
          (let ([kwd (syntax-e (car kwds))])
            (when (not (keyword? kwd))
              (syntax-error "expected a keyword"
                            (car kwds)))
            (cond
              [(eq? kwd '#:auto-value)
               (when (null? (cdr kwds))
                 (syntax-error "expected a following expression"
                               (car kwds)))
               (loop (cddr kwds) (cadr kwds)
                     transparent? mutable? def-stxs? def-vals?)]
              ;; let arbitrary properties through
              [(eq? kwd '#:property)
               (when (null? (cdr kwds))
                 (syntax-error "expected a property"
                               (car kwds)))
               (when (null? (cddr kwds))
                 (syntax-error "expected a value for the property"
                               (car kwds)))
               (loop (cdddr kwds) auto-value-stx
                     mutable? transparent? def-stxs? def-vals?)]
              [(eq? kwd '#:mutable)
               (when mutable?
                 (syntax-error "redundant #:mutable"
                               (car kwds)))
               (for ([finfo field-infos])
                 (set-field-info-mutable?! finfo #t))
               (loop (cdr kwds) auto-value-stx
                     #t transparent? def-stxs? def-vals?)]
              [(eq? kwd '#:transparent)
               (when transparent?
                 (syntax-error "redundant #:transparent"
                               (car kwds)))
               (loop (cdr kwds) auto-value-stx
                     mutable? #t def-stxs? def-vals?)]
              [(eq? kwd '#:omit-define-syntaxes)
               (when (not def-stxs?)
                 (syntax-error "redundant #:omit-define-syntaxes"
                               (car kwds)))
               (loop (cdr kwds) auto-value-stx
                     transparent? mutable? #f def-vals?)]
              [(eq? kwd '#:omit-define-values)
               (when (not def-vals?)
                 (syntax-error "redundant #:omit-define-values"
                               (car kwds)))
               (loop (cdr kwds) auto-value-stx
                     transparent? mutable? def-stxs? #f)]
              [else (syntax-error "unexpected keyword"
                                  (car kwds))])))))
  (syntax-case stx ()
    [(_ name ([field ctc] ...) kwds ...)
     (let ([fields (syntax->list #'(field ...))])
       (unless (or (identifier? #'name)
                   (syntax-case #'name ()
                     [(x y) (and (identifier? #'x)
                                 (identifier? #'y))]
                     [_ #f]))
         (syntax-error "expected identifier for struct name or a sub-type relationship (subtype supertype)"
                       #'name))
       (let* ([field-infos (map check-field fields (syntax->list #'(ctc ...)))]
              [sinfo (check-kwds (syntax->list #'(kwds ...)) field-infos)]
              [names (build-struct-names #'name field-infos)]
              [pred (cadddr names)]
              [ctcs (build-contracts stx pred field-infos)]
              [super-fields (syntax-case #'name ()
                              [(child parent)
                               (let ([v (syntax-local-value #'parent (lambda () #f))])
                                 (unless (struct-info? v)
                                   (raise-syntax-error #f "identifier is not bound to a structure type" stx #'parent))
                                 (let ([v (extract-struct-info v)])
                                   (cadddr v)))]
                              [else '()])])
         (let-values ([(non-auto-fields auto-fields)
                       (let loop ([fields field-infos]
                                  [nautos null]
                                  [autos null])
                         (if (null? fields)
                             (values (reverse nautos)
                                     (reverse autos))
                             (if (field-info-auto? (car fields))
                                 (loop (cdr fields)
                                       nautos
                                       (cons (car fields) autos))
                                 (if (null? autos)
                                     (loop (cdr fields)
                                           (cons (car fields) nautos)
                                           autos)
                                     (syntax-error "non-auto field after auto fields"
                                                   (field-info-stx (car fields)))))))])
           (with-syntax ([ctc-bindings
                          (if (s-info-def-vals? sinfo)
                              (map list (cddr names)
                                   ctcs)
                              null)]
                         [orig stx]
                         [struct-name (syntax-case #'name ()
                                        [id (identifier? #'id) #'id]
                                        [(id1 super) #'id1])]
                         [(auto-check ...)
                          (let* ([av-stx (if (s-info-auto-value-stx sinfo)
                                             (s-info-auto-value-stx sinfo)
                                             #'#f)]
                                 [av-id (datum->syntax av-stx
                                                       (string->symbol
                                                        (string-append (syntax-case #'name ()
                                                                         [id (identifier? #'id)
                                                                             (symbol->string (syntax-e #'id))]
                                                                         [(id1 super)
                                                                          (symbol->string (syntax-e #'id1))])
                                                                       ":auto-value"))
                                                       av-stx)])
                            (for/list ([finfo auto-fields])
                              #`(let ([#,av-id #,av-stx])
                                  (contract #,(field-info-ctc finfo)
                                            #,av-id
                                            '(struct name)
                                            'cant-happen
                                            #,(id->contract-src-info av-id)))))]
                         ;; a list of variables, one for each super field
                         [(super-fields ...) (generate-temporaries super-fields)]
                         ;; the contract for a super field is any/c becuase the
                         ;; super constructor will have its own contract
                         [(super-contracts ...) (for/list ([i (in-list super-fields)])
                                                  (datum->syntax stx 'any/c))]
                         [(non-auto-contracts ...)
                          (map field-info-ctc
                               (filter (lambda (f)
                                         (not (field-info-auto? f)))
                                       field-infos))]
                         ;; the make-foo function. this is used to make the contract
                         ;; print the right name in the blame
                         [maker (caddr names)]
                         [(non-auto-name ...)
                          (map field-info-stx non-auto-fields)])
             (syntax/loc stx
               (begin
                 (define-values () (begin auto-check ... (values)))
                 (define (guard super-fields ... non-auto-name ... struct-name)
                   (values super-fields ... non-auto-name ...))
                 (define blame-id
                   (current-contract-region))
                 (with-contract #:type struct struct-name
                                ctc-bindings
                                (define-struct/derived orig name (field ...)
                                  kwds ...
                                  #:guard (contract (-> super-contracts ... non-auto-contracts ... symbol? any)
                                                    guard
                                                    (current-contract-region) blame-id
                                                    #'maker)))))))))]
    [(_ name . bad-fields)
     (identifier? #'name)
     (syntax-error "expected a list of field name/contract pairs"
                   #'bad-fields)]
    [(_ . body)
     (syntax-error "expected a structure name"
                   #'body)]))

;                                                                                         
;                                                                                         
;              ;       ;                                                                  
;                  ;   ;                                     ;                         ;  
;                  ;   ;                                     ;                         ;  
;  ;    ;    ; ;  ;;;; ; ;;;          ;;;     ;;;    ; ;;;  ;;;; ; ;;   ;;;;     ;;;  ;;;;
;  ;    ;    ; ;   ;   ;;   ;        ;   ;   ;   ;   ;;   ;  ;   ;;    ;    ;   ;   ;  ;  
;   ;  ; ;  ;  ;   ;   ;    ;       ;       ;     ;  ;    ;  ;   ;    ;     ;  ;       ;  
;   ;  ; ;  ;  ;   ;   ;    ;       ;       ;     ;  ;    ;  ;   ;        ;;;  ;       ;  
;   ; ;   ; ;  ;   ;   ;    ;       ;       ;     ;  ;    ;  ;   ;     ;;;  ;  ;       ;  
;   ; ;   ; ;  ;   ;   ;    ; ;;;;  ;       ;     ;  ;    ;  ;   ;    ;     ;  ;       ;  
;   ; ;   ; ;  ;   ;   ;    ;       ;       ;     ;  ;    ;  ;   ;    ;     ;  ;       ;  
;    ;     ;   ;   ;   ;    ;        ;   ;   ;   ;   ;    ;  ;   ;    ;    ;;   ;   ;  ;  
;    ;     ;   ;   ;;; ;    ;         ;;;     ;;;    ;    ;  ;;; ;     ;;;; ;    ;;;   ;;;
;                                                                                         
;                                                                                         
;                                                                                         

(define-for-syntax (make-contracted-id-transformer id contract-stx pos-blame-id neg-blame-id)
  (make-set!-transformer
   (lambda (stx)
     (syntax-case stx (set!)
       [(set! i arg)
        (quasisyntax/loc stx
          (set! #,id
                (contract #,contract-stx
                          arg
                          #,neg-blame-id
                          #,pos-blame-id
                          #,(id->contract-src-info id))))]
       [(f arg ...)
        (quasisyntax/loc stx
          ((contract #,contract-stx
                     #,id
                     #,pos-blame-id
                     #,neg-blame-id
                     #,(id->contract-src-info id))
           arg ...))]
       [ident
        (identifier? (syntax ident))
        (quasisyntax/loc stx
          (contract #,contract-stx
                    #,id
                    #,pos-blame-id
                    #,neg-blame-id
                    #,(id->contract-src-info id)))]))))

(define-for-syntax (check-and-split-with-contracts args)
  (let loop ([args args]
             [protected null]
             [protections null])
    (cond
      [(null? args)
       (values protected protections)]
      [(let ([lst (syntax->list (car args))])
         (and (list? lst)
              (= (length lst) 2)
              (identifier? (first lst))
              lst))
       =>
       (lambda (l)
         (loop (cdr args)
               (cons (first l) protected)
               (cons (second l) protections)))]
      [else
       (raise-syntax-error 'with-contract
                           "expected (identifier contract)"
                           (car args))])))

(define-syntax (with-contract-helper stx)
  (syntax-case stx ()
    [(_ ())
     #'(begin)]
    [(_ (p0 p ...))
     (raise-syntax-error 'with-contract
                         "no definition found for identifier"
                         #'p0)]
    [(_ (p ...) body0 body ...)
     (let ([expanded-body0 (local-expand #'body0
                                         (syntax-local-context)
                                         (kernel-form-identifier-list))])
       (define (filter-ids to-filter to-remove)
         (filter (λ (i1)
                   (not (memf (λ (i2)
                                (bound-identifier=? i1 i2))
                              to-remove)))
                 to-filter))
       (syntax-case expanded-body0 (begin define-values define-syntaxes)
         [(begin sub ...)
          (syntax/loc stx
            (with-contract-helper (p ...) sub ... body ...))]
         [(define-syntaxes (id ...) expr)
          (let ([ids (syntax->list #'(id ...))])
            (with-syntax ([def expanded-body0]
                          [unused-ps (filter-ids (syntax->list #'(p ...)) ids)])
              (with-syntax ()
                (syntax/loc stx
                  (begin def (with-contract-helper unused-ps body ...))))))]
         [(define-values (id ...) expr)
          (let ([ids (syntax->list #'(id ...))])
            (with-syntax ([def expanded-body0]
                          [unused-ps (filter-ids (syntax->list #'(p ...)) ids)])
              (syntax/loc stx
                (begin def (with-contract-helper unused-ps body ...)))))]
         [else
          (quasisyntax/loc stx
            (begin #,expanded-body0
                   (with-contract-helper (p ...) body ...)))]))]))

(define-syntax (with-contract stx)
  (when (eq? (syntax-local-context) 'expression)
    (raise-syntax-error 'with-contract
                        "used in expression context"
                        stx))
  (syntax-case stx ()
    [(_ #:type type etc ...)
     (not (identifier? #'type))
     (raise-syntax-error 'with-contract
                         "expected identifier for type"
                         #'type)]
    [(_ #:type type args etc ...)
     (not (identifier? #'args))
     (raise-syntax-error 'with-contract
                         "expected identifier for blame"
                         #'args)]
    [(_ #:type type blame (arg ...) #:freevars (fv ...) #:freevar x c . body)
     (identifier? #'x)
     (syntax/loc stx
       (with-contract #:type type blame (arg ...) #:freevars (fv ... [x c]) . body))]
    [(_ #:type type blame (arg ...) #:freevars (fv ...) #:freevar x c . body)
     (raise-syntax-error 'with-contract
                         "use of #:freevar with non-identifier"
                         #'x)]
    [(_ #:type type blame (arg ...) #:freevars (fv ...) . body)
     (and (identifier? #'blame)
          (identifier? #'type))
     (let*-values ([(intdef) (syntax-local-make-definition-context)]
                   [(ctx) (list (gensym 'intdef))]
                   [(cid-marker) (make-syntax-introducer)]
                   [(free-vars free-ctcs)
                    (check-and-split-with-contracts (syntax->list #'(fv ...)))]
                   [(protected protections)
                    (check-and-split-with-contracts (syntax->list #'(arg ...)))])
       (define (add-context stx)
         (let ([ctx-added-stx (local-expand #`(quote #,stx)
                                            ctx
                                            (list #'quote)
                                            intdef)])
           (syntax-case ctx-added-stx ()
             [(_ expr) #'expr])))
       (when (eq? (syntax-local-context) 'expression)
         (raise-syntax-error 'with-contract
                             "cannot use in an expression context"
                             stx))
       (let ([dupd-id (check-duplicate-identifier protected)])
         (when dupd-id
           (raise-syntax-error 'with-contract
                               "identifier appears twice in exports"
                               dupd-id)))
       (syntax-local-bind-syntaxes protected #f intdef)
       (syntax-local-bind-syntaxes free-vars #f intdef)
       (internal-definition-context-seal intdef)
       (with-syntax ([blame-stx #''(type blame)]
                     [blame-id (car (generate-temporaries (list #t)))]
                     [(free-var ...) free-vars]
                     [(free-var-id ...) (add-context #`#,free-vars)]
                     [(free-ctc-id ...) (map cid-marker free-vars)]
                     [(free-ctc ...) (map (λ (c v)
                                            (syntax-property c 'inferred-name v))
                                          free-ctcs
                                          free-vars)]
                     [(free-src-info ...) (map id->contract-src-info free-vars)]
                     [(ctc-id ...) (map cid-marker protected)]
                     [(ctc ...) (map (λ (c v)
                                       (syntax-property (add-context c) 'inferred-name v))
                                     protections
                                     protected)]
                     [(p ...) protected]
                     [(marked-p ...) (add-context #`#,protected)]
                     [(src-info ...) (map (compose id->contract-src-info add-context) protected)])
         (with-syntax ([new-stx (add-context #'(splicing-syntax-parameterize 
                                                ([current-contract-region (λ (stx) #'blame-stx)])
                                                . body))])
           (quasisyntax/loc stx
           (begin
             (define-values (free-ctc-id ...)
               (values (verify-contract 'with-contract free-ctc) ...))
             (define blame-id
               (current-contract-region))
             (define-values ()
               (begin (contract free-ctc-id
                                free-var
                                blame-id
                                'cant-happen
                                free-src-info)
                      ...
                      (values)))
             (define-syntaxes (free-var-id ...)
               (values (make-contracted-id-transformer
                        (quote-syntax free-var)
                        (quote-syntax free-ctc-id)
                        (quote-syntax blame-id)
                        (quote-syntax blame-stx)) ...))
             (with-contract-helper (marked-p ...) new-stx)
             (define-values (ctc-id ...)
               (values (verify-contract 'with-contract ctc) ...))
             (define-values ()
               (begin (contract ctc-id
                                marked-p
                                blame-stx
                                'cant-happen
                                src-info)
                      ...
                      (values)))
             (define-syntaxes (p ...)
               (values (make-contracted-id-transformer
                        (quote-syntax marked-p)
                        (quote-syntax ctc-id)
                        (quote-syntax blame-stx)
                        (quote-syntax blame-id)) ...)))))))]
    [(_ #:type type blame (arg ...) #:freevar x c . body)
     (syntax/loc stx
       (with-contract #:type type blame (arg ...) #:freevars ([x c]) . body))]
    [(_ #:type type blame (arg ...) . body)
     (syntax/loc stx
       (with-contract #:type type blame (arg ...) #:freevars () . body))]
    [(_ #:type type blame bad-args etc ...)
     (raise-syntax-error 'with-contract
                         "expected list of identifier and/or (identifier contract)"
                         #'bad-args)]
    [(_ #:type type blame)
     (raise-syntax-error 'with-contract
                         "only blame"
                         stx)]
    [(_ etc ...)
     (syntax/loc stx
       (with-contract #:type region etc ...))]))
