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

package org.jline.console.impl;

import java.io.File;
import java.math.BigDecimal;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.Objects;
import org.jline.utils.AttributedStyle;
import java.util.Collections;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.Log;
import org.jline.console.CmdDesc;
import org.jline.builtins.Styles;
import java.util.Collection;
import java.util.Date;
import java.util.ArrayList;
import org.jline.console.SystemRegistry;
import org.jline.terminal.Terminal;
import java.util.List;
import org.jline.console.CommandInput;
import java.util.Arrays;
import org.jline.builtins.Options;
import java.util.Iterator;
import java.util.HashMap;
import org.jline.builtins.SyntaxHighlighter;
import java.util.LinkedHashMap;
import org.jline.utils.StyleResolver;
import org.jline.builtins.ConfigurationPath;
import org.jline.console.ScriptEngine;
import org.jline.utils.AttributedString;
import java.util.function.Function;
import java.util.Map;
import org.jline.console.Printer;

public class DefaultPrinter extends JlineCommandRegistry implements Printer
{
    protected static final String VAR_PRNT_OPTIONS = "PRNT_OPTIONS";
    protected static final int PRNT_MAX_ROWS = 100000;
    protected static final int PRNT_MAX_DEPTH = 1;
    protected static final int PRNT_INDENTION = 4;
    private static final int NANORC_MAX_STRING_LENGTH = 400;
    private static final int HIGHLIGHTER_CACHE_SIZE = 5;
    private Map<Class<?>, Function<Object, Map<String, Object>>> objectToMap;
    private Map<Class<?>, Function<Object, String>> objectToString;
    private Map<String, Function<Object, AttributedString>> highlightValue;
    private int totLines;
    private final ScriptEngine engine;
    private final ConfigurationPath configPath;
    private StyleResolver prntStyle;
    private final LinkedHashMap<String, SyntaxHighlighter> highlighters;
    
    public DefaultPrinter(final ConfigurationPath configPath) {
        this(null, configPath);
    }
    
    public DefaultPrinter(final ScriptEngine engine, final ConfigurationPath configPath) {
        this.objectToMap = new HashMap<Class<?>, Function<Object, Map<String, Object>>>();
        this.objectToString = new HashMap<Class<?>, Function<Object, String>>();
        this.highlightValue = new HashMap<String, Function<Object, AttributedString>>();
        this.highlighters = new LinkedHashMap<String, SyntaxHighlighter>(6, 0.75f, false) {
            @Override
            protected boolean removeEldestEntry(final Map.Entry<String, SyntaxHighlighter> eldest) {
                return this.size() > 5;
            }
        };
        this.engine = engine;
        this.configPath = configPath;
    }
    
    @Override
    public void println(final Object object) {
        this.internalPrintln(this.defaultPrntOptions(false), object);
    }
    
    @Override
    public void println(final Map<String, Object> optionsIn, final Object object) {
        final Map<String, Object> options = new HashMap<String, Object>(optionsIn);
        for (final Map.Entry<String, Object> entry : this.defaultPrntOptions(options.containsKey("skipDefaultOptions")).entrySet()) {
            options.putIfAbsent(entry.getKey(), entry.getValue());
        }
        this.manageBooleanOptions(options);
        this.internalPrintln(options, object);
    }
    
    @Override
    public boolean refresh() {
        this.highlighters.clear();
        return true;
    }
    
    public String[] appendUsage(final String[] customUsage) {
        final String[] usage = { "prnt -  print object", "Usage: prnt [OPTIONS] object", "  -? --help                       Displays command help", "  -a --all                        Ignore columnsOut configuration", "  -b --border=CHAR                Table cell vertical border character", "  -c --columns=COLUMNS,...        Display given columns on map/table", "  -e --exclude=COLUMNS,...        Exclude given columns on table", "  -i --include=COLUMNS,...        Include given columns on table", "     --indention=INDENTION        Indention size", "     --maxColumnWidth=WIDTH       Maximum column width", "  -d --maxDepth=DEPTH             Maximum depth objects are resolved", "  -n --maxrows=ROWS               Maximum number of lines to display", "  -m --multiColumns               Display the collection of simple data in multiple columns", "     --oneRowTable                Display one row data on table", "  -h --rowHighlight=ROW           Highlight table rows. ROW = EVEN, ODD, ALL", "  -r --rownum                     Display table row numbers", "     --shortNames                 Truncate table column names (property.field -> field)", "     --skipDefaultOptions         Ignore all options defined in PRNT_OPTIONS", "     --structsOnTable             Display structs and lists on table", "  -s --style=STYLE                Use nanorc STYLE to highlight Object.", "                                  STYLE = JSON serialize object to JSON string before printing", "     --toString                   Use object's toString() method to get print value", "                                  DEFAULT: object's fields are put to property map before printing", "     --valueStyle=STYLE           Use nanorc style to highlight string and column/map values", "  -w --width=WIDTH                Display width (default terminal width)" };
        String[] out;
        if (customUsage == null || customUsage.length == 0) {
            out = usage;
        }
        else {
            out = new String[usage.length + customUsage.length];
            System.arraycopy(usage, 0, out, 0, usage.length);
            System.arraycopy(customUsage, 0, out, usage.length, customUsage.length);
        }
        return out;
    }
    
