#lang racket/base
(require racket/contract/base
         racket/dict
         racket/list
         syntax/private/id-table
         racket/syntax
         syntax/parse/private/residual-ct ;; keep abs. path
         unstable/struct
         "minimatch.rkt"
         "kws.rkt"
         "rep-attrs.rkt"
         "rep-patterns.rkt")
;; from residual.rkt
(provide (struct-out stxclass)
         (struct-out options)
         (struct-out integrate)
         (struct-out conventions)
         (struct-out literalset)
         (struct-out eh-alternative-set)
         (struct-out eh-alternative))
;; from here
(provide stxclass/s?
         stxclass/h?
         stxclass-commit?
         stxclass-delimit-cut?
         (struct-out rhs)
         (struct-out variant)
         (struct-out clause:fail)
         (struct-out clause:with)
         (struct-out clause:attr)
         (struct-out clause:do))

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

(define (stxclass-commit? x)
  (options-commit? (stxclass-options x)))
(define (stxclass-delimit-cut? x)
  (options-delimit-cut? (stxclass-options x)))

#|
An RHS is
  #s(rhs stx (listof SAttr) bool stx/#f (listof Variant) (listof stx) Options Integrate/#f)
definitions: auxiliary definitions from #:declare
|#
(define-struct rhs (ostx attrs transparent? description variants definitions options integrate)
  #:prefab)

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

#|
SideClause is defined in rep-patterns
|#

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

;; Environments

#|
DeclEnv =
  (make-declenv immutable-bound-id-mapping[id => DeclEntry]
                (listof ConventionRule))

DeclEntry =
  (den:lit id id ct-phase ct-phase)
  (den:class id id Arguments)
  (den:magic-class id id Arguments stx)
  (den:parser id (listof SAttr) bool bool bool)
  (den:delayed id id)

Arguments is defined in rep-patterns.rkt

A DeclEnv is built up in stages:
  1) syntax-parse (or define-syntax-class) directives
     #:literals -> den:lit
     #:local-conventions -> den:class
     #:conventions -> den:delayed
     #:literal-sets -> den:lit
  2) pattern directives
     #:declare -> den:magic-class
  3) create-aux-def creates aux parser defs
     den:class -> den:parser or den:delayed

== Scoping ==

A #:declare directive results in a den:magic-class entry, which
indicates that the pattern variable's syntax class arguments (if any)
have "magical scoping": they are evaluated in the scope where the
pattern variable occurs. If the variable occurs multiple times, the
expressions are duplicated, and may be evaluated in different scopes.
|#

(define-struct declenv (table conventions))

(define-struct den:lit (internal external input-phase lit-phase))
(define-struct den:class (name class argu))
(define-struct den:magic-class (name class argu role))
(define-struct den:parser (parser attrs splicing? commit? delimit-cut?))
;; and from residual.rkt: (define-struct den:delayed (parser class))

