/*
 * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
 * Licensed under the Apache License, Version 2.0 (the "License")
 */
package rife.bld.dependencies;

import rife.bld.DependencySet;
import rife.bld.dependencies.exceptions.*;
import rife.tools.FileUtils;
import rife.tools.StringUtils;
import rife.tools.exceptions.FileUtilsErrorException;

import java.io.*;
import java.net.*;
import java.nio.channels.Channels;
import java.util.*;

public class DependencyResolver {
    public static final String MAVEN_METADATA_XML = "maven-metadata.xml";

    private final List<Repository> repositories_;
    private final Dependency dependency_;

    private Xml2MavenMetadata metadata_ = null;
    private Xml2MavenMetadata snapshotMetadata_ = null;

    public DependencyResolver(List<Repository> repositories, Dependency dependency) {
        if (repositories == null) {
            repositories = Collections.emptyList();
        }
        repositories_ = repositories;
        dependency_ = dependency;
    }

    public boolean exists() {
        try {
            if (getMavenMetadata() == null) {
                return false;
            }
            if (!dependency_.version().equals(VersionNumber.UNKNOWN)) {
                return getMavenMetadata().getVersions().contains(dependency_.version());
            }
            return true;
        } catch (ArtifactNotFoundException | ArtifactRetrievalErrorException e) {
            return false;
        }
    }

    public VersionNumber resolveVersion() {
        var version = dependency_.version();
        if (version.equals(VersionNumber.UNKNOWN)) {
            return latestVersion();
        }
        return version;
    }

    public DependencySet getDependencies(Scope... scopes) {
        var pom_dependencies = getMavenPom().getDependencies(scopes);
        var result = new DependencySet();
        for (var dependency : pom_dependencies) {
            result.add(convertPomDependency(dependency));
        }
        return result;
    }

    Dependency convertPomDependency(PomDependency pomDependency) {
        return new Dependency(
            pomDependency.groupId(),
            pomDependency.artifactId(),
            VersionNumber.parse(pomDependency.version()),
            pomDependency.classifier(),
            pomDependency.type());
    }

    public DependencySet getTransitiveDependencies(Scope... scopes) {
        var result = new DependencySet();
        var pom_dependencies = new ArrayList<>(getMavenPom().getDependencies(scopes));
        var exclusions = new Stack<Set<PomExclusion>>();
        getTransitiveDependencies(result, pom_dependencies, exclusions, scopes);
        return result;
    }

    void getTransitiveDependencies(DependencySet result, ArrayList<PomDependency> pomDependencies, Stack<Set<PomExclusion>> exclusions, Scope... scopes) {
        var next_dependencies = getMavenPom().getDependencies(scopes);

        pomDependencies.forEach(next_dependencies::remove);

        var next_it = next_dependencies.iterator();
        NextIterator:
        while (next_it.hasNext()) {
            var next_dependency = next_it.next();
            for (var exclusionset : exclusions) {
                if (exclusionset != null) {
                    for (var exclusion : exclusionset) {
                        if ((exclusion.groupId().equals("*") && exclusion.artifactId().equals("*")) ||
                            (exclusion.groupId().equals("*") && exclusion.artifactId().equals(next_dependency.artifactId())) ||
                            (exclusion.groupId().equals(next_dependency.groupId()) && exclusion.artifactId().equals("*")) ||
                            (exclusion.groupId().equals(next_dependency.groupId()) && exclusion.artifactId().equals(next_dependency.artifactId()))) {
                            next_it.remove();
                            continue NextIterator;
                        }
                    }
                }
            }
        }
        pomDependencies.addAll(next_dependencies);

        while (!pomDependencies.isEmpty()) {
            var it = pomDependencies.iterator();
            var pom_dependency = it.next();
            it.remove();

            var dependency = convertPomDependency(pom_dependency);
            if (!result.contains(dependency)) {
                result.add(dependency);

                exclusions.push(pom_dependency.exclusions());
                new DependencyResolver(repositories_, dependency).getTransitiveDependencies(result, pomDependencies, exclusions, scopes);
                exclusions.pop();
            }
        }
    }

    public List<VersionNumber> listVersions() {
        return getMavenMetadata().getVersions();
    }

    public VersionNumber latestVersion() {
        return getMavenMetadata().getLatest();
    }

    public VersionNumber releaseVersion() {
        return getMavenMetadata().getRelease();
    }

    List<String> getArtifactUrls() {
        return repositories_.stream().map(repository -> repository.getArtifactUrl(dependency_)).toList();
    }

    List<String> getMetadataUrls() {
        return getArtifactUrls().stream().map(s -> s + MAVEN_METADATA_XML).toList();
    }

    Xml2MavenMetadata getMavenMetadata() {
        if (metadata_ == null) {
            var urls = getMetadataUrls();
            metadata_ = parseMavenMetadata(urls);
        }

        return metadata_;
    }

    List<String> getSnapshotMetadataUrls() {
        var version = resolveVersion();
        return getArtifactUrls().stream().map(s -> s + version + "/" + MAVEN_METADATA_XML).toList();
    }

