;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC

(ns app.util.cache
  "In-memory cache backed by Caffeine"
  (:refer-clojure :exclude [get])
  (:require
   [app.util.time :as dt]
   [promesa.exec :as px])
  (:import
   com.github.benmanes.caffeine.cache.AsyncCache
   com.github.benmanes.caffeine.cache.Cache
   com.github.benmanes.caffeine.cache.Caffeine
   com.github.benmanes.caffeine.cache.RemovalListener
   com.github.benmanes.caffeine.cache.stats.CacheStats
   java.time.Duration
   java.util.concurrent.Executor
   java.util.function.Function))

(set! *warn-on-reflection* true)

(defprotocol ICache
  (get [_ k] [_ k load-fn] "get cache entry")
  (invalidate! [_] [_ k] "invalidate cache"))

(defprotocol ICacheStats
  (stats [_] "get stats"))

(defn- create-listener
  [f]
  (reify RemovalListener
    (onRemoval [_ key val cause]
      (when val
        (f key val cause)))))

(defn- get-stats
  [^Cache cache]
  (let [^CacheStats stats (.stats cache)]
    {:hit-rate (.hitRate stats)
     :hit-count (.hitCount stats)
     :req-count (.requestCount stats)
     :miss-count (.missCount stats)
     :miss-rate  (.missRate stats)}))

(defn create
  [& {:keys [executor on-remove max-size keepalive]}]
  (let [cache (as-> (Caffeine/newBuilder) builder
                (if (fn? on-remove) (.removalListener builder (create-listener on-remove)) builder)
                (if executor  (.executor builder ^Executor (px/resolve-executor executor)) builder)
                (if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
                (if (int? max-size) (.maximumSize builder (long max-size)) builder)
                (.recordStats builder)
                (.buildAsync builder))
        cache (.synchronous ^AsyncCache cache)]
    (reify
      ICache
      (get [_ k]
        (.getIfPresent ^Cache cache ^Object k))
      (get [_ k load-fn]
        (.get ^Cache cache
              ^Object k
              ^Function (reify Function
                          (apply [_ k]
                            (load-fn k)))))
      (invalidate! [_]
        (.invalidateAll ^Cache cache))
      (invalidate! [_ k]
        (.invalidateAll ^Cache cache ^Object k))

      ICacheStats
      (stats [_]
        (get-stats cache)))))

(defn cache?
  [o]
  (satisfies? ICache o))
