//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.start;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jetty.util.TopologicalSort;

/**
 * Access for all modules declared, as well as what is enabled.
 */
public class Modules implements Iterable<Module>
{
    private final List<Module> _modules = new ArrayList<>();
    private final Map<String, Module> _names = new HashMap<>();
    private final Map<String, Set<Module>> _provided = new HashMap<>();
    private final BaseHome _baseHome;
    private final StartArgs _args;
    private final Properties _deprecated = new Properties();

    public Modules(BaseHome basehome, StartArgs args)
    {
        this._baseHome = basehome;
        this._args = args;

        // Allow override mostly for testing
        if (!args.getProperties().containsKey("java.version"))
        {
            String javaVersion = System.getProperty("java.version");
            if (javaVersion != null)
            {
                args.setProperty("java.version", javaVersion, "<internal>");
            }
        }

        try
        {
            Path deprecatedPath = _baseHome.getPath("modules/deprecated.properties");
            if (deprecatedPath != null && FS.exists(deprecatedPath))
            {
                _deprecated.load(new FileInputStream(deprecatedPath.toFile()));
            }
        }
        catch (IOException e)
        {
            StartLog.debug(e);
        }
    }

    public void dump(List<String> tags)
    {
        Set<String> exclude = tags.stream().filter(t -> t.startsWith("-")).map(t -> t.substring(1)).collect(Collectors.toSet());
        Set<String> include = tags.stream().filter(t -> !t.startsWith("-")).collect(Collectors.toSet());
        boolean all = include.contains("*") || include.isEmpty();
        AtomicReference<String> tag = new AtomicReference<>();

        _modules.stream()
            .filter(m ->
            {
                boolean included = all || m.getTags().stream().anyMatch(t -> include.contains(t));
                boolean excluded = m.getTags().stream().anyMatch(t -> exclude.contains(t));
                return included && !excluded;
            })
            .sorted()
            .forEach(module ->
            {
                if (!module.getPrimaryTag().equals(tag.get()))
                {
                    tag.set(module.getPrimaryTag());
                    System.out.printf("%nModules for tag '%s':%n", module.getPrimaryTag());
                    System.out.print("-------------------");
                    for (int i = module.getPrimaryTag().length(); i-- > 0; )
                    {
                        System.out.print("-");
                    }
                    System.out.println();
                }

                String label;
                Set<String> provides = module.getProvides();
                provides.remove(module.getName());
                System.out.printf("%n     Module: %s %s%n", module.getName(), provides.size() > 0 ? provides : "");
                for (String description : module.getDescription())
                {
                    System.out.printf("           : %s%n", description);
                }
                if (!module.getTags().isEmpty())
                {
                    label = "       Tags: %s";
                    for (String t : module.getTags())
                    {
                        System.out.printf(label, t);
                        label = ", %s";
                    }
                    System.out.println();
                }
                if (!module.getDepends().isEmpty())
                {
                    label = "     Depend: %s";
                    for (String parent : module.getDepends())
                    {
                        System.out.printf(label, parent);
                        label = ", %s";
                    }
                    System.out.println();
                }
                if (!module.getOptional().isEmpty())
                {
                    label = "   Optional: %s";
                    for (String parent : module.getOptional())
                    {
                        System.out.printf(label, parent);
                        label = ", %s";
                    }
                    System.out.println();
                }
                for (String lib : module.getLibs())
                {
                    System.out.printf("        LIB: %s%n", lib);
                }
                for (String xml : module.getXmls())
                {
                    System.out.printf("        XML: %s%n", xml);
                }
                for (String jpms : module.getJPMS())
                {
                    System.out.printf("        JPMS: %s%n", jpms);
                }
                for (String jvm : module.getJvmArgs())
                {
                    System.out.printf("        JVM: %s%n", jvm);
                }
                if (module.isEnabled())
                {
                    for (String selection : module.getEnableSources())
                    {
                        System.out.printf("    Enabled: %s%n", selection);
                    }
                }
            });
    }

    public void dumpEnabled()
    {
        int i = 0;
        List<Module> enabled = getEnabled();
        for (Module module : enabled)
        {
            String name = module.getName();
            String index = (i++) + ")";
            for (String s : module.getEnableSources())
            {
                System.out.printf("  %4s %-15s %s%n", index, name, s);
                index = "";
                name = "";
            }
            if (module.isTransitive() && module.hasIniTemplate())
                System.out.printf("                       init template available with --add-to-start=%s%n", module.getName());
        }
    }

