/*
 * Copyright (c) 2011, 2021, 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.layout;

import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;

import java.util.List;

/**
 * AnchorPane allows the edges of child nodes to be anchored to an offset from
 * the anchor pane's edges.  If the anchor pane has a border and/or padding set, the
 * offsets will be measured from the inside edge of those insets.
 * <p>
 * AnchorPane lays out each managed child regardless of the child's visible property value;
 * unmanaged children are ignored for all layout calculations.</p>
 * <p>
 * AnchorPanes may be styled with backgrounds and borders using CSS.  See
 * {@link javafx.scene.layout.Region Region} superclass for details.</p>
 *
 * <h2>Anchor Constraints</h2>
 * <p>
 * The application sets anchor constraints on each child to configure the anchors
 * on one or more sides.  If a child is anchored on opposite sides (and is resizable), the
 * anchor pane will resize it to maintain both offsets, otherwise the anchor pane
 * will resize it to its preferred size.  If in the former case (anchored on opposite
 * sides) and the child is not resizable, then only the top/left anchor will be honored.
 * AnchorPane provides a static method for setting each anchor constraint.
 * </p>
 *
 * <table border="1">
 * <caption>AnchorPane Constraint Table</caption>
 * <tr><th scope="col">Constraint</th><th scope="col">Type</th><th scope="col">Description</th></tr>
 * <tr><th scope="row">topAnchor</th><td>double</td><td>distance from the anchor pane's top insets to the child's top edge.</td></tr>
 * <tr><th scope="row">leftAnchor</th><td>double</td><td>distance from the anchor pane's left insets to the child's left edge.</td></tr>
 * <tr><th scope="row">bottomAnchor</th><td>double</td><td>distance from the anchor pane's bottom insets to the child's bottom edge.</td></tr>
 * <tr><th scope="row">rightAnchor</th><td>double</td><td>distance from the anchor pane's right insets to the child's right edge.</td></tr>
 * </table>
 * <p>
 * AnchorPane Example:
 * <pre><code>     AnchorPane anchorPane = new AnchorPane();
 *     // List should stretch as anchorPane is resized
 *     ListView list = new ListView();
 *    <b> AnchorPane.setTopAnchor(list, 10.0);
 *     AnchorPane.setLeftAnchor(list, 10.0);
 *     AnchorPane.setRightAnchor(list, 65.0);</b>
 *     // Button will float on right edge
 *     Button button = new Button("Add");
 *     <b>AnchorPane.setTopAnchor(button, 10.0);
 *     AnchorPane.setRightAnchor(button, 10.0);</b>
 *     anchorPane.getChildren().addAll(list, button);
 * </code></pre>
 *
 * <h2>Resizable Range</h2>
 * <p>
 * An anchor pane's parent will resize the anchor pane within the anchor pane's resizable range
 * during layout.   By default the anchor pane computes this range based on its content
 * as outlined in the table below.
 * </p>
 *
 * <table border="1">
 * <caption>AnchorPane Resize Table</caption>
 * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr>
 * <tr><th scope="row">minimum</th>
 * <td>left/right insets plus width required to display children anchored at left/right with at least their min widths</td>
 * <td>top/bottom insets plus height required to display children anchored at top/bottom with at least their min heights</td></tr>
 * <tr><th scope="row">preferred</th>
 * <td>left/right insets plus width required to display children anchored at left/right with at least their pref widths</td>
 * <td>top/bottom insets plus height required to display children anchored at top/bottom with at least their pref heights</td></tr>
 * <tr><th scope="row">maximum</th>
 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
 * </table>
 * <p>
 * An anchor pane's unbounded maximum width and height are an indication to the parent that
 * it may be resized beyond its preferred size to fill whatever space is assigned
 * to it.
 * <p>
 * AnchorPane provides properties for setting the size range directly.  These
 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the
 * application may set them to other values as needed:
 * <pre><code>     <b>anchorPane.setPrefSize(300, 300);</b>
 * </code></pre>
 * Applications may restore the computed values by setting these properties back
 * to Region.USE_COMPUTED_SIZE.
 * <p>
 * AnchorPane does not clip its content by default, so it is possible that children's
 * bounds may extend outside its own bounds if the anchor pane is resized smaller
 * than its preferred size.</p>
 *
 * @since JavaFX 2.0
 */
public class AnchorPane extends Pane {

    private static final String TOP_ANCHOR = "pane-top-anchor";
    private static final String LEFT_ANCHOR = "pane-left-anchor";
    private static final String BOTTOM_ANCHOR = "pane-bottom-anchor";
    private static final String RIGHT_ANCHOR = "pane-right-anchor";

    /* ******************************************************************
     *  BEGIN static methods
     ********************************************************************/

