;;; asdf-generator: generate ASDF .asd files based solely on a list of
;;; system dependencies and files.  Heuristically finds definitions
;;; within the body of code and dependencies between files in the
;;; system, and then generates proper :depends-on clauses.

;;; The first obvious weakness of this is that it doesn't handle
;;; defpackages and in-packages at all, so the reader will intern
;;; unscoped symbols into the wrong package and will utterly fail on
;;; symbols which explicitly reference packages defined by this
;;; system.

(defpackage :net.a1k0n.asdf-generator
  (:nicknames :asdfgen)
  (:use :common-lisp)
  (:export make-asdf)
)


(in-package :net.a1k0n.asdf-generator)

(defvar *source-file*)
(defvar *definitions*)
(defvar *dependencies*)

(defun add-definition (what where srcform)
  (setf (gethash what *definitions*) (cons where srcform))
)


(defun add-dependency (file depends-on)
  (unless (string= file depends-on)
    (pushnew depends-on (gethash file *dependencies*) :test #'equal)
)
)


(defun scan-code-defs (code)
  ;; dumb heuristic: assume the only way to define anything is through
 ;; a function or macro called "DEF*" called at any point in the
 ;; code.
 (when (and (listp code)
             (symbolp (first code))
)

    (let ((fn-name (symbol-name (first code))))
      (if (and (> (length fn-name) 3)
               (string= (subseq (symbol-name (first code)) 0 3) "DEF")
)

          (add-definition (second code) *source-file* (first code))
          (dolist (c code)
            (scan-code-defs c)
)
)
)
)
)


(defun scan-code-deps (code)
  (cond
    ;; handle things like '(foo . 5) by making sure the cdr is also a list
   ((and (listp code)
          (listp (cdr code))
)

     ;; and don't worry about definitions of quoted things
    (unless (eql (car code) 'quote)
       (dolist (c code)
         (scan-code-deps c)
)
)
)

    ((symbolp code)
     (let ((def-location (gethash code *definitions*)))
       (when def-location
         (add-dependency *source-file* (car def-location))
)
)
)
)
)


;; TODO: actually use Common Lisp's pathname system here
(defun scan-file (pathname fname scan-function)
  (let ((*source-file* fname))
    (with-open-file (srcfile (format nil "~a/~a.lisp" pathname fname) :direction :input)
      (loop for sexp = (read srcfile nil)
            while sexp
            do (funcall scan-function sexp)
)
)
)
)


(defun gen-dependencies (pathname file-list)
  (let ((*definitions* (make-hash-table :test #'equal))
        (*dependencies* (make-hash-table :test #'equal))
)

    (format t "Scanning ~a for ~s..." pathname file-list)
    ;; scan through all the code and record all definition forms we
   ;; can find.
   (dolist (file file-list)
      (scan-file pathname file #'scan-code-defs)
)

    ;; scan through all the code and look for all uses of any of the
   ;; definitions we've found, saving dependencies along the way.
   (dolist (file file-list)
      (scan-file pathname file #'scan-code-deps)
)

    ;;(describe *definitions*)
   ;;(describe *dependencies*)
   *dependencies*
)
)


;; TODO: detect and verbosely warn about circular dependencies.

;; XXX: I don't see any way around the eval here, since defsystem
;; quotes its arguments, and I need to do the file scanning at load
;; time because that's when *load-truename* is set, and I don't want
;; to rely on *compile-file-truename* being set because ASDF just uses
;; LOAD and not COMPILE.
(defmacro make-asdf (system-name system-dependencies file-list)
  `(let ((file-list ',file-list)
         (system-dependencies ',system-dependencies)
         (system-name ',system-name)
)

    (dolist (x system-dependencies)
      (asdf:oos 'asdf:load-op x)
)

    (let* ((pathname (asdf::pathname-sans-name+type
                      (asdf::resolve-symlinks *load-truename*)
)
)

           (deps (gen-dependencies pathname file-list))
           (components (loop for file in file-list
           collect (let ((file-deps (gethash file deps)))
                     (if file-deps
                         `(:file ,file :depends-on ,file-deps)
                         `(:file ,file)
)
)
)
)
)

      (eval `(asdf:defsystem ,system-name
              :components ,components
              :depends-on ,system-dependencies
)
)
)
)
)