/*
 * 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.util.*;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.*;
import java.util.jar.*;
import java.io.*;
import java.util.zip.ZipEntry;

/** Create XML files corresponding to the set of known modules
 * without actually running the IDE.
 * @author Jesse Glick
 */
public class CreateModuleXML extends Task {

    private final List<FileSet> enabled = new ArrayList<>(1);
    private final List<FileSet> disabled = new ArrayList<>(1);
    private final List<FileSet> autoload = new ArrayList<>(1);
    private final List<FileSet> eager = new ArrayList<>(1);
    private final List<FileSet> hidden = new ArrayList<>(1);
    private Integer level;
    private boolean checkBundle = true;

    /** Add a set of module JARs that should be enabled.
     */
    public void addEnabled(FileSet fs) {
        enabled.add(fs);
    }
    
    /** Add a set of module JARs that should be disabled.
     */
    public void addDisabled(FileSet fs) {
        disabled.add(fs);
    }
    
    /** Add a set of module JARs that should be autoloads.
     */
    public void addAutoload(FileSet fs) {
        autoload.add(fs);
    }
    
    /** Add a set of module JARs that should be eager modules.
     */
    public void addEager(FileSet fs) {
        eager.add(fs);
    }
    
    /** Add a set of module JARs that should be hidden.
     */
    public void addHidden(FileSet fs) {
        hidden.add(fs);
    }
    
    private File xmldir = null;
    
    /** Set the modules directory where XML will be stored.
     * Normally the system/Modules/ directory in an installation.
     */
    public void setXmldir(File f) {
        xmldir = f;
    }

    private File trackingdir;
    /** Directory where to generate the update tracking file. Does not need
     * to be specified.
     */
    public void setUpdateTrackingRoot(File f) {
        trackingdir = f;
    }

    private boolean failOnMissingManifest = true;
    /** By default true. Set to false if JAR files without proper manifest
     * shall be ignored.
     */
    public void setFailOnMissingManifest(boolean fail) {
        failOnMissingManifest = fail;
    }
    public void setStartLevel(String level) {
        if (level.isEmpty()) {
            this.level = null;
        } else {
            this.level = Integer.parseInt(level);
        }
    }
    
    public void setStrictCheck(boolean check) {
        this.checkBundle = check;
    }
    
    private List<String> enabledNames = new ArrayList<>(50);
    private List<String> disabledNames = new ArrayList<>(10);
    private List<String> autoloadNames = new ArrayList<>(10);
    private List<String> eagerNames = new ArrayList<>(10);
    private List<String> hiddenNames = new ArrayList<>(10);
    private String hiddenList;

    /**
     * In addition to &gt;hidden&lt; nested fileset, list of hidden modules can
     * be specified as comma-separated list in attribute 'hidden'.
     * @param hiddenList
     */
    public void setHidden(String hiddenList) {
        // Only hidden parameters can be specified as list of absolute file paths,
        // enabled, eager and autoload modules need relative path to create proper module xml
        this.hiddenList = hiddenList;
    }

    @Override
    public void execute() throws BuildException {
        if (xmldir == null) throw new BuildException("Must set xmldir attribute", getLocation());
        if (!xmldir.exists ()) {
            if (!xmldir.mkdirs()) throw new BuildException("Cannot create directory " + xmldir, getLocation());
        }
        if (enabled.isEmpty() && disabled.isEmpty() && autoload.isEmpty() && eager.isEmpty() && hidden.isEmpty()
                && hiddenList == null) {
            log("Warning: <createmodulexml> with no modules listed", Project.MSG_WARN);
        }
        for (FileSet fs : enabled) {
            scanModules(fs, true, false, false, false, enabledNames);
        }
        for (FileSet fs : disabled) {
            scanModules(fs, false, false, false, false, disabledNames);
        }
        for (FileSet fs : autoload) {
            scanModules(fs, false, true, false, false, autoloadNames);
        }
        for (FileSet fs : eager) {
            scanModules(fs, false, false, true, false, eagerNames);
        }
        for (FileSet fs : hidden) {
            scanModules(fs, false, false, false, true, hiddenNames);
        }
        if (hiddenList != null && hiddenList.length() > 0)
            scanModulesInList(hiddenList, false, false, false, true, hiddenNames);
        Collections.sort(enabledNames);
        Collections.sort(disabledNames);
        Collections.sort(autoloadNames);
        Collections.sort(eagerNames);
        Collections.sort(hiddenNames);
        if (!enabledNames.isEmpty()) {
            log("Enabled modules: " + enabledNames, Project.MSG_VERBOSE);
        }
        if (!disabledNames.isEmpty()) {
            log("Disabled modules: " + disabledNames, Project.MSG_VERBOSE);
        }
        if (!autoloadNames.isEmpty()) {
            log("Autoload modules: " + autoloadNames, Project.MSG_VERBOSE);
        }
        if (!eagerNames.isEmpty()) {
            log("Eager modules: " + eagerNames, Project.MSG_VERBOSE);
        }
        if (!hiddenNames.isEmpty()) {
            log("Hidden modules: " + hiddenNames, Project.MSG_VERBOSE);
        }
    }
    
    private void scanModules(FileSet fs, boolean isEnabled, boolean isAutoload, boolean isEager, boolean isHidden, List<String> names) throws BuildException {
        FileScanner scan = fs.getDirectoryScanner(getProject());
        File dir = scan.getBasedir();
        for (String kid : scan.getIncludedFiles()) {
            File module = new File(dir, kid);
            scanOneModule(module, kid, isEnabled, isAutoload, isEager, isHidden, names);
        }
    }