    /**
     * Sets the top anchor for the child when contained by an anchor pane.
     * If set, the anchor pane will maintain the child's size and position so
     * that it's top is always offset by that amount from the anchor pane's top
     * content edge.
     * Setting the value to null will remove the constraint.
     * @param child the child node of an anchor pane
     * @param value the offset from the top of the anchor pane
     */
    public static void setTopAnchor(Node child, Double value) {
        setConstraint(child, TOP_ANCHOR, value);
    }

    /**
     * Returns the child's top anchor constraint if set.
     * @param child the child node of an anchor pane
     * @return the offset from the top of the anchor pane or null if no top anchor was set
     */
    public static Double getTopAnchor(Node child) {
        return (Double)getConstraint(child, TOP_ANCHOR);
    }

    /**
     * Sets the left anchor for the child when contained by an anchor pane.
     * If set, the anchor pane will maintain the child's size and position so
     * that it's left is always offset by that amount from the anchor pane's left
     * content edge.
     * Setting the value to null will remove the constraint.
     * @param child the child node of an anchor pane
     * @param value the offset from the left of the anchor pane
     */
    public static void setLeftAnchor(Node child, Double value) {
        setConstraint(child, LEFT_ANCHOR, value);
    }

    /**
     * Returns the child's left anchor constraint if set.
     * @param child the child node of an anchor pane
     * @return the offset from the left of the anchor pane or null if no left anchor was set
     */
    public static Double getLeftAnchor(Node child) {
        return (Double)getConstraint(child, LEFT_ANCHOR);
    }

    /**
     * Sets the bottom anchor for the child when contained by an anchor pane.
     * If set, the anchor pane will maintain the child's size and position so
     * that it's bottom is always offset by that amount from the anchor pane's bottom
     * content edge.
     * Setting the value to null will remove the constraint.
     * @param child the child node of an anchor pane
     * @param value the offset from the bottom of the anchor pane
     */
    public static void setBottomAnchor(Node child, Double value) {
        setConstraint(child, BOTTOM_ANCHOR, value);
    }

    /**
     * Returns the child's bottom anchor constraint if set.
     * @param child the child node of an anchor pane
     * @return the offset from the bottom of the anchor pane or null if no bottom anchor was set
     */
    public static Double getBottomAnchor(Node child) {
        return (Double)getConstraint(child, BOTTOM_ANCHOR);
    }

    /**
     * Sets the right anchor for the child when contained by an anchor pane.
     * If set, the anchor pane will maintain the child's size and position so
     * that it's right is always offset by that amount from the anchor pane's right
     * content edge.
     * Setting the value to null will remove the constraint.
     * @param child the child node of an anchor pane
     * @param value the offset from the right of the anchor pane
     */
    public static void setRightAnchor(Node child, Double value) {
        setConstraint(child, RIGHT_ANCHOR, value);
    }

    /**
     * Returns the child's right anchor constraint if set.
     * @param child the child node of an anchor pane
     * @return the offset from the right of the anchor pane or null if no right anchor was set
     */
    public static Double getRightAnchor(Node child) {
        return (Double)getConstraint(child, RIGHT_ANCHOR);
    }

    /**
     * Removes all anchor pane constraints from the child node.
     * @param child the child node
     */
    public static void clearConstraints(Node child) {
        setTopAnchor(child, null);
        setRightAnchor(child, null);
        setBottomAnchor(child, null);
        setLeftAnchor(child, null);
    }

    /* ******************************************************************
     *  END static methods
     ********************************************************************/

    /**
     * Creates an AnchorPane layout.
     */
    public AnchorPane() {
        super();
    }

    /**
     * Creates an AnchorPane layout with the given children.
     * @param children    The initial set of children for this pane.
     * @since JavaFX 8.0
     */
    public AnchorPane(Node... children) {
        super();
        getChildren().addAll(children);
    }

    @Override protected double computeMinWidth(double height) {
        return computeWidth(true, height);
    }

    @Override protected double computeMinHeight(double width) {
        return computeHeight(true, width);
    }

    @Override protected double computePrefWidth(double height) {
        return computeWidth(false, height);
    }

    @Override protected double computePrefHeight(double width) {
        return computeHeight(false, width);
    }