    public void registerAll() throws IOException
    {
        for (Path path : _baseHome.getPaths("modules/*.mod"))
        {
            registerModule(path);
        }
    }

    private Module registerModule(Path file)
    {
        if (!FS.canReadFile(file))
        {
            throw new IllegalStateException("Cannot read file: " + file);
        }
        String shortName = _baseHome.toShortForm(file);
        try
        {
            StartLog.debug("Registering Module: %s", shortName);
            Module module = new Module(_baseHome, file);
            _modules.add(module);
            _names.put(module.getName(), module);
            module.getProvides().forEach(n ->
            {
                _provided.computeIfAbsent(n, k -> new HashSet<Module>()).add(module);
            });

            return module;
        }
        catch (Error | RuntimeException t)
        {
            throw t;
        }
        catch (Throwable t)
        {
            throw new IllegalStateException("Unable to register module: " + shortName, t);
        }
    }

    @Override
    public String toString()
    {
        StringBuilder str = new StringBuilder();
        str.append("Modules[");
        str.append("count=").append(_modules.size());
        str.append(",<");
        final AtomicBoolean delim = new AtomicBoolean(false);
        _modules.forEach(m ->
        {
            if (delim.get())
                str.append(',');
            str.append(m.getName());
            delim.set(true);
        });
        str.append(">");
        str.append("]");
        return str.toString();
    }

    public List<Module> getEnabled()
    {
        List<Module> enabled = _modules.stream().filter(m -> m.isEnabled()).collect(Collectors.toList());

        TopologicalSort<Module> sort = new TopologicalSort<>();
        for (Module module : enabled)
        {
            Consumer<String> add = name ->
            {
                Module dependency = _names.get(name);
                if (dependency != null && dependency.isEnabled())
                    sort.addDependency(module, dependency);

                Set<Module> provided = _provided.get(name);
                if (provided != null)
                    for (Module p : provided)
                    {
                        if (p.isEnabled())
                            sort.addDependency(module, p);
                    }
            };
            module.getDepends().forEach(add);
            module.getOptional().forEach(add);
        }

        sort.sort(enabled);
        return enabled;
    }

    /**
     * Enable a module
     *
     * @param name The name of the module to enable
     * @param enabledFrom The source the module was enabled from
     * @return The set of modules newly enabled
     */
    public Set<String> enable(String name, String enabledFrom)
    {
        Module module = get(name);
        if (module == null)
            throw new UsageException(UsageException.ERR_UNKNOWN, "Unknown module='%s'. List available with --list-modules", name);

        Set<String> enabled = new HashSet<>();
        enable(enabled, module, enabledFrom, false);
        return enabled;
    }

