/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Ant;
import org.openide.modules.Dependency;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Try to convert an existing NetBeans source module to a project.
 * @author Jesse Glick
 */
public class ConvertToNbmProjectTask extends Task {
    
    private static final String NS_PROJECT = "http://www.netbeans.org/ns/project/1";
    private static final String NS_NBMPROJECT = "http://www.netbeans.org/ns/nb-module-project/2";
    private static final String SPL_COMMENT = "                Sun Public License Notice\n\nThe contents of this file are subject to the Sun Public License\nVersion 1.0 (the \"License\"). You may not use this file except in\ncompliance with the License. A copy of the License is available at\nhttp://www.sun.com/\n\nThe Original Code is NetBeans. The Initial Developer of the Original\nCode is Sun Microsystems, Inc. Portions Copyright 1997-2005 Sun\nMicrosystems, Inc. All Rights Reserved.";
    private static final String TYPE = "org.netbeans.modules.apisupport.project";
    
    private File nbroot;
    public void setNbroot(File r) {
        nbroot = r;
    }
    
    private String path;
    public void setPath(String p) {
        path = p;
    }
    
    public void execute() throws BuildException {
        File dir = new File(nbroot, path.replace('/', File.separatorChar));
        if (!dir.exists()) {
            throw new BuildException("No such dir " + dir);
        }
        File nbprojectDir = new File(dir, "nbproject");
        if (nbprojectDir.exists()) {
            throw new BuildException("Some project already defined in " + dir);
        }
        File buildXml = new File(dir, "build.xml");
        if (!buildXml.isFile()) {
            throw new BuildException("No such " + buildXml);
        }
        Ant ant = (Ant)getProject().createTask("ant");
        ant.setDir(dir);
        ant.setTarget("clean");
        ant.setLocation(getLocation());
        ant.init();
        try {
            ant.execute();
        } catch (BuildException e) {
            // Probably not fatal.
            e.printStackTrace();
        }
        {
            // Make sure DOM serialization is functional before we go any further
            // and start touching files on disk.
            Document testDoc = XMLUtil.createDocument("foo", "urn:testns", null, null);
            OutputStream testOs = new ByteArrayOutputStream();
            try {
                XMLUtil.write(testDoc, testOs, "UTF-8");
            } catch (Exception e) {
                throw new BuildException(e);
            }
        }
        File manifest = new File(dir, "manifest.mf");
        if (!manifest.isFile()) {
            throw new BuildException("No such file " + manifest, getLocation());
        }
        String cnb;
        String moduleDeps;
        String openideDeps;
        String pkgs;
        String implVers;
        Set/*<String>*/ packagesWithClasses = new TreeSet();
        try {
            InputStream is = new FileInputStream(manifest);
            try {
                Manifest mani = new Manifest(is);
                Attributes attr = mani.getMainAttributes();
                String cnbr = attr.getValue("OpenIDE-Module");
                if (cnbr == null) {
                    throw new BuildException("No OpenIDE-Module found in " + manifest);
                }
                int idx = cnbr.lastIndexOf('/');
                if (idx == -1) {
                    cnb = cnbr;
                } else {
                    cnb = cnbr.substring(0, idx);
                }
                moduleDeps = attr.getValue("OpenIDE-Module-Module-Dependencies");
                openideDeps = attr.getValue("OpenIDE-Module-IDE-Dependencies");
                pkgs = attr.getValue("OpenIDE-Module-Public-Packages");
                implVers = attr.getValue("OpenIDE-Module-Implementation-Version");
            } finally {
                is.close();
            }
            File src = new File(dir, "src");
            scanForClasses(src, "", packagesWithClasses);
            log("Found packages with classes in them: " + packagesWithClasses, Project.MSG_VERBOSE);
        } catch (IOException e) {
            throw new BuildException(e);
        }
        DocumentBuilder db;
        try {
            db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new BuildException(e);
        }
        Map/*<String,String>*/ javadocProperties = new TreeMap();
        try {
            Document doc = db.parse(buildXml);
            NodeList nl = doc.getElementsByTagName("target");
            for (int i = 0; i < nl.getLength(); i++) {
                Element targetEl = (Element)nl.item(i);
                String name = targetEl.getAttribute("name");
                if (name.equals("javadoc")) {
                    // Try to look for defined properties too.
                    NodeList nl2 = targetEl.getElementsByTagName("ant");
                    if (nl2.getLength() == 1) {
                        Element antEl = (Element)nl2.item(0);
                        NodeList nl3 = antEl.getElementsByTagName("property");
                        for (int j = 0; j < nl3.getLength(); j++) {
                            Element propEl = (Element)nl3.item(j);
                            String propName = propEl.getAttribute("name");
                            if (propName.equals("javadoc.base") || propName.equals("javadoc.packages") ||
                                    propName.equals("javadoc.name") ||
                                    propName.equals("javadoc.classpath") || propName.equals("javadoc.manifest")) {
                                // projectized.xml defines these.
                                continue;
                            }
                            String value = propEl.getAttribute("value");
                            if (!value.equals("")) {
                                javadocProperties.put(propName, value);
                            } else {
                                String location = propEl.getAttribute("location");
                                if (!location.equals("")) {
                                    javadocProperties.put(propName, "${basedir}/" + location);
                                } else {
                                    log("Warning: do not grok <property> with name " + propName + "in javadoc target in " + buildXml, Project.MSG_WARN);
                                }
                            }
                        }
                    } else {
                        log("Warning: do not grok javadoc target in " + buildXml, Project.MSG_WARN);
                    }
                    break;
                }
            }
        } catch (IOException e) {
            throw new BuildException(e);
        } catch (SAXException e) {
            throw new BuildException(e);
        }
        Set/*<Dependency>*/ deps = new TreeSet(new DependencyComparator());
        if (moduleDeps != null) {
            deps.addAll(Dependency.create(Dependency.TYPE_MODULE, moduleDeps));
        }
        if (openideDeps != null) {
            deps.addAll(Dependency.create(Dependency.TYPE_MODULE, openideDeps.replaceFirst("IDE/", "org.openide/")));
        } else {
            // fallback - latest in effect at time of writing
            deps.addAll(Dependency.create(Dependency.TYPE_MODULE, "org.openide/1 > 4.23"));
        }
        Set/*<String>*/ packages = new TreeSet();
        if (pkgs == null) {
            log("Warning - no OpenIDE-Module-Public-Packages declaration found; so assuming only standard package names are public", Project.MSG_WARN);
        }
        if (pkgs == null || !pkgs.equals("-")) {
            String _pkgs;
            if (pkgs != null) {
                _pkgs = pkgs;
            } else {
                _pkgs = "org.netbeans.api.**, org.netbeans.spi.**, org.openide.**, org.openidex.**";
            }
            StringTokenizer tok = new StringTokenizer(_pkgs, ", ");
            while (tok.hasMoreTokens()) {
                String piece = tok.nextToken();
                if (piece.endsWith(".*")) { // NOI18N
                    String pkg = piece.substring(0, piece.length() - 2);
                    packages.add(pkg);
                } else if (piece.endsWith(".**")) { // NOI18N
                    String pkg = piece.substring(0, piece.length() - 3);
                    if (packagesWithClasses.contains(pkg)) {
                        packages.add(pkg);
                    }
                    String prefix = piece.substring(0, piece.length() - 2);
                    Iterator it = packagesWithClasses.iterator();
                    while (it.hasNext()) {
                        String knownPkg = (String)it.next();
                        if (knownPkg.startsWith(prefix)) {
                            packages.add(knownPkg);
                        }
                    }
                } else {
                    throw new BuildException("Illegal OpenIDE-Module-Public-Packages: " + pkgs);
                }
            }
        }
        Document doc = db.getDOMImplementation().createDocument(NS_PROJECT, "project", null);
        Element projectEl = doc.getDocumentElement();
        doc.insertBefore(doc.createComment("\n" + SPL_COMMENT + "\n"), projectEl);
        Element typeEl = doc.createElementNS(NS_PROJECT, "type");
        typeEl.appendChild(doc.createTextNode(TYPE));
        projectEl.appendChild(typeEl);
        Element configEl = doc.createElementNS(NS_PROJECT, "configuration");
        Element dataEl = doc.createElementNS(NS_NBMPROJECT, "data");
        Element nameEl = doc.createElementNS(NS_NBMPROJECT, "code-name-base");
        nameEl.appendChild(doc.createTextNode(cnb));
        dataEl.appendChild(nameEl);
        Element modDepsEl = doc.createElementNS(NS_NBMPROJECT, "module-dependencies");
        Iterator it = deps.iterator();
        while (it.hasNext()) {
            Dependency d = (Dependency)it.next();
            modDepsEl.appendChild(dependencyToXml(d, doc));
        }
        dataEl.appendChild(modDepsEl);
        Element pubPkgsEl = doc.createElementNS(NS_NBMPROJECT, "public-packages");
        it = packages.iterator();
        while (it.hasNext()) {
            Element packageEl = doc.createElementNS(NS_NBMPROJECT, "package");
            packageEl.appendChild(doc.createTextNode((String)it.next()));
            pubPkgsEl.appendChild(packageEl);
        }
        dataEl.appendChild(pubPkgsEl);
        configEl.appendChild(dataEl);
        projectEl.appendChild(configEl);
        nbprojectDir.mkdir();
        File projectXml = new File(nbprojectDir, "project.xml");
        try {
            OutputStream os = new FileOutputStream(projectXml);
            try {
                XMLUtil.write(doc, os, "UTF-8");
            } finally {
                os.close();
            }
        } catch (IOException e) {
            throw new BuildException(e);
        }
        // XXX would be nice to validate project.xml now against the schema...
        File projectProps = new File(nbprojectDir, "project.properties");
        try {
            OutputStream os = new FileOutputStream(projectProps);
            try {
                PrintWriter w = new PrintWriter(os);
                w.print("# ");
                w.println(SPL_COMMENT.replaceAll("\n", "\n# "));
                it = javadocProperties.entrySet().iterator();
                if (it.hasNext()) {
                    w.println();
                }
                while (it.hasNext()) {
                    Map.Entry entry = (Map.Entry)it.next();
                    String name = (String)entry.getKey();
                    String value = (String)entry.getValue();
                    w.println(name + "=" + value);
                }
                w.flush();
            } finally {
                os.close();
            }
        } catch (IOException e) {
            throw new BuildException(e);
        }
        log("All done; inspect " + projectXml + " and " + projectProps + " for accuracy.");
        log("(You may need to add is.autoload=true or is.eager=true as this is not currently autodetected.)");
        log("Many modules forgot to declare a dep on org.openide.loaders in their manifests - check if you need to add this to project.xml.");
        log("Add <class-path-extension>s to " + projectXml + " if there are extra JARs on the module's compilation classpath.");
        log("Add some <extra-compilation-unit>s to " + projectXml + " if you have something like libsrc or antsrc.");
        String dots = path.replaceAll("[^/]+", "..");
        log("Edit build.xml to contain just <import file=\"" + dots + "/nbbuild/templates/projectized.xml\"/> and maybe other stuff as needed.");
        log("You need to remove some entries from " + manifest + ":");
        if (moduleDeps != null) {
            log("  OpenIDE-Module-Module-Dependencies: " + moduleDeps);
        }
        if (openideDeps != null) {
            log("  OpenIDE-Module-IDE-Dependencies: " + openideDeps);
        }
        if (pkgs != null) {
            log("  OpenIDE-Module-Public-Packages: " + pkgs);
        }
        if (implVers != null) {
            log("  OpenIDE-Module-Implementation-Version: " + implVers);
        }
        log("Don't forget to update " + new File(dir, ".cvsignore") + ": no more 'manifest-subst.mf' (or 'netbeans' or 'Info'); add 'build'.");
        log("(Usually need just 'build'.)");
        if (new File(new File(new File(dir, "test"), "unit"), "src").isDirectory()) {
            log("Delete or projectize any test/build.xml and test/build-unit.xml and define test.unit.cp.extra and/or test.unit.run.cp.extra in " + projectProps + " as needed.");
        }
        if (new File(dir, "javahelp").exists()) {
            log("You appear to have some JavaHelp. This should be built automatically.");
        }
        log("(If any unprojectized modules build against this one, edit their build.xml to use ${" + path + ".dir}/modules/" + cnb.replace('.', '-') + ".jar instead of the current JAR path.)");
        // XXX some info about which of -Spec-Vers and -Impl-Vers to keep in manifest
        // XXX create nbproject/.cvsignore: "private"
        // XXX delete Class-Path
    }

