/*
 * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.awt.X11;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Label;
import java.awt.MouseInfo;
import java.awt.Panel;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.BreakIterator;
import java.util.concurrent.ArrayBlockingQueue;

import sun.awt.SunToolkit;
import sun.awt.UNIXToolkit;

/**
 * An utility window class. This is a base class for Tooltip and Balloon.
 */
@SuppressWarnings("serial") // JDK-implementation class
public abstract class InfoWindow extends Window {
    private Container container;
    private Closer closer;

    protected InfoWindow(Frame parent, Color borderColor) {
        super(parent);
        setType(Window.Type.POPUP);
        container = new Container() {
            @Override
            public Insets getInsets() {
                return new Insets(1, 1, 1, 1);
            }
        };
        setLayout(new BorderLayout());
        setBackground(borderColor);
        add(container, BorderLayout.CENTER);
        container.setLayout(new BorderLayout());

        closer = new Closer();
    }

    @Override
    public Component add(Component c) {
        container.add(c, BorderLayout.CENTER);
        return c;
    }

    protected void setCloser(Runnable action, int time) {
        closer.set(action, time);
    }

    // Must be executed on EDT.
    @SuppressWarnings("deprecation")
    protected void show(Point corner, int indent) {
        assert EventQueue.isDispatchThread();

        pack();

        Dimension size = getSize();
        Rectangle scrSize = getGraphicsConfiguration().getBounds();

        if (corner.x < scrSize.x + scrSize.width/2 && corner.y < scrSize.y + scrSize.height/2) { // 1st square
            setLocation(corner.x + indent, corner.y + indent);

        } else if (corner.x >= scrSize.x + scrSize.width/2 && corner.y < scrSize.y + scrSize.height/2) { // 2nd square
            setLocation(corner.x - indent - size.width, corner.y + indent);

        } else if (corner.x < scrSize.x + scrSize.width/2 && corner.y >= scrSize.y + scrSize.height/2) { // 3rd square
            setLocation(corner.x + indent, corner.y - indent - size.height);

        } else if (corner.x >= scrSize.x +scrSize.width/2 && corner.y >= scrSize.y +scrSize.height/2) { // 4th square
            setLocation(corner.x - indent - size.width, corner.y - indent - size.height);
        }

        super.show();
        closer.schedule();
    }

    @Override
    @SuppressWarnings("deprecation")
    public void hide() {
        closer.close();
    }

    private final class Closer implements Runnable {
        Runnable action;
        int time;

        @Override
        public void run() {
            doClose();
        }

        void set(Runnable action, int time) {
            this.action = action;
            this.time = time;
        }

        void schedule() {
            XToolkit.schedule(this, time);
        }

        void close() {
            XToolkit.remove(this);
            doClose();
        }

        // WARNING: this method may be executed on Toolkit thread.
        @SuppressWarnings("deprecation")
        private void doClose() {
            SunToolkit.executeOnEventHandlerThread(InfoWindow.this, new Runnable() {
                public void run() {
                    InfoWindow.super.hide();
                    invalidate();
                    if (action != null) {
                        action.run();
                    }
                }
            });
        }
    }


    private interface LiveArguments {
        /** Whether the target of the InfoWindow is disposed. */
        boolean isDisposed();

        /** The bounds of the target of the InfoWindow. */
        Rectangle getBounds();
    }

    @SuppressWarnings("serial") // JDK-implementation class
    public static final class Tooltip extends InfoWindow {

        public interface LiveArguments extends InfoWindow.LiveArguments {
            /** The tooltip to be displayed. */
            String getTooltipString();
        }

        private final Object target;
        private final LiveArguments liveArguments;

        private final Label textLabel = new Label("");
        private final Runnable starter = new Runnable() {
                public void run() {
                    display();
                }};

        private static final int TOOLTIP_SHOW_TIME = 10000;
        private static final int TOOLTIP_START_DELAY_TIME = 1000;
        private static final int TOOLTIP_MAX_LENGTH = 64;
        private static final int TOOLTIP_MOUSE_CURSOR_INDENT = 5;
        private static final Color TOOLTIP_BACKGROUND_COLOR = new Color(255, 255, 220);
        private static final Font TOOLTIP_TEXT_FONT = XWindow.getDefaultFont();

        public Tooltip(Frame parent, Object target,
                LiveArguments liveArguments)
        {
            super(parent, Color.black);

            this.target = target;
            this.liveArguments = liveArguments;

            XTrayIconPeer.suppressWarningString(this);

            setCloser(null, TOOLTIP_SHOW_TIME);
            textLabel.setBackground(TOOLTIP_BACKGROUND_COLOR);
            textLabel.setFont(TOOLTIP_TEXT_FONT);
            add(textLabel);
        }

