#lang scheme/base
(require scheme/contract/base
         scheme/dict
         syntax/stx
         syntax/id-table
         "../util.ss"
         "rep-attrs.ss"
         "rep-patterns.ss")
(provide (all-from-out "rep-attrs.ss")
         (all-from-out "rep-patterns.ss")
         (struct-out stxclass)
         stxclass/s?
         stxclass/h?
         (struct-out attr)
         (struct-out rhs)
         (struct-out variant)
         (struct-out clause:fail)
         (struct-out clause:with)
         (struct-out clause:attr)
         (struct-out conventions)
         (struct-out literalset))

#|
A stxclass is
  (make-sc symbol (listof symbol) (list-of SAttr) identifier identifier boolean)
|#
(define-struct stxclass (name params attrs parser-name description splicing?)
  #:prefab)

(define (stxclass/s? x)
  (and (stxclass? x) (not (stxclass-splicing? x))))
(define (stxclass/h? x)
  (and (stxclass? x) (stxclass-splicing? x)))

#|
An RHS is
  (make-rhs stx (listof SAttr) boolean stx/#f (listof Variant) (listof stx))
definitions: auxiliary definitions from #:declare
|#
(define-struct rhs (ostx attrs transparent? description variants definitions)
  #:prefab)

#|
A Variant is
  (make-variant stx (listof SAttr) Pattern (listof SideClause))
|#
(define-struct variant (ostx attrs pattern sides definitions) #:prefab)

#|
A SideClause is one of
  (make-clause:fail stx stx)
  (make-clause:with pattern stx (listof stx))
  (make-clause:attr IAttr stx)
|#
(define-struct clause:fail (condition message) #:prefab)
(define-struct clause:with (pattern expr definitions) #:prefab)
(define-struct clause:attr (attr expr) #:prefab)

#|
A Conventions is
  (make-conventions (listof ConventionRule))
A ConventionRule is (list regexp DeclEntry)
|#
(define-struct conventions (rules) #:transparent)

#|
A LiteralSet is
  (make-literalset (listof (list symbol id)))
|#
(define-struct literalset (literals) #:transparent)

;; make-dummy-stxclass : identifier -> SC
;; Dummy stxclass for calculating attributes of recursive stxclasses.
(define (make-dummy-stxclass name)
  (make stxclass (syntax-e name) null null #f #f #f))


;; Environments

#|
DeclEnv =
  (make-declenv immutable-bound-id-mapping[id => DeclEntry]
                (listof ConventionRule))
DeclEntry =
  (list 'literal id id)
  (list 'stxclass id id (listof stx))
  (list 'parser id id (listof IAttr))
  #f
|#
(define-struct declenv (table conventions))

(define (new-declenv literals #:conventions [conventions null])
  (for/fold ([decls (make-declenv (make-immutable-bound-id-table) conventions)])
      ([literal literals])
    (declenv-put-literal decls (car literal) (cadr literal))))

(define (declenv-lookup env id #:use-conventions? [use-conventions? #t])
  (or (bound-id-table-ref (declenv-table env) id #f)
      (and use-conventions?
           (conventions-lookup (declenv-conventions env) id))))

(define (declenv-check-unbound env id [stxclass-name #f]
                               #:blame-declare? [blame-declare? #f])
  ;; Order goes: literals, pattern, declares
  ;; So blame-declare? only applies to stxclass declares
  (let ([val (declenv-lookup env id #:use-conventions? #f)])
    (when val
      (cond [(eq? 'literal (car val))
             (wrong-syntax id "identifier previously declared as literal")]
            [(and blame-declare? stxclass-name)
             (wrong-syntax (cadr val)
                           "identifier previously declared with syntax class ~a"
                           stxclass-name)]
            [else
             (wrong-syntax (if blame-declare? (cadr val) id)
                           "identifier previously declared")]))))

(define (declenv-put-literal env internal-id lit-id)
  (declenv-check-unbound env internal-id)
  (make-declenv
   (bound-id-table-set (declenv-table env) internal-id
                       (list 'literal internal-id lit-id))
   (declenv-conventions env)))

(define (declenv-put-stxclass env id stxclass-name args)
  (declenv-check-unbound env id)
  (make-declenv
   (bound-id-table-set (declenv-table env) id
                       (list 'stxclass id stxclass-name args))
   (declenv-conventions env)))

(define (declenv-put-parser env id parser get-description attrs splicing?)
  ;; no unbound check, since replacing 'stxclass entry
  (make-declenv
   (bound-id-table-set (declenv-table env) id
                       (list (if splicing? 'splicing-parser 'parser)
                             parser get-description attrs))
   (declenv-conventions env)))

;; returns ids in domain of env but not in given list
(define (declenv-domain-difference env ids)
  (define idbm (make-bound-id-table))
  (for ([id ids]) (bound-id-table-set! idbm id #t))
  (for/list ([(k v) (in-dict (declenv-table env))]
             #:when (and (pair? v) (not (eq? (car v) 'literal)))
             #:when (not (bound-id-table-ref idbm k #f)))
    k))

;; Conventions = (listof (list regexp DeclEntry))

(define (conventions-lookup conventions id)
  (let ([sym (symbol->string (syntax-e id))])
    (for/or ([c conventions])
      (and (regexp-match? (car c) sym) (cadr c)))))

;; Contracts

(define DeclEnv/c
  (flat-named-contract 'DeclEnv declenv?))

(define SideClause/c
  (or/c clause:fail? clause:with? clause:attr?))

(provide/contract
 [DeclEnv/c contract?]
 [SideClause/c contract?]

 [make-dummy-stxclass (-> identifier? stxclass?)]
 [stxclass-lookup-config (parameter/c (symbols 'no 'try 'yes))]

 [new-declenv
  (->* [(listof (list/c identifier? identifier?))]
       [#:conventions list?]
       DeclEnv/c)]
 [declenv-lookup
  (-> DeclEnv/c identifier? any)]
 [declenv-put-stxclass
  (-> DeclEnv/c identifier? identifier? (listof syntax?)
      DeclEnv/c)]
 [declenv-put-parser
  (-> DeclEnv/c identifier? any/c any/c (listof sattr?) boolean?
      DeclEnv/c)]
 [declenv-domain-difference
  (-> DeclEnv/c (listof identifier?)
      (listof identifier?))]
 [declenv-table
  (-> DeclEnv/c any)]

 [get-stxclass
  (-> identifier? any)]
 [get-stxclass/check-arg-count
  (-> identifier? exact-nonnegative-integer? any)]
 [split-id/get-stxclass
  (-> identifier? DeclEnv/c any)])

;; stxclass-lookup-config : (parameterof (U 'no 'try 'yes))
;;  'no means don't lookup, always use dummy (no nested attrs)
;;  'try means lookup, but on failure use dummy (-> nested attrs only from prev.)
;;  'yes means lookup, raise error on failure
(define stxclass-lookup-config (make-parameter 'yes))

(define (get-stxclass id)
  (define config (stxclass-lookup-config))
  (if (eq? config 'no)
      (make-dummy-stxclass id)
      (cond [(syntax-local-value/catch id stxclass?) => values]
            [(eq? config 'try)
             (make-dummy-stxclass id)]
            [else (wrong-syntax id "not defined as syntax class")])))

(define (get-stxclass/check-arg-count id arg-count)
  (let* ([sc (get-stxclass id)]
         [expected-arg-count (length (stxclass-params sc))])
    (unless (or (= expected-arg-count arg-count)
                (memq (stxclass-lookup-config) '(try no)))
      ;; (above: don't check error if stxclass may not be defined yet)
      (wrong-syntax id
                    "too few arguments for syntax-class ~a (expected ~s)"
                    (syntax-e id)
                    expected-arg-count))
    sc))

(define (split-id/get-stxclass id0 decls)
  (cond [(regexp-match #rx"^([^:]*):(.+)$" (symbol->string (syntax-e id0)))
         => (lambda (m)
              (define id
                (datum->syntax id0 (string->symbol (cadr m)) id0 id0))
              (define scname
                (datum->syntax id0 (string->symbol (caddr m)) id0 id0))
              (declenv-check-unbound decls id (syntax-e scname)
                                     #:blame-declare? #t)
              (let ([sc (get-stxclass/check-arg-count scname 0)])
                (values id sc)))]
        [else (values id0 #f)]))
