#lang scheme

(provide define-keywords 
         ;; (define-keywords (name1:identifier ... spec:expr) ...)
         ;; constraint: the first name is the original name
         ;; and it is also the name of the field in the class
         function-with-arity expr-with-check except err 
         ->args
         ->kwds-in
         clauses-use-kwd)

(require 
 (for-template "syn-aux-aux.ss" 
               scheme
               (rename-in lang/prim (first-order->higher-order f2h))))

(require (for-syntax syntax/parse))
(define-syntax (define-keywords stx)
  (syntax-parse stx 
    [(define-keywords the-list (kw:identifier ... coerce:expr) ...)
     #'(begin
         (provide kw ...) ...
         (define-syntaxes (kw ...)
           (values (lambda (x)
                     (raise-syntax-error 'kw "used out of context" x))
                   ...))
         ...
         (define-for-syntax the-list
           (apply append 
                  (list 
                   (let* ([x (list (list #'kw ''kw) ...)]
                          [f (caar x)])
                     (map (lambda (x) 
                            (define clause-name (car x))
                            (define clause-spec (cadr x))
                            (list clause-name f (coerce clause-spec)))
                          x))
                   ...))))]))

#;
(define-syntax-rule
  (define-keywords the-list (kw coerce) ...)
  (begin
    (provide kw ...) 
    (define-syntax kw
      (lambda (x)
        (raise-syntax-error 'kw "used out of context" x)))
    ...
    (define-for-syntax the-list
      (list (list #'kw (coerce ''kw)) ...))))

#|
  transform the clauses into the initial arguments specification 
  for a new expression that instantiates the appropriate class
  
  ensure that the initial state (state0) is not in the shape of a clause

  ensure that all clauses mention only keywords specified in AllSpec or PartSpec
  move the contracts from AppSpecl and PartSpec to the clauses 
  
  run ->rec? over all used keywords to discover the presence of special clauses
  
  if anything fails, use the legal keyword to specialize the error message
|#
(define (->args tag stx state0 clauses AllSpec PartSpec ->rec? legal)
  (define msg (format "not a legal clause in a ~a description" legal))
  (define Spec (append AllSpec PartSpec))
  (define kwds (map (compose (curry datum->syntax stx) car) Spec))
  (define spec (clauses-use-kwd (syntax->list clauses) ->rec? msg kwds))
  (duplicates? tag spec)
  (not-a-clause tag stx state0 kwds)
  (map (lambda (x) 
         (define kw (car x))
         (define-values (key coercion)
           (let loop ([kwds kwds][Spec Spec])
             (if (free-identifier=? (car kwds) kw)
                 ;; -- the original keyword, which is also the init-field name
                 ;; -- the coercion that comes with it 
                 (values (cadar Spec) (caddar Spec))
                 (loop (cdr kwds) (cdr Spec)))))
         (list key (coercion (cdr x))))
       spec))

;; Symbol Syntax Syntax [Listof Kw] -> true
;; effect: if state0 looks like a clause, raise special error 
(define (not-a-clause tag stx state0 kwds)
  (syntax-case state0 ()
    [(kw . E) 
     ((->kwds-in kwds) #'kw) 
     (raise-syntax-error tag "missing initial state" stx)]
    [_ #t]))

;; Symbol [Listof kw] -> true
;; effect: raise syntax error about duplicated clause 
(define (duplicates? tag lox)
  (let duplicates? ([lox lox])
    (cond
      [(empty? lox) false]
      [else
       (let* ([f (caar lox)]
              [id (syntax-e f)]
              [x (memf (lambda (x) (free-identifier=? (car x) f)) (rest lox))])
         (if x 
             (raise-syntax-error tag (format "duplicate ~a clause" id) (cdar x))
             (duplicates? (rest lox))))])))

;; check whether rec? occurs, produce list of keywords 
(define (clauses-use-kwd stx:list ->rec? legal-clause kwds)
  (define kwd-in? (->kwds-in kwds))
  (define double (string-append legal-clause ", ~a has been redefined"))
  (map (lambda (stx)
         (syntax-case stx ()
           [(kw . E) (kwd-in? #'kw) (begin (->rec? #'kw #'E) (cons #'kw stx))]
           [(kw . E)
            (let ([kw (syntax-e #'kw)])
              (if (member kw (map syntax-e kwds))
                  (raise-syntax-error #f (format double kw) stx)
                  (raise-syntax-error #f legal-clause stx)))]
           [_ (raise-syntax-error #f legal-clause stx)]))
       stx:list))

;; [Listof SyntaxIdentifier] -> (Syntax -> Boolean)
(define (->kwds-in kwds)
  (lambda (k)
    (and (identifier? k) (for/or ([n kwds]) (free-identifier=? k n)))))

(define-syntax (expr-with-check stx)
  (syntax-case stx ()
    [(_ check> msg)
     #`(lambda (tag)
         (lambda (p)
           (syntax-case p ()
             [(_ x) #`(check> #,tag x)]
             [_ (err tag p msg)])))]))

(define-syntax function-with-arity 
  (syntax-rules (except)
    [(_ arity)
     (lambda (tag)
       (lambda (p)
         (syntax-case p ()
           [(_ x) #`(proc> #,tag (f2h x) arity)]
           [_ (err tag p)])))]
    [(_ arity except extra)
     (lambda (tag)
       (lambda (p)
         (syntax-case p ()
           [(_ x) #`(proc> #,tag (f2h x) arity)]
           extra
           [_ (err tag p)])))]))

(define (err spec p . xtras)
  (raise-syntax-error (cadr spec)
                      (if (null? xtras)
                          "illegal specification"
                          (string-append "illegal specification: " (car xtras)))
                      p))