    private static void scanForClasses(File dir, String prefix, Set/*<String>*/ pkgs) throws IOException {
        File[] kids = dir.listFiles();
        if (kids == null) {
            throw new IOException("Could not list " + dir);
        }
        for (int i = 0; i < kids.length; i++) {
            if (kids[i].isDirectory()) {
                scanForClasses(kids[i], prefix + kids[i].getName() + '/', pkgs);
            } else if (kids[i].getName().endsWith(".java") && prefix.length() > 0) {
                pkgs.add(prefix.substring(0, prefix.length() - 1).replace('/', '.'));
            }
        }
    }
    
    private static final class DependencyComparator implements Comparator {
        
        public int compare(Object o1, Object o2) {
            Dependency d1 = (Dependency)o1;
            Dependency d2 = (Dependency)o2;
            // Trim release version so org.openide/1 sorts before org.openide.loaders:
            return trimRelVers(d1.getName()).compareTo(trimRelVers(d2.getName()));
        }
        
        private static String trimRelVers(String s) {
            int i = s.indexOf('/');
            if (i != -1) {
                return s.substring(0, i);
            } else {
                return s;
            }
        }
        
    }
    
    private Element dependencyToXml(Dependency d, Document doc) throws BuildException {
        assert d.getType() == Dependency.TYPE_MODULE : d;
        String name = d.getName();
        int slash = name.indexOf('/');
        String cnb, relvers;
        if (slash != -1) {
            cnb = name.substring(0, slash);
            relvers = name.substring(slash + 1);
        } else {
            cnb = name;
            relvers = null;
        }
        Element depEl = doc.createElementNS(NS_NBMPROJECT, "dependency");
        Element cnbEl = doc.createElementNS(NS_NBMPROJECT, "code-name-base");
        cnbEl.appendChild(doc.createTextNode(cnb));
        depEl.appendChild(cnbEl);
        Element buildEl = doc.createElementNS(NS_NBMPROJECT, "build-prerequisite");
        depEl.appendChild(buildEl);
        Element compileEl = doc.createElementNS(NS_NBMPROJECT, "compile-dependency");
        depEl.appendChild(compileEl);
        Element runEl = doc.createElementNS(NS_NBMPROJECT, "run-dependency");
        if (relvers != null) {
            Element releaseEl = doc.createElementNS(NS_NBMPROJECT, "release-version");
            releaseEl.appendChild(doc.createTextNode(relvers));
            runEl.appendChild(releaseEl);
        }
        switch (d.getComparison()) {
        case Dependency.COMPARE_ANY:
            // Nothing more needed.
            break;
        case Dependency.COMPARE_SPEC:
            Element specEl = doc.createElementNS(NS_NBMPROJECT, "specification-version");
            specEl.appendChild(doc.createTextNode(d.getVersion()));
            runEl.appendChild(specEl);
            break;
        case Dependency.COMPARE_IMPL:
            Element implEl = doc.createElementNS(NS_NBMPROJECT, "implementation-version");
            runEl.appendChild(implEl);
            break;
        default:
            assert false : d;
        }
        depEl.appendChild(runEl);
        return depEl;
    }
    
}
