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

public abstract class Item extends Object
{
    private static final class CommandList extends Object implements Command.Owner
    {
        private int commandSize;
        private Command[] commandList;
        private Command commandDefault;

        public CommandList() {
            this.commandList = new Command[7];
        }

        public boolean isMyOwnedCommand(Command command) {
            boolean result;
            synchronized(Command.MONITOR)
            {
                result = command != null && (command == commandDefault || Array.findf(commandList, 0, command) < commandSize);
            }
            return result;
        }

        public int size() {
            return commandSize;
        }

        public Command commandAt(int index) {
            return index >= 0 && index < commandSize ? commandList[index] : null;
        }

        public Command defaultCommand() {
            return commandDefault;
        }

        public void addCommand(Command command) {
            int len;
            Command[] commands;
            if(command == null || command == commandDefault || Array.findf(commands = commandList, 0, command) < (len = commandSize)) return;
            if(len == commands.length) Array.copy(commands, 0, commands = commandList = new Command[(len << 1) + 1], 0, len);
            commands[len++] = command;
            commandSize = len;
        }

        public void removeCommand(Command command) {
            int len;
            int index;
            Command[] commands;
            if(command == null) return;
            if(command == commandDefault)
            {
                commandDefault = null;
                return;
            }
            if((index = Array.findf(commands = commandList, 0, command)) > (len = commandSize - 1)) return;
            if(index < len) Array.copy(commands, index + 1, commands, index, len - index);
            commands[len] = null;
            commandSize = len;
        }

        public void setDefaultCommand(Command command) {
            Command previous = commandDefault;
            commandDefault = null;
            addCommand(previous);
            removeCommand(command);
            commandDefault = command;
        }
    }

    private static final char LABEL_END = ':';
    private static final int LAYOUT_MASK = ~0x7f33;
    public static final int LAYOUT_DEFAULT        = 0x0000;
    public static final int LAYOUT_LEFT           = 0x0001;
    public static final int LAYOUT_RIGHT          = 0x0002;
    public static final int LAYOUT_CENTER         = 0x0003;
    public static final int LAYOUT_TOP            = 0x0010;
    public static final int LAYOUT_BOTTOM         = 0x0020;
    public static final int LAYOUT_VCENTER        = 0x0030;
    public static final int LAYOUT_NEWLINE_BEFORE = 0x0100;
    public static final int LAYOUT_NEWLINE_AFTER  = 0x0200;
    public static final int LAYOUT_SHRINK         = 0x0400;
    public static final int LAYOUT_EXPAND         = 0x0800;
    public static final int LAYOUT_VSHRINK        = 0x1000;
    public static final int LAYOUT_VEXPAND        = 0x2000;
    public static final int LAYOUT_2              = 0x4000;
    public static final int PLAIN     = 0;
    public static final int HYPERLINK = 1;
    public static final int BUTTON    = 2;
    static final int LEFT   = 0;
    static final int TOP    = 1;
    static final int WIDTH  = 2;
    static final int HEIGHT = 3;
    static final int DIR_NONE  = 0;
    static final int DIR_UP    = Canvas.UP;
    static final int DIR_DOWN  = Canvas.DOWN;
    static final int DIR_LEFT  = Canvas.LEFT;
    static final int DIR_RIGHT = Canvas.RIGHT;
    private static final long WIDTH_MASK = (1L << 32) - 1L;
    private static final long HEIGHT_MASK = -1L << 32;

    static final Font LABEL_FONT;
    static final Object OWNER_MONITOR;

    static {
        LABEL_FONT = Font.getFont(Font.FONT_STATIC_TEXT);
        OWNER_MONITOR = new Object();
    }

    private static void writeLabel(StringBuilder builder, String label) {
        int len;
        builder.clear();
        if(label != null && (len = label.length()) > 0)
        {
            builder.append(label);
            if(label.charAt(len - 1) != LABEL_END) builder.append(LABEL_END);
        }
    }

