/*
 *  This file is part of Player Analytics (Plan).
 *
 *  Plan is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License v3 as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Plan 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Plan. If not, see <https://www.gnu.org/licenses/>.
 */
package com.djrapitops.plan.delivery.webserver.cache;

import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.storage.file.ResourceCache;
import com.djrapitops.plugin.task.AbsRunnable;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.StringUtils;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Cache for any JSON data sent via {@link RootJSONResolver}.
 *
 * @author Rsl1122
 */
public class JSONCache {

    private static final Cache<String, byte[]> cache = Caffeine.newBuilder()
            .expireAfterAccess(2, TimeUnit.MINUTES)
            .build();

    private JSONCache() {
        // Static class
    }

    public static Response getOrCache(String identifier, Supplier<Response> jsonResponseSupplier) {
        byte[] found = cache.getIfPresent(identifier);
        if (found == null) {
            Response response = jsonResponseSupplier.get();
            cache.put(identifier, response.getBytes());
            return response;
        }
        return Response.builder()
                .setMimeType(MimeType.JSON)
                .setContent(found)
                .build();
    }

    public static String getOrCacheString(DataID dataID, UUID serverUUID, Supplier<String> stringSupplier) {
        String identifier = dataID.of(serverUUID);
        byte[] found = cache.getIfPresent(identifier);
        if (found == null) {
            String result = stringSupplier.get();
            cache.put(identifier, result.getBytes(StandardCharsets.UTF_8));
            return result;
        }
        return new String(found, StandardCharsets.UTF_8);
    }

    public static <T> Response getOrCache(DataID dataID, Supplier<T> objectSupplier) {
        return getOrCache(dataID.name(), () -> Response.builder()
                .setMimeType(MimeType.JSON)
                .setJSONContent(objectSupplier.get())
                .build());
    }

    public static <T> Response getOrCache(DataID dataID, UUID serverUUID, Supplier<T> objectSupplier) {
        return getOrCache(dataID.of(serverUUID), () -> Response.builder()
                .setMimeType(MimeType.JSON)
                .setJSONContent(objectSupplier.get())
                .build());
    }

    public static void invalidate(String identifier) {
        cache.invalidate(identifier);
    }

    public static void invalidate(DataID dataID) {
        invalidate(dataID.name());
    }

    public static void invalidate(UUID serverUUID, DataID... dataIDs) {
        for (DataID dataID : dataIDs) {
            invalidate(dataID.of(serverUUID));
        }
    }

    public static void invalidate(DataID dataID, UUID serverUUID) {
        invalidate(dataID.of(serverUUID));
    }

    public static void invalidateMatching(DataID... dataIDs) {
        Set<String> toInvalidate = Arrays.stream(dataIDs)
                .map(DataID::name)
                .collect(Collectors.toSet());
        for (String identifier : cache.asMap().keySet()) {
            for (String identifierToInvalidate : toInvalidate) {
                if (StringUtils.startsWith(identifier, identifierToInvalidate)) {
                    invalidate(identifier);
                }
            }
        }
    }

    public static void invalidateMatching(DataID dataID) {
        String toInvalidate = dataID.name();
        for (String identifier : cache.asMap().keySet()) {
            if (StringUtils.startsWith(identifier, toInvalidate)) {
                invalidate(identifier);
            }
        }
    }

    public static void invalidateAll() {
        cache.invalidateAll();
    }

    public static void cleanUp() {
        cache.cleanUp();
    }

    public static List<String> getCachedIDs() {
        List<String> identifiers = new ArrayList<>(cache.asMap().keySet());
        Collections.sort(identifiers);
        return identifiers;
    }

    @Singleton
    public static class CleanTask extends AbsRunnable {

        @Inject
        public CleanTask() {
            // Dagger requires inject constructor
        }

        @Override
        public void run() {
            cleanUp();
            ResourceCache.cleanUp();
        }
    }
}