/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.shrinkwrap.resolver.impl.maven.bootstrap;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.maven.model.building.DefaultModelBuilderFactory;
import org.apache.maven.model.building.ModelBuilder;
import org.apache.maven.repository.internal.DefaultArtifactDescriptorReader;
import org.apache.maven.repository.internal.DefaultVersionRangeResolver;
import org.apache.maven.repository.internal.DefaultVersionResolver;
import org.apache.maven.repository.internal.SnapshotMetadataGeneratorFactory;
import org.apache.maven.repository.internal.VersionsMetadataGeneratorFactory;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.ArtifactDescriptorReader;
import org.eclipse.aether.impl.ArtifactResolver;
import org.eclipse.aether.impl.DependencyCollector;
import org.eclipse.aether.impl.Deployer;
import org.eclipse.aether.impl.Installer;
import org.eclipse.aether.impl.LocalRepositoryProvider;
import org.eclipse.aether.impl.MetadataGeneratorFactory;
import org.eclipse.aether.impl.MetadataResolver;
import org.eclipse.aether.impl.OfflineController;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.RepositoryConnectorProvider;
import org.eclipse.aether.impl.RepositoryEventDispatcher;
import org.eclipse.aether.impl.SyncContextFactory;
import org.eclipse.aether.impl.UpdateCheckManager;
import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
import org.eclipse.aether.impl.VersionRangeResolver;
import org.eclipse.aether.impl.VersionResolver;
import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
import org.eclipse.aether.internal.impl.DefaultDependencyCollector;
import org.eclipse.aether.internal.impl.DefaultDeployer;
import org.eclipse.aether.internal.impl.DefaultFileProcessor;
import org.eclipse.aether.internal.impl.DefaultInstaller;
import org.eclipse.aether.internal.impl.DefaultLocalRepositoryProvider;
import org.eclipse.aether.internal.impl.DefaultMetadataResolver;
import org.eclipse.aether.internal.impl.DefaultOfflineController;
import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
import org.eclipse.aether.internal.impl.DefaultSyncContextFactory;
import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManagerFactory;
import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.connector.transport.TransporterProvider;
import org.eclipse.aether.spi.io.FileProcessor;
import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
import org.eclipse.aether.spi.locator.Service;
import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.spi.log.LoggerFactory;
import org.eclipse.aether.transport.wagon.WagonProvider;
import org.eclipse.aether.transport.wagon.WagonTransporterFactory;
import org.jboss.shrinkwrap.resolver.impl.maven.logging.AetherLoggerFactory;

/**
 * A service locator for bootstrapping repository system
 *
 * @author <a href="kpiwko@redhat.com">Karel Piwko</a>
 *
 */
class ShrinkWrapResolverServiceLocator implements ServiceLocator {
    private static final Logger log = Logger.getLogger(ShrinkWrapResolverServiceLocator.class.getName());

    private final Map<Class<?>, CacheItem> cache;

    /**
     * Representation of either implementation class or instance for given service to allow lazy-loading of the services
     *
     * @author <a href="kpiwko@redhat.com">Karel Piwko</a>
     *
     */
    class CacheItem {
        Class<?> type;
        List<Class<?>> implementations;
        List<Object> instances;

        CacheItem(Class<?> type) {
            this.type = type;
            this.implementations = new ArrayList<Class<?>>();
            this.instances = new ArrayList<Object>();
        }

        void addImplementation(Class<?> classImpl) {
            implementations.add(classImpl);
        }

        synchronized List<Object> instantiate() {

            for (Class<?> impl : implementations) {
                try {
                    Object instance = SecurityActions.newInstance(impl, new Class<?>[0], new Object[0]);

                    // lazy load other services if required
                    if (instance instanceof Service) {
                        ((Service) instance).initService(ShrinkWrapResolverServiceLocator.this);
                    }
                    instances.add(type.cast(instance));
                } catch (Exception e) {
                    log.log(Level.SEVERE,
                            MessageFormat.format("Failed instantiating {0}, implementation of {1}", impl.getName(),
                                    type.getName()), e);
                }
            }
            return instances;
        }

        synchronized void replaceInstances(Object... newInstaces) {
            this.implementations.clear();
            this.instances.clear();
            this.instances.addAll(Arrays.asList(newInstaces));
        }

    }

