/*
 * 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.modules.apisupport.project;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.apisupport.project.api.ManifestManager;
import org.netbeans.modules.apisupport.project.api.Util;
import org.netbeans.modules.apisupport.project.api.EditableManifest;
import org.netbeans.modules.apisupport.project.api.LayerHandle;
import org.netbeans.modules.apisupport.project.ui.customizer.SingleModuleProperties;
import org.netbeans.modules.apisupport.project.ui.customizer.SuiteProperties;
import org.netbeans.modules.apisupport.project.ui.customizer.SuiteUtils;
import org.netbeans.modules.apisupport.project.universe.ClusterUtils;
import org.netbeans.modules.apisupport.project.universe.LocalizedBundleInfo;
import org.netbeans.modules.apisupport.project.universe.ModuleList;
import org.netbeans.modules.apisupport.project.universe.NbPlatform;
import org.netbeans.modules.apisupport.project.universe.HarnessVersion;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.support.ant.GeneratedFilesHelper;
import org.netbeans.spi.project.support.ant.PropertyUtils;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Mutex;
import org.openide.util.MutexException;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Servers for generating new NetBeans Modules templates.
 *
 * @author Martin Krauskopf
 */
public class NbModuleProjectGenerator {
    
    public static final String PLATFORM_PROPERTIES_PATH =
            "nbproject/platform.properties"; // NOI18N
    
    /** Use static factory methods instead. */
    private NbModuleProjectGenerator() {/* empty constructor*/}

    private static final String loggerName = "org.netbeans.ui.metrics.nbmodule"; // NOI18N
    private static final String loggerKey = "USG_PROJECT_CREATE_NBMODULE"; // NOI18N

    // http://wiki.netbeans.org/UsageLoggingSpecification
    private static void logUsage(String type) {
        LogRecord logRecord = new LogRecord(Level.INFO, loggerKey);
        logRecord.setLoggerName(loggerName);
        logRecord.setParameters(new Object[] {type});
        Logger.getLogger(loggerName).log(logRecord);
    }
    
