// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2012 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---

package com.netscape.cmstools.cli;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.lang.StringUtils;

import com.netscape.certsrv.client.ClientConfig;
import com.netscape.certsrv.client.PKIClient;


/**
 * @author Endi S. Dewata
 */
public class CLI {

    public static boolean verbose;

    public static CommandLineParser parser = new PosixParser();
    public static HelpFormatter formatter = new HelpFormatter();

    public String name;
    public String description;
    public CLI parent;

    public Options options = new Options();
    public Map<String, CLI> modules = new LinkedHashMap<String, CLI>();

    public PKIClient client;

    public CLI(String name, String description) {
        this(name, description, null);
    }

    public CLI(String name, String description, CLI parent) {
        this.name = name;
        this.description = description;
        this.parent = parent;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getFullName() {
        if (parent == null) {
            return name;
        } else {
            return parent.getFullName() + "-" + name;
        }
    }

    public String getFullModuleName(String moduleName) {
        return getFullName() + "-" + moduleName;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public CLI getParent() {
        return parent;
    }

    public boolean isDeprecated() {
        return getClass().getAnnotation(Deprecated.class) != null;
    }

    public Collection<CLI> getModules() {
        return modules.values();
    }

    public CLI getModule(String name) {
        return modules.get(name);
    }

    public void addModule(CLI module) {
        modules.put(module.getName(), module);
    }

    public CLI removeModule(String name) {
        return modules.remove(name);
    }

    /**
     * Find the list of modules that handle the specified command.
     */
    public List<CLI> findModules(String command) throws Exception {

        List<CLI> results = new ArrayList<CLI>();

        // split command into list of names:
        // <names[0]>-<names[1]>-<names[2]>-...-<names[n-1]>
        String[] names = command.split("-");

        CLI current = this;
        int i = 0;

        // translate all names into modules starting from the beginning
        while (i < names.length) {

            String moduleName = null;
            CLI module = null;
            int j = i;

            // find module that matches the shortest sequence of names
            while (j < names.length) {

                // construct module name
                if (moduleName == null) {
                    moduleName = names[j];
                } else {
                    moduleName = moduleName + "-" + names[j];
                }

                // find module with name <names[i]>-...-<names[j]>
                module = current.getModule(moduleName);

                if (module != null) {
                    // module found, stop
                    break;
                }

                // try again with longer sequence
                j++;
            }

            if (module == null)
                throw new Exception("Invalid module \"" + moduleName + "\".");

            // module found
            results.add(module);

            // repeat for the remaining parts
            current = module;
            i = j + 1;
        }

        return results;
    }

    /**
     * Find the last module that handles the specified command.
     */
    public CLI findModule(String command) throws Exception {
        List<CLI> modules = findModules(command);
        return modules.get(modules.size() - 1);
    }

    public String getManPage() {
        return null;
    }

    public ClientConfig getConfig() throws Exception {
        if (parent != null) return parent.getConfig();
        return null;
    }

    public PKIClient getClient() throws Exception {
        if (parent != null) return parent.getClient();
        return null;
    }

    public Object getClient(String name) throws Exception {
        if (parent != null) return parent.getClient(name);
        return null;
    }

    public Collection<CLI> getDeprecatedModules() {
        Collection<CLI> list = new ArrayList<CLI>();
        for (CLI module : modules.values()) {
            if (!module.isDeprecated()) continue;
            list.add(module);
        }
        return list;
    }

    public void printHelp() {

        int leftPadding = 1;
        int rightPadding = 25;

        System.out.println("Commands:");

        for (CLI module : modules.values()) {
            if (module.isDeprecated()) continue;

            String label = module.getFullName();

            int padding = rightPadding - leftPadding - label.length();
            if (padding < 1)
                padding = 1;

            System.out.print(StringUtils.repeat(" ", leftPadding));
            System.out.print(label);
            System.out.print(StringUtils.repeat(" ", padding));
            System.out.println(module.getDescription());
        }

        Collection<CLI> deprecatedModules = getDeprecatedModules();

        if (!deprecatedModules.isEmpty()) {
            System.out.println();
            System.out.println("Deprecated:");

            for (CLI module : deprecatedModules) {
                String label = module.getFullName();

                int padding = rightPadding - leftPadding - label.length();
                if (padding < 1)
                    padding = 1;

                System.out.print(StringUtils.repeat(" ", leftPadding));
                System.out.print(label);
                System.out.print(StringUtils.repeat(" ", padding));
                System.out.println(module.getDescription());
            }
        }
    }

    public void execute(String[] args) throws Exception {

        if ((args.length == 0) || (args[0].equals("--help"))) {
            // Print commands associated with this module
            printHelp();
            return;
        }

        // TODO: Rewrite using findModules().

        // A command consists of parts joined by dashes: <part 1>-<part 2>-...-<part N>.
        // For example: cert-request-find
        String command = args[0];

        // The command will be split into module name and sub command, for example:
        //  - module name: cert
        //  - sub command: request-find
        String moduleName = null;
        String subCommand = null;

        // Search the module by incrementally adding parts into module name.
        // Repeat until it finds the module or until there is no more parts to add.
        CLI module = null;
        int position = 0;

        while (true) {

            // Find the next dash.
            int i = command.indexOf('-', position);
            if (i >= 0) {
                // Dash found. Split command into module name and sub command.
                moduleName = command.substring(0, i);
                subCommand = command.substring(i+1);

            } else {
                // Dash not found. Use the whole command.
                moduleName = command;
                subCommand = null;
            }

            // Find module with that name.
            CLI m = getModule(moduleName);

            if (m != null) {
                // Module found. Check sub command.
                if (subCommand == null) {
                    // No sub command. Use this module.
                    module = m;
                    break;
                }

                // There is a sub command. It must be processed by module's children.
                if (!m.getModules().isEmpty()) {
                    // Module has children. Use this module.
                    module = m;
                    break;
                }

                // Module doesn't have children. Keep looking.
            }

            // If there's no more dashes, stop.
            if (i < 0) break;

            position = i + 1;
        }

        if (module == null) {
            throw new Exception("Invalid module \"" + getFullModuleName(moduleName) + "\".");
        }

        if (verbose) System.out.println("Module: " + moduleName);

        // Prepare module arguments.
        String[] moduleArgs;
        if (subCommand != null) {
            // If module command exists, include it as arguments: <module command> <args>...
            moduleArgs = new String[args.length];
            moduleArgs[0] = subCommand;
            System.arraycopy(args, 1, moduleArgs, 1, args.length-1);

        } else {
            // Otherwise, pass the original arguments: <args>...
            moduleArgs = new String[args.length-1];
            System.arraycopy(args, 1, moduleArgs, 0, args.length-1);
        }

        // Add "--help" option to all command modules
        module.options.addOption(null, "help", false, "Show help options");

        module.execute(moduleArgs);
    }

    public static boolean isVerbose() {
        return verbose;
    }

    public static void setVerbose(boolean verbose) {
        CLI.verbose = verbose;
    }

    public void runExternal(List<String> command) throws CLIException, IOException, InterruptedException {
        String[] array = command.toArray(new String[command.size()]);
        runExternal(array);
    }

    public void runExternal(String[] command) throws CLIException, IOException, InterruptedException {

        if (verbose) {

            System.out.print("External command:");

           for (String c : command) {

               boolean quote = c.contains(" ");

               System.out.print(" ");

               if (quote) System.out.print("\"");
               System.out.print(c);
               if (quote) System.out.print("\"");
           }

           System.out.println();
        }

        Runtime rt = Runtime.getRuntime();
        Process p = rt.exec(command);
        int rc = p.waitFor();

        if (rc != 0) {
            throw new CLIException("External command failed. RC: " + rc, rc);
        }
    }
}
