/*
    Реализация спецификаций 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 javax.microedition.lcdui;

import malik.emulator.application.*;
import malik.emulator.media.graphics.*;
import malik.emulator.microedition.*;
import malik.emulator.microedition.lcdui.*;

public class TextBox extends ScrollingScreen implements Input
{
    private static final Font EDITOR_FONT;

    static {
        EDITOR_FONT = Font.getFont(Font.FONT_INPUT_TEXT);
    }

    class Helper extends ScrollingScreen.Helper
    {
        public Helper() {
        }

        protected void execute() {
            serviceSizeChanged();
            serviceSplitText();
            serviceScroll();
            servicePaint();
        }

        protected final void serviceSplitText() {
            TextBox owner;
            synchronized((owner = TextBox.this).monitor)
            {
                if(owner.builder.isModified()) owner.correctScrollBar(true);
            }
        }
    }

    private char[] buffer;
    private final CommandOwner box;
    private final Input alternative;
    final InputStringBuilder builder;
    final Object monitor;

    public TextBox(String title, String inputText, int maximumLength, int constraints) {
        this(title, null, false, inputText, maximumLength, constraints);
    }

    public TextBox(String title, Ticker ticker, boolean fullScreen, String inputText, int maximumLength, int constraints) {
        super(title, ticker, fullScreen, SCROLL_VERT, new SystemScrollBarStyle());
        Command[] commands;
        CommandOwner box;
        InputStringBuilder builder;
        Object monitor = builder = new InputStringBuilder(inputText, maximumLength, constraints);
        this.box = box = builder.new AdditionalCapabilities(true, monitor) {
            public void commandAction(Command command, Displayable screen) {
                super.commandAction(command, screen);
                synchronized(monitor)
                {
                    if(parentBuilder().isModified()) (TextBox.this).correctScrollBar(true);
                }
            }
        };
        this.alternative = builder.newInput();
        this.builder = builder;
        this.monitor = monitor;
        for(int len = (commands = box.getMyOwnedCommands()) == null ? 0 : commands.length, i = 0; i < len; i++)
        {
            Command command;
            if((command = commands[i]) != null) super.addCommand(command);
        }
    }

    public void insert(char[] src, int offset, int length, int position) {
        int error;
        if(src == null)
        {
            throw new NullPointerException("TextBox.insert: аргумент src равен нулевой ссылке.");
        }
        Array.checkBound("TextBox.insert", src.length, offset, length);
        if(length <= 0) return;
        error = 0;
        synchronized(monitor)
        {
            label0:
            {
                int currentLength;
                InputStringBuilder builder = this.builder;
                if(length + (currentLength = builder.length()) > builder.getMaximumLength())
                {
                    error = 1;
                    break label0;
                }
                alternative.insert(src, offset, length, position);
                if(currentLength == builder.length()) error = 2;
            }
        }
        switch(error)
        {
        case 1:
            throw new IllegalArgumentException("TextBox.insert: попытка превысить максимальную длину.");
        case 2:
            throw new IllegalArgumentException("TextBox.insert: текст не соответствует ограничениям.");
        }
        requestPaint(CLIENT);
    }

    public void insert(String text, int position) {
        int error;
        int length;
        if(text == null)
        {
            throw new NullPointerException("TextBox.insert: аргумент text равен нулевой ссылке.");
        }
        if((length = text.length()) <= 0) return;
        error = 0;
        synchronized(monitor)
        {
            label0:
            {
                int currentLength;
                InputStringBuilder builder = this.builder;
                if(length + (currentLength = builder.length()) > builder.getMaximumLength())
                {
                    error = 1;
                    break label0;
                }
                alternative.insert(text, position);
                if(currentLength == builder.length()) error = 2;
            }
        }
        switch(error)
        {
        case 1:
            throw new IllegalArgumentException("TextBox.insert: попытка превысить максимальную длину.");
        case 2:
            throw new IllegalArgumentException("TextBox.insert: текст не соответствует ограничениям.");
        }
        requestPaint(CLIENT);
    }

    public void delete(int offset, int length) {
        int error = 0;
        synchronized(monitor)
        {
            label0:
            {
                int currentLength;
                InputStringBuilder builder;
                if(!Array.isBoundValid(currentLength = (builder = this.builder).length(), offset, length))
                {
                    error = 1;
                    break label0;
                }
                alternative.delete(offset, length);
                if(length > 0 && currentLength == builder.length()) error = 2;
            }
        }
        switch(error)
        {
        case 1:
            throw new StringIndexOutOfBoundsException("TextBox.delete: индекс выходит из диапазона.");
        case 2:
            throw new IllegalArgumentException("TextBox.delete: текст не соответствует ограничениям.");
        }
        requestPaint(CLIENT);
    }

    public void setConstraints(int constraints) {
        if(!InputStringBuilder.isConstraintsValid(constraints))
        {
            throw new IllegalArgumentException("TextBox.setConstraints: аргумент constraints имеет недопустимое значение.");
        }
        synchronized(monitor)
        {
            Command[] commands;
            CommandOwner box;
            for(int i = (commands = (box = this.box).getMyOwnedCommands()) == null ? 0 : commands.length; i-- > 0; ) super.removeCommand(commands[i]);
            alternative.setConstraints(constraints);
            for(int len = (commands = box.getMyOwnedCommands()) == null ? 0 : commands.length, i = 0; i < len; i++)
            {
                Command command;
                if((command = commands[i]) != null) super.addCommand(command);
            }
        }
        requestPaint(CLIENT);
    }

    public void setChars(char[] src, int offset, int length) {
        int error;
        if(src != null)
        {
            Array.checkBound("TextBox.setChars", src.length, offset, length);
            Array.copy(src, offset, src = new char[length], offset = 0, length);
        } else
        {
            offset = length = 0;
        }
        error = 0;
        synchronized(monitor)
        {
            label0:
            {
                InputStringBuilder builder;
                if(length > (builder = this.builder).getMaximumLength())
                {
                    error = 1;
                    break label0;
                }
                if(!InputStringBuilder.isConstraintsMatch(builder.getConstraints(), src, offset, length))
                {
                    error = 2;
                    break label0;
                }
                alternative.setChars(src, offset, length);
            }
        }
        switch(error)
        {
        case 1:
            throw new IllegalArgumentException("TextBox.setChars: попытка превысить максимальную длину.");
        case 2:
            throw new IllegalArgumentException("TextBox.setChars: текст не соответствует ограничениям.");
        }
        requestPaint(CLIENT);
    }

    public void setString(String inputText) {
        int error = 0;
        int length = inputText == null ? 0 : inputText.length();
        synchronized(monitor)
        {
            label0:
            {
                InputStringBuilder builder;
                if(length > (builder = this.builder).getMaximumLength())
                {
                    error = 1;
                    break label0;
                }
                if(!InputStringBuilder.isConstraintsMatch(builder.getConstraints(), inputText))
                {
                    error = 2;
                    break label0;
                }
                alternative.setString(inputText);
            }
        }
        switch(error)
        {
        case 1:
            throw new IllegalArgumentException("TextBox.setString: попытка превысить максимальную длину.");
        case 2:
            throw new IllegalArgumentException("TextBox.setString: текст не соответствует ограничениям.");
        }
        requestPaint(CLIENT);
    }

    public void setInitialInputMode(String characterSubset) {
        alternative.setInitialInputMode(characterSubset);
    }

    public int setMaxSize(int maximumSize) {
        int result;
        if(maximumSize <= 0)
        {
            throw new IllegalArgumentException("TextBox.setMaxSize: аргумент maximumSize может быть только положительным.");
        }
        synchronized(monitor)
        {
            result = alternative.setMaxSize(maximumSize);
        }
        requestPaint(CLIENT);
        return result;
    }

    public int size() {
        return alternative.size();
    }

    public int getConstraints() {
        return alternative.getConstraints();
    }

    public int getCaretPosition() {
        return alternative.getCaretPosition();
    }

    public int getChars(char[] dst) {
        int error;
        int result;
        if(dst == null)
        {
            throw new NullPointerException("TextBox.getChars: аргумент dst равен нулевой ссылке.");
        }
        error = 0;
        synchronized(monitor)
        {
            label0:
            {
                if(dst.length < builder.length())
                {
                    error = 1;
                    result = 0;
                    break label0;
                }
                result = alternative.getChars(dst);
            }
        }
        if(error == 1)
        {
            throw new ArrayIndexOutOfBoundsException("TextBox.getChars: индекс массива выходит из диапазона.");
        }
        return result;
    }

    public int getMaxSize() {
        return alternative.getMaxSize();
    }

    public String getString() {
        String result;
        synchronized(monitor)
        {
            result = alternative.getString();
        }
        return result;
    }

    void paint(ScreenGraphics render) {
        int temp;
        int line;
        int srow;
        int frow;
        Font font = EDITOR_FONT;
        srow = (temp = render.getClipY()) / (line = font.getHeight());
        frow = (temp + render.getClipHeight() - 1) / line;
        render.setFont(font);
        synchronized(monitor)
        {
            int caretLine;
            int caretPosition;
            char[] buffer = this.buffer;
            InputStringBuilder builder = this.builder;
            render.setColor(RasterCanvas.getSystemColor((builder.getConstraints() & UNEDITABLE) == 0 ? 0x21 : 0x23));
            caretLine = builder.lineAt(caretPosition = builder.getCaretPosition());
            for(int length = builder.lines(), top = srow * line, index = srow; index <= frow && index < length; top += line, index++)
            {
                int lofs = builder.lineOffset(index);
                int llen = builder.lineLength(index);
                if(buffer == null || buffer.length < llen) buffer = this.buffer = new char[InputStringBuilder.optimalCapacity(llen)];
                builder.copy(lofs, lofs + llen, buffer, 0);
                render.drawChars(buffer, 0, llen, 2, top, Graphics.LEFT | Graphics.TOP);
                if(caretLine == index)
                {
                    int caretLeft = font.charsWidth(buffer, 0, caretPosition - lofs) + 1;
                    render.drawLine(caretLeft, top, caretLeft, top + line - 1);
                }
            }
        }
    }

    void paintBackground(ScreenGraphics render, int width, int height, byte visibleElements, byte drawingElements) {
        int elementTop = 0;
        int elementHeight;
        if((visibleElements & TICKER) != 0)
        {
            elementHeight = getTickerHeight();
            if((drawingElements & TICKER) != 0) render.drawElement(13, 0, 0, 0, elementTop, width, elementHeight);
            elementTop += elementHeight;
        }
        if((visibleElements & TITLE) != 0)
        {
            elementHeight = getTitleHeight();
            if((drawingElements & TITLE) != 0) render.drawElement(6, 1, 0, 0, elementTop, width, elementHeight);
            elementTop += elementHeight;
        }
        elementHeight = height - elementTop;
        if((visibleElements & PANEL) != 0)
        {
            int panelHeight = getPanelHeight();
            if((drawingElements & PANEL) != 0)
            {
                render.setClip(0, height - panelHeight, width, panelHeight);
                render.drawElement(6, 5, 0, 0, elementTop, width, elementHeight);
                render.setClip(0, 0, width, height);
            }
            elementHeight -= panelHeight;
        }
        if((visibleElements & drawingElements & CLIENT) != 0) render.drawElement(8, (builder.getConstraints() & UNEDITABLE) != 0 ? 3 : 1, 0, -2, elementTop, width + 4, elementHeight);
    }

    void onSizeChanged(int width, int height) {
        builder.setModified(true);
        super.onSizeChanged(width, height);
    }

    void onCommandAction(Command command) {
        CommandOwner box;
        if((box = this.box).isMyOwnedCommand(command))
        {
            box.commandAction(command, this);
            return;
        }
        super.onCommandAction(command);
    }

    void onClientKeyboardEvent(KeyboardEvent event) {
        int key = event.getKey();
        int action = event.getAction();
        DeviceSettings settings = DeviceManager.getInstance().getSettings();
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_LEFT))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED) synchronized(monitor)
            {
                InputStringBuilder builder;
                (builder = this.builder).setCaretPosition(builder.getCaretPosition() - 1);
                if(builder.isModified()) correctScrollBar(false);
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_RIGHT))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED) synchronized(monitor)
            {
                InputStringBuilder builder;
                (builder = this.builder).setCaretPosition(builder.getCaretPosition() + 1);
                if(builder.isModified()) correctScrollBar(false);
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_UP))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED) synchronized(monitor)
            {
                int caretRow;
                int caretPosition;
                InputStringBuilder builder;
                if((caretRow = (builder = this.builder).lineAt(caretPosition = builder.getCaretPosition())) > 0) moveCaret(builder, caretRow--, caretPosition, caretRow);
            }
            return;
        }
        if(key == settings.getKeyUsedAs(DeviceSettings.DEVICE_KEY_DOWN))
        {
            if(action == KeyboardEvent.ACTION_KEY_PRESSED || action == KeyboardEvent.ACTION_KEY_REPEATED) synchronized(monitor)
            {
                int caretRow;
                int caretPosition;
                InputStringBuilder builder;
                if((caretRow = (builder = this.builder).lineAt(caretPosition = builder.getCaretPosition())) < builder.lines() - 1) moveCaret(builder, caretRow++, caretPosition, caretRow);
            }
            return;
        }
        synchronized(monitor)
        {
            InputStringBuilder builder;
            (builder = this.builder).keyboardEvent(event);
            if(builder.isModified()) correctScrollBar(true);
        }
    }

    void onClientPointerEvent(PointerEvent event) {
        int action = event.getAction();
        if(
            event.isButtonPressed(PointerEvent.BUTTON_MAIN) || (action == PointerEvent.ACTION_BUTTON_RELEASED ||
            action == PointerEvent.ACTION_POINTER_RELEASED) && event.getButton() == PointerEvent.BUTTON_MAIN
        ) synchronized(monitor)
        {
            int limit;
            int caretRow;
            InputStringBuilder builder;
            if((caretRow = event.getY() / EDITOR_FONT.getHeight()) > (limit = (builder = this.builder).lines() - 1)) caretRow = limit;
            if(caretRow < 0) caretRow = 0;
            moveCaret(builder, event.getX() - 2, caretRow);
        }
    }

    Displayable.Helper createHelper() {
        return this.new Helper();
    }

    final void correctScrollBar(boolean splitNeeded) {
        int pos;
        int page;
        int top1;
        int top2;
        int step;
        int line;
        ScrollBar scroll;
        Font font = EDITOR_FONT;
        InputStringBuilder builder = this.builder;
        if(splitNeeded) builder.split(font, getApplicationWidth() - 4);
        line = builder.lineAt(builder.getCaretPosition());
        step = font.getHeight();
        top1 = step * line;
        top2 = step + top1;
        (scroll = super.vert).setRange(builder.lines() * step);
        if(top1 < (pos = scroll.getPosition()))
        {
            scroll.setPosition(top1);
        }
        else if(top2 > pos + (page = scroll.getPage()))
        {
            scroll.setPosition(top2 - page);
        }
        else
        {
            requestPaint(CLIENT);
        }
    }

    private void moveCaret(InputStringBuilder builder, int caretRow, int caretPosition, int newCaretRow) {
        int lofs = builder.lineOffset(caretRow);
        int llen;
        int caretCol = caretPosition - lofs;
        int charsWidth;
        char[] buffer;
        Font font;
        if((buffer = this.buffer) == null || buffer.length < caretCol) buffer = this.buffer = new char[InputStringBuilder.optimalCapacity(caretCol)];
        builder.copy(lofs, caretPosition, buffer, 0);
        charsWidth = (font = EDITOR_FONT).charsWidth(buffer, 0, caretCol);
        lofs = builder.lineOffset(newCaretRow);
        llen = builder.lineLength(newCaretRow);
        if(buffer.length < llen) buffer = this.buffer = new char[InputStringBuilder.optimalCapacity(llen)];
        builder.copy(lofs, lofs + llen, buffer, 0);
        label0:
        {
            for(int w1 = 0, w2, i = 1; i <= llen; 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 = llen;
        }
        builder.setCaretPosition(lofs + caretCol);
        if(builder.isModified()) correctScrollBar(false);
    }

    private void moveCaret(InputStringBuilder builder, int charsWidth, int newCaretRow) {
        int caretCol;
        int lofs = builder.lineOffset(newCaretRow);
        int llen = builder.lineLength(newCaretRow);
        char[] lbuf;
        Font font = EDITOR_FONT;
        if(charsWidth < 0) charsWidth = 0;
        if((lbuf = buffer) == null || lbuf.length < llen) lbuf = buffer = new char[InputStringBuilder.optimalCapacity(llen - 1) + 1];
        builder.copy(lofs, lofs + llen, lbuf, 0);
        label0:
        {
            for(int w1 = 0, w2, i = 1; i <= llen; w1 = w2, i++)
            {
                w2 = font.charsWidth(lbuf, 0, i);
                if(charsWidth >= w1 && charsWidth < w2)
                {
                    caretCol = charsWidth - w1 <= w2 - charsWidth ? i - 1 : i;
                    break label0;
                }
            }
            caretCol = llen;
        }
        builder.setCaretPosition(lofs + caretCol);
        if(builder.isModified()) correctScrollBar(false);
    }
}
