/*
 * 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.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.openide.modules.Dependency;
import org.openide.modules.SpecificationVersion;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.TopologicalSortException;

// XXX public for reflection from contrib/modulemanager; should be changed to friend dep!

/**
 * Utility class to provide localized messages explaining problems
 * that modules had during attempted installation.
 * Used by both {@link org.netbeans.core.startup.NbEvents} and autoupdate's ModuleBean.
 * @author Jesse Glick
 * @see "#16636"
 */
public final class NbProblemDisplayer {
    
    private NbProblemDisplayer() {}
    
    /**
     * Provide a localized explanation of some installation problem.
     * Problem may be either an InvalidException or a Dependency.
     * Structure of message can assume that the module failing its
     * dependencies is already being displayed, and concentrate
     * on the problem.
     * @param m the module which cannot be installed
     * @param problem either an {@link InvalidException} or {@link Dependency} as returned from {@link Module#getProblems}
     * @return an explanation of the problem in the most human-friendly format available
     */
    // XXX only exists for reflective calls
    public static String messageForProblem(Module m, Object problem) {
        return messageForProblem(m, problem, true);
    }
    /**
     * @param localized true to use display names, false to use code name bases
     */
    static String messageForProblem(Module m, Object problem, boolean localized) {
        if (problem instanceof InvalidException) {
            String loc = Exceptions.findLocalizedMessage((InvalidException) problem);
            return loc != null ? loc : problem.toString();
        } else {
            Dependency dep = (Dependency)problem;
            switch (dep.getType()) {
            case Dependency.TYPE_MODULE:
                String polite = (String)m.getLocalizedAttribute("OpenIDE-Module-Module-Dependency-Message"); // NOI18N
                if (polite != null) {
                    return polite;
                } else {
                    String name = dep.getName();
                    // Find code name base:
                    int idx = name.lastIndexOf('/');
                    if (idx != -1) {
                        name = name.substring(0, idx);
                    }
                    Module other = m.getManager().get(name);
                    if (other != null && other.getCodeName().equals(dep.getName())) {
                        switch (dep.getComparison()) {
                        case Dependency.COMPARE_ANY:
                            // Just disabled (probably had its own problems).
                            return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_disabled", label(other, localized));
                        case Dependency.COMPARE_IMPL:
                            String requestedI = dep.getVersion();
                            String actualI = (other.getImplementationVersion() != null) ?
                                other.getImplementationVersion() :
                                NbBundle.getMessage(NbProblemDisplayer.class, "LBL_no_impl_version");
                            if (requestedI.equals(actualI)) {
                                // Just disabled (probably had its own problems).
                                return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_disabled", label(other, localized));
                            } else {
                                // Wrong version.
                                return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_wrong_version", label(other, localized), requestedI, actualI);
                            }
                        case Dependency.COMPARE_SPEC:
                            SpecificationVersion requestedS = new SpecificationVersion(dep.getVersion());
                            SpecificationVersion actualS = (other.getSpecificationVersion() != null) ?
                                other.getSpecificationVersion() :
                                new SpecificationVersion("0"); // NOI18N
                            if (actualS.compareTo(requestedS) >= 0) {
                                // Just disabled (probably had its own problems).
                                return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_disabled", label(other, localized));
                            } else {
                                // Too old.
                                return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_too_old", label(other, localized), requestedS, actualS);
                            }
                        default:
                            throw new IllegalStateException();
                        }
                    } else {
                        // Keep the release version info in this case.
                        // XXX would be nice to have a special message for mismatched major release
                        // version - i.e. other != null
                        return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_other_needed_not_found", dep.getName());
                    }
                }
            case Dependency.TYPE_REQUIRES:
            case Dependency.TYPE_NEEDS:
                polite = (String)m.getLocalizedAttribute("OpenIDE-Module-Requires-Message"); // NOI18N
                if (polite != null) {
                    return polite;
                } else {
                    for (Module other : m.getManager().getModules()) {
                        if (other.provides(dep.getName())) {
                            return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_require_disabled", dep.getName());
                        }
                    }
                    return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_require_not_found", dep.getName());
                }
            case Dependency.TYPE_PACKAGE:
                polite = (String)m.getLocalizedAttribute("OpenIDE-Module-Package-Dependency-Message"); // NOI18N
                if (polite != null) {
                    return polite;
                } else {
                    String name = dep.getName();
                    // Find package name or qualified name of probe class:
                    int idx = name.lastIndexOf('[');
                    if (idx == 0) {
                        // Probed class. [javax.television.Antenna]
                        return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_class_not_loaded", name.substring(1, name.length() - 1));
                    } else if (idx != -1) {
                        // Package plus sample class. javax.television[Antenna]
                        return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_package_not_loaded_or_old", name.substring(0, idx));
                    } else {
                        return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_package_not_loaded_or_old", name);
                    }
                }
            case Dependency.TYPE_JAVA:
                // XXX would OpenIDE-Module-Java-Dependency-Message be useful?
                if (dep.getName().equals(Dependency.JAVA_NAME) && dep.getComparison() == Dependency.COMPARE_SPEC) {
                    return NbBundle.getMessage(NbProblemDisplayer.class, "MSG_problem_java_too_old", dep.getVersion(), Dependency.JAVA_SPEC);
                } else {
                    // All other usages unlikely, don't bother making pretty.
                    return dep.toString();
                }
            default:
                throw new IllegalArgumentException(dep.toString());
            }
        }
    }

    private static String label(Module m, boolean localized) {
        if (localized) {
            return m.getDisplayName();
        } else {
            return m.getCodeNameBase();
        }
    }

    static void problemMessagesForModules(final Appendable writeTo, Collection<? extends Module> modules, final boolean justRootCause) {
        try {
            HashSet<String> names = new HashSet<String>();
            for (Module m : modules) {
                names.add(m.getCodeName());
            }
            HashSet<String> dependentModules = new HashSet<String>();
            class Report {
                final Module m;
                final List<Object> problems = new ArrayList<Object>();
                Report(Module m) {
                    this.m = m;
                }
                void write() throws IOException {
                    for (Object problem : problems) {
                        writeTo.append("\n\t").append(label(m, justRootCause) + " - " + NbProblemDisplayer.messageForProblem(m, problem, justRootCause));
                    }
                }
            }
            Map<Module,Report> reports = new HashMap<Module,Report>();
            Map<Module,Collection<Module>> edges = new HashMap<Module,Collection<Module>>();
            for (Module m : modules) {
                Set<Object> problems = m.getProblems();
                if (problems.isEmpty()) {
                    throw new IllegalStateException("Module " + m + " could not be installed but had no problems"); // NOI18N
                }
                Report r = new Report(m);
                for (Object problem : problems) {
                    if (problem instanceof Dependency && justRootCause) {
                        Dependency d = (Dependency) problem;
                        if (d.getType() == Dependency.TYPE_MODULE && names.contains(d.getName())) {
                            dependentModules.add(m.getCodeName());
                            continue;
                        }
                    }
                    r.problems.add(problem);
                }
                reports.put(m, r);
                edges.put(m, m.getManager().getModuleInterdependencies(m, true, false, false));
            }
            try {
                for (Module m : BaseUtilities.topologicalSort(edges.keySet(), edges)) {
                    reports.get(m).write();
                }
            } catch (TopologicalSortException x) {
                for (Report r : reports.values()) {
                    r.write();
                }
            }
            if (!dependentModules.isEmpty()) {
                writeTo.append("\n\t").append(NbBundle.getMessage(NbProblemDisplayer.class, "MSG_also_dep_modules", dependentModules.size()));
            }
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }
    
}
