#lang scheme/base

(module test racket/base)

;; On error, exit with 1 status code
(error-escape-handler (lambda () (exit 1)))

(error-print-width 512)

(require (prefix-in compiler:option: compiler/option)
         compiler/compiler
         compiler/distribute
         compiler/private/language
         raco/command-name
         racket/cmdline
         dynext/file
         dynext/compile
         dynext/link
         scheme/pretty
         setup/pack
         setup/getinfo
         setup/dirs
         racket/file
         racket/port)

(define dest-dir (make-parameter #f))

(define ld-output (make-parameter #f))

(define exe-output (make-parameter #f))
(define exe-embedded-flags (make-parameter '("-U" "--")))
(define exe-embedded-libraries (make-parameter null))
(define exe-embedded-languages (make-parameter null))
(define exe-aux (make-parameter null))
(define exe-embedded-collects-path (make-parameter #f))
(define exe-embedded-collects-dest (make-parameter #f))
(define exe-dir-add-collects-dirs (make-parameter null))

(define exe-dir-output (make-parameter #f))

(define mods-output (make-parameter #f))
(define runtime-files-dir (make-parameter #f))
(define runtime-files-relative-path (make-parameter #f))

(define default-plt-name "archive")

(define disable-inlining (make-parameter #f))

(define plt-output (make-parameter #f))
(define plt-name (make-parameter default-plt-name))
(define plt-files-replace (make-parameter #f))
(define plt-files-plt-relative? (make-parameter #f))
(define plt-files-plt-home-relative? (make-parameter #f))
(define plt-force-install-dir? (make-parameter #f))
(define plt-setup-collections (make-parameter null))
(define plt-include-compiled (make-parameter #f))

(define stop-at-source (make-parameter #f))

(define (extract-suffix appender)
  (bytes->string/latin-1
   (subbytes (path->bytes (appender (bytes->path #"x"))) 1)))

(define ((add-to-param param) f v) (param (append (param) (list v))))

(define mzc-symbol (string->symbol (short-program+command-name)))

;; Returns (values mode files prefixes)
;;  where mode is 'compile, 'make-zo, etc.
(define-values (mode source-files prefix)
  (parse-command-line
   (short-program+command-name)
   (current-command-line-arguments)
   `([help-labels
      "-------------------------------- mode flags ---------------------------------"]
     [once-any
      [("--cc")
       ,(lambda (f) 'cc)
       (,(format "Compile arbitrary file(s) for an extension: ~a -> ~a"
                 (extract-suffix append-c-suffix)
                 (extract-suffix append-object-suffix)))]
      [("--ld")
       ,(lambda (f name) (ld-output name) 'ld)
       (,(format "Link arbitrary file(s) to create <extension>: ~a -> ~a"
                 (extract-suffix append-object-suffix)
                 (extract-suffix append-extension-suffix))
        "extension")]
      [("-x" "--xform")
       ,(lambda (f) 'xform)
       ((,(format "Convert for 3m compilation: ~a -> ~a"
                  (extract-suffix append-c-suffix)
                  (extract-suffix append-c-suffix))
         ""))]
      [("--c-mods")
       ,(lambda (f name) (mods-output name) 'c-mods)
       ((,(format "Write C-embeddable compiled modules to <file>") "")
        "file")]
      [("--mods")
       ,(lambda (f name) (mods-output name) 'mods)
       ((,(format "Write raw embeddable compiled modules to <file>") "")
        "file")]]
     [help-labels ""]
     [once-any
      [("--3m")
       ,(lambda (f) (compiler:option:3m #t))
       (,(format "Compile/link for 3m~a"
                 (if (eq? '3m (system-type 'gc)) " [current default]" "")))]
      [("--cgc")
       ,(lambda (f) (compiler:option:3m #f))
       (,(format "Compile/link for CGC~a"
                 (if (eq? 'cgc (system-type 'gc)) " [current default]" "")))]]
     [once-each
      [("-n" "--name")
       ,(lambda (f name) (compiler:option:setup-prefix name))
       ("Use <name> as extra part of public low-level names" "name")]]
     [once-any
      [("-d" "--destination")
       ,(lambda (f d)
          (unless (directory-exists? d)
            (error mzc-symbol "the destination directory does not exist: ~s" d))
          (dest-dir d))
       ("Output --cc/--ld/-x file(s) to <dir>" "dir")]]
     [help-labels
      "------------------- compiler/linker configuration flags ---------------------"]
     [once-each
      [("--tool")
       ,(lambda (f v)
          (let ([v (string->symbol v)])
            (use-standard-compiler v)
            (use-standard-linker v)))
       (,(format "Use pre-defined <tool> as C compiler/linker:~a"
                 (apply string-append
                        (apply append (map (lambda (t)
                                             (list " " (symbol->string t)))
                                           (get-standard-compilers)))))
        "tool")]
      [("--compiler")
       ,(lambda (f v) (current-extension-compiler v))
       ("Use <compiler-path> as C compiler" "compiler-path")]]
     [multi
      [("++ccf")
       ,(add-to-param current-extension-compiler-flags)
       ("Add C compiler flag" "flag")]
      [("--ccf")
       ,(lambda (f v)
          (current-extension-compiler-flags
           (remove v (current-extension-compiler-flags))))
       ("Remove C compiler flag" "flag")]
      [("--ccf-clear")
       ,(lambda (f) (current-extension-compiler-flags null))
       ("Clear C compiler flags")]
      [("--ccf-show")
       ,(lambda (f)
          (printf "C compiler flags: ~s\n"
                  (expand-for-link-variant (current-extension-compiler-flags))))
       ("Show C compiler flags")]]
     [once-each
      [("--linker")
       ,(lambda (f v) (current-extension-linker v))
       ("Use <linker-path> as C linker" "linker-path")]]
     [multi
      [("++ldf")
       ,(add-to-param current-extension-linker-flags)
       ("Add C linker flag" "flag")]
      [("--ldf")
       ,(lambda (f v)
          (current-extension-linker-flags
           (remove v (current-extension-linker-flags))))
       ("Remove C linker flag" "flag")]
      [("--ldf-clear")
       ,(lambda (f) (current-extension-linker-flags null))
       ("Clear C linker flags")]
      [("--ldf-show")
       ,(lambda (f)
          (printf "C linker flags: ~s\n"
                  (expand-for-link-variant (current-extension-linker-flags))))
       ("Show C linker flags")]
      [("++ldl")
       ,(add-to-param current-standard-link-libraries)
       ("Add C linker library" "lib")]
      [("--ldl-show")
       ,(lambda (f)
          (printf "C linker libraries: ~s\n"
                  (expand-for-link-variant (current-standard-link-libraries))))
       ("Show C linker libraries")]]
     [multi
      [("++cppf")
       ,(add-to-param current-extension-preprocess-flags)
       ("Add C preprocess (xform) flag" "flag")]
      [("--cppf")
       ,(lambda (f v)
          (current-extension-preprocess-flags
           (remove v (current-extension-preprocess-flags))))
       ("Remove C preprocess (xform) flag" "flag")]
      [("--cppf-clear")
       ,(lambda (f) (current-extension-preprocess-flags null))
       ("Clear C preprocess (xform) flags")]
      [("--cppf-show")
       ,(lambda (f)
          (printf "C compiler flags: ~s\n"
                  (expand-for-link-variant (current-extension-preprocess-flags))))
       ("Show C preprocess (xform) flags")]]
     [help-labels
      "----------------------- C-embeddable module flags ---------------------------"]
     [multi
      [("++lang")
       ,(lambda (f l)
          (exe-embedded-languages (append (exe-embedded-languages) (list l))))
       ("Embed support for `#lang <lang>` in `--c-mods` or `--mods` output" "lang")]
      [("++lib")
       ,(lambda (f l)
          (exe-embedded-libraries (append (exe-embedded-libraries) (list l))))
       ("Embed <lib> in `--c-mods` or `--mods` output" "lib")]]
     [once-each
      [("--runtime")
       ,(lambda (f dir)
          (runtime-files-dir dir))
       ("Gather runtime files to <dir>" "dir")]
      [("--runtime-access")
       ,(lambda (f path)
          (runtime-files-relative-path path))
       ("Use <path> to reference runtime files at run time" "path")]]
     [help-labels
      "-------------------------- miscellaneous flags ------------------------------"]
     [once-each
      [("-v")
       ,(lambda (f) (compiler:option:somewhat-verbose #t))
       ("Slightly verbose mode, including version banner and output files")]
      [("--vv")
       ,(lambda (f) (compiler:option:somewhat-verbose #t) (compiler:option:verbose #t))
       ("Very verbose mode")]])
   (lambda (accum . files)
     (let ([mode (let ([l (filter symbol? accum)])
                   (if (null? l)
                       (error mzc-symbol "no mode flag specified")
                       (car l)))])
       (values
        mode
        files
        #f)))
   (list "file")))

(when (compiler:option:somewhat-verbose)
  (printf "~a v~a [~a], Copyright (c) 2004-2014 PLT Design Inc.\n"
          (short-program+command-name)
          (version)
          (system-type 'gc)))

(if (compiler:option:3m)
  (begin (link-variant '3m)  (compile-variant '3m))
  (begin (link-variant 'cgc) (compile-variant 'cgc)))

(define (compiler-warning)
  (eprintf "Warning: ~a\n         ~a\n"
           "compilation to C is usually less effective for performance"
           "than relying on the bytecode just-in-time compiler."))

(case mode
  [(cc)
   (for ([file source-files])
     (let* ([base (extract-base-filename/c file mzc-symbol)]
            [dest (append-object-suffix
                   (let-values ([(base name dir?) (split-path base)])
                     (build-path (or (dest-dir) 'same) name)))])
       (when (compiler:option:somewhat-verbose)
         (printf "\"~a\":\n" file))
       (compile-extension (not (compiler:option:verbose)) file dest null)
       (when (compiler:option:somewhat-verbose)
         (printf " [output to \"~a\"]\n" dest))))]
  [(ld)
   (extract-base-filename/ext (ld-output) mzc-symbol)
   ;; (for ([file source-files]) (extract-base-filename/o file mzc-symbol))
   (let ([dest (if (dest-dir)
                 (build-path (dest-dir) (ld-output))
                 (ld-output))])
     (when (compiler:option:somewhat-verbose)
       (printf "~a:\n" (let ([s (apply string-append
                                       (map (lambda (n) (format " \"~a\"" n))
                                            source-files))])
                         (substring s 1 (string-length s)))))
     (link-extension (not (compiler:option:verbose))
                     source-files
                     dest)
     (when (compiler:option:somewhat-verbose)
       (printf " [output to \"~a\"]\n" dest)))]
  [(xform)
   (for ([file source-files])
     (let* ([out-file (path-replace-suffix file ".3m.c")]
            [out-file  (if (dest-dir)
                         (build-path (dest-dir) out-file)
                         out-file)])
       ((dynamic-require 'compiler/xform 'xform)
        (not (compiler:option:verbose))
        file
        out-file
        (list (find-include-dir)))
       (when (compiler:option:somewhat-verbose)
         (printf " [output to \"~a\"]\n" out-file))))]
  [(c-mods mods)
   (let ([dest (mods-output)])
     (let-values ([(in out) (make-pipe)])
       (parameterize ([current-output-port out])
         ((dynamic-require 'compiler/embed 'write-module-bundle)
          #:verbose? (compiler:option:verbose)
          #:modules
          (append (map (lambda (l) `(#f (file ,l))) source-files)
                  (map (lambda (l) `(#t (lib ,l))) (exe-embedded-libraries))
                  (map (lambda (l) `(#t ,l))
                       (languages->libraries
                        (exe-embedded-languages)
                        #:who (string->symbol (short-program+command-name)))))))
       (close-output-port out)
       (let ([out (open-output-file dest #:exists 'truncate/replace)]
             [in (if (runtime-files-dir)
                     ;; Assumble runtime files to `runtime-files-dir`,
                     ;; and update the module bundle to refer to
                     ;; runtime files there:
                     (let ([tmp (make-temporary-file "ctool~a")])
                       (call-with-output-file*
                        tmp #:exists 'truncate
                        (lambda (o) (copy-port in o)))
                       (assemble-distribution (runtime-files-dir)
                                              (list tmp)
                                              #:relative-base (runtime-files-relative-path)
                                              #:executables? #f)
                       (begin0
                        (open-input-bytes (file->bytes tmp))
                        (delete-file tmp)))
                     in)])
         (case mode
           [(mods)
            (copy-port in out)]
           [else
            (fprintf out "#ifdef MZ_XFORM\n")
            (fprintf out "XFORM_START_SKIP;\n")
            (fprintf out "#endif\n")
            (fprintf out "static void declare_modules(~a) {\n"
                     (case (system-type 'vm)
                       [(racket) "Scheme_Env *env"]
                       [else ""]))
            (fprintf out "  static unsigned char data[] = {")
            (let loop ([pos 0])
              (let ([b (read-byte in)])
                (when (zero? (modulo pos 20)) (fprintf out "\n    "))
                (unless (eof-object? b) (fprintf out "~a," b) (loop (add1 pos)))))
            (fprintf out "0\n  };\n")
            (case (system-type 'vm)
              [(racket)
               (fprintf out "  scheme_register_embedded_load(~a, (const char *)data);\n"
                        (file-position in))
               (fprintf out "  scheme_embedded_load(~a, (const char *)data, 1);\n"
                        (file-position in))]
              [else
               (fprintf out "  racket_embedded_load_bytes((const char *)data, ~a, 1);\n"
                        (file-position in))])
            (fprintf out "}\n")
            (fprintf out "#ifdef MZ_XFORM\n")
            (fprintf out "XFORM_END_SKIP;\n")
            (fprintf out "#endif\n")])
         (close-output-port out)))
     (when (compiler:option:somewhat-verbose)
       (printf " [output to \"~a\"]\n" dest)))]
  [else (printf "bad mode: ~a\n" mode)])
