/*
    Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
    и других спецификаций для функционирования компактных приложений на языке
    Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

    Copyright © 2016–2017, 2019–2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять ее и/или изменять
    ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она будет полезной,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <https://www.gnu.org/licenses/>.
*/

package malik.emulator.microedition.system;

import javax.microedition.lcdui.*;
import malik.emulator.application.*;
import malik.emulator.media.graphics.*;
import malik.emulator.microedition.*;
import malik.emulator.microedition.lcdui.*;
import malik.emulator.microedition.system.console.*;

public final class Console extends CustomBackgroundScreen
{
    private static final char SPACE = 32;
    private static final int COLS = 0x0080;
    private static final int ROWS = 0x0200;
    private static final int BACK_COLOR   = 0x101010;
    private static final int TITLE_COLOR  = 0xc0d0c0;
    private static final int PANEL_COLOR  = 0xffffff;
    private static final int TICKER_COLOR = 0x809080;
    public static final int DEFAULT_COLOR = 0xc0c0c0;

    static final Command BACK;
    static final Command EXECUTE;
    private static final ConsoleCommand HELP_COMMAND;
    private static final ConsoleCommand CLEAR_COMMAND;

    static {
        BACK = new Command("Назад", Command.BACK, Integer.MIN_VALUE);
        EXECUTE = new Command("Выполнить", Command.OK, Integer.MIN_VALUE);
        HELP_COMMAND = new ConsoleCommand(
            "?",
            "Использование:\n ?\n ? <команда>\nВариант без аргумента выдаст общую справку по консоли.\nВариант с аргументом выдаст справку по команде, указанной в аргументе."
        ) {
            protected void execute(String[] arguments, Console console) {
                if(arguments.length > 0)
                {
                    ConsoleCommand command;
                    String name = arguments[0];
                    if((command = console.getConsoleCommand(name)) == null || command.hidden)
                    {
                        console.println("Неизвестная команда: ".concat(name));
                        return;
                    }
                    console.println(command.help);
                    return;
                }
                console.printHelp();
            }
        };
        CLEAR_COMMAND = new ConsoleCommand("очистить", "Использование:\n очистить\nОчищает консоль от текста.") {
            protected void execute(String[] arguments, Console console) {
                console.clear();
            }
        };
    }

    private static int getLength(short[] attrs, int offset) {
        int len = attrs.length;
        int attr = attrs[offset++];
        int result = 1;
        for(; offset < len && attrs[offset++] == attr; result++);
        return result;
    }

    private static int getLineEndIndex(String string, int startFromIndex, int defaultValue) {
        int result1 = string.indexOf('\r', startFromIndex);
        int result2 = string.indexOf('\n', startFromIndex);
        return result1 >= 0 ? result2 >= 0 ? (result1 >= result2 ? result2 : result1) : result1 : (result2 >= 0 ? result2 : defaultValue);
    }

    private static String toString(char[] src, int offset, int length) {
        int len;
        char[] str;
        Array.copy(src, offset, str = new char[len = length], 0, len);
        for(int i = len - 1; i-- > 0; )
        {
            char next;
            if(str[i] == '\\' && ((next = str[i + 1]) == '\"' || next == '\\' || next == 'n'))
            {
                Array.copy(str, i + 1, str, i, len-- - i - 1);
                if(next == 'n') str[i] = '\n';
            }
        }
        return new String(str, 0, len);
    }

    private boolean shiftPressed;
    private int size;
    private int index;
    private int count;
    private int lines;
    private int focused;
    private int scrollHorz;
    private int scrollVert;
    private final int lineHeight;
    private final char[] buffer;
    private final char[] chars;
    private final short[] attrs;
    private final short[] sizes;
    private final String[] history;
    private ConsoleCommand[] commands;
    private final InputStringBuilder entered;
    private final Font font;
    CommandListener listener;
    final SystemManager manager;
    private final Object monitor;

