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

package org.jline.builtins;

import java.nio.charset.CoderResult;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.Charset;
import org.jline.terminal.impl.LineDisciplineTerminal;
import java.io.Closeable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Comparator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import org.jline.utils.Colors;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedString;
import java.util.Date;
import java.text.DateFormat;
import java.util.concurrent.TimeUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalUnit;
import java.time.temporal.ChronoUnit;
import java.time.Instant;
import java.util.stream.Stream;
import java.util.stream.IntStream;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import java.util.Iterator;
import org.jline.utils.Log;
import java.util.Arrays;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.util.Objects;
import org.jline.keymap.BindingReader;
import org.jline.terminal.Attributes;
import java.util.concurrent.Executors;
import java.io.IOException;
import org.jline.utils.InfoCmp;
import java.util.HashMap;
import java.util.ArrayList;
import org.jline.keymap.KeyMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import org.jline.terminal.Size;
import java.util.List;
import java.util.function.Consumer;
import java.io.PrintStream;
import org.jline.utils.Display;
import org.jline.terminal.Terminal;
import java.util.concurrent.atomic.AtomicBoolean;

public class Tmux
{
    public static final String OPT_PREFIX = "prefix";
    public static final String CMD_COMMANDS = "commands";
    public static final String CMD_SEND_PREFIX = "send-prefix";
    public static final String CMD_SPLIT_WINDOW = "split-window";
    public static final String CMD_SPLITW = "splitw";
    public static final String CMD_SELECT_PANE = "select-pane";
    public static final String CMD_SELECTP = "selectp";
    public static final String CMD_RESIZE_PANE = "resize-pane";
    public static final String CMD_RESIZEP = "resizep";
    public static final String CMD_DISPLAY_PANES = "display-panes";
    public static final String CMD_DISPLAYP = "displayp";
    public static final String CMD_CLOCK_MODE = "clock-mode";
    public static final String CMD_SET_OPTION = "set-option";
    public static final String CMD_SET = "set";
    public static final String CMD_LIST_KEYS = "list-keys";
    public static final String CMD_LSK = "lsk";
    public static final String CMD_SEND_KEYS = "send-keys";
    public static final String CMD_SEND = "send";
    public static final String CMD_BIND_KEY = "bind-key";
    public static final String CMD_BIND = "bind";
    public static final String CMD_UNBIND_KEY = "unbind-key";
    public static final String CMD_UNBIND = "unbind";
    public static final String CMD_NEW_WINDOW = "new-window";
    public static final String CMD_NEWW = "neww";
    public static final String CMD_NEXT_WINDOW = "next-window";
    public static final String CMD_NEXT = "next";
    public static final String CMD_PREVIOUS_WINDOW = "previous-window";
    public static final String CMD_PREV = "prev";
    public static final String CMD_LIST_WINDOWS = "list-windows";
    public static final String CMD_LSW = "lsw";
    private static final int[][][] WINDOW_CLOCK_TABLE;
    private final AtomicBoolean dirty;
    private final AtomicBoolean resized;
    private final Terminal terminal;
    private final Display display;
    private final PrintStream err;
    private final String term;
    private final Consumer<Terminal> runner;
    private List<Window> windows;
    private Integer windowsId;
    private int activeWindow;
    private final AtomicBoolean running;
    private final Size size;
    private boolean identify;
    private ScheduledExecutorService executor;
    private ScheduledFuture<?> clockFuture;
    private final Map<String, String> serverOptions;
    private KeyMap<Object> keyMap;
    int ACTIVE_COLOR;
    int INACTIVE_COLOR;
    int CLOCK_COLOR;
    
    public Tmux(final Terminal terminal, final PrintStream err, final Consumer<Terminal> runner) throws IOException {
        this.dirty = new AtomicBoolean(true);
        this.resized = new AtomicBoolean(true);
        this.windows = new ArrayList<Window>();
        this.windowsId = 0;
        this.activeWindow = 0;
        this.running = new AtomicBoolean(true);
        this.size = new Size();
        this.serverOptions = new HashMap<String, String>();
        this.ACTIVE_COLOR = 3908;
        this.INACTIVE_COLOR = 1103;
        this.CLOCK_COLOR = 1103;
        this.terminal = terminal;
        this.err = err;
        this.runner = runner;
        this.display = new Display(terminal, true);
        final Integer colors = terminal.getNumericCapability(InfoCmp.Capability.max_colors);
        this.term = ((colors != null && colors >= 256) ? "screen-256color" : "screen");
        this.serverOptions.put("prefix", "`");
        this.keyMap = this.createKeyMap(this.serverOptions.get("prefix"));
    }
    
