// 
// Decompiled by Procyon v0.6.0
// 

package org.jline.widget;

import java.util.HashMap;
import org.jline.utils.StyleResolver;
import org.jline.builtins.Options;
import org.jline.reader.Reference;
import org.jline.reader.Binding;
import java.util.Collections;
import org.jline.utils.Status;
import java.util.Collection;
import java.util.ArrayList;
import org.jline.utils.AttributedStyle;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedString;
import java.util.Iterator;
import org.jline.console.ArgDesc;
import org.jline.reader.Buffer;
import java.util.List;
import java.util.regex.Pattern;
import org.jline.keymap.KeyMap;
import org.jline.utils.InfoCmp;
import org.jline.console.CmdLine;
import java.util.function.Function;
import org.jline.console.CmdDesc;
import java.util.Map;
import org.jline.reader.LineReader;

public class TailTipWidgets extends Widgets
{
    private boolean enabled;
    private final CommandDescriptions cmdDescs;
    private TipType tipType;
    private int descriptionSize;
    private boolean descriptionEnabled;
    private boolean descriptionCache;
    private Object readerErrors;
    
    public TailTipWidgets(final LineReader reader, final Map<String, CmdDesc> tailTips) {
        this(reader, tailTips, 0, TipType.COMBINED);
    }
    
    public TailTipWidgets(final LineReader reader, final Map<String, CmdDesc> tailTips, final TipType tipType) {
        this(reader, tailTips, 0, tipType);
    }
    
    public TailTipWidgets(final LineReader reader, final Map<String, CmdDesc> tailTips, final int descriptionSize) {
        this(reader, tailTips, descriptionSize, TipType.COMBINED);
    }
    
    public TailTipWidgets(final LineReader reader, final Map<String, CmdDesc> tailTips, final int descriptionSize, final TipType tipType) {
        this(reader, tailTips, descriptionSize, tipType, null);
    }
    
    public TailTipWidgets(final LineReader reader, final Function<CmdLine, CmdDesc> descFun, final int descriptionSize, final TipType tipType) {
        this(reader, null, descriptionSize, tipType, descFun);
    }
    
    private TailTipWidgets(final LineReader reader, final Map<String, CmdDesc> tailTips, final int descriptionSize, final TipType tipType, final Function<CmdLine, CmdDesc> descFun) {
        super(reader);
        this.enabled = false;
        this.descriptionEnabled = true;
        this.descriptionCache = false;
        if (this.existsWidget("_tailtip-accept-line")) {
            throw new IllegalStateException("TailTipWidgets already created!");
        }
        this.cmdDescs = ((tailTips != null) ? new CommandDescriptions(tailTips) : new CommandDescriptions(descFun));
        this.descriptionSize = descriptionSize;
        this.tipType = tipType;
        this.addWidget("_tailtip-accept-line", this::tailtipAcceptLine);
        this.addWidget("_tailtip-self-insert", this::tailtipInsert);
        this.addWidget("_tailtip-backward-delete-char", this::tailtipBackwardDelete);
        this.addWidget("_tailtip-delete-char", this::tailtipDelete);
        this.addWidget("_tailtip-expand-or-complete", this::tailtipComplete);
        this.addWidget("_tailtip-redisplay", this::tailtipUpdateStatus);
        this.addWidget("_tailtip-kill-line", this::tailtipKillLine);
        this.addWidget("_tailtip-kill-whole-line", this::tailtipKillWholeLine);
        this.addWidget("tailtip-window", this::toggleWindow);
        this.addWidget("tailtip-toggle", this::toggleKeyBindings);
    }
    
    public void setTailTips(final Map<String, CmdDesc> tailTips) {
        this.cmdDescs.setDescriptions(tailTips);
    }
    
    public void setDescriptionSize(final int descriptionSize) {
        this.descriptionSize = descriptionSize;
        this.initDescription();
    }
    