    public Map<String, Object> compileOptions(final Options opt) {
        final Map<String, Object> options = new HashMap<String, Object>();
        if (opt.isSet("skipDefaultOptions")) {
            options.put("skipDefaultOptions", true);
        }
        else if (opt.isSet("style")) {
            options.put("style", opt.get("style"));
        }
        if (opt.isSet("toString")) {
            options.put("toString", true);
        }
        if (opt.isSet("width")) {
            options.put("width", opt.getNumber("width"));
        }
        if (opt.isSet("rownum")) {
            options.put("rownum", true);
        }
        if (opt.isSet("oneRowTable")) {
            options.put("oneRowTable", true);
        }
        if (opt.isSet("shortNames")) {
            options.put("shortNames", true);
        }
        if (opt.isSet("structsOnTable")) {
            options.put("structsOnTable", true);
        }
        if (opt.isSet("columns")) {
            options.put("columns", Arrays.asList(opt.get("columns").split(",")));
        }
        if (opt.isSet("exclude")) {
            options.put("exclude", Arrays.asList(opt.get("exclude").split(",")));
        }
        if (opt.isSet("include")) {
            options.put("include", Arrays.asList(opt.get("include").split(",")));
        }
        if (opt.isSet("all")) {
            options.put("all", true);
        }
        if (opt.isSet("maxrows")) {
            options.put("maxrows", opt.getNumber("maxrows"));
        }
        if (opt.isSet("maxColumnWidth")) {
            options.put("maxColumnWidth", opt.getNumber("maxColumnWidth"));
        }
        if (opt.isSet("maxDepth")) {
            options.put("maxDepth", opt.getNumber("maxDepth"));
        }
        if (opt.isSet("indention")) {
            options.put("indention", opt.getNumber("indention"));
        }
        if (opt.isSet("valueStyle")) {
            options.put("valueStyle", opt.get("valueStyle"));
        }
        if (opt.isSet("border")) {
            options.put("border", opt.get("border"));
        }
        if (opt.isSet("rowHighlight")) {
            try {
                options.put("rowHighlight", this.optionRowHighlight(opt.get("rowHighlight")));
            }
            catch (final Exception e) {
                final RuntimeException exception = new BadOptionValueException("rowHighlight has a bad value: " + opt.get("rowHighlight"));
                exception.addSuppressed(e);
                throw exception;
            }
        }
        if (opt.isSet("multiColumns")) {
            options.put("multiColumns", true);
        }
        options.put("exception", "stack");
        return options;
    }
    
    private TableRows optionRowHighlight(final Object value) {
        if (value instanceof TableRows || value == null) {
            return (TableRows)value;
        }
        if (!(value instanceof String)) {
            throw new IllegalArgumentException("rowHighlight has a bad option value type: " + value.getClass());
        }
        final String val = ((String)value).trim().toUpperCase();
        if (!val.isEmpty() && !val.equals("NULL")) {
            return TableRows.valueOf(val);
        }
        return null;
    }
    
    @Override
    public Exception prntCommand(final CommandInput input) {
        Exception out = null;
        final String[] usage = this.appendUsage(null);
        try {
            final Options opt = this.parseOptions(usage, input.xargs());
            final Map<String, Object> options = this.compileOptions(opt);
            final List<Object> args = opt.argObjects();
            if (!args.isEmpty()) {
                this.println(options, args.get(0));
            }
        }
        catch (final Exception e) {
            out = e;
        }
        return out;
    }
    
    public void setObjectToMap(final Map<Class<?>, Function<Object, Map<String, Object>>> objectToMap) {
        this.objectToMap = objectToMap;
    }
    
    public void setObjectToString(final Map<Class<?>, Function<Object, String>> objectToString) {
        this.objectToString = objectToString;
    }
    
    public void setHighlightValue(final Map<String, Function<Object, AttributedString>> highlightValue) {
        this.highlightValue = highlightValue;
    }
    
    protected Terminal terminal() {
        return SystemRegistry.get().terminal();
    }
    
    protected void manageBooleanOptions(final Map<String, Object> options) {
        for (final String key : Printer.BOOLEAN_KEYS) {
            final Object option = options.get(key);
            final boolean value = option instanceof Boolean && (boolean)option;
            if (!value) {
                options.remove(key);
            }
        }
    }
    
    protected Map<String, Object> defaultPrntOptions(final boolean skipDefault) {
        final Map<String, Object> out = new HashMap<String, Object>();
        if (this.engine != null && !skipDefault && this.engine.hasVariable("PRNT_OPTIONS")) {
            out.putAll((Map<? extends String, ?>)this.engine.get("PRNT_OPTIONS"));
            out.remove("skipDefaultOptions");
            this.manageBooleanOptions(out);
        }
        out.putIfAbsent("maxrows", 100000);
        out.putIfAbsent("maxDepth", 1);
        out.putIfAbsent("indention", 4);
        out.putIfAbsent("columnsOut", new ArrayList());
        out.putIfAbsent("columnsIn", new ArrayList());
        if (this.engine == null) {
            out.remove("objectToMap");
            out.remove("objectToString");
            out.remove("highlightValue");
        }
        return out;
    }
    
