/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.cli.handlers;

import org.jboss.as.cli.EscapeSelector;
import org.jboss.as.cli.Util;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * Generates suggestions for module names. Each suggestion generates only next part of the name (ie. up to the name separator).
 *
 * Assumes the module repository used has standard layered repository layout. Matching suggestions are found:
 * <uL>
 *  <li>under the repository root (excluding 'system' directory)
 *  <li>under the system/layers/{layer name}
 *  <li>under the system/add-ons/{add-on name}
 * </uL>
 * Modules changed, removed or added via patches are not included in the suggestions.
 * The modules are not validated - invalid or disabled modules and empty directories are included in the suggestions.
 *
 * @author Bartosz Spyrko-Smietanko
 */
public class ModuleNameTabCompleter {

    private static final EscapeSelector ESCAPE_SELECTOR = ch -> ch == '\\' || ch == ' ' || ch == '"';
    private static final String MODULE_NAME_SEPARATOR = ".";
    public static final String LAYERS_DIR = "system/layers";
    public static final String ADDONS_DIR = "system/add-ons";

    private final File modulesRoot;
    private final File layersDir;
    private final File addonsDir;
    private final boolean includeSystemModules;
    private final boolean excludeNonModuleFolders;

    private ModuleNameTabCompleter(Builder builder) {
        modulesRoot = builder.modulesRoot.getAbsoluteFile();
        layersDir = new File(modulesRoot, LAYERS_DIR);
        addonsDir = new File(modulesRoot, ADDONS_DIR);

        this.excludeNonModuleFolders = builder.excludeNonModuleFolders;
        this.includeSystemModules = builder.includeSystemModules;
    }


    public List<String> complete(String buffer) {
        final String userEntry = buffer == null ? "" : buffer;
        final Set<String> suggestions = new TreeSet<>(); // TreeSet deals with duplication and ordering

        List<File> moduleTrees = findInitialModuleDirectories();

        for (File f : moduleTrees) {
            findSuggestion(f, f.getName(), userEntry, suggestions);
        }

        return new ArrayList<>(suggestions);
    }

    private List<File> findInitialModuleDirectories() {
        List<File> moduleTrees = new ArrayList<>();

        moduleTrees.addAll(Arrays.asList(modulesRoot.listFiles(this::isNotSystemFolder)));

        if (includeSystemModules && layersDir.exists()) {
            for (File layer : layersDir.listFiles(File::isDirectory)) {
                moduleTrees.addAll(Arrays.asList(layer.listFiles(this::isNotPatchFolder)));
            }
        }

        if (includeSystemModules && addonsDir.exists()) {
            for (File addon : addonsDir.listFiles(File::isDirectory)) {
                moduleTrees.addAll(Arrays.asList(addon.listFiles(this::isNotPatchFolder)));
            }
        }

        return moduleTrees;
    }

    private void findSuggestion(File currentDirectory, String suggestion, String userEntry, Collection<String> candidates) {
        if (!matchesUserEntry(currentDirectory, userEntry) || (excludeNonModuleFolders && isSlotDirectory(currentDirectory))) {
            return;
        }

        if (tail(userEntry).isEmpty() && !requestsSubmodules(userEntry)) {
            final String fullModuleName = Util.escapeString(suggestion, ESCAPE_SELECTOR);
            final String partialModuleName = Util.escapeString(suggestion + MODULE_NAME_SEPARATOR, ESCAPE_SELECTOR);

            if (excludeNonModuleFolders) {
                final boolean isExactMatch = currentDirectory.getName().equals(userEntry);
                final boolean hasNestedModules = hasNestedModules(currentDirectory);
                final boolean isCompleteModule = isCompleteModule(currentDirectory);


                /*
                The suggestion should have a trailing separator ('.') if it's a part of longer module name.
                If the suggested name is both a full module name and a part of longer name (ie. nested modules), suggest
                the name without separator - unless user input is a complete name in which case suggest both options.
                 */
                if (isCompleteModule && hasNestedModules && isExactMatch) {
                    candidates.add(fullModuleName);
                    candidates.add(partialModuleName);
                } else if (isCompleteModule) {
                    candidates.add(fullModuleName);
                } else if (hasNestedModules) {
                    candidates.add(partialModuleName);
                }
            } else {
                final boolean hasChildren = currentDirectory.listFiles(File::isDirectory).length > 0;
                final boolean isExactMatch = currentDirectory.getName().equals(userEntry);

                if (hasChildren && isExactMatch) {
                    candidates.add(partialModuleName);
                }
                candidates.add(fullModuleName);
            }
        } else {
            for (File file : currentDirectory.listFiles(File::isDirectory)) {
                findSuggestion(file, suggestion + MODULE_NAME_SEPARATOR + file.getName(), tail(userEntry), candidates);
            }
        }
    }

