/*
 * @(#)PortingUtils.java 4/12/2006
 *
 * Copyright 2002 - 2006 JIDE Software Inc. All rights reserved.
 */
package com.jidesoft.utils;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.List;

/**
 * A class that keeps all 1.4/1.3 different stuff.
 */
@SuppressWarnings({"UnusedDeclaration"})
public class PortingUtils {
    private static Rectangle SCREEN_BOUNDS = null;

    /**
     * Gets current focused components. If 1.3, just uses event's source; 1.4, used keyboard focus manager to get the
     * correct focused component.
     *
     * @param event the AWT event
     * @return current focused component
     */
    public static Component getCurrentFocusComponent(AWTEvent event) {
        return KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
    }

    /**
     * Gets frame's state. In 1.3, used getState; in 1.4, uses getExtendedState.
     *
     * @param frame the frame
     * @return frame's state
     */
    public static int getFrameState(Frame frame) {
        return frame.getExtendedState();
    }

    /**
     * Sets frame's state. In 1.3, uses sets frame's state; in 1.4, uses gets frame's state.
     *
     * @param frame the frame
     * @param state the state
     */
    public static void setFrameState(Frame frame, int state) {
        frame.setExtendedState(state);
    }

    /**
     * Gets mouse modifiers. If 1.3, uses getModifiers; 1.4, getModifiersEx.
     *
     * @param e the mouse event
     * @return mouse modifiers
     */
    public static int getMouseModifiers(MouseEvent e) {
        return e.getModifiersEx();
    }

    /**
     * Makes sure the component won't receive the focus.
     *
     * @param component the component
     */
    public static void removeFocus(JComponent component) {
        component.setRequestFocusEnabled(false);
        component.setFocusable(false);
    }

    /**
     * Removes the button border.
     *
     * @param button the button
     */
    public static void removeButtonBorder(AbstractButton button) {
        button.setContentAreaFilled(false);
        button.setMargin(new Insets(0, 0, 0, 0));
        button.setBorder(BorderFactory.createEmptyBorder());
    }

    /**
     * To make sure the rectangle is within the screen bounds.
     *
     * @param invoker the invoker component
     * @param rect    the rectangle
     * @return the rectangle that is in the screen bounds.
     */
    public static Rectangle containsInScreenBounds(Component invoker, Rectangle rect) {
        return containsInScreenBounds(invoker, rect, false);
    }

    /**
     * To make sure the rectangle is within the screen bounds.
     *
     * @param invoker          the invoker component
     * @param rect             the rectangle
     * @param useInvokerDevice the flag to return invoker device or not
     * @return the rectangle that is in the screen bounds.
     * @since 3.4.1
     */
    public static Rectangle containsInScreenBounds(Component invoker, Rectangle rect, boolean useInvokerDevice) {
        Rectangle screenBounds = getScreenBounds(invoker, useInvokerDevice);
        Point p = rect.getLocation();
        if (p.x + rect.width > screenBounds.x + screenBounds.width) {
            p.x = screenBounds.x + screenBounds.width - rect.width;
        }
        if (p.y + rect.height > screenBounds.y + screenBounds.height) {
            p.y = screenBounds.y + screenBounds.height - rect.height;
        }
        if (p.x < screenBounds.x) {
            p.x = screenBounds.x;
        }
        if (p.y < screenBounds.y) {
            p.y = screenBounds.y;
        }
        return new Rectangle(p, rect.getSize());
    }

    /**
     * To make sure the rectangle has overlap with the screen bounds.
     *
     * @param invoker the invoker component
     * @param rect    the rectangle
     * @return the rectangle that has overlap with the screen bounds.
     */
    public static Rectangle overlapWithScreenBounds(Component invoker, Rectangle rect) {
        Rectangle screenBounds = getScreenBounds(invoker);
        Point p = rect.getLocation();
        if (p.x > screenBounds.x + screenBounds.width) {
            p.x = screenBounds.x + screenBounds.width - rect.width;
        }
        if (p.y > screenBounds.y + screenBounds.height) {
            p.y = screenBounds.y + screenBounds.height - rect.height;
        }
        if (p.x + rect.width < screenBounds.x) {
            p.x = screenBounds.x;
        }
        if (p.y + rect.height < screenBounds.y) {
            p.y = screenBounds.y;
        }
        return new Rectangle(p, rect.getSize());
    }