    private static long packSize(int width, int height) {
        return (long) width & WIDTH_MASK | (long) height << 32;
    }

    boolean newLine;
    boolean visible;
    int left;
    int top;
    int width;
    int height;
    int minimumWidth;
    int minimumHeight;
    private int layout;
    private long preferredSize;
    private long copyPreferredSize;
    private long minimumContentSize;
    private long currentContentSize;
    private long previousContentSize;
    private char[] buffer;
    Screen owner;
    private ItemCommandListener listener;
    private String labelAsString;
    private final MultilinedStringBuilder labelAsBuilder;
    private final CommandList commands;

    Item() {
        this(null, LAYOUT_DEFAULT);
    }

    Item(String label) {
        this(label, LAYOUT_DEFAULT);
    }

    Item(String label, int layout) {
        MultilinedStringBuilder builder;
        if((layout & LAYOUT_MASK) != 0)
        {
            throw new IllegalArgumentException("Item: аргумент layout имеет недопустимое значение.");
        }
        this.layout = layout;
        this.preferredSize = -1L;
        this.previousContentSize = -1L;
        this.labelAsString = label;
        this.labelAsBuilder = builder = new MultilinedStringBuilder();
        this.commands = new CommandList();
        writeLabel(builder, label);
    }

    public void notifyStateChanged() {
        Screen owner;
        if((owner = this.owner) == null)
        {
            throw new IllegalStateException("Item.notifyStateChanged: элемент не принадлежит какому-либо экрану.");
        }
        owner.requestStateChanged(this);
    }

    public void addCommand(Command command) {
        Screen owner;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.addCommand: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        if(command == null)
        {
            throw new NullPointerException("Item.addCommand: аргумент command равен нулевой ссылке.");
        }
        synchronized(Command.MONITOR)
        {
            commands.addCommand(command);
        }
        if(owner != null && owner.isFocused(this))
        {
            owner.placeCommands();
            owner.requestPaintAll();
        }
    }

    public void removeCommand(Command command) {
        Screen owner;
        if(command == null) return;
        synchronized(Command.MONITOR)
        {
            commands.removeCommand(command);
        }
        if((owner = this.owner) != null && owner.isFocused(this))
        {
            owner.placeCommands();
            owner.requestPaintAll();
        }
    }

    public void setDefaultCommand(Command command) {
        Screen owner;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.setDefaultCommand: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        synchronized(Command.MONITOR)
        {
            commands.setDefaultCommand(command);
        }
        if(owner != null && owner.isFocused(this))
        {
            owner.placeCommands();
            owner.requestPaintAll();
        }
    }

    public void setItemCommandListener(ItemCommandListener listener) {
        Screen owner;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.setItemCommandListener: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        this.listener = listener;
    }

    public void setPreferredSize(int width, int height) {
        Screen owner;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.setPreferredSize: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        if(width < -1)
        {
            throw new IllegalArgumentException("Item.setPreferredSize: аргумент width не может быть меньше (-1).");
        }
        if(height < -1)
        {
            throw new IllegalArgumentException("Item.setPreferredSize: аргумент height не может быть меньше (-1).");
        }
        this.preferredSize = packSize(width, height);
        if(owner != null) owner.requestInvalidate();
    }

    public void setLayout(int layout) {
        Screen owner;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.setLayout: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        if((layout & LAYOUT_MASK) != 0)
        {
            throw new IllegalArgumentException("Item.setLayout: аргумент layout имеет недопустимое значение.");
        }
        this.layout = layout;
        if(owner != null) owner.requestInvalidate();
    }

    public void setLabel(String label) {
        Screen owner;
        StringBuilder builder;
        if((owner = this.owner) != null && !owner.focusSupported())
        {
            throw new IllegalStateException("Item.setLabel: экран-владелец не поддерживает установку фокуса ввода на элемент.");
        }
        synchronized(builder = labelAsBuilder)
        {
            labelAsString = label;
            writeLabel(builder, label);
        }
        if(owner != null) owner.requestInvalidate();
    }