    public ShrinkWrapResolverServiceLocator() {

        this.cache = new HashMap<Class<?>, CacheItem>();

        addService(RepositorySystem.class, DefaultRepositorySystem.class);
        addService(ArtifactResolver.class, DefaultArtifactResolver.class);
        addService(DependencyCollector.class, DefaultDependencyCollector.class);
        addService(Deployer.class, DefaultDeployer.class);
        addService(Installer.class, DefaultInstaller.class);
        addService(MetadataResolver.class, DefaultMetadataResolver.class);
        addService(RepositoryConnectorProvider.class, DefaultRepositoryConnectorProvider.class);
        addService(RemoteRepositoryManager.class, DefaultRemoteRepositoryManager.class);
        addService(UpdateCheckManager.class, DefaultUpdateCheckManager.class);
        addService(UpdatePolicyAnalyzer.class, DefaultUpdatePolicyAnalyzer.class);
        addService(FileProcessor.class, DefaultFileProcessor.class);
        addService(SyncContextFactory.class, DefaultSyncContextFactory.class);
        addService(RepositoryEventDispatcher.class, DefaultRepositoryEventDispatcher.class);
        addService(OfflineController.class, DefaultOfflineController.class);
        addService(LocalRepositoryProvider.class, DefaultLocalRepositoryProvider.class);
        addService(LocalRepositoryManagerFactory.class, SimpleLocalRepositoryManagerFactory.class);
        addService(LocalRepositoryManagerFactory.class, EnhancedLocalRepositoryManagerFactory.class);

        // add Maven supported services, we are not using MavenServiceLocator as it should not be used from
        // Maven plugins, however we need to do that for dependency tree output
        // class names for internal aether classes we need to register implementations for
        addService(ArtifactDescriptorReader.class, DefaultArtifactDescriptorReader.class);
        addService(VersionResolver.class, DefaultVersionResolver.class);
        addService(VersionRangeResolver.class, DefaultVersionRangeResolver.class);
        addService(MetadataGeneratorFactory.class, SnapshotMetadataGeneratorFactory.class);
        addService(MetadataGeneratorFactory.class, VersionsMetadataGeneratorFactory.class);

        // add our own services
        setServices(ModelBuilder.class, new DefaultModelBuilderFactory().newInstance());
        setServices(WagonProvider.class, new ManualWagonProvider());

        // add default services introduced after aether 0.9.0.M2
        addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
        addService(TransporterProvider.class, DefaultTransporterProvider.class);
        addService(TransporterFactory.class, WagonTransporterFactory.class);
        addService(RepositoryLayoutProvider.class, DefaultRepositoryLayoutProvider.class);
        addService(RepositoryLayoutFactory.class, Maven2RepositoryLayoutFactory.class);
        addService(ChecksumPolicyProvider.class, DefaultChecksumPolicyProvider.class);

        // to avoid problems with SLF4J, we are having a JUL bridge
        setServices(LoggerFactory.class, new AetherLoggerFactory());
    }

    private <T> ShrinkWrapResolverServiceLocator addService(Class<T> type, Class<? extends T> implementationType) {

        CacheItem item = cache.get(type);
        if (item == null) {
            item = new CacheItem(type);
        }

        item.addImplementation(implementationType);
        cache.put(type, item);
        return this;
    }

    /**
     * Sets the instances for a service.
     *
     * @param <T> The service type.
     * @param type The interface describing the service, must not be {@code null}.
     * @param services The instances of the service, may be {@code null} but must not contain {@code null} elements.
     * @return This locator for chaining, never {@code null}.
     */
    @SuppressWarnings("unchecked")
    private <T> ShrinkWrapResolverServiceLocator setServices(Class<T> type, T... services) {

        CacheItem item = cache.get(type);
        if (item == null) {
            item = new CacheItem(type);
        }

        item.replaceInstances(services);
        cache.put(type, item);
        return this;

    }

    @Override
    public <T> T getService(Class<T> serviceType) {

        List<T> services = getServices(serviceType);
        if (services.size() == 1) {
            return services.iterator().next();
        }

        if (services.size() > 1) {
            throw new IllegalStateException(MessageFormat.format(
                    "Unable to identify service for {0}, multiple ({1}) services implementations were registered.",
                    serviceType.getName(),
                    services.size()));
        }

        // this represents and exception we can't recover from
        if (serviceType.isAssignableFrom(RepositorySystem.class)) {
            // zero services available
            throw new IllegalStateException(
                    "Unable to boostrap Aether repository system, missing RepositoryService "
                            + serviceType.getName()
                            + ", probably due to missing or invalid Aether dependencies. "
                            + " You are either running from within Maven plugin with Maven 3.0.x version (make sure to update to Maven 3.1.0 or newer) or "
                            + " you have org.apache.maven:maven-aether-provider:3.0.x on classpath shading required binding (make sure to update dependencies in your project).");
        }

        // there might be some services, however we can live with them being null
        return null;

    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> getServices(Class<T> serviceType) {

        CacheItem item = cache.get(serviceType);
        if (item == null) {
            // this represents and exception we can't recover from
            if (serviceType.isAssignableFrom(RepositorySystem.class)) {

                throw new IllegalStateException(
                        "Unable to boostrap Aether repository system, missing RepositoryService "
                                + serviceType.getName()
                                + ", probably due to missing or invalid Aether dependencies. "
                                + " You are either running from within Maven plugin with Maven 3.0.x version (make sure to update to Maven 3.1.0 or newer) or "
                                + " you have org.apache.maven:maven-aether-provider:3.0.x on classpath shading required binding (make sure to update dependencies in your project).");
            }

            return Collections.emptyList();

        }

        // classes were not yet instantiated
        if (item.instances.isEmpty()) {
            return (List<T>) item.instantiate();
        }

        // we already have instances, so let's return them
        return (List<T>) item.instances;
    }
}