    /**
     * Gets the screen size. In JDK1.4+, the returned size will exclude task bar area on Windows OS.
     *
     * @param invoker the invoker component
     * @return the screen size.
     */
    public static Dimension getScreenSize(Component invoker) {
        ensureScreenBounds();

        // to handle multi-display case
        Dimension screenSize = SCREEN_BOUNDS.getSize();  // Toolkit.getDefaultToolkit().getScreenSize();

        // jdk1.4 only
        if (invoker != null && !(invoker instanceof JApplet) && invoker.getGraphicsConfiguration() != null) {
            Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(invoker.getGraphicsConfiguration());
            screenSize.width -= insets.left + insets.right;
            screenSize.height -= insets.top + insets.bottom;
        }

        return screenSize;
    }

    /**
     * Gets the screen size. In JDK1.4+, the returned size will exclude task bar area on Windows OS.
     *
     * @param invoker the invoker component
     * @return the screen size.
     */
    public static Dimension getLocalScreenSize(Component invoker) {
        ensureScreenBounds();

        // jdk1.4 only
        if (invoker != null && !(invoker instanceof JApplet) && invoker.getGraphicsConfiguration() != null) {
            // to handle multi-display case
            GraphicsConfiguration gc = invoker.getGraphicsConfiguration();
            Rectangle bounds = gc.getBounds();
            Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
            bounds.width -= insets.left + insets.right;
            bounds.height -= insets.top + insets.bottom;
            return bounds.getSize();
        }
        else {
            return getScreenSize(invoker);
        }
    }

    /**
     * Gets the screen bounds. In JDK1.4+, the returned bounds will exclude task bar area on Windows OS. If the invoker
     * is null, the whole screen bounds including all display devices will be returned. If the invoker is not null and
     * the useInvokeDevice flag is true, the screen of the display device for the invoker will be returned.
     *
     * @param invoker          the invoker component
     * @param useInvokerDevice the flag to return invoker device or not
     * @return the screen bounds.
     */
    public static Rectangle getScreenBounds(Component invoker, boolean useInvokerDevice) {
        ensureScreenBounds();

        // to handle multi-display case
        Rectangle bounds = (!useInvokerDevice || invoker == null || invoker.getGraphicsConfiguration() == null) ? (Rectangle) SCREEN_BOUNDS.clone() : invoker.getGraphicsConfiguration().getBounds();

        // TODO
        // jdk1.4 only
        if (invoker != null && !(invoker instanceof JApplet) && invoker.getGraphicsConfiguration() != null) {
            Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(invoker.getGraphicsConfiguration());
            bounds.x += insets.left;
            bounds.y += insets.top;
            bounds.width -= insets.left + insets.right;
            bounds.height -= insets.top + insets.bottom;
        }

        return bounds;
    }

    /**
     * Gets the screen bounds. In JDK1.4+, the returned bounds will exclude task bar area on Windows OS.
     * <p/>
     * By default, it will not use invoker graphic device automatically.
     *
     * @param invoker the invoker component
     * @return the screen bounds.
     * @see #getScreenBounds(java.awt.Component, boolean)
     */
    public static Rectangle getScreenBounds(Component invoker) {
        return getScreenBounds(invoker, false);
    }

    /**
     * Gets the local monitor's screen bounds.
     *
     * @return the screen bounds.
     */
    public static Rectangle getLocalScreenBounds() {
        GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();
        return e.getMaximumWindowBounds();
    }

    private static void ensureScreenBounds() {
        if (SCREEN_BOUNDS == null) {
            SCREEN_BOUNDS = new Rectangle();
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice[] gs = ge.getScreenDevices();
            for (GraphicsDevice gd : gs) {
                GraphicsConfiguration gc = gd.getDefaultConfiguration();
                SCREEN_BOUNDS = SCREEN_BOUNDS.union(gc.getBounds());
            }
        }
    }

