/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.util;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Window;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import java.util.Collection;
import java.util.Iterator;
import javax.swing.SwingUtilities;

/** Utility methods for Java2D geometry.
 * @since 2.0
 */
public final class JaxoGeometry {
    /**
     * Empty private constructor to prevent the class from being
     * explicitly instantiated.
     */
    private JaxoGeometry() {
        // empty on purpose
    }

    /**
     * Add 's' to 'r', but respect isEmpty: is 's' is empty, do
     * nothing; if 'r' is empty, 'r' is set to 's'.
     * The rounding behaviour if 'r' and 's' have different precision
     * is undefined.
     * @see Rectangle#add(Rectangle)
     * @see Rectangle#isEmpty
     * @param r The first Rectangle.
     * @param s The rectangle to add.
     */
    public static void add(final Rectangle2D r, final Rectangle2D s) {
        if (!s.isEmpty()) {
            if (r.isEmpty()) {
                r.setRect(s);
            } else {
                r.add(s);
            }
        }
    }

    /**
     * See {@link #add(Rectangle2D, Rectangle2D)}.
     * @param r The first rectangle.
     * @param x The x coordinate of the rectangle to add.
     * @param y The y coordinate of the rectangle to add.
     * @param width The width of the rectangle to add.
     * @param height The height of the rectangle to add.
     */
    public static void add(final Rectangle r, final int x, final int y, final int width, final int height) {
        if ((width > 0) && (height > 0)) {
            add(r, new Rectangle(x, y, width, height));
        }
    }

    /**
     * Clear: Ensure 'r.isEmpty()'.
     * @see Rectangle#isEmpty
     * @param r The rectangle to clear.
     */
    public static void clear(final Rectangle2D r) {
        r.setRect(0, 0, 0, 0);
    }

    /**
     * Increase the Rectangle 'r' by the amount given in each direction
     * in 'n'.
     * @param r The rectangle to grow.
     * @param n The insets to add.
     */
    public static void grow(final Rectangle r, final Insets n) {
        r.x -= n.left;
        r.y -= n.top;
        r.width += (n.left + n.right);
        r.height += (n.top + n.bottom);
    }

    /**
     * Decrease the Rectangle 'r' by the amount given in each direction
     * in 'n'. Currently does not handle underflow - so 'r' may have
     * negative width/height afterwards.
     * @param r The rectangle to shrink.
     * @param n The insets to subtract.
     */
    public static void shrink(final Rectangle r, final Insets n) {
        r.x += n.left;
        r.y += n.top;
        r.width -= (n.left + n.right);
        r.height -= (n.top + n.bottom);
    }

    /**
     * Increase the Dimension 'd' by the amount given in each direction
     * in 'n'.
     * @param d The dimension to grow.
     * @param n The insets to add.
     */
    public static void grow(final Dimension d, final Insets n) {
        d.width += (n.left + n.right);
        d.height += (n.top + n.bottom);
    }

    /**
     * Decrease the Dimension 'd' by the amount given in each direction
     * in 'n'. Currently does not handle underflow - so 'd' may have
     * negative width/height afterwards.
     * @param d The dimension to shrink.
     * @param n The insets to subtract.
     */
    public static void shrink(final Dimension d, final Insets n) {
        d.width -= (n.left + n.right);
        d.height -= (n.top + n.bottom);
    }

    /**
     * Bounding box of a Collection of Shapes.
     * Returns the union of all the object's bounding boxess.
     *
     * @param objects A collection of objects,
     * only Shapes contribute to the result.
     * @return A Rectangle containing the union bounding box.
     * Returns null for an empty Collection, or if there are no Shapes.
     */
    public static Rectangle getBounds(final Collection<?> objects) {
        final Iterator<?> i = objects.iterator();
        Rectangle bBox = null;
        Object o = null;

        while (i.hasNext()) {
            o = i.next();

            if (o instanceof Shape) {
                if (bBox == null) {
                    bBox = ((Shape) o).getBounds();
                } else {
                    bBox.add(((Shape) o).getBounds());
                }
            }
        }

        return bBox;
    }

