/*
 * 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.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

// XXX should use DOM, not text manipulation

/**
 * Moves classpath from properties to project.xml
 * @author pzajac
 */
public class FixTestDependencies extends Task {
    /**  entries for unit testing in order to avoid scanning modules
     */
    Set<ModuleListParser.Entry> cachedEntries ;
    
    /** Creates a new instance of FixTestClassPath */
    public FixTestDependencies() {
    }
    
    
    String cnb;
    
    File projectXmlFile;
    public void setProjectXml(File projectXml) {
        projectXmlFile = projectXml;
        
    }
    File propertiesFile;
    public void setPropertiesFile(File properties) {
        this.propertiesFile = properties;
    }
    public void execute() throws BuildException {
        // read project xml
        try {
            // test mode doesn't override project.xml
            boolean testFix = getProject().getProperty("test.fix.dependencies") != null;
            if (projectXmlFile == null || !projectXmlFile.isFile()) {
                throw new BuildException("project.xml file doesn't exist.");
            }
            byte xmlBytes [] = new byte[(int)projectXmlFile.length()];
            try (FileInputStream prjFis = new FileInputStream(projectXmlFile)) {
                prjFis.read(xmlBytes);
            }
            String xml = new String(xmlBytes);
            String oldXsd = "<data xmlns=\"http://www.netbeans.org/ns/nb-module-project/2";
            int xsdIndex = xml.indexOf(oldXsd);
            if (xsdIndex == -1) {
                xsdIndex = xml.indexOf("<data xmlns=\"http://www.netbeans.org/ns/nb-module-project/3");
            }
            if (xsdIndex != -1 || testFix) {
                // increase schema version
                String part1 = xml.substring(0,xsdIndex + oldXsd.length() - 1);
                String part2 = xml.substring(xsdIndex + oldXsd.length(), xml.length());
                xml = part1 + "3" + part2;
                
                ModuleType projectType = ModuleType.NB_ORG;
                if (xml.contains("<suite-component/>")) {
                    projectType = ModuleType.SUITE;
                } else if (xml.contains("<standalone/>")) {
                    projectType = ModuleType.STANDALONE;
                } 
                //grrr
                int typeStart = xml.indexOf("<code-name-base>");
                int typeEnd = xml.indexOf("</code-name-base>");
                if (typeStart <= 0 || typeEnd <= 0 || typeEnd <= typeStart) {
                    throw new BuildException("Parsing of project.xml failed.");
                }
                cnb = xml.substring(typeStart + "<code-name-base>".length(), typeEnd).trim();
                if (cnb.length() <= 0) {
                    throw new BuildException("Invalid codename base:" + cnb);
                }
                final boolean td = xml.contains("<test-dependencies>");
                // test if project.xml contains test-deps
                if (td && !testFix) {
                    // yes -> exit
                    xml = fixOpenideUtil(fixCoreStartup(xml));
                    try (PrintStream ps = new PrintStream(projectXmlFile)) {
                        ps.print(xml);
                    }                  
                    return ;
                }
                Set<ModuleListParser.Entry> entries = getModuleList(projectType);
                Set<String> allCnbs = getCNBsFromEntries(entries);
                // read properties

                // remove modules and test from properties and put it to project.xml

                // unittest
                //
                Set<String> compileCNB = new TreeSet<>();
                Set<String> compileTestCNB = new TreeSet<>();
                Set<String> runtimeCNB = new TreeSet<>();
                Set<String> runtimeTestCNB = new TreeSet<>();

                Properties projectProperties = getTestProperties();
                boolean found;
                found = readCodeNameBases(compileCNB,compileTestCNB,projectProperties,"test.unit.cp",allCnbs,entries);
                found |= readCodeNameBases(compileCNB,compileTestCNB,projectProperties,"test.unit.cp.extra",allCnbs,entries);
                found |= readCodeNameBases(runtimeCNB,runtimeTestCNB,projectProperties,"test.unit.run.cp",allCnbs,entries);
                found |= readCodeNameBases(runtimeCNB,runtimeTestCNB,projectProperties,"test.unit.run.cp.extra",allCnbs,entries);

                if (!found && !td) {
                    return;
                }

                updateProperties(projectProperties,new String[]{"test.unit.cp","test.unit.cp.extra","test.unit.run.cp","test.unit.run.cp.extra"});

                StringWriter writer = new StringWriter();
                PrintWriter buffer = new PrintWriter(writer);
                buffer.println("");
                buffer.println("          <test-dependencies>");
                buffer.println("              <test-type>");
                buffer.println("                  <name>unit</name>");
                addDependency(buffer,cnb,true,true,false);

                runtimeCNB.removeAll(compileCNB);
                //compileCNB.removeAll(runtimeCNB);
                compileCNB.addAll(compileTestCNB);
                runtimeTestCNB.removeAll(compileTestCNB);
                runtimeCNB.addAll(runtimeTestCNB);
                addDependencies(buffer,compileCNB,compileTestCNB,true,false);
                addDependencies(buffer,runtimeCNB,runtimeTestCNB,false,false);
                buffer.println("              </test-type>");

                // qa functional tests
                compileCNB.clear();
                runtimeCNB.clear();
                compileTestCNB.clear();
                runtimeTestCNB.clear();

                readCodeNameBases(compileCNB,compileTestCNB,projectProperties,"test.qa-functional.cp",allCnbs,entries);
                readCodeNameBases(compileCNB,compileTestCNB,projectProperties,"test.qa-functional.cp.extra",allCnbs,entries);

                readCodeNameBases(runtimeCNB,runtimeTestCNB,projectProperties,"test.qa-functional.runtime.cp",allCnbs,entries);
                readCodeNameBases(runtimeCNB,runtimeTestCNB,projectProperties,"test.qa-functional.runtime.extra",allCnbs,entries);
                if (!compileTestCNB.isEmpty() || !compileCNB.isEmpty() || !runtimeTestCNB.isEmpty() || !runtimeCNB.isEmpty()) {
                    buffer.println("              <test-type>");
                    buffer.println("                  <name>qa-functional</name>");

                    addDependencies(buffer,compileCNB,compileTestCNB,true,false);
                    addDependencies(buffer,runtimeCNB,runtimeTestCNB,false,false);
                    buffer.println("              </test-type>");
                }
                
                buffer.println("            </test-dependencies>");
                updateProperties(projectProperties,new String[]{"test.qa-functional.cp","test.qa-functional.cp","test.qa-functional.runtime.cp","test.qa-functional.runtime.extra"});

                // merge project properties
                String MODULE_DEP_END = "</module-dependencies>";
                int moduleDepEnd = xml.indexOf(MODULE_DEP_END);
                if (moduleDepEnd == -1) {
                    MODULE_DEP_END = "<module-dependencies/>";
                    moduleDepEnd = xml.indexOf(MODULE_DEP_END);
                }
                if (moduleDepEnd == -1) {
                    throw new BuildException("No module dependency found.");
                }
                moduleDepEnd += MODULE_DEP_END.length();
                StringBuffer resultXml = new StringBuffer();
                resultXml.append(xml.substring(0,moduleDepEnd));
                resultXml.append(writer.toString());
                // windows
                if (xml.charAt(moduleDepEnd) == '\r') {
                    moduleDepEnd++;
                }
                resultXml.append(xml.substring(moduleDepEnd + 1, xml.length()));
                if (!testFix) {
                    try (PrintStream ps = new PrintStream(projectXmlFile)) {
                        ps.print(fixOpenideUtil(
                                fixCoreStartup(
                                        resultXml.toString()
                                )));
                    }
                } else {
                    System.out.println(resultXml);
                }
            }
            
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
        
        // store project.properties and project.xml
    }

    private Set<ModuleListParser.Entry> getModuleList(final ModuleType projectType) throws IOException {
        if (cachedEntries == null ) {
          // scan for all modules
            ModuleListParser listParser = new ModuleListParser(getProject().getProperties(), projectType, getProject());
            return  listParser.findAll();
        } else {
            // used by FixTestDependenciesTest
            return cachedEntries;
        }
    }
    
    private Set<String> getCNBsFromEntries(Set<ModuleListParser.Entry> entries) {
        Set<String> cnbs = new HashSet<>();
        for (ModuleListParser.Entry e : entries) {
            cnbs.add(e.getCnb());
        }
        return cnbs;
    }
    /** parses all codenamebases from path
     */
     boolean readCodeNameBases(Set<String> compileCNB,
            Set <String> testsCNB,
            Properties projectPropertis,
            String property,
            Set <String> allCnbs,
            Set <ModuleListParser.Entry> entries) {
        String prop = projectPropertis.getProperty(property);
        StringBuffer newProp = new StringBuffer();
        if (prop !=   null) {
            StringTokenizer tokenizer = new StringTokenizer(prop,";:\\");
            while(tokenizer.hasMoreTokens()) {
                String token = tokenizer.nextToken().trim();
                boolean found = false;
                if (token.length() > 1) {
                    int lastSlash = token.lastIndexOf("/");
                    int lastDot = token.lastIndexOf(".");
                    // check if the file is module
                    //
                    // the boot.jar is org.netbeans.bootstrap
                    if (token.endsWith("lib/boot.jar")) {
                         compileCNB.add("org.netbeans.bootstrap");
                         found = true;
                    } else if (token.endsWith("core/core.jar")) {
                        compileCNB.add("org.netbeans.core.startup");
                        compileCNB.add("org.netbeans.core.startup.base");
                        found = true;
                    } else if (lastSlash != -1 && lastDot != -1 && lastSlash + 1< lastDot ) {
                        String codeBaseName = token.substring(lastSlash + 1, lastDot);
                        codeBaseName = codeBaseName.replace('-','.');
                        if (allCnbs.contains(codeBaseName)) {
                            compileCNB.add(codeBaseName);
                            found = true;
                        } else  {
                            String name = token.substring(lastSlash + 1, token.length());
                            // check if the file is wrapped library
                            String wrapCNB = null;
                            for (ModuleListParser.Entry entry : entries) {
                                  File extensions [] = entry.getClassPathExtensions();
                                  if (extensions != null) {
                                      for (File f : extensions) {
                                          if (f.getPath().endsWith( name)) {
                                              if (wrapCNB != null) {
                                                // collision
                                                  found = false;
                                                  System.out.println("wrapped? " + entry.getCnb() + " -> " + token + " = " + f);
                                              } else {
                                                  wrapCNB = entry.getCnb();
                                                  found = true;
                                              }
                                          }
                                      }    
                                  }

                            }
                            if (found && wrapCNB != null && allCnbs.contains(wrapCNB)) {
                                  compileCNB.add(wrapCNB);
                            }
                         }
                        // check if the dependency is dependency on test
                    } else {
                        int prjEnd = token.indexOf("/build/test/unit/class");
                        if (prjEnd != -1) {
                            // get the project folder
                            int firstSlash = token.indexOf("/");
                            if (firstSlash != -1 && firstSlash + 1 < prjEnd) {
                                String  prjFolder = token.substring(firstSlash + 1, prjEnd);
                                String  codebaseName = getCNBForFolder(prjFolder,entries);
                                if (codebaseName == null) {
                                    log("No code name base found for file " + token);
                                } else {
                                    testsCNB.add(codebaseName);
                                    found = true;
                                }
                            }
                        }
                    }
                    // check if the file is file
                    //
                    if (found == false) {
                        if (newProp.length() > 0) {
                            newProp.append(":");
                        }
                        // windows platform
                        token = token.replace(File.separatorChar,'/');
                        newProp.append(token);
                    }
                }
            } // while
            projectPropertis.setProperty(property,newProp.toString());
            return true;
        }
        return false;
    }
    
    private void addDependencies(PrintWriter buffer, Set<String> moduleCNB, Set<String> testCNB, boolean compile, boolean recursive) {
        for (String cnb : moduleCNB) {
            addDependency(buffer,cnb,compile,recursive,testCNB.contains(cnb));
        }
    }
    
    private void addDependency(PrintWriter buffer, String cnb, boolean compile, boolean recursive, boolean test) {
        buffer.println("                  <test-dependency>");
        buffer.println("                      <code-name-base>" + cnb + "</code-name-base>");
        if (recursive) {
            buffer.println("                      <recursive/>");
        }
        if (compile) {
            buffer.println("                      <compile-dependency/>");
        }
        if (test) {
            buffer.println("                      <test/>");
        }
        buffer.println("                  </test-dependency>");
        
    }
    
    private Properties getTestProperties() throws IOException {
        if (propertiesFile == null || !propertiesFile.isFile()) {
            return new Properties();
        }
        Properties props = new Properties();
        try (FileInputStream fis = new FileInputStream(propertiesFile)) {
            props.load(fis);
        }
        return props;
    }
    
    /** @return codeNameBase of project for relative folder prjFolder
     */
    private String getCNBForFolder(String prjFolder, Set<ModuleListParser.Entry> entries) {
        for (ModuleListParser.Entry elem : entries) {
            if (prjFolder.equals(elem.getNetbeansOrgPath())) {
                return elem.getCnb();
            }
        }
        return null;
    }
    private void updateProperties(Properties projectProperties,String names[]) {
        if (propertiesFile == null) {
            return;
        }
        try {
            List<String> lines = new ArrayList<>();
            if (propertiesFile.isFile()) {
                // read properties
                try (BufferedReader reader = new BufferedReader (new FileReader(propertiesFile))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        lines.add(line);
                    }
                }
            }
            
            // merge properties
            for (String propName : names) {
               String value = projectProperties.getProperty(propName);
               lines = replaceProperty(propName,value,lines);    
            }
            if (lines.isEmpty() && !propertiesFile.isFile()) {
                return;
            }

            // store properties
            try (PrintStream ps = new PrintStream(propertiesFile)) {
                for (String l : lines) {
                    ps.println(l);
                }
            }
            
        } catch (IOException ioe) {
            throw new BuildException(ioe);
        }
        
    }