    protected KeyMap<Object> createKeyMap(final String prefix) {
        final KeyMap<Object> keyMap = this.createEmptyKeyMap(prefix);
        keyMap.bind("send-prefix", prefix + prefix);
        keyMap.bind("split-window -v", prefix + "\"");
        keyMap.bind("split-window -h", prefix + "%");
        keyMap.bind("select-pane -U", prefix + KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        keyMap.bind("select-pane -D", prefix + KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        keyMap.bind("select-pane -L", prefix + KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        keyMap.bind("select-pane -R", prefix + KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        keyMap.bind("resize-pane -U 5", prefix + KeyMap.esc() + KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        keyMap.bind("resize-pane -D 5", prefix + KeyMap.esc() + KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        keyMap.bind("resize-pane -L 5", prefix + KeyMap.esc() + KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        keyMap.bind("resize-pane -R 5", prefix + KeyMap.esc() + KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        keyMap.bind("resize-pane -U", prefix + KeyMap.translate("^[[1;5A"), prefix + KeyMap.alt(KeyMap.translate("^[[A")));
        keyMap.bind("resize-pane -D", prefix + KeyMap.translate("^[[1;5B"), prefix + KeyMap.alt(KeyMap.translate("^[[B")));
        keyMap.bind("resize-pane -L", prefix + KeyMap.translate("^[[1;5C"), prefix + KeyMap.alt(KeyMap.translate("^[[C")));
        keyMap.bind("resize-pane -R", prefix + KeyMap.translate("^[[1;5D"), prefix + KeyMap.alt(KeyMap.translate("^[[D")));
        keyMap.bind("display-panes", prefix + "q");
        keyMap.bind("clock-mode", prefix + "t");
        keyMap.bind("new-window", prefix + "c");
        keyMap.bind("next-window", prefix + "n");
        keyMap.bind("previous-window", prefix + "p");
        return keyMap;
    }
    
    protected KeyMap<Object> createEmptyKeyMap(final String prefix) {
        final KeyMap<Object> keyMap = new KeyMap<Object>();
        keyMap.setUnicode(Binding.SelfInsert);
        keyMap.setNomatch(Binding.SelfInsert);
        for (int i = 0; i < 255; ++i) {
            keyMap.bind(Binding.Discard, prefix + (char)i);
        }
        keyMap.bind(Binding.Mouse, KeyMap.key(this.terminal, InfoCmp.Capability.key_mouse));
        return keyMap;
    }
    
    public void run() throws IOException {
        final Terminal.SignalHandler prevWinchHandler = this.terminal.handle(Terminal.Signal.WINCH, this::resize);
        final Terminal.SignalHandler prevIntHandler = this.terminal.handle(Terminal.Signal.INT, this::interrupt);
        final Terminal.SignalHandler prevSuspHandler = this.terminal.handle(Terminal.Signal.TSTP, this::suspend);
        final Attributes attributes = this.terminal.enterRawMode();
        this.terminal.puts(InfoCmp.Capability.enter_ca_mode, new Object[0]);
        this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
        this.terminal.trackMouse(Terminal.MouseTracking.Any);
        this.terminal.flush();
        this.executor = Executors.newSingleThreadScheduledExecutor();
        try {
            this.size.copy(this.terminal.getSize());
            this.windows.add(new Window(this));
            this.activeWindow = 0;
            this.runner.accept(this.active().getConsole());
            new Thread(this::inputLoop, "Mux input loop").start();
            this.redrawLoop();
        }
        catch (final RuntimeException e) {
            throw e;
        }
        finally {
            this.executor.shutdown();
            this.terminal.trackMouse(Terminal.MouseTracking.Off);
            this.terminal.puts(InfoCmp.Capability.keypad_local, new Object[0]);
            this.terminal.puts(InfoCmp.Capability.exit_ca_mode, new Object[0]);
            this.terminal.flush();
            this.terminal.setAttributes(attributes);
            this.terminal.handle(Terminal.Signal.WINCH, prevWinchHandler);
            this.terminal.handle(Terminal.Signal.INT, prevIntHandler);
            this.terminal.handle(Terminal.Signal.TSTP, prevSuspHandler);
        }
    }
    
    private VirtualConsole active() {
        return this.windows.get(this.activeWindow).getActive();
    }
    
    private List<VirtualConsole> panes() {
        return this.windows.get(this.activeWindow).getPanes();
    }
    
    private Window window() {
        return this.windows.get(this.activeWindow);
    }
    
    private void redrawLoop() {
        while (this.running.get()) {
            try {
                synchronized (this.dirty) {
                    while (this.running.get() && !this.dirty.compareAndSet(true, false)) {
                        this.dirty.wait();
                    }
                }
            }
            catch (final InterruptedException e) {
                e.printStackTrace();
            }
            this.handleResize();
            this.redraw();
        }
    }
    
    private void setDirty() {
        synchronized (this.dirty) {
            this.dirty.set(true);
            this.dirty.notifyAll();
        }
    }
    
    private void inputLoop() {
        try {
            final BindingReader reader = new BindingReader(this.terminal.reader());
            boolean first = true;
            while (this.running.get()) {
                Object b;
                if (first) {
                    b = reader.readBinding(this.keyMap);
                }
                else if (reader.peekCharacter(100L) >= 0) {
                    b = reader.readBinding(this.keyMap, null, false);
                }
                else {
                    b = null;
                }
                if (b == Binding.SelfInsert) {
                    if (this.active().clock) {
                        this.active().clock = false;
                        if (this.clockFuture != null && this.panes().stream().noneMatch(vc -> vc.clock)) {
                            this.clockFuture.cancel(false);
                            this.clockFuture = null;
                        }
                        this.setDirty();
                    }
                    else {
                        this.active().getMasterInputOutput().write(reader.getLastBinding().getBytes());
                        first = false;
                    }
                }
                else {
                    if (first) {
                        first = false;
                    }
                    else {
                        this.active().getMasterInputOutput().flush();
                        first = true;
                    }
                    if (b == Binding.Mouse) {
                        final Terminal terminal = this.terminal;
                        final BindingReader obj = reader;
                        Objects.requireNonNull(obj);
                        terminal.readMouseEvent(obj::readCharacter, reader.getLastBinding());
                    }
                    else {
                        if (!(b instanceof String) && !(b instanceof String[])) {
                            continue;
                        }
                        final ByteArrayOutputStream out = new ByteArrayOutputStream();
                        final ByteArrayOutputStream err = new ByteArrayOutputStream();
                        try (final PrintStream pout = new PrintStream(out);
                             final PrintStream perr = new PrintStream(err)) {
                            if (b instanceof String) {
                                this.execute(pout, perr, (String)b);
                            }
                            else {
                                this.execute(pout, perr, Arrays.asList((String[])b));
                            }
                        }
                        catch (final Exception ex) {}
                    }
                }
            }
        }
        catch (final IOException e) {
            if (this.running.get()) {
                Log.info("Error in tmux input loop", e);
            }
        }
        finally {
            this.running.set(false);
            this.setDirty();
        }
    }
    
    private synchronized void close(final VirtualConsole terminal) {
        int idx = -1;
        Window window = null;
        for (final Window w : this.windows) {
            idx = w.getPanes().indexOf(terminal);
            if (idx >= 0) {
                window = w;
                break;
            }
        }
        if (idx >= 0) {
            window.remove(terminal);
            if (window.getPanes().isEmpty()) {
                if (this.windows.size() > 1) {
                    this.windows.remove(window);
                    if (this.activeWindow >= this.windows.size()) {
                        --this.activeWindow;
                    }
                    this.resize(Terminal.Signal.WINCH);
                }
                else {
                    this.running.set(false);
                    this.setDirty();
                }
            }
            else {
                this.resize(Terminal.Signal.WINCH);
            }
        }
    }
    
    private void resize(final Terminal.Signal signal) {
        this.resized.set(true);
        this.setDirty();
    }
    
    private void interrupt(final Terminal.Signal signal) {
        this.active().getConsole().raise(signal);
    }
    
    private void suspend(final Terminal.Signal signal) {
        this.active().getConsole().raise(signal);
    }
    
    private void handleResize() {
        if (this.resized.compareAndSet(true, false)) {
            this.size.copy(this.terminal.getSize());
        }
        this.window().handleResize();
    }
    
    public void execute(final PrintStream out, final PrintStream err, final String command) throws Exception {
        final ParsedLine line = new DefaultParser().parse(command.trim(), 0);
        this.execute(out, err, line.words());
    }
    
    public synchronized void execute(final PrintStream out, final PrintStream err, final List<String> command) throws Exception {
        final String name = command.get(0);
        final List<String> args = command.subList(1, command.size());
        final String s = name;
        switch (s) {
            case "send-prefix": {
                this.sendPrefix(out, err, args);
                break;
            }
            case "split-window":
            case "splitw": {
                this.splitWindow(out, err, args);
                break;
            }
            case "select-pane":
            case "selectp": {
                this.selectPane(out, err, args);
                break;
            }
            case "resize-pane":
            case "resizep": {
                this.resizePane(out, err, args);
                break;
            }
            case "display-panes":
            case "displayp": {
                this.displayPanes(out, err, args);
                break;
            }
            case "clock-mode": {
                this.clockMode(out, err, args);
                break;
            }
            case "bind-key":
            case "bind": {
                this.bindKey(out, err, args);
                break;
            }
            case "unbind-key":
            case "unbind": {
                this.unbindKey(out, err, args);
                break;
            }
            case "list-keys":
            case "lsk": {
                this.listKeys(out, err, args);
                break;
            }
            case "send-keys":
            case "send": {
                this.sendKeys(out, err, args);
                break;
            }
            case "set-option":
            case "set": {
                this.setOption(out, err, args);
                break;
            }
            case "new-window":
            case "neww": {
                this.newWindow(out, err, args);
                break;
            }
            case "next-window":
            case "next": {
                this.nextWindow(out, err, args);
                break;
            }
            case "previous-window":
            case "prev": {
                this.previousWindow(out, err, args);
                break;
            }
            case "list-windows":
            case "lsw": {
                this.listWindows(out, err, args);
                break;
            }
        }
    }
    
    protected void listWindows(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "list-windows - ", "Usage: list-windows", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final Stream<Object> sorted = IntStream.range(0, this.windows.size()).mapToObj(i -> {
            final StringBuilder sb = new StringBuilder();
            sb.append(i);
            sb.append(": ");
            sb.append(this.windows.get(i).getName());
            sb.append((i == this.activeWindow) ? "* " : " ");
            sb.append("(");
            sb.append(this.windows.get(i).getPanes().size());
            sb.append(" panes)");
            if (i == this.activeWindow) {
                sb.append(" (active)");
            }
            return sb.toString();
        }).sorted();
        Objects.requireNonNull(out);
        sorted.forEach((Consumer<? super Object>)out::println);
    }
    
    protected void previousWindow(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "previous-window - ", "Usage: previous-window", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        if (this.windows.size() > 1) {
            --this.activeWindow;
            if (this.activeWindow < 0) {
                this.activeWindow = this.windows.size() - 1;
            }
            this.setDirty();
        }
    }
    
    protected void nextWindow(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "next-window - ", "Usage: next-window", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        if (this.windows.size() > 1) {
            ++this.activeWindow;
            if (this.activeWindow >= this.windows.size()) {
                this.activeWindow = 0;
            }
            this.setDirty();
        }
    }
    
    protected void newWindow(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "new-window - ", "Usage: new-window", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        this.windows.add(new Window(this));
        this.activeWindow = this.windows.size() - 1;
        this.runner.accept(this.active().getConsole());
        this.setDirty();
    }
    
    protected void setOption(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "set-option - ", "Usage: set-option [-agosquw] option [value]", "  -? --help                    Show help", "  -u --unset                   Unset the option" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final int nbargs = opt.args().size();
        if (nbargs < 1 || nbargs > 2) {
            throw new Options.HelpException(opt.usage());
        }
        final String name = opt.args().get(0);
        final String value = (nbargs > 1) ? opt.args().get(1) : null;
        if (!name.startsWith("@")) {
            final String s = name;
            switch (s) {
                case "prefix": {
                    if (value == null) {
                        throw new IllegalArgumentException("Missing argument");
                    }
                    final String prefix = KeyMap.translate(value);
                    final String oldPrefix = this.serverOptions.put("prefix", prefix);
                    final KeyMap<Object> newKeys = this.createEmptyKeyMap(prefix);
                    for (final Map.Entry<String, Object> e : this.keyMap.getBoundKeys().entrySet()) {
                        if (e.getValue() instanceof String) {
                            if (e.getKey().equals(oldPrefix + oldPrefix)) {
                                newKeys.bind(e.getValue(), prefix + prefix);
                            }
                            else if (e.getKey().startsWith(oldPrefix)) {
                                newKeys.bind(e.getValue(), prefix + e.getKey().substring(oldPrefix.length()));
                            }
                            else {
                                newKeys.bind(e.getValue(), e.getKey());
                            }
                        }
                    }
                    this.keyMap = newKeys;
                    break;
                }
            }
        }
    }
    
    protected void bindKey(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "bind-key - ", "Usage: bind-key key command [arguments]", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).setOptionsFirst(true).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final List<String> vargs = opt.args();
        if (vargs.size() < 2) {
            throw new Options.HelpException(opt.usage());
        }
        final String prefix = this.serverOptions.get("prefix");
        final String key = prefix + KeyMap.translate(vargs.remove(0));
        this.keyMap.unbind(key.substring(0, 2));
        this.keyMap.bind(vargs.toArray(new String[vargs.size()]), key);
    }
    
    protected void unbindKey(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "unbind-key - ", "Usage: unbind-key key", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).setOptionsFirst(true).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final List<String> vargs = opt.args();
        if (vargs.size() != 1) {
            throw new Options.HelpException(opt.usage());
        }
        final String prefix = this.serverOptions.get("prefix");
        final String key = prefix + KeyMap.translate(vargs.remove(0));
        this.keyMap.unbind(key);
        this.keyMap.bind(Binding.Discard, key);
    }
    
    protected void listKeys(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "list-keys - ", "Usage: list-keys ", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final String prefix = this.serverOptions.get("prefix");
        final Stream<Object> sorted = this.keyMap.getBoundKeys().entrySet().stream().filter(e -> e.getValue() instanceof String).map(e -> {
            String key = e.getKey();
            final String val = e.getValue();
            final StringBuilder sb = new StringBuilder();
            sb.append("bind-key -T ");
            if (key.startsWith(prefix)) {
                sb.append("prefix ");
                key = key.substring(prefix.length());
            }
            else {
                sb.append("root   ");
            }
            sb.append(KeyMap.display(key));
            while (sb.length() < 32) {
                sb.append(" ");
            }
            sb.append(val);
            return sb.toString();
        }).sorted();
        Objects.requireNonNull(out);
        sorted.forEach((Consumer<? super Object>)out::println);
    }
    
    protected void sendKeys(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "send-keys - ", "Usage: send-keys [-lXRM] [-N repeat-count] [-t target-pane] key...", "  -? --help                    Show help", "  -l --literal                Send key literally", "  -N --number=repeat-count     Specifies a repeat count" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        for (int i = 0, n = opt.getNumber("number"); i < n; ++i) {
            for (final String arg : opt.args()) {
                final String s = opt.isSet("literal") ? arg : KeyMap.translate(arg);
                this.active().getMasterInputOutput().write(s.getBytes());
            }
        }
    }
    
    protected void clockMode(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "clock-mode - ", "Usage: clock-mode", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        this.active().clock = true;
        if (this.clockFuture == null) {
            final long initial = Instant.now().until(Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(60L), ChronoUnit.MILLIS);
            final long delay = TimeUnit.MILLISECONDS.convert(1L, TimeUnit.SECONDS);
            this.clockFuture = this.executor.scheduleWithFixedDelay(this::setDirty, initial, delay, TimeUnit.MILLISECONDS);
        }
        this.setDirty();
    }
    
    protected void displayPanes(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "display-panes - ", "Usage: display-panes", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        this.identify = true;
        this.setDirty();
        this.executor.schedule(() -> {
            this.identify = false;
            this.setDirty();
        }, 1L, TimeUnit.SECONDS);
    }
    
    protected void resizePane(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "resize-pane - ", "Usage: resize-pane [-UDLR] [-x width] [-y height] [-t target-pane] [adjustment]", "  -? --help                    Show help", "  -U                           Resize pane upward", "  -D                           Select pane downward", "  -L                           Select pane to the left", "  -R                           Select pane to the right", "  -x --width=width             Set the width of the pane", "  -y --height=height           Set the height of the pane" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        int adjust;
        if (opt.args().size() == 0) {
            adjust = 1;
        }
        else {
            if (opt.args().size() != 1) {
                throw new Options.HelpException(opt.usage());
            }
            adjust = Integer.parseInt(opt.args().get(0));
        }
        this.window().resizePane(opt, adjust);
        this.setDirty();
    }
    
    protected void selectPane(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "select-pane - ", "Usage: select-pane [-UDLR] [-t target-pane]", "  -? --help                    Show help", "  -U                           Select pane up", "  -D                           Select pane down", "  -L                           Select pane left", "  -R                           Select pane right" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        if (this.window().selectPane(opt)) {
            this.setDirty();
        }
    }
    
    protected void sendPrefix(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "send-prefix - ", "Usage: send-prefix [-2] [-t target-pane]", "  -? --help                    Show help" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        this.active().getMasterInputOutput().write(this.serverOptions.get("prefix").getBytes());
    }
    
    protected void splitWindow(final PrintStream out, final PrintStream err, final List<String> args) throws Exception {
        final String[] usage = { "split-window - ", "Usage: split-window [-bdfhvP] [-c start-directory] [-F format] [-p percentage|-l size] [-t target-pane] [command]", "  -? --help                    Show help", "  -h --horizontal              Horizontal split", "  -v --vertical                Vertical split", "  -l --size=size               Size", "  -p --perc=percentage         Percentage", "  -b --before                  Insert the new pane before the active one", "  -f                           Split the full window instead of the active pane", "  -d                           Do not make the new pane the active one" };
        final Options opt = Options.compile(usage).parse(args);
        if (opt.isSet("help")) {
            throw new Options.HelpException(opt.usage());
        }
        final VirtualConsole newConsole = this.window().splitPane(opt);
        this.runner.accept(newConsole.getConsole());
        this.setDirty();
    }
    
    protected void layoutResize() {
    }
    
    protected synchronized void redraw() {
        final long[] screen = new long[this.size.getRows() * this.size.getColumns()];
        Arrays.fill(screen, 32L);
        final int[] cursor = new int[2];
        for (final VirtualConsole terminal : this.panes()) {
            if (terminal.clock) {
                final String str = DateFormat.getTimeInstance(3).format(new Date());
                this.print(screen, terminal, str, this.CLOCK_COLOR);
            }
            else {
                terminal.dump(screen, terminal.top(), terminal.left(), this.size.getRows(), this.size.getColumns(), (int[])((terminal == this.active()) ? cursor : null));
            }
            if (this.identify) {
                final String id = Integer.toString(terminal.id);
                this.print(screen, terminal, id, (terminal == this.active()) ? this.ACTIVE_COLOR : this.INACTIVE_COLOR);
            }
            this.drawBorder(screen, this.size, terminal, 0L);
        }
        this.drawBorder(screen, this.size, this.active(), 1155173304420532224L);
        Arrays.fill(screen, (this.size.getRows() - 1) * this.size.getColumns(), this.size.getRows() * this.size.getColumns(), 2305843558969507872L);
        final List<AttributedString> lines = new ArrayList<AttributedString>();
        int prevBg = 0;
        int prevFg = 0;
        boolean prevInv = false;
        boolean prevUl = false;
        boolean prevBold = false;
        boolean prevConceal = false;
        boolean prevHasFg = false;
        boolean prevHasBg = false;
        for (int y = 0; y < this.size.getRows(); ++y) {
            final AttributedStringBuilder sb = new AttributedStringBuilder(this.size.getColumns());
            for (int x = 0; x < this.size.getColumns(); ++x) {
                final long d = screen[y * this.size.getColumns() + x];
                final int c = (int)(d & 0xFFFFFFFFL);
                final int a = (int)(d >> 32);
                final int bg = a & 0xFFF;
                final int fg = (a & 0xFFF000) >> 12;
                final boolean ul = (a & 0x1000000) != 0x0;
                final boolean inv = (a & 0x2000000) != 0x0;
                final boolean conceal = (a & 0x4000000) != 0x0;
                final boolean bold = (a & 0x8000000) != 0x0;
                final boolean hasFg = (a & 0x10000000) != 0x0;
                final boolean hasBg = (a & 0x20000000) != 0x0;
                if ((hasBg && prevHasBg && bg != prevBg) || prevHasBg != hasBg) {
                    if (!hasBg) {
                        sb.style(sb.style().backgroundDefault());
                    }
                    else {
                        int col = bg;
                        col = Colors.roundRgbColor((col & 0xF00) >> 4, col & 0xF0, (col & 0xF) << 4, 256);
                        sb.style(sb.style().background(col));
                    }
                    prevBg = bg;
                    prevHasBg = hasBg;
                }
                if ((hasFg && prevHasFg && fg != prevFg) || prevHasFg != hasFg) {
                    if (!hasFg) {
                        sb.style(sb.style().foregroundDefault());
                    }
                    else {
                        int col = fg;
                        col = Colors.roundRgbColor((col & 0xF00) >> 4, col & 0xF0, (col & 0xF) << 4, 256);
                        sb.style(sb.style().foreground(col));
                    }
                    prevFg = fg;
                    prevHasFg = hasFg;
                }
                if (conceal != prevConceal) {
                    sb.style(conceal ? sb.style().conceal() : sb.style().concealOff());
                    prevConceal = conceal;
                }
                if (inv != prevInv) {
                    sb.style(inv ? sb.style().inverse() : sb.style().inverseOff());
                    prevInv = inv;
                }
                if (ul != prevUl) {
                    sb.style(ul ? sb.style().underline() : sb.style().underlineOff());
                    prevUl = ul;
                }
                if (bold != prevBold) {
                    sb.style(bold ? sb.style().bold() : sb.style().boldOff());
                    prevBold = bold;
                }
                sb.append((char)c);
            }
            lines.add(sb.toAttributedString());
        }
        this.display.resize(this.size.getRows(), this.size.getColumns());
        this.display.update(lines, this.size.cursorPos(cursor[1], cursor[0]));
    }
    
    private void print(final long[] screen, final VirtualConsole terminal, final String id, final int color) {
        if (terminal.height() > 5) {
            final long attr = (long)color << 32 | 0x2000000000000000L;
            final int yoff = (terminal.height() - 5) / 2;
            final int xoff = (terminal.width() - id.length() * 6) / 2;
            for (int i = 0; i < id.length(); ++i) {
                final char ch = id.charAt(i);
                int idx = 0;
                switch (ch) {
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9': {
                        idx = ch - '0';
                        break;
                    }
                    case ':': {
                        idx = 10;
                        break;
                    }
                    case 'A': {
                        idx = 11;
                        break;
                    }
                    case 'P': {
                        idx = 12;
                        break;
                    }
                    case 'M': {
                        idx = 13;
                        break;
                    }
                    default: {
                        idx = -1;
                        break;
                    }
                }
                if (idx >= 0) {
                    final int[][] data = Tmux.WINDOW_CLOCK_TABLE[idx];
                    for (int y = 0; y < data.length; ++y) {
                        for (int x = 0; x < data[y].length; ++x) {
                            if (data[y][x] != 0) {
                                final int off = (terminal.top + yoff + y) * this.size.getColumns() + terminal.left() + xoff + x + 6 * i;
                                screen[off] = (attr | 0x20L);
                            }
                        }
                    }
                }
            }
        }
        else {
            final long attr = (long)color << 44 | 0x1000000000000000L;
            final int yoff = (terminal.height() + 1) / 2;
            final int xoff = (terminal.width() - id.length()) / 2;
            final int off2 = (terminal.top + yoff) * this.size.getColumns() + terminal.left() + xoff;
            for (int j = 0; j < id.length(); ++j) {
                screen[off2 + j] = (attr | (long)id.charAt(j));
            }
        }
    }
    
    private void drawBorder(final long[] screen, final Size size, final VirtualConsole terminal, final long attr) {
        for (int i = terminal.left(); i < terminal.right(); ++i) {
            final int y0 = terminal.top() - 1;
            final int y2 = terminal.bottom();
            this.drawBorderChar(screen, size, i, y0, attr, 9472);
            this.drawBorderChar(screen, size, i, y2, attr, 9472);
        }
        for (int i = terminal.top(); i < terminal.bottom(); ++i) {
            final int x0 = terminal.left() - 1;
            final int x2 = terminal.right();
            this.drawBorderChar(screen, size, x0, i, attr, 9474);
            this.drawBorderChar(screen, size, x2, i, attr, 9474);
        }
        this.drawBorderChar(screen, size, terminal.left() - 1, terminal.top() - 1, attr, 9484);
        this.drawBorderChar(screen, size, terminal.right(), terminal.top() - 1, attr, 9488);
        this.drawBorderChar(screen, size, terminal.left() - 1, terminal.bottom(), attr, 9492);
        this.drawBorderChar(screen, size, terminal.right(), terminal.bottom(), attr, 9496);
    }
    
    private void drawBorderChar(final long[] screen, final Size size, final int x, final int y, final long attr, int c) {
        if (x >= 0 && x < size.getColumns() && y >= 0 && y < size.getRows() - 1) {
            final int oldc = (int)(screen[y * size.getColumns() + x] & 0xFFFFFFFFL);
            c = this.addBorder(c, oldc);
            screen[y * size.getColumns() + x] = (attr | (long)c);
        }
    }
    
    private int addBorder(final int c, final int oldc) {
        if (oldc == 32) {
            return c;
        }
        if (oldc == 9532) {
            return 9532;
        }
        switch (c) {
            case 9474: {
                return this.addBorder(9591, this.addBorder(9589, oldc));
            }
            case 9472: {
                return this.addBorder(9588, this.addBorder(9590, oldc));
            }
            case 9484: {
                return this.addBorder(9590, this.addBorder(9591, oldc));
            }
            case 9488: {
                return this.addBorder(9588, this.addBorder(9591, oldc));
            }
            case 9492: {
                return this.addBorder(9590, this.addBorder(9589, oldc));
            }
            case 9496: {
                return this.addBorder(9588, this.addBorder(9589, oldc));
            }
            case 9500: {
                return this.addBorder(9590, this.addBorder(9474, oldc));
            }
            case 9508: {
                return this.addBorder(9588, this.addBorder(9474, oldc));
            }
            case 9516: {
                return this.addBorder(9591, this.addBorder(9472, oldc));
            }
            case 9524: {
                return this.addBorder(9589, this.addBorder(9472, oldc));
            }
            case 9588: {
                switch (oldc) {
                    case 9474: {
                        return 9508;
                    }
                    case 9472: {
                        return 9472;
                    }
                    case 9484: {
                        return 9516;
                    }
                    case 9488: {
                        return 9488;
                    }
                    case 9492: {
                        return 9524;
                    }
                    case 9496: {
                        return 9496;
                    }
                    case 9500: {
                        return 9532;
                    }
                    case 9508: {
                        return 9508;
                    }
                    case 9516: {
                        return 9516;
                    }
                    case 9524: {
                        return 9524;
                    }
                    default: {
                        throw new IllegalArgumentException();
                    }
                }
                break;
            }
            case 9589: {
                switch (oldc) {
                    case 9474: {
                        return 9474;
                    }
                    case 9472: {
                        return 9524;
                    }
                    case 9484: {
                        return 9500;
                    }
                    case 9488: {
                        return 9508;
                    }
                    case 9492: {
                        return 9492;
                    }
                    case 9496: {
                        return 9496;
                    }
                    case 9500: {
                        return 9500;
                    }
                    case 9508: {
                        return 9508;
                    }
                    case 9516: {
                        return 9532;
                    }
                    case 9524: {
                        return 9524;
                    }
                    default: {
                        throw new IllegalArgumentException();
                    }
                }
                break;
            }
            case 9590: {
                switch (oldc) {
                    case 9474: {
                        return 9500;
                    }
                    case 9472: {
                        return 9472;
                    }
                    case 9484: {
                        return 9484;
                    }
                    case 9488: {
                        return 9516;
                    }
                    case 9492: {
                        return 9492;
                    }
                    case 9496: {
                        return 9524;
                    }
                    case 9500: {
                        return 9500;
                    }
                    case 9508: {
                        return 9532;
                    }
                    case 9516: {
                        return 9516;
                    }
                    case 9524: {
                        return 9524;
                    }
                    default: {
                        throw new IllegalArgumentException();
                    }
                }
                break;
            }
            case 9591: {
                switch (oldc) {
                    case 9474: {
                        return 9474;
                    }
                    case 9472: {
                        return 9516;
                    }
                    case 9484: {
                        return 9484;
                    }
                    case 9488: {
                        return 9488;
                    }
                    case 9492: {
                        return 9500;
                    }
                    case 9496: {
                        return 9508;
                    }
                    case 9500: {
                        return 9500;
                    }
                    case 9508: {
                        return 9508;
                    }
                    case 9516: {
                        return 9516;
                    }
                    case 9524: {
                        return 9532;
                    }
                    default: {
                        throw new IllegalArgumentException();
                    }
                }
                break;
            }
            default: {
                throw new IllegalArgumentException();
            }
        }
    }
    
    private static int findMatch(final String layout, final char c0, final char c1) {
        if (layout.charAt(0) != c0) {
            throw new IllegalArgumentException();
        }
        int nb = 0;
        int i;
        for (i = 0; i < layout.length(); ++i) {
            final char c2 = layout.charAt(i);
            if (c2 == c0) {
                ++nb;
            }
            else if (c2 == c1 && --nb == 0) {
                return i;
            }
        }
        if (nb > 0) {
            throw new IllegalArgumentException("No matching '" + c1 + "'");
        }
        return i;
    }
    
    static {
        WINDOW_CLOCK_TABLE = new int[][][] { { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 0, 0, 0, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 } }, { { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0 }, { 1, 1, 1, 1, 1 } }, { { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 1, 0, 0, 0, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0 }, { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0 }, { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 }, { 0, 0, 0, 0, 1 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 } }, { { 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 0, 0, 0, 1 } }, { { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0 } }, { { 1, 0, 0, 0, 1 }, { 1, 1, 0, 1, 1 }, { 1, 0, 1, 0, 1 }, { 1, 0, 0, 0, 1 }, { 1, 0, 0, 0, 1 } } };
    }
    
    enum Binding
    {
        Discard, 
        SelfInsert, 
        Mouse;
    }
    
    private class Window
    {
        private List<VirtualConsole> panes;
        private VirtualConsole active;
        private int lastActive;
        private final AtomicInteger paneId;
        private Layout layout;
        private Tmux tmux;
        private String name;
        
        public Window(final Tmux tmux) throws IOException {
            this.panes = new CopyOnWriteArrayList<VirtualConsole>();
            this.paneId = new AtomicInteger();
            this.tmux = tmux;
            this.layout = new Layout();
            this.layout.sx = Tmux.this.size.getColumns();
            this.layout.sy = Tmux.this.size.getRows();
            this.layout.type = Layout.Type.WindowPane;
            final int incrementAndGet = this.paneId.incrementAndGet();
            final String access$100 = Tmux.this.term;
            final int left = 0;
            final int top = 0;
            final int columns = Tmux.this.size.getColumns();
            final int rows = Tmux.this.size.getRows() - 1;
            Objects.requireNonNull(tmux);
            final Runnable dirty = () -> rec$.setDirty();
            Objects.requireNonNull(tmux);
            (this.active = new VirtualConsole(incrementAndGet, access$100, left, top, columns, rows, dirty, x$0 -> rec$.close(x$0), this.layout)).active = this.lastActive++;
            this.active.getConsole().setAttributes(Tmux.this.terminal.getAttributes());
            this.panes.add(this.active);
            this.name = "win" + ((Tmux.this.windowsId < 10) ? ("0" + Tmux.this.windowsId) : Tmux.this.windowsId);
            Tmux.this.windowsId;
            ++Tmux.this.windowsId;
        }
        
        public String getName() {
            return this.name;
        }
        
        public List<VirtualConsole> getPanes() {
            return this.panes;
        }
        
        public VirtualConsole getActive() {
            return this.active;
        }
        
        public void remove(final VirtualConsole console) {
            this.panes.remove(console);
            if (!this.panes.isEmpty()) {
                console.layout.remove();
                if (this.active == console) {
                    this.active = this.panes.stream().sorted(Comparator.comparingInt(p -> p.active).reversed()).findFirst().get();
                }
                this.layout = this.active.layout;
                while (this.layout.parent != null) {
                    this.layout = this.layout.parent;
                }
                this.layout.fixOffsets();
                this.layout.fixPanes(Tmux.this.size.getColumns(), Tmux.this.size.getRows());
            }
        }
        
        public void handleResize() {
            this.layout.resize(Tmux.this.size.getColumns(), Tmux.this.size.getRows() - 1);
            this.panes.forEach(vc -> {
                if (vc.width() != vc.layout.sx || vc.height() != vc.layout.sy || vc.left() != vc.layout.xoff || vc.top() != vc.layout.yoff) {
                    vc.resize(vc.layout.xoff, vc.layout.yoff, vc.layout.sx, vc.layout.sy);
                    Tmux.this.display.clear();
                }
            });
        }
        
        public VirtualConsole splitPane(final Options opt) throws IOException {
            final Layout.Type type = opt.isSet("horizontal") ? Layout.Type.LeftRight : Layout.Type.TopBottom;
            if (this.layout.type == Layout.Type.WindowPane) {
                final Layout p = new Layout();
                p.sx = this.layout.sx;
                p.sy = this.layout.sy;
                p.type = type;
                p.cells.add(this.layout);
                this.layout.parent = p;
                this.layout = p;
            }
            Layout cell = this.active.layout();
            if (opt.isSet("f")) {
                while (cell.parent != this.layout) {
                    cell = cell.parent;
                }
            }
            int size = -1;
            if (opt.isSet("size")) {
                size = opt.getNumber("size");
            }
            else if (opt.isSet("perc")) {
                final int p2 = opt.getNumber("perc");
                if (type == Layout.Type.TopBottom) {
                    size = cell.sy * p2 / 100;
                }
                else {
                    size = cell.sx * p2 / 100;
                }
            }
            final Layout newCell = cell.split(type, size, opt.isSet("before"));
            if (newCell == null) {
                Tmux.this.err.println("create pane failed: pane too small");
                return null;
            }
            final int incrementAndGet = this.paneId.incrementAndGet();
            final String access$100 = Tmux.this.term;
            final int xoff = newCell.xoff;
            final int yoff = newCell.yoff;
            final int sx = newCell.sx;
            final int sy = newCell.sy;
            Objects.requireNonNull(this.tmux);
            final Runnable dirty = () -> rec$.setDirty();
            Objects.requireNonNull(this.tmux);
            final VirtualConsole newConsole = new VirtualConsole(incrementAndGet, access$100, xoff, yoff, sx, sy, dirty, x$0 -> rec$.close(x$0), newCell);
            this.panes.add(newConsole);
            newConsole.getConsole().setAttributes(Tmux.this.terminal.getAttributes());
            if (!opt.isSet("d")) {
                (this.active = newConsole).active = this.lastActive++;
            }
            return newConsole;
        }
        
        public boolean selectPane(final Options opt) {
            final VirtualConsole prevActive = this.active;
            if (opt.isSet("L")) {
                this.active = this.panes.stream().filter(c -> c.bottom() > this.active.top() && c.top() < this.active.bottom()).filter(c -> c != this.active).sorted(Comparator.comparingInt(c -> (c.left() > this.active.left()) ? c.left() : (c.left() + Tmux.this.size.getColumns())).reversed().thenComparingInt(c -> -c.active)).findFirst().orElse(this.active);
            }
            else if (opt.isSet("R")) {
                this.active = this.panes.stream().filter(c -> c.bottom() > this.active.top() && c.top() < this.active.bottom()).filter(c -> c != this.active).sorted(Comparator.comparingInt(c -> (c.left() > this.active.left()) ? c.left() : (c.left() + Tmux.this.size.getColumns())).thenComparingInt(c -> -c.active)).findFirst().orElse(this.active);
            }
            else if (opt.isSet("U")) {
                this.active = this.panes.stream().filter(c -> c.right() > this.active.left() && c.left() < this.active.right()).filter(c -> c != this.active).sorted(Comparator.comparingInt(c -> (c.top() > this.active.top()) ? c.top() : (c.top() + Tmux.this.size.getRows())).reversed().thenComparingInt(c -> -c.active)).findFirst().orElse(this.active);
            }
            else if (opt.isSet("D")) {
                this.active = this.panes.stream().filter(c -> c.right() > this.active.left() && c.left() < this.active.right()).filter(c -> c != this.active).sorted(Comparator.comparingInt(c -> (c.top() > this.active.top()) ? c.top() : (c.top() + Tmux.this.size.getRows())).thenComparingInt(c -> -c.active)).findFirst().orElse(this.active);
            }
            boolean out = false;
            if (prevActive != this.active) {
                this.active.active = this.lastActive++;
                out = true;
            }
            return out;
        }
        
        public void resizePane(final Options opt, final int adjust) {
            if (opt.isSet("width")) {
                final int x = opt.getNumber("width");
                this.active.layout().resizeTo(Layout.Type.LeftRight, x);
            }
            if (opt.isSet("height")) {
                final int y = opt.getNumber("height");
                this.active.layout().resizeTo(Layout.Type.TopBottom, y);
            }
            if (opt.isSet("L")) {
                this.active.layout().resize(Layout.Type.LeftRight, -adjust, true);
            }
            else if (opt.isSet("R")) {
                this.active.layout().resize(Layout.Type.LeftRight, adjust, true);
            }
            else if (opt.isSet("U")) {
                this.active.layout().resize(Layout.Type.TopBottom, -adjust, true);
            }
            else if (opt.isSet("D")) {
                this.active.layout().resize(Layout.Type.TopBottom, adjust, true);
            }
        }
    }
    
    static class Layout
    {
        static final Pattern PATTERN;
        private static final int PANE_MINIMUM = 3;
        Type type;
        Layout parent;
        int sx;
        int sy;
        int xoff;
        int yoff;
        List<Layout> cells;
        
        Layout() {
            this.cells = new CopyOnWriteArrayList<Layout>();
        }
        
        public static Layout parse(String layout) {
            if (layout.length() < 6) {
                throw new IllegalArgumentException("Bad syntax");
            }
            final String chk = layout.substring(0, 4);
            if (layout.charAt(4) != ',') {
                throw new IllegalArgumentException("Bad syntax");
            }
            layout = layout.substring(5);
            if (Integer.parseInt(chk, 16) != checksum(layout)) {
                throw new IllegalArgumentException("Bad checksum");
            }
            return parseCell(null, layout);
        }
        
        public String dump() {
            final StringBuilder sb = new StringBuilder(64);
            sb.append("0000,");
            this.doDump(sb);
            final int chk = checksum(sb, 5);
            sb.setCharAt();
            sb.setCharAt();
            sb.setCharAt();
            sb.setCharAt();
            return sb.toString();
        }
        
        private static char toHexChar(final int i) {
            return (i < 10) ? ((char)(i + 48)) : ((char)(i - 10 + 97));
        }
        
        private void doDump(final StringBuilder sb) {
            sb.append(this.sx).append('x').append(this.sy).append(',').append(this.xoff).append(',').append(this.yoff);
            switch (this.type.ordinal()) {
                case 2: {
                    sb.append(',').append('0');
                    break;
                }
                case 0:
                case 1: {
                    sb.append((this.type == Type.TopBottom) ? '[' : '{');
                    boolean first = true;
                    for (final Layout c : this.cells) {
                        if (first) {
                            first = false;
                        }
                        else {
                            sb.append(',');
                        }
                        c.doDump(sb);
                    }
                    sb.append((this.type == Type.TopBottom) ? ']' : '}');
                    break;
                }
            }
        }
        
        public void resize(final Type type, final int change, final boolean opposite) {
            Layout lc;
            Layout lcparent;
            for (lc = this, lcparent = lc.parent; lcparent != null && lcparent.type != type; lcparent = lc.parent) {
                lc = lcparent;
            }
            if (lcparent == null) {
                return;
            }
            if (lc.nextSibling() == null) {
                lc = lc.prevSibling();
            }
            int needed = change;
            while (needed != 0) {
                int size;
                if (change > 0) {
                    size = lc.resizePaneGrow(type, needed, opposite);
                    needed -= size;
                }
                else {
                    size = lc.resizePaneShrink(type, needed);
                    needed += size;
                }
                if (size == 0) {
                    break;
                }
            }
            this.fixOffsets();
            this.fixPanes();
        }
        
        int resizePaneGrow(final Type type, final int needed, final boolean opposite) {
            int size = 0;
            final Layout lcadd = this;
            Layout lcremove;
            for (lcremove = this.nextSibling(); lcremove != null; lcremove = lcremove.nextSibling()) {
                size = lcremove.resizeCheck(type);
                if (size > 0) {
                    break;
                }
            }
            if (opposite && lcremove == null) {
                for (lcremove = this.prevSibling(); lcremove != null; lcremove = lcremove.prevSibling()) {
                    size = lcremove.resizeCheck(type);
                    if (size > 0) {
                        break;
                    }
                }
            }
            if (lcremove == null) {
                return 0;
            }
            if (size > needed) {
                size = needed;
            }
            lcadd.resizeAdjust(type, size);
            lcremove.resizeAdjust(type, -size);
            return size;
        }
        
        int resizePaneShrink(final Type type, final int needed) {
            int size = 0;
            Layout lcremove = this;
            do {
                size = lcremove.resizeCheck(type);
                if (size > 0) {
                    break;
                }
                lcremove = lcremove.prevSibling();
            } while (lcremove != null);
            if (lcremove == null) {
                return 0;
            }
            final Layout lcadd = this.nextSibling();
            if (lcadd == null) {
                return 0;
            }
            if (size > -needed) {
                size = -needed;
            }
            lcadd.resizeAdjust(type, size);
            lcremove.resizeAdjust(type, -size);
            return size;
        }
        
        Layout prevSibling() {
            final int idx = this.parent.cells.indexOf(this);
            if (idx > 0) {
                return this.parent.cells.get(idx - 1);
            }
            return null;
        }
        
        Layout nextSibling() {
            final int idx = this.parent.cells.indexOf(this);
            if (idx < this.parent.cells.size() - 1) {
                return this.parent.cells.get(idx + 1);
            }
            return null;
        }
        
        public void resizeTo(final Type type, final int new_size) {
            Layout lc;
            Layout lcparent;
            for (lc = this, lcparent = lc.parent; lcparent != null && lcparent.type != type; lcparent = lc.parent) {
                lc = lcparent;
            }
            if (lcparent == null) {
                return;
            }
            final int size = (type == Type.LeftRight) ? lc.sx : lc.sy;
            final int change = (lc.nextSibling() == null) ? (size - new_size) : (new_size - size);
            lc.resize(type, change, true);
        }
        
        public void resize(final int sx, final int sy) {
            int xchange = sx - this.sx;
            final int xlimit = this.resizeCheck(Type.LeftRight);
            if (xchange < 0 && xchange < -xlimit) {
                xchange = -xlimit;
            }
            if (xlimit == 0) {
                if (sx <= this.sx) {
                    xchange = 0;
                }
                else {
                    xchange = sx - this.sx;
                }
            }
            if (xchange != 0) {
                this.resizeAdjust(Type.LeftRight, xchange);
            }
            int ychange = sy - this.sy;
            final int ylimit = this.resizeCheck(Type.TopBottom);
            if (ychange < 0 && ychange < -ylimit) {
                ychange = -ylimit;
            }
            if (ylimit == 0) {
                if (sy <= this.sy) {
                    ychange = 0;
                }
                else {
                    ychange = sy - this.sy;
                }
            }
            if (ychange != 0) {
                this.resizeAdjust(Type.TopBottom, ychange);
            }
            this.fixOffsets();
            this.fixPanes(sx, sy);
        }
        
        public void remove() {
            if (this.parent == null) {
                throw new IllegalStateException();
            }
            final int idx = this.parent.cells.indexOf(this);
            final Layout other = this.parent.cells.get((idx == 0) ? 1 : (idx - 1));
            other.resizeAdjust(this.parent.type, (this.parent.type == Type.LeftRight) ? (this.sx + 1) : (this.sy + 1));
            this.parent.cells.remove(this);
            if (other.parent.cells.size() == 1) {
                if (other.parent.parent == null) {
                    other.parent = null;
                }
                else {
                    other.parent.parent.cells.set(other.parent.parent.cells.indexOf(other.parent), other);
                    other.parent = other.parent.parent;
                }
            }
        }
        
        private int resizeCheck(final Type type) {
            if (this.type == Type.WindowPane) {
                int min = 3;
                int avail;
                if (type == Type.LeftRight) {
                    avail = this.sx;
                }
                else {
                    avail = this.sy;
                    ++min;
                }
                if (avail > min) {
                    avail -= min;
                }
                else {
                    avail = 0;
                }
                return avail;
            }
            if (this.type == type) {
                return this.cells.stream().mapToInt(c -> (c != null) ? c.resizeCheck(type) : 0).sum();
            }
            return this.cells.stream().mapToInt(c -> (c != null) ? c.resizeCheck(type) : Integer.MAX_VALUE).min().orElse(Integer.MAX_VALUE);
        }
        
        private void resizeAdjust(final Type type, int change) {
            if (type == Type.LeftRight) {
                this.sx += change;
            }
            else {
                this.sy += change;
            }
            if (this.type == Type.WindowPane) {
                return;
            }
            if (this.type != type) {
                for (final Layout c : this.cells) {
                    c.resizeAdjust(type, change);
                }
                return;
            }
            while (change != 0) {
                for (final Layout c : this.cells) {
                    if (change == 0) {
                        break;
                    }
                    if (change > 0) {
                        c.resizeAdjust(type, 1);
                        --change;
                    }
                    else {
                        if (c.resizeCheck(type) <= 0) {
                            continue;
                        }
                        c.resizeAdjust(type, -1);
                        ++change;
                    }
                }
            }
        }
        
        public void fixOffsets() {
            if (this.type == Type.LeftRight) {
                int xoff = this.xoff;
                for (final Layout cell : this.cells) {
                    cell.xoff = xoff;
                    cell.yoff = this.yoff;
                    cell.fixOffsets();
                    xoff += cell.sx + 1;
                }
            }
            else if (this.type == Type.TopBottom) {
                int yoff = this.yoff;
                for (final Layout cell : this.cells) {
                    cell.xoff = this.xoff;
                    cell.yoff = yoff;
                    cell.fixOffsets();
                    yoff += cell.sy + 1;
                }
            }
        }
        
        public void fixPanes() {
        }
        
        public void fixPanes(final int sx, final int sy) {
        }
        
        public int countCells() {
            switch (this.type.ordinal()) {
                case 0:
                case 1: {
                    return this.cells.stream().mapToInt(Layout::countCells).sum();
                }
                default: {
                    return 1;
                }
            }
        }
        
        public Layout split(final Type type, final int size, final boolean insertBefore) {
            if (type == Type.WindowPane) {
                throw new IllegalStateException();
            }
            if (((type == Type.LeftRight) ? this.sx : this.sy) < 7) {
                return null;
            }
            if (this.parent == null) {
                throw new IllegalStateException();
            }
            final int saved_size = (type == Type.LeftRight) ? this.sx : this.sy;
            int size2 = (size < 0) ? ((saved_size + 1) / 2 - 1) : (insertBefore ? (saved_size - size - 1) : size);
            if (size2 < 3) {
                size2 = 3;
            }
            else if (size2 > saved_size - 2) {
                size2 = saved_size - 2;
            }
            final int size3 = saved_size - 1 - size2;
            if (this.parent.type != type) {
                final Layout p = new Layout();
                p.type = type;
                p.parent = this.parent;
                p.sx = this.sx;
                p.sy = this.sy;
                p.xoff = this.xoff;
                p.yoff = this.yoff;
                this.parent.cells.set(this.parent.cells.indexOf(this), p);
                p.cells.add(this);
                this.parent = p;
            }
            final Layout cell = new Layout();
            cell.type = Type.WindowPane;
            cell.parent = this.parent;
            this.parent.cells.add(this.parent.cells.indexOf(this) + (insertBefore ? 0 : 1), cell);
            final int sx = this.sx;
            final int sy = this.sy;
            final int xoff = this.xoff;
            final int yoff = this.yoff;
            Layout cell2;
            Layout cell3;
            if (insertBefore) {
                cell2 = cell;
                cell3 = this;
            }
            else {
                cell2 = this;
                cell3 = cell;
            }
            if (type == Type.LeftRight) {
                cell2.setSize(size3, sy, xoff, yoff);
                cell3.setSize(size2, sy, xoff + size3 + 1, yoff);
            }
            else {
                cell2.setSize(sx, size3, xoff, yoff);
                cell3.setSize(sx, size2, xoff, yoff + size3 + 1);
            }
            return cell;
        }
        
        private void setSize(final int sx, final int sy, final int xoff, final int yoff) {
            this.sx = sx;
            this.sy = sy;
            this.xoff = xoff;
            this.yoff = yoff;
        }
        
        private static int checksum(final CharSequence layout) {
            return checksum(layout, 0);
        }
        
        private static int checksum(final CharSequence layout, final int start) {
            int csum = 0;
            for (int i = start; i < layout.length(); ++i) {
                csum = (csum >> 1) + ((csum & 0x1) << 15);
                csum += layout.charAt(i);
            }
            return csum;
        }
        
        private static Layout parseCell(final Layout parent, String layout) {
            final Matcher matcher = Layout.PATTERN.matcher(layout);
            if (!matcher.matches()) {
                throw new IllegalArgumentException("Bad syntax");
            }
            final Layout cell = new Layout();
            cell.type = Type.WindowPane;
            cell.parent = parent;
            cell.sx = Integer.parseInt(matcher.group(1));
            cell.sy = Integer.parseInt(matcher.group(2));
            cell.xoff = Integer.parseInt(matcher.group(3));
            cell.yoff = Integer.parseInt(matcher.group(4));
            if (parent != null) {
                parent.cells.add(cell);
            }
            layout = matcher.group(5);
            if (layout == null || layout.isEmpty()) {
                return cell;
            }
            if (layout.charAt(0) == ',') {
                int i;
                for (i = 1; i < layout.length() && Character.isDigit(layout.charAt(i)); ++i) {}
                if (i == layout.length()) {
                    return cell;
                }
                if (layout.charAt(i) == ',') {
                    layout = layout.substring(i);
                }
            }
            switch (layout.charAt(0)) {
                case '{': {
                    cell.type = Type.LeftRight;
                    final int i = findMatch(layout, '{', '}');
                    parseCell(cell, layout.substring(1, i));
                    layout = layout.substring(i + 1);
                    if (!layout.isEmpty() && layout.charAt(0) == ',') {
                        parseCell(parent, layout.substring(1));
                    }
                    return cell;
                }
                case '[': {
                    cell.type = Type.TopBottom;
                    final int i = findMatch(layout, '[', ']');
                    parseCell(cell, layout.substring(1, i));
                    layout = layout.substring(i + 1);
                    if (!layout.isEmpty() && layout.charAt(0) == ',') {
                        parseCell(parent, layout.substring(1));
                    }
                    return cell;
                }
                case ',': {
                    parseCell(parent, layout.substring(1));
                    return cell;
                }
                default: {
                    throw new IllegalArgumentException("Unexpected '" + layout.charAt(0) + "'");
                }
            }
        }
        
        static {
            PATTERN = Pattern.compile("([0-9]+)x([0-9]+),([0-9]+),([0-9]+)([^0-9]\\S*)?");
        }
        
        enum Type
        {
            LeftRight, 
            TopBottom, 
            WindowPane;
        }
    }
    
    private static class VirtualConsole implements Closeable
    {
        private final ScreenTerminal terminal;
        private final Consumer<VirtualConsole> closer;
        private final int id;
        private int left;
        private int top;
        private final Layout layout;
        private int active;
        private boolean clock;
        private final OutputStream masterOutput;
        private final OutputStream masterInputOutput;
        private final LineDisciplineTerminal console;
        
        public VirtualConsole(final int id, final String type, final int left, final int top, final int columns, final int rows, final Runnable dirty, final Consumer<VirtualConsole> closer, final Layout layout) throws IOException {
            final String name = String.format("tmux%02d", id);
            this.id = id;
            this.left = left;
            this.top = top;
            this.closer = closer;
            this.terminal = new ScreenTerminal(columns, rows) {
                @Override
                protected void setDirty() {
                    super.setDirty();
                    dirty.run();
                }
            };
            this.masterOutput = new MasterOutputStream();
            this.masterInputOutput = new OutputStream() {
                @Override
                public void write(final int b) throws IOException {
                    VirtualConsole.this.console.processInputByte(b);
                }
            };
            (this.console = new LineDisciplineTerminal(name, type, this.masterOutput, null) {
                @Override
                protected void doClose() throws IOException {
                    super.doClose();
                    closer.accept(VirtualConsole.this);
                }
            }).setSize(new Size(columns, rows));
            this.layout = layout;
        }
        
        Layout layout() {
            return this.layout;
        }
        
        public int left() {
            return this.left;
        }
        
        public int top() {
            return this.top;
        }
        
        public int right() {
            return this.left() + this.width();
        }
        
        public int bottom() {
            return this.top() + this.height();
        }
        
        public int width() {
            return this.console.getWidth();
        }
        
        public int height() {
            return this.console.getHeight();
        }
        
        public LineDisciplineTerminal getConsole() {
            return this.console;
        }
        
        public OutputStream getMasterInputOutput() {
            return this.masterInputOutput;
        }
        
        public void resize(final int left, final int top, final int width, final int height) {
            this.left = left;
            this.top = top;
            this.console.setSize(new Size(width, height));
            this.terminal.setSize(width, height);
            this.console.raise(Terminal.Signal.WINCH);
        }
        
        public void dump(final long[] fullscreen, final int ftop, final int fleft, final int fheight, final int fwidth, final int[] cursor) {
            this.terminal.dump(fullscreen, ftop, fleft, fheight, fwidth, cursor);
        }
        
        @Override
        public void close() throws IOException {
            this.console.close();
        }
        
        private class MasterOutputStream extends OutputStream
        {
            private final ByteArrayOutputStream buffer;
            private final CharsetDecoder decoder;
            
            private MasterOutputStream() {
                this.buffer = new ByteArrayOutputStream();
                this.decoder = Charset.defaultCharset().newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);
            }
            
            @Override
            public synchronized void write(final int b) {
                this.buffer.write(b);
            }
            
            @Override
            public void write(final byte[] b, final int off, final int len) throws IOException {
                this.buffer.write(b, off, len);
            }
            
            @Override
            public synchronized void flush() throws IOException {
                int size = this.buffer.size();
                if (size > 0) {
                    CharBuffer out;
                    ByteBuffer in;
                    while (true) {
                        out = CharBuffer.allocate(size);
                        in = ByteBuffer.wrap(this.buffer.toByteArray());
                        final CoderResult result = this.decoder.decode(in, out, false);
                        if (!result.isOverflow()) {
                            break;
                        }
                        size *= 2;
                    }
                    this.buffer.reset();
                    this.buffer.write(in.array(), in.arrayOffset(), in.remaining());
                    if (out.position() > 0) {
                        out.flip();
                        VirtualConsole.this.terminal.write(out);
                        VirtualConsole.this.masterInputOutput.write(VirtualConsole.this.terminal.read().getBytes());
                    }
                }
            }
            
            @Override
            public void close() throws IOException {
                this.flush();
            }
        }
    }
}
