/*
 * 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.netbeans.nbbuild;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Build JUnit report from Apache Ant Report XXX
 *
 * @author skygo
 */
public class RatReportTask extends Task {

    private File sourceFile;
    private File root;
    // not nice but to be last
    private static final String OTHERS_AREA = "not cluster";
    private static final String NOT_CLUSTER = "not under cluster";
    private File reportFile;

    /**
     * source file Apache Rat xml report
     *
     * @param sourceFile
     */
    public void setSource(File sourceFile) {
        this.sourceFile = sourceFile;
    }

    /**
     * Folder to put the rat reports in
     *
     * @param report
     */
    public void setReport(File report) {
        this.reportFile = report;
    }

    private boolean haltonfailure;
    /** JUnit-format XML result file to generate, rather than halting the build. */
    public void setHaltonfailure(boolean haltonfailure) {
        this.haltonfailure = haltonfailure;
    }

    @Override
    public void execute() throws BuildException {
        root = sourceFile.getParentFile().getParentFile().getParentFile();
        File[] modulesFolder = root.listFiles();
        String repository = "";
        // get repository information from git
        //try {
        List<String> commandAndArgs = new ArrayList<>();
        commandAndArgs.add("git");
        commandAndArgs.add("config");
        commandAndArgs.add("--get");
        commandAndArgs.add("remote.origin.url");
        Stream<String> allFailures = Stream.empty();
        try {
            Process p = new ProcessBuilder(commandAndArgs).directory(root).start();
            try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
                String line;
                while ((line = r.readLine()) != null) {
                    if (line.startsWith("http")) {
                        repository = line.replace(".git", "");
                    }
                }
            } catch (IOException ex) {
                throw new BuildException("Cannot evaluate git information", ex);
            }
        } catch (IOException ex) {
            throw new BuildException("git process issue", ex);
        }
        // map module and report
        Map<String, ModuleInfo> moduleRATInfo = new TreeMap<>();

