/**
 *
 * 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.apache.karaf.tooling.cmdhelp;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.felix.gogo.commands.Action;
import org.apache.felix.gogo.commands.Argument;
import org.apache.felix.gogo.commands.Command;
import org.apache.felix.gogo.commands.Option;
import org.apache.felix.gogo.commands.basic.ActionPreparator;
import org.apache.karaf.shell.console.commands.BlueprintCommand;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.xbean.finder.ClassFinder;
import org.apache.felix.service.command.CommandSession;
import org.fusesource.jansi.Ansi;

/**
 * Generates help (in scalate or docbook format) for Karaf commands
 *
 * @goal cmdhelp
 * @phase generate-resources
 * @execute phase="generate-resources"
 * @requiresDependencyResolution runtime
 * @inheritByDefault false
 * @description Generates help for Karaf commands
 */
public class CmdHelpMojo extends AbstractMojo {

    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     */
    protected MavenProject project;

    /**
     * The output folder
     *
     * @parameter default-value="${project.build.directory}/docbkx/sources"
     */
    protected File targetFolder;

    /**
     * The output format
     *
     * @parameter default-value="docbx";
     */
    protected String format;

    /**
     * The classloader to use for loading the commands
     *
     * @parameter default-value="project"
     */
    protected String classLoader;

    /**
     * Adds the --help option to every commands documentation
     *
     * @parameter default-value="true"
     */
    protected boolean includeHelpOption;

    private static final String FORMAT_CONF = "conf";
    private static final String FORMAT_DOCBX = "docbx";

    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            if (!FORMAT_DOCBX.equals(format) && !FORMAT_CONF.equals(format)) {
                throw new MojoFailureException("Unsupported format: " + format + ". Supported formats are: docbx or conf.");
            }
            if (!targetFolder.exists()) {
                targetFolder.mkdirs();
            }

            ClassFinder finder;
            if ("project".equals(classLoader)) {
                List<URL> urls = new ArrayList<URL>();
                for (Object object : project.getCompileClasspathElements()) {
                    String path = (String) object;
                    urls.add(new File(path).toURI().toURL());
                }
                ClassLoader loader = new URLClassLoader(urls.toArray(new URL[urls.size()]), getClass().getClassLoader());
                finder = new ClassFinder(loader, urls);
            } else if ("plugin".equals(classLoader)) {
                finder = new ClassFinder(getClass().getClassLoader());
            } else {
                throw new MojoFailureException("classLoader attribute must be 'project' or 'plugin'");
            }
            List<Class<?>> classes = finder.findAnnotatedClasses(Command.class);
            if (classes.isEmpty()) {
                throw new MojoFailureException("No command found");
            }

            Map<String, Set<String>> commands = new TreeMap<String, Set<String>>();

            for (Class clazz : classes) {
                try {
                    String help = new HelpPrinter(clazz).printHelp(format, includeHelpOption);
                    Command cmd = (Command) clazz.getAnnotation(Command.class);
                    File output = null;

                    // skip the *-help command
                    if (cmd.scope().equals("*")) continue;

                    if (FORMAT_DOCBX.equals(format)) {
                        output = new File(targetFolder, cmd.scope() + "-" + cmd.name() + ".xml");
                    } else if (FORMAT_CONF.equals(format)) {
                        output = new File(targetFolder, cmd.scope() + "-" + cmd.name() + ".conf");
                    }
                    Writer writer = new OutputStreamWriter(new FileOutputStream(output));
                    writer.write(help);
                    writer.close();

                    Set<String> cmds = commands.get(cmd.scope());
                    if (cmds == null) {
                        cmds = new TreeSet<String>();
                        commands.put(cmd.scope(), cmds);
                    }
                    cmds.add(cmd.name());
                    getLog().info("Found command: " + cmd.scope() + ":" + cmd.name());
                } catch (Exception e) {
                    getLog().warn("Unable to write help for " + clazz.getName(), e);
                }
            }

