/*
 * Copyright (c) 2010, 2013, 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 javafx.scene;

import com.sun.javafx.test.TestHelper;
import javafx.geometry.BoundingBox;
import javafx.scene.transform.Rotate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Circle;
import javafx.scene.transform.Translate;

import org.junit.Test;

public class Parent_recomputeBounds_Test {
    private static final Rectangle r1 = new Rectangle(100, 100, 100, 100);
    private static final Rectangle r2 = new Rectangle(250, 250, 50, 50);
    private static final Rectangle r3 = new Rectangle(50, 50, 10, 10);

    @Test
    public void shouldRecomputeBoundsWhenNodeAdded() {
        final Group g = new Group();
        Bounds b;

        b = g.getBoundsInParent();
        assertTrue(b.isEmpty());

        g.getChildren().add(r1);

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(100, b.getWidth(), 0.0001);
        assertEquals(100, b.getHeight(), 0.0001);

        g.getChildren().add(r2);

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        g.getChildren().add(r3);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldRecomputeBoundsWhenNodeRemoved() {
        final Group g = new Group();
        g.getChildren().add(r1);
        g.getChildren().add(r2);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        g.getChildren().remove(r3);

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        g.getChildren().remove(r2);

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(100, b.getWidth(), 0.0001);
        assertEquals(100, b.getHeight(), 0.0001);

        g.getChildren().remove(r1);

        b = g.getBoundsInParent();
        assertTrue(b.isEmpty());
    }

    @Test
    public void shouldRecomputeBoundsWhenNodeHiddenOrShown() {
        final Group g = new Group();
        g.getChildren().add(r1);
        g.getChildren().add(r2);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r3.setVisible(false);

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        r3.setVisible(true);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotRecomputeBoundsWhenHiddenNodeChanges() {
        final Group g = new Group();
        final Rectangle r = new Rectangle(200,200,1,1);
        g.getChildren().add(r2);
        g.getChildren().add(r);
        g.getChildren().add(r3);
        Bounds b;

        r.setVisible(false);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setX(10);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setWidth(500);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotRecomputeBoundsWhenNodeChangesInsideEdges() {
        final Group g = new Group();
        final Rectangle r = new Rectangle(200,200,1,1);
        g.getChildren().add(r2);
        g.getChildren().add(r);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setX(190);
        r.setY(190);
        r.setWidth(48);
        r.setHeight(48);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotRecomputeBoundsWhenEdgeNodeEnlargesInwards() {
        final Group g = new Group();
        final Rectangle r = new Rectangle(200,200,50,50);
        g.getChildren().add(r);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        r.setX(100);
        r.setY(100);
        r.setWidth(150);
        r.setHeight(150);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldRecomputeBoundsWhenNodeMoved() {
        final Group g = new Group();
        final Rectangle r = new Rectangle(200,200,1,1);

        g.getChildren().add(r2);
        g.getChildren().add(r);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setX(40);

        b = g.getBoundsInParent();
        assertEquals(40, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(260, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setX(200);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldRecomputeBoundsWhenNodeResized() {
        final Group g = new Group();
        final Rectangle r = new Rectangle(200,200,1,1);

        g.getChildren().add(r2);
        g.getChildren().add(r);
        g.getChildren().add(r3);
        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setWidth(200);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(350, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);

        r.setWidth(5);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(250, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotRecomputeBoundsWhenManyNodesChange() {
        final Group g = new Group();
        final Rectangle lt = new Rectangle(100, 100, 100, 100);
        final Rectangle rt = new Rectangle(200, 100, 100, 100);
        final Rectangle lb = new Rectangle(100, 200, 100, 100);
        final Rectangle rb = new Rectangle(200, 200, 100, 100);
        final Rectangle rm1 = new Rectangle(150, 150, 50, 50);
        final Rectangle rm2 = new Rectangle(150, 150, 50, 50);
        Bounds b;

        g.getChildren().add(lt);
        g.getChildren().add(rt);
        g.getChildren().add(lb);
        g.getChildren().add(rb);
        g.getChildren().add(rm1);
        g.getChildren().add(rm2);
        for (int i = 0; i < 20; i++) {
            g.getChildren().add(new Rectangle(150 + i, 150 + i, 50 + i, 50 + i));
        }

        b = g.getBoundsInParent();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        lt.setX(50);
        rt.setX(250);
        lb.setY(250);
        rb.setX(220);
        rm1.setY(50);
        rm2.setX(151);
        rm2.setY(151);

        b = g.getBoundsInParent();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(300, b.getWidth(), 0.0001);
        assertEquals(300, b.getHeight(), 0.0001);
    }

    @Test
    public void transformedBoundsCalculationShouldNotInfluenceUntransformed() {
        final Group g = new Group();
        final Rectangle lt = new Rectangle(100, 100, 100, 100);
        final Rectangle rt = new Rectangle(200, 100, 100, 100);
        final Rectangle lb = new Rectangle(100, 200, 100, 100);
        final Rectangle rb = new Rectangle(200, 200, 100, 100);
        final Rectangle rm1 = new Rectangle(150, 150, 50, 50);
        final Rectangle rm2 = new Rectangle(150, 150, 50, 50);
        Bounds b;

        g.getChildren().addAll(lt, rt, lb, rb, rm1, rm2);
        for (int i = 0; i < 20; i++) {
            g.getChildren().add(new Rectangle(150 + i, 150 + i, 50 + i, 50 + i));
        }

        b = g.getBoundsInLocal();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        g.getTransforms().add(new Rotate(45));

        lt.setX(50);
        rt.setX(250);
        lb.setY(250);
        rb.setX(220);
        rm1.setY(50);
        rm2.setX(151);
        rm2.setY(151);

        // this call gets transformed Parent bounds and shouldn't clear the
        // Parent's dirty children list
        g.getBoundsInParent();

        b = g.getBoundsInLocal();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(300, b.getWidth(), 0.0001);
        assertEquals(300, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotStartBoundsCalculationFromEmptyBounds() {
        final Group g = new Group();
        final Rectangle lt = new Rectangle(50, 50, 50, 50);
        final Rectangle rb = new Rectangle(100, 100, 50, 50);
        Bounds b;

        g.getChildren().addAll(lt, rb);

        b = g.getBoundsInLocal();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(100, b.getWidth(), 0.0001);
        assertEquals(100, b.getHeight(), 0.0001);

        // invalidate (empty) bounds
        rb.setVisible(false);

        // add new rectangle, bounds should not be recalculated by using
        // empty bounds
        g.getChildren().add(new Rectangle(150, 150, 50, 50));

        b = g.getBoundsInLocal();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(50, b.getMinY(), 0.0001);
        assertEquals(150, b.getWidth(), 0.0001);
        assertEquals(150, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotIgnoreMultipleAddedNodes() {
        final Group g = new Group();
        final Rectangle lt = new Rectangle(100, 100, 100, 100);
        final Rectangle mt = new Rectangle(200, 100, 100, 100);
        final Rectangle rt = new Rectangle(300, 100, 100, 100);
        final Rectangle rb = new Rectangle(300, 200, 100, 100);
        Bounds b;

        g.getChildren().addAll(lt, mt);

        b = g.getBoundsInLocal();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(100, b.getHeight(), 0.0001);

        ((Node) rb).boundsChanged = false;
        // rt, rb should be either incorporated into parent bounds directly
        // or marked as dirty for parent bounds calculation
        g.getChildren().addAll(rt, rb);

        b = g.getBoundsInLocal();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(300, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);
    }

    @Test
    public void shouldNotCreateEmptyDirtyChildrenList() {
        final Group g = new Group();
        final Rectangle lt = new Rectangle(100, 100, 100, 100);
        final Rectangle rt = new Rectangle(200, 100, 100, 100);
        final Rectangle lb = new Rectangle(100, 200, 100, 100);
        final Rectangle rb = new Rectangle(200, 200, 100, 100);
        Bounds b;

        g.getChildren().addAll(lt, rt, lb, rb);
        final int toAdd = Parent.DIRTY_CHILDREN_THRESHOLD - 4;
        for (int i = 0; i < toAdd; ++i) {
            g.getChildren().add(
                    new Rectangle(150 + i * 80 / (toAdd - 1), 190, 20, 20));
        }

        b = g.getBoundsInLocal();
        assertEquals(100, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(200, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);

        lt.setX(50);
        // this should create a dirty children list on Parent, even though
        // the added node doesn't change the group gemetry, the created
        // dirty children list should still contain the previously modified
        // corner node (lt)
        g.getChildren().add(new Rectangle(150, 150, 100, 100));

        b = g.getBoundsInLocal();
        assertEquals(50, b.getMinX(), 0.0001);
        assertEquals(100, b.getMinY(), 0.0001);
        assertEquals(250, b.getWidth(), 0.0001);
        assertEquals(200, b.getHeight(), 0.0001);
    }

    @Test
    public void
            shouldNotSkipGeomChangedForChildAdditionInsideUntransformedBounds()
    {
        final Group g = new Group(new Circle(0, -100, 0.001),
                                  new Circle(0, 100, 0.001),
                                  new Circle(-100, 0, 0.001),
                                  new Circle(100, 0, 0.001));
        g.getTransforms().add(new Rotate(-45));

        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinY(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getHeight(), 0.1);

        g.getChildren().add(new Circle(95, -95, 0.001));

        b = g.getBoundsInParent();
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
        assertEquals(-95 * Math.sqrt(2), b.getMinY(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
        assertEquals((50 + 95) * Math.sqrt(2), b.getHeight(), 0.1);
    }

    @Test
    public void
            shouldNotSkipGeomChangedForChildRemovalInsideUntransformedBounds()
    {
        final Circle toRemove = new Circle(95, -95, 0.001);
        final Group g = new Group(toRemove,
                                  new Circle(0, -100, 0.001),
                                  new Circle(0, 100, 0.001),
                                  new Circle(-100, 0, 0.001),
                                  new Circle(100, 0, 0.001));
        g.getTransforms().add(new Rotate(-45));

        Bounds b;

        b = g.getBoundsInParent();
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
        assertEquals(-95 * Math.sqrt(2), b.getMinY(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
        assertEquals((50 + 95) * Math.sqrt(2), b.getHeight(), 0.1);

        g.getChildren().remove(toRemove);

        b = g.getBoundsInParent();
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinY(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
        assertEquals(100 * Math.sqrt(2), b.getHeight(), 0.1);
    }

    @Test
    public void nodeShouldNotifyParentEvenIfItsTransformedBoundsAreDirty() {
        final Rectangle child = new Rectangle(0, 0, 100, 100);
        final Group parent = new Group(child);

        // ensures that child's getTransformedBounds will be called with
        // a non-identity transform argument during parent's bounds calculations
        parent.getTransforms().add(new Rotate(-45));

        // make the cached child's transformed bounds dirty
        child.getTransforms().add(new Translate(50, 0));

        // the cached child's transformed bounds will remain dirty because
        // of a non-trivial parent transformation
        TestHelper.assertSimilar(
                boundsOfRotatedRect(50, 0, 100, 100, -45),
                parent.getBoundsInParent());

        // during the following call the child bounds changed notification
        // should still be generated even though the child's cached transformed
        // bounds are already dirty
        child.getTransforms().add(new Translate(50, 0));

        TestHelper.assertSimilar(
                boundsOfRotatedRect(100, 0, 100, 100, -45),
                parent.getBoundsInParent());
    }

    private static Bounds boundsOfRotatedRect(
            final double x, final double y,
            final double width, final double height,
            final double angle) {
        final Point2D p1 = rotatePoint(x, y, angle);
        final Point2D p2 = rotatePoint(x + width, y, angle);
        final Point2D p3 = rotatePoint(x, y + height, angle);
        final Point2D p4 = rotatePoint(x + width, y + height, angle);

        final double minx = min(p1.getX(), p2.getX(), p3.getX(), p4.getX());
        final double miny = min(p1.getY(), p2.getY(), p3.getY(), p4.getY());
        final double maxx = max(p1.getX(), p2.getX(), p3.getX(), p4.getX());
        final double maxy = max(p1.getY(), p2.getY(), p3.getY(), p4.getY());

        return new BoundingBox(minx, miny, maxx - minx, maxy - miny);
    }

    private static Point2D rotatePoint(final double x, final double y,
                                       final double angle) {
        final double rada = Math.toRadians(angle);
        final double sina = Math.sin(rada);
        final double cosa = Math.cos(rada);

        return new Point2D(x * cosa - y * sina,
                           x * sina + y * cosa);
    }

    private static double min(final double... values) {
        double result = values[0];
        for (int i = 1; i < values.length; ++i) {
            if (result > values[i]) {
                result = values[i];
            }
        }

        return result;
    }

    private static double max(final double... values) {
        double result = values[0];
        for (int i = 1; i < values.length; ++i) {
            if (result < values[i]) {
                result = values[i];
            }
        }

        return result;
    }
}
