#lang scheme/base
(require (for-syntax scheme/base
                     scheme/list
                     syntax/kerncase
                     syntax/boundmap
                     syntax/define
                     syntax/flatten-begin))

(provide define-package
         package-begin

         open-package
         open*-package

         define*
         define*-values
         define*-syntax
         define*-syntaxes

         (for-syntax package?
                     package-exported-identifiers
                     package-original-identifiers))

(define-for-syntax (do-define-* stx define-values-id)
  (syntax-case stx ()
    [(_ (id ...) rhs)
     (let ([ids (syntax->list #'(id ...))])
       (for-each (lambda (id)
                   (unless (identifier? id)
                     (raise-syntax-error
                      #f
                      "expected an identifier for definition"
                      stx
                      id)))
                 ids)
       (with-syntax ([define-values define-values-id])
         (syntax/loc stx
           (define-values (id ...) rhs))))]))
(define-syntax (-define*-values stx)
  (do-define-* stx #'define-values))
(define-syntax (-define*-syntaxes stx)
  (do-define-* stx #'define-syntaxes))
(define-syntax (define*-values stx)
  (syntax-case stx ()
    [(_ (id ...) rhs)
     (syntax-property
      (syntax/loc stx (-define*-values (id ...) rhs))
      'certify-mode
      'transparent-binding)]))
(define-syntax (define*-syntaxes stx)
  (syntax-case stx ()
    [(_ (id ...) rhs)
     (syntax-property
      (syntax/loc stx (-define*-syntaxes (id ...) rhs))
      'certify-mode
      'transparent-binding)]))

(define-syntax (define* stx)
  (let-values ([(id rhs) (normalize-definition stx #'lambda)])
    (quasisyntax/loc stx
      (define*-values (#,id) #,rhs))))
(define-syntax (define*-syntax stx)
  (let-values ([(id rhs) (normalize-definition stx #'lambda)])
    (quasisyntax/loc stx
      (define*-syntaxes (#,id) #,rhs))))

(begin-for-syntax
 (define-struct package (exports hidden)
   #:omit-define-syntaxes
   #:property prop:procedure (lambda (r stx)
                               (raise-syntax-error
                                #f
                                "misuse of a package name"
                                stx)))

 (define (generate-hidden id)
   ;; Like `generate-temporaries', but preserve the symbolic name
   ((make-syntax-introducer) (datum->syntax #f (syntax-e id))))
 
 (define (reverse-mapping who id exports hidden)
   (or (ormap (lambda (m)
                (and (free-identifier=? id (cdr m))
                     (car m)))
              exports)
       (ormap (lambda (h)
                (and (free-identifier=? id h)
                     ;; Not at top level, where free-id=? is unreliable,
                     ;; and re-definition is ok:
                     (identifier-binding id)
                     ;; Name is inaccessible. Generate a temporary to
                     ;; avoid potential duplicate-definition errors
                     ;; when the name is bound in the same context as
                     ;; the package.
                     (generate-hidden id)))
              hidden)
       id)))

(define-for-syntax (move-props orig new)
  (datum->syntax new
                 (syntax-e new)
                 orig
                 orig))

(define-for-syntax (do-define-package stx exp-stx)
  (syntax-case exp-stx ()
    [(_ pack-id mode exports form ...)
     (let ([id #'pack-id]
           [exports #'exports]
           [mode (syntax-e #'mode)])
       (unless (eq? mode '#:begin)
         (unless (identifier? id)
           (raise-syntax-error #f
                               "expected an identifier"
                               stx
                               id)))
       (let ([exports
              (cond
               [(syntax->list exports)
                => (lambda (l)
                     (for-each (lambda (i)
                                 (unless (identifier? i)
                                   (raise-syntax-error #f
                                                       "expected identifier to export"
                                                       stx
                                                       i)))
                               l)
                     (let ([dup-id (check-duplicate-identifier l)])
                       (when dup-id
                         (raise-syntax-error
                          #f
                          "duplicate export"
                          stx
                          dup-id)))
                     l)]
               [else (raise-syntax-error #f
                                         (format "expected a parenthesized sequence of identifiers ~a"
                                                 (case mode
                                                   [(#:only) "to export"]
                                                   [(#:all-defined-except) "to exclude from export"]
                                                   [else (format "for ~a" mode)]))
                                         stx
                                         exports)])])
         (let* ([def-ctx (syntax-local-make-definition-context)]
                [ctx (cons (gensym 'intdef)
                           (let ([orig-ctx (syntax-local-context)])
                             (if (pair? orig-ctx)
                                 orig-ctx
                                 null)))]
                [pre-package-id (lambda (id def-ctxes)
                                  (identifier-remove-from-definition-context 
                                   id 
                                   def-ctxes))]
                [kernel-forms (list*
                               #'-define*-values
                               #'-define*-syntaxes
                               (kernel-form-identifier-list))]
                [init-exprs (syntax->list #'(form ...))]
                [new-bindings (make-bound-identifier-mapping)]
                [fixup-sub-package (lambda (renamed-exports renamed-defines def-ctxes)
                                     (lambda (stx)
                                       (syntax-case* stx (define-syntaxes #%plain-app make-package quote-syntax 
                                                           list cons #%plain-lambda)
                                                     free-transformer-identifier=?
                                         [(define-syntaxes (pack-id)
                                            (#%plain-app
                                             make-package
                                             (#%plain-lambda ()
                                                             (#%plain-app list 
                                                                          (#%plain-app cons 
                                                                                       (quote-syntax export)
                                                                                       (quote-syntax renamed))
                                                                          ...))
                                             hidden))
                                          (with-syntax ([(export ...)
                                                         (map (lambda (id)
                                                                (if (or (ormap (lambda (e-id)
                                                                                 (bound-identifier=? id e-id))
                                                                               renamed-exports)
                                                                        (not (ormap (lambda (e-id)
                                                                                      (bound-identifier=? id e-id))
                                                                                    renamed-defines)))
                                                                    ;; Need to preserve the original
                                                                    (pre-package-id id def-ctxes)
                                                                    ;; It's not accessible, so just hide the name
                                                                    ;;  to avoid re-binding errors. (Is this necessary,
                                                                    ;;  or would `pre-package-id' take care of it?)
                                                                    (generate-hidden id)))
                                                              (syntax->list #'(export ...)))])
                                            (syntax/loc stx
                                              (define-syntaxes (pack-id)
                                                (make-package
                                                 (lambda ()
                                                   (list (cons (quote-syntax export)
                                                               (quote-syntax renamed))
                                                         ...))
                                                 hidden))))]
                                         [_ stx])))]
                [complement (lambda (bindings ids)
                              (let ([tmp (make-bound-identifier-mapping)])
                                (bound-identifier-mapping-for-each bindings
                                                                   (lambda (k v)
                                                                     (bound-identifier-mapping-put! tmp k #t)))
                                (for-each (lambda (id)
                                            (bound-identifier-mapping-put! tmp id #f))
                                          ids)
                                (filter
                                 values
                                 (bound-identifier-mapping-map tmp (lambda (k v) (and v k))))))])
           (let ([register-bindings!
                  (lambda (ids)
                    (for-each (lambda (id)
                                (when (bound-identifier-mapping-get new-bindings id (lambda () #f))
                                  (raise-syntax-error #f 
                                                      "duplicate binding"
                                                      stx
                                                      id))
                                (bound-identifier-mapping-put! new-bindings
                                                               id
                                                               #t))
                              ids))]
                 [add-package-context (lambda (def-ctxes)
                                        (lambda (stx)
                                          (let ([q (local-expand #`(quote #,stx)
                                                                 ctx
                                                                 (list #'quote)
                                                                 def-ctxes)])
                                            (syntax-case q ()
                                              [(_ stx) #'stx]))))])
             (let loop ([exprs init-exprs]
                        [rev-forms null]
                        [def-ctxes (list def-ctx)])
               (cond
                [(null? exprs)
                 (for-each (lambda (def-ctx)
                             (internal-definition-context-seal def-ctx))
                           def-ctxes)
                 (let ([exports-renamed (map (add-package-context def-ctxes) exports)]
                       [defined-renamed (bound-identifier-mapping-map new-bindings
                                                                      (lambda (k v) k))])
                   (for-each (lambda (ex renamed)
                               (unless (bound-identifier-mapping-get new-bindings
                                                                     renamed
                                                                     (lambda () #f))
                                 (raise-syntax-error #f
                                                     (format "no definition for ~a identifier"
                                                             (case mode
                                                               [(#:only) "exported"]
                                                               [(#:all-defined-except) "excluded"]))
                                                     stx
                                                     ex)))
                             exports
                             exports-renamed)
                   (let-values ([(exports exports-renamed)
                                 (if (memq mode '(#:only #:begin))
                                     (values exports exports-renamed)
                                     (let ([all-exports-renamed (complement new-bindings exports-renamed)])
                                       ;; In case of define*, get only the last definition:
                                       (let ([tmp (make-bound-identifier-mapping)])
                                         (for-each (lambda (id)
                                                     (bound-identifier-mapping-put!
                                                      tmp
                                                      ((add-package-context def-ctxes) 
                                                       (pre-package-id id def-ctxes))
                                                      #t))
                                                   all-exports-renamed)
                                         (let* ([exports-renamed (bound-identifier-mapping-map tmp (lambda (k v) k))]
                                                [exports (map (lambda (id) (pre-package-id id def-ctxes))
                                                              exports-renamed)])
                                           (values exports exports-renamed)))))])
                     (with-syntax ([(export ...) exports]
                                   [(renamed ...) exports-renamed]
                                   [(hidden ...) (complement new-bindings exports-renamed)])
                       (let ([body (map (fixup-sub-package exports-renamed defined-renamed def-ctxes) 
                                        (reverse rev-forms))])
                         (if (eq? mode '#:begin)
                             (if (eq? 'expression (syntax-local-context))
                                 (quasisyntax/loc stx (let () #,@body))
                                 (quasisyntax/loc stx (begin #,@body)))
                             (quasisyntax/loc stx
                               (begin
                                 #,@(if (eq? 'top-level (syntax-local-context))
                                        ;; delcare all bindings before they are used:
                                        #`((define-syntaxes #,defined-renamed (values)))
                                        null)
                                 #,@body
                                 (define-syntax pack-id
                                   (make-package
                                    (lambda ()
                                      (list (cons (quote-syntax export)
                                                  (quote-syntax renamed))
                                            ...))
                                    (lambda ()
                                      (list (quote-syntax hidden) ...)))))))))))]
                [else
                 (let ([expr (local-expand (car exprs)
                                           ctx
                                           kernel-forms 
                                           def-ctxes)])
                   (syntax-case expr (begin)
                     [(begin . rest)
                      (loop (append (flatten-begin expr) (cdr exprs))
                            rev-forms
                            def-ctxes)]
                     [(def (id ...) rhs)
                      (and (or (free-identifier=? #'def #'define-syntaxes)
                               (free-identifier=? #'def #'-define*-syntaxes))
                           (andmap identifier? (syntax->list #'(id ...))))
                      (with-syntax ([rhs (local-transformer-expand
                                          #'rhs
                                          'expression
                                          null)])
                        (let ([star? (free-identifier=? #'def #'-define*-syntaxes)]
                              [ids (syntax->list #'(id ...))])
                          (let* ([def-ctx (if star?
                                              (syntax-local-make-definition-context (car def-ctxes))
                                              (last def-ctxes))]
                                 [ids (if star? 
                                          (map (add-package-context (list def-ctx)) ids)
                                          ids)])
                            (syntax-local-bind-syntaxes ids #'rhs def-ctx)
                            (register-bindings! ids)
                            (loop (cdr exprs)
                                  (cons (move-props expr #`(define-syntaxes #,ids rhs))
                                        rev-forms)
                                  (if star? (cons def-ctx def-ctxes) def-ctxes)))))]
                     [(def (id ...) rhs)
                      (and (or (free-identifier=? #'def #'define-values)
                               (free-identifier=? #'def #'-define*-values))
                           (andmap identifier? (syntax->list #'(id ...))))
                      (let ([star? (free-identifier=? #'def #'-define*-values)]
                            [ids (syntax->list #'(id ...))])
                        (let* ([def-ctx (if star?
                                            (syntax-local-make-definition-context (car def-ctxes))
                                            (last def-ctxes))]
                               [ids (if star? 
                                        (map (add-package-context (list def-ctx)) ids)
                                        ids)])
                          (syntax-local-bind-syntaxes ids #f def-ctx)
                          (register-bindings! ids)
                          (loop (cdr exprs)
                                (cons (move-props expr #`(define-values #,ids rhs)) rev-forms)
                                (if star? (cons def-ctx def-ctxes) def-ctxes))))]
                     [else
                      (loop (cdr exprs) 
                            (cons (if (and (eq? mode '#:begin)
                                           (null? (cdr exprs)))
                                      expr
                                      #`(define-values () (begin #,expr (values))))
                                  rev-forms)
                            def-ctxes)]))]))))))]))

(define-syntax (define-package stx)
  (syntax-case stx ()
    [(_ id #:all-defined form ...)
     (do-define-package stx #'(define-package id #:all-defined () form ...))]
    [(_ id #:all-defined-except ids form ...)
     (do-define-package stx stx)]
    [(_ id #:only ids form ...)
     (do-define-package stx stx)]
    [(_ id ids form ...)
     (do-define-package stx #'(define-package id #:only ids form ...))]))

(define-syntax (package-begin stx)
  (syntax-case stx ()
    [(_ form ...)
     (do-define-package stx #'(define-package #f #:begin () form ...))]))

(define-for-syntax (do-open stx define-syntaxes-id)
  (syntax-case stx ()
    [(_ pack-id)
     (let ([id #'pack-id])
       (unless (identifier? id)
         (raise-syntax-error #f
                             "expected an identifier for a package"
                             stx
                             id))
       (let ([v (syntax-local-value id (lambda () #f))])
         (unless (package? v)
           (raise-syntax-error #f
                               "identifier is not bound to a package"
                               stx
                               id))
         (let ([introduce (syntax-local-make-delta-introducer 
                           (syntax-local-introduce id))])
           (with-syntax ([(intro ...)
                          (map (lambda (i)
                                 (syntax-local-introduce
                                  (syntax-local-get-shadower
                                   (introduce i))))
                               (map car ((package-exports v))))]
                         [(defined ...)
                          (map (lambda (v) (syntax-local-introduce (cdr v)))
                               ((package-exports v)))]
                         [((a . b) ...) (map (lambda (p)
                                               (cons (syntax-local-introduce (car p))
                                                     (syntax-local-introduce (cdr p))))
                                             ((package-exports v)))]
                         [(h ...) (map syntax-local-introduce ((package-hidden v)))])
             (syntax-property
              #`(#,define-syntaxes-id (intro ...)
                  (let ([rev-map (lambda (x)
                                   (reverse-mapping
                                    'pack-id
                                    x
                                    (list (cons (quote-syntax a)
                                                (quote-syntax b))
                                          ...)
                                    (list (quote-syntax h) ...)))])
                    (values (make-rename-transformer #'defined rev-map)
                            ...)))
              'disappeared-use
              (syntax-local-introduce id))))))]))

(define-syntax (open-package stx)
  (do-open stx #'define-syntaxes))
(define-syntax (open*-package stx)
  (do-open stx #'define*-syntaxes))

(define-for-syntax (package-exported-identifiers id)
  (let ([v (and (identifier? id)
                (syntax-local-value id (lambda () #f)))])
    (unless (package? v)
      (raise-type-error 'package-exported-identifiers "identifier bound to a package" id))
    (let ([introduce (syntax-local-make-delta-introducer 
                      (syntax-local-introduce id))])
      (map (lambda (i)
             (syntax-local-introduce
              (syntax-local-get-shadower
               (introduce (car i)))))
           ((package-exports v))))))

(define-for-syntax (package-original-identifiers id)
  (let ([v (and (identifier? id)
                (syntax-local-value id (lambda () #f)))])
    (unless (package? v)
      (raise-type-error 'package-exported-identifiers "identifier bound to a package" id))
    (map cdr ((package-exports v)))))