    private static Area SCREEN_AREA;
    private static Rectangle[] SCREENS;
    private static Insets[] INSETS;

    private static Thread _initializationThread = null;

    /**
     * If you use methods such as {@link #ensureOnScreen(java.awt.Rectangle)}, {@link
     * #getContainingScreenBounds(java.awt.Rectangle, boolean)} or {@link #getScreenArea()} for the first time, it will
     * take up to a few seconds to run because it needs to get device information. To avoid any slowness, you can call
     * call this method in the class where you will use those three methods. This method will spawn a thread to retrieve
     * device information thus it will return immediately. Hopefully, when you use the three methods, the thread is done
     * so user will not notice any slowness.
     */
    synchronized public static void initializeScreenArea() {
        initializeScreenArea(Thread.NORM_PRIORITY);
    }

    /**
     * If you use methods such as {@link #ensureOnScreen(java.awt.Rectangle)}, {@link
     * #getContainingScreenBounds(java.awt.Rectangle, boolean)} or {@link #getScreenArea()} for the first time, it will
     * take up to a couple of seconds to run because it needs to get device information. To avoid any slowness, you can
     * call {@link #initializeScreenArea()} method in the class where you will use those three methods. This method will
     * spawn a thread to retrieve device information thus it will return immediately. Hopefully, when you use the three
     * methods, the thread is done so user will not notice any slowness.
     *
     * @param priority as we will use a thread to calculate the screen area, you can use this parameter to control the
     *                 priority of the thread. If you are waiting for the result before the next step, you should use
     *                 normal priority (which is 5). If you just want to calculate when app starts, you can use a lower
     *                 priority (such as 3). For example, AbstractComboBox needs screen size so that the popup doesn't
     *                 go beyond the screen. So when AbstractComboBox is used, we will kick off the thread at priority
     *                 3. If user clicks on the drop down after the thread finished, there will be no time delay.
     */
    synchronized public static void initializeScreenArea(int priority) {
        if (_initializationThread == null) {
            _initializationThread = new Thread() {
                @Override
                public void run() {
                    SCREEN_AREA = new Area();
                    SCREEN_BOUNDS = new Rectangle();
                    GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
                    List<Rectangle> screensList = new ArrayList<Rectangle>();
                    List<Insets> insetsList = new ArrayList<Insets>();
                    GraphicsDevice[] screenDevices = environment.getScreenDevices();
                    for (GraphicsDevice device : screenDevices) {
                        GraphicsConfiguration configuration = device.getDefaultConfiguration();
                        Rectangle screenBounds = configuration.getBounds();
                        Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(configuration);
                        screensList.add(screenBounds);
                        insetsList.add(insets);
                        SCREEN_AREA.add(new Area(screenBounds));
                        SCREEN_BOUNDS = SCREEN_BOUNDS.union(screenBounds);
                    }
                    SCREENS = screensList.toArray(new Rectangle[screensList.size()]);
                    INSETS = insetsList.toArray(new Insets[screensList.size()]);
                    _initializationThread = null;
                }
            };
            _initializationThread.setPriority(priority);
            if (INITIALIZE_SCREEN_AREA_USING_THREAD) {
                _initializationThread.start();
            }
            else {
                _initializationThread.run();
            }
        }
    }

    public static boolean INITIALIZE_SCREEN_AREA_USING_THREAD = true;

    public static boolean isInitializationThreadAlive() {
        return _initializationThread != null && _initializationThread.isAlive();
    }

    public static boolean isInitalizationThreadStarted() {
        return _initializationThread != null;
    }