    public int getDescriptionSize() {
        return this.descriptionSize;
    }
    
    public void setTipType(final TipType type) {
        this.tipType = type;
        if (this.tipType == TipType.TAIL_TIP) {
            this.setSuggestionType(LineReader.SuggestionType.TAIL_TIP);
        }
        else {
            this.setSuggestionType(LineReader.SuggestionType.COMPLETER);
        }
    }
    
    public TipType getTipType() {
        return this.tipType;
    }
    
    public boolean isEnabled() {
        return this.enabled;
    }
    
    public void disable() {
        if (this.enabled) {
            this.toggleKeyBindings();
        }
    }
    
    public void enable() {
        if (!this.enabled) {
            this.toggleKeyBindings();
        }
    }
    
    public void setDescriptionCache(final boolean cache) {
        this.descriptionCache = cache;
    }
    
    public boolean tailtipComplete() {
        if (this.doTailTip("expand-or-complete")) {
            if ("\t".equals(this.lastBinding())) {
                this.callWidget("backward-char");
                this.reader.runMacro(KeyMap.key(this.reader.getTerminal(), InfoCmp.Capability.key_right));
            }
            return true;
        }
        return false;
    }
    
    public boolean tailtipAcceptLine() {
        if (this.tipType != TipType.TAIL_TIP) {
            this.setSuggestionType(LineReader.SuggestionType.COMPLETER);
        }
        this.clearDescription();
        this.setErrorPattern(null);
        this.setErrorIndex(-1);
        this.cmdDescs.clearTemporaryDescs();
        return this.clearTailTip("accept-line");
    }
    
    public boolean tailtipBackwardDelete() {
        return this.doTailTip(this.autopairEnabled() ? "_autopair-backward-delete-char" : "backward-delete-char");
    }
    
    private boolean clearTailTip(final String widget) {
        this.clearTailTip();
        this.callWidget(widget);
        return true;
    }
    
    public boolean tailtipDelete() {
        this.clearTailTip();
        return this.doTailTip("delete-char");
    }
    
    public boolean tailtipKillLine() {
        this.clearTailTip();
        return this.doTailTip("kill-line");
    }
    
    public boolean tailtipKillWholeLine() {
        this.callWidget("kill-whole-line");
        return this.doTailTip("redisplay");
    }
    
    public boolean tailtipInsert() {
        return this.doTailTip(this.autopairEnabled() ? "_autopair-insert" : "self-insert");
    }
    
    public boolean tailtipUpdateStatus() {
        return this.doTailTip("redisplay");
    }
    
    private boolean doTailTip(final String widget) {
        final Buffer buffer = this.buffer();
        this.callWidget(widget);
        final List<String> args = this.args();
        final Pair<String, Boolean> cmdkey = this.cmdDescs.evaluateCommandLine(buffer.toString(), this.args(), buffer.cursor());
        final CmdDesc cmdDesc = this.cmdDescs.getDescription(cmdkey.getU());
        if (cmdDesc == null) {
            this.setErrorPattern(null);
            this.setErrorIndex(-1);
            this.clearDescription();
            this.resetTailTip();
        }
        else if (cmdDesc.isValid()) {
            if (cmdkey.getV()) {
                if (cmdDesc.isCommand() && buffer.length() == buffer.cursor()) {
                    this.doCommandTailTip(widget, cmdDesc, args);
                }
            }
            else {
                this.doDescription(this.compileMainDescription(cmdDesc, this.descriptionSize));
                this.setErrorPattern(cmdDesc.getErrorPattern());
                this.setErrorIndex(cmdDesc.getErrorIndex());
            }
        }
        return true;
    }
    