    /** Returns a point that is obtained from a point (px, py) after a scale
     * transformation scale, keeping the point (orx, ory) fixed.
     * @param orx The x-coordinate of the fixed point.
     * @param ory The y-coordinate of the fixed point.
     * @param scale The scale factor.
     * @param px The x-coordinate of the point to be transformed.
     * @param py The y-coordinate of the point to be transformed.
     * @return A Point2D containing the x- and y-coordinate of the new point.
     */
    public static Point2D scaledPoint(final double orx, final double ory, final double scale,
        final double px, final double py) {
        final Point2D newP =
            new Point2D.Double((scale * (px - orx)) + orx,
                (scale * (py - ory)) + ory);

        return newP;
    }

    /**
     * Returns the approximate length of a Bezier curve.
     *
     * @param p the four control points of the Bezier curve.
     * @param error An absolute value for an error estimate
     * @return The length of the Bezier curve.
     */
    public static double bezierLength(final Point2D[] p, final double error) {
        // this is from http://steve.hollasch.net/cgindex/curves/cbezarclen.html

        final double l1 = p[0].distance(p[1]) + p[1].distance(p[2])
            + p[2].distance(p[3]);
        final double l0 = p[0].distance(p[3]);

        final double err = (l1 - l0);
        double length = 0.5d * (l0 + l1);

        if (err > error) {
            final Point2D[] left = new Point2D.Double[4];
            final Point2D[] right = new Point2D.Double[4];
            bezierSplit(p, left, right);
            length = bezierLength(left, error) + bezierLength(right, error);
        }

        return length;
    }

    /**
     * Splits a Bezier into half and calculates the
     * control points of the two parts.
     *
     * @param controlPoints the four control points of the Bezier.
     * @param left (output) the four control points of the left half.
     * @param right (output) the four control points of the right half.
     */
    public static void bezierSplit(final Point2D[] controlPoints, final Point2D[] left,
        final Point2D[] right) {
        for (int i = 0; i <= 3; i++) {
            // initialize the output arrays
            left[i] = new Point2D.Double();
            right[i] = new Point2D.Double();

            // copy the control points
            left[i].setLocation(controlPoints[i]);
        }

        for (int i = 1; i <= 3; i++) {
            right[4 - i].setLocation(left[3]);
            for (int j = 3; j >= i; j--) {
                left[j].setLocation(
                    (left[j - 1].getX() + left[j].getX()) / 2,
                    (left[j - 1].getY() + left[j].getY()) / 2);
            }
        }
        right[0].setLocation(left[3]);
    }

    /**
     * Returns a point of a Bezier curve at parameter u.
     *
     * @param p the four control points of the Bezier curve.
     * @param u the parameter value, needs to be between 0 and 1.
     * @return A Point2D.
     */
    public static Point2D bezierPoint(final Point2D[] p, final double u) {
        final double t = curveParameter(u);
        final double s = 1.d - t;

        final double x =
            (s * s * s * p[0].getX()) + (3.d * t * s * s * p[1].getX())
            + (3.d * t * t * s * p[2].getX()) + (t * t * t * p[3].getX());
        final double y =
            (s * s * s * p[0].getY()) + (3.d * t * s * s * p[1].getY())
            + (3.d * t * t * s * p[2].getY()) + (t * t * t * p[3].getY());
        return new Point2D.Double(x, y);
    }

