/*
 * 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.maven.cling.extensions;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.apache.maven.RepositoryUtils;
import org.apache.maven.api.Service;
import org.apache.maven.api.Session;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.cache.RequestCacheFactory;
import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.services.ArtifactCoordinatesFactory;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.ArtifactResolver;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.api.services.RepositoryFactory;
import org.apache.maven.api.services.VersionParser;
import org.apache.maven.api.services.VersionRangeResolver;
import org.apache.maven.cling.invoker.ProtoLookup;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.extension.internal.CoreExports;
import org.apache.maven.extension.internal.CoreExtensionEntry;
import org.apache.maven.impl.DefaultArtifactCoordinatesFactory;
import org.apache.maven.impl.DefaultArtifactResolver;
import org.apache.maven.impl.DefaultModelVersionParser;
import org.apache.maven.impl.DefaultRepositoryFactory;
import org.apache.maven.impl.DefaultVersionParser;
import org.apache.maven.impl.DefaultVersionRangeResolver;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.impl.cache.DefaultRequestCacheFactory;
import org.apache.maven.impl.model.DefaultInterpolator;
import org.apache.maven.internal.impl.DefaultArtifactManager;
import org.apache.maven.internal.impl.DefaultSession;
import org.apache.maven.plugin.PluginResolutionException;
import org.apache.maven.plugin.internal.PluginDependenciesResolver;
import org.apache.maven.resolver.MavenChainedWorkspaceReader;
import org.apache.maven.resolver.RepositorySystemSessionFactory;
import org.codehaus.plexus.DefaultPlexusContainer;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RepositorySystemSession.CloseableSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.filter.ExclusionsDependencyFilter;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * BootstrapCoreExtensionManager
 */
@Named
public class BootstrapCoreExtensionManager {
    public static final String STRATEGY_PARENT_FIRST = "parent-first";
    public static final String STRATEGY_PLUGIN = "plugin";
    public static final String STRATEGY_SELF_FIRST = "self-first";

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final PluginDependenciesResolver pluginDependenciesResolver;

    private final RepositorySystemSessionFactory repositorySystemSessionFactory;

    private final CoreExports coreExports;

    private final ClassWorld classWorld;

    private final ClassRealm parentRealm;

    private final WorkspaceReader ideWorkspaceReader;

    private final RepositorySystem repoSystem;

    private final Interpolator interpolator;

    @Inject
    public BootstrapCoreExtensionManager(
            PluginDependenciesResolver pluginDependenciesResolver,
            RepositorySystemSessionFactory repositorySystemSessionFactory,
            CoreExports coreExports,
            PlexusContainer container,
            @Nullable @Named("ide") WorkspaceReader ideWorkspaceReader,
            RepositorySystem repoSystem,
            Interpolator interpolator) {
        this.pluginDependenciesResolver = pluginDependenciesResolver;
        this.repositorySystemSessionFactory = repositorySystemSessionFactory;
        this.coreExports = coreExports;
        this.classWorld = ((DefaultPlexusContainer) container).getClassWorld();
        this.parentRealm = container.getContainerRealm();
        this.ideWorkspaceReader = ideWorkspaceReader;
        this.repoSystem = repoSystem;
        this.interpolator = interpolator;
    }

    public List<LoadedCoreExtension> loadCoreExtensions(
            MavenExecutionRequest request, Set<String> providedArtifacts, List<CoreExtension> extensions)
            throws Exception {
        try (CloseableSession repoSession = repositorySystemSessionFactory
                .newRepositorySessionBuilder(request)
                .setWorkspaceReader(new MavenChainedWorkspaceReader(request.getWorkspaceReader(), ideWorkspaceReader))
                .build()) {
            MavenSession mSession = new MavenSession(repoSession, request, new DefaultMavenExecutionResult());
            InternalSession iSession = new SimpleSession(mSession, repoSystem, null);
            InternalSession.associate(repoSession, iSession);

            List<RemoteRepository> repositories = RepositoryUtils.toRepos(request.getPluginArtifactRepositories());
            UnaryOperator<String> interpolator = createInterpolator(request);

            return resolveCoreExtensions(repoSession, repositories, providedArtifacts, extensions, interpolator);
        }
    }

    private List<LoadedCoreExtension> resolveCoreExtensions(
            RepositorySystemSession repoSession,
            List<RemoteRepository> repositories,
            Set<String> providedArtifacts,
            List<CoreExtension> configuration,
            UnaryOperator<String> interpolator)
            throws Exception {
        List<LoadedCoreExtension> extensions = new ArrayList<>();

        DependencyFilter dependencyFilter = new ExclusionsDependencyFilter(providedArtifacts);

        for (CoreExtension extension : configuration) {
            List<Artifact> artifacts =
                    resolveExtension(extension, repoSession, repositories, dependencyFilter, interpolator);
            if (!artifacts.isEmpty()) {
                extensions.add(new LoadedCoreExtension(extension, createExtension(extension, artifacts)));
            }
        }

        return Collections.unmodifiableList(extensions);
    }

