/*
 * Copyright 2010 SpringSource
 *
 * 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 org.springframework.build.osgi.validator;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.PlatformAdmin;
import org.eclipse.osgi.service.resolver.ResolverError;
import org.eclipse.osgi.service.resolver.State;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.osgi.service.packageadmin.PackageAdmin;

final class BundleResolutionValidator {

    private final File bundle;

    private final List<File> supportingBundles;

    private final File equinoxJar;

    public BundleResolutionValidator(File bundle, List<File> supportBundles) throws URISyntaxException {
        this(bundle, supportBundles, new File(FrameworkProperties.class.getProtectionDomain().getCodeSource().getLocation().toURI()));
    }

    BundleResolutionValidator(File bundle, List<File> supportBundles, File equinoxJar) {
        this.bundle = bundle;
        this.supportingBundles = supportBundles;
        this.equinoxJar = equinoxJar;
    }

    void validate() throws ValidationFailedException {
        printValidationInformation();

        Framework framework;

        try {
            framework = launchEquinox();
        } catch (BundleException e) {
            throw new ValidationFailedException("The OSGi framework failed to start", e);
        }

        try {
            BundleContext bundleContext = framework.getBundleContext();
            installSupportingBundles(bundleContext);
            resolveBundle(bundleContext);
        } finally {
            try {
                framework.stop();
                framework.waitForStop(10000);
            } catch (InterruptedException ie) {

            } catch (BundleException be) {

            }
        }
    }

    private Framework launchEquinox() throws BundleException, ValidationFailedException {
        ServiceLoader<FrameworkFactory> serviceLoader = ServiceLoader.load(FrameworkFactory.class, BundleResolutionValidator.class.getClassLoader());
        FrameworkFactory frameworkFactory = serviceLoader.iterator().next();

        Map<String, String> config = new HashMap<String, String>();
        config.put("osgi.clean", "true");
        config.put("osgi.configuration.area", new File("target").getAbsolutePath());
        config.put("osgi.install.area", new File("target").getAbsolutePath());
        config.put("osgi.framework", getUrlString(equinoxJar));

        Framework framework = frameworkFactory.newFramework(config);
        framework.start();

        return framework;
    }

    private void installSupportingBundles(BundleContext bundleContext) throws ValidationFailedException {
        List<String> failures = new ArrayList<String>();
        System.out.println("Installing bundles... ");

        for (File supportingBundle : supportingBundles) {
            try {
                installBundle(bundleContext, supportingBundle);
            } catch (BundleException e) {
                failures.add(supportingBundle.toURI().toString());
            }
        }
    }

    private void resolveBundle(BundleContext bundleContext) throws ValidationFailedException {
        Bundle bundle;
        try {
            bundle = installBundle(bundleContext, this.bundle);
        } catch (BundleException e) {
            String message = createFailureMessage(Arrays.asList(this.bundle.toURI().toString()), "The following bundles failed to install:");
            throw new ValidationFailedException(message, e);
        }

        ServiceReference packageAdminReference = bundleContext.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = (PackageAdmin) bundleContext.getService(packageAdminReference);

        System.out.println("Resolving bundles...");

        packageAdmin.resolveBundles(new Bundle[] { bundle });

        System.out.println("    " + bundle + " " + stateToString(bundle.getState()));

        reportFailureIfNecessary(bundleContext);
    }

    private void reportFailureIfNecessary(BundleContext bundleContext) throws ValidationFailedException {
        ServiceReference platformAdminReference = bundleContext.getServiceReference(PlatformAdmin.class.getName());
        PlatformAdmin platformAdmin = (PlatformAdmin) bundleContext.getService(platformAdminReference);
        State state = platformAdmin.getState(false);

        boolean failureFound = false;
        for (BundleDescription description : state.getBundles()) {
            if (!description.isResolved()) {
                if (!failureFound) {
                    System.out.println("Resolution failures...");
                }
                failureFound = true;
                System.out.println("    " + description.toString() + " failed to resolve:");
                ResolverError[] resolverErrors = state.getResolverErrors(description);
                for (ResolverError resolverError : resolverErrors) {
                    System.out.println("        " + resolverError);
                }
            }
        }

        if (failureFound) {
            throw new ValidationFailedException("Resolution failed");
        }
    }

    private Bundle installBundle(BundleContext bundleContext, File bundleFile) throws BundleException {
        try {
            String bundleUri = bundleFile.toURI().toString();
            Bundle bundle = bundleContext.installBundle(bundleUri);
            System.out.println("    Installed " + bundle);
            return bundle;
        } catch (BundleException e) {
            System.out.println("    " + bundleFile + " failed to install: " + e.getMessage());
            throw e;
        }
    }

    private void printValidationInformation() {
        System.out.println(String.format("Validating bundle:%n    %s", bundle.getAbsolutePath()));
        System.out.println("Validating against:");
        for (File supportingBundle : supportingBundles) {
            System.out.println("    " + supportingBundle.getAbsolutePath());
        }
    }

    private String stateToString(int state) {
        switch (state) {
            case Bundle.ACTIVE:
                return "ACTIVE";
            case Bundle.INSTALLED:
                return "INSTALLED";
            case Bundle.RESOLVED:
                return "RESOLVED";
            case Bundle.STARTING:
                return "STARTING";
            case Bundle.STOPPING:
                return "STOPPING";
            case Bundle.UNINSTALLED:
                return "UNINSTALLED";
            default:
                return "UNKNOWN";
        }
    }

    private String createFailureMessage(List<String> failures, String reason) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);

        writer.println(reason);
        for (String failure : failures) {
            writer.println("    " + failure);
        }

        return stringWriter.toString();
    }

    private String getUrlString(File f) throws ValidationFailedException {
        try {
            return f.toURI().toURL().toString();
        } catch (MalformedURLException e) {
            throw new ValidationFailedException("Invalid file location", e);
        }
    }
}