    /**
     * Returns the first derivative of a Bezier curve at parameter u.
     *
     * @param p the four control points of the Bezier curve.
     * @param u the parameter value, needs to be between 0 and 1.
     * @return the slope (in radians) at t.
     */
    public static double bezierSlope(final Point2D[] p, final double u) {
        final double t = curveParameter(u);
        final double s = 1.d - t;

        final double x =
            (-3.d * s * s * p[0].getX())
            + (3.d * ((s * s) - (2.d * t * s)) * p[1].getX())
            + (3.d * ((2 * t * s) - (t * t)) * p[2].getX())
            + (3.d * t * t * p[3].getX());
        final double y =
            (-3.d * s * s * p[0].getY())
            + (3.d * ((s * s) - (2.d * t * s)) * p[1].getY())
            + (3.d * ((2 * t * s) - (t * t)) * p[2].getY())
            + (3.d * t * t * p[3].getY());
        return Math.atan2(y, x);
    }

    /**
     * Checks that a curve parameter value is in range, within errors.
     *
     * @param u the parameter value, needs to be between 0 and 1.
     * @return a parameter that is strictly between 0 and 1.
     */
    public static double curveParameter(final double u) {
        if ((u < -0.00001d) || (u > 1.00001d)) {
            throw new IllegalArgumentException(
                "Curve-Parameter out of range: " + u);
        }

        double t = u;

        if (t < 0.d) {
            t = 0.d;
        }

        if (t > 1.d) {
            t = 1.d;
        }

        return t;
    }

    /**
     * Translate a point.
     *
     * @param p the point to move.
     * @param dx the translation in x direction.
     * @param dy the translation in y direction.
     */
    public static void translate(final Point2D p, final double dx, final double dy) {
        p.setLocation(p.getX() + dx, p.getY() + dy);
    }

    /**
     * Put Window into a nice place - in principle as setLocationRelativeTo(c),
     * but try to avoid covering the given Rectangle.
     *
     * @param w The window to set.
     * @param comp A compponent.
     * @param avoidme The region to avoid.
     */
    public static void setLocationRelativeToAvoiding(final Window w, final Component comp,
        final Rectangle avoidme) {
        Component c = comp;
        Rectangle avoid = avoidme;
        final int width = w.getWidth();
        final int height = w.getHeight();

        final Rectangle screen = w.getGraphicsConfiguration().getBounds();

        JaxoGeometry.shrink(screen,
            w.getToolkit().getScreenInsets(w.getGraphicsConfiguration()));

        final Point location = new Point();

        if (c != null) {
            if (c.isShowing()) {
                final Component root = SwingUtilities.getRoot(c);

                if (root == null) {
                    c = null;
                }
            } else {
                c = null;
            }
        }

        // First center on 'c' or 'screen'
        // This is roughly equivalent to what setLocationOnScreen does
        // (possibly better)
        if (c == null) {
            location.x = screen.x + ((screen.width - width) / 2);
            location.y = screen.y + ((screen.height - height) / 2);
        } else {
            final Point p = c.getLocationOnScreen();

            location.x = p.x + ((c.getWidth() - width) / 2);
            location.y = p.y + ((c.getHeight() - height) / 2);

            location.x =
                Math.min(location.x + width, screen.x + screen.width) - width;
            location.y =
                Math.min(location.y + width, screen.y + screen.width) - width;

            // Prefer to have the upper left corner contained
            location.x = Math.max(location.x, screen.x);
            location.y = Math.max(location.y, screen.y);
        }

        // Now try to avoid 'avoid'
        if (avoid != null) {
            avoid = avoid.intersection(screen);

            // Just change 'location.y' - really not very intelligently
            if (((location.y + height) >= avoid.y) // not above
                    && (location.y <= (avoid.y + avoid.height))) { // not below
                final int spaceAbove = avoid.y - screen.y;
                final int spaceBelow =
                    (screen.y + screen.height) - avoid.y - avoid.height;

                if (spaceAbove > spaceBelow) {
                    location.y = screen.y + Math.max(0, spaceAbove - height);
                } else {
                    // never put if above screen.y
                    location.y =
                        screen.y
                        + Math.max(0,
                            screen.height - Math.max(spaceBelow, height));
                }
            }
        }

        w.setLocation(location);
    }
}
