;;;
;;; lh-guid.lisp
;;;
;;; GUID representation and parsing.
;;;

;;; A GUID is a 128-bit integer used as a unique identifier.  It is
;;; broken up into several bitfields and has defined textual and data
;;; representations.  Our GUID objects are CLOS instances (thus
;;; discriminable), EQ-comparable (any two GUIDs with the same text
;;; representation are the same object), externalizable (can be
;;; printed readably, have a make-load-form method defined) and
;;; integrate with SB-ALIEN (can be struct members, are passed to
;;; functions as pointers to their encoded representation).

;;; One feature that is not implemented is custom reader syntax for
;;; GUID parsing, such as #G{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} =>
;;; #<a GUID>.

;;; If an alien function takes a pointer to a GUID for reading,
;;; declare it as taking an lh-guid:guid and pass a GUID object
;;; directly.  If an alien function takes a pointer to a GUID for
;;; writing, declare it as taking an (* lh-guid:guid), use WITH-ALIEN
;;; to allocate an alien variable of type lh-guid:guid, pass it to the
;;; function with ADDR, then DEREF the alien variable to get the lisp
;;; GUID object.
;;;
;;; That is, for reading:
;;;
;;;    (alien-funcall (extern-alien (function ... lh-guid:guid ...))
;;;                   ... guid ...)
;;;
;;; for writing:
;;;
;;;    (with-alien ((foo lh-guid:guid)
;;;                 (bar (function ... (* lh-guid:guid) ...)
;;;                      :extern "bar"))
;;;       (alien-funcall bar ... (addr foo) ...)
;;;       (deref foo))
;;;
;;; This is important to prevent the alien function from destructively
;;; modifying an interned GUID, which would be bad.

;;; Callback functions taking or returning a GUID are untested, and
;;; may well need to use an explicit pointer (that is, arguments would
;;; need to be of type (* lh-guid:guid) rather than lh-guid:guid, and
;;; need explicit dereferencing).


;;;; Package definition.

(cl:defpackage :lh-guid
  (:use :cl :sb-alien)
  (:import-from "SB-ALIEN-INTERNALS"
		"DEFINE-ALIEN-TYPE-TRANSLATOR"
		"DEFINE-ALIEN-TYPE-METHOD")
  (:export
   "GUID" "PARSE-GUID" "UNPARSE-GUID"))

(cl:in-package :lh-guid)


;;;; Storage representation.

;;; We represent a GUID as a wrapper class and an underlying octet
;;; vector containing the machine encoded form of the GUID.

(defclass guid ()
  ((data-bits :initarg data-bits
	      :type (simple-array (unsigned-byte 8) (16)))))


;;;; "Interning".

;;; To provide EQ-level GUID comparability, we "intern" GUIDs in a
;;; weak-valued hash table keyed based on a 128-bit unsigned integer
;;; representation of the underlying data bits.  This gives us the
;;; EQ-comparability we want while still allowing GUID objects to be
;;; garbage collected.

(defvar *known-guids* (make-hash-table :weakness :value))

(defun intern-guid (data-bits)
  "Given DATA-BITS, the underlying (simple-array (unsigned-byte 8)
(16)) storage representation of a GUID, return the unique GUID object
that represents it.  If the GUID object needs to be created then
DATA-BITS must not be modified after calling INTERN-GUID."
  ;; FIXME: Possibly should pin SAP.
  (let* ((sap (sb-sys:vector-sap data-bits))
	 (qw0 (sb-sys:sap-ref-64 sap 0))
	 (qw1 (sb-sys:sap-ref-64 sap 8))
	 (key (dpb qw1 (byte 64 64) qw0)))
    (or (gethash key *known-guids*)
	(setf (gethash key *known-guids*)
	      (make-instance 'guid 'data-bits data-bits)))))


;;;; Parsing and unparsing.