    private void internalPrintln(final Map<String, Object> options, final Object object) {
        if (object == null) {
            return;
        }
        final long start = new Date().getTime();
        if (options.containsKey("exclude")) {
            final List<String> colOut = this.optionList("exclude", options);
            final List<String> colIn = this.optionList("columnsIn", options);
            colIn.removeAll(colOut);
            colOut.addAll(options.get("columnsOut"));
            options.put("columnsIn", colIn);
            options.put("columnsOut", colOut);
        }
        if (options.containsKey("include")) {
            final List<String> colIn2 = this.optionList("include", options);
            colIn2.addAll(options.get("columnsIn"));
            options.put("columnsIn", colIn2);
        }
        options.put("valueStyle", this.valueHighlighter(options.getOrDefault("valueStyle", null)));
        this.prntStyle = Styles.prntStyle();
        options.putIfAbsent("width", this.terminal().getSize().getColumns());
        final String style = options.getOrDefault("style", "");
        options.put("style", this.valueHighlighter(style));
        final int width = options.get("width");
        final int maxrows = options.get("maxrows");
        if (!style.isEmpty() && object instanceof String) {
            this.highlightAndPrint(width, options.get("style"), (String)object, true, maxrows);
        }
        else if (style.equalsIgnoreCase("JSON")) {
            if (this.engine == null) {
                throw new IllegalArgumentException("JSON style not supported!");
            }
            final String json = this.engine.toJson(object);
            this.highlightAndPrint(width, options.get("style"), json, true, maxrows);
        }
        else if (options.containsKey("skipDefaultOptions")) {
            this.highlightAndPrint(options, object);
        }
        else if (object instanceof Exception) {
            this.highlightAndPrint(options, (Throwable)object);
        }
        else if (object instanceof CmdDesc) {
            this.highlight((CmdDesc)object).println(this.terminal());
        }
        else if (object instanceof String || object instanceof Number) {
            final String str = object.toString();
            final SyntaxHighlighter highlighter = options.getOrDefault("valueStyle", null);
            this.highlightAndPrint(width, highlighter, str, this.doValueHighlight(options, str), maxrows);
        }
        else {
            this.highlightAndPrint(options, object);
        }
        this.terminal().flush();
        Log.debug("println: ", new Date().getTime() - start, " msec");
    }
    
    protected void highlightAndPrint(final Map<String, Object> options, final Throwable exception) {
        SystemRegistry.get().trace(options.getOrDefault("exception", "stack").equals("stack"), exception);
    }
    
    private AttributedString highlight(final CmdDesc cmdDesc) {
        final StringBuilder sb = new StringBuilder();
        for (final AttributedString as : cmdDesc.getMainDesc()) {
            sb.append(as.toString());
            sb.append("\n");
        }
        final List<Integer> tabs = Arrays.asList(0, 2, 33);
        for (final Map.Entry<String, List<AttributedString>> entry : cmdDesc.getOptsDesc().entrySet()) {
            final AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.tabs(tabs);
            asb.append("\t");
            asb.append(entry.getKey());
            asb.append("\t");
            boolean first = true;
            for (final AttributedString as2 : entry.getValue()) {
                if (!first) {
                    asb.append("\t");
                    asb.append("\t");
                }
                asb.append(as2);
                asb.append("\n");
                first = false;
            }
            sb.append(asb);
        }
        return Options.HelpException.highlight(sb.toString(), Styles.helpStyle());
    }
    
    private SyntaxHighlighter valueHighlighter(final String style) {
        SyntaxHighlighter out;
        if (style == null || style.isEmpty()) {
            out = null;
        }
        else if (this.highlighters.containsKey(style)) {
            out = this.highlighters.get(style);
        }
        else if (style.matches("[a-z]+:.*")) {
            out = SyntaxHighlighter.build(style);
            this.highlighters.put(style, out);
        }
        else {
            Path nanorc = (this.configPath != null) ? this.configPath.getConfig("jnanorc") : null;
            if (this.engine != null && this.engine.hasVariable("NANORC")) {
                nanorc = Paths.get((String)this.engine.get("NANORC"), new String[0]);
            }
            if (nanorc == null) {
                nanorc = Paths.get("/etc/nanorc", new String[0]);
            }
            out = SyntaxHighlighter.build(nanorc, style);
            this.highlighters.put(style, out);
        }
        return out;
    }
    
    private String truncate4nanorc(final String obj) {
        String val = obj;
        if (val.length() > 400 && !val.contains("\n")) {
            val = val.substring(0, 399);
        }
        return val;
    }
    
    private AttributedString highlight(final Integer width, final SyntaxHighlighter highlighter, final String object, final boolean doValueHighlight) {
        final AttributedStringBuilder asb = new AttributedStringBuilder();
        String val = object;
        if (highlighter != null && doValueHighlight) {
            val = this.truncate4nanorc(object);
        }
        asb.append(val);
        AttributedString out;
        if (highlighter != null && val.length() < 400 && doValueHighlight) {
            out = highlighter.highlight(asb);
        }
        else {
            out = asb.toAttributedString();
        }
        if (width != null) {
            out = out.columnSubSequence(0, width);
        }
        return out;
    }
    
    private boolean doValueHighlight(final Map<String, Object> options, final String value) {
        return options.containsKey("valueStyleAll") || value.matches("\"(\\.|[^\"])*\"|'(\\.|[^'])*'") || (value.startsWith("[") && value.endsWith("]")) || (value.startsWith("(") && value.endsWith(")")) || (value.startsWith("{") && value.endsWith("}")) || (value.startsWith("<") && value.endsWith(">")) || (!value.contains(" ") && !value.contains("\t"));
    }
    
