001package org.jdrupes.builder.core.progessui;
002
003import java.util.ArrayDeque;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.Iterator;
007import java.util.LinkedHashMap;
008import java.util.List;
009import java.util.Map;
010import java.util.Objects;
011import java.util.Optional;
012import java.util.Queue;
013import java.util.stream.IntStream;
014
015import io.vavr.Tuple;
016import io.vavr.Tuple2;
017import sun.misc.Signal;
018import sun.misc.SignalHandler;
019
020public class SplitConsole {
021
022    private class ManagedLine {
023        public Thread thread;
024        public String text = "";
025        public String lastRendered = null;
026        public Spinner spinner = Spinner.DOTS;
027        public Color color = Color.BLUE;
028    }
029
030    private final Terminal term;
031
032    private final List<ManagedLine> managedLines = new ArrayList<>();
033    private final Map<Thread, String> offScreenLines = new LinkedHashMap<>();
034    private final Spinner[] spinners = Spinner.values();
035
036    public SplitConsole() {
037        this.term = new Terminal();
038
039        if (term.supportsAnsi) {
040            for (int i = 0; i < term.lines() * 1 / 3; i++) {
041                System.out.println();
042            }
043            recomputeLayout(term.lines() * 1 / 3);
044            redraw();
045            installSigWinchHandler();
046        }
047    }
048
049    private ManagedLine managedLine(Thread thread) {
050        return managedLines.stream().filter(l -> l.thread == thread).findFirst()
051            .orElse(null);
052    }
053
054    public synchronized void allocateLine() {
055        Thread thread = Thread.currentThread();
056        if (managedLine(thread) != null || offScreenLines.containsKey(thread)) {
057            return;
058        }
059
060        // Find free and allocate or use overflow
061        IntStream.range(0, managedLines.size())
062            .filter(i -> managedLines.get(i).thread == null).findFirst()
063            .ifPresentOrElse(i -> {
064                initManaged(i, thread, "");
065            }, () -> {
066                offScreenLines.put(thread, "");
067            });
068    }
069
070    private void initManaged(int index, Thread thread, String text) {
071        var line = managedLines.get(index);
072        line.thread = thread;
073        line.text = text;
074        line.spinner = Spinner.CYCLE;
075        line.color = pickColor(thread);
076    }
077
078    public synchronized void deallocateLine() {
079        Thread thread = Thread.currentThread();
080        var line = managedLine(thread);
081
082        if (line != null) {
083            line.thread = null;
084            line.text = "";
085            promoteOffScreenLines();
086            redraw();
087        } else {
088            offScreenLines.remove(thread);
089        }
090    }
091
092    private synchronized void recomputeLayout(int managedHeight) {
093        while (managedLines.size() > managedHeight) {
094            if (managedLines.get(managedLines.size() - 1).thread == null) {
095                managedLines.remove(managedLines.size() - 1);
096                break;
097            }
098            int freeSlot;
099            for (freeSlot = 0; freeSlot < managedLines.size() - 1; freeSlot++) {
100                if (managedLines.get(freeSlot).thread == null) {
101                    break;
102                }
103            }
104            if (freeSlot < managedLines.size() - 1) {
105                ManagedLine last = managedLines.get(managedLines.size() - 1);
106                managedLines.set(freeSlot, last);
107                continue;
108            }
109            ManagedLine last = managedLines.get(managedLines.size() - 1);
110            offScreenLines.put(last.thread, last.text);
111            last.thread = null;
112        }
113
114        if (managedLines.size() < managedHeight) {
115            while (managedLines.size() < managedHeight) {
116                managedLines.add(new ManagedLine());
117            }
118            promoteOffScreenLines();
119        }
120    }
121
122    private void promoteOffScreenLines() {
123        for (int i = 0; i < managedLines.size(); i++) {
124            if (offScreenLines.isEmpty()) {
125                break;
126            }
127            if (managedLines.get(i).thread == null) {
128                shiftManaged(i);
129                var offLine = offScreenLines.entrySet().iterator().next();
130                initManaged(managedLines.size() - 1, offLine.getKey(),
131                    offLine.getValue());
132                offScreenLines.remove(offLine.getKey());
133            }
134        }
135    }
136
137    private void shiftManaged(int index) {
138        var lastManaged = managedLines.size() - 1;
139        for (int i = index; i < lastManaged; i++) {
140            var toMove = managedLines.get(i + 1);
141            managedLines.set(i, toMove);
142            toMove.lastRendered = null;
143        }
144        managedLines.set(lastManaged, new ManagedLine());
145    }
146
147    private void installSigWinchHandler() {
148        try {
149            Signal.handle(new Signal("WINCH"), new SignalHandler() {
150                @Override
151                public void handle(Signal sig) {
152                    term.refreshSize();
153//                    recomputeLayout();
154                    redraw();
155                }
156            });
157        } catch (Throwable ignored) {
158        }
159    }
160
161    public synchronized void writeLine(String text) {
162        Thread thread = Thread.currentThread();
163//        Integer index = managedLineByThread.get(thread);
164        var line = managedLine(thread);
165
166//        String spinnerChar = "";
167//        Spinner spinner = threadSpinners.get(thread);
168//        if (spinner != null) {
169//            spinnerChar = spinner.next();
170//        }
171//
172//        String coloredText = applyColor(thread, spinnerChar + " " + text);
173
174        if (line != null) {
175            line.text = text;
176            redraw();
177        } else if (offScreenLines.containsKey(thread)) {
178            offScreenLines.put(thread, text);
179        }
180    }
181
182//    private String applyColor(Thread thread, String text) {
183//        Integer color = threadColors.get(thread);
184//        if (color != null) {
185//            return Ansi.color(color) + text + Ansi.RESET;
186//        }
187//        return text;
188//    }
189
190    private Color pickColor(Thread thread) {
191        Color[] codes = Color.values();
192        return codes[Math.abs(thread.hashCode()) % codes.length];
193    }
194
195    public void println(String text) {
196        int lastScrolled = term.lines() - managedLines.size() - 1;
197        System.out.println(Ansi.hideCursor() + Ansi.scrollRange(0, lastScrolled)
198            + Ansi.scrollUp() + Ansi.scrollAll()
199            + Ansi.cursorAt(lastScrolled - 1, 0)
200            + Ansi.showCursor() + text);
201        System.out.flush();
202    }
203
204    private synchronized void redraw() {
205        redraw(false);
206    }
207
208    private synchronized void redraw(boolean force) {
209        if (!term.supportsAnsi) {
210            return;
211        }
212
213        var managedStart = term.lines() - managedLines.size();
214        System.out.print(Ansi.hideCursor());
215        for (int i = 0; i < managedLines.size(); i++) {
216            ManagedLine line = managedLines.get(i);
217            var row = managedStart + i;
218            if (!Objects.equals(line.text, line.lastRendered) || force) {
219                System.out.print(Ansi.cursorAt(row, 0) + Ansi.clearLine()
220                    + line.text);
221                line.lastRendered = line.text;
222            }
223        }
224
225        System.out.print(Ansi.cursorAt(managedStart - 1, 0)
226            + Ansi.showCursor());
227        System.out.flush();
228    }
229
230    public void close() {
231        if (term.supportsAnsi) {
232            System.out.print(Ansi.showCursor());
233        }
234    }
235
236    enum Spinner {
237        CYCLE(new String[] { "-", "\\", "|", "/" }),
238        DOTS(new String[] { ".", "..", "...", "   " });
239
240        private final String[] frames;
241        private int idx = 0;
242
243        Spinner(String[] frames) {
244            this.frames = frames;
245        }
246
247        public String next() {
248            String frame = frames[idx % frames.length];
249            idx++;
250            return frame;
251        }
252    }
253
254    public enum Color {
255        RED(31),
256        GREEN(32),
257        YELLOW(33),
258        BLUE(34),
259        MAGENTA(35),
260        CYAN(36);
261
262        private final int code;
263
264        Color(int code) {
265            this.code = code;
266        }
267
268        public int code() {
269            return code;
270        }
271    }
272
273}