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

import java.beans.Introspector;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.core.startup.CoreBridge;
import org.netbeans.core.startup.Main;
import org.netbeans.core.startup.RunLevel;
import org.netbeans.core.startup.Splash;
import org.openide.modules.ModuleInstall;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbCollections;
import org.openide.util.SharedClassObject;
import org.openide.util.lookup.Lookups;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.SynchronousBundleListener;
import org.osgi.framework.launch.Framework;
import org.osgi.service.url.AbstractURLStreamHandlerService;
import org.osgi.service.url.URLConstants;
import org.osgi.service.url.URLStreamHandlerService;

/**
 * Initializes critical NetBeans infrastructure inside an OSGi container.
 */
public class Activator implements BundleActivator, SynchronousBundleListener {

    private static final Logger LOG = Logger.getLogger(Activator.class.getName());

    public Activator() {}

    /** Bundles which have been loaded or are in line to be loaded. */
    private DependencyQueue<String,Bundle> queue;
    private BundleContext context;
    private Framework framework;

    public @Override void start(final BundleContext context) throws Exception {
        if (System.getProperty("netbeans.home") != null) {
            throw new IllegalStateException("Should not be run from inside regular NetBeans module system");
        }
        String storage = context.getProperty(Constants.FRAMEWORK_STORAGE);
        if (storage != null) {
            System.setProperty("netbeans.user", storage);
        }
        System.setProperty("TopSecurityManager.disable", "true");
        NbBundle.setBranding(System.getProperty("branding.token"));
        OSGiMainLookup.initialize(context);
        queue = new DependencyQueue<String,Bundle>();
        this.context = context;
        framework = ((Framework) context.getBundle(0));
        if (framework.getState() == Bundle.STARTING) {
            LOG.fine("framework still starting");
            final AtomicReference<FrameworkListener> frameworkListener = new AtomicReference<FrameworkListener>();
            frameworkListener.set(new FrameworkListener() {
                public @Override void frameworkEvent(FrameworkEvent event) {
                    if (event.getType() == FrameworkEvent.STARTED) {
//                        System.err.println("framework started");
                        context.removeFrameworkListener(frameworkListener.get());
                        context.addBundleListener(Activator.this);
                        processLoadedBundles();
                    }
                }
            });
            context.addFrameworkListener(frameworkListener.get());
        } else {
            LOG.fine("framework already started");
            context.addBundleListener(this);
            processLoadedBundles();
        }
    }

    private void processLoadedBundles() {
        List<Bundle> toLoad = new ArrayList<Bundle>();
        for (Bundle b : context.getBundles()) {
            if (b.getState() == Bundle.ACTIVE) {
                Dictionary<?,?> headers = b.getHeaders();
                toLoad.addAll(queue.offer(b, provides(headers), requires(headers), needs(headers)));
            }
        }
//        System.err.println("processing already loaded bundles: " + toLoad);
        load(toLoad);
    }

    public @Override void stop(BundleContext context) throws Exception {}

    public @Override void bundleChanged(BundleEvent event) {
        Bundle bundle = event.getBundle();
        switch (event.getType()) {
        case BundleEvent.STARTED:
//            System.err.println("started " + bundle.getSymbolicName());
            Dictionary<?,?> headers = bundle.getHeaders();
            load(queue.offer(bundle, provides(headers), requires(headers), needs(headers)));
            break;
        case BundleEvent.STOPPED:
//            System.err.println("stopped " + bundle.getSymbolicName());
            if (framework.getState() == Bundle.STOPPING) {
//                System.err.println("fwork stopping during " + bundle.getSymbolicName());
//                ActiveQueue.stop();
            } else {
                unload(queue.retract(bundle));
            }
            break;
        }
    }

    static Set<String> provides(Dictionary<?,?> headers) {
        Set<String> deps = new TreeSet<String>(splitTokens((String) headers.get("OpenIDE-Module-Provides")));
        String name = (String) headers.get(Constants.BUNDLE_SYMBOLICNAME);
        if (name != null) {
            name = name.replaceFirst(";.+", "");
            deps.add("cnb." + name);
            if (name.equals("org.openide.modules")) {
                CoreBridge.defineOsTokens(deps);
            }
        }
        return deps;
    }

