/*_############################################################################
  _##
  _##  MibManager.java
  _##
  _##  SNMP4J-SMI-PRO
  _##  ----------------------------------
  _##  Copyright (C) 2012-2026 Frank Fock
  _##
  _##  Use of this software is subject to the license agreement you received
  _##  with this software in the "legal" folder and which can be downloaded
  _##  from https://snmp4j.org/smi/pro/LICENSE.txt
  _##
  _##########################################################################*/

import com.agentpp.repository.DefaultRepositoryManager;
import com.snmp4j.smi.*;
import org.snmp4j.smi.OID;
import org.snmp4j.util.ArgumentParser;
import org.snmp4j.util.ArgumentParser.ArgumentParseException;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * {@code MibManager} is a command line example application for SNMP4J-SMI-PRO
 * that manages a MIB repository. It can create and update a repository by
 * compiling MIB files, check a repository for unresolved imports, remove
 * modules, and look up SMI objects by OID or object name.
 * <p>
 * The command line parsing and the {@code help} concept are based on the
 * SNMP4J command line tool {@code org.snmp4j.clt.Browser} and use the
 * {@link org.snmp4j.util.ArgumentParser}.
 * <p>
 * All commands require a valid SNMP4J-SMI-PRO license which can be provided
 * either with the {@code -L <license> <key>} option or through a properties
 * configuration file named {@value #DEFAULT_CONFIG_FILE} (located in the
 * current working directory or in the user's home directory). The same
 * configuration file may also provide a default repository directory (used by
 * the {@code repo remove} and {@code mib find} commands) through the
 * {@code repository} property.
 *
 * @author Frank Fock
 * @version 2.0
 */
public class MibManager {

    public static final String TOOL_NAME = "MibManager";
    public static final String TOOL_VERSION = "2.0";

    public static final String DEFAULT_CONFIG_FILE =
            ".snmp4j-smi-pro-mib-manager.properties";

    /** Property keys understood in the configuration file. */
    public static final String CFG_LICENSE = "license";
    public static final String CFG_REPOSITORY = "repository";

    /**
     * ISO 8601 based timestamp used for repository backups. Colons (which are
     * not portable in file names) are replaced by hyphens so the name is safe
     * on all platforms.
     */
    private static final SimpleDateFormat ISO_BACKUP_FORMAT =
            new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss");

    /**
     * Well known built-in MIB modules that the compiler provides itself. They
     * are treated as always resolvable by the {@code repo check} command.
     */
    private static final Set<String> BUILTIN_MODULES = new HashSet<>(Arrays.asList(
            "RFC1155-SMI", "RFC-1212", "RFC-1213", "RFC-1215",
            "SNMPv2-SMI", "SNMPv2-TC", "SNMPv2-CONF"));

    /** Options shared by all commands (see {@link ArgumentParser}). */
    private static final String COMMON_OPTIONS =
            "+L[s{license},s{key}] +c[s{=" + DEFAULT_CONFIG_FILE + "}] +M[s] +r +v ";

    /**
     * Command table: each row holds {commandKey, optionFormat, parameterFormat}.
     * The command key is the (possibly two-word) command, e.g. "repo create".
     */
    private static final String[][] COMMANDS = {
            {
                    "repo create",
                    COMMON_OPTIONS,
                    "#command[s<repo>] #subcommand[s<create>] #fromMibDir[s] #newRepoDir[s]"
            },
            {
                    "repo create2pass",
                    COMMON_OPTIONS,
                    "#command[s<repo>] #subcommand[s<create2pass>] #fromMibDir[s] #newRepoDir[s]"
            }, {
                    "repo update",
                    COMMON_OPTIONS,
                    "#command[s<repo>] #subcommand[s<update>] #fromMibDir[s] #repoDir[s]"
            }, {
                    "repo check",
                    COMMON_OPTIONS,
                    "#command[s<repo>] #subcommand[s<check>] #repoDir[s]"
            }, {
                    "repo remove",
                    COMMON_OPTIONS,
                    "#command[s<repo>] #subcommand[s<remove>] #module[s] .."
            }, {
                    "mib find",
                    COMMON_OPTIONS,
                    "#command[s<mib>] #subcommand[s<find>] #target[s] +module[s]"
            }, {
                    "help",
                    COMMON_OPTIONS,
                    "#command[s<help>] +subject[s]"
            }, {
                    "version",
                    COMMON_OPTIONS,
                    "#command[s<version>]"
            }
    };

    /** Synopsis and description per command key (the "help concept"). */
    private static final String[][] HELP = {
            {"repo create",
                    "repo create <from-mib-dir> <new-repo-dir>",
                    "Create a new MIB repository in <new-repo-dir> by compiling all MIB " +
                            "files found (recursively) in <from-mib-dir>. If <new-repo-dir> " +
                            "already exists, it is renamed to <new-repo-dir>.<ISO-timestamp> " +
                            "as a backup before the new repository is created, unless the -r " +
                            "option is given, in which case the existing directory is deleted."},
            {"repo create2pass",
                    "repo create2pass <from-mib-dir> <new-repo-dir>",
                    "Like 'repo create', but if the first (strict) compilation pass leaves " +
                            "modules with errors, a second lenient pass is attempted for the " +
                            "remaining files. Honors the -r option in the same way as " +
                            "'repo create'."},
            {"repo update",
                    "repo update <from-mib-dir> <repo-dir>",
                    "Compile all MIB files found (recursively) in <from-mib-dir> into the " +
                            "existing repository <repo-dir>. Existing modules are overwritten."},
            {"repo check",
                    "repo check <repo-dir>",
                    "Check the repository <repo-dir> and report all modules together with " +
                            "their imports. Imports that cannot be resolved within the " +
                            "repository (and are not well known built-in modules) are flagged."},
            {"repo remove",
                    "repo remove <mib-module-name> [...]",
                    "Remove the given MIB module(s) from the repository. The repository " +
                            "directory is taken from the -M option or from the '" +
                            CFG_REPOSITORY + "' property in the configuration file."},
            {"mib find",
                    "mib find <oid-or-objectname> [<module-name>]",
                    "Find and print the SMI object identified by the given OID (e.g. " +
                            "1.3.6.1.2.1.1.1.0) or object name (e.g. sysDescr). An optional " +
                            "module name disambiguates the lookup by name. The repository " +
                            "directory is taken from the -M option or from the '" +
                            CFG_REPOSITORY + "' property in the configuration file."},
            {"help",
                    "help [<subject>]",
                    "Print general help or, if a subject (command) is given, detailed help " +
                            "for that command."},
            {"version",
                    "version",
                    "Print the version of this tool."}
    };

    /** Synopsis and description per option letter. */
    private static final String[][] OPTION_HELP = {
            {"L", "<license-key>",
                    "SNMP4J-SMI-PRO license string and license key. Overrides the values " +
                            "from the configuration file."},
            {"c", "<config-file>",
                    "Properties configuration file to read the license and the default " +
                            "repository from (default: " + DEFAULT_CONFIG_FILE + " in the " +
                            "current working directory or in the user's home directory)."},
            {"M", "<repo-dir>",
                    "MIB repository directory used by the 'repo remove' and 'mib find' " +
                            "commands."},
            {"r", "",
                    "On 'repo create' / 'repo create2pass': replace (delete) an existing " +
                            "target repository directory instead of renaming it to a " +
                            "timestamped backup (the default)."},
            {"v", "",
                    "Verbose output: print the compilation progress."}
    };

    private static final int STDOUT_WIDTH = 79;

    private int returnCode = 0;

    public static void main(String[] args) {
        MibManager mibManager = new MibManager();
        int rc = mibManager.run(args);
        System.exit(rc);
    }

    public int run(String[] args) {
        if (args.length == 0) {
            System.out.println(help(null));
            return 1;
        }
        // First pass: determine the (possibly two word) command using a
        // permissive parameter format so the actual options are skipped.
        String command;
        String subcommand;
        try {
            ArgumentParser pre =
                    new ArgumentParser(COMMON_OPTIONS, "#command[s] +sub[s] +rest[s] ..");
            Map<String, List<Object>> p = pre.parse(args);
            command = (String) ArgumentParser.getValue(p, "command", 0);
            subcommand = (String) ArgumentParser.getValue(p, "sub", 0);
        } catch (ParseException ex) {
            System.err.println("Error: " + ex.getMessage());
            System.out.println(help(null));
            return 2;
        }

        if ("help".equals(command)) {
            System.out.println(help(subcommand));
            return 0;
        }
        if ("version".equals(command)) {
            printVersion();
            return 0;
        }

        String key = ("repo".equals(command) || "mib".equals(command))
                ? command + " " + subcommand : command;
        String[] format = selectFormat(key);
        if (format == null) {
            System.err.println("Error: unknown command '" +
                    (command == null ? "" : command) +
                    (subcommand == null ? "" : " " + subcommand) + "'");
            System.out.println(help(null));
            return 2;
        }

        Map<String, List<Object>> params;
        try {
            params = new ArgumentParser(format[1], format[2]).parse(args);
        } catch (ArgumentParseException apex) {
            System.err.println("Error: " + apex.getMessage());
            System.out.println(help(key));
            return 2;
        } catch (ParseException ex) {
            System.err.println("Error: " + ex.getMessage());
            System.out.println(help(key));
            return 2;
        }

        Properties config = loadConfig(params);
        String licenseString = resolveLicense(params, config);
        if (licenseString == null) {
            System.err.println("Error: no license information. Provide it with " +
                    "'-L <license-key>' or in the configuration file '" +
                    DEFAULT_CONFIG_FILE + "'.");
            return 1;
        }
        boolean verbose = params.containsKey("v");
        boolean replace = params.containsKey("r");

        try {
            switch (key) {
                case "repo create":
                    repoCreate(params, licenseString, false, verbose, replace);
                    break;
                case "repo create2pass":
                    repoCreate(params, licenseString, true, verbose, replace);
                    break;
                case "repo update":
                    repoUpdate(params, licenseString, false, verbose);
                    break;
                case "repo check":
                    repoCheck(params, licenseString);
                    break;
                case "repo remove":
                    repoRemove(params, config, licenseString);
                    break;
                case "mib find":
                    mibFind(params, config, licenseString);
                    break;
                default:
                    System.out.println(help(null));
                    return 2;
            }
        } catch (IllegalArgumentException iaex) {
            System.err.println("Error: " + iaex.getMessage());
            return 1;
        } catch (IOException ioex) {
            System.err.println("Error: " + ioex.getMessage());
            return 3;
        }
        return returnCode;
    }

    // ------------------------------------------------------------------
    // Commands
    // ------------------------------------------------------------------

    /**
     * Creates a new repository. If the target directory already exists it is,
     * by default, renamed to a timestamped backup. When {@code replaceRepo} is
     * {@code true} the existing target directory is deleted instead.
     */
    private void repoCreate(Map<String, List<Object>> params,
                            String licenseString, boolean create2pass, boolean verbose,
                            boolean replaceRepo)
            throws IOException {
        File fromMibDir = new File((String) ArgumentParser.getValue(params, "fromMibDir", 0));
        File newRepoDir = new File((String) ArgumentParser.getValue(params, "newRepoDir", 0));
        if (!fromMibDir.exists()) {
            throw new IllegalArgumentException("MIB source '" + fromMibDir +
                    "' does not exist");
        }
        if (newRepoDir.exists()) {
            if (replaceRepo) {
                deleteRecursively(newRepoDir);
                System.out.println("Existing repository deleted: " + newRepoDir);
            } else {
                File backup = new File(newRepoDir.getParentFile(),
                        newRepoDir.getName() + "." + ISO_BACKUP_FORMAT.format(new Date()));
                if (!newRepoDir.renameTo(backup)) {
                    throw new IOException("Could not rename existing repository '" +
                            newRepoDir + "' to backup '" + backup + "'");
                }
                System.out.println("Existing repository backed up to: " + backup);
            }
        }
        if (!newRepoDir.mkdirs()) {
            throw new IOException("Could not create repository directory '" +
                    newRepoDir + "'");
        }
        SmiManager smiManager = new SmiManager(licenseString, newRepoDir);
        System.out.println("Creating repository '" + newRepoDir + "' from '" +
                fromMibDir + "'");
        compile(smiManager, fromMibDir, create2pass, verbose);
    }

    private void repoUpdate(Map<String, List<Object>> params,
                            String licenseString, boolean passTwice, boolean verbose)
            throws IOException {
        File fromMibDir = new File((String) ArgumentParser.getValue(params, "fromMibDir", 0));
        File repoDir = new File((String) ArgumentParser.getValue(params, "repoDir", 0));
        if (!fromMibDir.exists()) {
            throw new IllegalArgumentException("MIB source '" + fromMibDir +
                    "' does not exist");
        }
        if (!repoDir.isDirectory()) {
            throw new IllegalArgumentException("Repository '" + repoDir +
                    "' does not exist. Use 'repo create' to create a new repository.");
        }
        SmiManager smiManager = new SmiManager(licenseString, repoDir);
        System.out.println("Updating repository '" + repoDir + "' from '" +
                fromMibDir + "'");
        compile(smiManager, fromMibDir, passTwice, verbose);
    }

    private void repoCheck(Map<String, List<Object>> params, String licenseString)
            throws IOException {
        File repoDir = new File((String) ArgumentParser.getValue(params, "repoDir", 0));
        if (!repoDir.isDirectory()) {
            throw new IllegalArgumentException("Repository '" + repoDir +
                    "' does not exist");
        }
        // Constructing the SmiManager validates the license and opens the repo.
        SmiManager smiManager = new SmiManager(licenseString, repoDir);

        List<SmiModule> modules = smiManager.loadAllModules();
        if (modules.isEmpty()) {
            System.out.println("Repository '" + repoDir +
                    "' is empty or invalid.");
            return;
        }
        Set<String> present = new HashSet<>();
        for (SmiModule m : modules) {
            present.add(m.getModuleName());
        }
        int unresolvedTotal = 0;
        System.out.println("Repository '" + repoDir + "' contains " +
                modules.size() + " module(s):");
        for (SmiModule info : modules) {
            List<SmiImport> imports = info.getImports();
            List<String> unresolved = new ArrayList<>();
            if (imports != null) {
                for (SmiImport imp : imports) {
                    if (!present.contains(imp.getSourceModuleName()) &&
                            !BUILTIN_MODULES.contains(imp.getSourceModuleName())) {
                        unresolved.add(imp.getSourceModuleName());
                    }
                }
            }
            StringBuilder line = new StringBuilder("  ");
            line.append(info.getModuleName());
            if (info.getCompilerStrictness() == SmiCompiler.Strictness.lenient)
                line.append(" [lenient]");
            if ((imports != null) && (!imports.isEmpty())) {
                line.append(" [IMPORTS: ").append(imports.stream().map(SmiImport::getSourceModuleName).collect(Collectors.joining(", "))).append("]");
            }
            System.out.println(line);
            if (!unresolved.isEmpty()) {
                unresolvedTotal += unresolved.size();
                System.out.println("    ! UNRESOLVED IMPORTS: " +
                        String.join(", ", unresolved));
            }
        }
        if (unresolvedTotal == 0) {
            System.out.println("OK: all imports are resolvable.");
        } else {
            System.out.println("WARNING: " + unresolvedTotal +
                    " unresolved import(s) found.");
            returnCode = 1;
        }
    }

    private void repoRemove(Map<String, List<Object>> params, Properties config,
                            String licenseString) throws IOException {
        File repoDir = getRepositoryDir(params, config);
        @SuppressWarnings("unchecked")
        List<Object> modules = (List<Object>) (List<?>) params.get("module");
        if ((modules == null) || modules.isEmpty()) {
            throw new IllegalArgumentException("No module name(s) given");
        }
        // Validate license and ensure the repository is accessible.
        new SmiManager(licenseString, repoDir);
        DefaultRepositoryManager repMan = new DefaultRepositoryManager();
        repMan.open(repoDir);
        for (Object module : modules) {
            String name = (String) module;
            boolean removed = repMan.removeModule(name);
            if (removed) {
                System.out.println("Removed module '" + name + "'");
            } else {
                System.out.println("Module '" + name +
                        "' was not found in the repository");
                returnCode = 1;
            }
        }
    }

    private void mibFind(Map<String, List<Object>> params, Properties config,
                         String licenseString) throws IOException {
        File repoDir = getRepositoryDir(params, config);
        String target = (String) ArgumentParser.getValue(params, "target", 0);
        String module = (String) ArgumentParser.getValue(params, "module", 0);
        SmiManager smiManager = new SmiManager(licenseString, repoDir);

        SmiObject object;
        if (isOID(target)) {
            object = smiManager.findSmiObject(new OID(target));
        } else {
            object = smiManager.findSmiObject(target, module);
        }
        if (object == null) {
            System.out.println("No SMI object found for '" + target + "'" +
                    (module != null ? " in module '" + module + "'" : ""));
            returnCode = 1;
            return;
        }
        printSmiObject(object, smiManager);
    }

    // ------------------------------------------------------------------
    // Compilation
    // ------------------------------------------------------------------

    private void compile(SmiManager smiManager, File mibSource, boolean runLenient2ndPass, boolean verbose)
            throws IOException {
        List<File> files = collectFiles(mibSource);
        if (files.isEmpty()) {
            throw new IllegalArgumentException("No MIB files found in '" +
                    mibSource + "'");
        }
        List<NamedInputStream> streams = new ArrayList<>(files.size());
        try {
            for (File file : files) {
                streams.add(new NamedInputStream(file));
            }
            CompilationMonitor monitor = createMonitor(verbose);
            long startTime = System.currentTimeMillis();
            List<CompilationResult> results = smiManager.compile(
                    streams.toArray(new NamedInputStream[0]), monitor,
                    SmiCompiler.TargetMode.storeIntoRepositoryNoLoad,
                    SmiCompiler.OverwriteMode.overwriteAlways,
                    (runLenient2ndPass ? SmiCompiler.Strictness.lenientIfStandardFails : SmiCompiler.Strictness.standard),
                    Runtime.getRuntime().availableProcessors());
            long endTime = System.currentTimeMillis();
            printCompilationResults(results, startTime, endTime);
            for (CompilationResult result : results) {
                if (!result.hasErrors()) {
                    files.remove(new File(result.getFileName()));
                }
            }
        } finally {
            closeStreams(streams);
        }
    }

    private static void closeStreams(List<NamedInputStream> streams) {
        for (NamedInputStream stream : streams) {
            try {
                if (stream.getInputStream() != null) {
                    stream.getInputStream().close();
                }
            } catch (IOException ignore) {
                // ignore
            }
        }
    }

    private void printCompilationResults(List<CompilationResult> results, long startTime, long endTime) {
        int ok = 0;
        int failed = 0;
        for (CompilationResult result : results) {
            if (result.hasErrors()) {
                failed++;
            } else {
                ok += result.getModuleNames().size();
            }
        }
        System.out.println("Compiled " + results.size() + " file(s): " +
                ok + " module(s) OK, " + failed + " file(s) with errors in " + (endTime - startTime) + " ms.");
        String lastFile = null;
        for (CompilationResult result : results) {
            String fileName = result.getFileName();
            if (fileName != null) {
                int slash = fileName.lastIndexOf('/');
                if (slash >= 0) {
                    fileName = fileName.substring(slash + 1);
                }
            }
            if ((lastFile == null) || (!lastFile.equals(fileName))) {
                System.out.println("------ " + fileName + " ------");
                lastFile = fileName;
            }
            if (result.hasErrors()) {
                List<SmiError> errors = result.getSmiErrorList();
                for (int i = 0; i < errors.size(); i++) {
                    SmiError error = errors.get(i);
                    System.out.println("  #" + (i + 1) + " (" + error.getRow() +
                            ":" + error.getColumn() + "): " + error.getMessage());
                }
            } else {
                System.out.println("  OK: " + result.getModuleNames());
            }
        }
        if (failed > 0) {
            returnCode = failed;
        }
        else {
            // reset return code to 0 if all files were compiled successfully (2pass case).
            returnCode = 0;
        }
    }

    private CompilationMonitor createMonitor(final boolean verbose) {
        return new CompilationMonitor() {
            @Override
            public boolean loadingProgress(String fileName, int current, int maxCount) {
                if (verbose) {
                    System.out.println("Loading (" + current + "/" + maxCount + "): " +
                            fileName);
                }
                return true;
            }

            @Override
            public boolean sortingProgress(String fileName, int current, int maxCount) {
                return true;
            }

            public boolean sorted(List<SmiModuleImport> sortedModuleNamesWithDependencies, int failedOffset,
                                  Set<String> missingModules) {
                System.out.println("Sorted modules (failed offset " + (failedOffset) + "): ");
                int i = 1;
                for (SmiModuleImport module : sortedModuleNamesWithDependencies) {
                    System.out.println("[" + i++ + "] " + module);
                }
                if (failedOffset > 0) {
                    System.out.println("WARNING: " + failedOffset +
                            " module(s) could not be fully sorted, the missing modules are: " + missingModules);
                    System.out.print("Continue with compilation? (Y to continue, any other key to stop): ");
                    Scanner scanner = new Scanner(System.in);
                    String response = scanner.nextLine().trim();
                    if (!"Y".equals(response)) {
                        System.out.println("Compilation stopped by user.");
                        return false;
                    }
                }
                return true;
            }

            @Override
            public boolean compilationProgress(String moduleName, int current, int maxCount,
                                               SmiCompiler.Strictness strictness) {
                if (verbose) {
                    System.out.println("Compiling (" + current + "/" + maxCount + "): " +
                            moduleName + " [" + strictness + "]");
                }
                return true;
            }
        };
    }

    private List<File> collectFiles(File source) {
        List<File> result = new ArrayList<>();
        if (source.isDirectory()) {
            File[] children = source.listFiles();
            if (children != null) {
                Arrays.sort(children);
                for (File child : children) {
                    if (child.isDirectory()) {
                        result.addAll(collectFiles(child));
                    } else if (child.isFile() && !child.isHidden()) {
                        result.add(child);
                    }
                }
            }
        } else if (source.isFile()) {
            result.add(source);
        }
        return result;
    }

    /**
     * Recursively deletes the given file or directory and all its contents.
     *
     * @throws IOException
     *    if any file or directory could not be deleted.
     */
    private static void deleteRecursively(File file) throws IOException {
        File[] children = file.listFiles();
        if (children != null) {
            for (File child : children) {
                deleteRecursively(child);
            }
        }
        if (!file.delete()) {
            throw new IOException("Could not delete '" + file + "'");
        }
    }

    // ------------------------------------------------------------------
    // Output helpers
    // ------------------------------------------------------------------

    private void printSmiObject(SmiObject object, SmiManager smiManager) {
        System.out.println("Object name : " + object.getObjectName());
        System.out.println("OID         : " + object.getOID());
        System.out.println("SMI type    : " + object.getType());
        SmiSyntax syntax = object.getSyntax();
        if (syntax != null) {
            System.out.println("Syntax      : " + syntax.getSyntaxClause());
        }
        if (object instanceof SmiObjectType objectType) {
            if (objectType.getMaxAccess() != null) {
                System.out.println("Max-Access  : " + objectType.getMaxAccess());
            }
            if ((objectType.getUnits() != null) && !objectType.getUnits().isEmpty()) {
                System.out.println("Units       : " + objectType.getUnits());
            }
        }
        if (object.getStatus() != null) {
            System.out.println("Status      : " + object.getStatus());
        }
        List<SmiModule> modules = smiManager.findSmiModules(object.getOID());
        if ((modules != null) && !modules.isEmpty()) {
            List<String> names = new ArrayList<>();
            for (SmiModule m : modules) {
                names.add(m.getModuleName());
            }
            System.out.println("Module(s)   : " + String.join(", ", names));
        }
        if (object.getDescription() != null) {
            System.out.println("Description :");
            System.out.println(format(object.getDescription().trim(), 2));
        }
        if ((object.getReference() != null) && !object.getReference().trim().isEmpty()) {
            System.out.println("Reference   :");
            System.out.println(format(object.getReference().trim(), 2));
        }
    }

    private void printVersion() {
        System.out.println(TOOL_NAME + " " + TOOL_VERSION +
                " - SNMP4J-SMI-PRO MIB repository manager");
        System.out.println("Copyright (C) 2012-2026 Frank Fock");
    }

    // ------------------------------------------------------------------
    // Configuration / license
    // ------------------------------------------------------------------

    private Properties loadConfig(Map<String, List<Object>> params) {
        Properties config = new Properties();
        String configName = (String) ArgumentParser.getValue(params, "c", 0);
        if (configName == null) {
            configName = DEFAULT_CONFIG_FILE;
        }
        File configFile = new File(configName);
        if (!configFile.exists()) {
            // fall back to the user's home directory
            File home = new File(System.getProperty("user.home"), configName);
            if (home.exists()) {
                configFile = home;
            }
        }
        if (configFile.exists()) {
            try (java.io.FileInputStream in = new java.io.FileInputStream(configFile)) {
                config.load(in);
            } catch (IOException ex) {
                System.err.println("Warning: could not read configuration file '" +
                        configFile + "': " + ex.getMessage());
            }
        }
        return config;
    }

    private String resolveLicense(Map<String, List<Object>> params,
                                  Properties config) {
        String license = null;
        String key = null;
        if (params.containsKey("L")) {
            license = (String) ArgumentParser.getValue(params, "L", 0);
        }
        if (license == null) {
            license = config.getProperty(CFG_LICENSE);
        }
        if ((license == null) ||
                license.trim().isEmpty()) {
            return null;
        }
        return license.trim();
    }

    private File getRepositoryDir(Map<String, List<Object>> params,
                                 Properties config) {
        String repo = (String) ArgumentParser.getValue(params, "M", 0);
        if (repo == null) {
            repo = config.getProperty(CFG_REPOSITORY);
        }
        if ((repo == null) || repo.trim().isEmpty()) {
            throw new IllegalArgumentException("No repository directory given. " +
                    "Use the -M option or set the '" + CFG_REPOSITORY +
                    "' property in the configuration file.");
        }
        File repoDir = new File(repo.trim());
        if (!repoDir.isDirectory()) {
            throw new IllegalArgumentException("Repository '" + repoDir +
                    "' does not exist");
        }
        return repoDir;
    }

    private boolean isOID(String target) {
        return (target != null) && target.matches("\\.?\\d+(\\.\\d+)*");
    }

    // ------------------------------------------------------------------
    // Help (concept based on org.snmp4j.clt.Browser)
    // ------------------------------------------------------------------

    private String[] selectFormat(String key) {
        for (String[] command : COMMANDS) {
            if (command[0].equals(key)) {
                return command;
            }
        }
        return null;
    }

    private String[] selectHelp(String key) {
        for (String[] entry : HELP) {
            if (entry[0].equals(key)) {
                return entry;
            }
        }
        return null;
    }

    public String help(String subject) {
        StringBuilder buf = new StringBuilder();
        if ((subject == null) || subject.isEmpty() || "all".equals(subject)) {
            buf.append(TOOL_NAME).append(' ').append(TOOL_VERSION)
                    .append(" - SNMP4J-SMI-PRO MIB repository manager\n\n");
            buf.append("Usage: ").append(TOOL_NAME).append(" <command> [options] [parameters]\n\n");
            buf.append("Commands:\n");
            for (String[] entry : HELP) {
                buf.append("  ").append(entry[1]).append('\n');
                buf.append(format(entry[2], 6)).append('\n');
            }
            buf.append("\nOptions:\n");
            appendOptions(buf);
        } else {
            // allow "repo" / "mib" to map to their first sub command help group
            String[] entry = selectHelp(subject);
            if (entry == null) {
                // maybe the user asked for "repo" or "mib": list matching commands
                boolean any = false;
                for (String[] h : HELP) {
                    if (h[0].startsWith(subject + " ")) {
                        buf.append("  ").append(h[1]).append('\n');
                        buf.append(format(h[2], 6)).append('\n');
                        any = true;
                    }
                }
                if (!any) {
                    buf.append("No help available for '").append(subject).append("'.\n");
                }
                buf.append("\nOptions:\n");
                appendOptions(buf);
                return buf.toString();
            }
            buf.append(entry[1]).append("\n\n");
            buf.append(format(entry[2], 2)).append("\n\n");
            buf.append("Options:\n");
            appendOptions(buf);
        }
        return buf.toString();
    }

    private void appendOptions(StringBuilder buf) {
        TreeMap<String, String[]> sorted = new TreeMap<>();
        for (String[] option : OPTION_HELP) {
            sorted.put(option[0], option);
        }
        for (String[] option : sorted.values()) {
            String head = "  -" + option[0] +
                    (option[1].isEmpty() ? "" : " " + option[1]);
            buf.append(head).append('\n');
            buf.append(format(option[2], 6)).append('\n');
        }
    }

    /**
     * Word-wraps the given text at {@link #STDOUT_WIDTH} columns and indents
     * every line by {@code indent} spaces.
     */
    private static String format(String text, int indent) {
        StringBuilder buf = new StringBuilder();
        StringBuilder pad = new StringBuilder();
        pad.repeat(" ", Math.max(0, indent));
        int lineLength = indent;
        buf.append(pad);
        for (String word : text.split("\\s+")) {
            if (word.isEmpty()) {
                continue;
            }
            if ((lineLength > indent) && (lineLength + 1 + word.length() > STDOUT_WIDTH)) {
                buf.append('\n').append(pad);
                lineLength = indent;
            } else if (lineLength > indent) {
                buf.append(' ');
                lineLength++;
            }
            buf.append(word);
            lineLength += word.length();
        }
        return buf.toString();
    }
}
