/*
 * Copyright (c) 2010, 2015, 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 com.sun.javafx.util;

import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.Stop;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.util.List;
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.stage.StageHelper;

/**
 * Some basic utilities which need to be in java (for shifting operations or
 * other reasons), which are not toolkit dependent.
 *
 */
public class Utils {

    /***************************************************************************
     *                                                                         *
     * Math-related utilities                                                  *
     *                                                                         *
     **************************************************************************/

    /**
     * Simple utility function which clamps the given value to be strictly
     * between the min and max values.
     */
    public static float clamp(float min, float value, float max) {
        if (value < min) return min;
        if (value > max) return max;
        return value;
    }

    /**
     * Simple utility function which clamps the given value to be strictly
     * between the min and max values.
     */
    public static int clamp(int min, int value, int max) {
        if (value < min) return min;
        if (value > max) return max;
        return value;
    }

    /**
     * Simple utility function which clamps the given value to be strictly
     * between the min and max values.
     */
    public static double clamp(double min, double value, double max) {
        if (value < min) return min;
        if (value > max) return max;
        return value;
    }

    /**
     * Simple utility function which clamps the given value to be strictly
     * above the min value.
     */
    public static double clampMin(double value, double min) {
        if (value < min) return min;
        return value;
    }

    /**
     * Simple utility function which clamps the given value to be strictly
     * under the max value.
     */
    public static int clampMax(int value, int max) {
        if (value > max) return max;
        return value;
    }

    /**
     * Utility function which returns either {@code less} or {@code more}
     * depending on which {@code value} is closer to. If {@code value}
     * is perfectly between them, then either may be returned.
     */
    public static double nearest(double less, double value, double more) {
        double lessDiff = value - less;
        double moreDiff = more - value;
        if (lessDiff < moreDiff) return less;
        return more;
    }

    /***************************************************************************
     *                                                                         *
     * String-related utilities                                                *
     *                                                                         *
     **************************************************************************/

    /**
     * Helper to remove leading and trailing quotes from a string.
     * Works with single or double quotes.
     */
    public static String stripQuotes(String str) {
        if (str == null) return str;
        if (str.length() == 0) return str;

        int beginIndex = 0;
        final char openQuote = str.charAt(beginIndex);
        if ( openQuote == '\"' || openQuote=='\'' ) beginIndex += 1;

        int endIndex = str.length();
        final char closeQuote = str.charAt(endIndex - 1);
        if ( closeQuote == '\"' || closeQuote=='\'' ) endIndex -= 1;

        if ((endIndex - beginIndex) < 0) return str;

        // note that String.substring returns "this" if beginIndex == 0 && endIndex == count
        // or a new string that shares the character buffer with the original string.
        return str.substring(beginIndex, endIndex);
    }

    /**
     * Because mobile doesn't have string.split(s) function, this function
     * was written.
     */
    public static String[] split(String str, String separator) {
        if (str == null || str.length() == 0) return new String[] { };
        if (separator == null || separator.length() == 0) return new String[] { };
        if (separator.length() > str.length()) return new String[] { };

        java.util.List<String> result = new java.util.ArrayList<String>();

        int index = str.indexOf(separator);
        while (index >= 0) {
            String newStr = str.substring(0, index);
            if (newStr != null && newStr.length() > 0) {
                result.add(newStr);
            }
            str = str.substring(index + separator.length());
            index = str.indexOf(separator);
        }

        if (str != null && str.length() > 0) {
            result.add(str);
        }

        return result.toArray(new String[] { });
    }

    /**
     * Because mobile doesn't have string.contains(s) function, this function
     * was written.
     */
    public static boolean contains(String src, String s) {
        if (src == null || src.length() == 0) return false;
        if (s == null || s.length() == 0) return false;
        if (s.length() > src.length()) return false;

        return src.indexOf(s) > -1;
    }

    /***************************************************************************
     *                                                                         *
     * Color-related utilities                                                 *
     *                                                                         *
     **************************************************************************/

    /**
     * Calculates a perceptual brightness for a color between 0.0 black and 1.0 while
     */
    public static double calculateBrightness(Color color) {
          return  (0.3*color.getRed()) + (0.59*color.getGreen()) + (0.11*color.getBlue());
    }