        /*
         * WARNING: this method is executed on Toolkit thread!
         */
        private void display() {
            // Execute on EDT to avoid deadlock (see 6280857).
            SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
                    public void run() {
                        if (liveArguments.isDisposed()) {
                            return;
                        }

                        String tooltipString = liveArguments.getTooltipString();
                        if (tooltipString == null) {
                            return;
                        } else if (tooltipString.length() >  TOOLTIP_MAX_LENGTH) {
                            textLabel.setText(tooltipString.substring(0, TOOLTIP_MAX_LENGTH));
                        } else {
                            textLabel.setText(tooltipString);
                        }

                        Point pointer = !isPointerOverTrayIcon(liveArguments.getBounds())
                                        ? null : MouseInfo.getPointerInfo().getLocation();
                        if (pointer == null) {
                            return;
                        }
                        show(new Point(pointer.x, pointer.y), TOOLTIP_MOUSE_CURSOR_INDENT);
                    }
                });
        }

        public void enter() {
            XToolkit.schedule(starter, TOOLTIP_START_DELAY_TIME);
        }

        public void exit() {
            XToolkit.remove(starter);
            if (isVisible()) {
                hide();
            }
        }

        private boolean isPointerOverTrayIcon(Rectangle trayRect) {
            Point p = MouseInfo.getPointerInfo().getLocation();
            return !(p.x < trayRect.x || p.x > (trayRect.x + trayRect.width) ||
                     p.y < trayRect.y || p.y > (trayRect.y + trayRect.height));
        }
    }

    @SuppressWarnings("serial") // JDK-implementation class
    public static final class Balloon extends InfoWindow {

        public interface LiveArguments extends InfoWindow.LiveArguments {
            /** The action to be performed upon clicking the balloon. */
            String getActionCommand();
        }

        private final LiveArguments liveArguments;
        private final Object target;

        private static final int BALLOON_SHOW_TIME = 10000;
        private static final int BALLOON_TEXT_MAX_LENGTH = 256;
        private static final int BALLOON_WORD_LINE_MAX_LENGTH = 16;
        private static final int BALLOON_WORD_LINE_MAX_COUNT = 4;
        private static final int BALLOON_ICON_WIDTH = 32;
        private static final int BALLOON_ICON_HEIGHT = 32;
        private static final int BALLOON_TRAY_ICON_INDENT = 0;
        private static final Color BALLOON_CAPTION_BACKGROUND_COLOR = new Color(200, 200 ,255);
        private static final Font BALLOON_CAPTION_FONT = new Font(Font.DIALOG, Font.BOLD, 12);

        private Panel mainPanel = new Panel();
        private Panel captionPanel = new Panel();
        private Label captionLabel = new Label("");
        private Button closeButton = new Button("X");
        private Panel textPanel = new Panel();
        private XTrayIconPeer.IconCanvas iconCanvas = new XTrayIconPeer.IconCanvas(BALLOON_ICON_WIDTH, BALLOON_ICON_HEIGHT);
        private Label[] lineLabels = new Label[BALLOON_WORD_LINE_MAX_COUNT];
        private ActionPerformer ap = new ActionPerformer();

        private Image iconImage;
        private Image errorImage;
        private Image warnImage;
        private Image infoImage;
        private boolean gtkImagesLoaded;

        private Displayer displayer = new Displayer();

        public Balloon(Frame parent, Object target, LiveArguments liveArguments) {
            super(parent, new Color(90, 80 ,190));
            this.liveArguments = liveArguments;
            this.target = target;

            XTrayIconPeer.suppressWarningString(this);

            setCloser(new Runnable() {
                    public void run() {
                        if (textPanel != null) {
                            textPanel.removeAll();
                            textPanel.setSize(0, 0);
                            iconCanvas.setSize(0, 0);
                            XToolkit.awtLock();
                            try {
                                displayer.isDisplayed = false;
                                XToolkit.awtLockNotifyAll();
                            } finally {
                                XToolkit.awtUnlock();
                            }
                        }
                    }
                }, BALLOON_SHOW_TIME);

            add(mainPanel);

            captionLabel.setFont(BALLOON_CAPTION_FONT);
            captionLabel.addMouseListener(ap);

            captionPanel.setLayout(new BorderLayout());
            captionPanel.add(captionLabel, BorderLayout.WEST);
            captionPanel.add(closeButton, BorderLayout.EAST);
            captionPanel.setBackground(BALLOON_CAPTION_BACKGROUND_COLOR);
            captionPanel.addMouseListener(ap);

            closeButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        hide();
                    }
                });

            mainPanel.setLayout(new BorderLayout());
            mainPanel.setBackground(Color.white);
            mainPanel.add(captionPanel, BorderLayout.NORTH);
            mainPanel.add(iconCanvas, BorderLayout.WEST);
            mainPanel.add(textPanel, BorderLayout.CENTER);

            iconCanvas.addMouseListener(ap);

            for (int i = 0; i < BALLOON_WORD_LINE_MAX_COUNT; i++) {
                lineLabels[i] = new Label();
                lineLabels[i].addMouseListener(ap);
                lineLabels[i].setBackground(Color.white);
            }

            displayer.thread.start();
        }

        public void display(String caption, String text, String messageType) {
            if (!gtkImagesLoaded) {
                loadGtkImages();
            }
            displayer.display(caption, text, messageType);
        }

        private void _display(String caption, String text, String messageType) {
            captionLabel.setText(caption);

            BreakIterator iter = BreakIterator.getWordInstance();
            if (text != null) {
                iter.setText(text);
                int start = iter.first(), end;
                int nLines = 0;

                do {
                    end = iter.next();

                    if (end == BreakIterator.DONE ||
                        text.substring(start, end).length() >= 50)
                    {
                        lineLabels[nLines].setText(text.substring(start, end == BreakIterator.DONE ?
                                                                  iter.last() : end));
                        textPanel.add(lineLabels[nLines++]);
                        start = end;
                    }
                    if (nLines == BALLOON_WORD_LINE_MAX_COUNT) {
                        if (end != BreakIterator.DONE) {
                            lineLabels[nLines - 1].setText(
                                lineLabels[nLines - 1].getText() + " ...");
                        }
                        break;
                    }
                } while (end != BreakIterator.DONE);


                textPanel.setLayout(new GridLayout(nLines, 1));
            }

            if ("ERROR".equals(messageType)) {
                iconImage = errorImage;
            } else if ("WARNING".equals(messageType)) {
                iconImage = warnImage;
            } else if ("INFO".equals(messageType)) {
                iconImage = infoImage;
            } else {
                iconImage = null;
            }

            if (iconImage != null) {
                Dimension tpSize = textPanel.getSize();
                iconCanvas.setSize(BALLOON_ICON_WIDTH, (BALLOON_ICON_HEIGHT > tpSize.height ?
                                                        BALLOON_ICON_HEIGHT : tpSize.height));
                iconCanvas.validate();
            }

            SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
                    public void run() {
                        if (liveArguments.isDisposed()) {
                            return;
                        }
                        Point parLoc = getParent().getLocationOnScreen();
                        Dimension parSize = getParent().getSize();
                        show(new Point(parLoc.x + parSize.width/2, parLoc.y + parSize.height/2),
                             BALLOON_TRAY_ICON_INDENT);
                        if (iconImage != null) {
                            iconCanvas.updateImage(iconImage); // call it after the show(..) above
                        }
                    }
                });
        }

        @Override
        public void dispose() {
            displayer.thread.interrupt();
            super.dispose();
        }

        private void loadGtkImages() {
            if (!gtkImagesLoaded) {
                //check whether the gtk version is >= 3.10 as the Icon names were
                //changed from this release
                UNIXToolkit tk = (UNIXToolkit) Toolkit.getDefaultToolkit();
                if (tk.checkGtkVersion(3, 10, 0)) {
                    errorImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.dialog-error.6.rtl");
                    warnImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.dialog-warning.6.rtl");
                    infoImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.dialog-information.6.rtl");
                } else {
                    errorImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.gtk-dialog-error.6.rtl");
                    warnImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.gtk-dialog-warning.6.rtl");
                    infoImage = (Image) tk.getDesktopProperty(
                            "gtk.icon.gtk-dialog-info.6.rtl");
                }
                gtkImagesLoaded = true;
            }
        }
        @SuppressWarnings("deprecation")
        private final class ActionPerformer extends MouseAdapter {
            @Override
            public void mouseClicked(MouseEvent e) {
                // hide the balloon by any click
                hide();
                if (e.getButton() == MouseEvent.BUTTON1) {
                    ActionEvent aev = new ActionEvent(target, ActionEvent.ACTION_PERFORMED,
                                                      liveArguments.getActionCommand(),
                                                      e.getWhen(), e.getModifiers());
                    XToolkit.postEvent(aev);
                }
            }
        }

        private final class Displayer implements Runnable {
            final int MAX_CONCURRENT_MSGS = 10;

            ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(MAX_CONCURRENT_MSGS);
            boolean isDisplayed;
            final Thread thread;

            Displayer() {
                this.thread = new Thread(null, this, "Displayer", 0, false);
                this.thread.setDaemon(true);
            }

            @Override
            public void run() {
                while (true) {
                    Message msg = null;
                    try {
                        msg = messageQueue.take();
                    } catch (InterruptedException e) {
                        return;
                    }

                    /*
                     * Wait till the previous message is displayed if any
                     */
                    XToolkit.awtLock();
                    try {
                        while (isDisplayed) {
                            try {
                                XToolkit.awtLockWait();
                            } catch (InterruptedException e) {
                                return;
                            }
                        }
                        isDisplayed = true;
                    } finally {
                        XToolkit.awtUnlock();
                    }
                    _display(msg.caption, msg.text, msg.messageType);
                }
            }

            void display(String caption, String text, String messageType) {
                messageQueue.offer(new Message(caption, text, messageType));
            }
        }

        private static final class Message {
            String caption, text, messageType;

            Message(String caption, String text, String messageType) {
                this.caption = caption;
                this.text = text;
                this.messageType = messageType;
            }
        }
    }
}
