(in-package #:quicklisp)

(defun show-wrapped-list (words &key (indent 4) (margin 60))
 (let ((*print-right-margin* margin)
       (*print-pretty* t)
       (*print-escape* nil)
       (prefix (make-string indent :initial-element #\Space)))
   (pprint-logical-block (nil words :per-line-prefix prefix)
     (pprint-fill *standard-output* (sort (copy-seq words) #'string<) nil))
   (fresh-line)
   (finish-output)))

(defun recursively-install (name)
  (labels ((recurse (name)
             (let ((system (find-system name)))
               (unless system
                 (error "Unknown system ~S" name))
               (ensure-installed system)
               (mapcar #'recurse (required-systems system))
               name)))
    (with-consistent-dists
      (recurse name))))

(defclass load-strategy ()
  ((name
    :initarg :name
    :accessor name)
   (asdf-systems
    :initarg :asdf-systems
    :accessor asdf-systems)
   (quicklisp-systems
    :initarg :quicklisp-systems
    :accessor quicklisp-systems)))

(defmethod print-object ((strategy load-strategy) stream)
  (print-unreadable-object (strategy stream :type t)
    (format stream "~S (~D asdf, ~D quicklisp)"
            (name strategy)
            (length (asdf-systems strategy))
            (length (quicklisp-systems strategy)))))

(defgeneric quicklisp-releases (strategy)
  (:method (strategy)
    (remove-duplicates (mapcar 'release (quicklisp-systems strategy)))))

(defgeneric quicklisp-release-table (strategy)
  (:method ((strategy load-strategy))
    (let ((table (make-hash-table)))
      (dolist (system (quicklisp-systems strategy))
        (push system (gethash (release system) table nil)))
      table)))

(define-condition system-not-found (error)
  ((name
    :initarg :name
    :reader system-not-found-name))
  (:report (lambda (condition stream)
             (format stream "System ~S not found"
                     (system-not-found-name condition))))
  (:documentation "This condition is signaled by QUICKLOAD when a
  system given to load is not available via ASDF or a Quicklisp
  dist."))

(defun compute-load-strategy (name)
  (setf name (string-downcase name))
  (let ((asdf-systems '())
        (quicklisp-systems '()))
    (labels ((recurse (name)
               (let ((asdf-system (asdf:find-system name nil))
                     (quicklisp-system (find-system name)))
                 (cond (asdf-system
                        (push asdf-system asdf-systems))
                       (quicklisp-system
                        (push quicklisp-system quicklisp-systems)
                        (dolist (subname (required-systems quicklisp-system))
                          (recurse subname)))
                       (t
                        (cerror "Try again"
                                'system-not-found
                                :name name)
                        (recurse name))))))
      (with-consistent-dists
        (recurse name)))
    (make-instance 'load-strategy
                   :name name
                   :asdf-systems (remove-duplicates asdf-systems)
                   :quicklisp-systems (remove-duplicates quicklisp-systems))))

(defun show-load-strategy (strategy)
  (format t "To load ~S:~%" (name strategy))
  (let ((asdf-systems (asdf-systems strategy))
        (releases (quicklisp-releases strategy)))
    (when asdf-systems
      (format t "  Load ~D ASDF system~:P:~%" (length asdf-systems))
      (show-wrapped-list (mapcar 'asdf:component-name asdf-systems)))
    (when releases
      (format t "  Install ~D Quicklisp release~:P:~%" (length releases))
      (show-wrapped-list (mapcar 'name releases)))))

(defvar *macroexpand-progress-in-progress* nil)

(defun macroexpand-progress-fun (old-hook &key (char #\.)
                                 (chars-per-line 50)
                                 (forms-per-char 250))
  (let ((output-so-far 0)
        (seen-so-far 0))
    (labels ((finish-line ()
               (when (plusp output-so-far)
                 (dotimes (i (- chars-per-line output-so-far))
                   (write-char char))
                 (terpri)
                 (setf output-so-far 0)))
             (show-string (string)
               (let* ((length (length string))
                      (new-output (+ length output-so-far)))
                 (cond ((< chars-per-line new-output)
                        (finish-line)
                        (write-string string)
                        (setf output-so-far length))
                       (t
                        (write-string string)
                        (setf output-so-far new-output))))
               (finish-output))
             (show-package (name)
               ;; Only show package markers when compiling. Showing
               ;; them when loading shows a bunch of ASDF system
               ;; package noise.
               (when *compile-file-pathname*
                 (finish-line)
                 (show-string (format nil "[package ~(~A~)]" name)))))
      (lambda (fun form env)
        (when (and (consp form)
                   (eq (first form) 'cl:defpackage)
                   (ignore-errors (string (second form))))
          (show-package (second form)))
        (incf seen-so-far)
        (when (<= forms-per-char seen-so-far)
          (setf seen-so-far 0)
          (write-char char)
          (finish-output)
          (incf output-so-far)
          (when (<= chars-per-line output-so-far)
            (setf output-so-far 0)
            (terpri)
            (finish-output)))
        (funcall old-hook fun form env)))))

(defun call-with-macroexpand-progress (fun)
  (let ((*macroexpand-hook* (if *macroexpand-progress-in-progress*
                                *macroexpand-hook*
                                (macroexpand-progress-fun *macroexpand-hook*)))
        (*macroexpand-progress-in-progress* t))
    (funcall fun)
    (terpri)))

(defun apply-load-strategy (strategy)
  (map nil 'ensure-installed (quicklisp-releases strategy))
  (call-with-macroexpand-progress
   (lambda ()
     (format t "~&; Loading ~S~%" (name strategy))
     (asdf:load-system (name strategy) :verbose nil))))

(defun autoload-system-and-dependencies (name &key prompt)
  "Try to load the system named by NAME, automatically loading any
Quicklisp-provided systems first, and catching ASDF missing
dependencies too if possible."
  (setf name (string-downcase name))
  (with-simple-restart (abort "Give up on ~S" name)
    (let ((tried-so-far (make-hash-table :test 'equalp)))
      (tagbody
       retry
         (handler-case
             (let ((strategy (compute-load-strategy name)))
               (show-load-strategy strategy)
               (when (or (not prompt)
                         (press-enter-to-continue))
                 (apply-load-strategy strategy)))
           (asdf:missing-dependency-of-version (c)
             ;; Nothing Quicklisp can do to recover from this, so just
             ;; resignal
             (error c))
           (asdf:missing-dependency (c)
             (let ((parent (asdf::missing-required-by c))
                   (missing (asdf::missing-requires c)))
               (typecase parent
                 ((or null asdf:system)
                  ;; NIL parent comes from :defsystem-depends-on failures
                  (if (gethash missing tried-so-far)
                      (error "Dependency looping -- already tried to load ~
                                 ~A" missing)
                      (setf (gethash missing tried-so-far) missing))
                  (autoload-system-and-dependencies missing
                                                    :prompt prompt)
                  (go retry))
                 (t
                  ;; Error isn't from a system dependency, so there's
                  ;; nothing to autoload
                  (error c))))))))
    name))

(defvar *initial-dist-url*
  "http://beta.quicklisp.org/dist/quicklisp.txt")

(defun dists-initialized-p ()
  (not (not (ignore-errors (truename (qmerge "dists/"))))))

(defun quickstart-parameter (name &optional default)
  (let* ((package (find-package '#:quicklisp-quickstart))
         (symbol (and package (find-symbol (string '#:*quickstart-parameters*)
                                           package)))
         (plist (and symbol (symbol-value symbol)))
         (parameter (and plist (getf plist name))))
    (or parameter default)))

(defun maybe-initial-setup ()
  "Run the steps needed when Quicklisp setup is run for the first time
after the quickstart installation."
  (let ((quickstart-proxy-url (quickstart-parameter :proxy-url))
        (quickstart-initial-dist-url (quickstart-parameter :initial-dist-url)))
    (when (and quickstart-proxy-url (not *proxy-url*))
      (setf *proxy-url* quickstart-proxy-url)
      (setf (config-value "proxy-url") quickstart-proxy-url))
    (unless (dists-initialized-p)
      (let ((target (qmerge "dists/quicklisp/distinfo.txt"))
            (url (or quickstart-initial-dist-url
                     *initial-dist-url*)))
        (ensure-directories-exist target)
        (install-dist url :prompt nil)))))

(defun setup ()
  (unless (member 'system-definition-searcher
                  asdf:*system-definition-search-functions*)
    (setf asdf:*system-definition-search-functions*
          (append asdf:*system-definition-search-functions*
                  (list 'local-projects-searcher
                        'system-definition-searcher))))
  (let ((files (nconc (directory (qmerge "local-init/*.lisp"))
                      (directory (qmerge "local-init/*.cl")))))
    (with-simple-restart (abort "Stop loading local setup files")
      (dolist (file (sort files #'string< :key #'pathname-name))
        (with-simple-restart (skip "Skip local setup file ~S" file)
          ;; Don't try to load Emacs lock files, other hidden files
          (unless (char= (char (pathname-name file) 0)
                         #\.)
            (load file))))))
  (maybe-initial-setup)
  (ensure-directories-exist (qmerge "local-projects/"))
  (pushnew :quicklisp *features*)
  t)