    static Set<String> requires(Dictionary<?,?> headers) {
        Set<String> deps = new TreeSet<String>();
        String v = (String) headers.get(Constants.REQUIRE_BUNDLE);
        if (v != null) {
            // PackageAdmin.getRequiredBundles is not suitable for this - it is backwards.
            // XXX try to follow the spec more closely; this will work at least for headers created by MakeOSGi:
            for (String item : v.split(", ")) {
                deps.add("cnb." + item.replaceFirst(";.+", ""));
            }
        }
        // XXX also check for BUNDLE_SYMBOLICNAME_ATTRIBUTE in IMPORT_PACKAGE (though not currently used by MakeOSGi)
        deps.addAll(splitTokens((String) headers.get("OpenIDE-Module-Requires")));
        return deps;
    }

    static Set<String> needs(Dictionary<?,?> headers) {
        return splitTokens((String) headers.get("OpenIDE-Module-Needs"));
    }

    private static Set<String> splitTokens(String tokens) {
        if (tokens == null) {
            return Collections.emptySet();
        }
        Set<String> split = new TreeSet<String>(Arrays.asList(tokens.split("[, ]+")));
        split.remove("");
        return split;
    }

    static final Map<Bundle,ModuleInstall> installers = new HashMap<Bundle,ModuleInstall>();

    private void load(List<Bundle> bundles) {
        if (bundles.isEmpty() || bundles.size() == 1 && bundles.iterator().next().getBundleId() == 0) {
            return;
        }
        LOG.log(Level.FINE, "loading: {0}", bundles);
        OSGiMainLookup.bundlesAdded(bundles);
        boolean showWindowSystem = false; // trigger for showing main window and setting up related GUI elements
        boolean loadServicesFolder = false;
        for (Bundle bundle : bundles) {
            registerUrlProtocolHandlers(bundle);
            if (bundle.getSymbolicName().equals("org.netbeans.core")) { // NOI18N
                loadServicesFolder = true;
            } else if (bundle.getSymbolicName().equals("org.netbeans.bootstrap")) { // NOI18N
                System.setProperty("netbeans.buildnumber", bundle.getVersion().getQualifier()); // NOI18N
            } else if (bundle.getSymbolicName().equals("org.netbeans.core.windows")) { // NOI18N
                showWindowSystem = true;
            }
        }
        OSGiRepository.DEFAULT.addLayersFor(bundles);
        if (loadServicesFolder) {
            OSGiMainLookup.loadServicesFolder();
        }
        if (showWindowSystem) {
            Splash.getInstance().setRunning(true);
            Main.initUICustomizations();
        }
        for (Bundle bundle : bundles) {
            ModuleInstall mi = installerFor(bundle);
            if (mi != null) {
                installers.put(bundle, mi);
                LOG.log(Level.FINE, "restored: {0}", bundle.getSymbolicName());
                mi.restored();
            }
        }
        ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
        final Lookup start = Lookups.metaInfServices(l, "META-INF/namedservices/Modules/Start/");
        // NbStartStop not quite appropriate here; will not properly handle multiple enable/disable cycles
        // (but it does run them in parallel, which may be desirable)
        for (Runnable r : start.lookupAll(Runnable.class)) {
            if (bundles.contains(FrameworkUtil.getBundle(r.getClass()))) {
                LOG.log(Level.FINE, "starting {0}", r.getClass().getName());
                r.run();
            }
        }
        if (showWindowSystem) {
            // XXX set ${jdk.home}?
            List<String> bisp = new ArrayList<String>(Arrays.asList(Introspector.getBeanInfoSearchPath()));
            bisp.add("org.netbeans.beaninfo"); // NOI18N
            Introspector.setBeanInfoSearchPath(bisp.toArray(new String[bisp.size()]));
            CoreBridge.getDefault().registerPropertyEditors();
        }
        for (RunLevel rl : Lookup.getDefault().lookupAll(RunLevel.class)) {
            rl.run();
        }
        if (showWindowSystem) {
            Splash.getInstance().setRunning(false);
        }
    }