            if (FORMAT_DOCBX.equals(format)) {
                PrintStream writer = new PrintStream(new FileOutputStream(new File(targetFolder, "commands.xml")));
                writer.println("<chapter id='commands' xmlns:xi=\"http://www.w3.org/2001/XInclude\">");
                writer.println("  <title>Commands</title>");
                writer.println("  <toc></toc>");
                for (String key : commands.keySet()) {
                    writer.println("  <section id='commands-" + key + "'>");
                    writer.println("    <title>" + key + "</title>");
                    for (String cmd : commands.get(key)) {
                        writer.println("    <xi:include href='" + key + "-" + cmd + ".xml' parse='xml'/>");
                    }
                    writer.println("  </section>");
                }
                writer.println("</chapter>");
                writer.close();
            } else if (FORMAT_CONF.equals(format)) {
                PrintStream writer = new PrintStream(new FileOutputStream(new File(targetFolder, "commands.conf")));
                writer.println("h1. Commands");
                writer.println();
                for (String key : commands.keySet()) {
                    writer.println("h2. " + key);
                    writer.println();
                    for (String cmd : commands.get(key)) {
                        writer.println("* [" + key + ":" + cmd + "|" + key + "-" + cmd + "]");
                    }
                    writer.println();
                }
                writer.close();
            }

        } catch (Exception e) {
            throw new MojoExecutionException("Error building commands help", e);
        }
    }

    public static class HelpPrinter extends BlueprintCommand {

        private final Class<Action> actionClass;

        public HelpPrinter(Class<Action> actionClass) {
            this.actionClass = actionClass;
        }

        public String printHelp(String format, boolean includeHelpOption) throws Exception {
            PrintStream oldout = System.out;
            try {
                Action action = actionClass.newInstance();
                CommandSession session = new DummyCommandSession();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                PrintStream newout = new PrintStream(baos);
                System.setOut(newout);
                ActionPreparator preparator;
                if (FORMAT_DOCBX.equals(format)) {
                    preparator = new DocbxPreparator(includeHelpOption);
                } else {
                    preparator = new ConfPreparator(includeHelpOption);
                }
                preparator.prepare(action, session, Collections.<Object>singletonList("--help"));
                newout.close();
                baos.close();
                return baos.toString();
            } finally {
                System.setOut(oldout);
            }
        }

        protected class DocbxPreparator extends BlueprintActionPreparator {

            boolean includeHelpOption;

            DocbxPreparator(boolean includeHelpOption) {
                this.includeHelpOption = includeHelpOption;
            }

            @Override
            protected void printUsage(CommandSession session, Action action, Map<Option,Field> optionsMap, Map<Argument,Field> argsMap, PrintStream out)
            {
                Command command = action.getClass().getAnnotation(Command.class);
                List<Argument> arguments = new ArrayList<Argument>(argsMap.keySet());
                Collections.sort(arguments, new Comparator<Argument>() {
                    public int compare(Argument o1, Argument o2) {
                        return Integer.valueOf(o1.index()).compareTo(Integer.valueOf(o2.index()));
                    }
                });
                Set<Option> options = new HashSet<Option>(optionsMap.keySet());
                if (includeHelpOption) options.add(HELP);

                out.println("<section>");
                out.print("  <title>");
                out.print(command.scope());
                out.print(":");
                out.print(command.name());
                out.println("</title>");
                out.println("  <section>");
                out.println("    <title>Description</title>");
                out.println("    <para>");
                out.println(command.description());
                out.println("    </para>");
                out.println("  </section>");

                StringBuffer syntax = new StringBuffer();
                syntax.append(String.format("%s:%s", command.scope(), command.name()));
                if (options.size() > 0) {
                    syntax.append(" [options]");
                }
                if (arguments.size() > 0) {
                    syntax.append(' ');
                    for (Argument argument : arguments) {
                        syntax.append(String.format(argument.required() ? "%s " : "[%s] ", argument.name()));
                    }
                }
                out.println("  <section>");
                out.println("    <title>Syntax</title>");
                out.println("    <para>");
                out.println(syntax.toString());
                out.println("    </para>");
                out.println("  </section>");

                if (arguments.size() > 0)
                {
                    out.println("  <section>");
                    out.println("    <title>Arguments</title>");
                    out.println("    <informaltable>");
                    for (Argument argument : arguments)
                    {
                        out.println("    <tr>");
                        out.println("      <td>" + argument.name() + "</td>");
                        String description = argument.description();
                        if (!argument.required()) {
                            try {
                                argsMap.get(argument).setAccessible(true);
                                Object o = argsMap.get(argument).get(action);
                                if (o != null
                                    && (!(o instanceof Boolean) || ((Boolean) o))
                                    && (!(o instanceof Number) || ((Number) o).doubleValue() != 0.0)) {
                                    description += " (defaults to " + o.toString() + ")";
                                }
                            } catch (Exception e) {
                                // Ignore
                            }
                        }
                        out.println("     <td>" + description + "</td>");
                        out.println("    </tr>");
                    }

                    out.println("    </informaltable>");
                    out.println("  </section>");
                }
                if (options.size() > 0)
                {
                    out.println("  <section>");
                    out.println("    <title>Options</title>");
                    out.println("    <informaltable>");

                    for (Option option : options)
                    {
                        String opt = option.name();
                        String description = option.description();
                        for (String alias : option.aliases())
                        {
                            opt += ", " + alias;
                        }
                        try {
                            optionsMap.get(option).setAccessible(true);
                            Object o = optionsMap.get(option).get(action);
                            if (o != null
                                && (!(o instanceof Boolean) || ((Boolean) o))
                                && (!(o instanceof Number) || ((Number) o).doubleValue() != 0.0)) {
                                description += " (defaults to " + o.toString() + ")";
                            }
                        } catch (Exception e) {
                            // Ignore
                        }
                        out.println("    <tr>");
                        out.println("      <td>" + opt + "</td>");
                        out.println("      <td>" + description + "</td>");
                        out.println("    </tr>");
                    }

                    out.println("    </informaltable>");
                    out.println("  </section>");
                }

                if(command.detailedDescription() != null
                    && command.detailedDescription().trim().length() > 0) {
                    out.println("<section>");
                    out.println("    <title>Details</title>");
                    String description = loadDescription(action.getClass(), command.detailedDescription());
                    out.println("    <para>");
                    out.println(description);
                    out.println("    </para>");
                    out.println("</section>");
                }
                out.println("</section>");
            }

        }

        protected class ConfPreparator extends BlueprintActionPreparator {

            boolean includeHelpOption;

            ConfPreparator(boolean includeHelpOption) {
                this.includeHelpOption = includeHelpOption;
            }

            @Override
            protected void printUsage(CommandSession session, Action action, Map<Option, Field> optionsMap, Map<Argument,Field> argsMap, PrintStream out)
            {
                Command command = action.getClass().getAnnotation(Command.class);
                List<Argument> arguments = new ArrayList<Argument>(argsMap.keySet());
                Collections.sort(arguments, new Comparator<Argument>() {
                    public int compare(Argument o1, Argument o2) {
                        return Integer.valueOf(o1.index()).compareTo(Integer.valueOf(o2.index()));
                    }
                });
                Set<Option> options = new HashSet<Option>(optionsMap.keySet());
                if (includeHelpOption) options.add(HELP);

                out.println("h1. " + command.scope() + ":" + command.name());
                out.println();

                out.println("h2. Description");
                out.println(command.description());
                out.println();

                StringBuffer syntax = new StringBuffer();
                syntax.append(String.format("%s:%s", command.scope(), command.name()));
                if (options.size() > 0) {
                    syntax.append(" \\[options\\]");
                }
                if (arguments.size() > 0) {
                    syntax.append(' ');
                    for (Argument argument : arguments) {
                        syntax.append(String.format(argument.required() ? "%s " : "\\[%s\\] ", argument.name()));
                    }
                }
                out.println("h2. Syntax");
                out.println(syntax.toString());
                out.println();

                if (arguments.size() > 0)
                {
                    out.println("h2. Arguments");
                    out.println("|| Name || Description ||");
                    for (Argument argument : arguments)
                    {
                        String description = argument.description();
                        if (!argument.required()) {
                            try {
                                argsMap.get(argument).setAccessible(true);
                                Object o = argsMap.get(argument).get(action);
                                if (o != null
                                        && (!(o instanceof Boolean) || ((Boolean) o))
                                        && (!(o instanceof Number) || ((Number) o).doubleValue() != 0.0)) {
                                    description += " (defaults to " + o.toString() + ")";
                                }
                            } catch (Throwable t) {
                                // Ignore
                            }
                        }
                        out.println("| " + argument.name() + " | " + description + " |");
                    }
                    out.println();
                }
                if (options.size() > 0)
                {
                    out.println("h2. Options");
                    out.println("|| Name || Description ||");
                    for (Option option : options)
                    {
                        String opt = option.name();
                        String desc = option.description();
                        for (String alias : option.aliases())
                        {
                            opt += ", " + alias;
                        }
                        try {
                            optionsMap.get(option).setAccessible(true);
                            Object o = optionsMap.get(option).get(action);
                            if (o != null
                                    && (!(o instanceof Boolean) || ((Boolean) o))
                                    && (!(o instanceof Number) || ((Number) o).doubleValue() != 0.0)) {
                                desc += " (defaults to " + o.toString() + ")";
                            }
                        } catch (Throwable t) {
                            // Ignore
                        }
                        out.println("| " + opt + " | " + desc + " |");
                    }
                    out.println();
                }
                if (command.detailedDescription().length() > 0) {
                    out.println("h2. Details");
                    String desc = loadDescription(action.getClass(), command.detailedDescription());
                    out.println(desc);
                }
                out.println();
            }

        }

        protected static class DummyCommandSession implements CommandSession {
            public Object convert(Class<?> type, Object instance) {
                return null;
            }
            public CharSequence format(Object target, int level) {
                return null;
            }
            public void put(String name, Object value) {
            }
            public Object get(String name) {
                return null;
            }
            public PrintStream getConsole() {
                return null;
            }
            public InputStream getKeyboard() {
                return null;
            }
            public void close() {
            }
            public Object execute(CharSequence commandline) throws Exception {
                return null;
            }
        }

    }

}
