                                    (getKey [this] priv-key)
                                    (getCert [this] cert)))
                                (getMspId [this] msp-id)))
-    new-client))
+    client))
 
 (defn- priv-key+cert->hash [priv-key cert]
   (hash (str (.toString ^BCECPrivateKey priv-key) cert)))
 
 (defn evict-client-from-cache [client]
-  (let [enrollment (.getEnrollment ^User (hf-client/get-user-context client))
+  (let [enrollment (.getEnrollment ^User (.getUserContext ^HFClient client))
         cache-key (priv-key+cert->hash (.getKey ^Enrollment enrollment)
-                                      (.getCert ^Enrollment enrollment))]
+                                       (.getCert ^Enrollment enrollment))]
     (swap! client-lru-cache cache/evict cache-key)))
 
-;;;
-;;; Channel
-;;;
-(defrecord ClientOpts [roles account affiliation])
-
-(defn get-or-make-channel [client channel-id]
-  (or (hf-client/get-channel client channel-id) (hf-client/new-channel client channel-id)))
-
-(defprotocol IChannelEndOpts
-  (new-channel-end [channel-end client])
-  (get-channel-ends [channel-end client]))
-
-;;;
-;;; Orderer and Peer
-;;;     Peers
-;;;             Endorser
-;;;             Commiter
-;;;             (Submitter)
-
-;;;
-;; Channel ends are: Peers(Endorsers,...), Orderers
-(defrecord OrdererOpts [name grpc-url properties]
-  IChannelEndOpts
-    (new-channel-end [{:keys [name grpc-url properties]} client]
-    (hf-client/new-orderer client name grpc-url properties))
-  (get-channel-ends [this channel]
-    (channel/get-orderers channel)))
-
-(defrecord PeerOpts [name grpc-url properties]
-  IChannelEndOpts
-  (new-channel-end [{:keys [name grpc-url properties]} client]
-    (hf-client/new-peer client name grpc-url properties))
-  (get-channel-ends [this channel]
-    (channel/get-peers channel)))
-
-(defprotocol IChannelEnd
-  (add-channel-end-to-channel [channel-end channel]))
-  
-(extend-protocol IChannelEnd
-  Orderer
-  (add-channel-end-to-channel [this channel]
-    (channel/add-orderer channel this)))
-
-(extend-protocol IChannelEnd
-  Peer
-  (add-channel-end-to-channel [this channel]
-    (channel/add-peer channel this)))
-
-;;;
-;;; Chaincode
-;;;
-;;; Chaincode - a set of functions
-(defonce chaincode-lru-cache
-  ;; FIXME: magic number
-  (atom (cache/lru-cache-factory {} :threshold 64)))
-
-(defrecord ChaincodeOpts [name version path])
-(defn make-ChaincodeOpts [{:keys [name version path]}]
-  (->ChaincodeOpts (when name (str/trim name))
-                   (when version (str/trim version))
-                   (when path (str/trim path))))
-
-(defn- chaincode->hash [{:keys [name version path]}]
-  (hash (str name " " version " " path)))
-
-(defn make-chaincode [{:keys [name version path]}]
-  (-> (chaincode-id/new-builder)
-      (chaincode-id/set-name name)
-      (chaincode-id/set-version version)
-      (chaincode-id/set-path path)
-      (chaincode-id/build)))
-
-(defn get-or-make-chaincode [chaincode]
-  (general-lru-cache-read (chaincode->hash chaincode)
-                          chaincode-lru-cache
-                          make-chaincode
-                          chaincode))
-
-(defn evict-chaincode-from-cache [])
-
-;;;
-;;; High level interface
-;;;
 (defn get-or-make-client [msp-id name priv-key cert opts]
   (general-lru-cache-read (priv-key+cert->hash priv-key cert)
                           client-lru-cache
-                          make-client msp-id name priv-key cert opts))
-
-(defn add-channel-end
-  ([client channel-id channel-end-opts]
-   ;; Add a {orderer peer} if there is no {orderer peer}
-   (add-channel-end client channel-id channel-end-opts false))
-  ([client channel-id channel-end-opts force?]
-   ;; To add an additional {orderer peer} set 'force?' true
-   (let [channel (get-or-make-channel client channel-id)]
-     (when (or force?
-               (empty? (get-channel-ends channel-end-opts (get-or-make-channel client channel-id))))
-       (add-channel-end-to-channel (new-channel-end channel-end-opts client) channel)))))
-
-(defrecord TransactionOpts [fcn args ;; chaincode-language chaincode-endorsement-policy
-                            proposal-wait-time])
-
-#_
-(defn make-proposal-tx-request
-  [client channel-id chaincode-opts {:keys [fcn args proposal-wait-time]}]
-  (let [channel (get-or-make-channel client channel-id)
-        req (hf-client/new-transaction-proposal-request client)]
-    (when-not (channel/is-initialized channel)
-      (channel/initialize channel))
-    (transaction-request/set-chaincode-id req (get-or-make-chaincode chaincode-opts))
-    ;; Transaction options
-    ;; FIXME: what about chaincode-language, chaincode-endorsement-policy ?
-    (when fcn
-      (transaction-request/set-fcn req fcn))
-    (when args
-      (transaction-request/set-args req args))
-    (when proposal-wait-time
-      (transaction-request/set-proposal-wait-time req proposal-wait-time))
-    req))
-
-(defn prepare-chaincode-proposal-tx-req
-  [channel proposal-tx-req chaincode-opts {:keys [fcn args proposal-wait-time]}]
-  (when-not (channel/is-initialized channel)
-    (channel/initialize channel))
-  (transaction-request/set-chaincode-id proposal-tx-req (get-or-make-chaincode chaincode-opts))
-  ;; Transaction options
-  ;; FIXME: what about chaincode-language, chaincode-endorsement-policy ?
-  (when fcn
-    (transaction-request/set-fcn proposal-tx-req fcn))
-  (when args
-    (transaction-request/set-args proposal-tx-req args))
-  (when proposal-wait-time
-    (transaction-request/set-proposal-wait-time proposal-tx-req proposal-wait-time))
-  proposal-tx-req)
-
-(defn order-transaction
-  ([client channel-id proposal-responses]
-   (order-transaction client channel-id proposal-responses nil))
-  ([client channel-id proposal-responses orderers]
-   ;; Send Tx to orderers
-   (apply channel/send-transaction (get-or-make-channel client channel-id) proposal-responses orderers)))
-
-(defn get-order-transaction-result [f then-fn catch-fn]
-  (-> f
-      (promis/then then-fn)
-      (promis/catch catch-fn)))
-
-;;;;;;;;;;; Ex
+                          make-hf-client msp-id name priv-key cert opts))
+
+
+;;;
+;;;
+;;;
+(defonce ^:private client-config (atom {}))
+
+(def ^:dynamic ^:private *credentials* nil)
+
+(def ^:dynamic ^:private *client-config* nil)
+
+(def ^:dynamic ^:private *client-class* nil)
+
+
+
+
+
+(defn- keyword-converter
+  "Given something that tokenizes a string into parts, turn it into
+  a :kebab-case-keyword."
+  [separator-regex]
+  (fn [s]
+    (->> (str/split s separator-regex)
+         (map str/lower-case)
+         (interpose \-)
+         str/join
+         keyword)))
+
+(def ^:private camel->keyword
+  "from Emerick, Grande, Carper 2012 p.70"
+  (keyword-converter #"(?<=[a-z])(?=[A-Z])"))
+
+(defn- keyword->camel
+  [kw]
+  (let [n (name kw)
+        m (.replace n "?" "")]
+    (->> (str/split m #"\-")
+         (fmap str/capitalize)
+         str/join)))
+
+(defn- fabric-package?
+  [^Class clazz]
+  (->> (.getName clazz)
+       (re-find #"org\.hyperledger\.fabric\.sdk")
+       some?))
+
+(defn to-date
+  [date]
+  (cond
+    (instance? Date date) date
+    (instance? AbstractInstant date) (.toDate ^AbstractInstant date)
+    (integer? date) (Date. ^int date)
+    true (.. (SimpleDateFormat. @date-format)
+           (parse (str date) (ParsePosition. 0)))))
+
+(defn to-file
+  [file]
+  (if (instance? File file)
+    file
+    (if (string? file)
+      (File. ^String file))))
+
+(defn to-enum
+  "Case-insensitive resolution of Enum types by String."
+  [type value]
+  (some
+   #(if-let [enum-str (.toString %)]
+      (= (str/upper-case value) (str/upper-case enum-str))
+      %)
+    (-> type
+        (.getDeclaredMethod "values" (make-array Class 0))
+        (.invoke type (make-array Object 0)))))
+
+; assoc java Class to Clojure cast functions
+(defonce ^:private coercions
+  (->> [:String :Integer :Long :Double :Float]
+       (reduce
+         (fn [m e]
+           (let [arr (str "[Ljava.lang." (name e) ";")
+                 clazz (Class/forName arr)]
+             (assoc m clazz into-array))) {})
+       (merge {
+         String     str
+         Integer    int
+         Long       long
+         Boolean    boolean
+         Double     double
+         Float      float
+         BigDecimal bigdec
+         BigInteger bigint
+         Date       to-date
+         File       to-file
+         ByteBuffer #(-> % str .getBytes ByteBuffer/wrap)
+         "int"      int
+         "long"     long
+         "double"   double
+         "float"    float
+         "boolean"  boolean})
+       atom))
+
+(defn register-coercions
+  "Accepts key/value pairs of class/function, which defines
+  how data will be converted to the appropriate type
+  required by the Java method."
+  [& {:as coercion}]
+  (swap! coercions merge coercion))
+
+(defn coerce-value
+  "Coerces the supplied stringvalue to the required type as
+  defined by the Java method signature. String or keyword
+  conversion to Enum types (e.g. via valueOf()) is supported."
+  [value type]
+  (let [value (if (keyword? value) (name value) value)]
+    (if-not (instance? type value)
+      (if (= java.lang.Enum (.getSuperclass ^Class type))
+        (to-enum type value)
+        (if-let [coercion (@coercions (if (.isPrimitive ^Class type)
+                                        (str type)
+                                        type))]
+          (coercion value)
+          (throw (IllegalArgumentException.
+                  (format "No coercion is available to turn %s into an object of type %s"
+                          value type)))))
+      value)))
+
+(defn- default-value
+  [class-name]
+  (get
+    {"boolean" false
+     "double" (double 0.0)
+     "float" (float 0.0)
+     "long" 0
+     "int" (int 0)}
+    class-name))
+
+(defn- unwind-types
+  [depth param]
+  (if (instance? ParameterizedType param)
+      (let [f (partial unwind-types (inc depth))
+            types (-> param .getActualTypeArguments)
+            t (-> types last f)]
+        (if (and (instance? ParameterizedType (last types))
+                 (.contains (-> types last str) "java.util")
+                 (.contains (-> types last str) "java.lang"))
+          {:type [(-> types last .getRawType)]
+           :depth depth}
+          t))
+      {:type [param]
+       :depth depth}))
+
+(defn- paramter-types
+  [method]
+  (let [types (seq (.getGenericParameterTypes method))
+        param (last types)
+        rval  {:generic types}]
+    (if (instance? ParameterizedType param)
+        (let [t (unwind-types 1 param)]
+          (merge rval
+                 {:actual (:type t)
+                  :depth  (:depth t)}))
+        rval)))
+
+(defn- normalized-name
+  [method-name]
+  (-> (name method-name)
+      (.replaceFirst
+        (case (.substring method-name 0 3)
+          "get" "get"
+          "set" "set"
+          "default")
+      "")
+      (.toLowerCase)))
+
+(defn- matches?
+  "We exclude any mutators of the bean which
+   expect a java.util.Map$Entry as the first
+   argument, as we won't be dealing in these
+   from Clojure. Specifically, this is meant
+   to address the various setKey() methods
+   in the DynamoDBV2Client.
+   http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/model/DeleteItemRequest.html#setKey(java.util.Map.Entry, java.util.Map.Entry)
+   http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/model/GetItemRequest.html#setKey(java.util.Map.Entry, java.util.Map.Entry)"
+  [method name value]
+  (let [args (.getParameterTypes method)]
+    (and (= name (normalized-name (.getName method)))
+         (if (empty? value)
+             (= 0 (count args))
+             (and (< 0 (count args))
+                  (or (= (count args) (count (flatten value)))
+                      (and (coll? value)
+                           (= 1 (count args))
+                           (or (contains? @coercions (first args))
+                               (.isArray (first args))
+                               (and (.getPackage (first args))
+                                    (.startsWith
+                                      (.getName (.getPackage (first args)))
+                                      "java.util")))))
+                  (not (and (= 2 (count args))
+                            (every? (partial = java.util.Map$Entry)
+                                    args))))))))
+
+(defn- accessor-methods
+  [class-methods name value]
+  (reduce
+    #(if (matches? %2 name value)
+      (conj %1 %2)
+      %1)
+    []
+    class-methods))
+
+(defn- find-methods
+  [pojo k & v]
+  (-> (.getClass pojo)
+      (.getMethods)
+      (accessor-methods
+        (.toLowerCase (keyword->camel k))
+        v)))
+
+(defn to-java-coll
+  "Need this only because S3 methods actually try to
+   mutate (e.g. sort) collections passed to them."
+  [col]
+  (cond
+    (map? col)
+      (doto
+        (java.util.HashMap.)
+        (.putAll col))
+    (set? col)
+      (java.util.HashSet. col)
+    (or (list? col) (vector? col))
+      (java.util.ArrayList. col)))
+
+(defn- invoke
+  [pojo method v]
+  (.invoke method pojo
+    (into-array
+      ;; misbehaving S3Client mutates the coll
+      (if (and (coll? v)
+               (not (nil? *client-class*))
+               (= "AmazonS3Client" (.getSimpleName *client-class*)))
+        (if (and (> (count (.getParameterTypes method)) 1)
+                 (sequential? v))
+          (to-java-coll v)
+          [(to-java-coll v)])
+          [v])))
+  true)
+
+(defn kw->str [k]
+  (if (keyword? k) (name k) k))
+
+(defn- populate
+  [types key props]
+  (let [type (-> types key last)]
+    (assert type (str "Bad data type - there is no " key " in " types))
+    (if (contains? @coercions type)
+      (coerce-value props type)
+      (set-fields (new-instance type) props))))
+
+(defn- unmarshall
+  "Transform Clojure data to the required Java objects."
+  [types col]
+  (let [type (last (or (:actual types)
+                       (:generic types)))
+        pp   (partial populate types :actual)]
+    (try
+      (if (fabric-package? type)
+       (if (map? col)
+         (if (contains? types :actual)
+           (if (< (:depth types) 3)
+             (apply assoc {}
+                    (interleave (fmap kw->str (apply vector (keys col)))
+                                (fmap pp (apply vector (vals col)))))
+             (apply assoc {}
+                    (interleave (fmap kw->str (apply vector (keys col)))
+                                [(fmap #(populate {:generic [type]}
+                                                  :generic
+                                                  %)
+                                       (first (apply vector (vals col))))])))
+           (populate types :generic col))
+         (if (and (contains? types :actual)
+                  (= (:depth types) 3))
+           (fmap #(fmap pp %) col)
+           (fmap pp col)))
+       (if (and (contains? types :actual)
+                (fabric-package? type))
+         (fmap pp col)
+         (fmap #(coerce-value % type) col)))
+      (catch Throwable e
+        (throw (RuntimeException. (str
+                                    "Failed to create an instance of "
+                                    (.getName type)
+                                    " from " col
+                                    " due to " e
+                                    ". Make sure the data matches an existing constructor and setters.")))))))
+
+(defn- invoke-method
+  [pojo v method]
+  (let [f       (partial invoke pojo method)
+        types   (paramter-types method)
+        generic (last (:generic types))]
+    (if (and
+          (coll? v)
+          (not (contains? @coercions generic)))
+      (f (unmarshall types v))
+      (if (instance? generic v)
+        (f v)
+        (f (coerce-value v generic))))))
+
+(defn set-fields
+  "Returns the populated AWS *Request bean with 'args' as
+   the values. args is a map with keywords as keys and any
+   type values. Complex values will be recursively resolved
+   to the corresponding method calls on the object graph."
+  [pojo args]
+  (doseq [[k v] args]
+    (try
+      (->> (find-methods pojo k v)
+           (some (partial invoke-method pojo v)))
+      (catch Throwable e
+        (throw (ex-info
+                 (str "Error setting " k ": " (.getMessage e) ". Perhaps the value isn't compatible with the setter?")
+                 {:property k, :value v}
+                 e)))))
+  pojo)
+
+(defn- create-bean
+  [clazz args]
+  (-> clazz new-instance (set-fields args)))
+
+(defn- create-request-bean
+  "Create a new instance of an AWS *Request style Java
+   bean passed as the argument to a method call on the
+   Amazon*Client class. (Note that we assume all AWS
+   service calls take at most a single argument.)"
+  [method args]
+  (let [clazz (first (.getParameterTypes method))]
+    (if (contains? @coercions clazz)
+        (coerce-value (into {} args) clazz)
+        (create-bean clazz args))))
+
+
+(defprotocol IMarshall
+  "Defines the contract for converting Java types to Clojure
+  data. All return values from AWS service calls are
+  marshalled. As such, the AWS service-specific namespaces
+  will frequently need to implement this protocol in order
+  to provide convenient data representations. See also the
+  register-coercions function for coercing Clojure data to
+  Java types."
+  (marshall [obj]))
+
+(defn- getter?
+  [method]
+  (let [name (.getName method)
+        type (.getName (.getReturnType method))]
+    (or (and
+          (.startsWith name "get")
+          (= 0 (count (.getParameterTypes method))))
+        (and
+          (.startsWith name "is")
+          (= "boolean" type)))))
+
+(defn accessors
+  "Returns a vector of getters or setters for the class."
+  [clazz getters?]
+  (reduce
+    #(if (or
+           (and getters? (getter? %2))
+           (and (not getters?)
+                (.startsWith (.getName %2) "set")))
+      (conj % %2)
+      %)
+    []
+    (.getDeclaredMethods clazz)))
+
+(defn- prop->name
+  [method]
+  (let [name (.getName method)]
+    (if (.startsWith name "is")
+      (str (.substring name 2) "?")
+      (.substring name 3))))
+
+(defn get-fields
+  "Returns a map of all non-null values returned by
+  invoking all public getters on the specified object."
+  [obj]
+  (let [no-arg (make-array Object 0)]
+    (into {}
+      (for [m (accessors (class obj) true)]
+        (let [r (marshall (.invoke m obj no-arg))]
+          (if-not (nil? r)
+            (hash-map
+              (camel->keyword (prop->name m))
+              r)))))))
+
+(extend-protocol IMarshall
+  nil
+  (marshall [obj] nil)
+
+  java.util.Map
+  (marshall [obj]
+    (if-not (empty? obj)
+      (apply assoc {}
+        (interleave
+          (fmap #(if (string? %) (keyword %) %)
+                (apply vector (keys obj)))
+          (fmap marshall
+                (apply vector (vals obj)))))))
+
+  java.util.Collection
+  (marshall [obj]
+    (if (instance? clojure.lang.IPersistentSet obj)
+      obj
+      (fmap marshall (apply vector obj))))
+
+  java.util.Date
+  (marshall [obj] (DateTime. (.getTime obj)))
+
+  ; `false` boolean objects (i.e. (Boolean. false)) come out of e.g.
+  ; .doesBucketExist, which wreak havoc on Clojure truthiness
+  Boolean
+  (marshall [obj] (when-not (nil? obj) (.booleanValue obj)))
+
+  Object
+  (marshall [obj]
+    (if (fabric-package? (class obj))
+        (get-fields obj)
+        obj)))
+
+(defn- use-aws-request-bean?
+  [method args]
+  (let [types (.getParameterTypes method)]
+    (and (or (map? args) (< 1 (count args)))
+         (< 0 (count types))
+         (or (map? args)
+             (and
+                (or (and
+                      (even? (count args))
+                      (not= java.io.File (last types)))
+                    (and
+                      (odd? (count args))
+                      (= java.io.File (last types)))) ; s3 getObject() support
+                (some keyword? args)))
+         (or (fabric-package? (first types))
+             (and (fabric-package? (last types))
+                  (not (< (count types) (count args))))))))
+
+(defn- prepare-args
+  [method args]
+  (let [types (.getParameterTypes method)
+        num   (count types)]
+    (if (and (empty? args) (= 0 num))
+      (into-array Object args)
+      (if (= num (count args))
+        (into-array Object
+          (map #(if (and (fabric-package? %2) (seq (.getConstructors %2)))
+                  ; must be a concrete, instantiatable class
+                  (if (contains? @coercions %2)
+                      (coerce-value % %2)
+                      (create-bean %2 %))
+                  (coerce-value % %2))
+               (vec args)
+               types))
+        (if (use-aws-request-bean? method args)
+          (cond
+            (= 1 num)
+            (into-array Object
+                        [(create-request-bean
+                            method
+                            (seq (apply hash-map args)))])
+            (and (fabric-package? (first types))
+                 (= 2 num)
+                 (= File (last types)))
+            (into-array Object
+                        [(create-request-bean
+                            method
+                            (seq (apply hash-map (butlast args))))
+                         (last args)])))))))
+
+
+(defn- args-from
+  "Function arguments take an optional first parameter map
+  of AWS credentials. Addtional parameters are either a map,
+  or seq of keys and values."
+  [arg-seq]
+  (let [args (first arg-seq)]
+    (cond
+      (or (and (or (map? args)
+                   (map? (first args)))
+               (or (contains? (first args) :access-key)
+                   (contains? (first args) :endpoint)
+                   (contains? (first args) :profile)
+                   (contains? (first args) :client-config)))
+          (instance? AWSCredentialsProvider (first args))
+          (instance? AWSCredentials (first args)))
+      {:args (if (-> args rest first map?)
+                 (if (-> args rest first empty?)
+                     {}
+                     (mapcat identity (-> args rest args-from :args)))
+                 (rest args))
+       :credential (if (map? (first args))
+                       (dissoc (first args) :client-config)
+                       (first args))
+       :client-config (:client-config (first args))}
+      (map? (first args))
+      {:args (let [m (mapcat identity (first args))]
+               (if (seq m) m {}))}
+      :default {:args args})))
+
+(swap! client-config assoc :transfer-manager-fn (memoize transfer-manager*))
+
+(defn candidate-client
+  [clazz args]
+  (let [cred-bound (or *credentials* (:credential args))
+        credential (if (map? cred-bound)
+                             (merge @credential cred-bound)
+                             (or cred-bound @credential))
+        config-bound (or *client-config* (:client-config args))
+        client-config (merge @client-config config-bound)
+        encryption-client (:encryption-client-fn client-config)
+        amazon-client (:amazon-client-fn client-config)
+        transfer-manager (:transfer-manager-fn client-config)
+        crypto (if (even? (count (:args args)))
+                   (:encryption (apply hash-map (:args args))))
+        client  (if (and crypto (or (= (.getSimpleName clazz) "AmazonS3Client")
+                                    (= (.getSimpleName clazz) "TransferManager")))
+                    (delay (encryption-client crypto credential client-config))
+                    (delay (amazon-client clazz credential client-config)))]
+        (if (= (.getSimpleName clazz) "TransferManager")
+            (transfer-manager credential client-config crypto)
+            @client)))
+
+
+(defn- fn-call
+  "Returns a function that reflectively invokes method on
+   clazz with supplied args (if any). The 'method' here is
+   the Java method on the Amazon*Client class."
+  [clazz method & arg]
+  (binding [*client-class* clazz]
+    (let [args    (args-from arg)
+          arg-arr (prepare-args method (:args args))
+          client  (delay (candidate-client clazz args))]
+      (fn []
+        (try
+          (let [java (.invoke method @client arg-arr)
+                cloj (marshall java)]
+            (if (and
+                  @root-unwrapping
+                  (map? cloj)
+                  (= 1 (count (keys cloj))))
+              (-> cloj first second)
+              cloj))
+          (catch InvocationTargetException ite
+            (throw (.getTargetException ite))))))))
+
+(defn- types-match-args
+  [args method]
+  (let [types (.getParameterTypes method)]
+    (if (and (= (count types) (count args))
+             (every? identity (map instance? types args)))
+        method)))
+
+(defn- coercible? [type]
+  (and (contains? @coercions type)
+       (not (re-find #"java\.lang" (str type)))))
+
+(defn- choose-from [possible]
+  (if (= 1 (count possible))
+      (first possible)
+      (first
+        (sort-by
+          (fn [method]
+            (let [types (.getParameterTypes method)]
+              (cond
+                (some coercible? types) 1
+                (some #(= java.lang.Enum (.getSuperclass %)) types) 2
+                :else 3)))
+          possible))))
+
+(defn possible-methods
+  [methods args]
+  (filter
+    (fn [method]
+      (let [types (.getParameterTypes method)
+            num   (count types)]
+        (if (or
+              (and (empty? args) (= 0 num))
+              (use-aws-request-bean? method args)
+              (and
+                (= num (count args))
+                (not (keyword? (first args)))
+                (not (fabric-package? (first types)))))
+          method
+          false)))
+    methods))
+
+(defn- best-method
+  "Finds the appropriate method to invoke in cases where
+  the Amazon*Client has overloaded methods by arity or type."
+  [methods & arg]
+  (let [args (:args (args-from arg))]
+    (or (some (partial types-match-args args) methods)
+        (choose-from (possible-methods methods args)))))
+
+(defn- clojure-case
+  "Similar to \"kabob case\" but the returned string is suitable for
+  reading a single symbol with `read-string`."
+  [string]
+  (-> string
+      ;; Replace the space between a non-upper-case letter and an
+      ;; upper-case letter with a dash.
+      (str/replace #"(?<=[^A-Z])(?=[A-Z])" "-")
+      ;; Remove anything that a Clojure reader would not accept in a
+      ;; symbol.
+      (str/replace #"[\\'\"\[\]\(\){}\s]" "")
+      (str/lower-case)))
+
+(defn- type-clojure-name
+  "Given a `java.lang.Class` return it's name in kabob case"
+  [type]
+  (let [type-name (last (.. type getName (split "\\.")))
+        type-name (if-let [;; Check for a type name like "[C" etc. 
+                           [_ name] (re-matches #"\[([A-Z]+)$" type-name)]
+                    name
+                    type-name)]
+    (clojure-case type-name)))
+
+(defn- parameter-clojure-name
+  "Given a `java.lang.reflect.Parameter` return it's name in kabob
+  case."
+  [parameter]
+  (if (. parameter isNamePresent)
+    (clojure-case (. parameter getName))
+    ;; The name will be synthesized so instead we'll derive
+    ;; it from it's type.
+    (type-clojure-name (. parameter getType))))
+
+(def ^{:arglists '([method])
+       :private true}
+  parameter-names
+  "Given a `java.lang.reflect.Method` return a list of it's parameter
+  names."
+  ;; The regular expression here will only match against version
+  ;; numbers that are 1.7.X and below.
+  (if (re-matches #"[^2-9]\.[1-7]\..+" (System/getProperty "java.version")) 
+    ;; Java 1.7 and below.
+    (fn [method]
+      (map type-clojure-name (. method getParameterTypes)))
+    ;; Java 1.8 and above.
+    (fn [method]
+      (map parameter-clojure-name (. method getParameters)))))
+
+(defn- method-arglist
+  "Derives a Clojure `:arglist` vector from a
+  `java.lang.reflect.Method`."
+  [method]
+  (let [names (parameter-names method)
+        ;; This will help determine when parameter names should be
+        ;; suffixed with an index i.e. `parameter-1`. Suffixing is
+        ;; necessary when parameter names are synthesized from their
+        ;; type names and the likelihood duplicates is high.
+        name-frequency (frequencies names)]
+    (loop [names names
+           ;; This map keeps track of the index of names when they
+           ;; appear more than once.
+           name-index {}
+           arglist []]
+      (if (empty? names)
+        arglist
+        (let [[name & names*] names]
+          (if (= (name-frequency name) 1)
+            (let [arg-symbol (symbol name)
+                  arglist* (conj arglist arg-symbol)]
+              (recur names*
+                     name-index
+                     arglist*))
+            ;; The parameter name appears more than once so we need to
+            ;; attach an index to it and update our name-index for the
+            ;; next parameter with the same name.
+            (let [index (get name-index name 1)
+                  name-index* (assoc name-index name (inc index))
+                  arg-symbol (symbol (str name "-" index))
+                  arglist* (conj arglist arg-symbol)]
+              (recur names*
+                     name-index*
+                     arglist*))))))))
+
+(defn intern-function
+  "Interns into ns, the symbol mapped to a Clojure function
+   derived from the java.lang.reflect.Method(s). Overloaded
+   methods will yield a variadic Clojure function."
+  [client ns fname methods]
+  (intern ns (with-meta (symbol (name fname))
+               {:amazonica/client client
+                :amazonica/methods methods
+                :arglists (sort (map method-arglist methods))})
+    (fn [& args]
+      (if-let [method (best-method methods args)]
+        (if-not args
+          ((fn-call client method))
+          ((fn-call client method args)))
+        (throw (IllegalArgumentException.
+                 (format "Could not determine best method to invoke for %s using arguments %s"
+                         (name fname) args)))))))
+
+(defn- client-methods
+  "Returns a map with keys of idiomatic Clojure hyphenated keywords
+  corresponding to the public Java method names of the class argument, vals are
+  vectors of java.lang.reflect.Methods."
+  [client]
+  (->> (.getDeclaredMethods client)
+       (remove (fn [method]
+                 (let [mods (.getModifiers method)]
+                   (or (.isSynthetic method)
+                       (Modifier/isPrivate mods)
+                       (Modifier/isProtected mods)
+                       (Modifier/isStatic mods)))))
+       (group-by #(camel->keyword (.getName %)))))
+
+(defn- show-functions [ns]
+  (intern ns (symbol "show-functions")
+    (fn []
+      (->> (ns-publics ns)
+           sort
+           (map (comp println first))))))
+
+(defn set-client
+  "Intern into the specified namespace all public methods
+   from the Amazon*Client class as Clojure functions."
+  [client ns]
+  (show-functions ns)
+  (intern ns 'client-class client)
+  (doseq [[fname methods] (client-methods client)
+          :let [the-var (intern-function client ns fname methods)
+                fname2 (-> methods first .getName camel->keyword)]]
+    (when (not= fname fname2)
+      (let [the-var2 (intern-function client ns fname2 methods)]
+        (alter-meta! the-var assoc :amazonica/deprecated-in-favor-of the-var2)))))
 #_
 (comment
   ;; Copy from fabcar example

    (get-order-transaction-result tx-future #(println "OK" %) #(println "ERROR" %)))
  
  )


#_
(comment
  ;; Copy from fabcar example
  (defonce options
    {:channel-id "mychannel" :chaincode-id "fabcar" :network-url "grpc://localhost:7051"
     :peer-admin
     {:name "PeerAdmin"
      :mspid "Org1MSP" :roles nil :affiliation "" 
      :enrollment-secret "" 
      :enrollment {:signing-identity "cd96d5260ad4757551ed4a5a991e62130f8008a0bf996e4e4b84cd097a747fec" 
                   :identity {:certificate "-----BEGIN CERTIFICATE-----\nMIICGDCCAb+gAwIBAgIQFSxnLAGsu04zrFkAEwzn6zAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xNzA4MzEwOTE0MzJaFw0yNzA4MjkwOTE0MzJa\nMFsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMR8wHQYDVQQDDBZBZG1pbkBvcmcxLmV4YW1wbGUuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV1dfmKxsFKWo7o6DNBIaIVebCCPAM9C/\nsLBt4pJRre9pWE987DjXZoZ3glc4+DoPMtTmBRqbPVwYcUvpbYY8p6NNMEswDgYD\nVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0jBCQwIoAgQjmqDc122u64\nugzacBhR0UUE0xqtGy3d26xqVzZeSXwwCgYIKoZIzj0EAwIDRwAwRAIgXMy26AEU\n/GUMPfCMs/nQjQME1ZxBHAYZtKEuRR361JsCIEg9BOZdIoioRivJC+ZUzvJUnkXu\no2HkWiuxLsibGxtE\n-----END CERTIFICATE-----\n"}}}})

  ;; 1. Client setup
  (defonce cli (hf-client/create-new-instance))
  (hf-client/set-crypto-suite cli (crypto-suite/get-crypto-suite))
 
  ;; 2. User context
  (defonce peer-admin-enrollment
    (reify Enrollment
      ;; FIXME: enrollment!
      ;; How to iniitiate key and cert?
      (getKey [this]
        (-> (slurp (str "resources/creds/"
                     (->  options
                          (get-in [:peer-admin :enrollment :signing-identity])
                          (str "-priv"))))
            (keys/str->private-key)))
      (getCert [this]
        (get-in options [:peer-admin :enrollment :identity :certificate]))))
  
  (defonce peer-admin
    (reify User
      (getName [this]
        (get-in options [:peer-admin :name]))
      ;; Set<String>
      (getRoles [this]
        (get-in options [:peer-admin :roles]))
      (getAccount [this]
        nil)
      (getAffiliation [this]
        (get-in options [:peer-admin :affiliation]))
      (getEnrollment [this]
        peer-admin-enrollment)
      (getMspId [this]
        (get-in options [:peer-admin :mspid]))))
  
  (hf-client/set-user-context cli peer-admin)

  
  ;; 2. Channel
  (defonce chan (hf-client/new-channel cli (:channel-id options)))
  (channel/add-orderer chan (hf-client/new-orderer cli "orderer0" "grpc://localhost:7050"))
  (channel/add-peer chan (hf-client/new-peer cli "peer0" "grpc://localhost:7051"))
  (channel/initialize chan)

  ;; 3. Request
  (defonce req (hf-client/new-transaction-proposal-request cli))

  (defonce chaincode-id (-> (chaincode-id/new-builder)
                            (chaincode-id/set-name "fabcar")
                            (chaincode-id/set-version "1.0")
                            (chaincode-id/set-path "resources/")
                            (chaincode-id/build)))
  (transaction-request/set-chaincode-id req chaincode-id)
  (transaction-request/set-fcn req "createCar")
  ;; timeout
  (transaction-request/set-proposal-wait-time req 10000)
  ;; FIXME: marshall
  (transaction-request/set-args req (java.util.ArrayList. ["CAR10" "Chevy" "Volt" "Red" "Nick"]))

  (defonce resp (channel/send-transaction-proposal chan req))
  (defonce future1 (channel/send-transaction chan resp (hf-client/get-user-context cli)))
  (.get future1)
  )


        (swap! lru-cache cache/miss k new-entry)
        new-entry))))

(defonce client-lru-cache
  ;; FIXME: magic number
  (atom (cache/lru-cache-factory {} :threshold 64)))

(defn make-hf-client [msp-id name priv-key cert {:keys [roles account affiliation]}]
  (let [client (HFClient/createNewInstance)]
    (.setCryptoSuite client (CryptoSuite$Factory/getCryptoSuite))
    (.setUserContext client (reify User
                               (getName [this] name)
                               ;; FIXME: Set<String>
                               (getRoles [this] roles)
                               (getAccount [this] account)
                               (getAffiliation [this] affiliation)
