/*-
 * ========================LICENSE_START=================================
 * restheart-mongodb
 * %%
 * Copyright (C) 2014 - 2022 SoftInstigate
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * =========================LICENSE_END==================================
 */
package org.restheart.mongodb.db;

import java.util.List;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.bson.BsonDocument;
import com.mongodb.client.MongoCollection;
import static org.fusesource.jansi.Ansi.Color.GREEN;
import static org.fusesource.jansi.Ansi.Color.RED;
import static org.fusesource.jansi.Ansi.ansi;
import org.restheart.cache.Cache;
import org.restheart.cache.CacheFactory;
import org.restheart.mongodb.MongoServiceConfiguration;
import org.restheart.utils.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 *
 * @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
 */
public class GetCollectionCache {

    private static final Logger LOGGER = LoggerFactory.getLogger(GetCollectionCache.class);

    private static final long CACHE_SIZE = MongoServiceConfiguration.get().getGetCollectionCacheSize();
    private static final long CACHE_TTL = MongoServiceConfiguration.get().getGetCollectionCacheTTL();

    /**
     *
     * @return
     */
    public static GetCollectionCache getInstance() {
        return DBCursorPoolSingletonHolder.INSTANCE;
    }

    private final Cache<GetCollectionCacheKey, List<BsonDocument>> cache;

    private GetCollectionCache() {
        cache = CacheFactory.createLocalCache(CACHE_SIZE, Cache.EXPIRE_POLICY.AFTER_WRITE, CACHE_TTL);

        if (LOGGER.isTraceEnabled()) {
            // print stats every 1 minute
            Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
                getCacheSizes().forEach((s, c) -> {
                    LOGGER.debug("get collection cache size: {}\t{}", s, c);
                });

                LOGGER.trace("get collection cache entries: {}", cache.asMap().keySet());
            }, 1, 1, TimeUnit.MINUTES);
        }
    }

    public synchronized void put(GetCollectionCacheKey key, List<BsonDocument> value) {
        cache.put(key, value);
    }

    public synchronized Pair<GetCollectionCacheKey, List<BsonDocument>> find(GetCollectionCacheKey key) {
        return _get(key, false);
    }

    public synchronized List<BsonDocument> get(GetCollectionCacheKey key) {
        return _get(key, false).getValue();
    }

    public synchronized List<BsonDocument> remove(GetCollectionCacheKey key) {
        return _get(key, true).getValue();
    }

    /**
     *
     * @param key
     * @param allocationPolicy
     * @return
     */
    private synchronized Pair<GetCollectionCacheKey, List<BsonDocument>> _get(GetCollectionCacheKey key, boolean remove) {
        // return the first entry with all avaible documents
        var _bestKey = cache.asMap().keySet().stream()
            .filter(cacheKeyFilter(key))
            .findFirst();

        if (_bestKey.isPresent()) {
            var _cached = remove ? cache.remove(_bestKey.get()) : cache.get(_bestKey.get());

            if (_cached != null && _cached.isPresent()) {
                LOGGER.debug("{} cached documents. cache entry id {}", ansi().fg(GREEN).bold().a("found").reset().toString(), _bestKey.get().cursorId());
                return new Pair<>(_bestKey.get(), _cached.get());
            } else {
                LOGGER.debug("{} cached documents.", ansi().fg(RED).bold().a("no").reset().toString());
                return null;
            }
        } else {
            LOGGER.debug(ansi().fg(RED).bold().a("missed").reset().toString() + " get collection cache.");
            return null;
        }
    }

    public synchronized void invalidate(GetCollectionCacheKey key) {
        cache.invalidate(key);
    }

    public void invalidateAll(String db, String coll) {
        cache.asMap().keySet().stream()
            .filter(k -> k.collection().getNamespace().getDatabaseName().equals(db))
            .filter(k -> k.collection().getNamespace().getCollectionName().equals(coll))
            .forEach(k -> cache.invalidate(k));
    }

    public void invalidateAll(MongoCollection<?> coll) {
        cache.asMap().keySet().stream()
            .filter(k -> k.collection().getNamespace().equals(coll.getNamespace()))
            .forEach(k -> cache.invalidate(k));
    }

    private Predicate<? super GetCollectionCacheKey> cacheKeyFilter(GetCollectionCacheKey requested) {
        return cached
            -> Objects.equals(cached.collection().getNamespace(), requested.collection().getNamespace())
            && Objects.equals(cached.filter(), requested.filter())
            && Objects.equals(cached.sort(), requested.sort())
            && Objects.equals(cached.keys(), requested.keys())
            && cached.from() <= requested.from()
            && cached.to() >= requested.to();
    }

    private TreeMap<String, Long> getCacheSizes() {
        return new TreeMap<>(cache.asMap()
            .keySet()
            .stream()
            .collect(Collectors.groupingBy(GetCollectionCacheKey::getCacheStatsGroup, Collectors.counting())));
    }

    private static class DBCursorPoolSingletonHolder {
        private static final GetCollectionCache INSTANCE = new GetCollectionCache();

        private DBCursorPoolSingletonHolder() {
        }
    };
}

@@ -1838,16 +1838,64 @@
 E_SIZE =
+ MongoServiceConfiguration.get() == null ? 100 :
  MongoSe
@@ -1989,16 +1989,67 @@
 HE_TTL =
+ MongoServiceConfiguration.get() == null ? 10_000 :
  MongoSe

Fix GetCollectionCache initialization error in JUnit tests