    public int getMinimumWidth() {
        return minimumWidth;
    }

    public int getMinimumHeight() {
        return minimumHeight;
    }

    public int getPreferredWidth() {
        int result;
        return (result = (int) preferredSize) < 0 ? width : result;
    }

    public int getPreferredHeight() {
        int result;
        return (result = (int) (preferredSize >> 32)) < 0 ? height : result;
    }

    public int getLayout() {
        return layout;
    }

    public String getLabel() {
        return labelAsString;
    }

    abstract void paint(Graphics render, int contentWidth, int contentHeight);

    abstract int getMinimumContentWidth();

    abstract int getMinimumContentHeight();

    abstract int getPreferredContentWidth(int contentHeight);

    abstract int getPreferredContentHeight(int contentWidth);

    void onShow() {
    }

    void onHide() {
    }

    void onSizeChanged(int contentWidth, int contentHeight) {
    }

    void onCommandAction(Command command) {
        ItemCommandListener listener;
        if((listener = this.listener) != null) listener.commandAction(command, this);
    }

    void onKeyboardEvent(KeyboardEvent event) {
    }

    void onPointerEvent(PointerEvent event) {
    }

    void onFocusLost() {
    }

    boolean onFocusMove(int direction, int viewportWidth, int viewportHeight, int[] visibleRectangle) {
        return false;
    }

    boolean hasRowBreakAfter(int layout, int alignment) {
        int actual;
        return (layout & LAYOUT_NEWLINE_AFTER) != 0 || (actual = layout & LAYOUT_CENTER) != 0 && actual != alignment;
    }

    boolean hasRowBreakBefore(int layout, int alignment) {
        int actual;
        return (layout & LAYOUT_NEWLINE_BEFORE) != 0 || (actual = layout & LAYOUT_CENTER) != 0 && actual != alignment;
    }

    int getDefaultLayout() {
        return LAYOUT_DEFAULT;
    }

    final void onPaint(ScreenGraphics render) {
        try
        {
            int clipLeft = render.getClipX();
            int clipTop = render.getClipY();
            int clipWidth = render.getClipWidth();
            int clipHeight = render.getClipHeight();
            int translateX = render.getTranslateX();
            int translateY = render.getTranslateY();
            int labelHeight;
            int contentWidth;
            int contentHeight;
            long contentSize;
            contentWidth = (int) (contentSize = currentContentSize);
            contentHeight = (int) (contentSize >> 32);
            labelHeight = height - contentHeight;
            if(clipTop < labelHeight) paintLabel(render, width, labelHeight, clipTop, clipHeight);
            if(contentWidth > 0 && contentHeight > 0 && clipTop + clipHeight > labelHeight && clipLeft < contentWidth)
            {
                int diff;
                if((diff = labelHeight - clipTop) > 0)
                {
                    clipTop += diff;
                    clipHeight -= diff;
                }
                if(clipLeft + clipWidth > contentWidth) clipWidth = contentWidth - clipLeft;
                clipLeft += translateX;
                clipTop += translateY;
                translateY += labelHeight;
                render.reset();
                render.restricts(clipLeft, clipTop, clipWidth, clipHeight);
                render.setStartPoint(translateX, translateY);
                render.setClip(clipLeft, clipTop, clipWidth, clipHeight);
                render.translate(translateX, translateY);
                paint(render, contentWidth, contentHeight);
            }
        }
        catch(RuntimeException e)
        {
            e.printRealStackTrace();
        }
    }

    final void onSizeChanged() {
        long contentSize;
        if(previousContentSize != (contentSize = getContentSize()))
        {
            previousContentSize = contentSize;
            onSizeChanged((int) contentSize, (int) (contentSize >> 32));
        }
    }