        // build map to get cluster and module related from cluster.properties
        Set<String> moduleDB = new HashSet<>();
        Map<String, Set<String>> modulebycluster = new TreeMap<>();
        for (File module : modulesFolder) {
            if (module.isDirectory() && !module.isHidden()) {
                moduleDB.add(module.getName());
                moduleRATInfo.put(module.getName(), new ModuleInfo(module));
            }
        }
        Set<String> clusterList = new TreeSet<>();
        for (String key : getProject().getProperties().keySet()) {
            if (key.startsWith("nb.cluster.")) {
                String simplfiedKey = key.replaceAll("nb.cluster.", "");
                simplfiedKey = simplfiedKey.replaceAll(".dir", "");
                simplfiedKey = simplfiedKey.replaceAll(".depends", "");
                clusterList.add(simplfiedKey);
                modulebycluster.put(simplfiedKey, new HashSet<>());
            }
        }
        for (String clusterName : clusterList) {
            String property = getProject().getProperty("nb.cluster." + clusterName);
            String[] split = property.split(",");
            for (String amo : split) {
                moduleDB.remove(amo);
                modulebycluster.get(clusterName).add(amo);
            }
        }
        modulebycluster.put(OTHERS_AREA, new HashSet<>());
        for (String k : moduleDB) {
            modulebycluster.get(OTHERS_AREA).add(k);
        }
        // remaining module sorted in others
        modulebycluster.get(OTHERS_AREA).add(NOT_CLUSTER);
        clusterList.add(OTHERS_AREA);
        moduleRATInfo.put(NOT_CLUSTER, new ModuleInfo(root));
        //read XML
        try {
            DocumentBuilder dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            InputStream inputstream = new FileInputStream(sourceFile);
            InputStreamReader reader = new InputStreamReader(inputstream);
            InputSource inputSource = new InputSource(reader);
            Document doc = dBuilder.parse(inputSource); // open xml source
            XPathFactory xpf = XPathFactory.newInstance();

            XPath path = xpf.newXPath();
            Element rootElement = doc.getDocumentElement();

            doPopulateUnapproved(moduleRATInfo, rootElement, path);
            doPopulateApproved(moduleRATInfo, rootElement, path);

        } catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException ex) {
            throw new BuildException("Cannot parse Rat report ", ex);
        }

        for (String clusterName : clusterList) {
            // create a report file by cluster
            File file = new File(reportFile, clusterName + ".xml");
            if (!file.exists()) {
                try {
                    reportFile.mkdirs();
                    file.createNewFile();
                } catch (IOException ex) {
                    throw new BuildException("Impossible to create junit rat report for cluster " + clusterName, ex);
                }
            }
            Map<String, String> pseudoTests = new LinkedHashMap<>();
            for (String moduleName : modulebycluster.get(clusterName)) {
                ModuleInfo amoduleInfo = moduleRATInfo.get(moduleName);
                if (amoduleInfo != null) {
                    if (!amoduleInfo.getUnapproved().isEmpty()) {
                        pseudoTests.put(" module " + moduleName + " has " + amoduleInfo.getUnapproved().size() + " unapproved license(s)", "Unapproved license in " + amoduleInfo.getUnapproved().size() + " file(s) " + writeFiles(amoduleInfo.getUnapproved(), repository));
                    }
                    if (!amoduleInfo.getInvalidExternal().isEmpty()) {
                        pseudoTests.put(" module " + moduleName + " has " + amoduleInfo.getInvalidExternal().size() + " suspicious external binaries  file(s)", "List of file " + amoduleInfo.getInvalidExternal().size() + " file(s) " + writeFiles(amoduleInfo.getInvalidExternal(), null));
                    }
                } else {
                    // XXX if a moduleInfo is null folder is not present source base incomplete
                }

            }
            JUnitReportWriter.writeReport(this, "Cluster: " + clusterName, file, pseudoTests);
            allFailures = Stream.concat(allFailures, pseudoTests.values().stream().filter(err -> err != null));
        }
        if (haltonfailure) {
            String failuresString = allFailures.collect(Collectors.joining("\n"));
            if (!failuresString.isEmpty()) {
                throw new BuildException("Failed Rat test(s):\n" + failuresString,
                                         getLocation());
            }
        }
    }

    private static String writeFiles(Set<String> listFile, String repo) {
        StringBuilder sb = new StringBuilder();
        for (String fileentry : listFile) {
            sb.append("\n");
            if (repo != null) {
                sb.append(repo).append("/tree/master/");
            }
            sb.append(fileentry.replaceAll(" ", "%20"));
        }
        return sb.toString();
    }

    private void doPopulateUnapproved(Map<String, ModuleInfo> moduleRATInfo, Element rootElement, XPath path) throws XPathExpressionException {
        NodeList evaluate = (NodeList) path.evaluate("descendant::resource[license-approval/@name=\"false\"]", rootElement, XPathConstants.NODESET);
        for (int i = 0; i < evaluate.getLength(); i++) {
            String resources = relativize(evaluate.item(i).getAttributes().getNamedItem("name").getTextContent());
            String moduleName = getModuleName(resources);
            if (!moduleRATInfo.containsKey(moduleName)) {
                moduleRATInfo.get(NOT_CLUSTER).addUnapproved(resources);
            } else {
                moduleRATInfo.get(moduleName).addUnapproved(resources);
            }
        }
    }

    private void doPopulateApproved(Map<String, ModuleInfo> moduleRATInfo, Element rootElement, XPath path) throws XPathExpressionException {
        NodeList evaluate = (NodeList) path.evaluate("descendant::resource[license-approval/@name=\"true\"]", rootElement, XPathConstants.NODESET);
        for (int i = 0; i < evaluate.getLength(); i++) {
            String resources = relativize(evaluate.item(i).getAttributes().getNamedItem("name").getTextContent());
            String moduleName = getModuleName(resources);
            if (!moduleRATInfo.containsKey(moduleName)) {
                moduleRATInfo.get(NOT_CLUSTER).addApproved(resources);
            } else {
                moduleRATInfo.get(moduleName).addApproved(resources);
            }

        }
    }

    private String relativize(String target) {
        Path full = Paths.get(target);
        Path rootPath = root.toPath();
        return rootPath.relativize(full).toString();
    }

    private String getModuleName(String resource) {
        String moduleName;
        if (!resource.contains(File.separator)) {
            moduleName = NOT_CLUSTER;
        } else {
            moduleName = resource.substring(0, resource.indexOf(File.separator));
        }
        return moduleName;
    }

    class ModuleInfo {
// approved resource list
// very simple string for now.

        private final Set<String> approved = new HashSet<>();
        // unapproved resource list
        private final Set<String> unapproved = new HashSet<>();

        private final Set<String> external = new HashSet<>();
        private final File folder;

        private ModuleInfo(File moduleFolder) {
            this.folder = moduleFolder;
        }

        private File getFolder() {
            return folder;
        }

        private void addApproved(String resources) {
            approved.add(resources);
        }

        private void addInvalidExternal(String resources) {
            external.add(resources);
        }

        private void addUnapproved(String resources) {
            unapproved.add(resources);
        }

        private Set<String> getUnapproved() {
            return unapproved;
        }

        private Set<String> getInvalidExternal() {
            return external;
        }

    }
}
