/*
 * 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.project.artifact;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.metadata.ResolutionGroup;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
import org.codehaus.plexus.component.annotations.Component;

/**
 * DefaultMavenMetadataCache
 */
@Component(role = MavenMetadataCache.class)
public class DefaultMavenMetadataCache implements MavenMetadataCache {

    protected final Map<CacheKey, CacheRecord> cache = new ConcurrentHashMap<>();

    /**
     * CacheKey
     */
    public static class CacheKey {
        private final Artifact artifact;
        private final long pomHash;
        private final boolean resolveManagedVersions;
        private final List<ArtifactRepository> repositories = new ArrayList<>();
        private final int hashCode;

        public CacheKey(
                Artifact artifact,
                boolean resolveManagedVersions,
                ArtifactRepository localRepository,
                List<ArtifactRepository> remoteRepositories) {
            File file = artifact.getFile();
            this.artifact = ArtifactUtils.copyArtifact(artifact);
            if ("pom".equals(artifact.getType()) && file != null) {
                pomHash = file.getPath().hashCode() + file.lastModified();
            } else {
                pomHash = 0;
            }
            this.resolveManagedVersions = resolveManagedVersions;
            this.repositories.add(localRepository);
            this.repositories.addAll(remoteRepositories);

            int hash = 17;
            hash = hash * 31 + artifactHashCode(artifact);
            hash = hash * 31 + (resolveManagedVersions ? 1 : 2);
            hash = hash * 31 + repositoriesHashCode(repositories);
            this.hashCode = hash;
        }

        @Override
        public int hashCode() {
            return hashCode;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }

            if (!(o instanceof CacheKey)) {
                return false;
            }

            CacheKey other = (CacheKey) o;

