/**
 * 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.script.osgi;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;

import org.apache.camel.impl.osgi.tracker.BundleTracker;
import org.apache.camel.impl.osgi.tracker.BundleTrackerCustomizer;
import org.apache.camel.util.IOHelper;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Activator implements BundleActivator, BundleTrackerCustomizer {
    public static final String META_INF_SERVICES_DIR = "META-INF/services";
    public static final String SCRIPT_ENGINE_SERVICE_FILE = "javax.script.ScriptEngineFactory";

    private static final Logger LOG = LoggerFactory.getLogger(Activator.class);
    private static BundleContext context;
    private BundleTracker tracker;
    
    private Map<Long, List<BundleScriptEngineResolver>> resolvers 
        = new ConcurrentHashMap<Long, List<BundleScriptEngineResolver>>();

    public static BundleContext getBundleContext() {
        return context;
    }
    
    public void start(BundleContext context) throws Exception {
        Activator.context = context;
        LOG.info("Camel-Script activator starting");
        tracker = new BundleTracker(context, Bundle.ACTIVE, this);
        tracker.open();
        LOG.info("Camel-Script activator started");
    }

    public void stop(BundleContext context) throws Exception {
        LOG.info("Camel-Script activator stopping");
        tracker.close();
        LOG.info("Camel-Script activator stopped");
        Activator.context = null;
    }

    public Object addingBundle(Bundle bundle, BundleEvent event) {
        List<BundleScriptEngineResolver> r = new ArrayList<BundleScriptEngineResolver>();
        registerScriptEngines(bundle, r);
        for (BundleScriptEngineResolver service : r) {
            service.register();
        }
        resolvers.put(bundle.getBundleId(), r);
        return bundle;
    }

    public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) {
    }

    public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
        LOG.debug("Bundle stopped: {}", bundle.getSymbolicName());
        List<BundleScriptEngineResolver> r = resolvers.remove(bundle.getBundleId());
        if (r != null) {
            for (BundleScriptEngineResolver service : r) {
                service.unregister();
            }
        }
    }

    public static ScriptEngine resolveScriptEngine(String scriptEngineName) throws InvalidSyntaxException {
        ServiceReference<?>[] refs = context.getServiceReferences(ScriptEngineResolver.class.getName(), null);
        if (refs == null) {
            LOG.info("No OSGi script engine resolvers available!");
            return null;
        }
        
        LOG.debug("Found " + refs.length + " OSGi ScriptEngineResolver services");
        
        for (ServiceReference<?> ref : refs) {
            ScriptEngineResolver resolver = (ScriptEngineResolver) context.getService(ref);
            ScriptEngine engine = resolver.resolveScriptEngine(scriptEngineName);
            context.ungetService(ref);
            LOG.debug("OSGi resolver " + resolver + " produced " + scriptEngineName + " engine " + engine);
            if (engine != null) {
                return engine;
            }
        }
        return null;
    }

    protected void registerScriptEngines(Bundle bundle, List<BundleScriptEngineResolver> resolvers) {
        URL configURL = null;
        for (Enumeration<?> e = bundle.findEntries(META_INF_SERVICES_DIR, SCRIPT_ENGINE_SERVICE_FILE, false); e != null && e.hasMoreElements();) {
            configURL = (URL) e.nextElement();
        }
        if (configURL != null) {
            LOG.info("Found ScriptEngineFactory in " + bundle.getSymbolicName());
            resolvers.add(new BundleScriptEngineResolver(bundle, configURL));
        }
    } 

    public interface ScriptEngineResolver {
        ScriptEngine resolveScriptEngine(String name);
    }

    protected static class BundleScriptEngineResolver implements ScriptEngineResolver {
        protected final Bundle bundle;
        private ServiceRegistration reg;
        private final URL configFile;

        public BundleScriptEngineResolver(Bundle bundle, URL configFile) {
            this.bundle = bundle;
            this.configFile = configFile;
        }

        public void register() {
            reg = bundle.getBundleContext().registerService(ScriptEngineResolver.class.getName(), this, null);
        }

        public void unregister() {
            reg.unregister();
        }

        public ScriptEngine resolveScriptEngine(String name) {
            try {
                BufferedReader in = IOHelper.buffered(new InputStreamReader(configFile.openStream()));
                String className = in.readLine();
                in.close();
                Class<?> cls = bundle.loadClass(className);
                if (!ScriptEngineFactory.class.isAssignableFrom(cls)) {
                    throw new IllegalStateException("Invalid ScriptEngineFactory: " + cls.getName());
                }
                ScriptEngineFactory factory = (ScriptEngineFactory) cls.newInstance();
                List<String> names = factory.getNames();
                for (String test : names) {
                    if (test.equals(name)) {
                        ClassLoader old = Thread.currentThread().getContextClassLoader();
                        ScriptEngine engine;
                        try {
                            // JRuby seems to require the correct TCCL to call getScriptEngine
                            Thread.currentThread().setContextClassLoader(factory.getClass().getClassLoader());
                            engine = factory.getScriptEngine();
                        } finally {
                            Thread.currentThread().setContextClassLoader(old);
                        }
                        LOG.trace("Resolved ScriptEngineFactory: {} for expected name: {}", engine, name);
                        return engine;
                    }
                }
                LOG.debug("ScriptEngineFactory: {} does not match expected name: {}", factory.getEngineName(), name);
                return null;
            } catch (Exception e) {
                LOG.warn("Cannot create ScriptEngineFactory: " + e.getClass().getName(), e);
                return null;
            }
        }

        private boolean matchEngine(String name, String engineName) {
            if (name.equals(engineName)) {
                return true;
            }

            // javascript have many aliases
            if (name.equals("js") || name.equals("javaScript") || name.equals("ECMAScript")) {
                return engineName.equals("js") || engineName.equals("javaScript") || engineName.equals("ECMAScript");
            }

            return false;
        }

        @Override
        public String toString() {
            return "OSGi script engine resolver for " + bundle.getSymbolicName();
        }
    }

}
