package com.atlassian.stash.internal.plugin;

import com.atlassian.stash.internal.plugin.scan.PluginPathScanner;
import com.atlassian.stash.internal.plugin.scan.PluginPathScannerFactory;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Sets.newHashSet;
import static com.google.common.collect.Sets.union;

@Component
public class WebResourceGenerationHelper {

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

    private static final Pattern IE_ONLY = Pattern.compile(".*-ie(\\d*)\\.(css|js)");
    private static final String SVG_FILE_EXTENSION = ".svg";
    private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
    private static final Map<String, Collection<String>> RESOURCE_TRANSFORMS = ImmutableMap.of(
            "js", ImmutableList.of(
                    "stashJsHelp", // Adds help paths to client side js
                    "jsI18n" // cross-product i18n
            ),
            "soy", ImmutableList.of(
                    "soyTransformer",
                    // TODO Are the below transformers actually needed? The soy transformer should handle everything
                    "stashJsHelp", // Adds help paths to client side soy
                    "jsI18n" // cross-product i18n
            ),
            "less", ImmutableList.of(
                    "less-transformer"
            )
    );

    private final PluginPathScannerFactory pluginPathScannerFactory;

    @Autowired
    public WebResourceGenerationHelper(PluginPathScannerFactory pluginPathScannerFactory) {
        this.pluginPathScannerFactory = pluginPathScannerFactory;
    }

    public void addDefaultTransforms(Element container) {
        for (Map.Entry<String, Collection<String>> entry : RESOURCE_TRANSFORMS.entrySet()) {
            addTransforms(container, entry.getKey(), entry.getValue());
        }
    }

    public void addResourceElements(PreprocessedWebResourceModuleDescriptor moduleDescriptor) {
        Set<String> predefinedResources = getPredefinedResources(moduleDescriptor.getElement());

        @SuppressWarnings("unchecked")
        List<Element> directories = moduleDescriptor.getOriginalElement().elements("directory");
        for (Element directory : directories) {
            addResourceElementsForDirectory(moduleDescriptor, directory, predefinedResources);
        }
    }

    /**
     * Adds transforms to the module descriptor. For example
     * <transformation extension="js">
     *     <transformer key="stashJsHelp"/>
     *     <transformer key="jsI18n"/>
     * </transformation>
     */
    private void addTransforms(Element container, String fileExtension, Iterable<String> transformKeys) {
        Element e = container.addElement("transformation");
        e.addAttribute("extension", fileExtension);
        for (String key : transformKeys) {
            Element t = e.addElement("transformer");
            t.addAttribute("key", key);
        }
    }

    private String changeFileExtension(String path, String oldExtension, String newExtension) {
        if (path.endsWith(oldExtension)) {
            return path.substring(0, path.length() - oldExtension.length()) + newExtension;
        }
        return path;
    }

    private String getNameFromLocation(String location, String directoryToScan) {
        String name = location.substring(directoryToScan.length());
        name = changeFileExtension(name, ".less", ".css");
        name = changeFileExtension(name, ".soy", ".soy.js");
        return name;
    }

    private Set<String> getPredefinedResources(Element resourceContainer) {
        Set<String> predefinedResources = newHashSet();
        for (Object resourceObj : resourceContainer.elements("resource")) {
            Element resource = (Element) resourceObj;
            if ("download".equalsIgnoreCase(resource.attributeValue("type"))) {
                String location = resource.attributeValue("location");
                if (StringUtils.isNotBlank(location)) {
                    predefinedResources.add(location);
                }
            }
        }
        return predefinedResources;
    }

    private Set<String> getExcludedPatterns(Element directoryElement) {
        return union(newHashSet("/**/*-min*.*", "/**/*.min*.*", "/**/*.less.css", "/**/*-less.css"), getPatterns(directoryElement, "exclude"));
    }

    private Set<String> getIncludedPatterns(Element directoryElement) {
        return getPatterns(directoryElement, "include");
    }

    private Set<String> getPatterns(Element directoryElement, String attributeName) {
        Set<String> excludes = newHashSet();
        for (Object excludesObj : directoryElement.elements(attributeName)) {
            String exclude = ((Element) excludesObj).getText();
            if (StringUtils.isNotBlank(exclude)) {
                Collections.addAll(excludes, exclude.split(","));
            }
        }
        return excludes;
    }

    private void addResourceElementsForDirectory(PreprocessedWebResourceModuleDescriptor moduleDescriptor, Element directory, Set<String> predefinedResources) {
        Set<String> excludedPatterns = getExcludedPatterns(directory);
        Set<String> includedPatterns = getIncludedPatterns(directory);

        String directoryToScan = normalizedDirectory(directory.attributeValue("location"));
        boolean isWebContextStatic = "webContextStatic".equalsIgnoreCase(directory.attributeValue("source"));
        PluginPathScanner scanner = pluginPathScannerFactory.forPlugin(moduleDescriptor.getPlugin(), isWebContextStatic);

        if (StringUtils.isNotBlank(directoryToScan)) {
            for (String location : scanner.getPaths(directoryToScan)) {
                if (predefinedResources.contains(location)) {
                    log.debug("Web resource '{}:{}' explicitly defined, skipping", moduleDescriptor.getCompleteKey(), location);
                    continue;
                }

                // if excluded or not included
                if (matchesPath(excludedPatterns, location) ||
                        !(includedPatterns.isEmpty() || matchesPath(includedPatterns, location))) {
                    continue;
                }

                addResourceElement(moduleDescriptor.getElement(), directoryToScan, isWebContextStatic, location);
            }
        } else {
            log.info("<directory> element 'location' attribute not present for module '{}', skipping automatic resource generation", moduleDescriptor.getCompleteKey());
        }
    }

    private boolean matchesPath(Set<String> patterns, String location) {
        boolean matched = false;
        for (String pattern : patterns) {
            if (PATH_MATCHER.match(pattern, location)) {
                matched = true;
                break;
            }
        }
        return matched;
    }

    private void addResourceElement(Element resourceContainer, String directoryToScan, boolean webContextStatic, String location) {
        String name = getNameFromLocation(location, directoryToScan);

        Element e = resourceContainer.addElement("resource");
        e.addAttribute("type", "download");
        e.addAttribute("name", name);
        e.addAttribute("location", location);

        if (webContextStatic) {
            Element param = e.addElement("param");
            param.addAttribute("name", "source");
            param.addAttribute("value", "webContextStatic");
        }
        Matcher ieOnlyMatcher = IE_ONLY.matcher(name);
        if (ieOnlyMatcher.matches()) {
            Element param = e.addElement("param");
            param.addAttribute("name", "conditionalComment");
            param.addAttribute(
                    "value",
                    StringUtils.isNotEmpty(ieOnlyMatcher.group(1)) ?
                            "lte IE " + ieOnlyMatcher.group(1) :
                            "IE"
            );
        }
        if (name.endsWith(SVG_FILE_EXTENSION)) {
            Element param = e.addElement("param");
            param.addAttribute("name", "content-type");
            param.addAttribute("value", "image/svg+xml");
        }
    }

    private String normalizedDirectory(String location) {
        if (!Strings.isNullOrEmpty(location)) {
            location = location.endsWith("/") ? location : location + "/";
            location = location.startsWith("/") ? location : "/" + location;
        }
        return location;
    }
}
