/**
 * Copyright 2021 VMware, Inc.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * https://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.micrometer.core.instrument.binder.cache;

import static java.util.Objects.requireNonNull;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.lang.NonNullApi;
import io.micrometer.core.lang.NonNullFields;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.concurrent.TimeUnit;

/**
 * A {@link StatsCounter} instrumented with Micrometer. This will provide more detailed metrics than using
 * {@link CaffeineCacheMetrics}.
 * <p>
 * Note that this doesn't instrument the cache's size by default. Use {@link #registerSizeMetric(Cache)} to do so after
 * the cache has been built.
 * <p>
 * Use {@link com.github.benmanes.caffeine.cache.Caffeine#recordStats} to supply this class to the cache builder:
 * <pre>{@code
 * MeterRegistry registry = ...;
 * Cache<Key, Graph> graphs = Caffeine.newBuilder()
 *     .maximumSize(10_000)
 *     .recordStats(() -> new CaffeineStatsCounter(registry, "graphs"))
 *     .build();
 * }</pre>
 *
 * @author Ben Manes
 * @author John Karp
 * @author Johnny Lim
 * @see CaffeineCacheMetrics
 * @since 1.7.0
 */
@NonNullApi
@NonNullFields
public final class CaffeineStatsCounter implements StatsCounter {

    private final MeterRegistry registry;
    private final Tags tags;

    private final Counter hitCount;
    private final Counter missCount;
    private final Timer loadSuccesses;
    private final Timer loadFailures;
    private final EnumMap<RemovalCause, DistributionSummary> evictionMetrics;

    /**
     * Constructs an instance for use by a single cache.
     *
     * @param registry  the registry of metric instances
     * @param cacheName will be used to tag metrics with "cache".
     */
    public CaffeineStatsCounter(MeterRegistry registry, String cacheName) {
        this(registry, cacheName, Tags.empty());
    }

    /**
     * Constructs an instance for use by a single cache.
     *
     * @param registry  the registry of metric instances
     * @param cacheName will be used to tag metrics with "cache".
     * @param extraTags tags to apply to all recorded metrics.
     */
    public CaffeineStatsCounter(MeterRegistry registry, String cacheName, Iterable<Tag> extraTags) {
        requireNonNull(registry);
        requireNonNull(cacheName);
        requireNonNull(extraTags);
        this.registry = registry;
        this.tags = Tags.concat(extraTags, "cache", cacheName);

        hitCount = Counter.builder("cache.gets").tag("result", "hit").tags(tags)
                .description("The number of times cache lookup methods have returned a cached value.")
                .register(registry);
        missCount = Counter.builder("cache.gets").tag("result", "miss").tags(tags)
                .description("The number of times cache lookup methods have returned an uncached (newly loaded) value.")
                .register(registry);
        loadSuccesses = Timer.builder("cache.loads").tag("result", "success").tags(tags)
                .description("Successful cache loads.").register(registry);
        loadFailures = Timer.builder("cache.loads").tag("result", "failure").tags(tags)
                .description("Failed cache loads.").register(registry);

        evictionMetrics = new EnumMap<>(RemovalCause.class);
        Arrays.stream(RemovalCause.values()).forEach(cause -> evictionMetrics.put(
                cause,
                DistributionSummary.builder("cache.evictions").tag("cause", cause.name()).tags(tags)
                        .description("Entries evicted from cache.").register(registry)));
    }

    /**
     * Register a gauge for the size of the given cache.
     *
     * @param cache cache to register a gauge for its size
     */
    public void registerSizeMetric(Cache<?,?> cache) {
        Gauge.builder("cache.size", cache, Cache::estimatedSize).tags(tags)
                .description("The approximate number of entries in this cache.")
                .register(registry);
    }

    @Override
    public void recordHits(int count) {
        hitCount.increment(count);
    }

    @Override
    public void recordMisses(int count) {
        missCount.increment(count);
    }

    @Override
    public void recordLoadSuccess(long loadTime) {
        loadSuccesses.record(loadTime, TimeUnit.NANOSECONDS);
    }

    @Override
    public void recordLoadFailure(long loadTime) {
        loadFailures.record(loadTime, TimeUnit.NANOSECONDS);
    }

    @SuppressWarnings("deprecation")
    @Override
    public void recordEviction() {
    }

    @Override
    public void recordEviction(int weight, RemovalCause cause) {
        evictionMetrics.get(cause).record(weight);
    }

    @Override
    public CacheStats snapshot() {
        return new CacheStats(
                (long) hitCount.count(),
                (long) missCount.count(),
                loadSuccesses.count(),
                loadFailures.count(),
                (long) loadSuccesses.totalTime(TimeUnit.NANOSECONDS)
                        + (long) loadFailures.totalTime(TimeUnit.NANOSECONDS),
                evictionMetrics.values().stream().mapToLong(DistributionSummary::count).sum(),
                (long) evictionMetrics.values().stream().mapToDouble(DistributionSummary::totalAmount).sum()
        );
    }

    @Override
    public String toString() {
        return snapshot().toString();
    }
}

@@ -5550,30 +5550,16 @@
 ation%22)%0A
-    @Override%0A
     publ
@@ -5793,20 +5793,16 @@
  return 
-new 
 CacheSta
@@ -5803,16 +5803,19 @@
 cheStats
+.of
 (%0A      

Support Caffeine 3 (#2478)