    Console(String title, Ticker ticker, boolean fullScreen, SystemManager manager) {
        super(title, ticker, fullScreen);
        ConsoleCommand[] list;
        Command[] commands;
        Font font;
        SystemFont sfont;
        CommandOwner owner;
        InputStringBuilder entered;
        if((sfont = SystemFont.get("Console")) == SystemFont.getDefault())
        {
            font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
        } else
        {
            font = Font.getFont(sfont, Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
        }
        this.count = 2;
        this.focused = -1;
        this.lineHeight = font.getHeight();
        this.buffer = new char[InputStringBuilder.CAPACITY];
        this.chars = new char[ROWS * COLS];
        this.attrs = new short[ROWS * COLS];
        this.sizes = new short[ROWS];
        this.history = new String[100];
        this.commands = list = new ConsoleCommand[7];
        this.entered = entered = new InputStringBuilder(null, InputStringBuilder.CAPACITY, Input.ANY);
        this.font = font;
        this.manager = manager;
        this.monitor = entered;
        list[0] = HELP_COMMAND;
        list[1] = CLEAR_COMMAND;
        super.setCommandListener(owner = entered.new AdditionalCapabilities(false, entered) {
            public void commandAction(Command command, Displayable screen) {
                CommandListener listener;
                Console parent = Console.this;
                if(command == Console.BACK)
                {
                    parent.manager.switchToApplicationDisplay();
                    return;
                }
                if(command == Console.EXECUTE)
                {
                    parent.executeEnteredCommand();
                    return;
                }
                if(super.isMyOwnedCommand(command) || screen != parent)
                {
                    super.commandAction(command, screen);
                    if(parentBuilder().isModified()) parent.requestPaint();
                    return;
                }
                if((listener = parent.listener) != null) listener.commandAction(command, screen);
            }

            public Command[] getMyOwnedCommands() {
                int len;
                Command[] result;
                if((result = super.getMyOwnedCommands()) == null) return new Command[] { Console.BACK, Console.EXECUTE };
                Array.copy(result, 0, result = new Command[(len = result.length) + 2], 2, len);
                result[0] = Console.BACK;
                result[1] = Console.EXECUTE;
                return result;
            }
        });
        for(int len = (commands = owner.getMyOwnedCommands()) == null ? 0 : commands.length, i = 0; i < len; i++)
        {
            Command command;
            if((command = commands[i]) != null) super.addCommand(command);
        }
    }

    public void setCommandListener(CommandListener listener) {
        this.listener = listener;
    }

    public void addConsoleCommand(ConsoleCommand command) {
        if(command == null)
        {
            throw new NullPointerException("Console.addConsoleCommand: аргумент command равен нулевой ссылке.");
        }
        synchronized(monitor)
        {
            int len;
            ConsoleCommand[] list;
            if((list = commands).length == (len = count))
            {
                Array.copy(list, 0, list = new ConsoleCommand[(len << 1) + 1], 0, len);
                commands = list;
            }
            list[len++] = command;
            count = len;
        }
    }

    public void clear() {
        synchronized(monitor)
        {
            lines = 0;
            Array.fill(sizes, 0, ROWS, 0);
            updateScrollVert();
        }
        repaint();
    }

    public void print(String string) {
        write(string == null ? "null" : string, false, DEFAULT_COLOR);
    }

    public void print(String string, int color) {
        write(string == null ? "null" : string, false, color);
    }

    public void println(String string) {
        write(string == null ? "null" : string, true, DEFAULT_COLOR);
    }

    public void println(String string, int color) {
        write(string == null ? "null" : string, true, color);
    }

    protected void paintBackground(Graphics render, int width, int height, byte visibleElements, byte drawingElements) {
        int elementTop = 0;
        int elementHeight;
        render.setColor(BACK_COLOR);
        if((visibleElements & TICKER) != 0)
        {
            elementHeight = getTickerHeight();
            if((drawingElements & TICKER) != 0) render.fillRect(0, elementTop, width, elementHeight);
            elementTop += elementHeight;
        }
        if((visibleElements & TITLE) != 0)
        {
            elementHeight = getTitleHeight();
            if((drawingElements & TITLE) != 0) render.fillRect(0, elementTop, width, elementHeight);
            elementTop += elementHeight;
        }
        if((visibleElements & PANEL) != 0)
        {
            elementHeight = getPanelHeight();
            if((drawingElements & PANEL) != 0) render.fillRect(0, height - elementHeight, width, elementHeight);
            elementHeight = height - elementTop - elementHeight;
        } else
        {
            elementHeight = height - elementTop;
        }
        if((drawingElements & CLIENT) != 0) render.fillRect(0, elementTop, width, elementHeight);
    }

    protected void paintTicker(Graphics render, int width, int height, Ticker ticker) {
        render.setColor(TICKER_COLOR);
        render.drawString(ticker.getDisplayedString(), ticker.getPosition(), 0, 0);
    }

    protected void paintTitle(Graphics render, int width, int height, String title) {
        int posX = width - 1;
        int posY = height - 1;
        render.setColor(TITLE_COLOR);
        render.drawLine(0, 0, posX, 0);
        render.drawLine(0, posY, posX, posY);
        render.drawString(MultilinedStringBuilder.truncate(title, render.getFont(), width), 0, 2, 0);
    }

    protected void paintClient(Graphics render, int width, int height) {
        int sh;
        int sv;
        int tx;
        int cw;
        int ow;
        int pos;
        int len;
        int temp;
        int srow;
        int frow;
        int line = lineHeight;
        char[] buffer = this.buffer;
        Font font = this.font;
        InputStringBuilder entered = this.entered;
        render.setClip(0, 0, width, height -= line + 1);
        render.translate(-(sh = scrollHorz), -(sv = scrollVert));
        srow = (temp = render.getClipY()) / line;
        frow = (temp + render.getClipHeight() - 1) / line;
        render.setFont(font);
        synchronized(monitor)
        {
            int lines = this.lines;
            char[] chars = this.chars;
            short[] attrs = this.attrs;
            short[] sizes = this.sizes;
            for(int top = srow * line, index = srow, offset = srow * COLS; index <= frow && index <= lines; top += line, index++, offset += COLS)
            {
                for(int left = 0, length = sizes[index], lim = offset + length, begin = offset, end; begin < lim; begin = end)
                {
                    int limit;
                    int strlen;
                    int c = attrs[begin];
                    if((end = begin + getLength(attrs, begin)) > (limit = offset + length)) end = limit;
                    render.setColor((c & 0xf800) << 8 | (c & 0xe000) << 3 | (c & 0x07e0) << 5 | (c & 0x0600) >> 1 | (c & 0x001f) << 3 | (c & 0x0001c) >> 2);
                    render.drawChars(chars, begin, strlen = end - begin, left, top, 0);
                    left += font.charsWidth(chars, begin, strlen);
                }
            }
        }
        render.translate(sh, sv + height);
        render.setClip(0, 0, width, line + 1);
        render.setColor(PANEL_COLOR);
        render.drawLine(0, 0, width - 1, 0);
        entered.copy(0, len = entered.length(), buffer, 0);
        pos = entered.getCaretPosition();
        ow = font.charsWidth(buffer, 0, len);
        cw = font.charsWidth(buffer, 0, pos);
        if((tx = (width >> 1) - cw - 1) + (ow + 1) < width) tx = width - ow - 1;
        if(tx > 0) tx = 0;
        render.translate(tx, 1);
        render.drawChars(buffer, 0, len, 1, 0, 0);
        render.drawLine(cw, 0, cw, line - 1);
    }

    protected void paintPanel(Graphics render, int width, int height, Command[] commands, int pressedCommandIndex) {
        int p1 = width / 3;
        int p2 = width - p1;
        paintCommand(render, 0, 0, p1, height, commands[0], false, pressedCommandIndex == 0);
        paintCommand(render, p1, 0, p2 - p1, height, commands[1], true, pressedCommandIndex == 1);
        paintCommand(render, p2, 0, width - p2, height, commands[2], false, pressedCommandIndex == 2);
    }

    protected void showNotify() {
        shiftPressed = false;
    }

    protected void keyboardNotify(KeyboardEvent event) {
        int key = event.getKey();
        int action = event.getAction();
        InputStringBuilder entered = this.entered;
        DeviceSettings settings = manager.getSettings();
        shiftPressed = event.isKeyPressed(KeyboardEvent.KEY_SHIFT) || event.isKeyPressed(KeyboardEvent.KEY_LSHIFT) || event.isKeyPressed(KeyboardEvent.KEY_RSHIFT);
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_UP))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED)
            {
                int index;
                int size = this.size;
                this.index = index = (index = this.index) <= 0 ? size : index - 1;
                entered.clear();
                if(index < size) entered.append(history[index]);
                if(entered.isModified()) repaint();
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_DOWN))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED)
            {
                int index;
                int size = this.size;
                this.index = index = (index = this.index) >= size ? 0 : index + 1;
                entered.clear();
                if(index < size) entered.append(history[index]);
                if(entered.isModified()) repaint();
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_LEFT))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED)
            {
                entered.setCaretPosition(entered.getCaretPosition() - 1);
                if(entered.isModified()) repaint();
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_RIGHT))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED)
            {
                entered.setCaretPosition(entered.getCaretPosition() + 1);
                if(entered.isModified()) repaint();
            }
            return;
        }
        entered.keyboardEvent(event);
        if(entered.isModified()) repaint();
    }

    protected void pointerNotify(PointerEvent event) {
        int y;
        int line;
        int action;
        switch(action = event.getAction())
        {
        case PointerEvent.ACTION_POINTER_PRESSED:
        case PointerEvent.ACTION_BUTTON_PRESSED:
            switch(event.getButton())
            {
            case PointerEvent.BUTTON_WHEEL_UP:
                if(shiftPressed)
                {
                    scroll(-16, 0);
                } else
                {
                    scroll(0, -lineHeight);
                }
                repaint();
                break;
            case PointerEvent.BUTTON_WHEEL_DOWN:
                if(shiftPressed)
                {
                    scroll(+16, 0);
                } else
                {
                    scroll(0, +lineHeight);
                }
                repaint();
                break;
            default:
                if(action == PointerEvent.ACTION_BUTTON_PRESSED) break;
                y = event.getY();
                line = super.getHeight() - lineHeight - 1;
                if(y < line)
                {
                    focused = 0;
                    break;
                }
                if(y > line)
                {
                    int tx;
                    int cw;
                    int ow;
                    int pos;
                    int len;
                    int caretCol;
                    int charsWidth;
                    int width = super.getWidth();
                    char[] buffer = this.buffer;
                    Font font = this.font;
                    InputStringBuilder entered = this.entered;
                    entered.copy(0, len = entered.length(), buffer, 0);
                    pos = entered.getCaretPosition();
                    ow = font.charsWidth(buffer, 0, len);
                    cw = font.charsWidth(buffer, 0, pos);
                    if((tx = (width >> 1) - cw - 1) + (ow + 1) < width) tx = width - ow - 1;
                    if(tx > 0) tx = 0;
                    charsWidth = event.getX() - tx;
                    label0:
                    {
                        for(int w1 = 0, w2, i = 1; i <= len; w1 = w2, i++)
                        {
                            w2 = font.charsWidth(buffer, 0, i);
                            if(charsWidth >= w1 && charsWidth < w2)
                            {
                                caretCol = charsWidth - w1 <= w2 - charsWidth ? i - 1 : i;
                                break label0;
                            }
                        }
                        caretCol = len;
                    }
                    entered.setCaretPosition(caretCol);
                    if(entered.isModified()) repaint();
                }
                break;
            }
            break;
        case PointerEvent.ACTION_POINTER_RELEASED:
            if(focused == 0)
            {
                focused = -1;
                scroll(event.historicalX(1) - event.getX(), event.historicalY(1) - event.getY());
                repaint();
            }
            break;
        default:
            if(focused == 0)
            {
                scroll(event.historicalX(1) - event.getX(), event.historicalY(1) - event.getY());
                repaint();
            }
            break;
        }
    }

    protected int getPanelCommandIndex(int x, int y, int width, int height) {
        if(x >= 0 && x < width && y >= 0 && y < height)
        {
            int p1 = width / 3;
            int p2 = width - p1;
            return x < p1 ? 0 : x < p2 ? 1 : 2;
        }
        return -1;
    }

    protected int getTickerHeight() {
        return getTickerFont().getHeight() + 1;
    }

    protected int getTitleHeight() {
        return getTitleFont().getHeight() + 4;
    }

    protected int getPanelHeight() {
        return getDefaultCommandFont().getHeight() + 5;
    }

    void requestPaint() {
        repaint();
    }

    void printHelp() {
        int len = count;
        ConsoleCommand[] list = commands;
        write("Доступные команды:", true, DEFAULT_COLOR);
        for(int i = 0; i < len; i++)
        {
            ConsoleCommand command;
            if(!(command = list[i]).hidden) write(" ".concat(command.name), true, DEFAULT_COLOR);
        }
        write(
            "Команда\n ? <команда>\nвыведет справку по заданной команде.\nКоманды вводятся с учётом регистра, аргументы разделяются пробелами.\n" +
            "Аргументы с пробелами следует заключать в кавычки \"…\".\nПоддерживаются escape-последовательности: \\n, \\\" и \\\\.\n" +
            "С помощью стрелок \u2191 и \u2193 можно прокрутить последние набранные команды.\n" +
            "С помощью указывающего устройства можно прокрутить текст консоли.", true, DEFAULT_COLOR
        );
        write("Этим цветом ", false, StdConsoleOutputStream.COLOR);
        write("выводится всё, что приложение выводит через System.out.", true, DEFAULT_COLOR);
        write("Этим цветом ", false, ErrConsoleOutputStream.COLOR);
        write("выводится всё, что приложение выводит через System.err.", true, DEFAULT_COLOR);
    }

    void executeEnteredCommand() {
        boolean q;
        int i0;
        int len;
        int index;
        int argsLen;
        int argsStart;
        char[] buffer;
        String[] args;
        String name;
        String text;
        ConsoleCommand command;
        InputStringBuilder entered;

        /* инициализация */
        (entered = this.entered).copy(0, len = entered.length(), buffer = this.buffer, 0);
        argsStart = -1;
        i0 = -1;

        /* считывание названия команды */
        for(int i = 0; i <= len; i++)
        {
            char c;
            if((c = i < len ? buffer[i] : '\0') > SPACE && (i <= 0 || buffer[i - 1] <= SPACE)) i0 = i;
            if(i > 0 && c <= SPACE && buffer[i - 1] > SPACE)
            {
                argsStart = i + 1;
                break;
            }
        }
        if(i0 < 0 || argsStart < 0) return;
        name = new String(buffer, i0, argsStart - i0 - 1);

        /* считывание аргументов команды */
        q = false;
        argsLen = 0;
        for(int p = buffer[argsStart - 1], c, i = argsStart; i <= len; p = c, i++)
        {
            boolean cq;
            c = i < len ? buffer[i] : '\0';
            if(cq = c == '\"' && p != '\\')
            {
                q = !q;
                c = SPACE;
            }
            if(cq ? !q : q ? i == len : c <= SPACE && p > SPACE) argsLen++;
        }
        q = false;
        index = 0;
        args = new String[argsLen];
        for(int p = buffer[argsStart - 1], c, i = argsStart; i <= len; p = c, i++)
        {
            boolean cq;
            c = i < len ? buffer[i] : '\0';
            if(cq = c == '\"' && p != '\\')
            {
                q = !q;
                c = SPACE;
            }
            if(cq ? q : !q && c > SPACE && p <= SPACE) i0 = cq ? i + 1 : i;
            if(cq ? !q : q ? i == len : c <= SPACE && p > SPACE) args[index++] = toString(buffer, i0, i - i0);
        }
        if(index < argsLen) return;

        /* добавление команды в список недавно введённых */
        addToHistory(text = String.valueOf(buffer, 0, len).intern());
        entered.clear();

        /* выполнение команды */
        write(text, true, PANEL_COLOR);
        serviceRepaints();
        if((command = getConsoleCommand(name)) == null)
        {
            write("Неизвестная команда: ".concat(name), true, DEFAULT_COLOR);
            return;
        }
        command.execute(args, this);
    }

    ConsoleCommand getConsoleCommand(String name) {
        ConsoleCommand[] list = commands;
        for(int i = count; i-- > 0; )
        {
            ConsoleCommand command = list[i];
            if(name.equals(command.name)) return command;
        }
        return null;
    }

    private void paintCommand(Graphics render, int left, int top, int width, int height, Command command, boolean asDefault, boolean asPressed) {
        int x;
        int y;
        Font font;
        if(command == null) return;
        x = left + (width >> 1) + (asPressed ? 1 : 0);
        y = top + (asPressed ? 3 : 2);
        render.setFont(font = asDefault ? getDefaultCommandFont() : getNormalCommandFont());
        render.setColor(PANEL_COLOR);
        render.drawRoundRect(left, top, width - 1, height - 1, 2, 2);
        render.drawString(command.getTruncatedLabel(width - 4, font), x, y, Graphics.HCENTER | Graphics.TOP);
    }

    private void scroll(int deltaX, int deltaY) {
        int line;
        int maxScrollVert = (lines + 1) * (line = lineHeight);
        long newScrollHorz = (long) scrollHorz + (long) deltaX;
        long newScrollVert = (long) scrollVert + (long) deltaY;
        if((maxScrollVert -= super.getHeight() - line - 1) < 0) maxScrollVert = 0;
        scrollHorz = newScrollHorz < 0L ? 0 : newScrollHorz > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) newScrollHorz;
        scrollVert = newScrollVert < 0L ? 0 : newScrollVert > maxScrollVert ? maxScrollVert : (int) newScrollVert;
    }

    private void write(String string, boolean ending, int color) {
        short consoleAttr = (short) ((color & 0xf80000) >> 8 | (color & 0x00fc00) >> 5 | (color & 0x0000f8) >> 3);
        int stringLength = string.length();
        synchronized(monitor)
        {
            int consoleCharOffset;
            int consoleLineLength;
            int consoleLineIndex = lines;
            char[] consoleChars = chars;
            short[] consoleAttrs = attrs;
            short[] consoleSizes = sizes;
            consoleLineLength = consoleSizes[consoleLineIndex];
            consoleCharOffset = COLS * consoleLineIndex + consoleLineLength;
            for(int stringBegin = 0, stringEnd; stringBegin < stringLength; stringBegin = stringEnd)
            {
                boolean newLine;
                int substringLength;
                int stringLengthLimit = COLS - consoleLineLength;
                if((stringEnd = getLineEndIndex(string, stringBegin, stringLength)) < 0)
                {
                    newLine = false;
                    if(stringEnd - stringBegin > stringLengthLimit)
                    {
                        stringEnd = stringBegin + stringLengthLimit;
                    }
                } else
                {
                    if(stringEnd - stringBegin > stringLengthLimit)
                    {
                        newLine = false;
                        stringEnd = stringBegin + stringLengthLimit;
                    } else
                    {
                        newLine = true;
                    }
                }
                substringLength = stringEnd - stringBegin;
                consoleSizes[consoleLineIndex] = (short) (consoleLineLength + substringLength);
                Array.fill(consoleAttrs, consoleCharOffset, substringLength, consoleAttr);
                string.getChars(stringBegin, stringEnd, consoleChars, consoleCharOffset);
                if(stringEnd < stringLength)
                {
                    if(lift(consoleLineIndex, consoleChars, consoleAttrs, consoleSizes)) consoleLineIndex++;
                    consoleCharOffset = COLS * consoleLineIndex;
                    consoleLineLength = 0;
                }
                if(newLine)
                {
                    stringEnd++;
                    if(stringEnd < stringLength && string.charAt(stringEnd - 1) == '\r' && string.charAt(stringEnd) == '\n') stringEnd++;
                }
            }
            if(ending) lift(consoleLineIndex, consoleChars, consoleAttrs, consoleSizes);
            updateScrollVert();
        }
        repaint();
    }

    private void updateScrollVert() {
        if(focused < 0)
        {
            int line;
            int scrollPos;
            int scrollRange = (lines + 1) * (line = lineHeight);
            if((scrollPos = scrollRange - (super.getHeight() - line - 1)) < 0) scrollPos = 0;
            scrollHorz = 0;
            scrollVert = scrollPos;
        }
    }

    private void addToHistory(String command) {
        int size = this.size;
        String[] history = this.history;
        if(size < history.length)
        {
            history[size++] = command;
            this.index = size;
            this.size = size;
            return;
        }
        this.index = size;
        Array.copy(history, 1, history, 0, --size);
        history[size] = command;
    }

    private boolean lift(int lines, char[] chars, short[] attrs, short[] sizes) {
        if(lines < ROWS - 1)
        {
            this.lines = lines + 1;
            return true;
        }
        Array.copy(chars, COLS, chars, 0, COLS * (ROWS - 1));
        Array.fill(chars, COLS * (ROWS - 1), COLS, 0);
        Array.copy(attrs, COLS, attrs, 0, COLS * (ROWS - 1));
        Array.fill(attrs, COLS * (ROWS - 1), COLS, 0);
        Array.copy(sizes, 1, sizes, 0, ROWS - 1);
        sizes[ROWS - 1] = (short) 0;
        return false;
    }
}
