(module compile mzscheme
  (require "parameters.ss" "ast.ss" "types.ss" "parser.ss" "build-info.ss" "check.ss" "to-scheme.ss" "profj-pref.ss")
  (require (lib "list.ss")
           (lib "file.ss")
           (lib "class.ss"))

  (provide compile-java compile-interactions compile-files compile-ast compile-interactions-ast
           compilation-unit-code compilation-unit-contains set-compilation-unit-code!
           read-record write-record
           set-syntax-location create-type-record
           )
  
  (define (set-syntax-location so) (syntax-location so))
  
  (define (create-type-record)
    (let ((t (make-object type-records)))
      (set-importer! t find-implicit-import)
      (classpath (get-classpath))
      (load-lang t)
      t))
  
  ;kind = 'file | 'port
  ;level = 'beginner | 'intermediate | 'advanced | 'full
  
  ;compile: kind kind level (U #f string) (U #f port) (U #f location) -> (U (list compilation-unit) void)
  (define (compile-java src dest level name port loc . type-recs)
    (when (and (eq? src 'file)
               (not (file-exists? name)))
      (error 'compile-java "compile-java given file that does not exist: ~a" name))
    (when (and (eq? src 'file)
               (path? name)
               (regexp-match #".djava$" (path->bytes name)))
      (dynamic? #t))
    (when (null? (classpath)) (get-classpath))
    (let ((type-recs (if (null? type-recs)
                         (make-object type-records)
                         (car type-recs))))
      (cond
        ((and (eq? src 'file) (eq? dest 'file))
         (let-values  (((path-base file dir?) (split-path (path->complete-path (build-path name)))))
           (let ((compiled-path (build-path path-base "compiled" (path-replace-suffix file ".zo")))
                 (type-path (build-path path-base "compiled" (path-replace-suffix file ".jinfo"))))
             (unless #f #;(and (and (file-exists? compiled-path)
                              (> (file-or-directory-modify-seconds compiled-path)
                                 (file-or-directory-modify-seconds (build-path name))))
                         (and (file-exists? type-path)
                              (read-record type-path)))
               (call-with-input-file name (lambda (port) (compile-to-file port name level)))))))
        ((eq? dest 'file)
         (compile-to-file port loc level))
        ((eq? src 'file)
         (let-values  (((path-base file dir?) (split-path (path->complete-path (build-path name)))))
           (let ((compiled-path (build-path path-base "compiled" (path-replace-suffix file ".zo")))
                 (type-path (build-path path-base "compiled" (path-replace-suffix file ".jinfo"))))
             (unless (or (and (file-exists? compiled-path)
                              (> (file-or-directory-modify-seconds compiled-path)
                                 (file-or-directory-modify-seconds (build-path name))))
                         (and (file-exists? type-path)
                              (read-record type-path)))
               (call-with-input-file 
                   name
                 (lambda (port) (compile-java-internal port name type-recs #f level)))))))
        (else
         (compile-java-internal port loc type-recs #f level)))))
    
  ;compile-to-file: port location level -> void
  ;Should have side-effect of writing to file all files needed for compilation
  (define (compile-to-file port location level)
    (let ((type-recs (make-object type-records)))
      (send type-recs set-compilation-location
            location 
            (if (equal? (file-name-from-path location) location)
                (build-path "compiled")
                (build-path (substring (path->string location) 0 
                                       (- (string-length (path->string location))
                                          (string-length (path->string (file-name-from-path location))))) "compiled")))
      (for-each (lambda (dependents)
                  (let ((names (compilation-unit-contains dependents))
                        (syntaxes (compilation-unit-code dependents))
                        (locations (compilation-unit-locations dependents)))
                    (unless (= (length names) (length syntaxes))
                      ;(printf "Writing a composite file out~n")
                      ;(printf "~a~n~n" (syntax-object->datum (car syntaxes)))
                      (call-with-output-file* (build-path (send type-recs get-compilation-location)
                                                          (file-name-from-path
                                                           (send type-recs get-composite-location (car names))))
                                              (lambda (port) (write (compile (car syntaxes)) port)) 'truncate/replace)
                      (set! syntaxes (cdr syntaxes)))
                    (unless (= (length names) (length syntaxes) (length locations))
                      (error 'compile-to-file "Internal error: compilation unit not represented as expected"))
                    (for-each (lambda (name code location)
                                ;(printf "~a~n~n" (syntax-object->datum code))
                                (send type-recs set-location! location)
                                (let ((directory (send type-recs get-compilation-location)))
                                  (unless (directory-exists? directory) (make-directory directory))
                                  (call-with-output-file* (build-path directory (string-append name ".zo"))
                                                          (lambda (port) (write (compile code) port))
                                                          'truncate/replace)
                                  (call-with-output-file* (build-path directory (string-append name ".jinfo"))
                                                          (lambda (port) (write-record (send type-recs get-class-record 
                                                                                             (list name)
                                                                                             #f
                                                                                             class-record-error)
                                                                                       port))
                                                          'truncate/replace)))
                              names syntaxes locations)))
                (compile-java-internal port location type-recs #t level))))
  
  (define (class-record-error) (error 'compile-to-file "Internal error: class record not found"))
  
  ;package: (list string)
  
  ;compile-files: (list (list (list path) package)) boolean symbol ->
  ;               (list (list package (list (list compiliation-unit)) (list class-record)))
  (define (compile-files files to-file? level)
    (when (null? (classpath)) (classpath (get-classpath)))
    (let ((type-recs (make-object type-records))
          (get-class-names 
           (lambda (files)
             (map (lambda (f) (path->string (path-replace-suffix (file-name-from-path f) "")))
                  files))))
      (map (lambda (package-files)
             (let* ((files (car package-files))
                    (package-name (cadr package-files))
                    (class-names (get-class-names files)))
               (list package-name
                     (filter (lambda (t) t)
                             (map (lambda (file class)
                                    (let ((existing-record (send type-recs get-class-record (cons class package-name) #f
                                                                 (lambda () #f))))
                                      (and (or (not existing-record) 
                                               (procedure? existing-record))
                                           (call-with-input-file file 
                                             (lambda (port) (compile-java-internal port file type-recs to-file? level))))))
                                  files class-names))
                     (map (lambda (class)
                            (send type-recs get-class-record (cons class package-name) #f (lambda () (error 'internal-error))))
                          class-names))))
           files)))
  
  (define (compile-ast ast level type-recs)
    (packages null)
    (check-list null)
    (to-file #f)
    (load-lang type-recs)
    (build-info ast level type-recs #f)
    (unless (null? (check-list))
      (check-defs (car (check-list)) level type-recs))
    (remove-from-packages ast type-recs)
    (order-cus (translate-program ast type-recs) type-recs))
  
  ;compile-java-internal: port location type-records bool level-> (list compilation-unit)
  (define (compile-java-internal port location type-recs file? level)
    (packages null)
    (check-list null)
    (to-file file?)
    (let ((ast (parse port location level)))
      (remember-main ast)
      (load-lang type-recs)
      (set-importer! type-recs find-implicit-import)
      (build-info ast level type-recs #f)
      (unless (null? (check-list))
        (check-defs (car (check-list)) level type-recs))
      (remove-from-packages ast type-recs)
      (order-cus (translate-program ast type-recs) type-recs)))
  
  ;compile-interactions: port location type-records level -> syntax
  (define (compile-interactions port location type-recs level)
    (to-file #f)
    (let ((ast (parse-interactions port location level)))
      (if (null? ast)
          (datum->syntax-object #f '(void) #f)
          (begin 
            (build-interactions-info ast level location type-recs)
            (check-interactions-types ast level location type-recs)
            (translate-interactions ast location type-recs #t)))))
      
  (define (compile-interactions-ast ast location level type-recs gen-require?)
    (to-file #f)
    (if (null? ast)
        (datum->syntax-object #f '(void) #f)
        (begin
          (build-interactions-info ast level location type-recs)
          (check-interactions-types ast level location type-recs)
          (translate-interactions ast location type-recs gen-require?))))
  
  (define-struct elt (prev val next))
  
  (define fifo
    (class* object% ()
      (define head null)
      (define tail null)
      
      (define/public (empty?)
        (and (null? head) (null? tail)))
      
      (define/public (pop)
        (let ((old-head head))
          (set! head (elt-next head))
          (when (null? head) 
            (set! tail null))
          (unless (null? head)
            (set-elt-prev! head null))
          (elt-val old-head)))
      (define/public (push e)
        (let ((new-elt (make-elt tail e null)))
          (if (empty?)
              (begin (set! head new-elt)
                     (set! tail head))
              (begin
                (set-elt-next! tail new-elt)
                (set! tail new-elt)))))
      (super-instantiate ())))
  
  (define (make-queue) (make-object fifo))
  (define (empty-queue? q) (send q empty?))
  (define (add-to-work-queue q elts) (for-each (lambda (e) (send q push e)) elts))
  (define (queue-head q) (send q pop))

  ;split-cu (list compilation-unit) (list compilation-unit) (list compilation-unit) (lis compilation-unit) type-records 
  ;          -> (values (list compilation-unit) (list compilation-unit)
  (define (split-cu cus cus-full with-depends without-depends type-recs)
    (if (null? cus)
        (values with-depends without-depends)
        (if (null? (get-local-depends (compilation-unit-depends (car cus)) cus-full type-recs))
            (split-cu (cdr cus) cus-full with-depends (cons (car cus) without-depends) type-recs)
            (split-cu (cdr cus) cus-full (cons (car cus) with-depends) without-depends type-recs))))
  
  ;ok-to-add? compilation-unit (list compilation-unit) type-records -> bool
  (define (ok-to-add? cu cus cus-full type-recs)
    (andmap (lambda (depends-on)
              (or (is-in? depends-on cu type-recs)
                  (dependency-satisfied? depends-on cus type-recs)))
            (get-local-depends (compilation-unit-depends cu) cus-full type-recs)))
  
  ;dependency-satisfied? req (list compilation-unit) type-records -> bool
  (define (dependency-satisfied? depends-on cus type-recs)
    (and (not (null? cus))
         (or (is-in? depends-on (car cus) type-recs)
             (dependency-satisfied? depends-on (cdr cus) type-recs))))
  
  ;get-local-depends: (list req) (list compilation-unit) type-records -> (list req)
  (define (get-local-depends reqs cus type-recs)
    (if (null? reqs)
        null
        (if (ormap (lambda (cu) (is-in? (car reqs) cu type-recs)) cus)
            (cons (car reqs) (get-local-depends (cdr reqs) cus type-recs))
            (get-local-depends (cdr reqs) cus type-recs))))
  
  ;is-in? req compilation-unit type-records -> bool
  (define (is-in? class cu type-recs)
    (and (member (req-class class) (compilation-unit-contains cu))
         (equal? (req-path class)
                 (begin
                   (send type-recs set-location! (list-ref (compilation-unit-locations cu)
                                                           (get-position (req-class class) (compilation-unit-contains cu) 0)))
                   (send type-recs lookup-path (req-class class) (lambda () (error 'internal-error)))))))
  
  ;get-position: 'a (list 'a) int -> int
  (define (get-position name names pos)
    (if (or (null? (cdr names))
            (equal? name (car names)))
        pos
        (get-position name (cdr names) (add1 pos))))
  
  ;order-cus (list compilation-unit) type-records -> (list compilation-unit)
  (define (order-cus cus type-recs)
    (let-values (((work-list ordered) (split-cu cus cus null null type-recs)))
      ;(printf "order-cus: ~a ~a ~a ~n" (length cus) (length work-list) (length ordered)) 
      (unless (null? work-list)
        (let ((queue (make-queue)))
          (for-each (lambda (cu) (send queue push cu)) work-list)
          (let loop ()
            ;(printf "looping in order-cus: ~a ~a ~n" (empty-queue? queue) (length ordered))
            (unless (empty-queue? queue)
              (let ((cu (send queue pop)))
                (if (ok-to-add? cu ordered cus type-recs)
                    (set! ordered (cons cu ordered))
                    (send queue push cu)))
              (loop)))))
      (reverse ordered)))
  
  (define (remove-from-packages ast type-recs) 
    (packages (filter (lambda (def) (not (contained-in? def (package-defs ast))))
                      (packages))))

  (define (contained-in? def defs)
    (and (not (null? defs))
         (or (eq? def (car defs))
             (contained-in? def (cdr defs)))))
  
  (define (contains-main? members)
    (and (not (null? members))
         (or (and (method? (car members))
                  (equal? "main" (id-string (method-name (car members)))))
             (contains-main? (cdr members)))))
  
  (define (remember-main ast)
    (let ((main-class (filter (lambda (def)
                                (memq 'public 
                                      (map modifier-kind (header-modifiers (def-header def)))))
                              (filter class-def?
                                      (package-defs ast)))))
      (if (null? main-class)
          (main (list #f null))
          (main (list (contains-main? (def-members (car main-class)))
                      (id-string (header-id (def-header (car main-class)))))))))
    
  )