    private void enable(Set<String> newlyEnabled, Module module, String enabledFrom, boolean transitive)
    {
        StartLog.debug("enable %s from %s transitive=%b", module, enabledFrom, transitive);

        if (newlyEnabled.contains(module.getName()))
        {
            StartLog.debug("Cycle at %s", module);
            return;
        }

        // Check that this is not already provided by another module!
        for (String name : module.getProvides())
        {
            Set<Module> providers = _provided.get(name);
            if (providers != null)
            {
                for (Module p : providers)
                {
                    if (!p.equals(module) && p.isEnabled())
                    {
                        // If the already enabled module is transitive and this enable is not
                        if (p.isTransitive() && !transitive)
                            p.clearTransitiveEnable();
                        else
                            throw new UsageException("Module %s provides %s, which is already provided by %s enabled in %s", module.getName(), name, p.getName(), p.getEnableSources());
                    }
                }
            }
        }

        // Enable the  module
        if (module.enable(enabledFrom, transitive))
        {
            StartLog.debug("enabled %s", module.getName());
            newlyEnabled.add(module.getName());

            // Expand module properties
            module.expandDependencies(_args.getProperties());

            // Apply default configuration
            if (module.hasDefaultConfig())
            {
                for (String line : module.getDefaultConfig())
                {
                    _args.parse(line, module.getName() + "[ini]");
                }
                for (Module m : _modules)
                {
                    m.expandDependencies(_args.getProperties());
                }
            }
        }

        // Process module dependencies (always processed as may be dynamic)
        StartLog.debug("Enabled module %s depends on %s", module.getName(), module.getDepends());
        for (String dependsOn : module.getDepends())
        {
            // Look for modules that provide that dependency
            Set<Module> providers = getAvailableProviders(dependsOn);

            StartLog.debug("Module %s depends on %s provided by %s", module, dependsOn, providers);

            // If there are no known providers of the module
            if (providers.isEmpty())
            {
                // look for a dynamic module
                if (dependsOn.contains("/"))
                {
                    Path file = _baseHome.getPath("modules/" + dependsOn + ".mod");
                    registerModule(file).expandDependencies(_args.getProperties());
                    providers = _provided.get(dependsOn);
                    if (providers == null || providers.isEmpty())
                        throw new UsageException("Module %s does not provide %s", _baseHome.toShortForm(file), dependsOn);

                    enable(newlyEnabled, providers.stream().findFirst().get(), "dynamic dependency of " + module.getName(), true);
                    continue;
                }
                throw new UsageException("No module found to provide %s for %s", dependsOn, module);
            }

            // If a provider is already enabled, then add a transitive enable
            if (providers.stream().filter(Module::isEnabled).count() > 0)
                providers.stream().filter(m -> m.isEnabled() && !m.equals(module)).forEach(m -> enable(newlyEnabled, m, "transitive provider of " + dependsOn + " for " + module.getName(), true));
            else
            {
                // Is there an obvious default?
                Optional<Module> dftProvider = (providers.size() == 1)
                    ? providers.stream().findFirst()
                    : providers.stream().filter(m -> m.getName().equals(dependsOn)).findFirst();

                if (dftProvider.isPresent())
                    enable(newlyEnabled, dftProvider.get(), "transitive provider of " + dependsOn + " for " + module.getName(), true);
                else if (StartLog.isDebugEnabled())
                    StartLog.debug("Module %s requires a %s implementation from one of %s", module, dependsOn, providers);
            }
        }
    }

    private Set<Module> getAvailableProviders(String name)
    {
        // Get all available providers 
        Set<Module> providers = _provided.get(name);
        StartLog.debug("Providers of %s are %s", name, providers);
        if (providers == null || providers.isEmpty())
            return Collections.emptySet();

        providers = new HashSet<>(providers);

        // find all currently provided names by other modules
        Set<String> provided = new HashSet<>();
        for (Module m : _modules)
        {
            if (m.isEnabled())
            {
                provided.add(m.getName());
                provided.addAll(m.getProvides());
            }
        }

        // Remove any that cannot be selected
        for (Iterator<Module> i = providers.iterator(); i.hasNext(); )
        {
            Module provider = i.next();
            if (!provider.isEnabled())
            {
                for (String p : provider.getProvides())
                {
                    if (provided.contains(p))
                    {
                        StartLog.debug("Removing provider %s because %s already enabled", provider, p);
                        i.remove();
                        break;
                    }
                }
            }
        }

        StartLog.debug("Available providers of %s are %s", name, providers);
        return providers;
    }

    public Module get(String name)
    {
        Module module = _names.get(name);
        if (module == null)
        {
            String reason = _deprecated.getProperty(name);
            if (reason != null)
                StartLog.warn("Module %s is no longer available: %s", name, reason);
        }
        return module;
    }

    @Override
    public Iterator<Module> iterator()
    {
        return _modules.iterator();
    }

    public Stream<Module> stream()
    {
        return _modules.stream();
    }

    public void checkEnabledModules()
    {
        StringBuilder unsatisfied = new StringBuilder();
        _modules.stream().filter(Module::isEnabled).forEach(m ->
        {
            // Check dependencies
            m.getDepends().forEach(d ->
            {
                Set<Module> providers = getAvailableProviders(d);
                if (providers.stream().filter(Module::isEnabled).count() == 0)
                {
                    if (unsatisfied.length() > 0)
                        unsatisfied.append(',');
                    unsatisfied.append(m.getName());
                    StartLog.error("Module %s requires a module providing %s from one of %s%n", m.getName(), d, providers);
                }
            });
        });

        if (unsatisfied.length() > 0)
            throw new UsageException(-1, "Unsatisfied module dependencies: " + unsatisfied);
    }
}
