/*
 * 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.core.startup;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.DuplicateException;
import org.netbeans.Events;
import org.netbeans.JarClassLoader;
import org.netbeans.JaveleonModule;
import org.netbeans.Module;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.ModuleInfo;
import org.openide.modules.OnStop;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;

/** Controller of the IDE's whole module system.
 * Contains higher-level convenience methods to
 * access the basic functionality and procedural
 * stages of the module system.
 * Main should hold a reference to one instance.
 * Methods are thread-safe.
 * @author Jesse Glick
 */
public final class ModuleSystem {
    private static final Logger LOG = Logger.getLogger(ModuleSystem.class.getName());
    private final ModuleManager mgr;
    private final NbInstaller installer;
    private ModuleList list;
    private final Events ev;
    
    /** Initialize module system.
     * The system file system is needed as that holds the Modules/ folder.
     * Note if the systemFileSystem is read-only, no module list will be created,
     * so it is forbidden to call readList, scanForNewAndRestore, or installNew.
     */
    public ModuleSystem(FileSystem systemFileSystem) throws IOException {
        this();
        init(systemFileSystem);
    }
    ModuleSystem() {
        ev = Boolean.getBoolean("netbeans.modules.quiet") ? (Events) new QuietEvents() : new NbEvents();
        installer = new NbInstaller(ev);
        mgr = new ModuleManager(installer, ev);
    }

