
(module teachpack mzscheme
  (require (lib "unitsig.ss")
	   (lib "list.ss")
           (lib "file.ss")
           (lib "etc.ss")
	   (lib "framework.ss" "framework")
	   (lib "mred.ss" "mred")
	   (lib "string-constant.ss" "string-constants")
           "drsig.ss")

  (provide teachpack@)

  (define o (current-output-port))
  (define (oprintf . args) (apply fprintf o args))
  
  (define teachpack@
    (unit/sig drscheme:teachpack^
      (import)

      ;; type teachpack-cache = (make-teachpack-cache (listof cache-entry))
      ;; the timestamp indicates the last time this teachpack was loaded
      (define-struct teachpack-cache (tps))
      
      ;; type cache-entry = (make-cache-entry path)
      (define-struct cache-entry (filename))
      
      (define (cache-entry-require-spec cache-entry)
        (cache-entry-filename cache-entry))
      
      ;; new-teachpack-cache : -> teachpack-cache
      (define new-teachpack-cache
        (opt-lambda ([filenames '[]])
          (make-teachpack-cache (map make-cache-entry filenames))))
      
      ;; set-teachpack-cache-filenames! : teachpack-cache (listof string) -> void
      ;; this shouldn't remove all of the #fs.
      (define (set-teachpack-cache-filenames! teachpack-cache filenames)
        (set-teachpack-cache-tps!
         teachpack-cache
         (map (λ (filename) (make-cache-entry filename))
              filenames)))
      
      ;; install-teachpacks : teachpack-cache -> void
      ;; =User=
      ;; installs the loaded teachpacks
      ;; updates the cache, removing those teachpacks that failed to run
      ;; requires that none of the cache-entries have #fs in them.
      (define (install-teachpacks cache)
        (for-each install-teachpack (teachpack-cache-tps cache))
        (set-teachpack-cache-tps!
         cache
         (filter cache-entry-filename (teachpack-cache-tps cache))))
      
      ;; install-teachpack : cache-entry -> void
      ;; =User=
      ;; updates the cache-entry's filename to #f if the teachpack fails
      ;; to run properly
      (define (install-teachpack cache-entry)
        (let ([filename (cache-entry-filename cache-entry)])
          (with-handlers ([exn:fail?
                           (λ (exn)
                             (set-cache-entry-filename! cache-entry #f)
                             (show-teachpack-error filename exn))])
            (verify-no-new-exports filename)
            (namespace-require (cache-entry-require-spec cache-entry)))))

      ;; verify-no-new-exports : string -> void
      ;; ensures that the teachpack wouldn't override any thing in the user's namespace
      (define (verify-no-new-exports filename)
        (let ([exports (extract-provided-variables-from-module filename)]
              [ns-contents (namespace-mapped-symbols)]
              [ht (make-hash-table)])
          (for-each (λ (ns-sym) (hash-table-put! ht ns-sym #t)) ns-contents)
          (for-each (λ (expt)
                      (when (hash-table-get ht expt (λ () #f))
                        (error 'teachpack "export of ~a from ~s conflicts with already existing definitions"
                               expt filename)))
                    exports)))
      
      ;; extract-provided-variables-from-module : string -> (listof symbol)
      (define (extract-provided-variables-from-module filename)
        (let* ([module-stx 
                (parameterize ([current-namespace (make-namespace)])
                  (let-values ([(base name dir?) (split-path filename)])
                    (parameterize [(current-load-relative-directory base)
                                   (current-directory base)]
                      (expand 
                       (parameterize [(read-case-sensitive #f)
                                      (read-square-bracket-as-paren #t)
                                      (read-curly-brace-as-paren #t)
                                      (read-accept-box #t)
                                      (read-accept-compiled #t)
                                      (read-accept-bar-quote #t)
                                      (read-accept-graph #t)
                                      (read-decimal-as-inexact #t)
                                      (read-accept-dot #t)
                                      (read-accept-quasiquote #t)]
                         (call-with-input-file filename 
                           (λ (port) (read-syntax filename port))))))))]
               [var-prop (get-exported-names (syntax-property module-stx 'module-variable-provides))]
               [mac-prop (get-exported-names (syntax-property module-stx 'module-syntax-provides))])
          (append var-prop mac-prop)))
      
      ;; get-exported-names : module-variable-provides / module-syntax-provides info (see mz manual) -> (listof symbol)
      (define (get-exported-names names)
        (if names
            (map (λ (x)
                   (cond
                     [(symbol? x) x]
                     [(symbol? (cdr x)) (car x)]
                     [else (cadr x)]))
                 names)
            '()))
        
      ;; marshall-teachpack-cache : teachpack-cache -> writable
      (define (marshall-teachpack-cache cache)
        (map (λ (x) (path->bytes (cache-entry-filename x))) (teachpack-cache-tps cache)))
      
      ;; unmarshall-teachpack-cache : writable -> teachpack-cache
      (define (unmarshall-teachpack-cache lof)
        (make-teachpack-cache
         (if (and (list? lof)
                  (andmap bytes? lof))
             (map (λ (x) (make-cache-entry (bytes->path x))) lof)
             null)))
      
      ;; teachpack-cache-filenames : teachpack-cache -> (listof string)
      (define (teachpack-cache-filenames cache)
        (map cache-entry-filename (teachpack-cache-tps cache)))

      ;; teachpack-cache-filenames : teachpack-cache -> (listof string)
      (define (teachpack-cache-require-specs cache)
        (map (λ (x) (cache-entry-require-spec x))
             (teachpack-cache-tps cache)))
      
      ;; launcher-init-code : teachpack-cache -> sexp
      ;; constructs code to be put in  a module that loads the teachpacks.
      ;; used with launchers
      (define (launcher-init-code cache)
        `(begin
           (void)
           ,@(map (λ (ce)
                    (let ([req-spec (cache-entry-require-spec ce)])
                      (if (path? req-spec)
                          `(namespace-require (bytes->path ,(path->bytes req-spec)))
                          `(namespace-require ',req-spec))))
                  (teachpack-cache-tps cache))))
      
      ;; launcher-modules-to-embed : teachpack-cache -> (listof module-spec)
      ;; the modules to embed in a stand-alone executable.
      (define (launcher-modules-to-embed cache)
        (map (λ (ce) (cache-entry-require-spec ce))
             (teachpack-cache-tps cache)))

      ;; show-teachpack-error : string TST -> void
      ;; shows an error message for a bad teachpack.
      (define (show-teachpack-error tp-filename exn)
        (message-box 
         (string-constant teachpack-error-label)
         (string-append
          (format (string-constant teachpack-didnt-load)
                  tp-filename)
          (string #\newline)
          
          ;; should check for error trace and use that here (somehow)
          (if (exn? exn)
              (format "~a" (exn-message exn))
              (format "uncaught exception: ~s" exn))))))))
