;;; plist library for objc

(use objc cocoa)

(define-objc-classes NSDictionary NSArray)

;; Traverse a plist object (or any object composited of NSDictionaries
;; and NSArrays) using keys sequentially from path-list.  General
;; dictionary keys are NSObjects, but plist keys must be NSStrings--so
;; symbol and numeric dict keys in path-list are coerced to strings.
;; Array keys must be numeric and are coerced to integers.

(define (plist-path obj path-list)
  (define (ensure-numeric x)
    (if (number? x) x
        (error 'path-list "numeric key required at path index" path-list)))
  (define (stringify x)
    (if (or (symbol? x) (number? x))
        (->string x) x))
                         
  (cond ((null? path-list) obj)
        ((not (objc:instance? obj))
         (error 'plist-path (conc "Received " obj
                                  ", expected NSDictionary or NSArray object at path index")
                path-list))
        (else
         (plist-path (cond ((@ obj is-kind-of-class: NSDictionary)
                            (@ obj object-for-key:  (stringify (car path-list))))
                           ((@ obj is-kind-of-class: NSArray)
                            (@ obj object-at-index: (ensure-numeric (car path-list))))
                           (else
                            (error 'plist-path   ;; Don't print object--it may be gigantic :(
                                   "NSDictionary or NSArray object required at path index" path-list)))
                     (cdr path-list)))))


; (plist-path plist `(Playlists 0 Name))  => @"Library"

;; Integers and bools are returned as NSNumber types.  We don't auto-convert
;; these, but you can retrieve the value using NSNumber methods e.g.
;; (@ (@ b object-for-key: "Master") boolValue)  ;; => #\x1
;; We can also pass an int or bool in using the NSNumber class explicitly --
;; i.e. (@ b set-object: (@ NSNumber numberWithLong: 1234) for-key: "Playlist ID")

;; You could create e.g. (ns:array-ref a i) which would internally convert return
;; value, rather than always converting in objc:ref->instance.  And, ns:dictionary-set!
;; could convert the argument value.  Both could do auto-symbol -> string key conversion.
;; Perhaps plist-set! and plist-ref instead.

;; Can't test the following because objc:PTR pass not supported in bridge:
;; (@ NSValue valueWithBytes: (float->ref 3.14 (make-locative (make-byte-vector 4))) ;; gc?
;;            objCType: objc:FLT)

;; The perl plist->hash function at http://www.macdevcenter.com/pub/a/mac/2005/08/02/plist.html?page=4
;; takes the option whether to convert values or leave as objects; when conversion is enabled,
;; all values are converted to strings -- not the appropriate type (so an NSNumber 3.14 becomes
;; "3.14").

;; Ruby/Python keep dictionaries and arrays as NSObjects but provide native methods on them to
;; treat like a native dict/array.  Ruby does no explicit conversion and it appears can't
;; convert NSNumbers except via calling ObjC methods.  Python leaves NSDecimalNumbers alone,
;; converts NSNumber longlong->OC_PythonLong, float/double->OC_PythonFloat, NSNumber
;; else->OC_PythonInt which are subclasses of real number types that keep a pointer to the ObjC
;; object.  Passing to ObjC, it converts Py_Bool->NSNumber bool, Py_Int->NSNumber long,
;; Py_Float->NSNumber double.  Because plist values can only be <integer> or <real>, this does
;; not lose data.

;; Most types are handled via Python bridge conversion, and dictionaries (if originating from
;; Objective C) have python iterators defined upon them but remain ObjC.  However,
;; PyObjCTools/Conversion.py provides two conversion functions which convert a collection fully to or
;; from Objective C.  These do deep transforms to recursively convert into an actual python
;; array or dictionary, converting every value using the bridge if possible and special casing
;; NSDate, NSData, and NSDecimalNumber.  Additionally a thunk is called for unconvertible
;; types.

;; Determining if a number is int or floating point without an objc type guideline is hard.
;; 2.0 is considered a scheme integer, but is inexact.  2^31 is an integer, but is inexact.
;; 2^46 is an integer, but doesn't fit in a machine int.
;; Chicken can check this since "integer" type is fixnum or flonum as long as it fits
;; within a machine int.

;; Python converts NSStrings to python unicode objects automatically (this entails a copy).
;; If you're not going to convert NSStrings to strings, it doesn't save much to automatically convert
;; NSNumbers to scheme numbers, although the reverse could still be done.

(define-objc-classes NSNumber)
(define (ns:number-value x)
  (if (ns:number? x)
      (case (string-ref (@ x objC-type) 0)
        ((#\c)     (objc:char->char-or-bool (@ x char-value)))
        ((#\f #\d) (@ x double-value))
        (else      (@ x long-value)))
        ;; how about long long?
      (error 'ns:number-value "NSNumber expected" x)))

(define ns:string-value objc:nsstring->string)

(define (ns:string? x)
  (and (objc:instance? x)
       (is-nsstring (objc:instance->pointer x))))
(define (ns:number? x)
  (and (objc:instance? x)
       (@ x is-kind-of-class: NSNumber)))  ;; slow -- should write in ObjC
(define ns:date?
  (let ((NSDate (objc:string->class "NSDate")))
    (lambda (x)
      (and (objc:instance? x)
           (@ x is-kind-of-class: NSDate)))))


(define ns:make-string objc:nsstring)

;; Numeric conversion is a little tricky; our aim is that machine integers are translated into
;; integer NSNumbers, and flonums [even which pass INTEGER?] become double values.  However,
;; any number that fits into a machine int outside of fixnum range becomes an integer, as there is
;; no distinction between int and flonum in that spectrum.  This generally satisfies plist
;; applications, for which only two values--integer and real--are used.  If a specific type
;; of NSNumber is required, use NSNumber methods to create one.
(define (ns:make-number x)
  (cond ((boolean? x)
         (@ NSNumber number-with-bool: x))   ;; creates a char and coerces to YES or NO
        ((char? x)
         (@ NSNumber number-with-char: x))   ;; Creates an int: but should we convert #\x1 to bool?
        ((fixnum? x)
         (@ NSNumber number-with-int: x))
        ((not (number? x))
         (error 'ns:make-number "number expected" x))
        ((and (##sys#fits-in-int? x)
              (not (##sys#flonum-in-fixnum-range? x)))
         (@ NSNumber number-with-long: x))
        ((rational? x) (@ NSNumber number-with-double: x))
        (else
         (error 'ns:make-number "cannot make ns:number from" x))))

;; Are SCHEMIFY and OBJCIFY appropriate names?
(define (schemify x)
  (if (objc:instance? x)
      (cond ((ns:string? x) (ns:string-value x))  ;; types will be checked twice :(
            ((ns:number? x) (ns:number-value x))
            (else
             ;(error 'schemify "could not schemify objc:instance" x)
             x))  ;; should we error out?  We would have to handle NSDate and NSData then.
      x))

;; We don't recurse into lists or alists or hash tables--use list->ns:array or the like
;; in the middle of the collection instead. 
(define (objcify x)
  (if (objc:instance? x)
      x
      (cond ((string? x)  x)                       ;; ns:make-string done by bridge
            ((symbol? x) (symbol->string x))       ;; just for convenience
            ((number? x)  (ns:make-number x))
            ((boolean? x) (ns:make-number x))
            ((char? x)    (ns:make-number x))
            (else
             (error 'objcify "could not objcify objc:instance" x)))))

;;; Array creation
(define-objc-classes NSMutableArray)

;; Use [NSArray array] to create an NSArray, not alloc/init.
(define (list->ns:array x . conv)
  (let ((conv (:optional conv objcify))
        (a [objc:send NSMutableArray array]))
    (for-each (lambda (o)
                (objc:send a add-object: (conv o)))
              x)
    a))

;; Example of creating an ns:array with various types, including an embedded ns:array.
;; (list->ns:array (list 1 2 3.5
;;                      (list->ns:array (list 6 #f))
;;                      #t #\a))  ;; => #<objc-instance (1, 2, 3.5, (6, 0), 1, 97)>

;;; Dictionary creation
(define-objc-classes NSMutableDictionary)

;; Special note: plist keys -must- be strings, so we allow the user to convert KEY and
;; VALUE separately using a two argument conversion function.  Dictionary keys in
;; general may be any Objective C object.
;; Also note: these names are horrible.

;; Convert key and value appropriately for a generic dictionary.
(define (objcify/kv k v) (cons (objcify k)
                               (objcify v)))
;; Convert key and value appropriately for a plist (all keys must be strings).
(define (plistify/kv k v) (cons (cond ((ns:string? k) k)
                                      (else (->string k)))  ;; stringify objc:instance via description?
                                (objcify v)))

;; CONV takes a KEY and VALUE and returns a converted (KEY, VALUE) pair.
(define (alist->ns:dictionary alist . conv)
  (let* ((conv (:optional conv objcify/kv))
         (d [objc:send NSMutableDictionary dictionary]))
    (for-each (lambda (o)
                (let ((o (conv (car o)
                               (cdr o))))
                  (objc:send d
                             set-object: (cdr o)
                             for-key:    (car o))))
              alist)
    d))

;; Example use:
;; (alist->ns:dictionary '((5 . 3) (6 . 4)))              ;; creates NSNumber keys and values
;; (alist->ns:dictionary '((5 . 3) (6 . 4)) plistify/kv)  ;; creates string keys and NSNumber values

;;; Enumeration

;; Finalization note: if the number of live finalizers is greater than the
;; size of ##sys#pending-finalizers, a major GC will be forced for every
;; new finalizer created.  Use the -:fXXX option to increase this buffer size
;; and use ns:enumerator-fold (left fold) when performing conversions from
;; objc:instances to scheme objects.

;; Fold-left.
(define (ns:enumerator-fold kons knil e)
  (let loop ((seed knil))
    (let ((obj (objc:send e next-object)))
      (if (not obj)
          seed
          (loop (kons obj seed))))))

;; Fold-right.  Because this builds a chain of recursive calls to
;; KONS, a large number of pending objc:instance finalizers can build
;; up even when your KONS successfully converts OBJ to a scheme object,
;; An example is KONS := (lambda (x y) (cons (schemify x) y)) on an
;; NSArray of NSStrings.   Use ns:enumerator-fold to avoid this case.
(define (ns:enumerator-fold-right kons knil e)
  (let loop ()
    (let ((obj (objc:send e next-object)))
      (if (not obj)
          knil
          (kons obj ;(schemify obj)
                (loop))))))

;(define (cons-schemify x y)
;  (cons (schemify x) y))

(define (ns:array->list array . conv)
  (let ((schemify (:optional conv schemify)))
    (reverse
     (ns:enumerator-fold
      (lambda (x y)
        (cons (schemify x) y))
      '() (@ array object-enumerator)))))

;; We don't need a reverse here as keys are unordered.
(define (ns:dictionary->alist dict . conv)
  (let ((schemify (:optional conv schemify)))
     (ns:enumerator-fold
      (lambda (x y)
        (cons (cons (schemify x)
                    (schemify (objc:send dict
                                         object-for-key: x)))
              y))
      '()
      (@ dict key-enumerator))))

;;;; Dictionary->alist conversion examples
#|

(define track1 (@ (@ tracks object-enumerator) next-object))  ;; first key's value

;; example of default conversion (schemify, which is reversible):
(ns:dictionary->alist track1)

;; show reversibility (modulo out of order keys)
(define (alist-sort a) (sort a (lambda (x y) (string<? (car x) (car y)))))
(equal? (alist-sort (ns:dictionary->alist track1))
        (alist-sort (ns:dictionary->alist
                     (alist->ns:dictionary
                      (ns:dictionary->alist track1)))))
   ;; => #t

;; example of using optional dictionary item conversion: augment schemify by checking for
;; NSDate and return its NSString description.  Naturally, this is not reversible.
(ns:dictionary->alist track1
                      (lambda (x)
                        (if (ns:date? x)
                            (@ x description)
                            (schemify x))))
|#

;; If you don't care about the keys in a dict, just the values (e.g. under the "Tracks"
;; dictionary we may ignore the numeric keys) you could grab the values with
;; (ns:array->list (@ tracks allValues)).  This will be very slow until finalization is fixed,
;; even if you use a conversion function.   Alternatively, since ns:array->list is implemented
;; using an object-enumerator, you can use (ns:array->list tracks) to obtain the same thing
;; without an intermediate NSArray.  I guess this nomenclature is not ideal.


