;;; extracted from  itunes.scm
(use objc plist)

(define-objc-classes NSMutableDictionary)

(define (itunes:open-library name)
  (or (@ NSMutableDictionary
         dictionary-with-contents-of-file:
         (expand-tilde name))
      (error 'itunes:open-library "failed to open library" name)))

(define (expand-tilde name)
  (@ (if (ns:string? name) name (ns:make-string name))
     string-by-expanding-tilde-in-path))

(define (itunes:write-library plist dest-name)
  (@ plist write-to-file: (expand-tilde dest-name) atomically: #t))

(define (itunes:tracks plist)
  (let ((tracks (@ plist object-for-key: "Tracks")))
    (unless
      (@ tracks is-kind-of-class: NSDictionary)
      (error 'itunes:tracks "tracks key is not a dictionary."))
    tracks))

(define-objc-classes NSURL)

(define (ns:string->URL str) (@ NSURL URL-with-string: str))

(define (write-list x file) (with-output-to-file file (lambda () (pp x))))

(use srfi-13)

(define (string-index-right-n str c n)
  (let ((start 0))
    (let loop ((end (string-length str)) (n n))
      (if (zero? n)
        end
        (and end (loop (string-index-right str c start end) (fx- n 1)))))))

(define (pathname-components-right path n)
  (let ((index (string-index-right-n path pathname-directory-separator n)))
    (if index (substring path index) path)))

(use posix)

(define (music-table-ref table key)
  (let ((path (hash-table-ref/default table key #f)))
    (cond ((not (pair? path)) path)
          ((null? (cdr path)) (car path))
          (else (make-duplicate path)))))

(define (make-duplicate filenames) (cons 'duplicate path))

(define (duplicate? file) (and (pair? file) (eq? 'duplicate (car file))))

(define (bad-file? x) (eq? (car x) 'bad-file))

(define (bad-file-name x) (cadr x))

(define (bad-file-URL x) (caddr x))

(define (make-moved-item bad-file dest) (list 'moved bad-file dest))

(define (moved-item? x) (eq? (car x) 'moved))

(define (moved-item-source x) (cadr x))

(define (moved-item-destination x) (caddr x))

(define (make-bad-file-list x) (cons 'bad-file-list x))

(define (bad-file-list? x) (eq? (car x) 'bad-file-list))

(define (bad-file-list-items x) (cdr x))

(use srfi-1)

(define (bad-file-list-filter pred x)
  (make-bad-file-list (filter pred (bad-file-list-items x))))

(define (moved-item-duplicate? x) (duplicate? (moved-item-destination x)))

(define (moved-item-notfound? x) (not (moved-item-destination x)))

(define (moved-item-unique? x) (string? (moved-item-destination x)))

(define (make-URL->moved-item-table bad-file-list)
  (let ((h (make-hash-table string=?)))
    (for-each
      (lambda (m) (hash-table-set! h (bad-file-URL (moved-item-source m)) m))
      (bad-file-list-items bad-file-list))
    h))

(define (path->URL x) (@ (@ NSURL file-URL-with-path: x) absolute-string))

(define (create-directories pathname . traverse-symlinks?)
  (let ((make-pathname
          (if (absolute-pathname? pathname)
            make-absolute-pathname
            make-pathname)))
    (let loop ((path-done '())
               (path-left
                 (string-split
                   pathname
                   (string pathname-directory-separator))))
      (if (null? path-left)
        pathname
        (let ((next-path (append path-done (list (car path-left))))
              (pathname-so-far (make-pathname path-done (car path-left))))
          (if (directory? pathname-so-far)
            (loop next-path (cdr path-left))
            (if (file-exists? pathname-so-far)
              (if (and (symbolic-link? pathname-so-far)
                       (:optional traverse-symlinks? #t)
                       (directory? (make-pathname pathname-so-far #f)))
                (loop next-path (cdr path-left))
                (error 'create-directories "file exists" pathname-so-far))
              (begin
                (create-directory pathname-so-far)
                (loop next-path (cdr path-left))))))))))

(define (result-type x) (car x))

(define (result-items x) (cdr x))

(define (make-result type items) (cons type items))

(define (result? x) (symbol? (result-type x)))

(define (filter-result pred x)
  (if (result? x)
    (make-result (result-type x) (filter pred (result-items x)))
    (error 'filter-result "not a result list")))

(define (moved-item-error? x) (error? (moved-item-destination x)))

(define (error? x) (and (pair? x) (eq? (car x) 'error)))

(define (make-link-error msg dest) (list 'error msg dest))

(define (error->boolean thunk) (handle-exceptions _ #f (begin (thunk) #t)))

(define (create-symbolic-link* old new)
  (error->boolean (lambda () (create-symbolic-link old new))))

(define (error->string exn)
  (let ((msg ((condition-property-accessor 'exn 'message #f) exn))
        (loc ((condition-property-accessor 'exn 'location #f) exn))
        (args ((condition-property-accessor 'exn 'arguments #f) exn)))
    (cond ((null? args) msg)
          ((and (pair? args) (null? (cdr args))) (conc msg ": " (car args)))
          (else (conc msg ": " args)))))

(define-macro
  (try try-block catch-proc)
  (let ((exn (gensym)))
    `(handle-exceptions ,exn (,catch-proc ,exn) ,try-block)))

(define itunes:music-regexp (make-parameter ".*.([Mm][Pp]3|[Aa][Aa][Cc])"))

(define (find-music-files base-dir sig)
  (let ((table (make-hash-table string=?)))
    (for-each
      (lambda (fn)
        (let ((short-name (pathname-components-right fn sig)))
          (hash-table-set!
            table
            short-name
            (let ((old-key (hash-table-ref/default table short-name #f)))
              (if old-key (cons fn old-key) (list fn))))))
      (find-files
        base-dir
        (itunes:music-regexp)
        cons
        '()
        (lambda (fn) (not (symbolic-link? fn)))))
    table))

(define (make-music-lookup base-dir . significant-path-components)
  (let* ((sig-components (:optional significant-path-components 3))
         (table (find-music-files base-dir sig-components)))
    (lambda (path)
      (music-table-ref
        table
        (pathname-components-right path sig-components)))))

(define (make-moved-lookup bad-files)
  (let ((table (make-URL->moved-item-table bad-files)))
    (lambda (location) (hash-table-ref/default table location #f))))

(define (location-fold tracks meat)
  (ns:enumerator-fold
    (lambda (x xs)
      (let* ((location (@ x object-for-key: "Location"))
             (URL (ns:string->URL location)))
        (if URL
          (if (@ URL is-file-URL)
            (meat x xs location (ns:string-value (@ URL path)))
            xs)
          (begin (warning "invalid URL: " location) xs))))
    '()
    (@ tracks object-enumerator)))

(define (get-bad-files tracks music-lookup)
  (make-result
    'bad-file-list
    (location-fold
      tracks
      (lambda (x xs location name)
        (if (file-exists? name)
          xs
          (cons `(moved (bad-file ,name ,(ns:string-value location))
                        ,(music-lookup name))
                xs))))))

(define (link-files tracks moved-lookup)
  (make-result
    'link-results
    (location-fold
      tracks
      (lambda (x xs location name)
        (let ((moved-item (moved-lookup (ns:string-value location))))
          (if moved-item
            (if (string=? name (bad-file-name (moved-item-source moved-item)))
              (if (moved-item-unique? moved-item)
                (let ((src (bad-file-name (moved-item-source moved-item)))
                      (dst (moved-item-destination moved-item)))
                  (try (begin
                         (create-directories (pathname-directory src))
                         (create-symbolic-link dst src)
                         (cons moved-item xs))
                       (lambda (exn)
                         (cons (make-moved-item
                                 (moved-item-source moved-item)
                                 (make-link-error (error->string exn) dst))
                               xs))))
                xs)
              xs)
            xs))))))

(define (move-files tracks moved-lookup)
  (make-result
    'move-results
    (location-fold
      tracks
      (lambda (x xs location name)
        (let ((moved-item (moved-lookup (ns:string-value location))))
          (if moved-item
            (if (string=? name (bad-file-name (moved-item-source moved-item)))
              (if (moved-item-unique? moved-item)
                (begin
                  (@ x
                     set-object:
                     (path->URL (moved-item-destination moved-item))
                     for-key:
                     "Location")
                  (cons moved-item xs))
                xs)
              xs)
            xs))))))

