/*
 * 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.eclipse.aether.internal.impl;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
import org.eclipse.aether.transfer.NoRepositoryLayoutException;
import org.eclipse.aether.util.ConfigUtils;

import static java.util.Objects.requireNonNull;

/**
 * Provides a Maven-2 repository layout for repositories with content type {@code "default"}.
 */
@Singleton
@Named(Maven2RepositoryLayoutFactory.NAME)
public final class Maven2RepositoryLayoutFactory implements RepositoryLayoutFactory {
    public static final String NAME = "maven2";

    public static final String CONFIG_PROP_CHECKSUMS_ALGORITHMS = "aether.checksums.algorithms";

    private static final String DEFAULT_CHECKSUMS_ALGORITHMS = "SHA-1,MD5";

    public static final String CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS =
            "aether.checksums.omitChecksumsForExtensions";

    private static final String DEFAULT_OMIT_CHECKSUMS_FOR_EXTENSIONS = ".asc,.sigstore";

    private float priority;

    private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;

    public float getPriority() {
        return priority;
    }

    @Inject
    public Maven2RepositoryLayoutFactory(ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector) {
        this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
    }

    /**
     * Sets the priority of this component.
     *
     * @param priority The priority.
     * @return This component for chaining, never {@code null}.
     */
    public Maven2RepositoryLayoutFactory setPriority(float priority) {
        this.priority = priority;
        return this;
    }

    public RepositoryLayout newInstance(RepositorySystemSession session, RemoteRepository repository)
            throws NoRepositoryLayoutException {
        requireNonNull(session, "session cannot be null");
        requireNonNull(repository, "repository cannot be null");
        if (!"default".equals(repository.getContentType())) {
            throw new NoRepositoryLayoutException(repository);
        }

        List<ChecksumAlgorithmFactory> checksumsAlgorithms = checksumAlgorithmFactorySelector.selectList(
                ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString(
                        session,
                        DEFAULT_CHECKSUMS_ALGORITHMS,
                        CONFIG_PROP_CHECKSUMS_ALGORITHMS + "." + repository.getId(),
                        CONFIG_PROP_CHECKSUMS_ALGORITHMS)));

        // ensure uniqueness of (potentially user set) extension list
        Set<String> omitChecksumsForExtensions = Arrays.stream(ConfigUtils.getString(
                                session,
                                DEFAULT_OMIT_CHECKSUMS_FOR_EXTENSIONS,
                                CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS)
                        .split(","))
                .filter(s -> s != null && !s.trim().isEmpty())
                .collect(Collectors.toSet());

        // validation: enforce that all strings in this set are having leading dot
        if (omitChecksumsForExtensions.stream().anyMatch(s -> !s.startsWith("."))) {
            throw new IllegalArgumentException(String.format(
                    "The configuration %s contains illegal values: %s (all entries must start with '.' (dot))",
                    CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS, omitChecksumsForExtensions));
        }

        return new Maven2RepositoryLayout(
                checksumAlgorithmFactorySelector, checksumsAlgorithms, omitChecksumsForExtensions);
    }

    private static class Maven2RepositoryLayout implements RepositoryLayout {
        private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;

        private final List<ChecksumAlgorithmFactory> configuredChecksumAlgorithms;

        private final Set<String> extensionsWithoutChecksums;

        private Maven2RepositoryLayout(
                ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
                List<ChecksumAlgorithmFactory> configuredChecksumAlgorithms,
                Set<String> extensionsWithoutChecksums) {
            this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
            this.configuredChecksumAlgorithms = Collections.unmodifiableList(configuredChecksumAlgorithms);
            this.extensionsWithoutChecksums = requireNonNull(extensionsWithoutChecksums);
        }

        private URI toUri(String path) {
            try {
                return new URI(null, null, path, null);
            } catch (URISyntaxException e) {
                throw new IllegalStateException(e);
            }
        }

        @Override
        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
            return configuredChecksumAlgorithms;
        }

        @Override
        public boolean hasChecksums(Artifact artifact) {
            String artifactExtension = artifact.getExtension(); // ie. pom.asc
            for (String extensionWithoutChecksums : extensionsWithoutChecksums) {
                if (artifactExtension.endsWith(extensionWithoutChecksums)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public URI getLocation(Artifact artifact, boolean upload) {
            StringBuilder path = new StringBuilder(128);

            path.append(artifact.getGroupId().replace('.', '/')).append('/');

            path.append(artifact.getArtifactId()).append('/');

            path.append(artifact.getBaseVersion()).append('/');

            path.append(artifact.getArtifactId()).append('-').append(artifact.getVersion());

            if (!artifact.getClassifier().isEmpty()) {
                path.append('-').append(artifact.getClassifier());
            }

            if (!artifact.getExtension().isEmpty()) {
                path.append('.').append(artifact.getExtension());
            }

            return toUri(path.toString());
        }

        @Override
        public URI getLocation(Metadata metadata, boolean upload) {
            StringBuilder path = new StringBuilder(128);

            if (!metadata.getGroupId().isEmpty()) {
                path.append(metadata.getGroupId().replace('.', '/')).append('/');

                if (!metadata.getArtifactId().isEmpty()) {
                    path.append(metadata.getArtifactId()).append('/');

                    if (!metadata.getVersion().isEmpty()) {
                        path.append(metadata.getVersion()).append('/');
                    }
                }
            }

            path.append(metadata.getType());

            return toUri(path.toString());
        }

        @Override
        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
            if (!hasChecksums(artifact) || isChecksum(artifact.getExtension())) {
                return Collections.emptyList();
            }
            return getChecksumLocations(location);
        }

        @Override
        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
            return getChecksumLocations(location);
        }

        private List<ChecksumLocation> getChecksumLocations(URI location) {
            List<ChecksumLocation> checksumLocations = new ArrayList<>(configuredChecksumAlgorithms.size());
            for (ChecksumAlgorithmFactory checksumAlgorithmFactory : configuredChecksumAlgorithms) {
                checksumLocations.add(ChecksumLocation.forLocation(location, checksumAlgorithmFactory));
            }
            return checksumLocations;
        }

        private boolean isChecksum(String extension) {
            return checksumAlgorithmFactorySelector.isChecksumExtension(extension);
        }
    }
}
