(ns bbbg.util.core
  (:require
   [clojure.java.shell :refer [sh]]
   [clojure.string :as str])
  (:import
   java.util.UUID))

(defn remove-nils
  "Remove all keys with nil values from m"
  [m]
  (let [!m (transient m)]
    (doseq [[k v] m]
      (when (nil? v)
        (dissoc! !m k)))
    (persistent! !m)))


(defn alongside
  "Apply a pair of functions to the first and second element of a two element
  vector, respectively. The two argument form partially applies, such that:

  ((alongside f g) xy) ≡ (alongside f g xy)

  This is equivalent to (***) in haskell's Control.Arrow"
  ([f g] (partial alongside f g))
  ([f g [x y]] [(f x) (g y)]))

(defn map-kv
  "Map a pair of functions over the keys and values of a map, respectively.
  Preserves metadata on the incoming map.
  The two argument form returns a transducer that yields map-entries.

  (partial map-kv identity identity) ≡ identity"
  ([kf vf]
   (map (fn [[k v]]
          ;; important to return a map-entry here so that callers down the road
          ;; can use `key` or `val`
          (first {(kf k) (vf v)}))))
  ([kf vf m]
   (into (empty m) (map-kv kf vf) m)))

(defn filter-kv
  "Returns a map containing the elements of m for which (f k v) returns logical
  true. The one-argument form returns a transducer that yields map entries"
  ([f] (filter (partial apply f)))
  ([f m]
   (into (empty m) (filter-kv f) m)))

(defn map-keys
  "Map f over the keys of m. Preserves metadata on the incoming map. The
  one-argument form returns a transducer that yields map-entries."
  ([f] (map-kv f identity))
  ([f m] (map-kv f identity m)))

(defn keep-keys
  "Map f over the keys of m, keeping only those entries for which f does not
  return nil. Preserves metadata on the incoming map. The one-argument form
  returns a transducer that yields map-entries."
  ([f] (keep (fn [[k v]] (when-let [k' (f k)]
                          (first {k' v})))))
  ([f m] (into (empty m) (keep-keys f) m)))

(defn map-vals
  "Map f over the values of m. Preserves metadata on the incoming map. The
  one-argument form returns a transducer that yields map-entries."
  ([f] (map-kv identity f))
  ([f m] (map-kv identity f m)))

(defn map-keys-recursive [f x]
  (cond
    (map? x) (map-kv f (partial map-keys-recursive f) x)
    (sequential? x) (map (partial map-keys-recursive f) x)
    :else x))

(defn denamespace [x]
  (if (keyword? x)
    (keyword (name x))
    (map-keys-recursive denamespace x)))

(defn reverse-merge
  "Like `clojure.core/merge`, except duplicate keys from maps earlier in the
  argument list take precedence

    => (merge {:x 1} {:x 2})
    {:x 2}

    => (sut/reverse-merge {:x 1} {:x 2})
    {:x 1}"
  [& ms]
  (apply merge (reverse ms)))

(defn invert-map
  "Invert the keys and vals of m. Behavior with duplicate vals is undefined.

  => (sut/invert-map {:x 1 :y 2})
  {1 :x 2 :y}"
  [m]
  (into {} (map (comp vec reverse)) m))

(defn ->uuid
  "Converts x to uuid, returning nil if x is nil or empty"
  [x]
  (cond
    (not x) nil
    (uuid? x) x
    (and (string? x) (seq x))
    (UUID/fromString x)))

(defn key-by
  "Create a map from a seq obtaining keys via f

    => (sut/key-by :x [{:x 1} {:x 2 :y 3}])
    {1 {:x 1}, 2 {:x 2 :y 3}}"
  [f l]
  (into {} (map (juxt f identity)) l))

(defn distinct-by
  "Like clojure.core/distinct, but can take a function f by which
  distinctiveness is calculated"
  [distinction-fn coll]
  (let [step (fn step [xs seen]
               (lazy-seq
                ((fn [[f :as xs] seen]
                   (when-let [s (seq xs)]
                     (if (contains? seen (distinction-fn f))
                       (recur (rest s) seen)
                       (cons f (step (rest s) (conj seen (distinction-fn f)))))))
                 xs seen)))]
    (step coll #{})))

(defn pass [n]
  (let [{:keys [exit out err]} (sh "pass" n)]
    (if (= 0 exit)
      (str/trim out)
      (throw (Exception.
              (format "`pass` command failed\nStandard output:%s\nStandard Error:%s"
                      out
                      err))))))
