package com.atlassian.stash.internal.plugin.scan;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ResourceUtils;

import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class ClassLoaderPluginPathScanner implements PluginPathScanner {

    private static final Logger log = LoggerFactory.getLogger(ClassLoaderPluginPathScanner.class);

    private final ClassLoader classLoader;

    public ClassLoaderPluginPathScanner(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public List<String> getPaths(String prefix) {
        try {
            // We have to use "classpath*" here to find _all_ the files for i18n/ in our jars
            // Otherwise they will each need to be in differently named folders, which is fine too
            Resource[] resources = new PathMatchingResourcePatternResolver(classLoader)
                    .getResources("classpath*:" + StringUtils.appendIfMissing(prefix, "/") +  "**");
            List<String> paths = new ArrayList<>(resources.length);
            for (Resource resource : resources) {
                String path = getPathFromUri(resource);
                if (path == null) {
                    log.warn("Ignoring {}; its URI is invalid", resource);
                    continue;
                }

                // To handle scanning root directory
                int i = prefix.length() == 1 ? path.lastIndexOf('/') : path.indexOf(prefix);
                paths.add(path.substring(i));
            }
            return paths;
        } catch (IOException e) {
            log.error("Error scanning classpath {} in {}", classLoader, prefix, e);
            return new ArrayList<>();
        }
    }

    /**
     * Uses {@code Resource.getURI()} to get a path to the resource.
     *
     * @param resource the resource whose URI should be used to extract a path
     * @return the URI-derived path, or {@code null} if a path can't be extracted because both the URI and the
     *         URL are syntactically invalid
     * @throws IOException if the resource's URI can't be retrieved
     */
    @Nullable
    private String getPathFromUri(Resource resource) throws IOException {
        try {
            return resource.getURI().toString();
        } catch (NestedIOException e) {
            if (e.getCause() instanceof URISyntaxException) {
                try {
                    return maybeGetPathFromUrl(resource);
                } catch (IOException | URISyntaxException ex) {
                    // Prefer the original exception, if extracting a path from the URL fails
                    e.addSuppressed(ex);
                }
            }

            throw e;
        }
    }

    /**
     * On Windows, this method will normalize slashes and, if that changes the URL, it will try to convert that to a
     * URI to get a path. If normalizing slashes doesn't change the URL, or on any other operating system, this just
     * returns {@code null}.
     * <p>
     * Exactly how this should be handled is debatable. It appears this issue may only affect Windows. Some resource
     * URLs have paths like "jar:file:C:\Development\apache-tomcat-8.0.35\bin\tomcat-juli.jar!/META-INF/", and the
     * backslashes cause {@code Resource.getURI()} to fail. This is a best-effort to translate around that issue.
     *
     * @param resource the resource whose URL should be used to potentially extract a path
     * @return the URL-derived path, or {@code null} if normalizing the URL doesn't produce a different value, or
     *         on operating systems other than Windows
     * @throws IOException        if the resource's URL can't be retrieved
     * @throws URISyntaxException if the normalized URL cannot be converted to a URI
     */
    @Nullable
    private String maybeGetPathFromUrl(Resource resource) throws IOException, URISyntaxException {
        // If we're on Windows, try normalizing backslashes to forward slashes. This is skipped on Unix-like
        // operating systems because they deal in forward slashes natively
        if (SystemUtils.IS_OS_WINDOWS) {
            URL url = resource.getURL();
            String original = url.toString();
            String normalized = original.replace('\\', '/');

            // Reference equality intentional; String.replace returns the same _instance_ if no replacements
            // are made (that's part of its contract), so there's no need to check value equality. If replace
            // returns a different String instance, it made changes.
            // noinspection StringEquality
            if (normalized != original) {
                // If normalization made any changes, try to convert the normalized value to a URI and, if
                // that succeeds, return its value
                return ResourceUtils.toURI(normalized).toString();
            }
        }

        // If we make it here, it means we either skipped trying to use the URL (Unix-like OSes) or
        // normalizing slashes produced an identical URL (which means URI creation would still fail)
        return null;
    }
}