    private void highlightAndPrint(final int width, final SyntaxHighlighter highlighter, final String object, final boolean doValueHighlight, final int maxRows) {
        String lineBreak = null;
        if (object.indexOf("\r\n") >= 0) {
            lineBreak = "\r\n";
        }
        else if (object.indexOf("\n") >= 0) {
            lineBreak = "\n";
        }
        else if (object.indexOf("\r") >= 0) {
            lineBreak = "\r";
        }
        if (lineBreak == null) {
            this.highlightAndPrint(width, highlighter, object, doValueHighlight);
        }
        else {
            int rows = 0;
            int i0 = 0;
            while (rows < maxRows) {
                ++rows;
                final int i2 = object.indexOf(lineBreak, i0);
                final String line = (i2 >= 0) ? object.substring(i0, i2) : object.substring(i0);
                this.highlightAndPrint(width, highlighter, line, doValueHighlight);
                if (i2 < 0) {
                    break;
                }
                i0 = i2 + lineBreak.length();
            }
            if (rows == maxRows) {
                throw new TruncatedOutputException("Truncated output: " + maxRows);
            }
        }
    }
    
    private void highlightAndPrint(final int width, final SyntaxHighlighter highlighter, final String object, final boolean doValueHighlight) {
        final AttributedStringBuilder asb = new AttributedStringBuilder();
        final List<AttributedString> sas = asb.append(object).columnSplitLength(width);
        for (final AttributedString as : sas) {
            this.highlight(width, highlighter, as.toString(), doValueHighlight).println(this.terminal());
        }
    }
    
    private Map<String, Object> keysToString(final Map<Object, Object> map) {
        final Map<String, Object> out = new HashMap<String, Object>();
        for (final Map.Entry<Object, Object> entry : map.entrySet()) {
            if (entry.getKey() instanceof String) {
                out.put(entry.getKey(), entry.getValue());
            }
            else if (entry.getKey() != null) {
                out.put(entry.getKey().toString(), entry.getValue());
            }
            else {
                out.put("null", entry.getValue());
            }
        }
        return out;
    }
    
    private Object mapValue(final Map<String, Object> options, final String key, final Map<String, Object> map) {
        Object out = null;
        if (map.containsKey(key)) {
            out = map.get(key);
        }
        else if (key.contains(".")) {
            final String[] keys = key.split("\\.");
            out = map.get(keys[0]);
            for (int i = 1; i < keys.length; ++i) {
                if (out instanceof Map) {
                    final Map<String, Object> m = this.keysToString((Map<Object, Object>)out);
                    out = m.get(keys[i]);
                }
                else {
                    if (!this.canConvert(out)) {
                        break;
                    }
                    out = this.engine.toMap(out).get(keys[i]);
                }
            }
        }
        if (!(out instanceof Map) && this.canConvert(out)) {
            out = this.objectToMap(options, out);
        }
        return out;
    }
    
    private List<String> optionList(final String key, final Map<String, Object> options) {
        final List<String> out = new ArrayList<String>();
        final Object option = options.get(key);
        if (option instanceof String) {
            out.addAll(Arrays.asList(((String)option).split(",")));
        }
        else if (option instanceof Collection) {
            out.addAll((Collection<? extends String>)option);
        }
        else if (option != null) {
            throw new IllegalArgumentException("Unsupported option list: {key: " + key + ", type: " + option.getClass() + "}");
        }
        return out;
    }
    
    private boolean hasMatch(final List<String> regexes, final String value) {
        for (final String r : regexes) {
            if (value.matches(r)) {
                return true;
            }
        }
        return false;
    }
    
    private AttributedString addPadding(final AttributedString str, final int width) {
        final AttributedStringBuilder sb = new AttributedStringBuilder();
        for (int i = str.columnLength(); i < width; ++i) {
            sb.append(" ");
        }
        sb.append(str);
        return sb.toAttributedString();
    }
    
    private String addPadding(final String str, final int width) {
        final AttributedStringBuilder sb = new AttributedStringBuilder();
        for (int i = str.length(); i < width; ++i) {
            sb.append(" ");
        }
        sb.append(str);
        return sb.toString();
    }
    
    private String columnValue(final String value) {
        return value.replaceAll("\r", "CR").replaceAll("\n", "LF");
    }
    
    private Map<String, Object> objectToMap(final Map<String, Object> options, final Object obj) {
        if (obj != null) {
            final Map<Class<?>, Object> toMap = options.getOrDefault("objectToMap", Collections.emptyMap());
            if (toMap.containsKey(obj.getClass())) {
                return (Map)this.engine.execute(toMap.get(obj.getClass()), obj);
            }
            if (this.objectToMap.containsKey(obj.getClass())) {
                return this.objectToMap.get(obj.getClass()).apply(obj);
            }
        }
        return this.engine.toMap(obj);
    }
    
    private String objectToString(final Map<String, Object> options, final Object obj) {
        String out = "null";
        if (obj != null) {
            final Map<Class<?>, Object> toString = options.containsKey("objectToString") ? options.get("objectToString") : new HashMap<Class<?>, Object>();
            if (toString.containsKey(obj.getClass())) {
                out = (String)this.engine.execute(toString.get(obj.getClass()), obj);
            }
            else if (this.objectToString.containsKey(obj.getClass())) {
                out = this.objectToString.get(obj.getClass()).apply(obj);
            }
            else if (obj instanceof Class) {
                out = ((Class)obj).getName();
            }
            else if (this.engine != null) {
                out = this.engine.toString(obj);
            }
            else {
                out = obj.toString();
            }
        }
        return out;
    }
    
    private AttributedString highlightMapValue(final Map<String, Object> options, final String key, final Map<String, Object> map) {
        return this.highlightValue(options, key, this.mapValue(options, key, map));
    }
    
    private boolean isHighlighted(final AttributedString value) {
        for (int i = 0; i < value.length(); ++i) {
            if (value.styleAt(i).getStyle() != AttributedStyle.DEFAULT.getStyle()) {
                return true;
            }
        }
        return false;
    }
    
