/*
 * Copyright © 2014-2017 EntIT Software LLC, a Micro Focus company (L.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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.cloudslang.runtime.impl.python;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.cloudslang.dependency.api.services.DependencyService;
import io.cloudslang.runtime.api.python.PythonEvaluationResult;
import io.cloudslang.runtime.api.python.PythonExecutionResult;
import io.cloudslang.runtime.impl.ExecutionEngine;
import io.cloudslang.runtime.impl.python.pool.ViburEmbeddedPythonPoolService;
import io.cloudslang.runtime.impl.python.pool.ViburEmbeddedPythonPoolServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.vibur.objectpool.util.ConcurrentLinkedQueueCollection;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import static io.cloudslang.runtime.api.python.enums.PythonStrategy.JYTHON;
import static io.cloudslang.runtime.api.python.enums.PythonStrategy.PYTHON_EXECUTOR;
import static io.cloudslang.runtime.api.python.enums.PythonStrategy.getPythonStrategy;
import static java.lang.Integer.getInteger;
import static java.lang.Math.max;
import static java.lang.Runtime.getRuntime;
import static java.util.concurrent.Executors.newFixedThreadPool;

/**
 * Uses io.cloudslang.runtime.impl.python.EmbeddedPythonExecutorWrapper that brings more security
 * than io.cloudslang.runtime.impl.python.PythonExecutor
 * For python that has no dependencies, it uses a pool of EmbeddedPythonExecutorWrapper instances,
 * that are totally isolated not using ThreadLocal at all.
 * For python with dependencies, it uses a dedicated cache of EmbeddedPythonExecutorWrapper.
 */
public class PythonExecutionPooledAndCachedEngine extends ExecutionEngine implements PythonExecutionEngine {

    @Autowired
    private DependencyService dependencyService;

    @Autowired
    @Qualifier("numberOfExecutionThreads")
    private Integer numberOfThreads;

    private ViburEmbeddedPythonPoolService pythonExecutorPool;
    private Cache<String, EmbeddedPythonExecutorWrapper> cachedExecutors;

    @PostConstruct
    public void init() {
        this.cachedExecutors = Caffeine.newBuilder()
                .maximumSize(getInteger("jython.executor.cacheSize", numberOfThreads * 4 / 3))
                .build();
        doSetPythonExecutorPool();
    }

    private void doSetPythonExecutorPool() {
        final boolean useExternalPython = getPythonStrategy(System.getProperty("python.expressionsEval"), PYTHON_EXECUTOR) != JYTHON;
        // 25% of number of thread, in case of external python expression evaluation
        // 75% of number of threads in case of jython expression evaluation
        int defaultPoolSize = useExternalPython ? max(2, numberOfThreads / 4) : max(2, numberOfThreads * 3 / 4);
        int maxPoolSize = getInteger("jython.executor.maxPoolSize", defaultPoolSize);
        if (maxPoolSize > 100) { // 100 = hard limit for EmbeddedPythonExecutorWrapper pools
            maxPoolSize = 100;
        }
        ExecutorService executorService = newFixedThreadPool(max(2, getRuntime().availableProcessors()));
        ConcurrentLinkedQueueCollection<EmbeddedPythonExecutorWrapper> collection =
                new ConcurrentLinkedQueueCollection<>();
        for (int i = 0; i < maxPoolSize; i++) {
            executorService.submit(() -> {
                try {
                    collection.offerLast(new EmbeddedPythonExecutorWrapper());
                } catch (Exception ignored) {
                }
            });
        }
        try {
            executorService.shutdown();
            //noinspection ResultOfMethodCallIgnored
            executorService.awaitTermination(7, TimeUnit.MINUTES);
            executorService.shutdownNow();
        } catch (Exception ignored) {
        }
        while (collection.size() < maxPoolSize) {
            collection.offerLast(new EmbeddedPythonExecutorWrapper());
        }
        this.pythonExecutorPool = new ViburEmbeddedPythonPoolServiceImpl(collection, maxPoolSize, maxPoolSize);
    }

    @Override
    public PythonExecutionResult exec(Set<String> dependencies, String script, Map<String, Serializable> vars) {
        Set<String> resultedDependencies = dependencyService.getDependencies(dependencies);
        // When dependencies are not set we take an EmbeddedPythonExecutorWrapper from the 'pythonExecutorPool' pool
        // When dependencies are set we take an EmbeddedPythonExecutorWrapper from the 'cachedExecutors' cache
        if (resultedDependencies.isEmpty()) {
            EmbeddedPythonExecutorWrapper pooledPythonExecutor = null;
            try {
                pooledPythonExecutor = pythonExecutorPool.tryTakeWithTimeout();
                return pooledPythonExecutor.exec(script, vars);
            } finally {
                doRestoreExecutor(pooledPythonExecutor);
            }
        } else {
            String dependenciesKey = generatedDependenciesKey(dependencies);
            EmbeddedPythonExecutorWrapper executor = cachedExecutors.getIfPresent(dependenciesKey);
            if (executor == null) {
                EmbeddedPythonExecutorWrapper newPythonExecutor = new EmbeddedPythonExecutorWrapper(resultedDependencies);
                executor = cachedExecutors.get(dependenciesKey, k -> newPythonExecutor);
            }
            // At this point executor cannot be null, since it was created in in the function  k -> newPythonExecutor
            // That is the reason we don't want to call an extra Objects.requireNotNull(executor)
            return executor.exec(script, vars);
        }
    }

    @Override
    public PythonEvaluationResult eval(String prepareEnvironmentScript, String script, Map<String, Serializable> vars) {
        EmbeddedPythonExecutorWrapper embeddedPythonExecutor = null;
        try {
            embeddedPythonExecutor = pythonExecutorPool.tryTakeWithTimeout();
            return embeddedPythonExecutor.eval(prepareEnvironmentScript, script, vars);
        } finally {
            doRestoreExecutor(embeddedPythonExecutor);
        }
    }

    @Override
    public PythonEvaluationResult test(String prepareEnvironmentScript, String script,
                                       Map<String, Serializable> vars, long timeout) {
        EmbeddedPythonExecutorWrapper embeddedPythonExecutor = null;
        try {
            embeddedPythonExecutor = pythonExecutorPool.tryTakeWithTimeout();
            // For Jython test is identical with eval
            return embeddedPythonExecutor.eval(prepareEnvironmentScript, script, vars);
        } finally {
            doRestoreExecutor(embeddedPythonExecutor);
        }
    }

    private void doRestoreExecutor(EmbeddedPythonExecutorWrapper pooledPythonExecutor) {
        try {
            pythonExecutorPool.restore(pooledPythonExecutor);
        } catch (Exception ignored) {
        }
    }

    @PreDestroy
    public void closeResources() {
        pythonExecutorPool.close();
        try {
            cachedExecutors.asMap().values().forEach(EmbeddedPythonExecutorWrapper::close);
        } catch (Exception ignored) {
        }
    }

}
