public interface ParameterMetadata {
    enum ParamType {
        /**
         * {@see Environment} or stdout, stderr, stdin.
         */
        INTERNAL,

        /**
         * type matches the service registry of the {@see Environment}.
         */
        SERVICE,

        /**
         * a parameter without {@see org.tomitribe.crest.api.Option}.
         */
        PLAIN,

        /**
         * a parameter with an option, ie a name.
         */
        OPTION,

        /**
         * a bean option parameter.
         */
        BEAN_OPTION
    }

    /**
     * @return the parameter type.
     */
    ParamType getType();

    /**
     * @return the name of the parameter.
     */
    String getName();

    /**
     * @return the parameter type.
     */
    Type getReflectType();

    /**
     * @return true if the parameter is a collection or array.
     */
    boolean isListable();

    /**
     * @return the component type for a {@see isListable()} parameter, throw {@see java.lang.UnsupportedOperationException} otherwise.
     */
    Class<?> getComponentType();

    /**
     * @return the list of nested parameters if a complex parameter (BEAN_OPTION type) or null otherwise.
     */
    List<ParameterMetadata> getNested();
} public class Commands {

    private Commands() {
        // no-op
    }

    public static Iterable<Method> commands(final Class<?> clazz) {
        return new FilteredIterable<>(Reflection.methods(clazz),
                new FilteredIterator.Filter<Method>() {
                    @Override
                    public boolean accept(final Method method) {
                        return method.isAnnotationPresent(Command.class);
                    }
                }
        );
    }

    public static Map<String, Cmd> get(final Object bean) {
        return get(bean.getClass(), new SimpleBean(bean), new SystemPropertiesDefaultsContext());
    }

    public static Map<String, Cmd> get(final Object bean, final DefaultsContext dc) {
        return get(bean.getClass(), new SimpleBean(bean), dc);
    }

    public static Map<String, Cmd> get(final Class<?> clazz) {
        return get(clazz, new SimpleBean(null), new SystemPropertiesDefaultsContext());
    }

    public static Map<String, Cmd> get(final Class<?> clazz, final DefaultsContext dc) {
        return get(clazz, new SimpleBean(null), dc);
    }

    public static Map<String, Cmd> get(final Class<?> clazz, final Target target, final DefaultsContext dc) {
        if (target == null) {
            throw new IllegalArgumentException("Target cannot be null");
        }

        final Map<String, Cmd> map = new HashMap<>();

        for (final Method method : commands(clazz)) {

            final CmdMethod cmd = new CmdMethod(method, target, dc);

            final Cmd existing = map.get(cmd.getName());

            if (existing == null) {

                map.put(cmd.getName(), cmd);

            } else if (existing instanceof OverloadedCmdMethod) {

                final OverloadedCmdMethod overloaded = (OverloadedCmdMethod) existing;
                overloaded.add(cmd);

            } else {

                final OverloadedCmdMethod overloaded = new OverloadedCmdMethod(cmd.getName());
                overloaded.add((CmdMethod) existing);
                overloaded.add(cmd);
                map.put(overloaded.getName(), overloaded);
            }
        }

        if (clazz.isAnnotationPresent(Command.class)) {

            final CmdGroup cmdGroup = new CmdGroup(clazz, map);

            final HashMap<String, Cmd> group = new HashMap<>();
            group.put(cmdGroup.getName(), cmdGroup);

            return group;

        }
        return map;
    }

    public static String name(final Method method) {
        final Command command = method.getAnnotation(Command.class);
        if (command == null) {
            return method.getName();
        }
        return value(command.value(), method.getName());
    }

    public static String name(final Class<?> clazz) {
        final Command command = clazz.getAnnotation(Command.class);
        final String defaultName = Strings.lcfirst(clazz.getSimpleName());
        if (command == null) {
            return defaultName;
        }
        return value(command.value(), defaultName);
    }

    public static String value(final String value, final String defaultValue) {
        return value == null || value.isEmpty() ? defaultValue : value;
    }

    /**
     * Interface whose only purpose is to be used in conjunction
     * with the java.util.ServiceLoader API as one potential
     * way to load the list of classes that have commands.
     *
     * This interface intentionally has zero methods and never will
     * so that the simplest implementation is a plain java.util.ArrayList
     * (or pick your favorite collection)
     *
     * This interface is intentionally not used in any method or constructor of crest.
     * @deprecated use org.tomitribe.crest.api.Loader
     */
    @Deprecated
    public static interface Loader extends Iterable<Class<?>> {
    }

    public static Iterable<Class<?>> load() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader == null) {
            loader = ClassLoader.getSystemClassLoader();
        }

        // Let them tell us the list of classes to use
        final LinkedHashSet<Class<?>> classes = new LinkedHashSet<>();

        addAll(classes, ServiceLoader.load(Loader.class, loader).iterator());
        addAll(classes, ServiceLoader.load(org.tomitribe.crest.api.Loader.class, loader).iterator());

        // if maven plugin has been used just let add the found classes
        for (final String prefix : asList("", "/")) {
            try {
                final Enumeration<URL> urls = loader.getResources(prefix + "crest-commands.txt");
                final boolean done = urls.hasMoreElements();
                while (urls.hasMoreElements()) {
                    final URL url = urls.nextElement();
                    try (InputStream stream = url.openStream();
                         BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            line = normalize(line);
                            try {
                                onClass(classes, loader.loadClass(line));
                            } catch (final ClassNotFoundException e) {
                                // no-op: we can log it but don't fail cause one command didn't load
                            }
                        }
                    } catch (final IOException ioe) {
                        // no-op
                    }
                }
                if (done) {
                    break;
                }
            } catch (final IOException e) {
                // no-op
            }
        }

        return classes;
    }

    /**
     * Remove any whitespace to improve ability to understand what the user meant
     * Remove 'class ' at the start in case the user added a class via its toString() vs getName()
     */
    private static String normalize(String line) {
        line = line.trim();
        if (line.startsWith("class ")) {
            line = line.substring(5).trim();
        }
        return line;
    }

    private static void addAll(final LinkedHashSet<Class<?>> classes, final Iterator<? extends Iterable<Class<?>>> all) {
        while (all.hasNext()) {
            for (final Class<?> clazz : all.next()) {
                onClass(classes, clazz);
            }
        }
    }

    // enable to handle auto-loading of some features specifically, in particular editor implicit auto-loading
    private static void onClass(final Collection<Class<?>> classes, final Class<?> clazz) {
        if (EditorLoader.class == clazz) { // was loaded but we don't need anything else but triggering the init
            EditorLoader.Lazy.lightInit(Thread.currentThread().getContextClassLoader());
        } else {
            classes.add(clazz);
        }
    }
} public class Help {

    private final Map<String, Cmd> commands;

    public Help(final Map<String, Cmd> commands1) {
        commands = commands1;
    }

    public static void optionHelp(final Method method, final String commandName,
                                  final Collection<OptionParam> optionParams, final PrintStream out) {
        if (optionParams.isEmpty()) {
            return;
        }

        final List<Item> items = getItems(method, commandName, optionParams);

        int width = 20;
        for (final Item item : items) {
            width = Math.max(width, item.getFlag().length());
        }

        final String format = "  %-" + width + "s     %s%n";

        out.println("Options: ");

        for (final Item item : items) {
            final List<String> lines = new ArrayList<>();

            if (item.getDescription() != null) {
                lines.add(item.getDescription());
            }

            lines.addAll(item.getNote());
            if (lines.isEmpty()) {
                lines.add("");
            }

            out.printf(format, item.getFlag(), lines.remove(0));
            for (final String line : lines) {
                out.printf(format, "", String.format("(%s)", line));
            }

//            out.println();
        }
    }

    public static List<Item> getItems(final Method method, final String commandName, final Collection<OptionParam> optionParams) {
        final CommandJavadoc commandJavadoc = CommandJavadoc.getCommandJavadocs(method, commandName);

        final List<Item> items = getItems(method, commandName, optionParams, commandJavadoc);

        return items.stream()
                .map(Help::trimDescriptions)
                .collect(Collectors.toList());
    }

    private static Item trimDescriptions(final Item item) {
        if (item.getDescription() == null) return item;

        final Document document = DocumentParser.parser(item.getDescription());

        if (document.getElements().size() == 0) return item;

        final Element element = document.getElements().get(0);

        if (!(element instanceof Paragraph)) return item;

        final String content = element.getContent();
        final String[] sentences = content.split("\\. ");

        if (sentences.length == 0) return item;

        return new Item(item.getFlag(), sentences[0], item.getParam(), item.getNote());
    }

    public static List<Item> getItems(final Method method, final String commandName, final Collection<OptionParam> optionParams, final CommandJavadoc commandJavadoc) {
        final Class<?> clazz = method.getDeclaringClass();
        final List<Item> items = getItems(clazz, commandName, optionParams);

        if (commandJavadoc == null || commandJavadoc.getJavadoc() == null) return items;

        final Javadoc javadoc = JavadocParser.parse(commandJavadoc.getJavadoc());

        final Map<String, Javadoc.Param> params = javadoc.getParametersByName();

        final ListIterator<Item> iterator = items.listIterator();
        while (iterator.hasNext()) {
            final Item item = iterator.next();

            if (item.getDescription() != null) continue;

            final String optionName = item.getParam().getName();
            final String javadocParameterName = commandJavadoc.getProperties().getProperty(optionName);

            if (javadocParameterName == null) continue;

            final Javadoc.Param param = params.get(javadocParameterName);

            if (param == null || param.getDescription() == null) continue;

            final Item updated = new Item(item.getFlag(), param.getDescription(), item.getParam(), item.getNote());

            iterator.set(updated);
        }

        return items;
    }

    public static List<Item> getItems(final Class<?> clazz, final String commandName, final Collection<OptionParam> optionParams) {
        ResourceBundle general = null; // lazily loaded cause breaks the annotation driven API so not considered as default

        final List<Item> items = new ArrayList<>(optionParams.size());

        for (final OptionParam optionParam : optionParams) {
            String description = optionParam.getDescription();
            if (description == null || description.isEmpty()) {
                if (general == null) {
                    general = getResourceBundle(clazz);
                }
                description = getDescription(general, commandName, optionParam.getName());
            }
            final Item item = new Item(optionParam, description);
            items.add(item);
        }
        return items;
    }

    public static ResourceBundle getResourceBundle(final Class<?> clazz) {
        try {
            return ResourceBundle.getBundle(Classes.packageName(clazz) + ".OptionDescriptions");
        } catch (final java.util.MissingResourceException ok) {
            return null;
        }
    }

    public static String getDescription(final ResourceBundle general, final String commandName, final String name) {
        if (general == null) {
            return null;
        }

        try {

            return general.getString(commandName + "." + name);

        } catch (final MissingResourceException e) {

            try {

                return general.getString(name);

            } catch (final MissingResourceException e1) {

                return null;
            }
        }
    }


    @Command
    public String help() {
        final PrintString string = new PrintString();
        string.println("Commands: ");
        string.printf("   %-20s", "");
        string.println();

        final SortedSet<String> strings = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(final String s1, final String s2) {
                assert null != s1;
                assert null != s2;
                return s1.compareTo(s2);
            }
        });

        strings.addAll(commands.keySet());

        for (final String command : strings) {
            string.printf("   %-20s%n", command);
        }

        return string.toString();
    }

    @Command
    public String help(final String name) {
        final Cmd cmd = commands.get(name);

        if (cmd == null) {
            return String.format("No such command: %s%n", name);
        }

        final PrintString out = new PrintString();
        cmd.manual(out);
        return out.toString();
    }

    @Command
    public String help(final String name, final String subCommand) {
        final Cmd cmd = commands.get(name);

        if (cmd == null) {
            return String.format("No such command: %s%n", name);
        }

        final PrintString out = new PrintString();

        if (cmd instanceof CmdGroup) {
            CmdGroup cmdGroup = (CmdGroup) cmd;
            cmdGroup.manual(subCommand, out);
        } else {
            cmd.manual(out);
        }

        return out.toString();
    }

} public class Item {

    private final String flag;
    private final List<String> note = new LinkedList<>();
    private final String description;
    private final OptionParam param;

    public Item(final String flag, final String description, final OptionParam param, final List<String> notes) {
        this.flag = flag;
        this.description = description;
        this.param = param;
        this.note.addAll(notes);
    }

    Item(final OptionParam p, final String description) {
        this.description = description;
        this.param = p;
        final String prefix = p.getName().length() > 1 ? "--" : "-";

        final List<String> alias = new ArrayList<>();

        final Option option = p.getAnnotation(Option.class);
        for (int i = 1; i < option.value().length; i++) {
            final String aliasName = option.value()[i];
            alias.add(aliasName);
        }

        final boolean hasAlias = !alias.isEmpty();
        final Class<?> type = p.getType();

        String defaultValue = p.getDefaultValue();

        final String name = p.getName();
        if (boolean.class.equals(type) || (Boolean.class.equals(type) && defaultValue != null)) {

            if ("true".equals(defaultValue)) {
                this.flag = hasAlias ? Join.join(", ", "--no-" + name, getAlias(alias, false, true)) : "--no-" + name;
            } else {
                final String optName = name.startsWith("-") ? name : prefix + name;
                this.flag = hasAlias ? Join.join(", ", optName, getAlias(alias, true, false)) : optName;
            }

            defaultValue = null;

        } else {
            final String optName = name.startsWith("-") ? name : prefix + name;
            this.flag = hasAlias ? String.format("%s, %s=<%s>", optName, getAlias(alias, true, false), p.getDisplayType())
                    : String.format("%s=<%s>", optName, p.getDisplayType());
        }

        if (defaultValue != null) {

            if (p.isListable()) {
                final List<String> defaultValues = p.getDefaultValues();

                if (!defaultValues.isEmpty()) {
                    this.getNote().add(String.format("default: %s", Join.join(", ", defaultValues)));
                }

            } else {

                this.getNote().add(String.format("default: %s", p.getDefaultValue()));

            }
        }

        if (Enum.class.isAssignableFrom(type)) {
            final Class<? extends Enum> enumType = (Class<? extends Enum>) type;
            final EnumSet<? extends Enum> enums = EnumSet.allOf(enumType);
            final String join = Join.join(", ", enums);
            this.getNote().add(String.format("enum: %s", join));
        }

    }

    public OptionParam getParam() {
        return param;
    }

    private String getAlias(List<String> aliasList, boolean withDemiliter, boolean isBooleanValue) {
        StringBuilder sb = new StringBuilder();
        for (String alias : aliasList) {
            if (isBooleanValue) {
                sb.append(", --no-" + alias);
            } else {
                if (alias.length() > 1) {
                    sb.append(withDemiliter ? ", --" + alias : alias);
                } else {
                    sb.append(withDemiliter ? ", -" + alias : alias);
                }
            }
        }
        return sb.length() > 0 ? sb.toString().replaceFirst(", ", "") : "";
    }

    public String getFlag() {
        return flag;
    }

    public List<String> getNote() {
        return note;
    }

    public String getDescription() {
        return description;
    }
} public class Param extends Parameter {
    private volatile ParameterMetadata apiView;

    public Param(final Parameter parameter) {
        super(parameter.getAnnotations(), parameter.getType(), parameter.getGenericType(), parameter.getIndex());
    }

    public String getDisplayType() {
        if (isListable()) {

            return getListableType().getSimpleName() + "[]";
        }

        return getType().getSimpleName();
    }

    public boolean isListable() {
        final Class<?> type = getType();

        return Collection.class.isAssignableFrom(type) || type.isArray();
    }

    public Class getListableType() {

        if (getType().isArray()) {

            return getType().getComponentType();

        } else {

            return (Class<?>) Generics.getType(this);
        }
    }

    public ParameterMetadata getApiView() {
        return apiView;
    }

    public void setApiView(final ParameterMetadata apiView) {
        this.apiView = apiView;
    }
} public class Substitution {

    private static final Map<String, DefaultsContext> DEFAULTS_JVM_CONTEXTS = new HashMap<String, DefaultsContext>() {{
        put("env.", new EnvDefaultsContext());
        put("sys.", new SystemPropertiesDefaultsContext());
        for (final DefaultsContext defaultsContext : ServiceLoader.load(DefaultsContext.class)) {
            put(defaultsContext.getClass().getSimpleName().toLowerCase(ROOT), defaultsContext);
        }
    }};

    private Substitution() {
        // no-op
    }

    public static String format(final Target target, final Method method, final String input, final DefaultsContext df) {
        return format(target, method, input, df, new HashSet<String>());
    }

    private static String format(final Target target, final Method method, final String input,
                                 final DefaultsContext df, final Set<String> seen) {
        if (!seen.add(input)) {
            throw new IllegalStateException("Circular reference in " + input);
        }

        return new Substitutor() {
            @Override
            protected String getOrDefault(final String varName, final String varDefaultValue) {
                for (final Map.Entry<String, DefaultsContext> ctx : DEFAULTS_JVM_CONTEXTS.entrySet()) {
                    if (varName.startsWith(ctx.getKey())) {
                        final String value = ctx.getValue().find(target, method, varName.substring(ctx.getKey().length()));
                        if (value != null) {
                            return value;
                        }
                    }
                }
                final String value = df.find(target, method, varName);
                return value == null ? varDefaultValue : value;
            }
        }.replace(input);
    }
} public class SystemPropertiesDefaultsContext implements DefaultsContext {
    @Override
    public String find(final Target cmd, final Method commandMethod, final String key) {
        return System.getProperty(key);
    }
} public interface Environment {

    ThreadLocal<Environment> ENVIRONMENT_THREAD_LOCAL = new ThreadLocal<Environment>() {
        @Override
        protected Environment initialValue() {
            return new SystemEnvironment();
        }
    };

    PrintStream getOutput();

    PrintStream getError();

    InputStream getInput();

    Properties getProperties();

    default Map<String, String> getEnv() {
        return System.getenv();
    }

    <T> T findService(Class<T> type);
} public class CommandJavadoc {
    private final String clazzName;
    private final String commandName;
    private final Properties properties = new Properties();

    public CommandJavadoc(final String clazzName, final String commandName) {
        this.clazzName = clazzName;
        this.commandName = commandName;
    }


    public void setJavadoc(final String javadoc) {
        this.getProperties().put("@javadoc", javadoc);
    }

    public String getJavadoc() {
        return this.getProperties().getProperty("@javadoc");
    }

    public void setArgNames(final List<String> argNames) {
        this.getProperties().put("@arg.names", Join.join(", ", argNames));
    }

    public List<String> getArgNames() {
        final String property = getProperties().getProperty("@arg.names");
        return Arrays.asList(property.split(", "));
    }

    public void setArgTypes(final List<String> argTypes) {

        final List<String> normalized = argTypes.stream()
                .map(s -> s.replaceAll("<[^<>]+>", ""))
                .collect(Collectors.toList());
        this.getProperties().put("@arg.types", Join.join(", ", normalized));
    }

    public boolean matches(final Class<?>[] types) {
        final List<String> list = Stream.of(types)
                .map(this::classSignature)
                .collect(Collectors.toList());
        final String expected = Join.join(", ", list);
        final String argtypes = getProperties().getProperty("@arg.types");
        return expected.equals(argtypes);
    }

    public String classSignature(final Class<?> aClass) {
        if (aClass.isArray()) {
            return String.format("%s[]", aClass.getComponentType().getName());
        } else {
            return aClass.getName();
        }
    }

    public String getClazzName() {
        return clazzName;
    }

    public String getCommandName() {
        return commandName;
    }

    public Properties getProperties() {
        return properties;
    }

    public String getName() {
        return String.format("%s/%s", clazzName, commandName);
    }

    public String getResourceFileName(final int i) {
        return getResourceFileName(clazzName, commandName, i);
    }

    public static String getResourceFileName(final String clazzName, final String commandName, final int i) {
        return String.format("META-INF/crest/%s/%s.%s.properties", clazzName, commandName, i);
    }

    public static List<CommandJavadoc> loadJavadoc(final Class clazz, final String commandName) {
        return loadJavadoc(clazz.getName().replace('$', '.'), commandName);
    }

    public static List<CommandJavadoc> loadJavadoc(final String clazzName, final String commandName) {
        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
        final List<CommandJavadoc> javadocs = new ArrayList<>();

        for (int i = 0; true; i++) {
            final String resourceFileName = getResourceFileName(clazzName, commandName, i);
            final URL resource = loader.getResource(resourceFileName);
            if (resource == null) break;

            final CommandJavadoc javadoc = new CommandJavadoc(clazzName, commandName);
            try (final InputStream in = IO.read(resource)) {
                javadoc.getProperties().load(in);
                javadocs.add(javadoc);
            } catch (IOException e) {
                throw new InvalidJavadocFileException(resourceFileName, e);
            }
        }

        return javadocs;
    }

    public static CommandJavadoc getCommandJavadocs(final Method method, final String name) {
        final List<CommandJavadoc> javadocs = loadJavadoc(method.getDeclaringClass(), name);

        return javadocs.stream()
                .filter(commandJavadoc -> commandJavadoc.matches(method.getParameterTypes()))
                .findFirst().orElse(null);
    }
} public class Document {

    final List<Element> elements = new ArrayList<>();

    private Document(final List<Element> elements) {
        this.elements.addAll(elements);
    }

    public List<Element> getElements() {
        return elements;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        final List<Element> elements = new ArrayList<>();

        public Builder paragraph(final String content) {
            elements.add(new Paragraph(content));
            return this;
        }

        public Builder bullet(final String content) {
            elements.add(new Bullet(content));
            return this;
        }

        public Builder preformatted(final String content) {
            elements.add(new Preformatted(content));
            return this;
        }

        public Builder heading(final String content) {
            elements.add(new Heading(content));
            return this;
        }

        public Builder inline(final Document document) {
            elements.addAll(document.elements);
            return this;
        }

        public Builder element(final Element element) {
            elements.add(element);
            return this;
        }

        public Document build() {
            return new Document(elements);
        }
    }
} public class DocumentFormatter {
    /**
     * Specifies the padding on the left and right
     * of paragraphs and other indented content
     */
    private final String margin = "       ";
    private final int width;
    private final int column;
    private final boolean color;

    public DocumentFormatter(final int width) {
        this(width, true);
    }

    public DocumentFormatter(final int width, final boolean color) {
        this.width = width;
        // Add padding to the left and right
        this.column = width - margin.length() - margin.length();
        this.color = color;
    }

    public String format(final Document document) {
        final PrintString out = new PrintString();

        final Highlight highlighter = new Highlight(getOptions(document));

        final Function<String, String> highlight = color ? highlighter::highlight : Function.identity();
        final Function<String, String> highlightKeywords = color ? highlighter::matches : Function.identity();

        final Iterator<Element> iterator = document.getElements().iterator();
        while (iterator.hasNext()) {
            final Element element = iterator.next();

            if (element instanceof Heading) {

                final Heading heading = (Heading) element;
                final String text = Strings.uppercase(heading.getContent());

                out.println(highlight.apply(text));

            } else if (element instanceof Paragraph) {
                final Paragraph paragraph = (Paragraph) element;
                final String content = Justify.wrapAndJustify(paragraph.getContent() + "", column);
                Stream.of(content.split("\n"))
                        .map(highlightKeywords)
                        .forEach(s -> out.format("%s%s%n", margin, s));

                if (iterator.hasNext()) out.println();

            } else if (element instanceof Option) {
                final Option option = (Option) element;
                /**
                 * Construct a document formatter for the option's content.  We want it to fill the same
                 * width  with just one additional indent on the left.
                 */
                final DocumentFormatter formatter = new DocumentFormatter(width - margin.length(), false);
                final String content = formatter.format(option.getDocument());

                final List<String> lines;
                if (content == null || content.length() == 0) {
                    lines = Collections.EMPTY_LIST;
                } else {
                    lines = new ArrayList<>(Arrays.asList(content.split("\n")));
                }


                if (option.getFlag().length() < 7 && lines.size() > 0) {
                    final String firstLine = lines.remove(0).trim();
                    if (color) {
                        out.format("%s\033[0m\033[1m%-6s\033[0m %s%n",
                                margin,
                                option.getFlag(),
                                highlightKeywords.apply(firstLine));
                    } else {
                        out.format("       %-6s %s%n", option.getFlag(), firstLine);
                    }
                    lines.stream()
                            .map(highlightKeywords)
                            .forEach(s -> out.format("%s%s%n", margin, s));
                } else {
                    final String flag = highlight.apply(option.getFlag());
                    out.format("%s%s%n", margin, flag);
                    lines.stream()
                            .map(highlightKeywords)
                            .forEach(s -> out.format("%s%s%n", margin, s));
                }

                if (iterator.hasNext()) out.println();

            } else if (element instanceof Bullet) {
                final Bullet bullet = (Bullet) element;

                final String content = Wrap.wrap(bullet.getContent(), column - margin.length());
                final List<String> lines = Stream.of(content.split("\n"))
                        .map(s -> String.format("%s%s", margin, s))
                        .map(highlightKeywords)
                        .collect(Collectors.toList());


                { // Set the first line as the bullet
                    final String first = lines.get(0).substring(1);
                    lines.set(0, "o" + first);
                }

                lines.forEach(s -> out.format("%s%s%n", margin, s));

                if (iterator.hasNext()) out.println();

            } else if (element instanceof Preformatted) {
                final Preformatted preformatted = (Preformatted) element;
                Stream.of(preformatted.getContent().split("\n"))
                        .forEach(s -> out.format("%s    %s%n", margin, s));

                if (iterator.hasNext()) out.println();
            }
        }
        return out.toString();
    }

    public static List<Option> getOptions(final Document document) {
        return document.getElements().stream()
                .filter(element -> element instanceof Option)
                .map(Option.class::cast)
                .collect(Collectors.toList());
    }
} public class DocumentParser {

    private final String content;
    private final Pattern leadingSpaces = Pattern.compile("^( *)");
    private final Pattern bullet = Pattern.compile("^( *-) *(.+)");
    private final Pattern heading = Pattern.compile("^[=#]+ *(.+)|^([A-Z]+[^a-z]+)$");
    private final Pattern preformatted = Pattern.compile("^    (.+)");
    private final Document.Builder doc = Document.builder();

    public DocumentParser(final String content) {
        this.content = content;
    }

    public static Document parser(String content) {
        if (content == null || content.length() == 0) return Document.builder().build();
        if (HtmlDocumentParser.isHtml(content)) {
            return HtmlDocumentParser.parse(content);
        } else {
            return new DocumentParser(content).parse();
        }
    }

    /**
     * Item descriptions are allowed to have only a subset of the element types
     * a complete manual would get.
     */
    public static Document parseOptionDescription(final String description) {
        final Document.Builder document = Document.builder();

        final Document parser = DocumentParser.parser(description);
        parser.getElements().stream()
                .filter(element -> !(element instanceof Option))
                .map(DocumentParser::convertHeaders)
                .forEach(document::element);

        return document.build();
    }

    /**
     * Item Descriptions aren't allowed to have headings.  We will convert them to paragraphs
     */
    private static Element convertHeaders(final Element element) {
        if (element instanceof Heading) {
            return new Paragraph(element.getContent());
        }
        return element;
    }

    private Document parse() {

        final List<String> lines = normalizeAndSplitContent();

        for (final String line : lines) {
            if (processTerminator(line)) continue;
            if (processHeader(line)) continue;
            if (processBullet(line)) continue;
            if (processPreformatted(line)) continue;
            processParagraph(line);
        }

        terminate();

        return doc.build();
    }

    private List<String> normalizeAndSplitContent() {
        List<String> lines = new ArrayList<>(Arrays.asList(content.trim()
                .replace("\t", "    ")
                .replaceAll("\n *\n", "\n\n")
                .split("\n")));

        lines = stripIndent(lines);
        return lines;
    }

    private boolean processPreformatted(final String line) {
        final Matcher matcher = preformatted.matcher(line);
        if (!matcher.find()) return false;

        if (state != null && !(state instanceof ReadingPreformatted)) {
            state.terminate();
            state = null;
        }

        if (state == null) {
            state = new ReadingPreformatted();
        }

        state.process(matcher.group(1));

        return true;
    }

    private boolean processTerminator(final String line) {
        if (line.length() != 0) return false;

        terminate();

        return true;
    }

    private void processParagraph(final String line) {
        if (state != null && !(state instanceof ReadingParagraph)) {
            state.terminate();
            state = null;
        }

        if (state == null) {
            state = new ReadingParagraph();
        }

        state.process(line);
    }

    private boolean processHeader(final String line) {
        final Matcher matcher = heading.matcher(line);
        if (!matcher.find()) return false;


        if (matcher.group(1) != null) {
            terminate();
            final String text = matcher.group(1);
            doc.heading(text);
            return true;
        }
        if (matcher.group(2) != null) {
            terminate();
            final String text = matcher.group(2);
            doc.heading(text);
            return true;
        }

        return false;
    }

    private boolean processBullet(final String line) {
        { // Is this line the start of a bullet?
            final Matcher matcher = bullet.matcher(line);
            if (matcher.find()) {
                terminate();

                final String prefix = matcher.group(1);
                final String text = matcher.group(2);

                this.state = new ReadingBullet(prefix);
                this.state.process(text);

                return true;
            }
        }

        // Is this line a continuation of a bullet?
        if (state instanceof ReadingBullet) {
            final ReadingBullet readingBullet = (ReadingBullet) this.state;
            final Matcher matcher = readingBullet.continued.matcher(line);
            if (!matcher.find()) return false;

            final String text = matcher.group(1);
            readingBullet.process(text);
            return true;
        }

        return false;
    }


    private void terminate() {
        if (state == null) return;
        state.terminate();
        state = null;
    }


    private State state;

    public interface State {
        void process(final String line);

        void terminate();
    }

    private class ReadingParagraph implements State {
        private final List<String> lines = new ArrayList<>();

        @Override
        public void process(final String line) {
            lines.add(line.trim());
        }

        @Override
        public void terminate() {
            final String content = Join.join(" ", lines)
                    .replaceAll("  +", " ");
            doc.paragraph(content);
        }
    }

    private class ReadingPreformatted implements State {
        private final List<String> lines = new ArrayList<>();

        @Override
        public void process(final String line) {
            lines.add(line);
        }

        @Override
        public void terminate() {
            doc.preformatted(Join.join("\n", lines));
        }
    }

    private class ReadingBullet implements State {
        private final List<String> lines = new ArrayList<>();
        private final Pattern continued;

        public ReadingBullet(final String bullet) {
            final String spaces = bullet.replaceAll(".", " ");
            this.continued = Pattern.compile(String.format("^%s(.+)", spaces));
        }

        @Override
        public void process(final String line) {
            lines.add(line);
        }

        @Override
        public void terminate() {
            final String content = Join.join(" ", lines)
                    .replaceAll("  +", " ");

            doc.bullet(content);
        }
    }

    private List<String> stripIndent(final List<String> lines) {
        final String first = lines.remove(0);

        int indent = getIndent(lines);

        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);
            if (line.length() < indent) continue;
            line = line.substring(indent);
            lines.set(i, line);
        }
        lines.add(0, first);
        return lines;
    }

    private int getIndent(final List<String> lines) {
        int indent = Integer.MAX_VALUE;
        for (final String line : lines) {
            if (line.length() == 0) continue;
            final Matcher matcher = leadingSpaces.matcher(line);
            if (matcher.find()) {
                final String spaces = matcher.group(1);
                indent = Math.min(indent, spaces.length());
            }
        }
        return indent;
    }

} public class Javadoc {

    private final String content;
    private final List<Param> params;
    private final List<Throws> throwing;
    private final List<Author> authors;
    private final List<See> sees;
    private final List<Tag> unknown;
    private final Return aReturn;
    private final Since since;
    private final Version version;
    private final Deprecated deprecated;

    Javadoc(final String content,
            final List<Param> params,
            final List<Throws> throwing,
            final List<Author> authors,
            final List<See> sees,
            final List<Tag> unknown,
            final Return aReturn,
            final Since since,
            final Deprecated deprecated,
            final Version version) {
        this.content = content;
        this.params = params;
        this.throwing = throwing;
        this.authors = authors;
        this.sees = sees;
        this.unknown = unknown;
        this.aReturn = aReturn;
        this.since = since;
        this.version = version;
        this.deprecated = deprecated;
    }

    public static Builder builder() {
        return new Javadoc.Builder();
    }

    public Map<String, Param> getParametersByName() {
        if (this.getParams() == null) return new HashMap<>();

        return this.getParams().stream()
                .collect(Collectors.toMap(Param::getName, Function.identity()));
    }

    public List<Tag> getUnknown() {
        return unknown;
    }

    public Deprecated getDeprecated() {
        return deprecated;
    }

    public String getContent() {
        return this.content;
    }

    public List<Param> getParams() {
        return this.params;
    }

    public List<Throws> getThrowing() {
        return this.throwing;
    }

    public List<Author> getAuthors() {
        return this.authors;
    }

    public List<See> getSees() {
        return this.sees;
    }

    public Return getReturn() {
        return this.aReturn;
    }

    public Since getSince() {
        return this.since;
    }

    public Version getVersion() {
        return this.version;
    }

    public String toString() {
        return "Javadoc(content=" + this.getContent() + ", params=" + this.getParams() +
                ", throwing=" + this.getThrowing() + ", authors=" + this.getAuthors() +
                ", sees=" + this.getSees() + ", return=" + this.getReturn() +
                ", since=" + this.getSince() + ", version=" + this.getVersion() + ")";
    }

    public static class Param {
        private final String name;
        private final String description;

        Param(String name, String description) {
            this.name = name;
            this.description = description;
        }

        public static Builder builder() {
            return new Param.Builder();
        }

        public String getName() {
            return this.name;
        }

        public String getDescription() {
            return this.description;
        }

        public String toString() {
            return "Javadoc.Param(name=" + this.getName() + ", description=" + this.getDescription() + ")";
        }

        public static class Builder {
            private String name;
            private String description;

            Builder() {
            }

            public Param.Builder name(String name) {
                this.name = name;
                return this;
            }

            public Param.Builder description(String description) {
                this.description = description;
                return this;
            }

            public Param build() {
                return new Param(name, description);
            }

            public String toString() {
                return "Javadoc.Param.Builder(name=" + this.name + ", description=" + this.description + ")";
            }
        }
    }

    public static class Tag {
        private final String name;
        private final String content;

        Tag(String name, String content) {
            this.name = name;
            this.content = content;
        }

        public static Builder builder() {
            return new Tag.Builder();
        }

        public String getName() {
            return this.name;
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Tag(name=" + this.getName() + ", content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String name;
            private String content;

            Builder() {
            }

            public Tag.Builder name(String name) {
                this.name = name;
                return this;
            }

            public Tag.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Tag build() {
                return new Tag(name, content);
            }

            public String toString() {
                return "Javadoc.Tag.Builder(name=" + this.name + ", content=" + this.content + ")";
            }
        }
    }

    public static class Author {
        private final String content;

        Author(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new Author.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Author(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public Author.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Author build() {
                return new Author(content);
            }

            public String toString() {
                return "Javadoc.Author.Builder(content=" + this.content + ")";
            }
        }
    }

    public static class Version {
        private final String content;

        Version(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new Version.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Version(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public Version.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Version build() {
                return new Version(content);
            }

            public String toString() {
                return "Javadoc.Version.Builder(content=" + this.content + ")";
            }
        }
    }

    public static class See {
        private final String content;

        See(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new See.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.See(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public See.Builder content(String content) {
                this.content = content;
                return this;
            }

            public See build() {
                return new See(content);
            }

            public String toString() {
                return "Javadoc.See.Builder(content=" + this.content + ")";
            }
        }
    }

    public static class Return {
        private final String content;

        Return(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new Return.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Return(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public Return.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Return build() {
                return new Return(content);
            }

            public String toString() {
                return "Javadoc.Return.Builder(content=" + this.content + ")";
            }
        }
    }

    public static class Throws {
        private final String classname;
        private final String description;

        Throws(String classname, String description) {
            this.classname = classname;
            this.description = description;
        }

        public static Builder builder() {
            return new Throws.Builder();
        }

        public String getClassname() {
            return this.classname;
        }

        public String getDescription() {
            return this.description;
        }

        public String toString() {
            return "Javadoc.Throws(classname=" + this.getClassname() + ", description=" + this.getDescription() + ")";
        }

        public static class Builder {
            private String classname;
            private String description;

            Builder() {
            }

            public Throws.Builder classname(String classname) {
                this.classname = classname;
                return this;
            }

            public Throws.Builder description(String description) {
                this.description = description;
                return this;
            }

            public Throws build() {
                return new Throws(classname, description);
            }

            public String toString() {
                return "Javadoc.Throws.Builder(classname=" + this.classname + ", description=" + this.description + ")";
            }
        }
    }

    public static class Since {
        private final String content;

        Since(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new Since.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Since(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public Since.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Since build() {
                return new Since(content);
            }

            public String toString() {
                return "Javadoc.Since.Builder(content=" + this.content + ")";
            }
        }
    }

    public static class Deprecated {
        private final String content;

        Deprecated(String content) {
            this.content = content;
        }

        public static Builder builder() {
            return new Deprecated.Builder();
        }

        public String getContent() {
            return this.content;
        }

        public String toString() {
            return "Javadoc.Deprecated(content=" + this.getContent() + ")";
        }

        public static class Builder {
            private String content;

            Builder() {
            }

            public Deprecated.Builder content(String content) {
                this.content = content;
                return this;
            }

            public Deprecated build() {
                return new Deprecated(content);
            }

            public String toString() {
                return "Javadoc.Deprecated.Builder(content=" + this.content + ")";
            }
        }
    }


    public static class Builder {
        private String content;
        private List<Param> params;
        private List<Throws> throwing;
        private List<Author> authors;
        private List<See> sees;
        private List<Tag> unknown;
        private Return aReturn;
        private Since since;
        private Version version;
        private Deprecated deprecated;

        Builder() {
        }

        public Javadoc.Builder content(String content) {
            this.content = content;
            return this;
        }

        public Javadoc.Builder param(final Param param) {
            if (params == null) params = new ArrayList<>();
            this.params.add(param);
            return this;
        }

        public Javadoc.Builder params(List<Param> params) {
            this.params = params;
            return this;
        }

        public Javadoc.Builder unknown(final Tag tag) {
            if (unknown == null) unknown = new ArrayList<>();
            this.unknown.add(tag);
            return this;
        }

        public Javadoc.Builder unknown(List<Tag> tags) {
            this.unknown = tags;
            return this;
        }

        public Javadoc.Builder throwing(List<Throws> throwing) {
            this.throwing = throwing;
            return this;
        }

        public Javadoc.Builder throwing(final Throws throwing) {
            if (this.throwing == null) this.throwing = new ArrayList<>();
            this.throwing.add(throwing);
            return this;
        }

        public Javadoc.Builder author(final Author author) {
            if (authors == null) authors = new ArrayList<>();
            this.authors.add(author);
            return this;
        }

        public Javadoc.Builder authors(List<Author> authors) {
            this.authors = authors;
            return this;
        }

        public Javadoc.Builder see(final See see) {
            if (sees == null) sees = new ArrayList<>();
            this.sees.add(see);
            return this;
        }

        public Javadoc.Builder sees(List<See> sees) {
            this.sees = sees;
            return this;
        }

        public Javadoc.Builder aReturn(Return aReturn) {
            this.aReturn = aReturn;
            return this;
        }

        public Javadoc.Builder since(Since since) {
            this.since = since;
            return this;
        }

        public Javadoc.Builder deprecated(Deprecated deprecated) {
            this.deprecated = deprecated;
            return this;
        }

        public Javadoc.Builder version(Version version) {
            this.version = version;
            return this;
        }

        public Javadoc build() {
            return new Javadoc(content, params, throwing, authors, sees, unknown, aReturn, since, deprecated, version);
        }

        public String toString() {
            return "Javadoc.Builder(content=" + this.content + ", params=" + this.params +
                    ", throwsList=" + this.throwing + ", authors=" + this.authors +
                    ", sees=" + this.sees + ", aReturn=" + this.aReturn + ", since=" +
                    this.since + ", version=" + this.version + ")";
        }

    }
} public class JavadocParser {

    private final Javadoc.Builder javadoc = Javadoc.builder();
    private final String content;

    public JavadocParser(final String content) {
        this.content = content;
    }

    private Javadoc parse() {
        final ArrayList<String> parts = splitParts();

        if (parts.size() == 0) return javadoc.build();

        if (!parts.get(0).startsWith("@")) {
            javadoc.content(parts.remove(0));
        }

        final List<Javadoc.Tag> tags = parts.stream()
                .filter(s -> s.startsWith("@"))
                .map(JavadocParser::toTag)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        for (final Javadoc.Tag tag : tags) {
            convert(tag);
        }
        return javadoc.build();
    }

    private void convert(final Javadoc.Tag tag) {
        if ("param".equals(tag.getName())) {
            param(tag);
        } else if ("throws".equals(tag.getName())) {
            throwing(tag);
        } else if ("author".equals(tag.getName())) {
            author(tag);
        } else if ("see".equals(tag.getName())) {
            see(tag);
        } else if ("return".equals(tag.getName())) {
            returning(tag);
        } else if ("since".equals(tag.getName())) {
            since(tag);
        } else if ("version".equals(tag.getName())) {
            version(tag);
        } else if ("deprecated".equals(tag.getName())) {
            deprecated(tag);
        } else {
            javadoc.unknown(tag);
        }
    }

    private void deprecated(final Javadoc.Tag tag) {
        javadoc.deprecated(new Javadoc.Deprecated(tag.getContent()));
    }

    private void version(final Javadoc.Tag tag) {
        javadoc.version(new Javadoc.Version(tag.getContent()));
    }

    private void since(final Javadoc.Tag tag) {
        javadoc.since(new Javadoc.Since(tag.getContent()));
    }

    private void returning(final Javadoc.Tag tag) {
        javadoc.aReturn(new Javadoc.Return(tag.getContent()));
    }

    private void see(final Javadoc.Tag tag) {
        javadoc.see(new Javadoc.See(tag.getContent()));
    }

    private void author(final Javadoc.Tag tag) {
        javadoc.author(new Javadoc.Author(tag.getContent()));
    }

    private void throwing(final Javadoc.Tag tag) {
        final Map.Entry<String, String> entry = parseKeyValue(tag.getContent());
        if (entry != null) javadoc.throwing(new Javadoc.Throws(entry.getKey(), entry.getValue()));
    }

    private void param(final Javadoc.Tag tag) {
        final Map.Entry<String, String> entry = parseKeyValue(tag.getContent());
        if (entry != null) javadoc.param(new Javadoc.Param(entry.getKey(), entry.getValue()));
    }

    static Javadoc.Tag toTag(String text) {
        // Strip off the leading '@'
        text = text.substring(1).trim();

        final Map.Entry<String, String> pair = parseKeyValue(text);

        return new Javadoc.Tag(pair.getKey(), pair.getValue());
    }

    private static Map.Entry<String, String> parseKeyValue(final String text) {
        final String delimiter = "\000\000\000";
        final String[] parts = text
                .replaceFirst("[\n\t ]", delimiter)
                .split(delimiter);

        if (parts.length == 0) {

            return null;

        } else if (parts.length == 1) {

            return new Pair(parts[0], null);

        } else if (parts.length == 2) {

            final String name = parts[0].trim();
            final String content = parts[1].trim();

            return new Pair(name, content);

        } else {
            // Wow, they have javadoc with \000\000\000 in it
            // What the heck are they doing?
            // Anyway, let's handle it
            final ArrayList<String> strings = new ArrayList<>(Arrays.asList(parts));
            final String name = strings.remove(0).trim();
            final String content = Join.join(delimiter, strings).trim();
            return new Pair(name, content);
        }
    }

    public static class Pair implements Map.Entry<String, String> {
        private final String key;
        private final String value;

        Pair(final String key, final String value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String getKey() {
            return key;
        }

        @Override
        public String getValue() {
            return value;
        }

        @Override
        public String setValue(final String value) {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Three things make parsing Javadoc interesting
     *
     * - tags can have any amount of leading spaces before '@'
     * - tags can also be multi-line and contain line breaks like newline
     * - the '@' character can be used inside descriptions
     *
     * It would be tempting to parse line-by-line, however it would result
     * in some pretty complicated buffering lines for multi-line content.
     *
     * Instead we simply split our content using '\n *@' as our regex and
     * we are done with the bulk of parsing and need only refine the parts.
     *
     * The result of the parsing will be a list where the first entry
     * will contain the javadoc text, if any, and the subsequent parts
     * contain the individual tags.
     */
    private ArrayList<String> splitParts() {
        if (content == null) return new ArrayList<>();
        final String[] parts = content
                .replaceFirst("^ *@", "@")
                .replaceAll("\n *@", "\n\000@")
                .split("\n\000");

        return new ArrayList<>(Arrays.asList(parts));
    }

    public Javadoc build() {
        return javadoc.build();
    }

    public static Javadoc parse(final String content) {
        return new JavadocParser(content).parse();
    }
} public class Screen {

    private Screen() {
    }

    public static String exec() throws IOException {
        return exec(true, "stty", "-a");
    }

    public static int guessWidth() {
        try {
            return parseWidth(exec());
        } catch (Exception e) {
            return -1;
        }
    }

    public static int parseWidth(final String output) {
        return Stream.of(Lines.split(output))
                .filter(s -> s.contains(" columns;"))
                .map(s -> s.split(";"))
                .flatMap(Stream::of)
                .map(String::trim)
                .filter(s -> s.endsWith(" columns"))
                .map(s -> s.split("[= ]+"))
                .filter(strings -> strings.length == 2)
                .map(strings -> strings[0])
                .map(String::trim)
                .map(Integer::parseInt)
                .findFirst()
                .orElse(-1);
    }

    public static String exec(boolean redirectInput, final String... cmd) throws IOException {
        Objects.requireNonNull(cmd);
        try {
            ProcessBuilder pb = new ProcessBuilder(cmd);
            if (redirectInput) {
                pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
            }
            Process p = pb.start();
            String result = waitAndCapture(p);
            if (p.exitValue() != 0) {
                if (result.endsWith("\n")) {
                    result = result.substring(0, result.length() - 1);
                }
                throw new IOException("Error executing '" + String.join(" ", (CharSequence[]) cmd) + "': " + result);
            }
            return result;
        } catch (InterruptedException e) {
            throw (IOException) new InterruptedIOException("Command interrupted").initCause(e);
        }
    }

    public static String waitAndCapture(Process p) throws IOException, InterruptedException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        InputStream in = null;
        InputStream err = null;
        OutputStream out = null;
        try {
            int c;
            in = p.getInputStream();
            while ((c = in.read()) != -1) {
                bout.write(c);
            }
            err = p.getErrorStream();
            while ((c = err.read()) != -1) {
                bout.write(c);
            }
            out = p.getOutputStream();
            p.waitFor();
        } finally {
            close(in, out, err);
        }

        return bout.toString();
    }

    private static void close(final Closeable... closeables) {
        for (Closeable c : closeables) {
            if (c != null) {
                try {
                    c.close();
                } catch (Exception e) {
                    // Ignore
                }
            }
        }
    }

} public static Document parseOptionDescription(final String description) {
    final Document.Builder document = Document.builder();

    final Document parser = DocumentParser.parser(description);
    parser.getElements().stream()
            .filter(element -> !(element instanceof Option))
            .map(DocumentParser::convertHeaders)
            .forEach(document::element);

    return document.build();
}
import org.tomitribe.crest.api.Command;
import org.tomitribe.crest.api.CrestAnnotation;
import org.tomitribe.crest.api.Default;
import org.tomitribe.crest.api.Defaults;
import org.tomitribe.crest.api.Err;
import org.tomitribe.crest.api.Exit;
import org.tomitribe.crest.api.In;
import org.tomitribe.crest.api.NotAService;
import org.tomitribe.crest.api.Option;
import org.tomitribe.crest.api.Options;
import org.tomitribe.crest.api.Out;
import org.tomitribe.crest.api.Required;
import org.tomitribe.crest.api.interceptor.ParameterMetadata;
import org.tomitribe.crest.cmds.processors.Commands;
import org.tomitribe.crest.cmds.processors.Help;
import org.tomitribe.crest.cmds.processors.Item;
import org.tomitribe.crest.cmds.processors.OptionParam;
import org.tomitribe.crest.cmds.processors.Param;
import org.tomitribe.crest.cmds.targets.SimpleBean;
import org.tomitribe.crest.cmds.targets.Substitution;
import org.tomitribe.crest.cmds.targets.Target;
import org.tomitribe.crest.cmds.utils.CommandLine;
import org.tomitribe.crest.contexts.DefaultsContext;
import org.tomitribe.crest.contexts.SystemPropertiesDefaultsContext;
import org.tomitribe.crest.environments.Environment;
import org.tomitribe.crest.help.CommandJavadoc;
import org.tomitribe.crest.help.Document;
import org.tomitribe.crest.help.DocumentFormatter;
import org.tomitribe.crest.help.DocumentParser;
import org.tomitribe.crest.interceptor.internal.InternalInterceptor;
import org.tomitribe.crest.interceptor.internal.InternalInterceptorInvocationContext;
import org.tomitribe.crest.javadoc.Javadoc;
import org.tomitribe.crest.javadoc.JavadocParser;
import org.tomitribe.crest.term.Screen;
import org.tomitribe.crest.val.BeanValidation;
import org.tomitribe.util.IO;
import org.tomitribe.util.Join;
import org.tomitribe.util.editor.Converter;
import org.tomitribe.util.reflect.Parameter;
import org.tomitribe.util.reflect.Reflection;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import static java.util.Collections.unmodifiableList;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.BEAN_OPTION;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.INTERNAL;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.OPTION;
import static org.tomitribe.crest.api.interceptor.ParameterMetadata.ParamType.SERVICE;
import static org.tomitribe.crest.help.DocumentParser.parseOptionDescription;
                    return type;
                }

                @Override
                public String getName() {
                    return name;
                }

                @Override
                public List<ParameterMetadata> getNested() {
                    return nested;
                }

                @Override
                public Type getReflectType() {
                    return param.getGenericType();
                }

                @Override
                public boolean isListable() {
                    return param.isListable();
                }

                @Override
                public Class<?> getComponentType() {
                    return param.getListableType();
                }

                @Override
                public String toString() {
                    return getType() + ": " + getReflectType() + ", name=" + getName() + ", nested=" + getNested();
                }
            };
            param.setApiView(parameterMetadata);
            parameterMetadatas.add(parameterMetadata);
        }
        return unmodifiableList(parameterMetadatas);
    }

    protected Object doInvoke(final List<Object> list) {
        final Object[] args;
        try {
            args = list.toArray();
            BeanValidation.validateParameters(target.getInstance(method), method, args);
        } catch (final Exception e) {
            reportWithHelp(e);
            throw toRuntimeException(e);
        }

        try {
            return target.invoke(method, args);
        } catch (final InvocationTargetException e) {
            final Throwable cause = e.getCause();
            if (cause instanceof IllegalArgumentException) {
                reportWithHelp(e);
            }
            throw new CommandFailedException(cause, getName());
        } catch (final Throwable e) {
            throw toRuntimeException(e);
        }
    }

    private void reportWithHelp(final Exception e) {
        final PrintStream err = Environment.ENVIRONMENT_THREAD_LOCAL.get().getError();

        if (BeanValidation.isActive()) {
            for (final String message : BeanValidation.messages(e)) {
                err.println(message);
            }
        } else {
            err.println(e.getMessage());
        }
        help(err);

        throw new HelpPrintedException(e);
    }

    public static RuntimeException toRuntimeException(final Throwable e) {
        if (e instanceof RuntimeException) {
            return (RuntimeException) e;
        }
        return new IllegalArgumentException(e);
    }

    public Map<String, OptionParam> getOptionParameters() {
        return Collections.unmodifiableMap(spec.options);
    }

    public Spec getSpec() {
        return spec;
    }

    @Override
    public void manual(final PrintStream out) {
        final CommandJavadoc commandJavadoc = CommandJavadoc.getCommandJavadocs(method, name);

        if (commandJavadoc == null) {
            help(out);
            return;
        }

        final Javadoc javadoc = JavadocParser.parse(commandJavadoc.getJavadoc());

        final Document.Builder manual = Document.builder()
                .heading("NAME")
                .paragraph(name)
                .heading("SYNOPSIS")
                .paragraph(getUsage());

        {
            final Document description = DocumentParser.parser(javadoc.getContent());
            if (description.getElements().size() > 0) {
                manual.heading("DESCRIPTION")
                        .inline(description);
            }
        }

        if (spec.getOptions().size() > 0) {
            manual.heading("OPTIONS");

            final List<Item> items = Help.getItems(method, name, spec.options.values(), commandJavadoc);