    private void scanModulesInList(String modulesList, boolean isEnabled, boolean isAutoload,
            boolean isEager, boolean isHidden, List<String> names) throws BuildException {
        String[] modules = modulesList.split(",");
        for (String modulePath : modules) {
            File module = new File(modulePath);
            scanOneModule(module, null, isEnabled, isAutoload, isEager, isHidden, names);
        }
    }

    private void scanOneModule(
        File module, String kid, boolean isEnabled,
        boolean isAutoload, boolean isEager,
        boolean isHidden, List<String> names
    ) throws BuildException {
        UpdateTracking ut = null;
        if (trackingdir != null) {
            ut = new UpdateTracking(trackingdir.getAbsolutePath());
        }

        processModule(module, kid, isEnabled, isAutoload, isEager, isHidden, names, ut);
    }
    private void processModule(
        File module, String kid, boolean isEnabled,boolean isAutoload,
        boolean isEager, boolean isHidden,
        List<String> names, UpdateTracking ut
    ) throws BuildException {
        if (!module.exists()) {
            throw new BuildException("Module file does not exist: " + module, getLocation());
        }
        if (!module.getName().endsWith(".jar")) {
            throw new BuildException("Only *.jar may be listed, check the fileset: " + module, getLocation());
        }
        try {
            try (JarFile jar = new JarFile(module)) {
                Manifest m = jar.getManifest();
                Attributes attr = null;
                String codename = null;
                if (m != null) {
                    attr = m.getMainAttributes();
                    codename = JarWithModuleAttributes.extractCodeName(attr);
                }
                if (codename == null) {
                    if (failOnMissingManifest) {
                        throw new BuildException("Missing manifest tag OpenIDE-Module; " + module + " is not a module", getLocation());
                    } else {
                        log("No module manifest in " + module, Project.MSG_WARN);
                        return;
                    }
                }
                if (codename.endsWith(" ") || codename.endsWith("\t")) {
                    // #62887
                    throw new BuildException("Illegal trailing space in OpenIDE-Module value from " + module, getLocation());
                }
                int idx = codename.lastIndexOf('/');
                String codenamebase;
                if (idx == -1) {
                    codenamebase = codename;
                } else {
                    codenamebase = codename.substring(0, idx);
                }
                File xml = new File(xmldir, codenamebase.replace('.', '-') + ".xml");
                if (xml.exists()) {
                    // XXX should check that the old file actually matches what we would have written
                    log("Will not overwrite " + xml + "; skipping...", Project.MSG_VERBOSE);
                    return;
                }
                String displayname = attr.getValue("OpenIDE-Module-Name");
                if (displayname == null) {
                    String bundle = attr.getValue("OpenIDE-Module-Localizing-Bundle");
                    if (bundle != null) {
                        // Display name actually found in a bundle, not manifest.
                        ZipEntry entry = jar.getEntry(bundle);
                        InputStream is;
                        if (entry != null) {
                            is = jar.getInputStream(entry);
                        } else {
                            File moduleloc = new File(new File(module.getParentFile(), "locale"), module.getName());
                            if (!moduleloc.isFile()) {
                                throw new BuildException("Expecting localizing bundle: " + bundle + " in: " + module);
                            }
                            try (JarFile jarloc = new JarFile(moduleloc)) {
                                ZipEntry entry2 = jarloc.getEntry(bundle);
                                if (entry2 == null) {
                                    throw new BuildException("Expecting localizing bundle: " + bundle + " in: " + module);
                                }
                                is = jarloc.getInputStream(entry2);
                            }
                        }
                        try {
                            Properties p = new Properties();
                            p.load(is);
                            displayname = p.getProperty("OpenIDE-Module-Name");
                        } finally {
                            is.close();
                        }
                    }
                }
                if (displayname == null) {
                    displayname = codename;
                }
                names.add(displayname);
                String spec = attr.getValue("OpenIDE-Module-Specification-Version");
                UpdateTracking.Version v = null;
                if (ut != null) {
                    v = ut.addNewModuleVersion(codename, spec);
                    v.addFileForRoot(module);
                }
                if (isHidden) {
                    File h = new File(xml.getParentFile(), xml.getName() + "_hidden");
                    h.createNewFile();
                } else {
                    try (OutputStream os = new FileOutputStream(xml)) {
                        PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
                        // Please make sure formatting matches what the IDE actually spits
                        // out; it could matter.
                        pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                        pw.println("<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"");
                        pw.println("                        \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">");
                        pw.println("<module name=\"" + codenamebase + "\">");
                        pw.println("    <param name=\"autoload\">" + isAutoload + "</param>");
                        pw.println("    <param name=\"eager\">" + isEager + "</param>");
                        if (!isAutoload && !isEager) {
                            pw.println("    <param name=\"enabled\">" + isEnabled + "</param>");
                        }
                        pw.println("    <param name=\"jar\">" + kid.replace(File.separatorChar, '/') + "</param>");
                        pw.println("    <param name=\"reloadable\">false</param>");
                        OUT: if (level != null) {
                            if (attr == null || attr.getValue("Bundle-SymbolicName") == null) {
                                if (!checkBundle) {
                                    break OUT;
                                }
                                throw new BuildException("startlevel specified, but " + module + " is not OSGi bundle!");
                            }
                            pw.println("    <param name=\"startlevel\">" + level + "</param>");
                        }
                        pw.println("</module>");
                        pw.flush();
                        pw.close();
                    }
                    if (v != null) {
                        v.addFileForRoot(xml);
                    }
                }
            }
            if (ut != null) {
                ut.write();
            }
        } catch (IOException ioe) {
            throw new BuildException("Caught while processing " + module + ": " + ioe, ioe, getLocation());
        }
    }
    
}