    /** Generates standalone NetBeans Module. */
    public static void createStandAloneModule(final File projectDir, final String cnb,
            final String name, final String bundlePath,
            final String layerPath, final String platformID, final boolean osgi, final boolean tests) throws IOException {
        try {
            logUsage("StandAloneModule"); // NOI18N
            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                public Void run() throws IOException {
                    final FileObject dirFO = FileUtil.createFolder(projectDir);
                    if (ProjectManager.getDefault().findProject(dirFO) != null) {
                        throw new IllegalArgumentException("Already a project in " + dirFO); // NOI18N
                    }
                    createProjectXML(dirFO, cnb, NbModuleType.STANDALONE, osgi);
                    createPlatformProperties(dirFO, platformID);
                    createManifest(dirFO, cnb, bundlePath, layerPath, osgi);
                    if (bundlePath != null) {
                        createBundle(dirFO, bundlePath, name);
                    }
                    if (layerPath != null) {
                        createLayerInSrc(dirFO, layerPath);
                    }
                    createEmptyTestDir(dirFO, tests);
                    createInitialProperties(dirFO);
                    ModuleList.refresh();
                    ProjectManager.getDefault().clearNonProjectCache();
                    return null;
                }
            });
        } catch (MutexException e) {
            throw (IOException) e.getException();
        }
    }
    
    /** Generates suite component NetBeans Module. */
    public static void createSuiteComponentModule(final File projectDir, final String cnb,
            final String name, final String bundlePath,
            final String layerPath, final File suiteDir, final boolean osgi, final boolean tests) throws IOException {
        try {
            logUsage("SuiteComponentModule"); // NOI18N
            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                public Void run() throws IOException {
                    final FileObject dirFO = FileUtil.createFolder(projectDir);
                    if (ProjectManager.getDefault().findProject(dirFO) != null) {
                        throw new IllegalArgumentException("Already a project in " + dirFO); // NOI18N
                    }
                    createProjectXML(dirFO, cnb, NbModuleType.SUITE_COMPONENT, osgi);
                    createSuiteProperties(dirFO, suiteDir);
                    createManifest(dirFO, cnb, bundlePath, layerPath, osgi);
                    if (bundlePath != null) {
                        createBundle(dirFO, bundlePath, name);
                    }
                    if (layerPath != null) {
                        createLayerInSrc(dirFO, layerPath);
                    }
                    createEmptyTestDir(dirFO, tests);
                    createInitialProperties(dirFO);
                    ModuleList.refresh();
                    ProjectManager.getDefault().clearNonProjectCache();
                    appendToSuite(cnb, dirFO, suiteDir);
                    return null;
                }
            });
        } catch (MutexException e) {
            throw (IOException) e.getException();
        }
    }
    
    /** Generates suite component Library Wrapper NetBeans Module. */
    public static void createSuiteLibraryModule(final File projectDir, final String cnb,
            final String name, final String bundlePath, final File suiteDir,
            final File license, final File[] jars) throws IOException {
        try {
            logUsage("SuiteLibraryModule"); // NOI18N
            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                public Void run() throws IOException {
                    final FileObject dirFO = FileUtil.createFolder(projectDir);
                    if (ProjectManager.getDefault().findProject(dirFO) != null) {
                        throw new IllegalArgumentException("Already a project in " + dirFO); // NOI18N
                    }

                    EditableProperties props = new EditableProperties(true);
                    props.put(SingleModuleProperties.IS_AUTOLOAD, "true"); // NOI18N
                    SortedSet<String> packageList = new TreeSet<String>();
                    Map<String,String> classPathExtensions = new HashMap<String, String>();
                    for (File jar : jars) {
                        try {
                            String[] entry = ApisupportAntUtils.copyClassPathExtensionJar(projectDir, jar);
                            if (entry != null) {
                                classPathExtensions.put(entry[0], entry[1]);
                                ApisupportAntUtils.scanJarForPackageNames(packageList, jar);
                            }
                        } catch (IOException e) {
                            //TODO report
                            Util.err.notify(e);
                        }
                    }

                    if (license != null && license.exists()) {
                        FileObject fo = FileUtil.toFileObject(license);
                        try {
                            FileUtil.copyFile(fo, dirFO, fo.getName());
                            props.put(SingleModuleProperties.LICENSE_FILE, "${basedir}/" + fo.getNameExt()); // NOI18N
                            //TODO set the nbm.license property
                        } catch (IOException e) {
                            //TODO report
                            Util.err.notify(e);
                        }
                        
                    }
                    ProjectXMLManager.generateLibraryModuleTemplate(
                            createFileObject(dirFO, AntProjectHelper.PROJECT_XML_PATH),
                            cnb, NbModuleType.SUITE_COMPONENT, packageList, classPathExtensions);
                    createSuiteProperties(dirFO, suiteDir);
                    createManifest(dirFO, cnb, bundlePath, null, false);
                    createBundle(dirFO, bundlePath, name);
                    
                    // write down the nbproject/properties file
                    FileObject bundleFO = createFileObject(
                            dirFO, "nbproject/project.properties"); // NOI18N
                    Util.storeProperties(bundleFO, props);
                    
                    ModuleList.refresh();
                    ProjectManager.getDefault().clearNonProjectCache();
                    appendToSuite(cnb, dirFO, suiteDir);
                    return null;
                }
            });
        } catch (MutexException e) {
            throw (IOException) e.getException();
        }
    }
    
    /**
     * Generates NetBeans Module within the netbeans.org source tree.
     */
    public static void createNetBeansOrgModule(final File projectDir, final String cnb,
            final String name, final String bundlePath, final String layerPath, final boolean osgi) throws IOException {
        try {
            logUsage("NetBeansOrgModule"); // NOI18N
            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                public Void run() throws IOException {
                    File nborg = ModuleList.findNetBeansOrg(projectDir);
                    if (nborg == null) {
                        throw new IllegalArgumentException(projectDir + " doesn't " + // NOI18N
                                "point to a top-level directory within the netbeans.org main or contrib repositories"); // NOI18N
                    }
                    final FileObject dirFO = FileUtil.createFolder(projectDir);
                    if (ProjectManager.getDefault().findProject(dirFO) != null) {
                        throw new IllegalArgumentException("Already a project in " + dirFO); // NOI18N
                    }
                    createNetBeansOrgBuildXML(dirFO, cnb, nborg);
                    createProjectXML(dirFO, cnb, NbModuleType.NETBEANS_ORG, osgi);
                    createManifest(dirFO, cnb, bundlePath, layerPath, osgi);
                    createBundle(dirFO, bundlePath, name);
                    if (layerPath != null) {
                        createLayerInSrc(dirFO, layerPath);
                    }
                    createEmptyTestDir(dirFO, false);
                    createInitialProperties(dirFO);
                    ModuleList.refresh();
                    ProjectManager.getDefault().clearNonProjectCache();
                    return null;
                }
            });
        } catch (MutexException e) {
            throw (IOException) e.getException();
        }
    }
    
    /**
     * Creates basic <em>nbbuild/project.xml</em> or whatever
     * <code>AntProjectHelper.PROJECT_XML_PATH</code> is pointing to for
     * <em>standalone</em> or <em>module in suite</em> module.
     */
    private static void createProjectXML(FileObject projectDir,
            String cnb, NbModuleType type, boolean osgi) throws IOException {
        String[] deps = osgi ? new String[] {"org.netbeans.libs.osgi"} : new String[0]; // NOI18N
        ProjectXMLManager.generateEmptyModuleTemplate(
                createFileObject(projectDir, AntProjectHelper.PROJECT_XML_PATH),
                cnb, type, deps);
    }
    
    /**
     * Creates basic <em>build.xml</em> or whatever
     * <code>GeneratedFilesHelper.BUILD_XML_PATH</code> is pointing to.
     */
    private static void createNetBeansOrgBuildXML(FileObject projectDir, String cnb,
            File nborg) throws IOException {
        FileObject buildScript = NbModuleProjectGenerator.createFileObject(
                projectDir, GeneratedFilesHelper.BUILD_XML_PATH);
        Document prjDoc = XMLUtil.createDocument("project", null, null, null); // NOI18N
        Element prjEl = prjDoc.getDocumentElement();
        prjEl.setAttribute("name", PropertyUtils.relativizeFile(nborg, // NOI18N
                FileUtil.toFile(projectDir)));
        prjEl.setAttribute("default", "netbeans"); // NOI18N
        prjEl.setAttribute("basedir", "."); // NOI18N
        
        Element el = prjDoc.createElement("description"); // NOI18N
        el.appendChild(prjDoc.createTextNode("Builds, tests, and runs the " + // NOI18N
                "project " + cnb)); // NOI18N
        prjEl.appendChild(el);
        
        el = prjDoc.createElement("import"); // NOI18N
        el.setAttribute("file", PropertyUtils.relativizeFile(FileUtil.toFile(projectDir), // NOI18N
                new File(nborg, "nbbuild/templates/projectized.xml"))); // NOI18N
        prjEl.appendChild(el);
        
        // store document to disk
        OutputStream os = buildScript.getOutputStream();
        try {
            XMLUtil.write(prjDoc, os, "UTF-8"); // NOI18N
        } finally {
            os.close();
        }
    }
    
    /**
     * Detects whether <code>projectDir</code> is relative to
     * <code>suiteDir</code> and creates <em>nbproject/suite.properties</em> or
     * <em>nbproject/private/suite-private.properties</em> with
     * <em>suite.dir</em> appropriately set.
     */
    public static void createSuiteProperties(FileObject projectDir, File suiteDir) throws IOException {
        File projectDirF = FileUtil.toFile(projectDir);
        String suiteLocation;
        String suitePropertiesLocation;
        //mkleint: removed CollocationQuery.areCollocated() reference
        // when AlwaysRelativeCQI gets removed the condition resolves to false more frequently.
        // that might not be desirable.
        String rel = PropertyUtils.relativizeFile(projectDirF, suiteDir);
        if (rel != null) {
            suiteLocation = "${basedir}/" + rel; // NOI18N
            suitePropertiesLocation = "nbproject/suite.properties"; // NOI18N
        } else {
            suiteLocation = suiteDir.getAbsolutePath();
            suitePropertiesLocation = "nbproject/private/suite-private.properties"; // NOI18N
        }
        EditableProperties props = new EditableProperties(true);
        props.setProperty("suite.dir", suiteLocation); // NOI18N
        FileObject suiteProperties = projectDir.getFileObject(suitePropertiesLocation);
        if (suiteProperties == null || !suiteProperties.isValid()) {
            suiteProperties = createFileObject(projectDir, suitePropertiesLocation);
        }
        Util.storeProperties(suiteProperties, props);
    }
    
    /**
     * Appends currently created project in the <code>projectDir<code> to a
     * suite project contained in the <code>suiteDir</code>. Also intelligently
     * decides whether an added project is relative to a destination suite or
     * absolute and uses either <em>nbproject/project.properties</em> or
     * <em>nbproject/private/private.properties</em> appropriately.
     */
    private static void appendToSuite(String cnb, FileObject projectDir, File suiteDir) throws IOException {
        File projectDirF = FileUtil.toFile(projectDir);
        File suiteGlobalPropsFile = new File(suiteDir, "nbproject/project.properties"); // NOI18N
        FileObject suiteGlobalPropFO;
        if (suiteGlobalPropsFile.exists()) {
            suiteGlobalPropFO = FileUtil.toFileObject(suiteGlobalPropsFile);
        } else {
            suiteGlobalPropFO = createFileObject(suiteGlobalPropsFile);
        }
        EditableProperties globalProps = Util.loadProperties(suiteGlobalPropFO);
        String projectPropKey = "project." + cnb; // NOI18N
        String rel = PropertyUtils.relativizeFile(suiteDir, projectDirF);
        //mkleint: removed CollocationQuery.areCollocated() reference
        // when AlwaysRelativeCQI gets removed the condition resolves to false more frequently.
        // that might not be desirable.
        if (rel != null) {
            globalProps.setProperty(projectPropKey,
                    rel);
        } else {
            File suitePrivPropsFile = new File(suiteDir, "nbproject/private/private.properties"); // NOI18N
            FileObject suitePrivPropFO;
            if (suitePrivPropsFile.exists()) {
                suitePrivPropFO = FileUtil.toFileObject(suitePrivPropsFile);
            } else {
                suitePrivPropFO = createFileObject(suitePrivPropsFile);
            }
            EditableProperties privProps= Util.loadProperties(suitePrivPropFO);
            privProps.setProperty(projectPropKey, projectDirF.getAbsolutePath());
            Util.storeProperties(suitePrivPropFO, privProps);
        }
        String modulesProp = globalProps.getProperty("modules"); // NOI18N
        if (modulesProp == null) {
            modulesProp = "";
        }
        if (modulesProp.length() > 0) {
            modulesProp += ":"; // NOI18N
        }
        modulesProp += "${" + projectPropKey + "}"; // NOI18N
        globalProps.setProperty("modules", modulesProp.split("(?<=:)", -1)); // NOI18N
        Util.storeProperties(suiteGlobalPropFO, globalProps);
    }
    
    private static void createPlatformProperties(FileObject projectDir, String platformID) throws IOException {
        FileObject plafPropsFO = createFileObject(
                projectDir, NbModuleProjectGenerator.PLATFORM_PROPERTIES_PATH);
        EditableProperties props = new EditableProperties(true);
        props.put("nbplatform.active", platformID); // NOI18N
        NbPlatform plaf = NbPlatform.getPlatformByID(platformID);
        if (plaf != null && plaf.getHarnessVersion().compareTo(HarnessVersion.V65) > 0) {
            List<String> clusterPath = new ArrayList<String>();
            File[] files = plaf.getDestDir().listFiles();
            for (File file : files) {
                if (ClusterUtils.isValidCluster(file))
                    clusterPath.add(SuiteProperties.toPlatformClusterEntry(file.getName()));
            }
            props.setProperty(SuiteProperties.CLUSTER_PATH_PROPERTY, SuiteUtils.getAntProperty(clusterPath));
        }
        Util.storeProperties(plafPropsFO, props);
    }
    
    private static void createManifest(FileObject projectDir, String cnb,
            String bundlePath, String layerPath, boolean osgi) throws IOException {
        FileObject manifestFO = createFileObject(
                projectDir, "manifest.mf"); // NOI18N
        createManifest(manifestFO, cnb, "1.0", bundlePath, layerPath, osgi); // NOI18N
    }
    
    /**
     * Generates module manifest with the given values into the given
     * <code>manifest</code>.
     */
    static void createManifest(FileObject manifest, String cnb, String specVer,
            String bundlePath, String layerPath, boolean osgi) throws IOException {
        EditableManifest em = new EditableManifest();
        if (osgi) {
            em.setAttribute(ManifestManager.BUNDLE_SYMBOLIC_NAME, cnb, null);
            em.setAttribute(ManifestManager.BUNDLE_VERSION, specVer, null);
            em.setAttribute(ManifestManager.BUNDLE_LOCALIZATION, bundlePath.replaceFirst("[.]properties$", ""), null);
            em.setAttribute("Bundle-Name", "%" + LocalizedBundleInfo.NAME, null); // NOI18N
        } else {
            em.setAttribute(ManifestManager.OPENIDE_MODULE, cnb, null);
            em.setAttribute(ManifestManager.OPENIDE_MODULE_SPECIFICATION_VERSION, specVer, null);
            em.setAttribute(ManifestManager.OPENIDE_MODULE_LOCALIZING_BUNDLE, bundlePath, null);
        }
        em.setAttribute(ManifestManager.AUTO_UPDATE_SHOW_IN_CLIENT, Boolean.TRUE.toString(), null);
        if (layerPath != null) {
            em.setAttribute(ManifestManager.OPENIDE_MODULE_LAYER, layerPath, null);
        }
        Util.storeManifest(manifest, em);
    }
    
    private static void createBundle(FileObject projectDir, String bundlePath,
            String name) throws IOException {
        String pathToBundle = "src/" + bundlePath.replace('\\','/'); // NOI18N
        FileObject bundleFO = createFileObject(projectDir, pathToBundle);
        EditableProperties props = new EditableProperties(true);
        props.put(LocalizedBundleInfo.NAME, name);
        Util.storeProperties(bundleFO, props);
    }
    
    private static void createLayerInSrc(FileObject projectDir, String layerPath) throws IOException {
        LayerHandle.createLayer(projectDir, "src/" + layerPath); // NOI18N
    }
    
    private static void createEmptyTestDir(FileObject projectDir, boolean tests) throws IOException {
        if (tests) {
            FileUtil.createFolder(projectDir, "test/unit/src"); // NOI18N
        }
    }
    
    private static void createInitialProperties(FileObject projectDir) throws IOException {
        EditableProperties props = new EditableProperties(false);
        props.put(SingleModuleProperties.JAVAC_SOURCE, "1.7"); // NOI18N
        props.put(SingleModuleProperties.JAVAC_COMPILERARGS, "-Xlint -Xlint:-serial"); // NOI18N
        FileObject f = createFileObject(projectDir, AntProjectHelper.PROJECT_PROPERTIES_PATH);
        Util.storeProperties(f, props);
    }
    
    /**
     * Creates a new <code>FileObject</code>.
     * Throws <code>IllegalArgumentException</code> if such an object already
     * exists. Throws <code>IOException</code> if creation fails.
     */
    private static FileObject createFileObject(FileObject dir, String relToDir) throws IOException {
        FileObject createdFO = dir.getFileObject(relToDir);
        if (createdFO != null) {
            throw new IllegalArgumentException("File " + createdFO + " already exists."); // NOI18N
        }
        createdFO = FileUtil.createData(dir, relToDir);
        return createdFO;
    }
    
    /**
     * Creates a new <code>FileObject</code>.
     * Throws <code>IllegalArgumentException</code> if such an object already
     * exists. Throws <code>IOException</code> if creation fails.
     */
    private static FileObject createFileObject(File fileToCreate) throws IOException {
        File parent = fileToCreate.getParentFile();
        if (parent == null) {
            throw new IllegalArgumentException("Cannot create: " + fileToCreate); // NOI18N
        }
        if (!parent.exists()) {
            parent.mkdirs();
        }
        return createFileObject(
                FileUtil.toFileObject(parent), fileToCreate.getName());
    }
    
}
