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

package org.jline.builtins;

import java.util.Iterator;
import java.lang.management.MemoryMXBean;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.function.Function;
import java.util.Collections;
import java.lang.management.GarbageCollectorMXBean;
import java.util.Date;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import java.lang.management.ThreadInfo;
import java.io.IOException;
import org.jline.terminal.Attributes;
import java.lang.management.ThreadMXBean;
import org.jline.utils.Log;
import org.jline.utils.InfoCmp;
import java.lang.management.ManagementFactory;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Objects;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Arrays;
import java.io.PrintStream;
import java.util.Comparator;
import org.jline.terminal.Size;
import org.jline.keymap.KeyMap;
import org.jline.keymap.BindingReader;
import org.jline.utils.Display;
import org.jline.terminal.Terminal;
import java.util.Map;
import java.util.List;

public class TTop
{
    public static final String STAT_UPTIME = "uptime";
    public static final String STAT_TID = "tid";
    public static final String STAT_NAME = "name";
    public static final String STAT_STATE = "state";
    public static final String STAT_BLOCKED_TIME = "blocked_time";
    public static final String STAT_BLOCKED_COUNT = "blocked_count";
    public static final String STAT_WAITED_TIME = "waited_time";
    public static final String STAT_WAITED_COUNT = "waited_count";
    public static final String STAT_LOCK_NAME = "lock_name";
    public static final String STAT_LOCK_OWNER_ID = "lock_owner_id";
    public static final String STAT_LOCK_OWNER_NAME = "lock_owner_name";
    public static final String STAT_USER_TIME = "user_time";
    public static final String STAT_USER_TIME_PERC = "user_time_perc";
    public static final String STAT_CPU_TIME = "cpu_time";
    public static final String STAT_CPU_TIME_PERC = "cpu_time_perc";
    public List<String> sort;
    public long delay;
    public List<String> stats;
    public int nthreads;
    private final Map<String, Column> columns;
    private final Terminal terminal;
    private final Display display;
    private final BindingReader bindingReader;
    private final KeyMap<Operation> keys;
    private final Size size;
    private Comparator<Map<String, Comparable<?>>> comparator;
    private Map<Long, Map<String, Object>> previous;
    private Map<Long, Map<String, Long>> changes;
    private Map<String, Integer> widths;
    
    public static void ttop(final Terminal terminal, final PrintStream out, final PrintStream err, final String[] argv) throws Exception {
        final String[] usage = { "ttop -  display and update sorted information about threads", "Usage: ttop [OPTIONS]", "  -? --help                    Show help", "  -o --order=ORDER             Comma separated list of sorting keys", "  -t --stats=STATS             Comma separated list of stats to display", "  -s --seconds=SECONDS         Delay between updates in seconds", "  -m --millis=MILLIS           Delay between updates in milliseconds", "  -n --nthreads=NTHREADS       Only display up to NTHREADS threads" };
        final Options opt = Options.compile(usage).parse(argv);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final TTop ttop = new TTop(terminal);
        ttop.sort = (opt.isSet("order") ? Arrays.asList(opt.get("order").split(",")) : null);
        ttop.delay = (opt.isSet("seconds") ? (opt.getNumber("seconds") * 1000) : ttop.delay);
        ttop.delay = (opt.isSet("millis") ? opt.getNumber("millis") : ttop.delay);
        ttop.stats = (opt.isSet("stats") ? Arrays.asList(opt.get("stats").split(",")) : null);
        ttop.nthreads = (opt.isSet("nthreads") ? opt.getNumber("nthreads") : ttop.nthreads);
        ttop.run();
    }
    