    Xml2MavenMetadata getSnapshotMavenMetadata() {
        if (snapshotMetadata_ == null) {
            var urls = getSnapshotMetadataUrls();
            snapshotMetadata_ = parseMavenMetadata(urls);
        }

        return snapshotMetadata_;
    }

    Xml2MavenMetadata parseMavenMetadata(List<String> urls) {
        String retrieved_url = null;
        String metadata = null;
        for (var url : urls) {
            try {
                var content = FileUtils.readString(new URL(url));
                if (content == null) {
                    throw new ArtifactNotFoundException(dependency_, url);
                }

                retrieved_url = url;
                metadata = content;

                break;
            } catch (IOException e) {
                throw new ArtifactRetrievalErrorException(dependency_, url, e);
            } catch (FileUtilsErrorException e) {
                if (e.getCause() instanceof FileNotFoundException) {
                    continue;
                }
                throw new ArtifactRetrievalErrorException(dependency_, url, e);
            }
        }

        if (metadata == null) {
            throw new ArtifactNotFoundException(dependency_, StringUtils.join(urls, ", "));
        }

        var xml = new Xml2MavenMetadata();
        if (!xml.processXml(metadata)) {
            throw new DependencyXmlParsingErrorException(dependency_, retrieved_url, xml.getErrors());
        }

        return xml;
    }

    List<String> getPomUrls() {
        final var version = resolveVersion();
        final VersionNumber pom_version;
        if (version.qualifier().equals("SNAPSHOT")) {
            var metadata = getSnapshotMavenMetadata();
            pom_version = metadata.getSnapshot();
        } else {
            pom_version = version;
        }

        return getArtifactUrls().stream().map(s -> s + version + "/" + dependency_.artifactId() + "-" + pom_version + ".pom").toList();
    }

    Xml2MavenPom getMavenPom() {
        String retrieved_url = null;
        String pom = null;
        var urls = getPomUrls();
        for (var url : urls) {
            try {
                var content = FileUtils.readString(new URL(url));
                if (content == null) {
                    throw new ArtifactNotFoundException(dependency_, url);
                }

                retrieved_url = url;
                pom = content;

                break;
            } catch (IOException e) {
                throw new ArtifactRetrievalErrorException(dependency_, url, e);
            } catch (FileUtilsErrorException e) {
                if (e.getCause() instanceof FileNotFoundException) {
                    continue;
                }
                throw new ArtifactRetrievalErrorException(dependency_, url, e);
            }
        }

        if (pom == null) {
            throw new ArtifactNotFoundException(dependency_, StringUtils.join(urls, ", "));
        }

        var xml = new Xml2MavenPom(repositories_);
        if (!xml.processXml(pom)) {
            throw new DependencyXmlParsingErrorException(dependency_, retrieved_url, xml.getErrors());
        }

        return xml;
    }

    List<String> getDownloadUrls() {
        final var version = resolveVersion();
        final VersionNumber pom_version;
        if (version.qualifier().equals("SNAPSHOT")) {
            var metadata = getSnapshotMavenMetadata();
            pom_version = metadata.getSnapshot();
        } else {
            pom_version = version;
        }

        return getArtifactUrls().stream().map(s -> {
            var result = new StringBuilder(s);
            result.append(version).append("/").append(dependency_.artifactId()).append("-").append(pom_version);
            if (!dependency_.classifier().isEmpty()) {
                result.append("-").append(dependency_.classifier());
            }
            var type = dependency_.type();
            if (type == null) {
                type = "jar";
            }
            result.append(".").append(type);
            return result.toString();
        }).toList();
    }

    public void downloadIntoFolder(File file)
    throws DependencyDownloadException {
        if (file == null) throw new IllegalArgumentException("file can't be null");
        if (!file.exists()) throw new IllegalArgumentException("file '" + file + "' doesn't exit");
        if (!file.canWrite()) throw new IllegalArgumentException("file '" + file + "' can't be written to");
        if (!file.isDirectory()) throw new IllegalArgumentException("file '" + file + "' is not a directory");

        String retrieved_url = null;
        var urls = getDownloadUrls();
        for (var download_url : urls) {
            var download_filename = download_url.substring(download_url.lastIndexOf("/") + 1);
            var download_file = new File(file, download_filename);
            try {
                System.out.print("Downloading: " + download_url + " ... ");
                System.out.flush();
                var url = new URL(download_url);
                var readableByteChannel = Channels.newChannel(url.openStream());
                try (var fileOutputStream = new FileOutputStream(download_file)) {
                    var fileChannel = fileOutputStream.getChannel();
                    fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);

                    retrieved_url = download_url;
                    System.out.print("done");
                    break;
                }
            } catch (FileNotFoundException e) {
                System.out.print("not found");
                continue;
            } catch (IOException e) {
                throw new DependencyDownloadException(dependency_, download_url, download_file, e);
            } finally {
                System.out.println();
            }
        }

        if (retrieved_url == null) {
            throw new ArtifactNotFoundException(dependency_, StringUtils.join(urls, ","));
        }
    }

    public void downloadTransitivelyIntoFolder(File file, Scope... scopes) {
        downloadIntoFolder(file);
        for (var dep : getTransitiveDependencies(scopes)) {
            new DependencyResolver(repositories_, dep).downloadIntoFolder(file);
        }
    }
}
