/**
 * Copyright 2015-2017 Red Hat, Inc, and 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.wildfly.swarm.bootstrap.modules;

import org.jboss.modules.Module;
import org.jboss.modules.maven.ArtifactCoordinates;
import org.jboss.modules.maven.MavenArtifactUtil;
import org.jboss.modules.maven.MavenResolver;

import javax.xml.xpath.XPathExpressionException;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

/**
 * This resolver try to find the requested artifact in the local gradle cache. If the artifact is missing, it try
 * to download it from a remote repository. Default is the https://repo1.maven.org/maven2/ repository. With the
 * system property 'remote.maven.repo' it's possible to specify additional repositories.
 *
 * @author Michael Fraefel
 */
public class GradleResolver implements MavenResolver {
    private final String gradleCachePath;
    private final List<String> remoteRepositories = new LinkedList<>();

    public GradleResolver(String gradleCachePath) {
        this.gradleCachePath = gradleCachePath;

        String remoteRepository = System.getProperty("remote.maven.repo");
        if (remoteRepository != null) {
            for (String repo : remoteRepository.split(",")) {
                if (!repo.endsWith("/")) {
                    this.remoteRepositories.add(repo + "/");
                } else {
                    this.remoteRepositories.add(repo);
                }
            }
        }
        this.remoteRepositories.add("https://repo1.maven.org/maven2/");
    }

    @Override
    public File resolveArtifact(ArtifactCoordinates artifactCoordinates, String packaging) throws IOException {
        //Search the matching artifact in a gradle cache.
        String filter = toGradleArtifactFileName(artifactCoordinates, packaging);
        Path artifactDirectory = Paths.get(gradleCachePath, artifactCoordinates.getGroupId(), artifactCoordinates.getArtifactId(), artifactCoordinates.getVersion());
        if (Files.exists(artifactDirectory)) {
            File latestArtifactFile = null;
            for (Path hashDir : Files.list(artifactDirectory).collect(Collectors.toList())) {
                for (Path artifact : Files.list(hashDir).collect(Collectors.toList())) {
                    if (artifact.endsWith(filter)) {
                        File artifactFile = artifact.toFile();
                        if (latestArtifactFile == null || latestArtifactFile.lastModified() < artifactFile.lastModified()) {
                            //take always the latest version of the artifact
                            latestArtifactFile = artifactFile;
                        }
                    }
                }
            }
            if (latestArtifactFile != null) {
                return latestArtifactFile;
            }
        }

        //Artifact not found in the locale gradle cache. Try to resolve it from the remote respository
        return downloadFromRemoteRepository(artifactCoordinates, packaging, artifactDirectory);
    }

    /**
     * Download artifact from remote repository.
     *
     * @param artifactCoordinates
     * @param packaging
     * @param artifactDirectory
     * @return
     */
    File downloadFromRemoteRepository(ArtifactCoordinates artifactCoordinates, String packaging, Path artifactDirectory) {
        String artifactRelativeHttpPath = toGradleArtifactPath(artifactCoordinates);
        String artifactRelativeMetadataHttpPath = artifactCoordinates.relativeMetadataPath('/');
        for (String remoteRepos : remoteRepositories) {
            String artifactAbsoluteHttpPath = remoteRepos + artifactRelativeHttpPath + ".";
            File targetArtifactPomDirectory = artifactDirectory.resolve(computeGradleUUID(artifactCoordinates + ":pom")).toFile();
            File targetArtifactDirectory = artifactDirectory.resolve(computeGradleUUID(artifactCoordinates + ":" + packaging)).toFile();

            File artifactFile = doDownload(remoteRepos, artifactAbsoluteHttpPath, artifactRelativeHttpPath, artifactCoordinates, packaging, targetArtifactPomDirectory, targetArtifactDirectory);
            if (artifactFile != null) {
                return artifactFile; // Success
            }

            //Try to doDownload the snapshot version
            if (artifactCoordinates.isSnapshot()) {
                String remoteMetadataPath = remoteRepos + artifactRelativeMetadataHttpPath;
                try {
                    String timestamp = MavenArtifactUtil.downloadTimestampVersion(artifactCoordinates + ":" + packaging, remoteMetadataPath);

                    String timestampedArtifactRelativePath = artifactCoordinates.relativeArtifactPath('/', timestamp);
                    String artifactTimestampedAbsoluteHttpPath = remoteRepos + timestampedArtifactRelativePath + ".";
                    File targetTimestampedArtifactPomDirectory = artifactDirectory.resolve(computeGradleUUID(artifactCoordinates + ":" + timestamp + ":pom")).toFile();
                    File targetTimestampedArtifactDirectory = artifactDirectory.resolve(computeGradleUUID(artifactCoordinates + ":" + packaging)).toFile();

                    File snapshotArtifactFile = doDownload(remoteRepos, artifactTimestampedAbsoluteHttpPath, timestampedArtifactRelativePath, artifactCoordinates, packaging, targetTimestampedArtifactPomDirectory, targetTimestampedArtifactDirectory);
                    if (snapshotArtifactFile != null) {
                        return snapshotArtifactFile; //Success
                    }
                } catch (XPathExpressionException | IOException ex) {
                    Module.getModuleLogger().trace(ex, "Could not doDownload '%s' from '%s' repository", artifactRelativeHttpPath, remoteRepos);
                    // try next one
                }
            }
        }
        return null;
    }

