/*
 * 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.karaf.features.internal;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

import org.apache.felix.utils.manifest.Clause;
import org.apache.felix.utils.manifest.Parser;
import org.apache.felix.utils.version.VersionRange;
import org.apache.karaf.features.FeaturesService.Option;
import org.apache.karaf.features.Resolver;
import org.apache.karaf.region.persist.RegionsPersistence;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.Version;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.service.url.URLStreamHandlerService;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.jar.JarFile.MANIFEST_NAME;

public class BundleManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(BundleManager.class);
    private final BundleContext bundleContext;
    private final RegionsPersistence regionsPersistence;
    private final long refreshTimeout;
    private List<String> defaultProtocols = Arrays.asList("http", "https", "ftp", "file", "jar");

    public BundleManager(BundleContext bundleContext) {
        this(bundleContext, null);
    }

    public BundleManager(BundleContext bundleContext, RegionsPersistence regionsPersistence) {
        this(bundleContext, regionsPersistence, 5000);
    }

    public BundleManager(BundleContext bundleContext, RegionsPersistence regionsPersistence, long refreshTimeout) {
        this.bundleContext = bundleContext;
        this.regionsPersistence = regionsPersistence;
        this.refreshTimeout = refreshTimeout;
    }

    public BundleInstallerResult installBundleIfNeeded(String bundleLocation, int startLevel, String regionName) throws IOException, BundleException {
        Bundle foundByLocationBundle = findInstalledByLocation(bundleLocation);
        if (foundByLocationBundle != null) {
            LOGGER.debug("Found installed bundle by location: {}", foundByLocationBundle);
            return new BundleInstallerResult(foundByLocationBundle, false);
        }
        BundleInstallerResult result = doInstallBundleIfNeeded(bundleLocation, startLevel);
        installToRegion(regionName, result.bundle, result.isNew);
        return result;
    }

    private BundleInstallerResult doInstallBundleIfNeeded(String bundleLocation, int startLevel) throws IOException, BundleException {
        InputStream is = getInputStreamForBundle(bundleLocation);
        try {
            is.mark(256 * 1024);
            @SuppressWarnings("resource")
            JarInputStream jar = new JarInputStream(is);
            Manifest m = jar.getManifest();
            if (m == null) {
                ZipEntry entry;
                while ((entry = jar.getNextEntry()) != null) {
                    if (MANIFEST_NAME.equals(entry.getName())) {
                        m = new Manifest(jar);
                        break;
                    }
                }
                if (m == null) {
                    throw new BundleException("Manifest not present in the zip " + bundleLocation);
                }
            }
            String sn = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
            if (sn == null) {
                throw new BundleException("Jar is not a bundle, no Bundle-SymbolicName " + bundleLocation);
            }
            // remove attributes from the symbolic name (like
            // ;blueprint.graceperiod:=false suffix)
            int attributeIndexSep = sn.indexOf(';');
            if (attributeIndexSep != -1) {
                sn = sn.substring(0, attributeIndexSep);
            }
            String vStr = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
            Version v = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
            Bundle existingBundle = findInstalled(sn, v);
            if (existingBundle != null) {
                LOGGER.debug("Found installed bundle: " + existingBundle);
                return new BundleInstallerResult(existingBundle, false);
            }
            try {
                is.reset();
            } catch (IOException e) {
                is.close();
                is = new URL(bundleLocation).openStream();
                // is = new BufferedInputStream(new
                // URL(bundleLocation).openStream());
            }
            is = new FilterInputStream(is) {
                @Override
                public int read(byte b[], int off, int len) throws IOException {
                    if (Thread.currentThread().isInterrupted()) {
                        throw new InterruptedIOException();
                    }
                    return super.read(b, off, len);
                }
            };

            LOGGER.debug("Installing bundle " + bundleLocation);
            Bundle b = bundleContext.installBundle(bundleLocation, is);

            if (startLevel > 0) {
                b.adapt(BundleStartLevel.class).setStartLevel(startLevel);
            }

            return new BundleInstallerResult(b, true);
        } finally {
            is.close();
        }
    }
    
    public Bundle isBundleInstalled(String bundleLocation) throws IOException, BundleException {
        Bundle foundByLocationBundle = findInstalledByLocation(bundleLocation);
        if (foundByLocationBundle != null) {
            return foundByLocationBundle;
        }
        InputStream is = getInputStreamForBundle(bundleLocation);
        try {
            is.mark(256 * 1024);
            @SuppressWarnings("resource")
            JarInputStream jar = new JarInputStream(is);
            Manifest m = jar.getManifest();
            if (m == null) {
                ZipEntry entry;
                while ((entry = jar.getNextEntry()) != null) {
                    if (MANIFEST_NAME.equals(entry.getName())) {
                        m = new Manifest(jar);
                        break;
                    }
                }
                if (m == null) {
                    throw new BundleException("Manifest not present in the zip " + bundleLocation);
                }
            }
            String sn = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
            if (sn == null) {
                throw new BundleException("Jar is not a bundle, no Bundle-SymbolicName " + bundleLocation);
            }
            // remove attributes from the symbolic name (like
            // ;blueprint.graceperiod:=false suffix)
            int attributeIndexSep = sn.indexOf(';');
            if (attributeIndexSep != -1) {
                sn = sn.substring(0, attributeIndexSep);
            }
            String vStr = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
            Version v = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
            Bundle existingBundle = findInstalled(sn, v);
            if (existingBundle != null) {
                LOGGER.debug("Found installed bundle: " + existingBundle);
                return existingBundle;
            }
            return null;
        } finally {
            is.close();
        }
    }

    private Bundle findInstalled(String symbolicName, Version version) {
        String vStr;
        for (Bundle b : bundleContext.getBundles()) {
            if (b.getSymbolicName() != null && b.getSymbolicName().equals(symbolicName)) {
                vStr = (String) b.getHeaders().get(Constants.BUNDLE_VERSION);
                Version bv = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
                if (version.equals(bv)) {
                    return b;
                }
            }
        }
        return null;
    }

    private Bundle findInstalledByLocation(String location) {
        for (Bundle b : bundleContext.getBundles()) {
            if (b.getLocation() != null && b.getLocation().equals(location)) {
                return b;
            }
        }
        return null;
    }

    private InputStream getInputStreamForBundle(String bundleLocation) throws MalformedURLException, IOException {
        InputStream is;
        LOGGER.debug("Checking " + bundleLocation);
        try {
            int protocolIndex = bundleLocation.indexOf(":");
            if (protocolIndex != -1) {
                String protocol = bundleLocation.substring(0, protocolIndex);
                if (!protocol.endsWith("file") && !protocol.endsWith("http")) {
                    waitForUrlHandler(protocol);
                }
            }
            URL bundleUrl = new URL(bundleLocation);
            is = new BufferedInputStream(bundleUrl.openStream());
        } catch (RuntimeException e) {
            LOGGER.error(e.getMessage());
            throw e;
        }
        return is;
    }

    private void installToRegion(String region, Bundle b, boolean isNew) throws BundleException {
        if (region != null && isNew) {
            if (regionsPersistence != null) {
                regionsPersistence.install(b, region);
            } else {
                throw new RuntimeException("Unable to find RegionsPersistence service, while installing " + region);
            }
        }
    }

    /**
     * Will wait for the {@link URLStreamHandlerService} service for the
     * specified protocol to be registered.
     *
     * @param protocol
     */
    private void waitForUrlHandler(String protocol) {
        if (!defaultProtocols.contains(protocol)) {
            try {
                Filter filter = bundleContext.createFilter("(&(" + Constants.OBJECTCLASS + "=" + URLStreamHandlerService.class.getName() + ")(url.handler.protocol=" + protocol + "))");
                if (filter == null) {
                    return;
                }
                ServiceTracker<URLStreamHandlerService, URLStreamHandlerService> urlHandlerTracker = new ServiceTracker<URLStreamHandlerService, URLStreamHandlerService>(bundleContext, filter, null);
                try {
                    urlHandlerTracker.open();
                    urlHandlerTracker.waitForService(30000);
                } catch (InterruptedException e) {
                    LOGGER.debug("Interrupted while waiting for URL handler for protocol {}.", protocol);
                } finally {
                    urlHandlerTracker.close();
                }
            } catch (Exception ex) {
                LOGGER.error("Error creating service tracker.", ex);
            }
        }
    }

    protected Set<Bundle> findBundlesToRefresh(Set<Bundle> existing, Set<Bundle> installed) {
        Set<Bundle> bundles = new HashSet<Bundle>();
        bundles.addAll(findBundlesWithOptionalPackagesToRefresh(existing, installed));
        bundles.addAll(findBundlesWithFragmentsToRefresh(existing, installed));
        return bundles;
    }

    protected Set<Bundle> findBundlesWithFragmentsToRefresh(Set<Bundle> existing, Set<Bundle> installed) {
        Set<Bundle> bundles = new HashSet<Bundle>();
        Set<Bundle> oldBundles = new HashSet<Bundle>(existing);
        oldBundles.removeAll(installed);
        if (!oldBundles.isEmpty()) {
            for (Bundle b : installed) {
                String hostHeader = (String) b.getHeaders().get(Constants.FRAGMENT_HOST);
                if (hostHeader != null) {
                    Clause[] clauses = Parser.parseHeader(hostHeader);
                    if (clauses != null && clauses.length > 0) {
                        Clause path = clauses[0];
                        for (Bundle hostBundle : oldBundles) {
                            if (hostBundle.getSymbolicName().equals(path.getName())) {
                                String ver = path.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE);
                                if (ver != null) {
                                    VersionRange v = VersionRange.parseVersionRange(ver);
                                    if (v.contains(hostBundle.getVersion())) {
                                        bundles.add(hostBundle);
                                    }
                                } else {
                                    bundles.add(hostBundle);
                                }
                            }
                        }
                    }
                }
            }
        }
        return bundles;
    }

    protected Set<Bundle> findBundlesWithOptionalPackagesToRefresh(Set<Bundle> existing, Set<Bundle> installed) {
        // First pass: include all bundles contained in these features
        Set<Bundle> bundles = new HashSet<Bundle>(existing);
        bundles.removeAll(installed);
        if (bundles.isEmpty()) {
            return bundles;
        }
        // Second pass: for each bundle, check if there is any unresolved
        // optional package that could be resolved
        Map<Bundle, List<Clause>> imports = new HashMap<Bundle, List<Clause>>();
        for (Iterator<Bundle> it = bundles.iterator(); it.hasNext(); ) {
            Bundle b = it.next();
            String importsStr = (String) b.getHeaders().get(
                    Constants.IMPORT_PACKAGE);
            List<Clause> importsList = getOptionalImports(importsStr);
            if (importsList.isEmpty()) {
                it.remove();
            } else {
                imports.put(b, importsList);
            }
        }
        if (bundles.isEmpty()) {
            return bundles;
        }
        // Third pass: compute a list of packages that are exported by our
        // bundles and see if
        // some exported packages can be wired to the optional imports
        List<Clause> exports = new ArrayList<Clause>();
        for (Bundle b : installed) {
            String exportsStr = (String) b.getHeaders().get(
                    Constants.EXPORT_PACKAGE);
            if (exportsStr != null) {
                Clause[] exportsList = Parser.parseHeader(exportsStr);
                exports.addAll(Arrays.asList(exportsList));
            }
        }
        for (Iterator<Bundle> it = bundles.iterator(); it.hasNext(); ) {
            Bundle b = it.next();
            List<Clause> importsList = imports.get(b);
            for (Iterator<Clause> itpi = importsList.iterator(); itpi.hasNext(); ) {
                Clause pi = itpi.next();
                boolean matching = false;
                for (Clause pe : exports) {
                    if (pi.getName().equals(pe.getName())) {
                        String evStr = pe
                                .getAttribute(Constants.VERSION_ATTRIBUTE);
                        String ivStr = pi
                                .getAttribute(Constants.VERSION_ATTRIBUTE);
                        Version exported = evStr != null ? Version
                                .parseVersion(evStr) : Version.emptyVersion;
                        VersionRange imported = ivStr != null ? VersionRange
                                .parseVersionRange(ivStr)
                                : VersionRange.ANY_VERSION;
                        if (imported.contains(exported)) {
                            matching = true;
                            break;
                        }
                    }
                }
                if (!matching) {
                    itpi.remove();
                }
            }
            if (importsList.isEmpty()) {
                it.remove();
            } else {
                LOGGER.debug(
                        "Refeshing bundle {} ({}) to solve the following optional imports",
                        b.getSymbolicName(), b.getBundleId());
                for (Clause p : importsList) {
                    LOGGER.debug("    {}", p);
                }

            }
        }
        return bundles;
    }

    /*
     * Get the list of optional imports from an OSGi Import-Package string
     */
    protected List<Clause> getOptionalImports(String importsStr) {
        Clause[] imports = Parser.parseHeader(importsStr);
        List<Clause> result = new LinkedList<Clause>();
        for (Clause anImport : imports) {
            String resolution = anImport
                    .getDirective(Constants.RESOLUTION_DIRECTIVE);
            if (Constants.RESOLUTION_OPTIONAL.equals(resolution)) {
                result.add(anImport);
            }
        }
        return result;
    }

    protected void refreshPackages(Collection<Bundle> bundles) {
        final Object refreshLock = new Object();
        FrameworkWiring wiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class);
        if (wiring != null) {
            synchronized (refreshLock) {
                wiring.refreshBundles(bundles, new FrameworkListener() {
                    public void frameworkEvent(FrameworkEvent event) {
                        if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
                            synchronized (refreshLock) {
                                refreshLock.notifyAll();
                            }
                        }
                    }
                });
                try {
                    refreshLock.wait(refreshTimeout);
                } catch (InterruptedException e) {
                    LOGGER.warn(e.getMessage(), e);
                }
            }
        }
    }

    public void uninstall(Set<Bundle> bundles) {
        uninstall(bundles, true);
    }

    public void uninstall(Set<Bundle> bundles, boolean refresh) {
        for (Bundle b : bundles) {
            try {
                b.uninstall();
            } catch (Exception e2) {
                // Ignore
            }
        }
        if (refresh) {
            refreshPackages(null);
        }
    }

    public void uninstall(List<Bundle> bundles) {
        uninstall(bundles, true);
    }

    public void uninstall(List<Bundle> bundles, boolean refresh) {
        for (Bundle b : bundles) {
            try {
                b.uninstall();
            } catch (Exception e2) {
                // Ignore
            }
        }
        if (refresh) {
            refreshPackages(null);
        }
    }

    public void uninstallById(Set<Long> bundles) throws BundleException, InterruptedException {
        uninstallById(bundles, true);
    }

    public void uninstallById(Set<Long> bundles, boolean refresh) throws BundleException,
            InterruptedException {
        for (long bundleId : bundles) {
            Bundle b = bundleContext.getBundle(bundleId);
            if (b != null) {
                b.uninstall();
            }
        }
        if (refresh) {
            refreshPackages(null);
        }
    }

    public File getDataFile(String fileName) {
        return bundleContext.getDataFile(fileName);
    }

    EventAdminListener createAndRegisterEventAdminListener() {
        EventAdminListener listener = null;
        try {
            getClass().getClassLoader().loadClass(
                    "org.bundles.service.event.EventAdmin");
            listener = new EventAdminListener(bundleContext);
        } catch (Throwable t) {
            // Ignore, if the EventAdmin package is not available, just don't
            // use it
            LOGGER.debug("EventAdmin package is not available, just don't use it");
        }
        return listener;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public ServiceTracker createServiceTrackerForResolverName(String resolver)
            throws InvalidSyntaxException {
        String filter = "(&(" + Constants.OBJECTCLASS + "="
                + Resolver.class.getName() + ")(name=" + resolver + "))";
        return new ServiceTracker(bundleContext,
                FrameworkUtil.createFilter(filter), null);
    }

    public void refreshBundles(Set<Bundle> existing, Set<Bundle> installed,
                               EnumSet<Option> options) {
        boolean print = options.contains(Option.PrintBundlesToRefresh);
        boolean refresh = !options.contains(Option.NoAutoRefreshBundles);
        if (print || refresh) {
            Set<Bundle> bundlesToRefresh = findBundlesToRefresh(existing,
                    installed);
            StringBuilder sb = new StringBuilder();
            for (Bundle b : bundlesToRefresh) {
                if (sb.length() > 0) {
                    sb.append(", ");
                }
                sb.append(b.getSymbolicName()).append(" (")
                        .append(b.getBundleId()).append(")");
            }
            LOGGER.debug("Bundles to refresh: {}", sb.toString());
            if (!bundlesToRefresh.isEmpty()) {
                if (print) {
                    if (refresh) {
                        System.out.println("Refreshing bundles "
                                + sb.toString());
                    } else {
                        System.out
                                .println("The following bundles may need to be refreshed: "
                                        + sb.toString());
                    }
                }
                if (refresh) {
                    LOGGER.debug("Refreshing bundles: {}", sb.toString());
                    refreshPackages(bundlesToRefresh);
                }
            }
        }
    }

    public BundleContext getBundleContext() {
        return this.bundleContext;
    }

    public static class BundleInstallerResult {
        Bundle bundle;
        boolean isNew;

        public BundleInstallerResult(Bundle bundle, boolean isNew) {
            super();
            this.bundle = bundle;
            this.isNew = isNew;
        }

    }

}