    private boolean matchesUserEntry(File currentDirectory, String userEntry) {
        if (!userEntry.endsWith(MODULE_NAME_SEPARATOR) && tail(userEntry).isEmpty()) {
            return currentDirectory.getName().startsWith(head(userEntry));
        } else {
            return currentDirectory.getName().equals(head(userEntry));
        }
    }

    private boolean isCompleteModule(File file) {
        return file.listFiles(f -> f.isDirectory() && isSlotDirectory(f)).length > 0;
    }


    private boolean hasNestedModules(File file) {
        final File[] nonSlotChildren = file.listFiles(f -> f.isDirectory() && !isSlotDirectory(f));
        for (File potentialModule : nonSlotChildren) {
            if (subModuleExists(potentialModule)) {
                return true;
            }
        }
        return false;
    }

    // depth- first search for any module - just to check that the suggestion has any chance of delivering correct result
    private boolean subModuleExists(File dir) {
        if (isSlotDirectory(dir)) {
            return true;
        } else {
            File[] children = dir.listFiles(File::isDirectory);
            for (File child : children) {
                if (subModuleExists(child)) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean isSlotDirectory(File currentDirectory) {
        return currentDirectory.listFiles(f -> f.getName().equals("module.xml")).length > 0;
    }

    private boolean requestsSubmodules(String moduleNamePattern) {
        return moduleNamePattern.endsWith(MODULE_NAME_SEPARATOR);
    }

    private boolean isNotSystemFolder(File f) {
        return f.isDirectory() && !f.getName().equals("system");
    }

    private boolean isNotPatchFolder(File f) {
        return f.isDirectory() && !f.getName().equals("patches");
    }

    // get first part of module name (up to separator)
    private String head(String moduleName) {
        if (moduleName.indexOf(MODULE_NAME_SEPARATOR) > 0) {
            return moduleName.substring(0, moduleName.indexOf(MODULE_NAME_SEPARATOR));
        } else {
            return moduleName;
        }
    }

    // get all parts of module name apart from first
    private String tail(String moduleName) {
        if (moduleName.indexOf(MODULE_NAME_SEPARATOR) > 0) {
            return moduleName.substring(moduleName.indexOf(MODULE_NAME_SEPARATOR) + 1);
        } else {
            return "";
        }
    }

    public static Builder completer(File modulesRoot) {
        return new Builder(modulesRoot);
    }

    public static class Builder {
        private final File modulesRoot;
        private boolean includeSystemModules;
        private boolean excludeNonModuleFolders;

        public Builder(File modulesRoot) {
            this.modulesRoot = modulesRoot;
        }

        public Builder includeSystemModules(boolean includeSystemModules) {
            this.includeSystemModules = includeSystemModules;
            return this;
        }

        public Builder excludeNonModuleFolders(boolean excludeNonModuleFolders) {
            this.excludeNonModuleFolders = excludeNonModuleFolders;
            return this;
        }

        public ModuleNameTabCompleter build() {
            return new ModuleNameTabCompleter(this);
        }
    }
}