(defun parse-guid (string)
  "Return the GUID described by STRING."
  ;; A GUID looks like "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}", where
  ;; each x is a hexadecimal digit.  That's 38 characters, with some
  ;; elements having a fixed content and the rest being digit-char-p
  ;; true for radix 16 (hexadecimal).
  (unless (and (= (length string) 38)
	       (eql #\{ (char string 0))
	       (eql #\- (char string 9))
	       (eql #\- (char string 14))
	       (eql #\- (char string 19))
	       (eql #\- (char string 24))
	       (eql #\} (char string 37)))
    (error "~S is not a valid GUID" string))
  (let ((data-bits (make-array 16 :element-type '(unsigned-byte 8))))
    (loop for offset in '(7 5 3 1 12 10 17 15 20 22 25 27 29 31 33 35)
	  for index from 0
	  do (setf (aref data-bits index)
		   (parse-integer string :radix 16
				  :start offset :end (+ offset 2))))
    (intern-guid data-bits)))

(defun unparse-guid (guid)
  "Return the textual form of GUID as a string."
  (apply #'format nil
	 "~({~4@{~2,'0X~}-~2@{~2,'0X~}-~2@{~2,'0X~}-~2@{~2,'0X~}-~6@{~2,'0X~}}~)"
	 (loop with data-bits = (slot-value guid 'data-bits)
	       for index in '(3 2 1 0 5 4 7 6 8 9 10 11 12 13 14 15)
	       collect (aref data-bits index))))


;;;; Serialization.

;;; Print-read consistency.

(defmethod print-object ((guid guid) stream)
  ;; FIXME: Make sure this behaves right wrt *read-eval*.
  (format stream "#.(~S ~S)" 'parse-guid (unparse-guid guid)))

;;; Externalizability in FASLs.

(defmethod make-load-form ((guid guid) &optional environment)
  (declare (ignore environment))
  `(intern-guid ,(slot-value guid 'data-bits)))


;;;; Alien type definition.

;;; The following is a heavily-commented example of defining a
;;; translated alien type in SBCL.  The mechanisms for doing this are,
;;; of course, undocumented and internal to SBCL and are likey easy to
;;; break with a type-name collision.

;;; KLUDGE: define-alien-type-class is not package-safe, so we need to
;;; do our damage in the sb-alien package in order to make it work.

(cl:in-package :sb-alien)

;;; First, we define our alien-type-class.  We inherit from
;;; system-area-pointer because we would like to be passed to alien
;;; functions as a pointer type.

(define-alien-type-class (lh-guid:guid :include system-area-pointer))

;;; KLUDGE: Now that we've defined our alien-type-class, switch back
;;; to our package.

(cl:in-package :lh-guid)

;;; Next, we define a type translator.  This is used for when someone
;;; creates a struct or union with a GUID as a member or when someone
;;; uses WITH-ALIEN to create a GUID on the heap.

(define-alien-type-translator guid ()
  (sb-alien::make-alien-guid-type :bits 128))

;;; Next, we need an unparse method to be able to convert back from an
;;; alien-type object to a type specifier that will recreate it
;;; (essentially similar to print-read consistency).

(define-alien-type-method (guid :unparse) (type)
  (declare (ignore type))
  'guid)

;;; Next, we define a lisp-rep method to inform the system what lisp
;;; type to check for when deporting a value from lisp to alien form.

(define-alien-type-method (guid :lisp-rep) (type)
  (declare (ignore type))
  'guid)

;;; We don't need a type= method because the wrapper that calls it
;;; will only do so if the two types are of the same type class, and
;;; the default implementation (inherited from root) is to just return
;;; T.  This means that it only makes sense to define a type= method
;;; for parameterized type classes, which GUID is not.

;;; We don't need a subtypep method because the default implementation
;;; (inherited from root) is to just call alien-type-= on its
;;; arguments, which does the right thing for non-parameterized type
;;; classes.

;;; That's the basic type-system integration done.  All that remains
;;; is to tell the system how to convert guids to and from alien form.

;;; First, we define a naturalize-gen method to create a lisp guid
;;; from a pointer to an alien guid structure.

(define-alien-type-method (guid :naturalize-gen) (type alien)
  (declare (ignore type))
  ;; FIXME: This should be doable without consing up a fresh array if
  ;; the GUID has already been interned.  Probably by using SAP-REF-64
  ;; to obtain the key for the hash table just as INTERN-GIUD does.
  (let ((data-bits (gensym)))
    `(let ((,data-bits (make-array 16 :element-type '(unsigned-byte 8))))
      (sb-kernel:copy-ub8-from-system-area ,alien 0 ,data-bits 0 16)
      (intern-guid ,data-bits))))

;;; Next, we define the deport-gen, deport-alloc-gen and deport-pin-p
;;; methods.  The deport-alloc-gen method retrieves the object that
;;; needs to be pinned if there is one, or it can return the actual
;;; value to use or the original object.  The deport-gen method is
;;; passed the value returned by the deport-alloc-gen method and must
;;; return the actual value to use.  The deport-pin-p method tells the
;;; system that the value returned from deport-alloc-gen must be
;;; pinned.

(define-alien-type-method (guid :deport-gen) (type object)
  (declare (ignore type))
  ;; deport-gen is the only method that can reasonably return multiple
  ;; values.  The (optional) second value is to be the type of the
  ;; value returned by the expression computed in the deport-alloc-gen
  ;; method, defaulting to the return of the lisp-rep method or (in
  ;; the case of alien-value types, for which lisp-rep returns NIL) an
  ;; ALIEN type specifier.
  (values `(sb-sys:vector-sap ,object)
	  '(simple-array (unsigned-byte 8) (16))))

(define-alien-type-method (guid :deport-alloc-gen) (type value)
  (declare (ignore type))
  `(slot-value ,value 'data-bits))

(define-alien-type-method (guid :deport-pin-p) (type)
  (declare (ignore type))
  t)

;;; Next, the extract-gen and deposit-gen methods are used to access
;;; structure slots and such that are GUIDs.  The default deposit-gen
;;; method passes the buck to setf, but as we're doing an area-copy,
;;; we can't let it do that.  The result of extract-gen is passed to
;;; naturalize, so in our case it should "just" do a SAP+.

(define-alien-type-method (guid :extract-gen) (type sap offset)
  (declare (ignore type))
  `(sb-sys:sap+ ,sap (truncate ,offset sb-vm:n-byte-bits)))

(define-alien-type-method (guid :deposit-gen) (type sap offset value)
  (declare (ignore type))
  `(sb-kernel:copy-ub8-to-system-area ,value
    0 ,sap (truncate ,offset sb-vm:n-byte-bits) 16))


;;; EOF