    private void doCommandTailTip(final String widget, final CmdDesc cmdDesc, final List<String> args) {
        int argnum = 0;
        String prevArg = "";
        for (final String a : args) {
            if (!a.startsWith("-") && (!prevArg.matches("-[a-zA-Z]") || !cmdDesc.optionWithValue(prevArg))) {
                ++argnum;
            }
            prevArg = a;
        }
        String lastArg = "";
        prevArg = args.get(args.size() - 1);
        if (!this.prevChar().equals(" ") && args.size() > 1) {
            lastArg = args.get(args.size() - 1);
            prevArg = args.get(args.size() - 2);
        }
        int bpsize = argnum;
        boolean doTailTip = true;
        boolean noCompleters = false;
        if (widget.endsWith("backward-delete-char")) {
            this.setSuggestionType(LineReader.SuggestionType.TAIL_TIP);
            noCompleters = true;
            if (!lastArg.startsWith("-") && (!prevArg.matches("-[a-zA-Z]") || !cmdDesc.optionWithValue(prevArg))) {
                --bpsize;
            }
            if (this.prevChar().equals(" ")) {
                ++bpsize;
            }
        }
        else if (!this.prevChar().equals(" ")) {
            doTailTip = false;
            this.doDescription(this.compileMainDescription(cmdDesc, this.descriptionSize, cmdDesc.isSubcommand() ? lastArg : null));
        }
        else if (cmdDesc != null) {
            this.doDescription(this.compileMainDescription(cmdDesc, this.descriptionSize));
        }
        if (cmdDesc != null) {
            if (prevArg.startsWith("-") && !prevArg.contains("=") && !prevArg.matches("-[a-zA-Z][\\S]+") && cmdDesc.optionWithValue(prevArg)) {
                this.doDescription(this.compileOptionDescription(cmdDesc, prevArg, this.descriptionSize));
                this.setTipType(this.tipType);
            }
            else if (lastArg.matches("-[a-zA-Z][\\S]+") && cmdDesc.optionWithValue(lastArg.substring(0, 2))) {
                this.doDescription(this.compileOptionDescription(cmdDesc, lastArg.substring(0, 2), this.descriptionSize));
                this.setTipType(this.tipType);
            }
            else if (lastArg.startsWith("-")) {
                this.doDescription(this.compileOptionDescription(cmdDesc, lastArg, this.descriptionSize));
                if (!lastArg.contains("=")) {
                    this.setSuggestionType(LineReader.SuggestionType.TAIL_TIP);
                    noCompleters = true;
                }
                else {
                    this.setTipType(this.tipType);
                }
            }
            else if (!widget.endsWith("backward-delete-char")) {
                this.setTipType(this.tipType);
            }
            if (bpsize > 0 && doTailTip) {
                final List<ArgDesc> params = cmdDesc.getArgsDesc();
                if (!noCompleters) {
                    this.setSuggestionType((this.tipType == TipType.COMPLETER) ? LineReader.SuggestionType.COMPLETER : LineReader.SuggestionType.TAIL_TIP);
                }
                if (bpsize - 1 < params.size()) {
                    if (!lastArg.startsWith("-")) {
                        List<AttributedString> d;
                        if (!prevArg.startsWith("-") || !cmdDesc.optionWithValue(prevArg)) {
                            d = params.get(bpsize - 1).getDescription();
                        }
                        else {
                            d = this.compileOptionDescription(cmdDesc, prevArg, this.descriptionSize);
                        }
                        if (d == null || d.isEmpty()) {
                            d = this.compileMainDescription(cmdDesc, this.descriptionSize, cmdDesc.isSubcommand() ? lastArg : null);
                        }
                        this.doDescription(d);
                    }
                    final StringBuilder tip = new StringBuilder();
                    for (int i = bpsize - 1; i < params.size(); ++i) {
                        tip.append(params.get(i).getName());
                        tip.append(" ");
                    }
                    this.setTailTip(tip.toString());
                }
                else if (!params.isEmpty() && params.get(params.size() - 1).getName().startsWith("[")) {
                    this.setTailTip(params.get(params.size() - 1).getName());
                    this.doDescription(params.get(params.size() - 1).getDescription());
                }
            }
            else if (doTailTip) {
                this.resetTailTip();
            }
        }
        else {
            this.clearDescription();
            this.resetTailTip();
        }
    }
    