            return pomHash == other.pomHash
                    && artifactEquals(artifact, other.artifact)
                    && resolveManagedVersions == other.resolveManagedVersions
                    && repositoriesEquals(repositories, other.repositories);
        }
    }

    private static int artifactHashCode(Artifact a) {
        int result = 17;
        result = 31 * result + a.getGroupId().hashCode();
        result = 31 * result + a.getArtifactId().hashCode();
        result = 31 * result + a.getType().hashCode();
        if (a.getVersion() != null) {
            result = 31 * result + a.getVersion().hashCode();
        }
        result = 31 * result + (a.getClassifier() != null ? a.getClassifier().hashCode() : 0);
        result = 31 * result + (a.getScope() != null ? a.getScope().hashCode() : 0);
        result = 31 * result
                + (a.getDependencyFilter() != null ? a.getDependencyFilter().hashCode() : 0);
        result = 31 * result + (a.isOptional() ? 1 : 0);
        return result;
    }

    private static boolean artifactEquals(Artifact a1, Artifact a2) {
        if (a1 == a2) {
            return true;
        }

        return Objects.equals(a1.getGroupId(), a2.getGroupId())
                && Objects.equals(a1.getArtifactId(), a2.getArtifactId())
                && Objects.equals(a1.getType(), a2.getType())
                && Objects.equals(a1.getVersion(), a2.getVersion())
                && Objects.equals(a1.getClassifier(), a2.getClassifier())
                && Objects.equals(a1.getScope(), a2.getScope())
                && Objects.equals(a1.getDependencyFilter(), a2.getDependencyFilter())
                && a1.isOptional() == a2.isOptional();
    }

    private static int repositoryHashCode(ArtifactRepository repository) {
        int result = 17;
        result = 31 * result + (repository.getId() != null ? repository.getId().hashCode() : 0);
        return result;
    }

    private static int repositoriesHashCode(List<ArtifactRepository> repositories) {
        int result = 17;
        for (ArtifactRepository repository : repositories) {
            result = 31 * result + repositoryHashCode(repository);
        }
        return result;
    }

    private static boolean repositoryEquals(ArtifactRepository r1, ArtifactRepository r2) {
        if (r1 == r2) {
            return true;
        }

        return Objects.equals(r1.getId(), r2.getId())
                && Objects.equals(r1.getUrl(), r2.getUrl())
                && repositoryPolicyEquals(r1.getReleases(), r2.getReleases())
                && repositoryPolicyEquals(r1.getSnapshots(), r2.getSnapshots());
    }

    private static boolean repositoryPolicyEquals(ArtifactRepositoryPolicy p1, ArtifactRepositoryPolicy p2) {
        if (p1 == p2) {
            return true;
        }

        return p1.isEnabled() == p2.isEnabled() && Objects.equals(p1.getUpdatePolicy(), p2.getUpdatePolicy());
    }

    private static boolean repositoriesEquals(List<ArtifactRepository> r1, List<ArtifactRepository> r2) {
        if (r1.size() != r2.size()) {
            return false;
        }

        for (Iterator<ArtifactRepository> it1 = r1.iterator(), it2 = r2.iterator(); it1.hasNext(); ) {
            if (!repositoryEquals(it1.next(), it2.next())) {
                return false;
            }
        }

        return true;
    }

    /**
     * CacheRecord
     */
    public class CacheRecord {
        private Artifact pomArtifact;
        private Artifact relocatedArtifact;
        private List<Artifact> artifacts;
        private Map<String, Artifact> managedVersions;
        private List<ArtifactRepository> remoteRepositories;

        private long length;
        private long timestamp;

        CacheRecord(
                Artifact pomArtifact,
                Artifact relocatedArtifact,
                Set<Artifact> artifacts,
                Map<String, Artifact> managedVersions,
                List<ArtifactRepository> remoteRepositories) {
            this.pomArtifact = ArtifactUtils.copyArtifact(pomArtifact);
            this.relocatedArtifact = ArtifactUtils.copyArtifactSafe(relocatedArtifact);
            this.artifacts = ArtifactUtils.copyArtifacts(artifacts, new ArrayList<Artifact>());
            this.remoteRepositories = new ArrayList<>(remoteRepositories);

            this.managedVersions = managedVersions;
            if (managedVersions != null) {
                this.managedVersions =
                        ArtifactUtils.copyArtifacts(managedVersions, new LinkedHashMap<String, Artifact>());
            }

            File pomFile = pomArtifact.getFile();
            if (pomFile != null && pomFile.canRead()) {
                this.length = pomFile.length();
                this.timestamp = pomFile.lastModified();
            } else {
                this.length = -1;
                this.timestamp = -1;
            }
        }

        public Artifact getArtifact() {
            return pomArtifact;
        }

        public Artifact getRelocatedArtifact() {
            return relocatedArtifact;
        }

        public List<Artifact> getArtifacts() {
            return artifacts;
        }

        public Map<String, Artifact> getManagedVersions() {
            return managedVersions;
        }

        public List<ArtifactRepository> getRemoteRepositories() {
            return remoteRepositories;
        }

        public boolean isStale() {
            File pomFile = pomArtifact.getFile();
            if (pomFile != null) {
                if (pomFile.canRead()) {
                    return length != pomFile.length() || timestamp != pomFile.lastModified();
                } else {
                    // if the POM didn't exist, retry if any repo is configured to always update
                    boolean snapshot = pomArtifact.isSnapshot();
                    for (ArtifactRepository repository : remoteRepositories) {
                        ArtifactRepositoryPolicy policy =
                                snapshot ? repository.getSnapshots() : repository.getReleases();
                        if (ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS.equals(policy.getUpdatePolicy())) {
                            return true;
                        }
                    }
                }
            }

            return length != -1 || timestamp != -1;
        }
    }

    public ResolutionGroup get(
            Artifact artifact,
            boolean resolveManagedVersions,
            ArtifactRepository localRepository,
            List<ArtifactRepository> remoteRepositories) {
        CacheKey cacheKey = newCacheKey(artifact, resolveManagedVersions, localRepository, remoteRepositories);

        CacheRecord cacheRecord = cache.get(cacheKey);

        if (cacheRecord != null && !cacheRecord.isStale()) {
            Artifact pomArtifact = ArtifactUtils.copyArtifact(cacheRecord.getArtifact());
            Artifact relocatedArtifact = ArtifactUtils.copyArtifactSafe(cacheRecord.getRelocatedArtifact());
            Set<Artifact> artifacts =
                    ArtifactUtils.copyArtifacts(cacheRecord.getArtifacts(), new LinkedHashSet<Artifact>());
            Map<String, Artifact> managedVersions = cacheRecord.getManagedVersions();
            if (managedVersions != null) {
                managedVersions = ArtifactUtils.copyArtifacts(managedVersions, new LinkedHashMap<String, Artifact>());
            }
            return new ResolutionGroup(
                    pomArtifact, relocatedArtifact, artifacts, managedVersions, cacheRecord.getRemoteRepositories());
        }

        cache.remove(cacheKey);

        return null;
    }

    public void put(
            Artifact artifact,
            boolean resolveManagedVersions,
            ArtifactRepository localRepository,
            List<ArtifactRepository> remoteRepositories,
            ResolutionGroup result) {
        put(newCacheKey(artifact, resolveManagedVersions, localRepository, remoteRepositories), result);
    }

    protected CacheKey newCacheKey(
            Artifact artifact,
            boolean resolveManagedVersions,
            ArtifactRepository localRepository,
            List<ArtifactRepository> remoteRepositories) {
        return new CacheKey(artifact, resolveManagedVersions, localRepository, remoteRepositories);
    }

    protected void put(CacheKey cacheKey, ResolutionGroup result) {
        CacheRecord cacheRecord = new CacheRecord(
                result.getPomArtifact(),
                result.getRelocatedArtifact(),
                result.getArtifacts(),
                result.getManagedVersions(),
                result.getResolutionRepositories());

        cache.put(cacheKey, cacheRecord);
    }

    public void flush() {
        cache.clear();
    }
}