    private static void waitForInitialization() {
        initializeScreenArea();

        while (_initializationThread != null && _initializationThread.isAlive()) {
            try {
                Thread.sleep(100);
            }
            catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * Ensures the rectangle is visible on the screen.
     *
     * @param invoker the invoking component
     * @param bounds  the input bounds
     * @return the modified bounds.
     */
    public static Rectangle ensureVisible(Component invoker, Rectangle bounds) {
        Rectangle mainScreenBounds = PortingUtils.getLocalScreenBounds(); // this is fast. Only if it is outside this bounds, we try the more expensive one.
        if (!mainScreenBounds.contains(bounds.getLocation())) {
            Rectangle screenBounds = PortingUtils.getScreenBounds(invoker, false);
            if (bounds.x > screenBounds.x + screenBounds.width || bounds.x < screenBounds.x) {
                bounds.x = screenBounds.x;
            }
            if (bounds.y > screenBounds.y + screenBounds.height || bounds.y < screenBounds.y) {
                bounds.y = screenBounds.y;
            }
        }
        return bounds;
    }

    /**
     * Modifies the position of rect so that it is completely on screen if that is possible. By default, it will allow
     * the rect to cross two screens. You can call {@link #ensureOnScreen(java.awt.Rectangle, boolean)} and set the
     * second parameter to false if you don't want to allow that case.
     *
     * @param rect The rectangle to be moved to a single screen
     * @return rect after its position has been modified
     */
    public static Rectangle ensureOnScreen(Rectangle rect) {
        return ensureOnScreen(rect, true);
    }

    /**
     * Modifies the position of rect so that it is completely on screen if that is possible.
     *
     * @param rect             The rectangle to be moved to a single screen
     * @param allowCrossScreen a flag to allow or disallow when the rect is cross two screens.
     * @return rect after its position has been modified
     */
    public static Rectangle ensureOnScreen(Rectangle rect, boolean allowCrossScreen) {
        // optimize it so that it is faster for most cases
        Rectangle localScreenBounds = getLocalScreenBounds();
        if (localScreenBounds.contains(rect)) {
            return rect;
        }

        waitForInitialization();

        // check if rect is total on screen
        if (allowCrossScreen && getScreenArea().contains(rect)) return rect;
        // see if the top left is on any of the screens
        Rectangle containingScreen = null;
        Point rectPos = rect.getLocation();
        for (Rectangle screenBounds : SCREENS) {
            if (screenBounds.contains(rectPos)) {
                containingScreen = screenBounds;
                break;
            }
        }
        // if not see if rect partial on any screen
        for (Rectangle screenBounds : SCREENS) {
            if (screenBounds.intersects(rect)) {
                containingScreen = screenBounds;
                break;
            }
        }
        // check if it was on any screen
        if (containingScreen == null) {
            // it was not on any of the screens so center it on the first screen
            rect.x = (SCREENS[0].width - rect.width) / 2;
            rect.y = (SCREENS[0].height - rect.height) / 2;
            return rect;
        }
        else {
            // move rect so it is completely on a single screen
            // check X
            int rectRight = rect.x + rect.width;
            int screenRight = containingScreen.x + containingScreen.width;
            if (rectRight > screenRight) {
                rect.x = screenRight - rect.width;
            }
            if (rect.x < containingScreen.x) rect.x = containingScreen.x;
            // check Y
            int rectBottom = rect.y + rect.height;
            int screenBottom = containingScreen.y + containingScreen.height;
            if (rectBottom > screenBottom) {
                rect.y = screenBottom - rect.height;
            }
            if (rect.y < containingScreen.y) rect.y = containingScreen.y;
            // return corrected rect
            return rect;
        }
    }

    /**
     * Gets the screen bounds that contains the rect. The screen bounds consider the screen insets if any.
     *
     * @param rect           the rect of the component.
     * @param considerInsets if consider the insets. The insets is for thing like Windows Task Bar.
     * @return the screen bounds that contains the rect.
     */
    public static Rectangle getContainingScreenBounds(Rectangle rect, boolean considerInsets) {
        waitForInitialization();
        // check if rect is total on screen
//        if (SCREEN_AREA.contains(rect)) return SCREEN_AREA;

        // see if the top left is on any of the screens
        Rectangle containingScreen = null;
        Insets insets = null;
        Point rectPos = rect.getLocation();
        for (int i = 0; i < SCREENS.length; i++) {
            Rectangle screenBounds = SCREENS[i];
            if (screenBounds.contains(rectPos)) {
                containingScreen = screenBounds;
                insets = INSETS[i];
                break;
            }
        }
        // if not see if rect partial on any screen
        for (int i = 0; i < SCREENS.length; i++) {
            Rectangle screenBounds = SCREENS[i];
            if (screenBounds.intersects(rect)) {
                containingScreen = screenBounds;
                insets = INSETS[i];
                break;
            }
        }

        // fall back to the first screen
        if (containingScreen == null) {
            containingScreen = SCREENS[0];
            insets = INSETS[0];
        }

        Rectangle bounds = new Rectangle(containingScreen);
        if (considerInsets && insets != null) {
            bounds.x += insets.left;
            bounds.y += insets.top;
            bounds.width -= insets.left + insets.right;
            bounds.height -= insets.top + insets.bottom;
        }
        return bounds;
    }


    /**
     * Get screen area of all monitors.
     *
     * @return Union of all screens
     */
    public static Area getScreenArea() {
        waitForInitialization();
        return SCREEN_AREA;
    }

    /**
     * Notifies user something is wrong. We use Toolkit beep method by default.
     */
    public static void notifyUser() {
        notifyUser(null);
    }

    /**
     * Notifies user something is wrong. We use Toolkit beep method by default.
     *
     * @param component the component that has the error or null if the error is not associated with any component.
     */
    public static void notifyUser(Component component) {
        String beep = SecurityUtils.getProperty("jide.beepNotifyUser", "true");
        if ("true".equals(beep)) {
            UIManager.getLookAndFeel().provideErrorFeedback(component);
        }
    }

    /**
     * Checks the prerequisite needed by JIDE demos. If the prerequisite doesn't meet, it will prompt a message box and
     * exit.
     */
    public static void prerequisiteChecking() {
        if (!SystemInfo.isJdk14Above()) {
            PortingUtils.notifyUser();
            JOptionPane.showMessageDialog(null, "J2SE 1.4 or above is required for this demo.", "JIDE Software, Inc.", JOptionPane.WARNING_MESSAGE);
            java.lang.System.exit(0);
        }

        if (!SystemInfo.isJdk142Above()) {
            PortingUtils.notifyUser();
            JOptionPane.showMessageDialog(null, "J2SE 1.4.2 or above is recommended for this demo for the best experience of seamless integration with Windows XP.", "JIDE Software, Inc.", JOptionPane.WARNING_MESSAGE);
        }

        if (SystemInfo.isMacOSX()) { // set special properties for Mac OS X
            java.lang.System.setProperty("apple.laf.useScreenMenuBar", "true");
            System.setProperty("apple.awt.brushMetalLook", "true");
        }
    }

    /**
     * Sets the preferred size on a component. This method is there mainly to fix the issue that setPreferredSize method
     * is there on Component only after JDK5. For JDK1.4 and before, you need to cast to JComponent first. So this
     * method captures this logic and only call setPreferedSize when the JDK is 1.5 and above or when the component is
     * instance of JComponent.
     *
     * @param component the component
     * @param size      the preferred size.
     */
    public static void setPreferredSize(Component component, Dimension size) {
        if (SystemInfo.isJdk15Above()) {
            component.setPreferredSize(size);
        }
        else if (component instanceof JComponent) {
            //noinspection RedundantCast
            ((JComponent) component).setPreferredSize(size);
        }
    }

    /**
     * Sets the minimum size on a component. This method is there mainly to fix the issue that setMinimumSize method is
     * there on Component only after JDK5. For JDK1.4 and before, you need to cast to JComponent first. So this method
     * captures this logic and only call setMinimumSize when the JDK is 1.5 and above or when the component is
     *
     * @param component the component
     * @param size      the preferred size.
     */
    public static void setMinimumSize(Component component, Dimension size) {
        if (SystemInfo.isJdk15Above()) {
            component.setMinimumSize(size);
        }
        else if (component instanceof JComponent) {
            //noinspection RedundantCast
            ((JComponent) component).setMinimumSize(size);
        }
    }
}