    private void resetTailTip() {
        this.setTailTip("");
        if (this.tipType != TipType.TAIL_TIP) {
            this.setSuggestionType(LineReader.SuggestionType.COMPLETER);
        }
    }
    
    private void doDescription(final List<AttributedString> desc) {
        if (this.descriptionSize == 0 || !this.descriptionEnabled) {
            return;
        }
        List<AttributedString> list = desc;
        if (list.size() > this.descriptionSize) {
            final AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.append(list.get(this.descriptionSize - 1)).append("\u2026", new AttributedStyle(AttributedStyle.INVERSE));
            final List<AttributedString> mod = new ArrayList<AttributedString>(list.subList(0, this.descriptionSize - 1));
            mod.add(asb.toAttributedString());
            list = mod;
        }
        else if (list.size() < this.descriptionSize) {
            final List<AttributedString> mod2 = new ArrayList<AttributedString>(list);
            while (mod2.size() != this.descriptionSize) {
                mod2.add(new AttributedString(""));
            }
            list = mod2;
        }
        this.setDescription(list);
    }
    
    public void initDescription() {
        Status.getStatus(this.reader.getTerminal()).setBorder(true);
        this.clearDescription();
    }
    
    @Override
    public void clearDescription() {
        this.doDescription(Collections.emptyList());
    }
    
    private boolean autopairEnabled() {
        final Binding binding = this.getKeyMap().getBound("(");
        return binding instanceof Reference && ((Reference)binding).name().equals("_autopair-insert");
    }
    
    public boolean toggleWindow() {
        this.descriptionEnabled = !this.descriptionEnabled;
        if (this.descriptionEnabled) {
            this.initDescription();
        }
        else {
            this.destroyDescription();
        }
        this.callWidget("redraw-line");
        return true;
    }
    
    public boolean toggleKeyBindings() {
        if (this.enabled) {
            this.defaultBindings();
            this.destroyDescription();
            this.reader.setVariable("errors", this.readerErrors);
        }
        else {
            this.customBindings();
            if (this.descriptionEnabled) {
                this.initDescription();
            }
            this.readerErrors = this.reader.getVariable("errors");
            this.reader.setVariable("errors", 0);
        }
        try {
            this.callWidget("redraw-line");
        }
        catch (final Exception ex) {}
        return this.enabled;
    }
    
    private boolean defaultBindings() {
        if (!this.enabled) {
            return false;
        }
        this.aliasWidget(".accept-line", "accept-line");
        this.aliasWidget(".backward-delete-char", "backward-delete-char");
        this.aliasWidget(".delete-char", "delete-char");
        this.aliasWidget(".expand-or-complete", "expand-or-complete");
        this.aliasWidget(".self-insert", "self-insert");
        this.aliasWidget(".redisplay", "redisplay");
        this.aliasWidget(".kill-line", "kill-line");
        this.aliasWidget(".kill-whole-line", "kill-whole-line");
        final KeyMap<Binding> map = this.getKeyMap();
        map.bind(new Reference("insert-close-paren"), ")");
        this.setSuggestionType(LineReader.SuggestionType.NONE);
        if (this.autopairEnabled()) {
            this.callWidget("autopair-toggle");
            this.callWidget("autopair-toggle");
        }
        this.enabled = false;
        return true;
    }
    
