#+fw.dump
(eval-when (:compile-toplevel :load-toplevel :execute)
  (load "~/quicklisp/setup.lisp")
  (require :sb-posix)
  (require :uiop))

#+fw.dump
(ql:quickload '(:net.didierverna.clon :data-lens :yason :local-time))

(defpackage :fwoar.cls
  (:use :cl)
  (:export #:dump
           #:prepare-dump
           #:main))
(in-package :fwoar.cls)

(defun format-stat-time (accessor stat)
  (local-time:format-timestring
   nil
   (local-time:unix-to-timestamp
    (funcall accessor
             stat))))

(defun map-directory-entries (dir cb)
  (let ((dp (sb-posix:opendir dir)))
    (unwind-protect (loop for dirent = (sb-posix:readdir dp)
                          until (sb-alien:null-alien dirent)
                          collect (funcall cb dirent))
      (sb-posix:closedir dp))))

(defun handle-non-directory (path)
  path)

(defvar *synopsis*
  (net.didierverna.clon:defsynopsis (:postfix "PATHS..." :make-default nil)
    (flag :short-name "h" :long-name "help")))

(defun stat-file-type (path-stat)
  (let ((mode (sb-posix:stat-mode path-stat)))
    (cond
      ((sb-posix:s-isdir mode))
      ((sb-posix:s-isreg mode) :regular-file)
      ((sb-posix:s-isblk mode) :block-device)
      ((sb-posix:s-ischr mode) :character-device)
      ((sb-posix:s-issock mode) :socket)
      ((sb-posix:s-islnk mode) :symlink)
      ((sb-posix:s-isfifo mode) :fifo)
      (t :other))))

(defun decode-permissions (mode)
  (let* ((user (ash (logand #o700 mode) -6))
         (group (ash (logand #o70 mode) -3))
         (other (logand #o7 mode)))
    (list (list :user
                (when (= #o4 (logand #o4 user))
                  :read)
                (when (= #o2 (logand #o2 user))
                  :write)
                (when (= 1 (logand #o1 user))
                  :execute))
          (list :group
                (when (= #o4 (logand #o4 group))
                  :read)
                (when (= #o2 (logand #o2 group))
                  :write)
                (when (= 1 (logand #o1 group))
                  :execute))
          (list :other
                (when (= #o4 (logand #o4 other))
                  :read)
                (when (= #o2 (logand #o2 other))
                  :write)
                (when (= 1 (logand #o1 other))
                  :execute)))))

(defun directoryp (path-stat)
  (sb-posix:s-isdir (sb-posix:stat-mode path-stat)))

(defun prepend-path-component (prefix)
  (let ((prefix (if (eql #\/
                         (elt prefix (1- (length prefix))))
                    (subseq prefix 0 (1- (length prefix)))
                    prefix)))
    (lambda (suffix)
      (format nil "~a/~a" prefix suffix))))

(defun list-directory (path stat)
  (handler-case (if (directoryp stat)
                    (values (map-directory-entries
                             path
                             (data-lens:juxt
                              (constantly :name) 'sb-posix:dirent-name
                              (constantly :path) (data-lens:∘
                                                  (prepend-path-component path)
                                                  'sb-posix:dirent-name)
                              (constantly :inode) 'sb-posix:dirent-ino))
                            :directory
                            (decode-permissions (sb-posix:stat-mode stat)))
                    (values (handle-non-directory path)
                            (stat-file-type stat)
                            (decode-permissions (sb-posix:stat-mode stat))))
    (error (c) (format *error-output* "~a (~a) ~a" path (type-of c) c))))

(defun name-or-dirname (pathname)
  (or (pathname-name pathname)
      (car (last (pathname-directory pathname)))))

(defun list-path (path)
  (yason:with-output (*standard-output*)
    (let ((yason:*symbol-key-encoder* 'yason:encode-symbol-as-lowercase)
          (yason:*symbol-encoder* 'yason:encode-symbol-as-lowercase)
          (stat (sb-posix:lstat path)))
      (yason:with-object ()
        (yason:encode-object-element
         "name"
         (name-or-dirname (uiop:parse-unix-namestring path)))
        (yason:encode-object-element "path" path)
        (yason:encode-object-element "atime" (format-stat-time 'sb-posix:stat-atime
                                                               stat))
        (yason:encode-object-element "mtime" (format-stat-time 'sb-posix:stat-atime
                                                               stat))
        (yason:encode-object-element "ctime" (format-stat-time 'sb-posix:stat-atime
                                                               stat))
        (multiple-value-bind (data type permissions) (list-directory path stat)
          (yason:encode-object-element "type" type)
          (yason:encode-object-element "mode" (alexandria:alist-hash-table
                                               permissions))
          (when (eql :directory type)
            (yason:with-object-element ("children")
              (yason:with-array ()
                (loop for it in data
                      do (yason:encode-array-element
                          (alexandria:plist-hash-table it))))))))
      (terpri *standard-output*))))

(defun main-ld (paths)
  (loop for path in paths
        do
           (list-path path)))

(defun main ()
  (let* ((context (net.didierverna.clon:make-context :synopsis *synopsis*))
         (net.didierverna.clon:*context* context))
    (cond ((net.didierverna.clon:getopt :context context
                                        :long-name "help")
           (net.didierverna.clon:help))


          (t
           (let ((input (net.didierverna.clon:remainder :context context)))
             (if input
                 (main-ld input)
                 (loop for input = (read-line *standard-input*
                                              nil)
                       while input
                       for parsed = (coerce (gethash "path" (yason:parse input))
                                            'simple-string)
                       do (main-ld (list parsed)))))
           (fresh-line)))))

(defun prepare-dump ()
  (setf net.didierverna.clon:*context* nil
        *features* (remove :fw.main (remove :fw.dump *features*))))
(defun dump ()
  (prepare-dump)
  (net.didierverna.clon:dump "cls" main))