(define (new-declenv literals #:conventions [conventions null])
  (make-declenv
   (for/fold ([table (make-immutable-bound-id-table)])
       ([literal (in-list literals)])
     (bound-id-table-set table (car literal)
                         (make den:lit (first literal) (second literal)
                               (third literal) (fourth literal))))
   conventions))

(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)])
    (match val
      [(den:lit _i _e _ip _lp)
       (wrong-syntax id "identifier previously declared as literal")]
      [(den:magic-class name _c _a _r)
       (if (and blame-declare? stxclass-name)
           (wrong-syntax name
                         "identifier previously declared with syntax class ~a"
                         stxclass-name)
           (wrong-syntax (if blame-declare? name id)
                         "identifier previously declared"))]
      [(den:class name _c _a)
       (if (and blame-declare? stxclass-name)
           (wrong-syntax name
                         "identifier previously declared with syntax class ~a"
                         stxclass-name)
           (wrong-syntax (if blame-declare? name id)
                         "identifier previously declared"))]
      [(den:parser _p _a _sp _c _dc?)
       (wrong-syntax id "(internal error) late unbound check")]
      ['#f (void)])))

(define (declenv-put-stxclass env id stxclass-name argu [role #f])
  (declenv-check-unbound env id)
  (make-declenv
   (bound-id-table-set (declenv-table env) id
                       (make den:magic-class id stxclass-name argu role))
   (declenv-conventions env)))

;; declenv-update/fold : DeclEnv (Id/Regexp DeclEntry a -> DeclEntry a) a
;;                    -> (values DeclEnv a)
(define (declenv-update/fold env0 f acc0)
  (define-values (acc1 rules1)
    (for/fold ([acc acc0] [newrules null])
        ([rule (in-list (declenv-conventions env0))])
      (let-values ([(val acc) (f (car rule) (cadr rule) acc)])
        (values acc (cons (list (car rule) val) newrules)))))
  (define-values (acc2 table2)
    (for/fold ([acc acc1] [table (make-immutable-bound-id-table)])
        ([(k v) (in-dict (declenv-table env0))])
      (let-values ([(val acc) (f k v acc)])
        (values acc (bound-id-table-set table k val)))))
  (values (make-declenv table2 (reverse rules1))
          acc2))

;; 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 (in-list ids)]) (bound-id-table-set! idbm id #t))
  (for/list ([(k v) (in-dict (declenv-table env))]
             #:when (or (den:class? v) (den:magic-class? v) (den:parser? v))
             #:unless (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 (in-list conventions)])
      (and (regexp-match? (car c) sym) (cadr c)))))

;; Contracts

(define DeclEnv/c declenv?)

(define DeclEntry/c 
  (or/c den:lit? den:class? den:magic-class? den:parser? den:delayed?))

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

;; ct-phase = syntax, expr that computes absolute phase
;;   usually = #'(syntax-local-phase-level)
(define ct-phase/c syntax?)

(provide (struct-out den:lit)
         (struct-out den:class)
         (struct-out den:magic-class)
         (struct-out den:parser)
         ;; from residual.rkt:
         (struct-out den:delayed))

(provide/contract
 [DeclEnv/c contract?]
 [DeclEntry/c contract?]
 [SideClause/c contract?]
 [ct-phase/c contract?]

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

 [new-declenv
  (->* [(listof (list/c identifier? identifier? ct-phase/c ct-phase/c))]
       [#:conventions list?]
       DeclEnv/c)]
 [declenv-lookup
  (-> DeclEnv/c identifier? any)]
 [declenv-put-stxclass
  (-> DeclEnv/c identifier? identifier? arguments? (or/c syntax? #f)
      DeclEnv/c)]
 [declenv-domain-difference
  (-> DeclEnv/c (listof identifier?)
      (listof identifier?))]
 [declenv-update/fold
  (-> DeclEnv/c
      (-> (or/c identifier? regexp?) DeclEntry/c any/c (values DeclEntry/c any/c))
      any/c
      (values DeclEnv/c any/c))]

 [get-stxclass
  (-> identifier? stxclass?)]
 [get-stxclass/check-arity
  (-> identifier? syntax? exact-nonnegative-integer? (listof keyword?)
      stxclass?)]
 [split-id/get-stxclass
  (-> identifier? DeclEnv/c
      (values identifier? (or/c stxclass? #f)))])

;; 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))

;; stxclass-colon-notation? : (parameterof boolean)
;;   if #t, then x:sc notation means (~var x sc)
;;   otherwise, just a var
(define stxclass-colon-notation? (make-parameter #t))

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

(define (get-stxclass/check-arity id stx pos-count keywords)
  (let ([sc (get-stxclass id)])
    (unless (memq (stxclass-lookup-config) '(try no))
      (check-arity (stxclass-arity sc) pos-count keywords
                   (lambda (msg)
                     (raise-syntax-error #f msg stx))))
    sc))

(define (split-id/get-stxclass id0 decls)
  (cond [(and (stxclass-colon-notation?)
              (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-arity scname id0 0 null)])
                (values id sc)))]
        [else (values id0 #f)]))

;; ----

(provide get-eh-alternative-set)

(define (get-eh-alternative-set id)
  (let ([v (syntax-local-value id (lambda () #f))])
    (unless (eh-alternative-set? v)
      (wrong-syntax id "not defined as an eh-alternative-set"))
    v))