    /**
     * Download the POM and the artifact file and return it.
     *
     * @param remoteRepos
     * @param artifactAbsoluteHttpPath
     * @param artifactRelativeHttpPath
     * @param artifactCoordinates
     * @param packaging
     * @param targetArtifactPomDirectory
     * @param targetArtifactDirectory
     * @return
     */
    File doDownload(String remoteRepos, String artifactAbsoluteHttpPath, String artifactRelativeHttpPath, ArtifactCoordinates artifactCoordinates, String packaging, File targetArtifactPomDirectory, File targetArtifactDirectory) {
        //Download POM
        File targetArtifactPomFile = new File(targetArtifactPomDirectory, toGradleArtifactFileName(artifactCoordinates, "pom"));
        try {
            MavenArtifactUtil.downloadFile(artifactCoordinates + ":pom", artifactAbsoluteHttpPath + "pom", targetArtifactPomFile);
        } catch (IOException e) {
            Module.getModuleLogger().trace(e, "Could not doDownload '%s' from '%s' repository", artifactRelativeHttpPath, remoteRepos);
            // try next one
        }

        //Download Artifact
        File targetArtifactFile = new File(targetArtifactDirectory, toGradleArtifactFileName(artifactCoordinates, packaging));
        try {
            MavenArtifactUtil.downloadFile(artifactCoordinates + ":" + packaging, artifactAbsoluteHttpPath + packaging, targetArtifactFile);
            if (targetArtifactFile.exists()) {
                return targetArtifactFile;
            }
        } catch (IOException e) {
            Module.getModuleLogger().trace(e, "Could not doDownload '%s' from '%s' repository", artifactRelativeHttpPath, remoteRepos);
            // try next one
        }
        return null;
    }

    /**
     * Build file name for artifact.
     *
     * @param artifactCoordinates
     * @param packaging
     * @return
     */
    String toGradleArtifactFileName(ArtifactCoordinates artifactCoordinates, String packaging) {
        StringBuilder sbFileFilter = new StringBuilder();
        sbFileFilter
                .append(artifactCoordinates.getArtifactId())
                .append("-")
                .append(artifactCoordinates.getVersion());
        if (artifactCoordinates.getClassifier() != null && artifactCoordinates.getClassifier().length() > 0) {
            sbFileFilter
                    .append("-")
                    .append(artifactCoordinates.getClassifier());
        }
        sbFileFilter
                .append(".")
                .append(packaging);
        return sbFileFilter.toString();
    }

    /**
     * Build the artifact path including the classifier.
     *
     * @param artifactCoordinates
     * @return
     */
    String toGradleArtifactPath(ArtifactCoordinates artifactCoordinates) {
        // e.g.: org/jboss/ws/cxf/jbossws-cxf-resources/5.1.5.Final/jbossws-cxf-resources-5.1.5.Final
        String pathWithMissingClassifier = artifactCoordinates.relativeArtifactPath('/');
        StringBuilder sb = new StringBuilder(pathWithMissingClassifier);

        if (artifactCoordinates.getClassifier() != null && artifactCoordinates.getClassifier().length() > 0) {
            // org/jboss/ws/cxf/jbossws-cxf-resources/5.1.5.Final/jbossws-cxf-resources-5.1.5.Final-wildfly1000
            sb
                    .append("-")
                    .append(artifactCoordinates.getClassifier());
        }

        return sb.toString();
    }

    /**
     * Compute gradle uuid for artifacts.
     *
     * @param content
     * @return
     */
    String computeGradleUUID(String content) {
        try {
            MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
            md.reset();
            byte[] bytes = content.trim().toLowerCase(Locale.US).getBytes("UTF-8");
            md.update(bytes, 0, bytes.length);
            return byteArrayToHexString(md.digest());
        } catch (NoSuchAlgorithmException e) {
            // Impossible
            throw new IllegalArgumentException("unknown algorithm " + MD5_ALGORITHM, e);
        } catch (UnsupportedEncodingException e) {
            // Impossible except with IBM :)
            throw new IllegalArgumentException("unknown charset UTF-8", e);
        }
    }

    String byteArrayToHexString(byte[] in) {
        byte ch = 0x00;

        if (in == null || in.length <= 0) {
            return null;
        }

        StringBuffer out = new StringBuffer(in.length * 2);

        //CheckStyle:MagicNumber OFF
        for (byte b : in) {
            ch = (byte) (b & 0xF0); // Strip off high nibble
            ch = (byte) (ch >>> 4); // shift the bits down
            ch = (byte) (ch & 0x0F); // must do this is high order bit is on!

            out.append(CHARS[(int) ch]); // convert the nibble to a String Character
            ch = (byte) (b & 0x0F); // Strip off low nibble
            out.append(CHARS[(int) ch]); // convert the nibble to a String Character
        }
        //CheckStyle:MagicNumber ON

        return out.toString();
    }

    // algorithm for gradle uuid
    private static final String MD5_ALGORITHM = "md5";
    // byte to hex string converter
    private static final char[] CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'a', 'b', 'c', 'd', 'e', 'f'};

}