    /**
     * Derives a lighter or darker of a given color.
     *
     * @param c           The color to derive from
     * @param brightness  The brightness difference for the new color -1.0 being 100% dark which is always black, 0.0 being
     *                    no change and 1.0 being 100% lighter which is always white
     */
    public static Color deriveColor(Color c, double brightness) {
        double baseBrightness = calculateBrightness(c);
        double calcBrightness = brightness;
        // Fine adjustments to colors in ranges of brightness to adjust the contrast for them
        if (brightness > 0) {
            if (baseBrightness > 0.85) {
                calcBrightness = calcBrightness * 1.6;
            } else if (baseBrightness > 0.6) {
                // no change
            } else if (baseBrightness > 0.5) {
                calcBrightness = calcBrightness * 0.9;
            } else if (baseBrightness > 0.4) {
                calcBrightness = calcBrightness * 0.8;
            } else if (baseBrightness > 0.3) {
                calcBrightness = calcBrightness * 0.7;
            } else {
                calcBrightness = calcBrightness * 0.6;
            }
        } else {
            if (baseBrightness < 0.2) {
                calcBrightness = calcBrightness * 0.6;
            }
        }
        // clamp brightness
        if (calcBrightness < -1) { calcBrightness = -1; } else if (calcBrightness > 1) {calcBrightness = 1;}
        // window two take the calculated brightness multiplyer and derive color based on source color
        double[] hsb = RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue());
        // change brightness
        if (calcBrightness > 0) { // brighter
            hsb[1] *= 1 - calcBrightness;
            hsb[2] += (1 - hsb[2]) * calcBrightness;
        } else { // darker
            hsb[2] *=  calcBrightness + 1;
        }
        // clip saturation and brightness
        if (hsb[1] < 0) { hsb[1] = 0;} else if (hsb[1] > 1) {hsb[1] = 1;}
        if (hsb[2] < 0) { hsb[2] = 0;} else if (hsb[2] > 1) {hsb[2] = 1;}
        // convert back to color
        Color c2 = Color.hsb((int)hsb[0], hsb[1], hsb[2],c.getOpacity());
        return Color.hsb((int)hsb[0], hsb[1], hsb[2],c.getOpacity());