    public TTop(final Terminal terminal) {
        this.columns = new LinkedHashMap<String, Column>();
        this.size = new Size();
        this.previous = new HashMap<Long, Map<String, Object>>();
        this.changes = new HashMap<Long, Map<String, Long>>();
        this.widths = new HashMap<String, Integer>();
        this.terminal = terminal;
        this.display = new Display(terminal, true);
        this.bindingReader = new BindingReader(terminal.reader());
        final DecimalFormatSymbols dfs = new DecimalFormatSymbols();
        dfs.setDecimalSeparator('.');
        final DecimalFormat perc = new DecimalFormat("0.00%", dfs);
        this.register("tid", Align.Right, "TID", o -> String.format("%3d", o));
        this.register("name", Align.Left, "NAME", padcut(40));
        this.register("state", Align.Left, "STATE", o -> o.toString().toLowerCase());
        this.register("blocked_time", Align.Right, "T-BLOCKED", o -> millis((long)o));
        this.register("blocked_count", Align.Right, "#-BLOCKED", Object::toString);
        this.register("waited_time", Align.Right, "T-WAITED", o -> millis((long)o));
        this.register("waited_count", Align.Right, "#-WAITED", Object::toString);
        this.register("lock_name", Align.Left, "LOCK-NAME", Object::toString);
        this.register("lock_owner_id", Align.Right, "LOCK-OWNER-ID", id -> ((long)id >= 0L) ? id.toString() : "");
        final String name;
        this.register("lock_owner_name", Align.Left, "LOCK-OWNER-NAME", name -> (name != null) ? name.toString() : "");
        this.register("user_time", Align.Right, "T-USR", o -> nanos((long)o));
        this.register("cpu_time", Align.Right, "T-CPU", o -> nanos((long)o));
        name = "user_time_perc";
        final Align right = Align.Right;
        final String header = "%-USR";
        final DecimalFormat obj = perc;
        Objects.requireNonNull(obj);
        this.register(name, right, header, obj::format);
        final String name2 = "cpu_time_perc";
        final Align right2 = Align.Right;
        final String header2 = "%-CPU";
        final DecimalFormat obj2 = perc;
        Objects.requireNonNull(obj2);
        this.register(name2, right2, header2, obj2::format);
        this.bindKeys(this.keys = new KeyMap<Operation>());
    }
    
    public KeyMap<Operation> getKeys() {
        return this.keys;
    }
    
