/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.language.groovy;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.apache.camel.Exchange;
import org.apache.camel.Ordered;
import org.apache.camel.Service;
import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ScriptingLanguage;
import org.apache.camel.spi.annotations.Language;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.LRUCacheFactory;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.SimpleEventNotifierSupport;
import org.apache.camel.support.TypedLanguageSupport;
import org.apache.camel.support.service.ServiceHelper;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Language("groovy")
public class GroovyLanguage extends TypedLanguageSupport implements ScriptingLanguage, Service {

    private static final Logger LOG = LoggerFactory.getLogger(GroovyLanguage.class);

    static final int RELOAD_ORDER = 100;

    /**
     * In case the expression is referring to an external resource, it indicates whether it is still needed to load the
     * resource.
     */
    private final boolean loadExternalResource;

    /**
     * Cache used to stores the compiled scripts (aka their classes)
     */
    private final Map<String, GroovyClassService> scriptCache;

    private EventNotifier notifier;

    private GroovyLanguage(Map<String, GroovyClassService> scriptCache, boolean loadExternalResource) {
        this.scriptCache = scriptCache;
        this.loadExternalResource = loadExternalResource;
    }

    public GroovyLanguage() {
        this(LRUCacheFactory.newLRUSoftCache(16, 1000, true), true);
    }

    @Override
    public void start() {
        // are we in dev mode then support flushing cache on reload
        if (getCamelContext() != null) {
            String profile = getCamelContext().getCamelContextExtension().getProfile();
            if ("dev".equals(profile)) {
                if (notifier == null) {
                    notifier = new ReloadNotifier();
                    getCamelContext().getManagementStrategy().addEventNotifier(notifier);
                }
            }
        }
    }

    @Override
    public void stop() {
        ServiceHelper.stopService(scriptCache.values());
        scriptCache.clear();
        if (notifier != null) {
            getCamelContext().getManagementStrategy().removeEventNotifier(notifier);
            notifier = null;
        }
    }

    private final class ReloadNotifier extends SimpleEventNotifierSupport implements Ordered {

        @Override
        public void notify(CamelEvent event) throws Exception {
            // if context or route is reloading then clear cache to ensure old scripts are removed from memory.
            if (event instanceof CamelEvent.CamelContextReloadingEvent || event instanceof CamelEvent.RouteReloadedEvent) {
                ServiceHelper.stopService(scriptCache.values());
                scriptCache.clear();
            }
        }

        @Override
        public int getOrder() {
            return RELOAD_ORDER;
        }
    }

    private static final class GroovyClassService implements Service {

        private final Class<Script> script;

        private GroovyClassService(Class<Script> script) {
            this.script = script;
        }

        @Override
        public void start() {
        }

        @Override
        public void stop() {
            InvokerHelper.removeClass(script);
        }

    }

    public static GroovyExpression groovy(String expression) {
        try (GroovyLanguage factory = new GroovyLanguage()) {
            return factory.createExpression(expression);
        } catch (IOException e) {
            LOG.error("Some error happened while creating a Groovy expression", e);
            return null;
        }
    }

    @Override
    public GroovyExpression createPredicate(String expression) {
        return createExpression(expression);
    }

    @Override
    public GroovyExpression createExpression(String expression) {
        expression = loadResource(expression);
        return new GroovyExpression(expression);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T evaluate(String script, Map<String, Object> bindings, Class<T> resultType) {
        if (loadExternalResource) {
            script = loadResource(script);
        }
        Class<Script> clazz = getScriptFromCache(script);
        if (clazz == null) {
            // prefer to use classloader from groovy script compiler, and if not fallback to app context
            ClassLoader cl = getCamelContext().getCamelContextExtension().getContextPlugin(GroovyScriptClassLoader.class);
            GroovyShell shell = cl != null ? new GroovyShell(cl) : new GroovyShell();
            clazz = shell.getClassLoader().parseClass(script);
            addScriptToCache(script, clazz);
        }
        Script gs = ObjectHelper.newInstance(clazz, Script.class);
        if (bindings != null) {
            gs.setBinding(new Binding(bindings));
        }
        gs.getBinding().setVariable("log", LOG);
        Object value = gs.run();
        return getCamelContext().getTypeConverter().convertTo(resultType, value);
    }

    // use by tooling
    public boolean validateExpression(String expression) throws GroovyValidationException {
        final Exchange dummy = new DefaultExchange(getCamelContext());
        Map<String, Object> globalVariables = new HashMap<>();

        try {
            GroovyExpression ge = createExpression(expression);
            Script script = ge.instantiateScript(dummy, globalVariables);
            script.setBinding(ge.createBinding(dummy, globalVariables));
            script.run();
        } catch (Exception e) {
            throw new GroovyValidationException(expression, e);
        }
        return true;
    }

    // use by tooling
    public boolean validatePredicate(String expression) throws GroovyValidationException {
        return validateExpression(expression);
    }

    Class<Script> getScriptFromCache(String script) {
        final GroovyClassService cached = scriptCache.get(script);
        if (cached == null) {
            return null;
        }
        return cached.script;
    }

    void addScriptToCache(String script, Class<Script> scriptClass) {
        scriptCache.put(script, new GroovyClassService(scriptClass));
    }

    public static class Builder {
        private final Map<String, GroovyClassService> cache = new HashMap<>();

        public void addScript(String content, Class<Script> scriptClass) {
            cache.put(content, new GroovyClassService(scriptClass));
        }

        public GroovyLanguage build() {
            return new GroovyLanguage(cache, false);
        }
    }
}