    final void requestPaint() {
        Screen owner;
        if((owner = this.owner) != null && visible) owner.requestPaint(Displayable.CLIENT);
    }

    final void requestInvalidate() {
        Screen owner;
        if((owner = this.owner) != null) owner.requestInvalidate();
    }

    final void requestDefaultCommandAction() {
        Screen owner;
        final Command command;
        if((owner = this.owner) != null && (command = commands.defaultCommand()) != null)
        {
            owner.requestAction(new Runnable() {
                public void run() {
                    (Item.this).onCommandAction(command);
                }
            });
        }
    }

    final void resetContentSize() {
        currentContentSize = -1L;
    }

    final void writeContentRectangleTo(int[] rect) {
        int itemHeight;
        long contentSize = currentContentSize;
        rect[0] = 0;
        rect[1] = (itemHeight = height) - (int) (contentSize >> 32);
        rect[2] = (int) contentSize;
        rect[3] = itemHeight;
    }

    final boolean hasLabel() {
        return labelAsString != null;
    }

    final int suppliedLayout() {
        return layout;
    }

    final int suppliedPreferredWidth() {
        long copy = copyPreferredSize = preferredSize;
        return (int) copy;
    }

    final int suppliedPreferredHeight() {
        return (int) (copyPreferredSize >> 32);
    }

    final int computeMinimumWidth() {
        int minLabelWidth = getMinimumLabelWidth();
        int minContentWidth = getMinimumContentWidth();
        minimumContentSize = minimumContentSize & HEIGHT_MASK | (long) minContentWidth;
        return minLabelWidth < minContentWidth ? minContentWidth : minLabelWidth;
    }

    final int computeMinimumHeight() {
        int minLabelHeight = getMinimumLabelHeight();
        int minContentHeight = getMinimumContentHeight();
        minimumContentSize = minimumContentSize & WIDTH_MASK | (long) minContentHeight << 32;
        return minLabelHeight + minContentHeight;
    }

    final int computePreferredWidth(int height) {
        int minSize;
        int maxWidth = width;
        int prefLabelWidth;
        int prefLabelHeight;
        int prefContentWidth;
        long prefLabelSize;
        prefLabelWidth = (int) (prefLabelSize = getPreferredLabelSize(height < 0 ? 100 : maxWidth));
        prefLabelHeight = (int) (prefLabelSize >> 32);
        if(height >= 0) height = height > prefLabelHeight ? height - prefLabelHeight : 0;
        if((prefContentWidth = getPreferredContentWidth(height)) < (minSize = (int) minimumContentSize)) prefContentWidth = minSize;
        if(height >= 0 && prefContentWidth > maxWidth) prefContentWidth = maxWidth;
        currentContentSize = packSize(prefContentWidth, height);
        return prefLabelWidth < prefContentWidth ? prefContentWidth : prefLabelWidth;
    }

    final int computePreferredHeight(int width) {
        int minSize;
        int prefLabelHeight = getPreferredLabelHeight(width);
        int prefContentHeight;
        if((prefContentHeight = getPreferredContentHeight(width)) < (minSize = (int) (minimumContentSize >> 32))) prefContentHeight = minSize;
        currentContentSize = packSize(width, prefContentHeight);
        return prefLabelHeight + prefContentHeight;
    }

    final Command.Owner getCommands() {
        return commands;
    }

    final ItemCommandListener getListener() {
        return listener;
    }

    private void paintLabel(Graphics render, int labelWidth, int labelHeight, int clipTop, int clipHeight) {
        int line;
        int srow;
        int frow;
        Font font = LABEL_FONT;
        MultilinedStringBuilder builder;
        srow = clipTop / (line = font.getHeight());
        frow = (clipTop + clipHeight - 1) / line;
        render.setFont(font);
        render.setColor(RasterCanvas.getSystemColor(0x28));
        synchronized(builder = labelAsBuilder)
        {
            char[] lbuf = buffer;
            builder.split(font, labelWidth);
            for(int len = builder.lines(), top = srow * line, index = srow; index <= frow && index < len; top += line, index++)
            {
                int lofs = builder.lineOffset(index);
                int llen = builder.lineLength(index);
                if(lbuf == null || lbuf.length < llen) lbuf = buffer = new char[StringBuilder.optimalCapacity(llen)];
                builder.copy(lofs, lofs + llen, lbuf, 0);
                render.drawChars(lbuf, 0, llen, 0, top, Graphics.LEFT | Graphics.TOP);
            }
        }
    }