    public void run() throws IOException, InterruptedException {
        this.comparator = this.buildComparator(this.sort);
        this.delay = ((this.delay > 0L) ? Math.max(this.delay, 100L) : 1000L);
        if (this.stats == null || this.stats.isEmpty()) {
            this.stats = new ArrayList<String>(Arrays.asList("tid", "name", "state", "cpu_time", "lock_owner_id"));
        }
        Boolean isThreadContentionMonitoringEnabled = null;
        final ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean();
        if (this.stats.contains("blocked_time") || this.stats.contains("blocked_count") || this.stats.contains("waited_time") || this.stats.contains("waited_count")) {
            if (threadsBean.isThreadContentionMonitoringSupported()) {
                isThreadContentionMonitoringEnabled = threadsBean.isThreadContentionMonitoringEnabled();
                if (!isThreadContentionMonitoringEnabled) {
                    threadsBean.setThreadContentionMonitoringEnabled(true);
                }
            }
            else {
                this.stats.removeAll(Arrays.asList("blocked_time", "blocked_count", "waited_time", "waited_count"));
            }
        }
        Boolean isThreadCpuTimeEnabled = null;
        if (this.stats.contains("user_time") || this.stats.contains("cpu_time")) {
            if (threadsBean.isThreadCpuTimeSupported()) {
                isThreadCpuTimeEnabled = threadsBean.isThreadCpuTimeEnabled();
                if (!isThreadCpuTimeEnabled) {
                    threadsBean.setThreadCpuTimeEnabled(true);
                }
            }
            else {
                this.stats.removeAll(Arrays.asList("user_time", "cpu_time"));
            }
        }
        this.size.copy(this.terminal.getSize());
        final Terminal.SignalHandler prevHandler = this.terminal.handle(Terminal.Signal.WINCH, this::handle);
        final Attributes attr = this.terminal.enterRawMode();
        try {
            if (!this.terminal.puts(InfoCmp.Capability.enter_ca_mode, new Object[0])) {
                this.terminal.puts(InfoCmp.Capability.clear_screen, new Object[0]);
            }
            this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
            this.terminal.puts(InfoCmp.Capability.cursor_invisible, new Object[0]);
            this.terminal.writer().flush();
            long t0 = System.currentTimeMillis();
            Operation op;
            do {
                this.display();
                this.checkInterrupted();
                op = null;
                final long delta = ((System.currentTimeMillis() - t0) / this.delay + 1L) * this.delay + t0 - System.currentTimeMillis();
                final int ch = this.bindingReader.peekCharacter(delta);
                if (ch == -1) {
                    op = Operation.EXIT;
                }
                else if (ch != -2) {
                    op = this.bindingReader.readBinding(this.keys, null, false);
                }
                if (op == null) {
                    continue;
                }
                switch (op.ordinal()) {
                    default: {
                        continue;
                    }
                    case 0: {
                        this.delay *= 2L;
                        t0 = System.currentTimeMillis();
                        continue;
                    }
                    case 1: {
                        this.delay = Math.max(this.delay / 2L, 16L);
                        t0 = System.currentTimeMillis();
                        continue;
                    }
                    case 4: {
                        this.display.clear();
                        continue;
                    }
                    case 5: {
                        this.comparator = this.comparator.reversed();
                        continue;
                    }
                }
            } while (op != Operation.EXIT);
        }
        catch (final InterruptedException ex) {}
        catch (final Error err) {
            Log.info("Error: ", err);
        }
        finally {
            this.terminal.setAttributes(attr);
            if (prevHandler != null) {
                this.terminal.handle(Terminal.Signal.WINCH, prevHandler);
            }
            if (!this.terminal.puts(InfoCmp.Capability.exit_ca_mode, new Object[0])) {
                this.terminal.puts(InfoCmp.Capability.clear_screen, new Object[0]);
            }
            this.terminal.puts(InfoCmp.Capability.keypad_local, new Object[0]);
            this.terminal.puts(InfoCmp.Capability.cursor_visible, new Object[0]);
            this.terminal.writer().flush();
            if (isThreadContentionMonitoringEnabled != null) {
                threadsBean.setThreadContentionMonitoringEnabled(isThreadContentionMonitoringEnabled);
            }
            if (isThreadCpuTimeEnabled != null) {
                threadsBean.setThreadCpuTimeEnabled(isThreadCpuTimeEnabled);
            }
        }
    }
    
    private void handle(final Terminal.Signal signal) {
        final int prevw = this.size.getColumns();
        this.size.copy(this.terminal.getSize());
        try {
            if (this.size.getColumns() < prevw) {
                this.display.clear();
            }
            this.display();
        }
        catch (final IOException ex) {}
    }
    
    private List<Map<String, Comparable<?>>> infos() {
        final long ctime = ManagementFactory.getRuntimeMXBean().getUptime();
        final Long ptime = (Long)this.previous.computeIfAbsent(Long.valueOf(-1L), id -> new HashMap()).put((Object)"uptime", (Object)ctime);
        final long delta = (ptime != null) ? (ctime - ptime) : 0L;
        final ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean();
        final ThreadInfo[] infos = threadsBean.dumpAllThreads(false, false);
        final List<Map<String, Comparable<?>>> threads = new ArrayList<Map<String, Comparable<?>>>();
        for (final ThreadInfo ti : infos) {
            final Map<String, Comparable<?>> t = new HashMap<String, Comparable<?>>();
            t.put("tid", ti.getThreadId());
            t.put("name", ti.getThreadName());
            t.put("state", ti.getThreadState());
            if (threadsBean.isThreadContentionMonitoringEnabled()) {
                t.put("blocked_time", ti.getBlockedTime());
                t.put("blocked_count", ti.getBlockedCount());
                t.put("waited_time", ti.getWaitedTime());
                t.put("waited_count", ti.getWaitedCount());
            }
            t.put("lock_name", ti.getLockName());
            t.put("lock_owner_id", ti.getLockOwnerId());
            t.put("lock_owner_name", ti.getLockOwnerName());
            if (threadsBean.isThreadCpuTimeSupported() && threadsBean.isThreadCpuTimeEnabled()) {
                final long tid = ti.getThreadId();
                long t2 = threadsBean.getThreadCpuTime(tid);
                long t3 = (long)this.previous.computeIfAbsent(Long.valueOf(tid), id -> new HashMap()).getOrDefault("cpu_time", t2);
                t.put("cpu_time", t2);
                t.put("cpu_time_perc", (delta != 0L) ? ((t2 - t3) / (delta * 1000000.0)) : 0.0);
                t2 = threadsBean.getThreadUserTime(tid);
                t3 = (long)this.previous.computeIfAbsent(Long.valueOf(tid), id -> new HashMap()).getOrDefault("user_time", t2);
                t.put("user_time", t2);
                t.put("user_time_perc", (delta != 0L) ? ((t2 - t3) / (delta * 1000000.0)) : 0.0);
            }
            threads.add(t);
        }
        return threads;
    }
    