     /*   var hsb:Number[] = RGBtoHSB(c.red,c.green,c.blue);
        // change brightness
        if (brightness > 0) {
            //var bright:Number = brightness * (1-calculateBrightness(c));
            var bright:Number = if (calculateBrightness(c)<0.65 and brightness > 0.5) {
                    if (calculateBrightness(c)<0.2) then brightness * 0.55 else brightness * 0.7
            } else brightness;
            // brighter
            hsb[1] *= 1 - bright;
            hsb[2] += (1 - hsb[2]) * bright;
        } else {
            // darker
            hsb[2] *= brightness+1;
        }
        // clip saturation and brightness
        if (hsb[1] < 0) { hsb[1] = 0;} else if (hsb[1] > 1) {hsb[1] = 1}
        if (hsb[2] < 0) { hsb[2] = 0;} else if (hsb[2] > 1) {hsb[2] = 1}
        // convert back to color
        return Color.hsb(hsb[0],hsb[1],hsb[2]) */
    }

    /**
     * interpolate at a set {@code position} between two colors {@code color1} and {@code color2}.
     * The interpolation is done is linear RGB color space not the default sRGB color space.
     */
    private static Color interpolateLinear(double position, Color color1, Color color2) {
        Color c1Linear = convertSRGBtoLinearRGB(color1);
        Color c2Linear = convertSRGBtoLinearRGB(color2);
        return convertLinearRGBtoSRGB(Color.color(
            c1Linear.getRed()     + (c2Linear.getRed()     - c1Linear.getRed())     * position,
            c1Linear.getGreen()   + (c2Linear.getGreen()   - c1Linear.getGreen())   * position,
            c1Linear.getBlue()    + (c2Linear.getBlue()    - c1Linear.getBlue())    * position,
            c1Linear.getOpacity() + (c2Linear.getOpacity() - c1Linear.getOpacity()) * position
        ));
    }

    /**
     * Get the color at the give {@code position} in the ladder of color stops
     */
    private static Color ladder(final double position, final Stop[] stops) {
        Stop prevStop = null;
        for (int i=0; i<stops.length; i++) {
            Stop stop = stops[i];
            if(position <= stop.getOffset()){
                if (prevStop == null) {
                    return stop.getColor();
                } else {
                    return interpolateLinear((position-prevStop.getOffset())/(stop.getOffset()-prevStop.getOffset()), prevStop.getColor(), stop.getColor());
                }
            }
            prevStop = stop;
        }
        // position is greater than biggest stop, so will we biggest stop's color
        return prevStop.getColor();
    }

    /**
     * Get the color at the give {@code position} in the ladder of color stops
     */
    public static Color ladder(final Color color, final Stop[] stops) {
        return ladder(calculateBrightness(color), stops);
    }

    public static double[] HSBtoRGB(double hue, double saturation, double brightness) {
        // normalize the hue
        double normalizedHue = ((hue % 360) + 360) % 360;
        hue = normalizedHue/360;

        double r = 0, g = 0, b = 0;
        if (saturation == 0) {
            r = g = b = brightness;
        } else {
            double h = (hue - Math.floor(hue)) * 6.0;
            double f = h - java.lang.Math.floor(h);
            double p = brightness * (1.0 - saturation);
            double q = brightness * (1.0 - saturation * f);
            double t = brightness * (1.0 - (saturation * (1.0 - f)));
            switch ((int) h) {
                case 0:
                    r = brightness;
                    g = t;
                    b = p;
                    break;
                case 1:
                    r = q;
                    g = brightness;
                    b = p;
                    break;
                case 2:
                    r = p;
                    g = brightness;
                    b = t;
                    break;
                case 3:
                    r = p;
                    g = q;
                    b = brightness;
                    break;
                case 4:
                    r = t;
                    g = p;
                    b = brightness;
                    break;
                case 5:
                    r = brightness;
                    g = p;
                    b = q;
                    break;
            }
        }
        double[] f = new double[3];
        f[0] = r;
        f[1] = g;
        f[2] = b;
        return f;
    }

    public static double[] RGBtoHSB(double r, double g, double b) {
        double hue, saturation, brightness;
        double[] hsbvals = new double[3];
        double cmax = (r > g) ? r : g;
        if (b > cmax) cmax = b;
        double cmin = (r < g) ? r : g;
        if (b < cmin) cmin = b;

        brightness = cmax;
        if (cmax != 0)
            saturation = (double) (cmax - cmin) / cmax;
        else
            saturation = 0;

        if (saturation == 0) {
            hue = 0;
        } else {
            double redc = (cmax - r) / (cmax - cmin);
            double greenc = (cmax - g) / (cmax - cmin);
            double bluec = (cmax - b) / (cmax - cmin);
            if (r == cmax)
                hue = bluec - greenc;
            else if (g == cmax)
                hue = 2.0 + redc - bluec;
            else
                hue = 4.0 + greenc - redc;
            hue = hue / 6.0;
            if (hue < 0)
                hue = hue + 1.0;
        }
        hsbvals[0] = hue * 360;
        hsbvals[1] = saturation;
        hsbvals[2] = brightness;
        return hsbvals;
    }

    /**
     * Helper function to convert a color in sRGB space to linear RGB space.
     */
    public static Color convertSRGBtoLinearRGB(Color color) {
        double[] colors = new double[] { color.getRed(), color.getGreen(), color.getBlue() };
        for (int i=0; i<colors.length; i++) {
            if (colors[i] <= 0.04045) {
                colors[i] = colors[i] / 12.92;
            } else {
                colors[i] = Math.pow((colors[i] + 0.055) / 1.055, 2.4);
            }
        }
        return Color.color(colors[0], colors[1], colors[2], color.getOpacity());
    }

    /**
     * Helper function to convert a color in linear RGB space to SRGB space.
     */
    public static Color convertLinearRGBtoSRGB(Color color) {
        double[] colors = new double[] { color.getRed(), color.getGreen(), color.getBlue() };
        for (int i=0; i<colors.length; i++) {
            if (colors[i] <= 0.0031308) {
                colors[i] = colors[i] * 12.92;
            } else {
                colors[i] = (1.055 * Math.pow(colors[i], (1.0 / 2.4))) - 0.055;
            }
        }
        return Color.color(colors[0], colors[1], colors[2], color.getOpacity());
    }

    /** helper function for calculating the sum of a series of numbers */
    public static double sum(double[] values) {
        double sum = 0;
        for (double v : values) sum = sum+v;
        return sum / values.length;
    }

    public static Point2D pointRelativeTo(Node parent, Node node, HPos hpos,
            VPos vpos, double dx, double dy, boolean reposition)
    {
        final double nodeWidth = node.getLayoutBounds().getWidth();
        final double nodeHeight = node.getLayoutBounds().getHeight();
        return pointRelativeTo(parent, nodeWidth, nodeHeight, hpos, vpos, dx, dy, reposition);
    }

    public static Point2D pointRelativeTo(Node parent, double anchorWidth,
            double anchorHeight, HPos hpos, VPos vpos, double dx, double dy,
            boolean reposition)
    {
        final Bounds parentBounds = getBounds(parent);
        Scene scene = parent.getScene();
        NodeOrientation orientation = parent.getEffectiveNodeOrientation();

        if (orientation == NodeOrientation.RIGHT_TO_LEFT) {
            if (hpos == HPos.LEFT) {
                hpos = HPos.RIGHT;
            } else if (hpos == HPos.RIGHT) {
                hpos = HPos.LEFT;
            }
            dx *= -1;
        }

        double layoutX = positionX(parentBounds, anchorWidth, hpos) + dx;
        final double layoutY = positionY(parentBounds, anchorHeight, vpos) + dy;

        if (orientation == NodeOrientation.RIGHT_TO_LEFT && hpos == HPos.CENTER) {
            //TODO - testing for an instance of Stage seems wrong but works for menus
            if (scene.getWindow() instanceof Stage) {
                layoutX = layoutX + parentBounds.getWidth() - anchorWidth;
            } else {
                layoutX = layoutX - parentBounds.getWidth() - anchorWidth;
            }
        }

        if (reposition) {
            return pointRelativeTo(parent, anchorWidth, anchorHeight, layoutX, layoutY, hpos, vpos);
        } else {
            return new Point2D(layoutX, layoutY);
        }
    }

    /**
     * This is the fallthrough function that most other functions fall into. It takes
     * care specifically of the repositioning of the item such that it remains onscreen
     * as best it can, given it's unique qualities.
     *
     * As will all other functions, this one returns a Point2D that represents an x,y
     * location that should safely position the item onscreen as best as possible.
     *
     * Note that <code>width</code> and <height> refer to the width and height of the
     * node/popup that is needing to be repositioned, not of the parent.
     *
     * Don't use the BASELINE vpos, it doesn't make sense and would produce wrong result.
     */
    public static Point2D pointRelativeTo(Object parent, double width,
            double height, double screenX, double screenY, HPos hpos, VPos vpos)
    {
        double finalScreenX = screenX;
        double finalScreenY = screenY;
        final Bounds parentBounds = getBounds(parent);

        // ...and then we get the bounds of this screen
        final Screen currentScreen = getScreen(parent);
        final Rectangle2D screenBounds =
                hasFullScreenStage(currentScreen)
                        ? currentScreen.getBounds()
                        : currentScreen.getVisualBounds();

        // test if this layout will force the node to appear outside
        // of the screens bounds. If so, we must reposition the item to a better position.
        // We firstly try to do this intelligently, so as to not overlap the parent if
        // at all possible.
        if (hpos != null) {
            // Firstly we consider going off the right hand side
            if ((finalScreenX + width) > screenBounds.getMaxX()) {
                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
            }

            // don't let the node go off to the left of the current screen
            if (finalScreenX < screenBounds.getMinX()) {
                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
            }
        }

        if (vpos != null) {
            // don't let the node go off the bottom of the current screen
            if ((finalScreenY + height) > screenBounds.getMaxY()) {
                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos,vpos));
            }

            // don't let the node out of the top of the current screen
            if (finalScreenY < screenBounds.getMinY()) {
                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos,vpos));
            }
        }

        // --- after all the moving around, we do one last check / rearrange.
        // Unlike the check above, this time we are just fully committed to keeping
        // the item on screen at all costs, regardless of whether or not that results
        /// in overlapping the parent object.
        if ((finalScreenX + width) > screenBounds.getMaxX()) {
            finalScreenX -= (finalScreenX + width - screenBounds.getMaxX());
        }
        if (finalScreenX < screenBounds.getMinX()) {
            finalScreenX = screenBounds.getMinX();
        }
        if ((finalScreenY + height) > screenBounds.getMaxY()) {
            finalScreenY -= (finalScreenY + height - screenBounds.getMaxY());
        }
        if (finalScreenY < screenBounds.getMinY()) {
            finalScreenY = screenBounds.getMinY();
        }

        return new Point2D(finalScreenX, finalScreenY);
    }

    /**
     * Utility function that returns the x-axis position that an object should be positioned at,
     * given the parents screen bounds, the width of the object, and
     * the required HPos.
     */
    private static double positionX(Bounds parentBounds, double width, HPos hpos) {
        if (hpos == HPos.CENTER) {
            // this isn't right, but it is needed for root menus to show properly
            return parentBounds.getMinX();
        } else if (hpos == HPos.RIGHT) {
            return parentBounds.getMaxX();
        } else if (hpos == HPos.LEFT) {
            return parentBounds.getMinX() - width;
        } else {
            return 0;
        }
    }

    /**
     * Utility function that returns the y-axis position that an object should be positioned at,
     * given the parents screen bounds, the height of the object, and
     * the required VPos.
     *
     * The BASELINE vpos doesn't make sense here, 0 is returned for it.
     */
    private static double positionY(Bounds parentBounds, double height, VPos vpos) {
        if (vpos == VPos.BOTTOM) {
            return parentBounds.getMaxY();
        } else if (vpos == VPos.CENTER) {
            return parentBounds.getMinY();
        } else if (vpos == VPos.TOP) {
            return parentBounds.getMinY() - height;
        } else {
            return 0;
        }
    }

    /**
     * To facilitate multiple types of parent object, we unfortunately must allow for
     * Objects to be passed in. This method handles determining the bounds of the
     * given Object. If the Object type is not supported, a default Bounds will be returned.
     */
    private static Bounds getBounds(Object obj) {
        if (obj instanceof Node) {
            final Node n = (Node)obj;
            Bounds b = n.localToScreen(n.getLayoutBounds());
            return b != null ? b : new BoundingBox(0, 0, 0, 0);
        } else if (obj instanceof Window) {
            final Window window = (Window)obj;
            return new BoundingBox(window.getX(), window.getY(), window.getWidth(), window.getHeight());
        } else {
            return new BoundingBox(0, 0, 0, 0);
        }
    }

    /*
     * Simple utitilty function to return the 'opposite' value of a given HPos, taking
     * into account the current VPos value. This is used to try and avoid overlapping.
     */
    private static HPos getHPosOpposite(HPos hpos, VPos vpos) {
        if (vpos == VPos.CENTER) {
            if (hpos == HPos.LEFT){
                return HPos.RIGHT;
            } else if (hpos == HPos.RIGHT){
                return HPos.LEFT;
            } else if (hpos == HPos.CENTER){
                return HPos.CENTER;
            } else {
                // by default center for now
                return HPos.CENTER;
            }
        } else {
            return HPos.CENTER;
        }
    }

    /*
     * Simple utitilty function to return the 'opposite' value of a given VPos, taking
     * into account the current HPos value. This is used to try and avoid overlapping.
     */
    private static VPos getVPosOpposite(HPos hpos, VPos vpos) {
        if (hpos == HPos.CENTER) {
            if (vpos == VPos.BASELINE){
                return VPos.BASELINE;
            } else if (vpos == VPos.BOTTOM){
                return VPos.TOP;
            } else if (vpos == VPos.CENTER){
                return VPos.CENTER;
            } else if (vpos == VPos.TOP){
                return VPos.BOTTOM;
            } else {
                // by default center for now
                return VPos.CENTER;
            }
        } else {
            return VPos.CENTER;
        }
    }

    public static boolean hasFullScreenStage(final Screen screen) {
        final List<Stage> allStages = StageHelper.getStages();

        for (final Stage stage: allStages) {
            if (stage.isFullScreen() && (getScreen(stage) == screen)) {
                return true;
            }
        }

        return false;
    }

    /*
     * Returns true if the primary Screen has QVGA dimensions, in landscape or portrait mode.
     */
    public static boolean isQVGAScreen() {
        Rectangle2D bounds = Screen.getPrimary().getBounds();
        return ((bounds.getWidth() == 320 && bounds.getHeight() == 240) ||
                (bounds.getWidth() == 240 && bounds.getHeight() == 320));
    }

    /**
     * This function attempts to determine the best screen given the parent object
     * from which we are wanting to position another item relative to. This is particularly
     * important when we want to keep items from going off screen, and for handling
     * multiple monitor support.
     */
    public static Screen getScreen(Object obj) {
        final Bounds parentBounds = getBounds(obj);

        final Rectangle2D rect = new Rectangle2D(
                parentBounds.getMinX(),
                parentBounds.getMinY(),
                parentBounds.getWidth(),
                parentBounds.getHeight());

        return getScreenForRectangle(rect);
    }

    public static Screen getScreenForRectangle(final Rectangle2D rect) {
        final List<Screen> screens = Screen.getScreens();

        final double rectX0 = rect.getMinX();
        final double rectX1 = rect.getMaxX();
        final double rectY0 = rect.getMinY();
        final double rectY1 = rect.getMaxY();

        Screen selectedScreen;

        selectedScreen = null;
        double maxIntersection = 0;
        for (final Screen screen: screens) {
            final Rectangle2D screenBounds = screen.getBounds();
            final double intersection =
                    getIntersectionLength(rectX0, rectX1,
                                          screenBounds.getMinX(),
                                          screenBounds.getMaxX())
                        * getIntersectionLength(rectY0, rectY1,
                                                screenBounds.getMinY(),
                                                screenBounds.getMaxY());

            if (maxIntersection < intersection) {
                maxIntersection = intersection;
                selectedScreen = screen;
            }
        }

        if (selectedScreen != null) {
            return selectedScreen;
        }

        selectedScreen = Screen.getPrimary();
        double minDistance = Double.MAX_VALUE;
        for (final Screen screen: screens) {
            final Rectangle2D screenBounds = screen.getBounds();
            final double dx = getOuterDistance(rectX0, rectX1,
                                               screenBounds.getMinX(),
                                               screenBounds.getMaxX());
            final double dy = getOuterDistance(rectY0, rectY1,
                                               screenBounds.getMinY(),
                                               screenBounds.getMaxY());
            final double distance = dx * dx + dy * dy;

            if (minDistance > distance) {
                minDistance = distance;
                selectedScreen = screen;
            }
        }

        return selectedScreen;
    }

    public static Screen getScreenForPoint(final double x, final double y) {
        final List<Screen> screens = Screen.getScreens();

        // first check whether the point is inside some screen
        for (final Screen screen: screens) {
            // can't use screen.bounds.contains, because it returns true for
            // the min + width point
            final Rectangle2D screenBounds = screen.getBounds();
            if ((x >= screenBounds.getMinX())
                    && (x < screenBounds.getMaxX())
                    && (y >= screenBounds.getMinY())
                    && (y < screenBounds.getMaxY())) {
                return screen;
            }
        }

        // the point is not inside any screen, find the closest screen now
        Screen selectedScreen = Screen.getPrimary();
        double minDistance = Double.MAX_VALUE;
        for (final Screen screen: screens) {
            final Rectangle2D screenBounds = screen.getBounds();
            final double dx = getOuterDistance(screenBounds.getMinX(),
                                               screenBounds.getMaxX(),
                                               x);
            final double dy = getOuterDistance(screenBounds.getMinY(),
                                               screenBounds.getMaxY(),
                                               y);
            final double distance = dx * dx + dy * dy;
            if (minDistance >= distance) {
                minDistance = distance;
                selectedScreen = screen;
            }
        }

        return selectedScreen;
    }

    private static double getIntersectionLength(
            final double a0, final double a1,
            final double b0, final double b1) {
        // (a0 <= a1) && (b0 <= b1)
        return (a0 <= b0) ? getIntersectionLengthImpl(b0, b1, a1)
                          : getIntersectionLengthImpl(a0, a1, b1);
    }

    private static double getIntersectionLengthImpl(
            final double v0, final double v1, final double v) {
        // (v0 <= v1)
        if (v <= v0) {
            return 0;
        }

        return (v <= v1) ? v - v0 : v1 - v0;
    }

    private static double getOuterDistance(
            final double a0, final double a1,
            final double b0, final double b1) {
        // (a0 <= a1) && (b0 <= b1)
        if (a1 <= b0) {
            return b0 - a1;
        }

        if (b1 <= a0) {
            return b1 - a0;
        }

        return 0;
    }

    private static double getOuterDistance(final double v0,
                                           final double v1,
                                           final double v) {
        // (v0 <= v1)
        if (v <= v0) {
            return v0 - v;
        }

        if (v >= v1) {
            return v - v1;
        }

        return 0;
    }

    /***************************************************************************
     *                                                                         *
     * Miscellaneous utilities                                                 *
     *                                                                         *
     **************************************************************************/

    public static boolean assertionEnabled() {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;  // Intentional side-effect !!!

        return assertsEnabled;
    }

    /**
     * Returns true if the operating system is a form of Windows.
     */
    public static boolean isWindows(){
        return PlatformUtil.isWindows();
    }

    /**
     * Returns true if the operating system is a form of Mac OS.
     */
    public static boolean isMac(){
        return PlatformUtil.isMac();
    }

    /**
     * Returns true if the operating system is a form of Unix, including Linux.
     */
    public static boolean isUnix(){
        return PlatformUtil.isUnix();
    }

    /***************************************************************************
     *                                                                         *
     * Unicode-related utilities                                               *
     *                                                                         *
     **************************************************************************/

    public static String convertUnicode(String src) {
        /** The input buffer, index of next character to be read,
         *  index of one past last character in buffer.
         */
        char[] buf;
        int bp;
        int buflen;

        /** The current character.
         */
        char ch;

        /** The buffer index of the last converted unicode character
         */
        int unicodeConversionBp = -1;

        buf = src.toCharArray();
        buflen = buf.length;
        bp = -1;

        char[] dst = new char[buflen];
        int dstIndex = 0;

        while (bp < buflen - 1) {
            ch = buf[++bp];
            if (ch == '\\') {
                if (unicodeConversionBp != bp) {
                    bp++; ch = buf[bp];
                    if (ch == 'u') {
                        do {
                            bp++; ch = buf[bp];
                        } while (ch == 'u');
                        int limit = bp + 3;
                        if (limit < buflen) {
                            char c = ch;
                            int result = Character.digit(c, 16);
                            if (result >= 0 && c > 0x7f) {
                                //lexError(pos+1, "illegal.nonascii.digit");
                                ch = "0123456789abcdef".charAt(result);
                            }
                            int d = result;
                            int code = d;
                            while (bp < limit && d >= 0) {
                                bp++; ch = buf[bp];
                                char c1 = ch;
                                int result1 = Character.digit(c1, 16);
                                if (result1 >= 0 && c1 > 0x7f) {
                                    //lexError(pos+1, "illegal.nonascii.digit");
                                    ch = "0123456789abcdef".charAt(result1);
                                }
                                d = result1;
                                code = (code << 4) + d;
                            }
                            if (d >= 0) {
                                ch = (char)code;
                                unicodeConversionBp = bp;
                            }
                        }
                        //lexError(bp, "illegal.unicode.esc");
                    } else {
                        bp--;
                        ch = '\\';
                    }
                }
            }
            dst[dstIndex++] = ch;
        }

        return new String(dst, 0, dstIndex);
    }
}