    private void customBindings() {
        if (this.enabled) {
            return;
        }
        this.aliasWidget("_tailtip-accept-line", "accept-line");
        this.aliasWidget("_tailtip-backward-delete-char", "backward-delete-char");
        this.aliasWidget("_tailtip-delete-char", "delete-char");
        this.aliasWidget("_tailtip-expand-or-complete", "expand-or-complete");
        this.aliasWidget("_tailtip-self-insert", "self-insert");
        this.aliasWidget("_tailtip-redisplay", "redisplay");
        this.aliasWidget("_tailtip-kill-line", "kill-line");
        this.aliasWidget("_tailtip-kill-whole-line", "kill-whole-line");
        final KeyMap<Binding> map = this.getKeyMap();
        map.bind(new Reference("_tailtip-self-insert"), ")");
        if (this.tipType != TipType.TAIL_TIP) {
            this.setSuggestionType(LineReader.SuggestionType.COMPLETER);
        }
        else {
            this.setSuggestionType(LineReader.SuggestionType.TAIL_TIP);
        }
        this.enabled = true;
    }
    
    private List<AttributedString> compileMainDescription(final CmdDesc cmdDesc, final int descriptionSize) {
        return this.compileMainDescription(cmdDesc, descriptionSize, null);
    }
    
    private List<AttributedString> compileMainDescription(final CmdDesc cmdDesc, final int descriptionSize, final String lastArg) {
        if (descriptionSize == 0 || !this.descriptionEnabled) {
            return new ArrayList<AttributedString>();
        }
        List<AttributedString> out = new ArrayList<AttributedString>();
        List<AttributedString> mainDesc = cmdDesc.getMainDesc();
        if (mainDesc == null) {
            return out;
        }
        if (cmdDesc.isCommand() && cmdDesc.isValid() && !cmdDesc.isHighlighted()) {
            mainDesc = new ArrayList<AttributedString>();
            final StyleResolver resolver = Options.HelpException.defaultStyle();
            for (final AttributedString as : cmdDesc.getMainDesc()) {
                mainDesc.add(Options.HelpException.highlightSyntax(as.toString(), resolver));
            }
        }
        if (mainDesc.size() <= descriptionSize && lastArg == null) {
            out.addAll(mainDesc);
        }
        else {
            int tabs = 0;
            for (final AttributedString as : mainDesc) {
                if (as.columnLength() >= tabs) {
                    tabs = as.columnLength() + 2;
                }
            }
            int row = 0;
            int col = 0;
            final List<AttributedString> descList = new ArrayList<AttributedString>();
            for (int i = 0; i < descriptionSize; ++i) {
                descList.add(new AttributedString(""));
            }
            for (final AttributedString as2 : mainDesc) {
                if (lastArg != null && !as2.toString().startsWith(lastArg)) {
                    continue;
                }
                final AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs);
                if (col > 0) {
                    asb.append(descList.get(row));
                    asb.append("\t");
                }
                asb.append(as2);
                descList.remove(row);
                descList.add(row, asb.toAttributedString());
                if (++row < descriptionSize) {
                    continue;
                }
                row = 0;
                ++col;
            }
            out = new ArrayList<AttributedString>(descList);
        }
        return out;
    }
    
    private List<AttributedString> compileOptionDescription(final CmdDesc cmdDesc, String opt, final int descriptionSize) {
        if (descriptionSize == 0 || !this.descriptionEnabled) {
            return new ArrayList<AttributedString>();
        }
        List<AttributedString> out = new ArrayList<AttributedString>();
        final Map<String, List<AttributedString>> optsDesc = cmdDesc.getOptsDesc();
        final StyleResolver resolver = Options.HelpException.defaultStyle();
        if (!opt.startsWith("-")) {
            return out;
        }
        final int ind = opt.indexOf("=");
        if (ind > 0) {
            opt = opt.substring(0, ind);
        }
        final List<String> matched = new ArrayList<String>();
        int tabs = 0;
        for (final String key : optsDesc.keySet()) {
            final String[] split = key.split("\\s+");
            final int length = split.length;
            int l = 0;
            while (l < length) {
                final String k = split[l];
                if (k.trim().startsWith(opt)) {
                    matched.add(key);
                    if (key.length() >= tabs) {
                        tabs = key.length() + 2;
                        break;
                    }
                    break;
                }
                else {
                    ++l;
                }
            }
        }
        if (matched.size() == 1) {
            out.add(Options.HelpException.highlightSyntax(matched.get(0), resolver));
            for (final AttributedString as : optsDesc.get(matched.get(0))) {
                final AttributedStringBuilder asb = new AttributedStringBuilder().tabs(8);
                asb.append("\t");
                asb.append(as);
                out.add(asb.toAttributedString());
            }
        }
        else if (matched.size() <= descriptionSize) {
            for (final String key : matched) {
                final AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs);
                asb.append(Options.HelpException.highlightSyntax(key, resolver));
                asb.append("\t");
                asb.append(cmdDesc.optionDescription(key));
                out.add(asb.toAttributedString());
            }
        }
        else if (matched.size() <= 2 * descriptionSize) {
            final List<AttributedString> keyList = new ArrayList<AttributedString>();
            int row = 0;
            int columnWidth;
            for (columnWidth = 2 * tabs; columnWidth < 50; columnWidth += tabs) {}
            for (final String key2 : matched) {
                AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(tabs);
                if (row < descriptionSize) {
                    asb2.append(Options.HelpException.highlightSyntax(key2, resolver));
                    asb2.append("\t");
                    asb2.append(cmdDesc.optionDescription(key2));
                    if (asb2.columnLength() > columnWidth - 2) {
                        final AttributedString trunc = asb2.columnSubSequence(0, columnWidth - 5);
                        asb2 = new AttributedStringBuilder().tabs(tabs);
                        asb2.append(trunc);
                        asb2.append("...", new AttributedStyle(AttributedStyle.INVERSE));
                        asb2.append("  ");
                    }
                    else {
                        for (int i = asb2.columnLength(); i < columnWidth; ++i) {
                            asb2.append(" ");
                        }
                    }
                    keyList.add(asb2.toAttributedString().columnSubSequence(0, columnWidth));
                }
                else {
                    asb2.append(keyList.get(row - descriptionSize));
                    asb2.append(Options.HelpException.highlightSyntax(key2, resolver));
                    asb2.append("\t");
                    asb2.append(cmdDesc.optionDescription(key2));
                    keyList.remove(row - descriptionSize);
                    keyList.add(row - descriptionSize, asb2.toAttributedString());
                }
                ++row;
            }
            out = new ArrayList<AttributedString>(keyList);
        }
        else {
            final List<AttributedString> keyList = new ArrayList<AttributedString>();
            for (int j = 0; j < descriptionSize; ++j) {
                keyList.add(new AttributedString(""));
            }
            int row = 0;
            for (final String key3 : matched) {
                final AttributedStringBuilder asb3 = new AttributedStringBuilder().tabs(tabs);
                asb3.append(keyList.get(row));
                asb3.append(Options.HelpException.highlightSyntax(key3, resolver));
                asb3.append("\t");
                keyList.remove(row);
                keyList.add(row, asb3.toAttributedString());
                if (++row >= descriptionSize) {
                    row = 0;
                }
            }
            out = new ArrayList<AttributedString>(keyList);
        }
        return out;
    }
    
    public enum TipType
    {
        TAIL_TIP, 
        COMPLETER, 
        COMBINED;
    }
    
    private class CommandDescriptions
    {
        Map<String, CmdDesc> descriptions;
        Map<String, CmdDesc> temporaryDescs;
        Map<String, CmdDesc> volatileDescs;
        Function<CmdLine, CmdDesc> descFun;
        
        public CommandDescriptions(final Map<String, CmdDesc> descriptions) {
            this.descriptions = new HashMap<String, CmdDesc>();
            this.temporaryDescs = new HashMap<String, CmdDesc>();
            this.volatileDescs = new HashMap<String, CmdDesc>();
            this.descriptions = new HashMap<String, CmdDesc>(descriptions);
        }
        
        public CommandDescriptions(final Function<CmdLine, CmdDesc> descFun) {
            this.descriptions = new HashMap<String, CmdDesc>();
            this.temporaryDescs = new HashMap<String, CmdDesc>();
            this.volatileDescs = new HashMap<String, CmdDesc>();
            this.descFun = descFun;
        }
        
        public void setDescriptions(final Map<String, CmdDesc> descriptions) {
            this.descriptions = new HashMap<String, CmdDesc>(descriptions);
        }
        
        public Pair<String, Boolean> evaluateCommandLine(final String line, final int curPos) {
            return this.evaluateCommandLine(line, TailTipWidgets.this.args(), curPos);
        }
        
        public Pair<String, Boolean> evaluateCommandLine(final String line, final List<String> args) {
            return this.evaluateCommandLine(line, args, line.length());
        }
        
        private Pair<String, Boolean> evaluateCommandLine(final String line, final List<String> args, final int curPos) {
            String cmd = null;
            CmdLine.DescriptionType descType = CmdLine.DescriptionType.METHOD;
            String head = line.substring(0, curPos);
            String tail = line.substring(curPos);
            if (TailTipWidgets.this.prevChar().equals(")")) {
                descType = CmdLine.DescriptionType.SYNTAX;
                cmd = head;
            }
            else {
                if (line.length() == curPos) {
                    cmd = ((args != null && (args.size() > 1 || (args.size() == 1 && line.endsWith(" ")))) ? TailTipWidgets.this.parser().getCommand(args.get(0)) : null);
                    descType = CmdLine.DescriptionType.COMMAND;
                }
                int brackets = 0;
                for (int i = head.length() - 1; i >= 0; --i) {
                    if (head.charAt(i) == ')') {
                        ++brackets;
                    }
                    else if (head.charAt(i) == '(') {
                        --brackets;
                    }
                    if (brackets < 0) {
                        descType = CmdLine.DescriptionType.METHOD;
                        head = (cmd = head.substring(0, i));
                        break;
                    }
                }
                if (descType == CmdLine.DescriptionType.METHOD) {
                    brackets = 0;
                    for (int i = 0; i < tail.length(); ++i) {
                        if (tail.charAt(i) == ')') {
                            ++brackets;
                        }
                        else if (tail.charAt(i) == '(') {
                            --brackets;
                        }
                        if (brackets > 0) {
                            tail = tail.substring(i + 1);
                            break;
                        }
                    }
                }
            }
            if (cmd != null && this.descFun != null && !this.descriptions.containsKey(cmd) && !this.temporaryDescs.containsKey(cmd)) {
                final CmdDesc c = this.descFun.apply(new CmdLine(line, head, tail, args, descType));
                if (descType == CmdLine.DescriptionType.COMMAND) {
                    if (!TailTipWidgets.this.descriptionCache) {
                        this.volatileDescs.put(cmd, c);
                    }
                    else if (c != null) {
                        this.descriptions.put(cmd, c);
                    }
                    else {
                        this.temporaryDescs.put(cmd, null);
                    }
                }
                else {
                    this.temporaryDescs.put(cmd, c);
                }
            }
            return new Pair<String, Boolean>(cmd, descType == CmdLine.DescriptionType.COMMAND);
        }
        
        public CmdDesc getDescription(final String command) {
            CmdDesc out;
            if (this.descriptions.containsKey(command)) {
                out = this.descriptions.get(command);
            }
            else if (this.temporaryDescs.containsKey(command)) {
                out = this.temporaryDescs.get(command);
            }
            else {
                out = this.volatileDescs.remove(command);
            }
            return out;
        }
        
        public void clearTemporaryDescs() {
            this.temporaryDescs.clear();
        }
    }
    
    static class Pair<U, V>
    {
        final U u;
        final V v;
        
        public Pair(final U u, final V v) {
            this.u = u;
            this.v = v;
        }
        
        public U getU() {
            return this.u;
        }
        
        public V getV() {
            return this.v;
        }
    }
}