    private AttributedString highlightValue(final Map<String, Object> options, final String column, final Object obj) {
        AttributedString out = null;
        final Object raw = (options.containsKey("toString") && obj != null) ? this.objectToString(options, obj) : obj;
        final Map<String, Object> hv = options.containsKey("highlightValue") ? options.get("highlightValue") : new HashMap<String, Object>();
        if (column != null && this.simpleObject(raw)) {
            for (final Map.Entry<String, Object> entry : hv.entrySet()) {
                if (!entry.getKey().equals("*") && column.matches(entry.getKey())) {
                    out = (AttributedString)this.engine.execute(hv.get(entry.getKey()), raw);
                    break;
                }
            }
            if (out == null) {
                for (final Map.Entry<String, Function<Object, AttributedString>> entry2 : this.highlightValue.entrySet()) {
                    if (!entry2.getKey().equals("*") && column.matches(entry2.getKey())) {
                        out = this.highlightValue.get(entry2.getKey()).apply(raw);
                        break;
                    }
                }
            }
        }
        if (out == null) {
            if (raw instanceof String) {
                out = new AttributedString(this.columnValue((String)raw));
            }
            else {
                out = new AttributedString(this.columnValue(this.objectToString(options, raw)));
            }
        }
        if ((this.simpleObject(raw) || raw == null) && (hv.containsKey("*") || this.highlightValue.containsKey("*")) && !this.isHighlighted(out)) {
            if (hv.containsKey("*")) {
                out = (AttributedString)this.engine.execute(hv.get("*"), out);
            }
            final Function<Object, AttributedString> func = this.highlightValue.get("*");
            if (func != null) {
                out = func.apply(out);
            }
        }
        if (options.containsKey("valueStyle") && !this.isHighlighted(out)) {
            out = this.highlight(null, options.get("valueStyle"), out.toString(), this.doValueHighlight(options, out.toString()));
        }
        return this.truncateValue(options, out);
    }
    
    private AttributedString truncateValue(final Map<String, Object> options, final AttributedString value) {
        if (value.columnLength() > options.getOrDefault("maxColumnWidth", Integer.MAX_VALUE)) {
            final AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.append(value.columnSubSequence(0, options.get("maxColumnWidth") - 3));
            asb.append("...");
            return asb.toAttributedString();
        }
        return value;
    }
    
    private String truncateValue(final int maxWidth, final String value) {
        if (value.length() > maxWidth) {
            return (Object)value.subSequence(0, maxWidth - 3) + "...";
        }
        return value;
    }
    
    private List<Object> objectToList(final Object obj) {
        List<Object> out = new ArrayList<Object>();
        if (obj instanceof List) {
            out = (List)obj;
        }
        else if (obj instanceof Collection) {
            out.addAll((Collection<?>)obj);
        }
        else if (obj instanceof Object[]) {
            out.addAll(Arrays.asList((Object[])obj));
        }
        else if (obj instanceof Iterator) {
            final Iterator iterator = (Iterator)obj;
            final List<Object> obj2 = out;
            Objects.requireNonNull((ArrayList)obj2);
            iterator.forEachRemaining(obj2::add);
        }
        else if (obj instanceof Iterable) {
            final Iterable iterable = (Iterable)obj;
            final List<Object> obj3 = out;
            Objects.requireNonNull((ArrayList)obj3);
            iterable.forEach(obj3::add);
        }
        else {
            out.add(obj);
        }
        return out;
    }
    
    private boolean similarSets(final List<String> ref, final Set<String> c2, final int matchLimit) {
        boolean out = false;
        int limit = matchLimit;
        for (final String s : ref) {
            if (c2.contains(s) && --limit == 0) {
                out = true;
                break;
            }
        }
        return out;
    }
    
    private void println(final AttributedString line, final int maxrows) {
        line.println(this.terminal());
        ++this.totLines;
        if (this.totLines > maxrows) {
            this.totLines = 0;
            throw new TruncatedOutputException("Truncated output: " + maxrows);
        }
    }
    
    private String columnName(final String name, final boolean shortName) {
        String out = name;
        if (shortName) {
            final String[] p = name.split("\\.");
            out = p[p.length - 1];
        }
        return out;
    }
    
    private boolean isNumber(final String str) {
        return str.matches("-?\\d+(\\.\\d+)?");
    }
    