    private double computeWidth(final boolean minimum, final double height) {
        double max = 0;
        double contentHeight = height != -1 ? height - getInsets().getTop() - getInsets().getBottom() : -1;
        final List<Node> children = getManagedChildren();
        for (Node child : children) {
            Double leftAnchor = getLeftAnchor(child);
            Double rightAnchor = getRightAnchor(child);

            double left = leftAnchor != null? leftAnchor :
                    (rightAnchor != null? 0 : child.getLayoutBounds().getMinX() + child.getLayoutX());
            double right = rightAnchor != null? rightAnchor : 0;
            double childHeight = -1;
            if (child.getContentBias() == Orientation.VERTICAL && contentHeight != -1) {
                // The width depends on the node's height!
                childHeight = computeChildHeight(child, getTopAnchor(child), getBottomAnchor(child), contentHeight, -1);
            }
            max = Math.max(max, left + (minimum && leftAnchor != null && rightAnchor != null?
                    child.minWidth(childHeight) : computeChildPrefAreaWidth(child, -1, null, childHeight, false)) + right);
        }

        final Insets insets = getInsets();
        return insets.getLeft() + max + insets.getRight();
    }

    private double computeHeight(final boolean minimum, final double width) {
        double max = 0;
        double contentWidth = width != -1 ? width - getInsets().getLeft()- getInsets().getRight() : -1;
        final List<Node> children = getManagedChildren();
        for (Node child : children) {
            Double topAnchor = getTopAnchor(child);
            Double bottomAnchor = getBottomAnchor(child);

            double top = topAnchor != null? topAnchor :
                    (bottomAnchor != null? 0 : child.getLayoutBounds().getMinY() + child.getLayoutY());
            double bottom = bottomAnchor != null? bottomAnchor : 0;
            double childWidth = -1;
            if (child.getContentBias() == Orientation.HORIZONTAL && contentWidth != -1) {
                childWidth = computeChildWidth(child, getLeftAnchor(child), getRightAnchor(child), contentWidth, -1);
            }
            max = Math.max(max, top + (minimum && topAnchor != null && bottomAnchor != null?
                    child.minHeight(childWidth) : computeChildPrefAreaHeight(child, -1, null, childWidth)) + bottom);
        }

        final Insets insets = getInsets();
        return insets.getTop() + max + insets.getBottom();
    }

    private double computeChildWidth(Node child, Double leftAnchor, Double rightAnchor, double areaWidth, double height) {
        if (leftAnchor != null && rightAnchor != null && child.isResizable()) {
            final Insets insets = getInsets();
            return areaWidth - insets.getLeft() - insets.getRight() - leftAnchor - rightAnchor;
        }
        return computeChildPrefAreaWidth(child, -1, Insets.EMPTY, height, true);
    }

    private double computeChildHeight(Node child, Double topAnchor, Double bottomAnchor, double areaHeight, double width) {
        if (topAnchor != null && bottomAnchor != null && child.isResizable()) {
            final Insets insets = getInsets();
            return areaHeight - insets.getTop() - insets.getBottom() - topAnchor - bottomAnchor;
        }
        return computeChildPrefAreaHeight(child, -1, Insets.EMPTY, width);
    }

    @Override protected void layoutChildren() {
        final Insets insets = getInsets();
        final List<Node> children = getManagedChildren();
        for (Node child : children) {
            final Double topAnchor = getTopAnchor(child);
            final Double bottomAnchor = getBottomAnchor(child);
            final Double leftAnchor = getLeftAnchor(child);
            final Double rightAnchor = getRightAnchor(child);
            final Bounds childLayoutBounds = child.getLayoutBounds();
            final Orientation bias = child.getContentBias();

            double x = child.getLayoutX() + childLayoutBounds.getMinX();
            double y = child.getLayoutY() + childLayoutBounds.getMinY();
            double w;
            double h;

            if (bias == Orientation.VERTICAL) {
                // width depends on height
                // WARNING: The order of these calls is crucial, there is some
                // hidden ordering dependency here!
                h = computeChildHeight(child, topAnchor, bottomAnchor, getHeight(), -1);
                w = computeChildWidth(child, leftAnchor, rightAnchor, getWidth(), h);
            } else if (bias == Orientation.HORIZONTAL) {
                w = computeChildWidth(child, leftAnchor, rightAnchor, getWidth(), -1);
                h = computeChildHeight(child, topAnchor, bottomAnchor, getHeight(), w);
            } else {
                // bias may be null
                w = computeChildWidth(child, leftAnchor, rightAnchor, getWidth(), -1);
                h = computeChildHeight(child, topAnchor, bottomAnchor, getHeight(), -1);
            }

            if (leftAnchor != null) {
                x = insets.getLeft() + leftAnchor;
            } else if (rightAnchor != null) {
                x = getWidth() - insets.getRight() - rightAnchor - w;
            }

            if (topAnchor != null) {
                y = insets.getTop() + topAnchor;
            } else if (bottomAnchor != null) {
                y = getHeight() - insets.getBottom() - bottomAnchor - h;
            }

            child.resizeRelocate(x, y, w, h);
        }
    }
}