    private List<String> replaceProperty(String name, String value, List<String> lines) {
        List<String> retLines = new ArrayList<>();
        for (int i = 0 ; i < lines.size() ; i++) {
            String line = lines.get(i);
            String trimmedLine = line.trim();
            int eqIdx = trimmedLine.indexOf("=");
            if (eqIdx != -1) {
                String pName = line.substring(0,eqIdx).trim();
                if (pName.equals(name)) {
                    // skip property
                    for (; i < lines.size() && lines.get(i).trim().endsWith("\\") ; i++) ;
                    // append new property 
                    if (value != null && !value.trim().equals("")) {
                        retLines.add(name + "=" + value);
                    }
                    continue;
                }
            }
            // either empty line, comment or other property
            retLines.add(line);
        }
        return retLines;
    }
    
    private String fixCoreStartup(String xml) {
        Pattern l = Pattern.compile(
                "^ *<test-dependency>[^/]*" +
                "<code-name-base>org.netbeans.core.startup.base</code-name-base>", Pattern.MULTILINE);
        if (l.matcher(xml).find()) {
            return xml;
        }

        Pattern p = Pattern.compile(
                "^ *<test-dependency>[^/]*" +
                "<code-name-base>org.netbeans.core.startup</code-name-base>", Pattern.MULTILINE);

        Matcher m = p.matcher(xml);
        if (m.find()) {
            final String txt = "</test-dependency>";
            final int s = m.start();
            int end = xml.indexOf(txt, s);
            if (end == -1) {
                throw new BuildException("No end of dependency " + xml);
            }
            final int e = end + txt.length();
            String dep = xml.substring(s, e);
            return xml.substring(0, s) + dep + '\n' + dep.replace("org.netbeans.core.startup", "org.netbeans.core.startup.base") + xml.substring(e);
        }
        return xml;
    }

    private String fixOpenideUtil(String xml) {
        Pattern l = Pattern.compile(
                "^ *<test-dependency>[^/]*" +
                "<code-name-base>org.openide.util.lookup</code-name-base>", Pattern.MULTILINE);
        if (l.matcher(xml).find()) {
            return xml;
        }

        Pattern p = Pattern.compile(
                "^ *<test-dependency>[^/]*" +
                "<code-name-base>org.openide.util</code-name-base>", Pattern.MULTILINE);

        Matcher m = p.matcher(xml);
        if (m.find()) {
            final String txt = "</test-dependency>";
            final int s = m.start();
            int end = xml.indexOf(txt, s);
            if (end == -1) {
                throw new BuildException("No end of dependency " + xml);
            }
            final int e = end + txt.length();
            String dep = xml.substring(s, e);
            return xml.substring(0, s) + dep + '\n' + dep.replace("org.openide.util", "org.openide.util.lookup") + xml.substring(e);
        }
        return xml;
    }

}