    final void init(FileSystem systemFileSystem) throws IOException {
        if (Boolean.getBoolean("org.netbeans.core.startup.ModuleSystem.CULPRIT")) Thread.dumpStack(); // NOI18N
        PropertyChangeListener l = new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent ev) {
                if (ModuleManager.PROP_CLASS_LOADER.equals(ev.getPropertyName())) {
                    org.netbeans.core.startup.MainLookup.systemClassLoaderChanged(mgr.getClassLoader());
                }
            }
        };
        mgr.addPropertyChangeListener(l);
        
        // now initialize to core/* classloader, later we reassign to all modules
        org.netbeans.core.startup.MainLookup.systemClassLoaderChanged(installer.getClass ().getClassLoader ());
        // #28465: initialize module lookup early
        org.netbeans.core.startup.MainLookup.moduleLookupReady(mgr.getModuleLookup());
        if (systemFileSystem.isReadOnly()) {
            list = null;
        } else {
            FileObject root = systemFileSystem.getRoot();
            FileObject modulesFolder = root.getFileObject("Modules"); // NOI18N
            if (modulesFolder == null) {
                modulesFolder = root.createFolder("Modules"); // NOI18N
            }
            list = new ModuleList(mgr, modulesFolder, ev);
            installer.registerList(list);
            installer.registerManager(mgr);
        }
        ev.log(Events.CREATED_MODULE_SYSTEM);
    }
    
    /** Get the raw module manager.
     * Useful for pieces of the UI needing to directly affect the set of installed modules.
     * For example, the Modules node in the Options window may use this.
     */
    public ModuleManager getManager() {
        return mgr;
    }
    
    /** Get the event-logging handler.
     */
    public Events getEvents() {
        return ev;
    }
    
    /** Produce a list of JAR files including all installed modules,
     * their extensions, and enabled locale variants of both.
     * Will be returned in a classpath-like order.
     * Intended for use by the execution engine (though sort of deprecated).
     * @return list of module-related JARs/ZIPs
     */
    public List<File> getModuleJars () {
        mgr.mutexPrivileged().enterReadAccess();
        try {
            List<File> l = new ArrayList<File>();
            for (Module m: mgr.getEnabledModules()) {
                l.addAll(m.getAllJars());
            }
            return l;
        } finally {
            mgr.mutexPrivileged().exitReadAccess();
        }
    }

    /** We just make the modules now, restore them later
     * to optimize the layer merge.
     */
    private Set<Module> bootModules = null;
    
    /** Load modules found in the classpath.
     * Note that they might not satisfy all their dependencies, in which
     * case oh well...
     */
    public void loadBootModules() {
        // Keep a list of manifest URL bases which we know we do not need to
        // parse. Some of these manifests might be signed, and if so, we do not
        // want to touch them, as it slows down startup quite a bit.
        Set<File> ignoredJars = new HashSet<File>();
        String javaHome = System.getProperty("java.home"); // NOI18N
        if (javaHome != null) {
            File lib = new File(new File(javaHome).getParentFile(), "lib"); // NOI18N
            ignoredJars.add(new File(lib, "tools.jar")); // NOI18N
            ignoredJars.add(new File(lib, "dt.jar")); // NOI18N
        }
        for (String entry : System.getProperty("sun.boot.class.path", "").split(File.pathSeparator)) { // NOI18N
            ignoredJars.add(new File(entry));
        }
        LOG.log(Level.FINE, "Ignored JARs: {0}", ignoredJars);
            
        mgr.mutexPrivileged().enterWriteAccess();
        ev.log(Events.START_LOAD_BOOT_MODULES);
        try {
            bootModules = new HashSet<Module>(10);
            ClassLoader upperLoader = ModuleSystem.class.getClassLoader();
            // wrap alien loader, so it can be used among parent loaders of module (instanceof ProxyClassLoader)
            ClassLoader loader = new JarClassLoader(Collections.<File>emptyList(), new ClassLoader[] { Module.class.getClassLoader() });
            
            Enumeration<URL> e = loader.getResources("META-INF/MANIFEST.MF"); // NOI18N
            Enumeration<URL> upperE = upperLoader.getResources("META-INF/MANIFEST.MF"); // NOI18N
            ev.log(Events.PERF_TICK, "got all manifests"); // NOI18N
            
            // There will be duplicates: cf. #32576.
            Set<URL> checkedManifests = new HashSet<URL>();
            
            // process libs in 2 passes; first, process bootstrap libraries in platform/libs, creating
            // FixedModules with a classloader that only loads from those libs.
            // 2nd pass will process the remaining libraries, using this classloader.
            createBootModules(e, checkedManifests, ignoredJars, loader);
            createBootModules(upperE, checkedManifests, ignoredJars, upperLoader);

            if (list == null) {
                // Plain calling us, we have to install now.
                // Do it the simple way.
                mgr.enable(bootModules);
            }
            ev.log(Events.PERF_TICK, "added all classpath modules"); // NOI18N
	    
        } catch (IOException ioe) {
            // Note: includes also InvalidException's for malformed this and that.
            // Probably if a bootstrap module is corrupt we are in pretty bad shape
            // anyway, so don't bother trying to be fancy and install just some of
            // them etc.
            LOG.log(Level.WARNING, null, ioe);
        } catch (DuplicateException de) {
            LOG.log(Level.WARNING, null, de);
        } finally {
            // Not 100% accurate in this case:
            ev.log(Events.FINISH_LOAD_BOOT_MODULES);
            mgr.mutexPrivileged().exitWriteAccess();
        }
    }

    /**
     * Creates bootstrap (fixed) modules for the given manifests.
     * 
     * @param e manifest URLs.
     * @param checkedManifests manifests already processed earlier
     * @param ignoredJars JARs which should be ignored
     * @param loader module classloader for the created modules
     * 
     * @throws IOException
     * @throws DuplicateException 
     */
    private void createBootModules(Enumeration<URL> e, Set<URL> checkedManifests, Set<File> ignoredJars, ClassLoader loader) throws IOException, DuplicateException {
        while (e.hasMoreElements()) {
            URL manifestUrl = e.nextElement();
            if (!checkedManifests.add(manifestUrl)) {
                // Already seen, ignore.
                continue;
            }
            URL jarURL = FileUtil.getArchiveFile(manifestUrl);
            if (jarURL != null && jarURL.getProtocol().equals("file") &&
                    /* #121777 */ jarURL.getPath().startsWith("/")) {
                LOG.log(Level.FINE, "Considering JAR: {0}", jarURL);
                try {
                    if (ignoredJars.contains(BaseUtilities.toFile(jarURL.toURI()))) {
                        LOG.log(Level.FINE, "ignoring JDK/JRE manifest: {0}", manifestUrl);
                        continue;
                    }
                } catch (URISyntaxException x) {
                    Exceptions.printStackTrace(x);
                }
            }
            LOG.log(Level.FINE, "Checking boot manifest: {0}", manifestUrl);
            
            InputStream is;
            try {
                is = manifestUrl.openStream();
            } catch (IOException ioe) {
                // Debugging for e.g. #32493 - which JAR was guilty?
                throw Exceptions.attachMessage(ioe, "URL: " + manifestUrl); // NOI18N
            }
            try {
                Manifest mani = new Manifest(is);
                Attributes attr = mani.getMainAttributes();
                if (attr.getValue("OpenIDE-Module") == null) { // NOI18N
                    // Not a module.
                    continue;
                }
                bootModules.add(mgr.createFixed(mani, manifestUrl, loader));
            } finally {
                is.close();
            }
        }
    }
    
    /** Refreshes the list of modules. Used from autoupdate.services.
     * @since 1.28
     */
    public final void refresh() {
        list.moduleListChanged();
    }
    
    /** Read disk settings and determine what the known modules are.
     */
    public void readList() {
        ev.log(Events.PERF_START, "ModuleSystem.readList"); // NOI18N
        mgr.mutexPrivileged().enterWriteAccess();
        try {
            list.readInitial();
        } finally {
            mgr.mutexPrivileged().exitWriteAccess();
        }
	ev.log(Events.PERF_END, "ModuleSystem.readList"); // NOI18N
    }
    
    /** Install read modules.
     */
    public void restore() {
	ev.log(Events.PERF_START, "ModuleSystem.restore"); // NOI18N
        mgr.mutexPrivileged().enterWriteAccess();
        try {
            Set<Module> toTrigger = new HashSet<Module>(bootModules/*Collections.EMPTY_SET*/);
            list.trigger(toTrigger);
            mgr.releaseModuleManifests();
        } finally {
            mgr.mutexPrivileged().exitWriteAccess();
        }
        installer.preloadCache(mgr.getModules());
        ev.log(Events.PERF_END, "ModuleSystem.restore"); // NOI18N	
    	ev.log(Events.PERF_START, "ModuleSystem.waitOnStart"); // NOI18N
        installer.waitOnStart();
    	ev.log(Events.PERF_END, "ModuleSystem.waitOnStart"); // NOI18N
    }
    
    /** Shut down the system: ask modules to shut down.
     * Some of them may refuse.
     */
    public boolean shutDown(final Runnable midHook) {
        try {
            return shutDownAsync(midHook).get();
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        } catch (ExecutionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return false;
    }
    /** Initializes the shutdown, asks modules to confirm shut down, if some
     * of the refure, returns <code>false</code> immediately. If they
     * agree, run midHook code and asks modules to really shut down.
     * Returns even {@link OnStop} code may not have finished yet. One
     * can wait for the returned future till all post clean up hooks are
     * finished.
     * 
     * @since 1.44
     * @param midHook the code to run when the shutdown is approved
     * @return future for tracking final progress of shutdown and obtaining
     *    the final value
     */
    public Future<Boolean> shutDownAsync(final Runnable midHook) {
        mgr.mutexPrivileged().enterWriteAccess();
        Runnable both = new Runnable() {
            @Override
            public void run() {
                midHook.run();
                Stamps.getModulesJARs().shutdown();
            }
        };
        Future<Boolean> res;
        try {
            res = mgr.shutDownAsync(both);
        } finally {
            mgr.mutexPrivileged().exitWriteAccess();
        }
        return res;
    }
    
    /** Load a module in test (reloadable) mode.
     * If there is an existing module with a different JAR, get
     * rid of it and load this one instead.
     * If it is already installed, disable it and reenable it
     * to reload its contents.
     * If other modules depend on it, disable them first and
     * then (try to) enable them again later.
     */
    final void deployTestModule(File jar) throws IOException {
        if (! jar.isAbsolute()) throw new IOException("Absolute paths only please"); // NOI18N

        // Check to see if Javeleon is enabled. If so,
        // let Javeleon handle the module reloading.
        if (JaveleonModule.isJaveleonPresent && JaveleonModuleReloader.getDefault().reloadJaveleonModule(jar, mgr, installer, ev)) {
            return;
        }
        
        mgr.mutexPrivileged().enterWriteAccess();
        ev.log(Events.START_DEPLOY_TEST_MODULE, jar);
        // For now, just print to stderr directly; could also go thru Events.
        // No need for I18N, module developers are expected to know English
        // well enough.
        System.err.println("Deploying test module " + jar + "..."); // NOI18N
        try {
            // The test module:
            Module tm = null;
            // Anything that needs to be turned back on later:
            Set<Module> toReenable = new HashSet<Module>();
            // First see if this refers to an existing module.
            // (If so, make sure it is reloadable.)
            for (Module m : mgr.getModules()) {
                if (m.getJarFile() != null) {
                    if (jar.equals(m.getJarFile())) {
                        if (m.isAutoload() || m.isEager()) {
                            System.err.println("Reloading autoload and eager modules is not supported, sorry! (Try Javeleon.)");
                            return;
                        }
                        // Hah, found it.
                        if (! m.isReloadable()) {
                            m.setReloadable(true);
                        }
                        turnOffModule(m, toReenable);
                        mgr.reload(m);
                        tm = m;
                        break;
                    }
                }
            }
            if (tm == null) {
                // This JAR not encountered before. Try to load it. If it is
                // a duplicate of an existing module in a different location,
                // kill the existing one and replace it with this one.
                try {
                    tm = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), true, false, false);
                } catch (DuplicateException dupe) {
                    Module old = dupe.getOldModule();
                    System.err.println("Replacing old module in " + old); // NOI18N
                    turnOffModule(old, toReenable);
                    mgr.delete(old);
                    try {
                        tm = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), true, false, false);
                    } catch (DuplicateException dupe2) {
                        // Should not happen.
                        throw (IOException) new IOException(dupe2.toString()).initCause(dupe2);
                    }
                }
            }
            // Try to turn on the test module. It might throw InvalidExc < IOExc.
            System.err.println("Enabling " + tm + "..."); // NOI18N
            if (!mgr.simulateEnable(Collections.singleton(tm)).contains(tm)) {
                throw new IOException("Cannot enable " + tm + "; problems: " + tm.getProblems());
            }
            mgr.enable(tm);
            // OK, so far so good; also try to turn on any other modules if
            // we can that were on before. Just try to turn them all on.
            // Don't get fancy; if some of them could not be turned on, the
            // developer will be told and can clean up the situation as needed.
            // Also any of them marked as reloadable, reload them now.
            if (! toReenable.isEmpty()) {
                System.err.println("Also re-enabling:"); // NOI18N
                for (Module m : toReenable) {
                    System.err.println("\t" + m.getDisplayName()); // NOI18N
                    if (m.isReloadable()) {
                        m.reload();
                    }
                }
                try {
                    mgr.enable(toReenable);
                } catch (IllegalArgumentException iae) {
                    // Strange new dependencies, etc.
                    throw new IOException(iae.toString());
                }
            }
            System.err.println("Done."); // NOI18N
        } finally {
            ev.log(Events.FINISH_DEPLOY_TEST_MODULE, jar);
            mgr.mutexPrivileged().exitWriteAccess();
        }
    }
    /** Make sure some module is disabled.
     * If there were any other non-autoload modules enabled
     * which depended on it, make note of them.
     */
    private void turnOffModule(Module m, Set<Module> toReenable) {
        if (! m.isEnabled()) {
            // Already done.
            return;
        }
        for (Module m2: mgr.simulateDisable(Collections.<Module>singleton(m))) {
            if (!m2.isAutoload() && !m2.isEager()) {
                toReenable.add(m2);
            }
        }
        try {
            System.err.println("Disabling " + m + "..."); // NOI18N
            // Don't mention the others, they will be mentioned later anyway.
            mgr.disable(toReenable);
        } finally {
            toReenable.remove(m);
        }
    }
    
    /** Get the effective "classpath" used by a module.
     * <p>This is a somewhat stretched notion, but should give something that looks
     * as much like a classpath as possible, i.e. a list of directories or JARs
     * separated by the standard separator, which roughly represents what resources
     * are visible to the module's classloader. May use special syntax to represent
     * situations in which only certain packages are available from a particular
     * "classpath" entry.
     * <p>Disabled modules have no classpath (empty string).
     * <p>Call within a mutex.
     * @param m the module to build a classpath for
     * @return an approximation of that module's classpath
     * @see "#22466"
     * @since org.netbeans.core/1 > 1.5
     */
    public String getEffectiveClasspath(Module m) {
        return installer.getEffectiveClasspath(m);
    }

    /** Checks whether the provided module will be visible in autoupdate client.
     * Seeks for AutoUpdate-Show-In-Client attribute in manifest. The module
     * is visible if the attribute is missing or if it is set to <code>true</code>.
     * Uses caches to remember the value between restarts.
     * 
     * @since 1.38
     * @param mi the module to test
     * @return <code>true</code> if the module is supposed to be visible in 
     *   autoupdate client
     */
    public boolean isShowInAutoUpdateClient(ModuleInfo mi) {
        return this.installer.isShowInAutoUpdateClient(mi);
    }

    /** Creates files that instruct the native launcher to perform restart as
     * soon as the Java process finishes. 
     * 
     * @since 1.45
     * @throws UnsupportedOperationException some environments (like WebStart)
     *   do not support restart and may throw an exception to indicate that
     */
    public static void markForRestart() throws UnsupportedOperationException {
        ModuleLifecycleManager.markReadyForRestart();
    }
    
    /** Dummy event handler that does not print anything.
     * Useful for test scripts where you do not really want to see
     * everything going by.
     */
    private static final class QuietEvents extends Events {
        QuietEvents() {}
        protected void logged(String message, Object[] args) {}
    }

}