    private void unload(List<Bundle> bundles) {
        if (bundles.isEmpty()) {
            return;
        }
        LOG.log(Level.FINE, "unloading: {0}", bundles);
        ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
        final Lookup stop = Lookups.metaInfServices(l, "META-INF/namedservices/Modules/Stop/");
        for (Callable<?> r : stop.lookupAll(Callable.class)) {
            if (bundles.contains(FrameworkUtil.getBundle(r.getClass()))) {
                try {
                    if (!((Boolean) r.call())) {
                        LOG.log(Level.WARNING, "ignoring false return value from {0}", r.getClass().getName());
                    }
                } catch (Exception x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
        }
        for (Runnable r : stop.lookupAll(Runnable.class)) {
            if (bundles.contains(FrameworkUtil.getBundle(r.getClass()))) {
                r.run();
            }
        }
        for (Bundle bundle : bundles) {
            ModuleInstall mi = installers.remove(bundle);
            if (mi != null) {
                LOG.log(Level.FINE, "uninstalled: {0}", bundle.getSymbolicName());
                mi.uninstalled();
            }
        }
        OSGiRepository.DEFAULT.removeLayersFor(bundles);
        OSGiMainLookup.bundlesRemoved(bundles);
    }

    private static ModuleInstall installerFor(Bundle b) {
        if (b.getSymbolicName().equals("org.netbeans.modules.autoupdate.ui")) { // NOI18N
            // Won't work anyway, so don't even try.
            return null;
        }
        String respath = (String) b.getHeaders().get("OpenIDE-Module-Install");
        if (respath != null) {
            String fqn = respath.replaceFirst("[.]class$", "").replace('/', '.');
            try {
                return SharedClassObject.findObject(((Class<?>) b.loadClass(fqn)).asSubclass(ModuleInstall.class), true);
            } catch (Exception x) { // CNFE, CCE, ...
                LOG.log(Level.WARNING, "Could not load " + fqn, x);
                return null;
            }
        }
        return null;
    }

    private void registerUrlProtocolHandlers(final Bundle bundle) {
        Enumeration<?> e = bundle.getEntryPaths("META-INF/namedservices/URLStreamHandler/");
        if (e != null) {
            for (String path : NbCollections.iterable(NbCollections.checkedEnumerationByFilter(e, String.class, true))) {
                URL entry = bundle.getEntry(path + "java.net.URLStreamHandler");
                if (entry != null) {
                    String protocol = path.replaceAll("^META-INF/namedservices/URLStreamHandler/|/$", "");
                    try {
                        InputStream is = entry.openStream();
                        try {
                            BufferedReader r = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                            String line;
                            while ((line = r.readLine()) != null) {
                                if (!line.isEmpty() && !line.startsWith("#")) {
                                    final String fqn = line;
                                    Dictionary<String,Object> props = new Hashtable<String,Object>();
                                    props.put(URLConstants.URL_HANDLER_PROTOCOL, protocol);
                                    class Svc extends AbstractURLStreamHandlerService {
                                        public @Override URLConnection openConnection(final URL u) throws IOException {
                                            try {
                                                URLStreamHandler handler = (URLStreamHandler) bundle.loadClass(fqn).newInstance();
                                                Method openConnection = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class);
                                                openConnection.setAccessible(true);
                                                return (URLConnection) openConnection.invoke(handler, u);
                                            } catch (Exception x) {
                                                throw (IOException) new IOException(x.toString()).initCause(x);
                                            }
                                        }
                                    }
                                    BundleContext context = bundle.getBundleContext();
                                    if (context != null) {
                                        context.registerService(URLStreamHandlerService.class.getName(), new Svc(), props);
                                    } else {
                                        LOG.log(Level.WARNING, "no context for {0} in state {1}", new Object[] {bundle.getSymbolicName(), bundle.getState()});
                                    }
                                }
                            }
                        } finally {
                            is.close();
                        }
                    } catch (Exception x) {
                        LOG.log(Level.WARNING, "Could not load protocol handler for " + protocol + " from " + bundle.getSymbolicName(), x);
                    }
                }
            }
        }
    }

}