    private int getMinimumLabelWidth() {
        int result = 0;
        StringBuilder builder;
        synchronized(builder = labelAsBuilder)
        {
            if(!builder.isEmpty()) result = 100;
        }
        return result;
    }

    private int getMinimumLabelHeight() {
        int result = 0;
        MultilinedStringBuilder builder;
        synchronized(builder = labelAsBuilder)
        {
            if(!builder.isEmpty())
            {
                Font font;
                builder.split(font = LABEL_FONT, Integer.MAX_VALUE);
                result = font.getHeight() * builder.lines();
            }
        }
        return result;
    }

    private int getPreferredLabelHeight(int labelWidth) {
        int result = 0;
        MultilinedStringBuilder builder;
        synchronized(builder = labelAsBuilder)
        {
            if(!builder.isEmpty())
            {
                Font font;
                builder.split(font = LABEL_FONT, labelWidth >= 0 ? labelWidth : Integer.MAX_VALUE);
                result = font.getHeight() * builder.lines();
            }
        }
        return result;
    }

    private long getPreferredLabelSize(int labelWidth) {
        int width = 0;
        int height = 0;
        MultilinedStringBuilder builder;
        synchronized(builder = labelAsBuilder)
        {
            if(!builder.isEmpty())
            {
                int index;
                Font font;
                builder.split(font = LABEL_FONT, labelWidth);
                width = 100;
                height = font.getHeight() * (index = builder.lines());
                for(char[] lbuf = buffer; index-- > 0; )
                {
                    int lwid;
                    int lofs = builder.lineOffset(index);
                    int llen = builder.lineLength(index);
                    if(lbuf == null || lbuf.length < llen) lbuf = buffer = new char[StringBuilder.optimalCapacity(llen)];
                    builder.copy(lofs, lofs + llen, lbuf, 0);
                    if(width < (lwid = font.charsWidth(lbuf, 0, llen))) width = lwid;
                }
            }
        }
        return packSize(width, height);
    }

    private long getContentSize() {
        int contentWidth;
        int contentHeight;
        long contentSize;
        contentWidth = (int) (contentSize = currentContentSize);
        contentHeight = (int) (contentSize >> 32);
        if(contentWidth < 0)
        {
            int minSize;
            int itemWidth = width;
            int itemHeight = height;
            int labelWidth;
            int labelHeight;
            long labelSize;
            labelWidth = (int) (labelSize = getPreferredLabelSize(itemWidth));
            labelHeight = (int) (labelSize >> 32);
            if(contentHeight < 0) contentHeight = labelHeight < itemHeight ? itemHeight - labelHeight : 0;
            if(labelWidth < itemWidth)
            {
                contentWidth = itemWidth;
            }
            else if((contentWidth = (int) preferredSize) < (minSize = (int) minimumContentSize))
            {
                if((contentWidth = getPreferredContentWidth(contentHeight)) < minSize) contentWidth = minSize;
            }
            if(contentWidth > itemWidth) contentWidth = itemWidth;
        }
        else if(contentHeight < 0)
        {
            int itemWidth = width;
            int itemHeight = height;
            int labelHeight = getPreferredLabelHeight(itemWidth);
            contentHeight = labelHeight < itemHeight ? itemHeight - labelHeight : 0;
        }
        else
        {
            return contentSize;
        }
        return currentContentSize = packSize(contentWidth, contentHeight);
    }
}