    private void highlightAndPrint(final Map<String, Object> options, final Object obj) {
        final int width = options.get("width");
        final int maxrows = options.get("maxrows");
        this.totLines = 0;
        String message = null;
        RuntimeException runtimeException = null;
        if (obj != null) {
            if (obj instanceof Map) {
                this.highlightMap(options, this.keysToString((Map<Object, Object>)obj), width);
            }
            else if (this.collectionObject(obj)) {
                List<Object> collection = this.objectToList(obj);
                if (collection.size() > maxrows) {
                    message = "Truncated output: " + maxrows + "/" + collection.size();
                    collection = collection.subList(collection.size() - maxrows, collection.size());
                }
                if (!collection.isEmpty()) {
                    if (collection.size() == 1 && !options.containsKey("oneRowTable")) {
                        final Object elem = collection.iterator().next();
                        if (elem instanceof Map) {
                            this.highlightMap(options, this.keysToString((Map<Object, Object>)elem), width);
                        }
                        else if (this.canConvert(elem) && !options.containsKey("toString")) {
                            this.highlightMap(options, this.objectToMap(options, elem), width);
                        }
                        else if (elem instanceof String && options.get("style") != null) {
                            this.highlightAndPrint(width, options.get("style"), (String)elem, true, maxrows);
                        }
                        else {
                            this.highlightValue(options, null, this.objectToString(options, obj)).println(this.terminal());
                        }
                    }
                    else {
                        String columnSep = "";
                        TableRows tableRows = null;
                        final boolean rownum = options.containsKey("rownum");
                        try {
                            columnSep = options.getOrDefault("border", "");
                            tableRows = this.optionRowHighlight(options.getOrDefault("rowHighlight", null));
                        }
                        catch (final Exception e) {
                            runtimeException = new BadOptionValueException("Option border or rowHighlight has a bad value!");
                            runtimeException.addSuppressed(e);
                        }
                        try {
                            final Object elem2 = collection.iterator().next();
                            final boolean convert = this.canConvert(elem2);
                            if ((elem2 instanceof Map || convert) && !options.containsKey("toString")) {
                                final List<Map<String, Object>> convertedCollection = new ArrayList<Map<String, Object>>();
                                final Set<String> keys = new HashSet<String>();
                                for (final Object o : collection) {
                                    final Map<String, Object> m = convert ? this.objectToMap(options, o) : this.keysToString((Map<Object, Object>)o);
                                    convertedCollection.add(m);
                                    keys.addAll(m.keySet());
                                }
                                final List<String> columnsIn = this.optionList("columnsIn", options);
                                final List<String> columnsOut = options.containsKey("all") ? new ArrayList<String>() : this.optionList("columnsOut", options);
                                List<String> _header;
                                if (options.containsKey("columns")) {
                                    _header = options.get("columns");
                                }
                                else {
                                    _header = columnsIn;
                                    final int k;
                                    _header.addAll(keys.stream().filter(k -> !columnsIn.contains(k) && !this.hasMatch(columnsOut, k)).collect((Collector<? super Object, ?, Collection<? extends String>>)Collectors.toList()));
                                }
                                final List<String> header = new ArrayList<String>();
                                final List<Integer> columns = new ArrayList<Integer>();
                                int headerWidth = 0;
                                final List<String> refKeys = new ArrayList<String>();
                                for (final String v : _header) {
                                    final String value = v.split("\\.")[0];
                                    if (!keys.contains(value) && !keys.contains(v)) {
                                        continue;
                                    }
                                    boolean addKey = false;
                                    for (final Map<String, Object> i : convertedCollection) {
                                        final Object val = this.mapValue(options, v, i);
                                        if (val != null) {
                                            addKey = (this.simpleObject(val) || options.containsKey("columns") || options.containsKey("structsOnTable"));
                                            break;
                                        }
                                    }
                                    if (!addKey) {
                                        continue;
                                    }
                                    refKeys.add(value);
                                    header.add(v);
                                    final String cn = this.columnName(v, options.containsKey("shortNames"));
                                    columns.add(cn.length() + 1);
                                    headerWidth += cn.length() + 1;
                                    if (headerWidth > width) {
                                        break;
                                    }
                                }
                                if (header.size() == 0) {
                                    throw new Exception("No columns for table!");
                                }
                                final double mapSimilarity = options.getOrDefault("mapSimilarity", new BigDecimal("0.8")).doubleValue();
                                final int matchLimit = (int)Math.ceil(header.size() * mapSimilarity);
                                for (final Map<String, Object> j : convertedCollection) {
                                    if (!this.similarSets(refKeys, j.keySet(), matchLimit)) {
                                        throw new Exception("Not homogenous list!");
                                    }
                                    for (int k = 0; k < header.size(); ++k) {
                                        final int cw = this.highlightMapValue(options, header.get(k), j).columnLength();
                                        if (cw > columns.get(k) - 1) {
                                            columns.set(k, cw + 1);
                                        }
                                    }
                                }
                                this.toTabStops(columns, collection.size(), rownum, columnSep);
                                final AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns);
                                asb.style(this.prntStyle.resolve(".th"));
                                int firstColumn = 0;
                                if (rownum) {
                                    asb.append(this.addPadding("", columns.get(0) - columnSep.length() - 1));
                                    asb.append(columnSep);
                                    asb.append("\t");
                                    firstColumn = 1;
                                }
                                boolean first = true;
                                for (final String s : header) {
                                    if (!first) {
                                        asb.append(columnSep);
                                    }
                                    asb.append(this.columnName(s, options.containsKey("shortNames")));
                                    asb.append("\t");
                                    first = false;
                                }
                                asb.columnSubSequence(0, width).println(this.terminal());
                                int row = 0;
                                for (final Map<String, Object> l : convertedCollection) {
                                    final AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(columns);
                                    if (this.doRowHighlight(row, tableRows)) {
                                        asb2.style(this.prntStyle.resolve(".rs"));
                                    }
                                    if (rownum) {
                                        asb2.styled(this.prntStyle.resolve(".rn"), this.addPadding(Integer.toString(row), columns.get(0) - columnSep.length() - 1));
                                        asb2.append(columnSep);
                                        asb2.append("\t");
                                    }
                                    ++row;
                                    for (int i2 = 0; i2 < header.size(); ++i2) {
                                        if (i2 > 0) {
                                            asb2.append(columnSep);
                                        }
                                        AttributedString v2 = this.highlightMapValue(options, header.get(i2), l);
                                        if (this.isNumber(v2.toString())) {
                                            v2 = this.addPadding(v2, this.cellWidth(firstColumn + i2, columns, rownum, columnSep) - 1);
                                        }
                                        asb2.append(v2);
                                        asb2.append("\t");
                                    }
                                    asb2.columnSubSequence(0, width).println(this.terminal());
                                }
                            }
                            else if (this.collectionObject(elem2) && !options.containsKey("toString")) {
                                final List<Integer> columns2 = new ArrayList<Integer>();
                                for (final Object o2 : collection) {
                                    final List<Object> inner = this.objectToList(o2);
                                    for (int i3 = 0; i3 < inner.size(); ++i3) {
                                        final int len1 = this.objectToString(options, inner.get(i3)).length() + 1;
                                        if (columns2.size() <= i3) {
                                            columns2.add(len1);
                                        }
                                        else if (len1 > columns2.get(i3)) {
                                            columns2.set(i3, len1);
                                        }
                                    }
                                }
                                this.toTabStops(columns2, collection.size(), rownum, columnSep);
                                int row2 = 0;
                                final int firstColumn2 = rownum ? 1 : 0;
                                for (final Object o3 : collection) {
                                    final AttributedStringBuilder asb3 = new AttributedStringBuilder().tabs(columns2);
                                    if (this.doRowHighlight(row2, tableRows)) {
                                        asb3.style(this.prntStyle.resolve(".rs"));
                                    }
                                    if (rownum) {
                                        asb3.styled(this.prntStyle.resolve(".rn"), this.addPadding(Integer.toString(row2), columns2.get(0) - columnSep.length() - 1));
                                        asb3.append(columnSep);
                                        asb3.append("\t");
                                    }
                                    ++row2;
                                    final List<Object> inner2 = this.objectToList(o3);
                                    for (int i4 = 0; i4 < inner2.size(); ++i4) {
                                        if (i4 > 0) {
                                            asb3.append(columnSep);
                                        }
                                        AttributedString v3 = this.highlightValue(options, null, inner2.get(i4));
                                        if (this.isNumber(v3.toString())) {
                                            v3 = this.addPadding(v3, this.cellWidth(firstColumn2 + i4, columns2, rownum, columnSep) - 1);
                                        }
                                        asb3.append(v3);
                                        asb3.append("\t");
                                    }
                                    asb3.columnSubSequence(0, width).println(this.terminal());
                                }
                            }
                            else {
                                this.highlightList(options, collection, width);
                            }
                        }
                        catch (final Exception e) {
                            Log.debug("Stack: ", e);
                            this.highlightList(options, collection, width);
                        }
                    }
                }
                else {
                    this.highlightValue(options, null, this.objectToString(options, obj)).println(this.terminal());
                }
            }
            else if (this.canConvert(obj) && !options.containsKey("toString")) {
                this.highlightMap(options, this.objectToMap(options, obj), width);
            }
            else {
                this.highlightValue(options, null, this.objectToString(options, obj)).println(this.terminal());
            }
        }
        if (message != null) {
            final AttributedStringBuilder asb4 = new AttributedStringBuilder();
            asb4.styled(this.prntStyle.resolve(".em"), message);
            asb4.println(this.terminal());
        }
        if (runtimeException != null) {
            throw runtimeException;
        }
    }
    
    private boolean doRowHighlight(final int row, final TableRows tableRows) {
        if (tableRows == null) {
            return false;
        }
        switch (tableRows) {
            case EVEN: {
                return row % 2 == 0;
            }
            case ODD: {
                return row % 2 == 1;
            }
            case ALL: {
                return true;
            }
            default: {
                return false;
            }
        }
    }
    
    private void highlightList(final Map<String, Object> options, final List<Object> collection, final int width) {
        this.highlightList(options, collection, width, 0);
    }
    
    private void highlightList(final Map<String, Object> options, final List<Object> collection, final int width, final int depth) {
        int row = 0;
        final int maxrows = options.get("maxrows");
        final int indent = options.get("indention");
        final List<Integer> tabs = new ArrayList<Integer>();
        final SyntaxHighlighter highlighter = (depth == 0) ? options.get("style") : null;
        if (!options.getOrDefault("multiColumns", false)) {
            tabs.add(indent * depth);
            if (options.containsKey("rownum")) {
                tabs.add(indent * depth + this.digits(collection.size()) + 2);
            }
            options.remove("maxColumnWidth");
            for (final Object o : collection) {
                final AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs);
                if (depth > 0) {
                    asb.append("\t");
                }
                if (options.containsKey("rownum")) {
                    asb.styled(this.prntStyle.resolve(".rn"), Integer.toString(row)).append(":");
                    asb.append("\t");
                    ++row;
                }
                if (highlighter != null && o instanceof String) {
                    asb.append(highlighter.highlight((String)o));
                }
                else {
                    asb.append(this.highlightValue(options, null, o));
                }
                this.println(asb.columnSubSequence(0, width), maxrows);
            }
        }
        else {
            int maxWidth = 0;
            for (final Object o2 : collection) {
                AttributedString as;
                if (highlighter != null && o2 instanceof String) {
                    as = highlighter.highlight((String)o2);
                }
                else {
                    as = this.highlightValue(options, null, o2);
                }
                if (as.length() > maxWidth) {
                    maxWidth = as.length();
                }
            }
            final int mcw = options.getOrDefault("maxColumnWidth", Integer.MAX_VALUE);
            maxWidth = ((mcw < maxWidth) ? mcw : maxWidth);
            tabs.add(maxWidth + 1);
            AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs);
            for (final Object o3 : collection) {
                if (asb.length() + maxWidth > width) {
                    this.println(asb.columnSubSequence(0, width), maxrows);
                    asb = new AttributedStringBuilder().tabs(tabs);
                }
                if (highlighter != null && o3 instanceof String) {
                    asb.append(highlighter.highlight((String)o3));
                }
                else {
                    asb.append(this.highlightValue(options, null, o3));
                }
                asb.append("\t");
            }
            this.println(asb.columnSubSequence(0, width), maxrows);
        }
    }
    
    private boolean collectionObject(final Object obj) {
        return obj instanceof Iterator || obj instanceof Iterable || obj instanceof Object[];
    }
    
    private boolean simpleObject(final Object obj) {
        return obj instanceof Number || obj instanceof String || obj instanceof Date || obj instanceof File || obj instanceof Boolean || obj instanceof Enum;
    }
    
    private boolean canConvert(final Object obj) {
        return this.engine != null && obj != null && !(obj instanceof Class) && !(obj instanceof Map) && !this.simpleObject(obj) && !this.collectionObject(obj);
    }
    
    private int digits(final int number) {
        if (number < 100) {
            return (number < 10) ? 1 : 2;
        }
        if (number < 1000) {
            return 3;
        }
        return (number < 10000) ? 4 : 5;
    }
    
    private int cellWidth(final int pos, final List<Integer> columns, final boolean rownum, final String columnSep) {
        if (pos == 0) {
            return columns.get(0);
        }
        return columns.get(pos) - columns.get(pos - 1) - ((rownum && pos == 1) ? 0 : columnSep.length());
    }
    
    private void toTabStops(final List<Integer> columns, final int rows, final boolean rownum, final String columnSep) {
        if (rownum) {
            columns.add(0, this.digits(rows) + 2 + columnSep.length());
        }
        for (int i = 1; i < columns.size(); ++i) {
            columns.set(i, columns.get(i - 1) + columns.get(i) + ((i > 1 || !rownum) ? columnSep.length() : 0));
        }
    }
    
    private void highlightMap(final Map<String, Object> options, final Map<String, Object> map, final int width) {
        if (!map.isEmpty()) {
            this.highlightMap(options, map, width, 0);
        }
        else {
            this.highlightValue(options, null, this.objectToString(options, map)).println(this.terminal());
        }
    }
    
    private void highlightMap(final Map<String, Object> options, final Map<String, Object> map, final int width, final int depth) {
        final int maxrows = options.get("maxrows");
        int max = map.keySet().stream().map((Function<? super Object, ? extends Integer>)String::length).max(Integer::compareTo).get();
        if (max > options.getOrDefault("maxColumnWidth", Integer.MAX_VALUE)) {
            max = options.get("maxColumnWidth");
        }
        final Map<String, Object> mapOptions = new HashMap<String, Object>(options);
        mapOptions.remove("maxColumnWidth");
        final int indent = options.get("indention");
        final int maxDepth = options.get("maxDepth");
        for (final Map.Entry<String, Object> entry : map.entrySet()) {
            if (depth == 0 && options.containsKey("columns") && !options.get("columns").contains(entry.getKey())) {
                continue;
            }
            AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(0, depth * indent, depth * indent + max + 1));
            if (depth != 0) {
                asb.append("\t");
            }
            asb.styled(this.prntStyle.resolve(".mk"), this.truncateValue(max, entry.getKey()));
            final Object elem = entry.getValue();
            final boolean convert = this.canConvert(elem);
            boolean highlightValue = true;
            if (depth < maxDepth && !options.containsKey("toString")) {
                if (elem instanceof Map || convert) {
                    final Map<String, Object> childMap = convert ? this.objectToMap(options, elem) : this.keysToString((Map<Object, Object>)elem);
                    if (!childMap.isEmpty()) {
                        this.println(asb.columnSubSequence(0, width), maxrows);
                        this.highlightMap(options, childMap, width, depth + 1);
                        highlightValue = false;
                    }
                }
                else if (this.collectionObject(elem)) {
                    final List<Object> collection = this.objectToList(elem);
                    if (!collection.isEmpty()) {
                        this.println(asb.columnSubSequence(0, width), maxrows);
                        final Map<String, Object> listOptions = new HashMap<String, Object>(options);
                        listOptions.put("toString", true);
                        this.highlightList(listOptions, collection, width, depth + 1);
                        highlightValue = false;
                    }
                }
            }
            if (!highlightValue) {
                continue;
            }
            AttributedString val = this.highlightMapValue(mapOptions, entry.getKey(), map);
            asb.append("\t");
            if (map.size() == 1) {
                if (val.contains('\n')) {
                    for (final String v : val.toString().split("\\r?\\n")) {
                        asb.append(this.highlightValue(options, entry.getKey(), v));
                        this.println(asb.columnSubSequence(0, width), maxrows);
                        asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1));
                    }
                }
                else {
                    asb.append(val);
                    this.println(asb.columnSubSequence(0, width), maxrows);
                }
            }
            else {
                if (val.contains('\n')) {
                    val = new AttributedString(Arrays.asList(val.toString().split("\\r?\\n")).toString());
                    asb.append(this.highlightValue(options, entry.getKey(), val.toString()));
                }
                else {
                    asb.append(val);
                }
                this.println(asb.columnSubSequence(0, width), maxrows);
            }
        }
    }
    
    private static class BadOptionValueException extends RuntimeException
    {
        public BadOptionValueException(final String message) {
            super(message);
        }
    }
    
    private static class TruncatedOutputException extends RuntimeException
    {
        public TruncatedOutputException(final String message) {
            super(message);
        }
    }
}
