;;;; Persistent Hash Map

;;; Copyright MMXV-MMXVII Arthur A. Gleckler.  All rights reserved.

;; Permission is hereby granted, free of charge, to any person
;; obtaining a copy of this software and associated documentation
;; files (the "Software"), to deal in the Software without
;; restriction, including without limitation the rights to use, copy,
;; modify, merge, publish, distribute, sublicense, and/or sell copies
;; of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:

;; The above copyright notice and this permission notice shall be
;; included in all copies or substantial portions of the Software.

;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
;; NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.

;;; Public protocol (API)

;; (phm? datum)

;;   Return true iff `datum' is a persistent hash map.

;; (make-phm hash = [alist])

;;   Return a new immutable persistent hash map that uses the `hash'
;;   procedure to hash its keys and `=' to compare them.  If `alist'
;;   is supplied, include all its keys and data in the result.  Later
;;   occurrences of the same key override earlier ones.

;; (phm/count phm)

;;   Return the number of elements in `phm'.

;; (phm/empty? phm)

;;   Return true iff `phm' is empty.

;; (phm/immutable phm)
;; (phm/immutable phm replace)

;;   Return a PHM equivalent to `phm', but that is immutable.  Even if
;;   `phm' is mutable, no change to it will affect the returned one.
;;   If `replace' is supplied, replace the datum associated with each
;;   key whose value has been modified since the PHM was made mutable
;;   with the result of calling `replace' on that key and datum.  This
;;   is useful for converting PHSs (sets) stored as values in a PHM
;;   back into immutable ones when the containing PHM is made
;;   immutable.

;; (phm/mutable phm)

;;   Return a PHM equivalent to `phm', but that is mutable.  If `phm'
;;   was immutable, no change to the returned PHM will affect `phm'.

;; (phm/mutable? phm)

;;   Return true iff `phm' is mutable.

;; (phm/put phm key datum)

;;   Return a PHM equivalent to `phm' except that `datum' is at `key'.

;; (phm/put! phm key datum)

;;   Return a PHM equivalent to `phm' except that `datum' is at `key'.
;;   Modify `phm', which must be mutable, in the process.

;; (phm/replace phm key replace)

;;   Return a PHM equivalent to `phm' except that whatever value is at
;;   `key' has been replaced by `(replace datum)', or `(replace
;;   hamt-null)' if there was no value there already.  If `replace'
;;   returns `hamt-null', the value is removed.

;; (phm/replace! phm key replace)

;;   Return a PHM equivalent to `phm' except that whatever value is at
;;   `key' has been replaced by `(replace datum)', or `(replace
;;   hamt-null)' if there was no value there already.  If `replace'
;;   returns `hamt-null', the value is removed.  Modify `phm', which
;;   must be mutable, in the process.

;; (phm/get phm key [default])

;;   Return the datum stored at `key' in `phm'.  If none is present,
;;   return `default' if it was supplied, or #f if it was not.

;; (phm/contains? phm key)

;;   Return true iff `phm' has a datum at `key'.

;; (phm/remove phm key)

;;   Return a PHM equivalent to `phm' except that there is no datum at
;;   `key'.

;; (phm/remove! phm key)

;;   Return a PHM equivalent to `phm' except that there is no datum at
;;   `key'.  Modify `phm', which must be mutable, in the process.

;; (phm/add-alist phm alist)

;;   Return a PHM equivalent to `phm' except that, for every pair in
;;   `alist', the datum in its cdr is stored in the new PHM at the key
;;   in its car.  Later occurrences of the same key override earlier
;;   ones.

;; (phm/add-alist! phm alist)

;;   Return a PHM equivalent to `phm' except that, for every pair in
;;   `alist', the datum in its cdr is stored in the new PHM at the key
;;   in its car.  Later occurrences of the same key override earlier
;;   ones.  Modify `phm', which must be mutable, in the process.

;; (phm->alist phm)

;;   Return an alist mapping the keys in `phm' to their values.

;; (phm/keys phm)

;;   Return a list of the keys in `phm'.

;; (phm/for-each procedure phm)

;;   Run `procedure' on each key and datum in `phm'.

;;; Implementation of public protocol (API)

(define (phm? datum)
  (and (hash-array-mapped-trie? datum)
       (hamt/payload? datum)))

(define (make-phm-inner hash = alist)
  (let ((phm (make-hamt = hash #t)))
    (if (null? alist)
	phm
	(let ((phm-1 (phm/mutable phm)))
	  (phm/add-alist! phm-1 alist)
	  (phm/immutable phm-1)))))

(define make-phm
  (case-lambda
   ((hash =) (make-phm-inner hash = '()))
   ((hash = alist) (make-phm-inner hash = alist))))

(define (phm/count phm)
  (assert (phm? phm))
  (hamt/count phm))

(define (phm/empty? phm)
  (assert (phm? phm))
  (hamt/empty? phm))

(define phm/immutable
  (case-lambda
   ((phm)
    (assert (phm? phm))
    (hamt/immutable phm))
   ((phm replace)
    (assert (phm? phm))
    (hamt/immutable phm replace))))

(define (phm/mutable phm)
  (assert (phm? phm))
  (hamt/mutable phm))

(define (phm/mutable? phm)
  (assert (phm? phm))
  (hamt/mutable? phm))

(define (phm/put phm key datum)
  (assert (phm? phm))
  (hamt/put phm key datum))

(define (phm/put! phm key datum)
  (assert (phm? phm))
  (hamt/put! phm key datum))

(define (phm/replace phm key replace)
  (assert (phm? phm))
  (hamt/replace phm key replace))

(define (phm/replace! phm key replace)
  (assert (phm? phm))
  (hamt/replace! phm key replace))

(define (phm/get-inner phm key default)
  (assert (phm? phm))
  (let ((result (hamt-fetch phm key)))
    (if (hamt-null? result)
	default
	result)))

(define phm/get
  (case-lambda
   ((phm key) (phm/get-inner phm key #f))
   ((phm key default) (phm/get-inner phm key default))))

(define (phm/contains? phm key)
  (assert (phm? phm))
  (not (hamt-null? (hamt-fetch phm key))))

(define (phm/remove phm key)
  (assert (phm? phm))
  (phm/put phm key hamt-null))

(define (phm/remove! phm key)
  (assert (phm? phm))
  (assert (hamt/mutable? phm))
  (phm/put! phm key hamt-null))

(define (phm/add-alist phm alist)
  (assert (phm? phm))
  (fold (lambda (a phm) (phm/put phm (car a) (cdr a))) phm alist))

(define (phm/add-alist! phm alist)
  (assert (phm? phm))
  (do-list (a alist)
    (phm/put! phm (car a) (cdr a)))
  phm)

(define (phm->alist phm)
  (assert (phm? phm))
  (hamt->list phm cons))

(define (phm/data phm)
  (assert (phm? phm))
  (hamt->list phm (lambda (k d) d)))

(define (phm/keys phm)
  (assert (phm? phm))
  (hamt->list phm (lambda (k d) k)))

(define (phm/for-each procedure phm)
  (assert (phm? phm))
  (hamt/for-each procedure phm))