/*
 * 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.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.Events;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.netbeans.ModuleInstaller;
import org.netbeans.ModuleManager;
import org.openide.modules.Dependency;

/**
 * Utility class permitting you to verify that a set of modules could be enabled together.
 * Currently used from <code>org.netbeans.core.validation.ValidateUpdateCenterTest</code>.
 * @author Jesse Glick
 */
public class ConsistencyVerifier {

    private ConsistencyVerifier() {}

    /**
     * Find all expected installation problems for a set of modules.
     * Standard OS and module format tokens are provided, but all other dependencies
     * must be accessible from the set of modules supplied.
     * @param modules a set of module manifests to test together
     * @return a map from module code name bases, to sets of problems, expressed in an unspecified but readable format
     * @throws IllegalArgumentException if the set of modules is illegal (e.g. contains duplicates)
     */
    public static SortedMap<String,SortedSet<String>> findInconsistencies(Set<Manifest> modules) throws IllegalArgumentException {
        return findInconsistencies(modules, null);
    }
    /**
     * Find all expected installation problems for a set of modules.
     * Standard OS and module format tokens are provided, but all other dependencies
     * must be accessible from the set of modules supplied.
     * <p>The manifests may contain the pseudoattributes <code>autoload</code> and <code>eager</code>
     * which if set to <code>true</code> will mark the corresponding modules autoload or eager (resp.).
     * It is considered an error if some nondeprecated autoload modules would not be enabled unless explicitly whitelisted.
     * @param modules a set of module manifests to test together
     * @param permittedDisabledAutoloads if null, do not check autoload enablement;
     *                                   otherwise (not null but possibly empty) permit the listed modules (as CNBs) to be disabled
     * @return a map from module code name bases, to sets of problems, expressed in an unspecified but readable format
     * @throws IllegalArgumentException if the set of modules is illegal (e.g. contains duplicates)
     */
    public static SortedMap<String,SortedSet<String>> findInconsistencies(
            Set<Manifest> modules, Set<String> permittedDisabledAutoloads) throws IllegalArgumentException {
        return findInconsistencies(modules, permittedDisabledAutoloads, true);
    }
    /* accessible to test */ static SortedMap<String,SortedSet<String>> findInconsistencies(
            Set<Manifest> modules, Set<String> permittedDisabledAutoloads, boolean formatted) throws IllegalArgumentException {
        ModuleManager mgr = new ModuleManager(new DummyInstaller(), new DummyEvents());
        mgr.mutexPrivileged().enterWriteAccess();
        Manifest dummy = new Manifest();
        dummy.getMainAttributes().putValue("OpenIDE-Module", "__dummy__"); // NOI18N
        dummy.getMainAttributes().putValue("OpenIDE-Module-Provides",
                "org.openide.modules.ModuleFormat1, " + // NOI18N
                "org.openide.modules.ModuleFormat2, " + // NOI18N
                "org.openide.modules.jre.JavaFX, " + // NOI18N
                "org.openide.modules.os.Unix, " + // NOI18N
                "org.openide.modules.os.PlainUnix, " + // NOI18N
                "org.openide.modules.os.Windows, " + // NOI18N
                "org.openide.modules.os.MacOSX, " + // NOI18N
                "org.openide.modules.os.Linux, " + // NOI18N
                "org.openide.modules.os.Solaris, " + // NOI18N
                "org.openide.modules.os.OS2"); // NOI18N
        dummy.getMainAttributes().putValue("OpenIDE-Module-Public-Packages", "-"); // NOI18N
        try {
            mgr.createFixed(dummy, null, ClassLoader.getSystemClassLoader());
        } catch (Exception x) {
            throw new AssertionError(x);
        }
        Set<Module> mods = new HashSet<Module>();
        Set<Module> regularMods = new HashSet<Module>();
        for (Manifest m : modules) {
            final Attributes man = m.getMainAttributes();
            try {
                man.putValue("OpenIDE-Module-Public-Packages", "-"); // NOI18N
                man.remove(new Attributes.Name("OpenIDE-Module-Friends")); // NOI18N
                man.remove(new Attributes.Name("OpenIDE-Module-Localizing-Bundle")); // NOI18N
                String bsn = man.getValue("Bundle-SymbolicName"); // NOI18N
                String ver = man.getValue("Bundle-Version"); // NOI18N
                if (bsn != null && ver != null) {
                    bsn = bsn.replaceAll(";.*", "");
                    if (man.getValue("OpenIDE-Module") == null) { // NOI18N
                        man.putValue("OpenIDE-Module", bsn); // NOI18N
                    }
                    if (man.getValue("OpenIDE-Module-Specification-Version") == null) { // NOI18N
                        Matcher match = Pattern.compile("[0-9]*(\\.[0-9]*)?(\\.[0-9]*)?").matcher(ver);
                        if (match.find()) {
                            ver = match.group();
                        }
                        man.putValue("OpenIDE-Module-Specification-Version", ver.replace("(.*)", "")); // NOI18N
                    }
                }
                boolean autoload = "true".equals(man.getValue("autoload"));
                boolean eager = "true".equals(man.getValue("eager"));
                if (autoload) {
                    // discard dependency on JDK: will allow other modules, dependent on these autoloads, to enable
                    man.remove(new Attributes.Name("OpenIDE-Module-Java-Dependencies"));
                }
                Module mod = mgr.createFixed(m, null, ClassLoader.getSystemClassLoader(), autoload, eager);
                mods.add(mod);
                if (!autoload && !eager) {
                    regularMods.add(mod);
                }
            } catch (Exception x) {
                throw new IllegalArgumentException("Error parsing " + man.entrySet() + ": " + x, x);
            }
        }
        SortedMap<String,SortedSet<String>> problems = new TreeMap<String,SortedSet<String>>();
        List<Module> regularModsEnabled = mgr.simulateEnable(regularMods);
        for (Module m : mods) {
            String cnb = m.getCodeNameBase();
            Set<Object> probs = m.getProblems();
            if (probs.isEmpty()) {
                if (permittedDisabledAutoloads != null && !permittedDisabledAutoloads.contains(cnb) && m.isAutoload() &&
                        !regularModsEnabled.contains(m) && !"true".equals(m.getAttribute("OpenIDE-Module-Deprecated"))) {
                    problems.put(cnb, new TreeSet<String>(Collections.singleton("module is autoload but would not be enabled")));
                }
                continue;
            }
            SortedSet<String> probnames = new TreeSet<String>();
            for (Object prob : probs) {
                if (prob instanceof Dependency) {
                    Dependency d = (Dependency) prob;
                    if (Dependency.TYPE_JAVA == d.getType() && (m.isEager() || m.isAutoload())) {
                        continue;
                    }
                }
                String description;
                if (formatted) {
                    description = NbProblemDisplayer.messageForProblem(m, prob, false);
                } else {
                    description = prob.toString();
                }
                probnames.add(description);
            }
            if (!probnames.isEmpty()) {
                problems.put(cnb, probnames);
            }
        }
        return problems;
    }

    private static final class DummyInstaller extends ModuleInstaller {
        public void prepare(Module m) throws InvalidException {
            throw new AssertionError();
        }
        public void dispose(Module m) {
            throw new AssertionError();
        }
        public void load(List<Module> modules) {
            throw new AssertionError();
        }
        public void unload(List<Module> modules) {
            throw new AssertionError();
        }
        public boolean closing(List<Module> modules) {
            throw new AssertionError();
        }
        public void close(List<Module> modules) {
            throw new AssertionError();
        }
    }

    private static final class DummyEvents extends Events {
        protected void logged(String message, Object[] args) {}
    }

}
