package cn.lihongjie.coal.common;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.CaffeineSpec;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Script;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;

@Slf4j
public class GroovyScriptManager {

    private static final AtomicInteger counter = new AtomicInteger(0);
    private final String name;
    private final Cache<String, Class> scriptCache;
    private final GroovyClassLoader groovyClassLoader;
    private boolean logCompileTime = false;

    public GroovyScriptManager() {
        this(true);
    }

    public GroovyScriptManager(boolean logCompileTime) {
        this((c) -> {}, logCompileTime);
    }

    public GroovyScriptManager(
            Consumer<CompilerConfiguration> configCustomizer, boolean logCompileTime) {
        this(null, configCustomizer, logCompileTime);
    }

    public GroovyScriptManager(
            String name, Consumer<CompilerConfiguration> configCustomizer, boolean logCompileTime) {
        this(name, "maximumSize=1000,expireAfterAccess=1h", configCustomizer, logCompileTime);
    }

    public GroovyScriptManager(
            String name,
            String specification,
            Consumer<CompilerConfiguration> configConsumer,
            boolean logCompileTime) {

        this.name = StringUtils.defaultIfBlank(name, "GroovyScriptManager-" + counter.get());
        scriptCache = Caffeine.from(CaffeineSpec.parse(specification)).build();

        CompilerConfiguration config = new CompilerConfiguration();
        configConsumer.accept(config);

        this.groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader(), config);
        this.logCompileTime = logCompileTime;

        counter.incrementAndGet();

        log.info("GroovyScriptManager: {} created", name);
    }

    /** 高性能版本API, 允许自定义缓存key */
    public Class compile(String key, Supplier<String> scriptSupplier) {

        StringBuilder logText = new StringBuilder();

        logText.append("GroovyScriptManager: ").append(name).append("\n");

        logText.append("compile groovy script: ").append(key).append("\n");

        long start = System.currentTimeMillis();

        try {

            return scriptCache.get(
                    key,
                    k -> {
                        long getStart = System.currentTimeMillis();
                        String script = scriptSupplier.get();

                        long getEnd = System.currentTimeMillis();

                        logText.append("getScriptTime: ").append(getEnd - getStart).append("ms\n");
                        GroovyCodeSource codeSource =
                                new GroovyCodeSource(script, key, "/groovy/script");
                        codeSource.setCachable(false);

                        long parseStart = System.currentTimeMillis();
                        Class parsedClass = this.groovyClassLoader.parseClass(codeSource);
                        long parseEnd = System.currentTimeMillis();

                        logText.append("parseTime: ").append(parseEnd - parseStart).append("ms\n");
                        return parsedClass;
                    });
        } finally {
            long end = System.currentTimeMillis();

            logText.append("totalTime: ").append(end - start).append("ms");
        }
    }

    public Script newInstance(String key, Supplier<String> scriptSupplier) {

        Class aClass = compile(key, scriptSupplier);

        Object o = ReflectUtils.newInstance(aClass);

        if (o instanceof Script) {
            return (Script) o;
        } else {
            throw new IllegalArgumentException(
                    "script must be instance of groovy.lang.Script, get " + aClass.getName());
        }
    }

    public void runWithScript(
            String key, Supplier<String> scriptSupplier, Consumer<Script> scriptCustomizer) {
        Script script = newInstance(key, scriptSupplier);

        scriptCustomizer.accept(script);

        script.run();
    }

    /** 易用性API, key 为 脚本内容的md5 */
    public Class compile(Supplier<String> scriptSupplier) {
        return compile(
                DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
                scriptSupplier);
    }

    public Class compile(String script) {
        return compile(
                DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)), () -> script);
    }

    public Script newInstance(Supplier<String> scriptSupplier) {
        return newInstance(
                DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
                scriptSupplier);
    }

    public Script newInstance(String script) {
        return newInstance(
                DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)), () -> script);
    }

    public void runWithScript(Supplier<String> scriptSupplier, Consumer<Script> scriptCustomizer) {
        runWithScript(
                DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
                scriptSupplier,
                scriptCustomizer);
    }

    public void runWithScript(String script, Consumer<Script> scriptCustomizer) {
        runWithScript(
                DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)),
                () -> script,
                scriptCustomizer);
    }

    public void invalidateAll() {
        scriptCache.invalidateAll();
    }

    public void invalidate(String key) {
        scriptCache.invalidate(key);
    }

    @SneakyThrows
    public void destroy() {
        scriptCache.invalidateAll();
        groovyClassLoader.close();
    }
}