    private CoreExtensionEntry createExtension(CoreExtension extension, List<Artifact> artifacts) throws Exception {
        String realmId = "coreExtension>" + extension.getGroupId() + ":" + extension.getArtifactId() + ":"
                + extension.getVersion();
        final ClassRealm realm = classWorld.newRealm(realmId, null);
        Set<String> providedArtifacts = Collections.emptySet();
        String classLoadingStrategy = extension.getClassLoadingStrategy();
        if (STRATEGY_PARENT_FIRST.equals(classLoadingStrategy)) {
            realm.importFrom(parentRealm, "");
        } else if (STRATEGY_PLUGIN.equals(classLoadingStrategy)) {
            coreExports.getExportedPackages().forEach((p, cl) -> realm.importFrom(cl, p));
            providedArtifacts = coreExports.getExportedArtifacts();
        } else if (STRATEGY_SELF_FIRST.equals(classLoadingStrategy)) {
            realm.setParentRealm(parentRealm);
        } else {
            throw new IllegalArgumentException("Unsupported class-loading strategy '"
                    + classLoadingStrategy + "'. Supported values are: " + STRATEGY_PARENT_FIRST
                    + ", " + STRATEGY_PLUGIN + " and " + STRATEGY_SELF_FIRST);
        }
        log.debug("Populating class realm {}", realm.getId());
        for (Artifact artifact : artifacts) {
            String id = artifact.getGroupId() + ":" + artifact.getArtifactId();
            if (providedArtifacts.contains(id)) {
                log.debug("  Excluded {}", id);
            } else {
                Path file = artifact.getPath();
                log.debug("  Included {} located at {}", id, file);
                realm.addURL(file.toUri().toURL());
            }
        }
        return CoreExtensionEntry.discoverFrom(
                realm,
                Collections.singleton(artifacts.get(0).getPath().toFile()),
                extension.getGroupId() + ":" + extension.getArtifactId(),
                extension.getConfiguration());
    }

    private List<Artifact> resolveExtension(
            CoreExtension extension,
            RepositorySystemSession repoSession,
            List<RemoteRepository> repositories,
            DependencyFilter dependencyFilter,
            UnaryOperator<String> interpolator)
            throws ExtensionResolutionException {
        try {
            /* TODO: Enhance the PluginDependenciesResolver to provide a
             * resolveCoreExtensionAndFlatten method which uses a CoreExtension
             * object instead of a Plugin as this makes no sense.
             */
            Plugin plugin = Plugin.newBuilder()
                    .groupId(interpolator.apply(extension.getGroupId()))
                    .artifactId(interpolator.apply(extension.getArtifactId()))
                    .version(interpolator.apply(extension.getVersion()))
                    .build();

            DependencyResult result = pluginDependenciesResolver.resolveCoreExtensionAndFlatten(
                    new org.apache.maven.model.Plugin(plugin), dependencyFilter, repositories, repoSession);
            return result.getArtifactResults().stream()
                    .filter(ArtifactResult::isResolved)
                    .map(ArtifactResult::getArtifact)
                    .collect(Collectors.toList());
        } catch (PluginResolutionException | InterpolatorException e) {
            throw new ExtensionResolutionException(extension, e);
        }
    }

    private UnaryOperator<String> createInterpolator(MavenExecutionRequest request) {
        UnaryOperator<String> callback = v -> {
            String r = request.getUserProperties().getProperty(v);
            if (r == null) {
                r = request.getSystemProperties().getProperty(v);
            }
            return r != null ? r : v;
        };
        return v -> interpolator.interpolate(v, callback);
    }

    static class SimpleSession extends DefaultSession {
        SimpleSession(
                MavenSession session,
                RepositorySystem repositorySystem,
                List<org.apache.maven.api.RemoteRepository> repositories) {
            super(
                    session,
                    repositorySystem,
                    repositories,
                    null,
                    ProtoLookup.builder()
                            .addMapping(RequestCacheFactory.class, new DefaultRequestCacheFactory())
                            .build(),
                    null);
        }

        @Override
        protected Session newSession(
                MavenSession mavenSession, List<org.apache.maven.api.RemoteRepository> repositories) {
            return new SimpleSession(mavenSession, getRepositorySystem(), repositories);
        }

        @SuppressWarnings("unchecked")
        @Override
        public <T extends Service> T getService(Class<T> clazz) throws NoSuchElementException {
            if (clazz == ArtifactCoordinatesFactory.class) {
                return (T) new DefaultArtifactCoordinatesFactory();
            } else if (clazz == VersionParser.class) {
                return (T) new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme()));
            } else if (clazz == VersionRangeResolver.class) {
                return (T) new DefaultVersionRangeResolver(repositorySystem);
            } else if (clazz == ArtifactResolver.class) {
                return (T) new DefaultArtifactResolver();
            } else if (clazz == ArtifactManager.class) {
                return (T) new DefaultArtifactManager(this);
            } else if (clazz == RepositoryFactory.class) {
                return (T) new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager(
                        new DefaultUpdatePolicyAnalyzer(),
                        new DefaultChecksumPolicyProvider(),
                        new DefaultRepositoryKeyFunctionFactory()));
            } else if (clazz == Interpolator.class) {
                return (T) new DefaultInterpolator();
                // } else if (clazz == ModelResolver.class) {
                //    return (T) new DefaultModelResolver();
            }
            throw new NoSuchElementException("No service for " + clazz.getName());
        }
    }
}