    private void align(final AttributedStringBuilder sb, final String val, final int width, final Align align) {
        if (align == Align.Left) {
            sb.append(val);
            for (int i = 0; i < width - val.length(); ++i) {
                sb.append(' ');
            }
        }
        else {
            for (int i = 0; i < width - val.length(); ++i) {
                sb.append(' ');
            }
            sb.append(val);
        }
    }
    
    private synchronized void display() throws IOException {
        final long now = System.currentTimeMillis();
        this.display.resize(this.size.getRows(), this.size.getColumns());
        final List<AttributedString> lines = new ArrayList<AttributedString>();
        final AttributedStringBuilder sb = new AttributedStringBuilder(this.size.getColumns());
        sb.style(sb.style().bold());
        sb.append("ttop");
        sb.style(sb.style().boldOff());
        sb.append(" - ");
        sb.append(String.format("%8tT", new Date()));
        sb.append(".");
        final OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
        final String osinfo = "OS: " + os.getName() + " " + os.getVersion() + ", " + os.getArch() + ", " + os.getAvailableProcessors() + " cpus.";
        if (sb.length() + 1 + osinfo.length() < this.size.getColumns()) {
            sb.append(" ");
        }
        else {
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        sb.append(osinfo);
        final ClassLoadingMXBean cl = ManagementFactory.getClassLoadingMXBean();
        final String clsinfo = "Classes: " + cl.getLoadedClassCount() + " loaded, " + cl.getUnloadedClassCount() + " unloaded, " + cl.getTotalLoadedClassCount() + " loaded total.";
        if (sb.length() + 1 + clsinfo.length() < this.size.getColumns()) {
            sb.append(" ");
        }
        else {
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        sb.append(clsinfo);
        final ThreadMXBean th = ManagementFactory.getThreadMXBean();
        final String thinfo = "Threads: " + th.getThreadCount() + ", peak: " + th.getPeakThreadCount() + ", started: " + th.getTotalStartedThreadCount() + ".";
        if (sb.length() + 1 + thinfo.length() < this.size.getColumns()) {
            sb.append(" ");
        }
        else {
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        sb.append(thinfo);
        final MemoryMXBean me = ManagementFactory.getMemoryMXBean();
        final String meinfo = "Memory: heap: " + memory(me.getHeapMemoryUsage().getUsed(), me.getHeapMemoryUsage().getMax()) + ", non heap: " + memory(me.getNonHeapMemoryUsage().getUsed(), me.getNonHeapMemoryUsage().getMax()) + ".";
        if (sb.length() + 1 + meinfo.length() < this.size.getColumns()) {
            sb.append(" ");
        }
        else {
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        sb.append(meinfo);
        final StringBuilder sbc = new StringBuilder();
        sbc.append("GC: ");
        boolean first = true;
        for (final GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
            if (first) {
                first = false;
            }
            else {
                sbc.append(", ");
            }
            final long count = gc.getCollectionCount();
            final long time = gc.getCollectionTime();
            sbc.append(gc.getName()).append(": ").append(count).append(" col. / ").append(String.format("%d", time / 1000L)).append(".").append(String.format("%03d", time % 1000L)).append(" s");
        }
        sbc.append(".");
        if (sb.length() + 1 + sbc.length() < this.size.getColumns()) {
            sb.append(" ");
        }
        else {
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        sb.append(sbc);
        lines.add(sb.toAttributedString());
        sb.setLength(0);
        lines.add(sb.toAttributedString());
        final List<Map<String, Comparable<?>>> threads = this.infos();
        Collections.sort(threads, this.comparator);
        final int nb = Math.min(this.size.getRows() - lines.size() - 2, (this.nthreads > 0) ? this.nthreads : threads.size());
        String key = null;
        Map<String, Comparable<?>> thread = null;
        final List<Map<String, String>> values = threads.subList(0, nb).stream().map(thread -> this.stats.stream().collect(Collectors.toMap((Function<? super Object, ?>)Function.identity(), key -> this.columns.get(key).format.apply(thread.get(key))))).collect((Collector<? super Object, ?, List<Map<String, String>>>)Collectors.toList());
        final Iterator<String> iterator2 = this.stats.iterator();
        while (iterator2.hasNext()) {
            key = iterator2.next();
            final int width = values.stream().mapToInt(map -> map.get(key).length()).max().orElse(0);
            this.widths.put(key, Math.max(this.columns.get(key).header.length(), Math.max(width, this.widths.getOrDefault(key, 0))));
        }
        List<String> cstats;
        if (this.widths.values().stream().mapToInt(Integer::intValue).sum() + this.stats.size() - 1 < this.size.getColumns()) {
            cstats = this.stats;
        }
        else {
            cstats = new ArrayList<String>();
            int sz = 0;
            for (final String stat : this.stats) {
                int nsz = sz;
                if (nsz > 0) {
                    ++nsz;
                }
                nsz += this.widths.get(stat);
                if (nsz >= this.size.getColumns()) {
                    break;
                }
                sz = nsz;
                cstats.add(stat);
            }
        }
        for (final String key2 : cstats) {
            if (sb.length() > 0) {
                sb.append(" ");
            }
            final Column col = this.columns.get(key2);
            this.align(sb, col.header, this.widths.get(key2), col.align);
        }
        lines.add(sb.toAttributedString());
        sb.setLength(0);
        for (int i = 0; i < nb; ++i) {
            thread = threads.get(i);
            final long tid = thread.get("tid");
            for (final String key3 : cstats) {
                if (sb.length() > 0) {
                    sb.append(" ");
                }
                final Object cur = thread.get(key3);
                final Object prv = this.previous.computeIfAbsent(Long.valueOf(tid), id -> new HashMap()).put(key3, cur);
                long last;
                if (prv != null && !prv.equals(cur)) {
                    this.changes.computeIfAbsent(Long.valueOf(tid), id -> new HashMap()).put(key3, now);
                    last = now;
                }
                else {
                    last = (long)this.changes.computeIfAbsent(Long.valueOf(tid), id -> new HashMap()).getOrDefault((Object)key3, (Object)0L);
                }
                final long fade = this.delay * 24L;
                if (now - last < fade) {
                    final int r = (int)((now - last) / (fade / 24L));
                    sb.style(sb.style().foreground(255 - r).background(9));
                }
                this.align(sb, (String)values.get(i).get(key3), this.widths.get(key3), this.columns.get(key3).align);
                sb.style(sb.style().backgroundOff().foregroundOff());
            }
            lines.add(sb.toAttributedString());
            sb.setLength(0);
        }
        this.display.update(lines, 0);
    }
    
    private Comparator<Map<String, Comparable<?>>> buildComparator(List<String> sort) {
        if (sort == null || sort.isEmpty()) {
            sort = Collections.singletonList("tid");
        }
        Comparator<Map<String, Comparable<?>>> comparator = null;
        for (final String key : sort) {
            String fkey;
            boolean asc;
            if (key.startsWith("+")) {
                fkey = key.substring(1);
                asc = true;
            }
            else if (key.startsWith("-")) {
                fkey = key.substring(1);
                asc = false;
            }
            else {
                fkey = key;
                asc = true;
            }
            if (!this.columns.containsKey(fkey)) {
                throw new IllegalArgumentException("Unsupported sort key: " + fkey);
            }
            Comparator<Map<String, Comparable<?>>> comp = Comparator.comparing(m -> m.get(fkey));
            if (asc) {
                comp = comp.reversed();
            }
            if (comparator != null) {
                comparator = comparator.thenComparing(comp);
            }
            else {
                comparator = comp;
            }
        }
        return comparator;
    }
    
    private void register(final String name, final Align align, final String header, final Function<Object, String> format) {
        this.columns.put(name, new Column(name, align, header, format));
    }
    
    private static String nanos(final long nanos) {
        return millis(nanos / 1000000L);
    }
    
    private static String millis(long millis) {
        long secs = millis / 1000L;
        millis %= 1000L;
        long mins = secs / 60L;
        secs %= 60L;
        final long hours = mins / 60L;
        mins %= 60L;
        if (hours > 0L) {
            return String.format("%d:%02d:%02d.%03d", hours, mins, secs, millis);
        }
        if (mins > 0L) {
            return String.format("%d:%02d.%03d", mins, secs, millis);
        }
        return String.format("%d.%03d", secs, millis);
    }
    
    private static Function<Object, String> padcut(final int nb) {
        return o -> padcut(o.toString(), nb);
    }
    
    private static String padcut(final String str, final int nb) {
        if (str.length() <= nb) {
            final StringBuilder sb = new StringBuilder(nb);
            sb.append(str);
            while (sb.length() < nb) {
                sb.append(' ');
            }
            return sb.toString();
        }
        return str.substring(0, nb - 3) + "...";
    }
    
    private static String memory(final long cur, final long max) {
        if (max > 0L) {
            final String smax = humanReadableByteCount(max, false);
            final String cmax = humanReadableByteCount(cur, false);
            final StringBuilder sb = new StringBuilder(smax.length() * 2 + 3);
            for (int i = cmax.length(); i < smax.length(); ++i) {
                sb.append(' ');
            }
            sb.append(cmax).append(" / ").append(smax);
            return sb.toString();
        }
        return humanReadableByteCount(cur, false);
    }
    
    private static String humanReadableByteCount(final long bytes, final boolean si) {
        final int unit = si ? 1000 : 1024;
        if (bytes < 1024L) {
            return bytes + " B";
        }
        final int exp = (int)(Math.log((double)bytes) / Math.log(1024.0));
        final String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }
    
    private void checkInterrupted() throws InterruptedException {
        Thread.yield();
        if (Thread.currentThread().isInterrupted()) {
            throw new InterruptedException();
        }
    }
    
    private void bindKeys(final KeyMap<Operation> map) {
        map.bind(Operation.HELP, "h", "?");
        map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ");
        map.bind(Operation.INCREASE_DELAY, "+");
        map.bind(Operation.DECREASE_DELAY, "-");
        map.bind(Operation.CLEAR, KeyMap.ctrl('L'));
        map.bind(Operation.REVERSE, "r");
    }
    
    public enum Align
    {
        Left, 
        Right;
    }
    
    public enum Operation
    {
        INCREASE_DELAY, 
        DECREASE_DELAY, 
        HELP, 
        EXIT, 
        CLEAR, 
        REVERSE;
    }
    
    private static class Column
    {
        final String name;
        final Align align;
        final String header;
        final Function<Object, String> format;
        
        Column(final String name, final Align align, final String header, final Function<Object, String> format) {
            this.name = name;
            this.align = align;
            this.header = header;
            this.format = format;
        }
    }
}
