/*
 * Copyright (c) 2011, 2018, 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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import com.sun.javafx.collections.TrackableObservableList;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.EnumConverter;
import javafx.css.converter.SizeConverter;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import javafx.beans.Observable;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.util.Callback;



/**
 * GridPane lays out its children within a flexible grid of rows and columns.
 * If a border and/or padding is set, then its content will be laid out within
 * those insets.
 * <p>
 * A child may be placed anywhere within the grid and may span multiple
 * rows/columns.  Children may freely overlap within rows/columns and their
 * stacking order will be defined by the order of the gridpane's children list
 * (0th node in back, last node in front).
 * <p>
 * GridPane may be styled with backgrounds and borders using CSS.  See
 * {@link javafx.scene.layout.Region Region} superclass for details.</p>
 *
 * <h3>Grid Constraints</h3>
 * <p>
 * A child's placement within the grid is defined by it's layout constraints:
 * </p>
 *
 * <table border="1">
 * <caption>Grid Constraint Table</caption>
 * <tr><th scope="col">Constraint</th><th scope="col">Type</th><th scope="col">Description</th></tr>
 * <tr><th scope="row">columnIndex</th><td>integer</td><td>column where child's layout area starts.</td></tr>
 * <tr><th scope="row">rowIndex</th><td>integer</td><td>row where child's layout area starts.</td></tr>
 * <tr><th scope="row">columnSpan</th><td>integer</td><td>the number of columns the child's layout area spans horizontally.</td></tr>
 * <tr><th scope="row">rowSpan</th><td>integer</td><td>the number of rows the child's layout area spans vertically.</td></tr>
 * </table>
 * <p>
 * If the row/column indices are not explicitly set, then the child will be placed
 * in the first row/column.  If row/column spans are not set, they will default to 1.
 * A child's placement constraints can be changed dynamically and the gridpane
 * will update accordingly.
 * <p>
 * The total number of rows/columns does not need to be specified up front as the
 * gridpane will automatically expand/contract the grid to accommodate the content.
 * <p>
 * To use the GridPane, an application needs to set the layout constraints on
 * the children and add those children to the gridpane instance.
 * Constraints are set on the children using static setter methods on the GridPane
 * class:
 * <pre><code>     GridPane gridpane = new GridPane();
 *
 *     // Set one constraint at a time...
 *     // Places the button at the first row and second column
 *     Button button = new Button();
 *     <b>GridPane.setRowIndex(button, 0);
 *     GridPane.setColumnIndex(button, 1);</b>
 *
 *     // or convenience methods set more than one constraint at once...
 *     Label label = new Label();
 *     <b>GridPane.setConstraints(label, 2, 0);</b> // column=2 row=0
 *
 *     // don't forget to add children to gridpane
 *     <b>gridpane.getChildren().addAll(button, label);</b>
 * </code></pre>
 *
 * Applications may also use convenience methods which combine the steps of
 * setting the constraints and adding the children:
 * <pre><code>
 *     GridPane gridpane = new GridPane();
 *     <b>gridpane.add(new Button(), 1, 0);</b> // column=1 row=0
 *     <b>gridpane.add(new Label(), 2, 0);</b>  // column=2 row=0
 * </code></pre>
 *
 *
 * <h3>Row/Column Sizing</h3>
 *
 * By default, rows and columns will be sized to fit their content;
 * a column will be wide enough to accommodate the widest child, a
 * row tall enough to fit the tallest child.However, if an application needs
 * to explicitly control the size of rows or columns, it may do so by adding
 * RowConstraints and ColumnConstraints objects to specify those metrics.
 * For example, to create a grid with two fixed-width columns:
 * <pre><code>
 *     GridPane gridpane = new GridPane();
 *     <b>gridpane.getColumnConstraints().add(new ColumnConstraints(100));</b> // column 0 is 100 wide
 *     <b>gridpane.getColumnConstraints().add(new ColumnConstraints(200));</b> // column 1 is 200 wide
 * </code></pre>
 * By default the gridpane will resize rows/columns to their preferred sizes (either
 * computed from content or fixed), even if the gridpane is resized larger than
 * its preferred size.   If an application needs a particular row or column to
 * grow if there is extra space, it may set its grow priority on the RowConstraints
 * or ColumnConstraints object.  For example:
 * <pre><code>
 *     GridPane gridpane = new GridPane();
 *     ColumnConstraints column1 = new ColumnConstraints(100,100,Double.MAX_VALUE);
 *     <b>column1.setHgrow(Priority.ALWAYS);</b>
 *     ColumnConstraints column2 = new ColumnConstraints(100);
 *     gridpane.getColumnConstraints().addAll(column1, column2); // first column gets any extra width
 * </code></pre>
 * <p>
 * Note: Nodes spanning multiple rows/columns will be also size to the preferred sizes.
 * The affected rows/columns are resized by the following priority: grow priorities, last row.
 * This is with respect to row/column constraints.
 *
 * <h3>Percentage Sizing</h3>
 *
 * Alternatively, RowConstraints and ColumnConstraints allow the size to be specified
 * as a percentage of gridpane's available space:
 * <pre><code>
 *     GridPane gridpane = new GridPane();
 *     ColumnConstraints column1 = new ColumnConstraints();
 *     <b>column1.setPercentWidth(50);</b>
 *     ColumnConstraints column2 = new ColumnConstraints();
 *     <b>column2.setPercentWidth(50);</b>
 *     gridpane.getColumnConstraints().addAll(column1, column2); // each get 50% of width
 * </code></pre>
 * If a percentage value is set on a row/column, then that value takes precedent and the
 * row/column's min, pref, max, and grow constraints will be ignored.
 * <p>
 * Note that if the sum of the widthPercent (or heightPercent) values total greater than 100, the values will
 * be treated as weights.  e.g.  if 3 columns are each given a widthPercent of 50,
 * then each will be allocated 1/3 of the gridpane's available width (50/(50+50+50)).
 *
 * <h3>Mixing Size Types</h3>
 *
 * An application may freely mix the size-types of rows/columns (computed from content, fixed,
 * or percentage).  The percentage rows/columns will always be allocated space first
 * based on their percentage of the gridpane's available space (size minus insets and gaps).
 * The remaining space will be allocated to rows/columns given their minimum, preferred,
 * and maximum sizes and grow priorities.
 *
 * <h3>Resizable Range</h3>
 * <p>
 * A gridpane's parent will resize the gridpane within the gridpane's resizable range
 * during layout.   By default the gridpane computes this range based on its content
 * and row/column constraints as outlined in the table below.
 * </p>
 *
 * <table border="1">
 * <caption>GridPane 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 the sum of each column's min width.</td>
 * <td>top/bottom insets plus the sum of each row's min height.</td></tr>
 * <tr><th scope="row">preferred</th>
 * <td>left/right insets plus the sum of each column's pref width.</td>
 * <td>top/bottom insets plus the sum of each row's pref height.</td></tr>
 * <tr><th scope="row">maximum</th>
 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
 * </table>
 * <p>
 * A gridpane'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>
 * GridPane provides properties for setting the size range directly.  These
 * properties default to the sentinel value USE_COMPUTED_SIZE, however the
 * application may set them to other values as needed:
 * <pre><code>     <b>gridpane.setPrefSize(300, 300);</b>
 *     // never size the gridpane larger than its preferred size:
 *     <b>gridpane.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);</b>
 * </code></pre>
 * Applications may restore the computed values by setting these properties back
 * to USE_COMPUTED_SIZE.
 * <p>
 * GridPane does not clip its content by default, so it is possible that children's
 * bounds may extend outside its own bounds if a child's min size prevents it from
 * being fit within it space.</p>
 *
 * <h3>Optional Layout Constraints</h3>
 *
 * <p>
 * An application may set additional constraints on children to customize how the
 * child is sized and positioned within the layout area established by it's row/column
 * indices/spans:
 * </p>
 *
 * <table border="1">
 * <caption>GridPane Constraint Table</caption>
 * <tr><th scope="col">Constraint</th><th scope="col">Type</th><th scope="col">Description</th></tr>
 * <tr><th scope="row">halignment</th><td>javafx.geometry.HPos</td><td>The horizontal alignment of the child within its layout area.</td></tr>
 * <tr><th scope="row">valignment</th><td>javafx.geometry.VPos</td><td>The vertical alignment of the child within its layout area.</td></tr>
 * <tr><th scope="row">hgrow</th><td>javafx.scene.layout.Priority</td><td>The horizontal grow priority of the child.</td></tr>
 * <tr><th scope="row">vgrow</th><td>javafx.scene.layout.Priority</td><td>The vertical grow priority of the child.</td></tr>
 * <tr><th scope="row">margin</th><td>javafx.geometry.Insets</td><td>Margin space around the outside of the child.</td></tr>
 * </table>
 * <p>
 * By default the alignment of a child within its layout area is defined by the
 * alignment set for the row and column.  If an individual alignment constraint is
 * set on a child, that alignment will override the row/column alignment only
 * for that child.  Alignment of other children in the same row or column will
 * not be affected.
 * <p>
 * Grow priorities, on the other hand, can only be applied to entire rows or columns.
 * Therefore, if a grow priority constraint is set on a single child, it will be
 * used to compute the default grow priority of the encompassing row/column.  If
 * a grow priority is set directly on a RowConstraint or ColumnConstraint object,
 * it will override the value computed from content.
 *
 *
 * @since JavaFX 2.0
 */
public class GridPane extends Pane {

    /**
     * Sentinel value which may be set on a child's row/column span constraint to
     * indicate that it should span the remaining rows/columns.
     */
    public static final int REMAINING = Integer.MAX_VALUE;

    /********************************************************************
     *  BEGIN static methods
     ********************************************************************/
    private static final String MARGIN_CONSTRAINT = "gridpane-margin";
    private static final String HALIGNMENT_CONSTRAINT = "gridpane-halignment";
    private static final String VALIGNMENT_CONSTRAINT = "gridpane-valignment";
    private static final String HGROW_CONSTRAINT = "gridpane-hgrow";
    private static final String VGROW_CONSTRAINT = "gridpane-vgrow";
    private static final String ROW_INDEX_CONSTRAINT = "gridpane-row";
    private static final String COLUMN_INDEX_CONSTRAINT = "gridpane-column";
    private static final String ROW_SPAN_CONSTRAINT = "gridpane-row-span";
    private static final String COLUMN_SPAN_CONSTRAINT = "gridpane-column-span";
    private static final String FILL_WIDTH_CONSTRAINT = "gridpane-fill-width";
    private static final String FILL_HEIGHT_CONSTRAINT = "gridpane-fill-height";

    /**
     * Sets the row index for the child when contained by a gridpane
     * so that it will be positioned starting in that row of the gridpane.
     * If a gridpane child has no row index set, it will be positioned in the
     * first row.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the row index of the child
     */
    public static void setRowIndex(Node child, Integer value) {
        if (value != null && value < 0) {
            throw new IllegalArgumentException("rowIndex must be greater or equal to 0, but was "+value);
        }
        setConstraint(child, ROW_INDEX_CONSTRAINT, value);
    }

    /**
     * Returns the child's row index constraint if set.
     * @param child the child node of a gridpane
     * @return the row index for the child or null if no row index was set
     */
    public static Integer getRowIndex(Node child) {
        return (Integer)getConstraint(child, ROW_INDEX_CONSTRAINT);
    }

    /**
     * Sets the column index for the child when contained by a gridpane
     * so that it will be positioned starting in that column of the gridpane.
     * If a gridpane child has no column index set, it will be positioned in
     * the first column.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the column index of the child
     */
    public static void setColumnIndex(Node child, Integer value) {
        if (value != null && value < 0) {
            throw new IllegalArgumentException("columnIndex must be greater or equal to 0, but was "+value);
        }
        setConstraint(child, COLUMN_INDEX_CONSTRAINT, value);
    }

    /**
     * Returns the child's column index constraint if set.
     * @param child the child node of a gridpane
     * @return the column index for the child or null if no column index was set
     */
    public static Integer getColumnIndex(Node child) {
        return (Integer)getConstraint(child, COLUMN_INDEX_CONSTRAINT);
    }

    /**
     * Sets the row span for the child when contained by a gridpane
     * so that it will span that number of rows vertically.  This may be
     * set to REMAINING, which will cause the span to extend across all the remaining
     * rows.
     * <p>
     * If a gridpane child has no row span set, it will default to spanning one row.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the row span of the child
     */
    public static void setRowSpan(Node child, Integer value) {
        if (value != null && value < 1) {
            throw new IllegalArgumentException("rowSpan must be greater or equal to 1, but was "+value);
        }
        setConstraint(child, ROW_SPAN_CONSTRAINT, value);
    }

    /**
     * Returns the child's row-span constraint if set.
     * @param child the child node of a gridpane
     * @return the row span for the child or null if no row span was set
     */
    public static Integer getRowSpan(Node child) {
        return (Integer)getConstraint(child, ROW_SPAN_CONSTRAINT);
    }

    /**
     * Sets the column span for the child when contained by a gridpane
     * so that it will span that number of columns horizontally.   This may be
     * set to REMAINING, which will cause the span to extend across all the remaining
     * columns.
     * <p>
     * If a gridpane child has no column span set, it will default to spanning one column.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the column span of the child
     */
    public static void setColumnSpan(Node child, Integer value) {
        if (value != null && value < 1) {
            throw new IllegalArgumentException("columnSpan must be greater or equal to 1, but was "+value);
        }
        setConstraint(child, COLUMN_SPAN_CONSTRAINT, value);
    }

    /**
     * Returns the child's column-span constraint if set.
     * @param child the child node of a gridpane
     * @return the column span for the child or null if no column span was set
     */
    public static Integer getColumnSpan(Node child) {
        return (Integer)getConstraint(child, COLUMN_SPAN_CONSTRAINT);
    }

    /**
     * Sets the margin for the child when contained by a gridpane.
     * If set, the gridpane will lay it out with the margin space around it.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the margin of space around the child
     */
    public static void setMargin(Node child, Insets value) {
        setConstraint(child, MARGIN_CONSTRAINT, value);
    }

    /**
     * Returns the child's margin constraint if set.
     * @param child the child node of a gridpane
     * @return the margin for the child or null if no margin was set
     */
    public static Insets getMargin(Node child) {
        return (Insets)getConstraint(child, MARGIN_CONSTRAINT);
    }

    private double getBaselineComplementForChild(Node child) {
        if (isNodePositionedByBaseline(child)) {
            return rowMinBaselineComplement[getNodeRowIndex(child)];
        }
        return -1;
    }

    private static final Callback<Node, Insets> marginAccessor = n -> getMargin(n);

    /**
     * Sets the horizontal alignment for the child when contained by a gridpane.
     * If set, will override the gridpane's default horizontal alignment.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the hozizontal alignment for the child
     */
    public static void setHalignment(Node child, HPos value) {
        setConstraint(child, HALIGNMENT_CONSTRAINT, value);
    }

    /**
     * Returns the child's halignment constraint if set.
     * @param child the child node of a gridpane
     * @return the horizontal alignment for the child or null if no alignment was set
     */
    public static HPos getHalignment(Node child) {
        return (HPos)getConstraint(child, HALIGNMENT_CONSTRAINT);
    }

    /**
     * Sets the vertical alignment for the child when contained by a gridpane.
     * If set, will override the gridpane's default vertical alignment.
     * Setting the value to null will remove the constraint.
     * @param child the child node of a gridpane
     * @param value the vertical alignment for the child
     */
    public static void setValignment(Node child, VPos value) {
        setConstraint(child, VALIGNMENT_CONSTRAINT, value);
    }

    /**
     * Returns the child's valignment constraint if set.
     * @param child the child node of a gridpane
     * @return the vertical alignment for the child or null if no alignment was set
     */
    public static VPos getValignment(Node child) {
        return (VPos)getConstraint(child, VALIGNMENT_CONSTRAINT);
    }

    /**
     * Sets the horizontal grow priority for the child when contained by a gridpane.
     * If set, the gridpane will use the priority to allocate the child additional
     * horizontal space if the gridpane is resized larger than it's preferred width.
     * Setting the value to null will remove the constraint.
     * @param child the child of a gridpane
     * @param value the horizontal grow priority for the child
     */
    public static void setHgrow(Node child, Priority value) {
        setConstraint(child, HGROW_CONSTRAINT, value);
    }

    /**
     * Returns the child's hgrow constraint if set.
     * @param child the child node of a gridpane
     * @return the horizontal grow priority for the child or null if no priority was set
     */
    public static Priority getHgrow(Node child) {
        return (Priority)getConstraint(child, HGROW_CONSTRAINT);
    }

    /**
     * Sets the vertical grow priority for the child when contained by a gridpane.
     * If set, the gridpane will use the priority to allocate the child additional
     * vertical space if the gridpane is resized larger than it's preferred height.
     * Setting the value to null will remove the constraint.
     * @param child the child of a gridpane
     * @param value the vertical grow priority for the child
     */
    public static void setVgrow(Node child, Priority value) {
        setConstraint(child, VGROW_CONSTRAINT, value);
    }

    /**
     * Returns the child's vgrow constraint if set.
     * @param child the child node of a gridpane
     * @return the vertical grow priority for the child or null if no priority was set
     */
    public static Priority getVgrow(Node child) {
        return (Priority)getConstraint(child, VGROW_CONSTRAINT);
    }

    /**
     * Sets the horizontal fill policy for the child when contained by a gridpane.
     * If set, the gridpane will use the policy to determine whether node
     * should be expanded to fill the column or resized to its preferred width.
     * Setting the value to null will remove the constraint.
     * If not value is specified for the node nor for the column, the default value is true.
     * @param child the child node of a gridpane
     * @param value the horizontal fill policy or null for unset
     * @since JavaFX 8.0
     */
    public static void setFillWidth(Node child, Boolean value) {
        setConstraint(child, FILL_WIDTH_CONSTRAINT, value);
    }

    /**
     * Returns the child's horizontal fill policy if set
     * @param child the child node of a gridpane
     * @return the horizontal fill policy for the child or null if no policy was set
     * @since JavaFX 8.0
     */
    public static Boolean isFillWidth(Node child) {
        return (Boolean) getConstraint(child, FILL_WIDTH_CONSTRAINT);
    }

    /**
     * Sets the vertical fill policy for the child when contained by a gridpane.
     * If set, the gridpane will use the policy to determine whether node
     * should be expanded to fill the row or resized to its preferred height.
     * Setting the value to null will remove the constraint.
     * If not value is specified for the node nor for the row, the default value is true.
     * @param child the child node of a gridpane
     * @param value the vertical fill policy or null for unset
     * @since JavaFX 8.0
     */
    public static void setFillHeight(Node child, Boolean value) {
        setConstraint(child, FILL_HEIGHT_CONSTRAINT, value);
    }

    /**
     * Returns the child's vertical fill policy if set
     * @param child the child node of a gridpane
     * @return the vertical fill policy for the child or null if no policy was set
     * @since JavaFX 8.0
     */
    public static Boolean isFillHeight(Node child) {
        return (Boolean) getConstraint(child, FILL_HEIGHT_CONSTRAINT);
    }

    /**
     * Sets the column,row indeces for the child when contained in a gridpane.
     * @param child the child node of a gridpane
     * @param columnIndex the column index position for the child
     * @param rowIndex the row index position for the child
     */
    public static void setConstraints(Node child, int columnIndex, int rowIndex) {
        setRowIndex(child, rowIndex);
        setColumnIndex(child, columnIndex);
    }

    /**
     * Sets the column, row, column-span, and row-span value for the child when
     * contained in a gridpane.
     * @param child the child node of a gridpane
     * @param columnIndex the column index position for the child
     * @param rowIndex the row index position for the child
     * @param columnspan the number of columns the child should span
     * @param rowspan the number of rows the child should span
     */
    public static void setConstraints(Node child, int columnIndex, int rowIndex, int columnspan, int rowspan) {
        setRowIndex(child, rowIndex);
        setColumnIndex(child, columnIndex);
        setRowSpan(child, rowspan);
        setColumnSpan(child, columnspan);
    }

    /**
     * Sets the grid position, spans, and alignment for the child when contained in a gridpane.
     * @param child the child node of a gridpane
     * @param columnIndex the column index position for the child
     * @param rowIndex the row index position for the child
     * @param columnspan the number of columns the child should span
     * @param rowspan the number of rows the child should span
     * @param halignment the horizontal alignment of the child
     * @param valignment the vertical alignment of the child
     */
    public static void setConstraints(Node child, int columnIndex, int rowIndex, int columnspan, int rowspan,
            HPos halignment, VPos valignment) {
        setRowIndex(child, rowIndex);
        setColumnIndex(child, columnIndex);
        setRowSpan(child, rowspan);
        setColumnSpan(child, columnspan);
        setHalignment(child, halignment);
        setValignment(child, valignment);
    }

    /**
     * Sets the grid position, spans, and alignment for the child when contained in a gridpane.
     * @param child the child node of a gridpane
     * @param columnIndex the column index position for the child
     * @param rowIndex the row index position for the child
     * @param columnspan the number of columns the child should span
     * @param rowspan the number of rows the child should span
     * @param halignment the horizontal alignment of the child
     * @param valignment the vertical alignment of the child
     * @param hgrow the horizontal grow priority of the child
     * @param vgrow the vertical grow priority of the child
     */
    public static void setConstraints(Node child, int columnIndex, int rowIndex, int columnspan, int rowspan,
            HPos halignment, VPos valignment, Priority hgrow, Priority vgrow) {
        setRowIndex(child, rowIndex);
        setColumnIndex(child, columnIndex);
        setRowSpan(child, rowspan);
        setColumnSpan(child, columnspan);
        setHalignment(child, halignment);
        setValignment(child, valignment);
        setHgrow(child, hgrow);
        setVgrow(child, vgrow);
    }

    /**
     * Sets the grid position, spans, alignment, grow priorities, and margin for
     * the child when contained in a gridpane.
     * @param child the child node of a gridpane
     * @param columnIndex the column index position for the child
     * @param rowIndex the row index position for the child
     * @param columnspan the number of columns the child should span
     * @param rowspan the number of rows the child should span
     * @param halignment the horizontal alignment of the child
     * @param valignment the vertical alignment of the child
     * @param hgrow the horizontal grow priority of the child
     * @param vgrow the vertical grow priority of the child
     * @param margin the margin of space around the child
     */
    public static void setConstraints(Node child, int columnIndex, int rowIndex, int columnspan, int rowspan,
            HPos halignment, VPos valignment, Priority hgrow, Priority vgrow, Insets margin) {
        setRowIndex(child, rowIndex);
        setColumnIndex(child, columnIndex);
        setRowSpan(child, rowspan);
        setColumnSpan(child, columnspan);
        setHalignment(child, halignment);
        setValignment(child, valignment);
        setHgrow(child, hgrow);
        setVgrow(child, vgrow);
        setMargin(child, margin);
    }

    /**
     * Removes all gridpane constraints from the child node.
     * @param child the child node
     */
    public static void clearConstraints(Node child) {
        setRowIndex(child, null);
        setColumnIndex(child, null);
        setRowSpan(child, null);
        setColumnSpan(child, null);
        setHalignment(child, null);
        setValignment(child, null);
        setHgrow(child, null);
        setVgrow(child, null);
        setMargin(child, null);
    }


    private static final Color GRID_LINE_COLOR = Color.rgb(30, 30, 30);
    private static final double GRID_LINE_DASH = 3;

    static void createRow(int rowIndex, int columnIndex, Node... nodes) {
        for (int i = 0; i < nodes.length; i++) {
            setConstraints(nodes[i], columnIndex + i, rowIndex);
        }
    }

    static void createColumn(int columnIndex, int rowIndex, Node... nodes) {
        for (int i = 0; i < nodes.length; i++) {
            setConstraints(nodes[i], columnIndex, rowIndex + i);
        }
    }

    static int getNodeRowIndex(Node node) {
        Integer rowIndex = getRowIndex(node);
        return rowIndex != null? rowIndex : 0;
    }

    private static int getNodeRowSpan(Node node) {
        Integer rowspan = getRowSpan(node);
        return rowspan != null? rowspan : 1;
    }

    static int getNodeRowEnd(Node node) {
        int rowSpan = getNodeRowSpan(node);
        return rowSpan != REMAINING? getNodeRowIndex(node) + rowSpan - 1 : REMAINING;
    }

    static int getNodeColumnIndex(Node node) {
        Integer columnIndex = getColumnIndex(node);
        return columnIndex != null? columnIndex : 0;
    }

    private static int getNodeColumnSpan(Node node) {
        Integer colspan = getColumnSpan(node);
        return colspan != null? colspan : 1;
    }

    static int getNodeColumnEnd(Node node) {
        int columnSpan = getNodeColumnSpan(node);
        return columnSpan != REMAINING? getNodeColumnIndex(node) + columnSpan - 1 : REMAINING;
    }

    private static Priority getNodeHgrow(Node node) {
        Priority hgrow = getHgrow(node);
        return hgrow != null? hgrow : Priority.NEVER;
    }

    private static Priority getNodeVgrow(Node node) {
        Priority vgrow = getVgrow(node);
        return vgrow != null? vgrow : Priority.NEVER;
    }

    private static Priority[] createPriorityArray(int length, Priority value) {
        Priority[] array = new Priority[length];
        Arrays.fill(array, value);
        return array;
    }

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

    /**
     * Creates a GridPane layout with hgap/vgap = 0 and TOP_LEFT alignment.
     */
    public GridPane() {
        super();
        getChildren().addListener((Observable o) -> requestLayout());
    }

    /**
     * The width of the horizontal gaps between columns.
     * @return the width of the horizontal gaps between columns
     */
    public final DoubleProperty hgapProperty() {
        if (hgap == null) {
            hgap = new StyleableDoubleProperty(0) {
                @Override
                public void invalidated() {
                    requestLayout();
                }

                @Override
                public CssMetaData<GridPane, Number> getCssMetaData() {
                    return StyleableProperties.HGAP;
                }

                @Override
                public Object getBean() {
                    return GridPane.this;
                }

                @Override
                public String getName() {
                    return "hgap";
                }
            };
        }
        return hgap;
    }

    private DoubleProperty hgap;
    public final void setHgap(double value) { hgapProperty().set(value); }
    public final double getHgap() { return hgap == null ? 0 : hgap.get(); }

    /**
     * The height of the vertical gaps between rows.
     * @return the height of the vertical gaps between rows
     */
    public final DoubleProperty vgapProperty() {
        if (vgap == null) {
            vgap = new StyleableDoubleProperty(0) {
                @Override
                public void invalidated() {
                    requestLayout();
                }

                @Override
                public CssMetaData<GridPane, Number> getCssMetaData() {
                    return StyleableProperties.VGAP;
                }

                @Override
                public Object getBean() {
                    return GridPane.this;
                }

                @Override
                public String getName() {
                    return "vgap";
                }
            };
        }
        return vgap;
    }

    private DoubleProperty vgap;
    public final void setVgap(double value) { vgapProperty().set(value); }
    public final double getVgap() { return vgap == null ? 0 : vgap.get(); }

    /**
     * The alignment of the grid within the gridpane's width and height.
     * @return the alignment of the grid within the gridpane's width and height
     */
    public final ObjectProperty<Pos> alignmentProperty() {
        if (alignment == null) {
            alignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) {
                @Override
                public void invalidated() {
                    requestLayout();
                }

                @Override
                public CssMetaData<GridPane, Pos> getCssMetaData() {
                    return StyleableProperties.ALIGNMENT;
                }

                @Override
                public Object getBean() {
                    return GridPane.this;
                }

                @Override
                public String getName() {
                    return "alignment";
                }
            };
        }
        return alignment;
    }

    private ObjectProperty<Pos> alignment;
    public final void setAlignment(Pos value) {
        alignmentProperty().set(value);
    }
    public final Pos getAlignment() {
        return alignment == null ? Pos.TOP_LEFT : alignment.get();
    }
    private Pos getAlignmentInternal() {
        Pos localPos = getAlignment();
        return localPos == null ? Pos.TOP_LEFT : localPos;
    }

    /**
     * For debug purposes only: controls whether lines are displayed to show the gridpane's rows and columns.
     * Default is <code>false</code>.
     * @return true if lines are displayed to show the gridpane's rows and columns
     */
    public final BooleanProperty gridLinesVisibleProperty() {
        if (gridLinesVisible == null) {
            gridLinesVisible = new StyleableBooleanProperty() {
                @Override
                protected void invalidated() {
                    if (get()) {
                        gridLines = new Group();
                        gridLines.setManaged(false);
                        getChildren().add(gridLines);
                    } else {
                        getChildren().remove(gridLines);
                        gridLines = null;
                    }
                    requestLayout();
                }

                @Override
                public CssMetaData<GridPane, Boolean> getCssMetaData() {
                    return StyleableProperties.GRID_LINES_VISIBLE;
                }

                @Override
                public Object getBean() {
                    return GridPane.this;
                }

                @Override
                public String getName() {
                    return "gridLinesVisible";
                }
            };
        }
        return gridLinesVisible;
    }

    private BooleanProperty gridLinesVisible;
    public final void setGridLinesVisible(boolean value) { gridLinesVisibleProperty().set(value); }
    public final boolean isGridLinesVisible() { return gridLinesVisible == null ? false : gridLinesVisible.get(); }

    /**
     * RowConstraints instances can be added to explicitly control individual row
     * sizing and layout behavior.
     * If not set, row sizing and layout behavior will be computed based on content.
     *
     */
    private final ObservableList<RowConstraints> rowConstraints = new TrackableObservableList<RowConstraints>() {
        @Override
        protected void onChanged(Change<RowConstraints> c) {
            while (c.next()) {
                for (RowConstraints constraints : c.getRemoved()) {
                    if (constraints != null && !rowConstraints.contains(constraints)) {
                        constraints.remove(GridPane.this);
                    }
                }
                for (RowConstraints constraints : c.getAddedSubList()) {
                    if (constraints != null) {
                        constraints.add(GridPane.this);
                    }
                }
            }
            requestLayout();
        }
    };

    /**
     * Returns list of row constraints. Row constraints can be added to
     * explicitly control individual row sizing and layout behavior.
     * If not set, row sizing and layout behavior is computed based on content.
     *
     * Index in the ObservableList denotes the row number, so the row constraint for the first row
     * is at the position of 0.
     * @return the list of row constraints
     */
    public final ObservableList<RowConstraints> getRowConstraints() { return rowConstraints; }
    /**
     * ColumnConstraints instances can be added to explicitly control individual column
     * sizing and layout behavior.
     * If not set, column sizing and layout behavior will be computed based on content.
     */
    private final ObservableList<ColumnConstraints> columnConstraints = new TrackableObservableList<ColumnConstraints>() {
        @Override
        protected void onChanged(Change<ColumnConstraints> c) {
            while(c.next()) {
                for (ColumnConstraints constraints : c.getRemoved()) {
                    if (constraints != null && !columnConstraints.contains(constraints)) {
                        constraints.remove(GridPane.this);
                    }
                }
                for (ColumnConstraints constraints : c.getAddedSubList()) {
                    if (constraints != null) {
                        constraints.add(GridPane.this);
                    }
                }
            }
            requestLayout();
        }
    };

    /**
     * Returns list of column constraints. Column constraints can be added to
     * explicitly control individual column sizing and layout behavior.
     * If not set, column sizing and layout behavior is computed based on content.
     *
     * Index in the ObservableList denotes the column number, so the column constraint for the first column
     * is at the position of 0.
     * @return the list of column constraints
     */
    public final ObservableList<ColumnConstraints> getColumnConstraints() { return columnConstraints; }

    /**
     * Adds a child to the gridpane at the specified column,row position.
     * This convenience method will set the gridpane column and row constraints
     * on the child.
     * @param child the node being added to the gridpane
     * @param columnIndex the column index position for the child within the gridpane, counting from 0
     * @param rowIndex the row index position for the child within the gridpane, counting from 0
     */
    public void add(Node child, int columnIndex, int rowIndex) {
        setConstraints(child, columnIndex, rowIndex);
        getChildren().add(child);
    }

    /**
     * Adds a child to the gridpane at the specified column,row position and spans.
     * This convenience method will set the gridpane column, row, and span constraints
     * on the child.
     * @param child the node being added to the gridpane
     * @param columnIndex the column index position for the child within the gridpane, counting from 0
     * @param rowIndex the row index position for the child within the gridpane, counting from 0
     * @param colspan the number of columns the child's layout area should span
     * @param rowspan the number of rows the child's layout area should span
     */
    public void add(Node child, int columnIndex, int rowIndex, int colspan, int rowspan) {
        setConstraints(child, columnIndex, rowIndex, colspan, rowspan);
        getChildren().add(child);
    }

    /**
     * Convenience method for placing the specified nodes sequentially in a given
     * row of the gridpane.    If the row already contains nodes the specified nodes
     * will be appended to the row.  For example, the first node will be positioned at [column,row],
     * the second at [column+1,row], etc.   This method will set the appropriate gridpane
     * row/column constraints on the nodes as well as add the nodes to the gridpane's
     * children sequence.
     *
     * @param rowIndex the row index position for the children within the gridpane
     * @param children the nodes to be added as a row in the gridpane
     */
    public void addRow(int rowIndex, Node... children) {
        int columnIndex = 0;
        final List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            final int nodeRowIndex = getNodeRowIndex(child);
            final int nodeRowEnd = getNodeRowEnd(child);
            if (rowIndex >= nodeRowIndex &&
                    (rowIndex <= nodeRowEnd || nodeRowEnd == REMAINING)) {
                int index = getNodeColumnIndex(child);
                int end = getNodeColumnEnd(child);
                columnIndex = Math.max(columnIndex, (end != REMAINING? end : index) + 1);
            }
        }
        createRow(rowIndex, columnIndex, children);
        getChildren().addAll(children);
    }

    /**
     * Convenience method for placing the specified nodes sequentially in a given
     * column of the gridpane.    If the column already contains nodes the specified nodes
     * will be appended to the column.  For example, the first node will be positioned at [column, row],
     * the second at [column, row+1], etc.   This method will set the appropriate gridpane
     * row/column constraints on the nodes as well as add the nodes to the gridpane's
     * children sequence.
     *
     * @param columnIndex the column index position for the children within the gridpane
     * @param children the nodes to be added as a column in the gridpane
     */
    public void addColumn(int columnIndex, Node... children)  {
        int rowIndex = 0;
        final List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            final int nodeColumnIndex = getNodeColumnIndex(child);
            final int nodeColumnEnd = getNodeColumnEnd(child);
            if (columnIndex >= nodeColumnIndex
                    && (columnIndex <= nodeColumnEnd || nodeColumnEnd == REMAINING)) {
                int index = getNodeRowIndex(child);
                int end = getNodeRowEnd(child);
                rowIndex = Math.max(rowIndex, (end != REMAINING? end : index) + 1);
            }
        }
        createColumn(columnIndex, rowIndex, children);
        getChildren().addAll(children);
    }

    private Group gridLines;
    private Orientation bias;

    private double[] rowPercentHeight;
    private double rowPercentTotal = 0;

    private CompositeSize rowMinHeight;
    private CompositeSize rowPrefHeight;
    private CompositeSize  rowMaxHeight;
    private List<Node>[] rowBaseline;
    private double[] rowMinBaselineComplement;
    private double[] rowPrefBaselineComplement;
    private double[] rowMaxBaselineComplement;
    private Priority[] rowGrow;

    private double[] columnPercentWidth;
    private double columnPercentTotal = 0;

    private CompositeSize columnMinWidth;
    private CompositeSize columnPrefWidth;
    private CompositeSize columnMaxWidth;
    private Priority[] columnGrow;

    private boolean metricsDirty = true;

    // This is set to true while in layoutChildren and set false on the conclusion.
    // It is used to decide whether to update metricsDirty in requestLayout().
    private boolean performingLayout = false;

    private int numRows;
    private int numColumns;

    private int getNumberOfRows() {
        computeGridMetrics();
        return numRows;
    }

    private int getNumberOfColumns() {
        computeGridMetrics();
        return numColumns;
    }

    private boolean isNodePositionedByBaseline(Node n){
        return (getRowValignment(getNodeRowIndex(n)) == VPos.BASELINE && getValignment(n) == null)
                || getValignment(n) == VPos.BASELINE;
    }

    private void computeGridMetrics() {
        if (metricsDirty) {
            numRows = rowConstraints.size();
            numColumns = columnConstraints.size();
            final List<Node> managed = getManagedChildren();
            for (int i = 0, size = managed.size(); i < size; i++) {
                Node child = managed.get(i);
                int rowIndex = getNodeRowIndex(child);
                int columnIndex = getNodeColumnIndex(child);
                int rowEnd = getNodeRowEnd(child);
                int columnEnd = getNodeColumnEnd(child);
                numRows = Math.max(numRows, (rowEnd != REMAINING ? rowEnd : rowIndex) + 1);
                numColumns = Math.max(numColumns, (columnEnd != REMAINING ? columnEnd : columnIndex) + 1);
            }
            rowPercentHeight = createDoubleArray(numRows, -1);
            rowPercentTotal = 0;
            columnPercentWidth = createDoubleArray(numColumns, -1);
            columnPercentTotal = 0;
            columnGrow = createPriorityArray(numColumns, Priority.NEVER);
            rowGrow = createPriorityArray(numRows, Priority.NEVER);
            rowMinBaselineComplement = createDoubleArray(numRows, -1);
            rowPrefBaselineComplement = createDoubleArray(numRows, -1);
            rowMaxBaselineComplement = createDoubleArray(numRows, -1);
            rowBaseline = new List[numRows];
            for (int i = 0, sz = numRows; i < sz; ++i) {
                if (i < rowConstraints.size()) {
                    final RowConstraints rc = rowConstraints.get(i);
                    double percentHeight = rc.getPercentHeight();
                    Priority vGrow = rc.getVgrow();
                    if (percentHeight >= 0) {
                        rowPercentHeight[i] = percentHeight;
                    }
                    if (vGrow != null) {
                        rowGrow[i] = vGrow;
                    }
                }

                List<Node> baselineNodes = new ArrayList<>(numColumns);
                for (int j = 0, size = managed.size(); j < size; j++) {
                    Node n = managed.get(j);
                    if (getNodeRowIndex(n) == i && isNodePositionedByBaseline(n)) {
                        baselineNodes.add(n);
                    }
                }
                rowMinBaselineComplement[i] = getMinBaselineComplement(baselineNodes);
                rowPrefBaselineComplement[i] = getPrefBaselineComplement(baselineNodes);
                rowMaxBaselineComplement[i] = getMaxBaselineComplement(baselineNodes);
                rowBaseline[i] = baselineNodes;

            }
            for (int i = 0, sz = Math.min(numColumns, columnConstraints.size()); i < sz; ++i) {
                final ColumnConstraints cc = columnConstraints.get(i);
                double percentWidth = cc.getPercentWidth();
                Priority hGrow = cc.getHgrow();
                if (percentWidth >= 0)
                    columnPercentWidth[i] = percentWidth;
                if (hGrow != null)
                    columnGrow[i] = hGrow;
            }

            for (int i = 0, size = managed.size(); i < size; i++) {
                Node child = managed.get(i);
                if (getNodeColumnSpan(child) == 1) {
                    Priority hg = getNodeHgrow(child);
                    int idx = getNodeColumnIndex(child);
                    columnGrow[idx] = Priority.max(columnGrow[idx], hg);
                }
                if (getNodeRowSpan(child) == 1) {
                    Priority vg = getNodeVgrow(child);
                    int idx = getNodeRowIndex(child);
                    rowGrow[idx] = Priority.max(rowGrow[idx], vg);
                }
            }

            for (int i = 0; i < rowPercentHeight.length; i++) {
                if (rowPercentHeight[i] > 0) {
                    rowPercentTotal += rowPercentHeight[i];
                }
            }
            if (rowPercentTotal > 100) {
                double weight = 100 / rowPercentTotal;
                for (int i = 0; i < rowPercentHeight.length; i++) {
                    if (rowPercentHeight[i] > 0) {
                        rowPercentHeight[i] *= weight;
                    }
                }
                rowPercentTotal = 100;
            }
            for (int i = 0; i < columnPercentWidth.length; i++) {
                if (columnPercentWidth[i] > 0) {
                    columnPercentTotal += columnPercentWidth[i];
                }
            }
            if (columnPercentTotal > 100) {
                double weight = 100 / columnPercentTotal;
                for (int i = 0; i < columnPercentWidth.length; i++) {
                    if (columnPercentWidth[i] > 0) {
                        columnPercentWidth[i] *= weight;
                    }
                }
                columnPercentTotal = 100;
            }

            bias = null;
            for (int i = 0; i < managed.size(); ++i) {
                final Orientation b = managed.get(i).getContentBias();
                if (b != null) {
                    bias = b;
                    if (b == Orientation.HORIZONTAL) {
                        break;
                    }
                }
            }

            metricsDirty = false;
        }
    }

    @Override protected double computeMinWidth(double height) {
        computeGridMetrics();
        performingLayout = true;
        try {
            final double[] heights = height == -1 ? null : computeHeightsToFit(height).asArray();

            return snapSpaceX(getInsets().getLeft()) +
                    computeMinWidths(heights).computeTotalWithMultiSize() +
                    snapSpaceX(getInsets().getRight());
        } finally {
            performingLayout = false;
        }

    }

    @Override protected double computeMinHeight(double width) {
        computeGridMetrics();
        performingLayout = true;
        try {
            final double[] widths = width == -1 ? null : computeWidthsToFit(width).asArray();

            return snapSpaceY(getInsets().getTop()) +
                    computeMinHeights(widths).computeTotalWithMultiSize() +
                    snapSpaceY(getInsets().getBottom());
        } finally {
            performingLayout = false;
        }
    }

    @Override protected double computePrefWidth(double height) {
        computeGridMetrics();
        performingLayout = true;
        try {
            final double[] heights = height == -1 ? null : computeHeightsToFit(height).asArray();

            return snapSpaceX(getInsets().getLeft()) +
                    computePrefWidths(heights).computeTotalWithMultiSize() +
                    snapSpaceX(getInsets().getRight());
        } finally {
            performingLayout = false;
        }
    }

    @Override protected double computePrefHeight(double width) {
        computeGridMetrics();
        performingLayout = true;
        try {
            final double[] widths = width == -1 ? null : computeWidthsToFit(width).asArray();

            return snapSpaceY(getInsets().getTop()) +
                    computePrefHeights(widths).computeTotalWithMultiSize() +
                    snapSpaceY(getInsets().getBottom());
        } finally {
            performingLayout = false;
        }
    }

    private VPos getRowValignment(int rowIndex) {
        if (rowIndex < getRowConstraints().size()) {
            RowConstraints constraints = getRowConstraints().get(rowIndex);
            if (constraints.getValignment() != null) {
                return constraints.getValignment();
            }
        }
        return VPos.CENTER;
    }

    private HPos getColumnHalignment(int columnIndex) {
        if (columnIndex < getColumnConstraints().size()) {
            ColumnConstraints constraints = getColumnConstraints().get(columnIndex);
            if (constraints.getHalignment() != null) {
                return constraints.getHalignment();
            }
        }
        return HPos.LEFT;
    }

    private double getColumnMinWidth(int columnIndex) {
        if (columnIndex < getColumnConstraints().size()) {
            ColumnConstraints constraints = getColumnConstraints().get(columnIndex);
            return constraints.getMinWidth();

        }
        return USE_COMPUTED_SIZE;
    }

    private double getRowMinHeight(int rowIndex) {
        if (rowIndex < getRowConstraints().size()) {
            RowConstraints constraints = getRowConstraints().get(rowIndex);
            return constraints.getMinHeight();
        }
        return USE_COMPUTED_SIZE;
    }

    private double getColumnMaxWidth(int columnIndex) {
        if (columnIndex < getColumnConstraints().size()) {
            ColumnConstraints constraints = getColumnConstraints().get(columnIndex);
            return constraints.getMaxWidth();

        }
        return USE_COMPUTED_SIZE;
    }

    private double getColumnPrefWidth(int columnIndex) {
        if (columnIndex < getColumnConstraints().size()) {
            ColumnConstraints constraints = getColumnConstraints().get(columnIndex);
            return constraints.getPrefWidth();

        }
        return USE_COMPUTED_SIZE;
    }

    private double getRowPrefHeight(int rowIndex) {
        if (rowIndex < getRowConstraints().size()) {
            RowConstraints constraints = getRowConstraints().get(rowIndex);
            return constraints.getPrefHeight();

        }
        return USE_COMPUTED_SIZE;
    }

    private double getRowMaxHeight(int rowIndex) {
        if (rowIndex < getRowConstraints().size()) {
            RowConstraints constraints = getRowConstraints().get(rowIndex);
            return constraints.getMaxHeight();
        }
        return USE_COMPUTED_SIZE;
    }

    private boolean shouldRowFillHeight(int rowIndex) {
        if (rowIndex < getRowConstraints().size()) {
            return getRowConstraints().get(rowIndex).isFillHeight();
        }
        return true;
    }

    private boolean shouldColumnFillWidth(int columnIndex) {
        if (columnIndex < getColumnConstraints().size()) {
            return getColumnConstraints().get(columnIndex).isFillWidth();
        }
        return true;
    }

    private double getTotalWidthOfNodeColumns(Node child, double[] widths) {
        if (getNodeColumnSpan(child) == 1) {
            return widths[getNodeColumnIndex(child)];
        } else {
            double total = 0;
            for (int i = getNodeColumnIndex(child), last = getNodeColumnEndConvertRemaining(child); i <= last; ++i) {
                total += widths[i];
            }
            return total;
        }
    }

    private CompositeSize computeMaxHeights() {
        if (rowMaxHeight == null) {
            rowMaxHeight = createCompositeRows(Double.MAX_VALUE); // Do not restrict the row (to allow grow). The
                                                                  // Nodes will be restricted to their computed size
                                                                  // in Region.layoutInArea call
            final ObservableList<RowConstraints> rowConstr = getRowConstraints();
            CompositeSize prefHeights = null;
            for (int i = 0; i < rowConstr.size(); ++i) {
                final RowConstraints curConstraint = rowConstr.get(i);
                final double constrMaxH = curConstraint.getMaxHeight();
                if (constrMaxH == USE_PREF_SIZE) {
                    if (prefHeights == null) {
                        prefHeights = computePrefHeights(null);
                    }
                    rowMaxHeight.setPresetSize(i, prefHeights.getSize(i));
                } else if (constrMaxH != USE_COMPUTED_SIZE) {
                    final double maxRowHeight = snapSizeY(constrMaxH);
                    final double constrMinH = curConstraint.getMinHeight();
                    if (constrMinH >= 0 ) {
                        final double min = snapSizeY(curConstraint.getMinHeight());
                        rowMaxHeight.setPresetSize(i, boundedSize(min, maxRowHeight, maxRowHeight));
                    } else {
                        rowMaxHeight.setPresetSize(i, maxRowHeight);
                    }
                }
            }
        }
        return rowMaxHeight;
    }

    private CompositeSize computePrefHeights(double[] widths) {
        CompositeSize result;
        if (widths == null) {
            if (rowPrefHeight != null) {
                return rowPrefHeight;
            }
            rowPrefHeight = createCompositeRows(0);
            result = rowPrefHeight;
        } else {
            result = createCompositeRows(0);
        }

        final ObservableList<RowConstraints> rowConstr = getRowConstraints();
        for (int i = 0; i < rowConstr.size(); ++i) {
            final RowConstraints curConstraint = rowConstr.get(i);
            final double constrMinH = curConstraint.getMinHeight();
            final double constrPrefH = curConstraint.getPrefHeight();
            if (constrPrefH != USE_COMPUTED_SIZE) {
                final double prefRowHeight = snapSizeY(constrPrefH);
                final double constrMaxH = curConstraint.getMaxHeight();
                if (constrMinH >= 0 || constrMaxH >= 0) {
                    final double min = (constrMinH < 0 ? 0 : snapSizeY(constrMinH));
                    final double max = (constrMaxH < 0 ? Double.POSITIVE_INFINITY : snapSizeY(constrMaxH));
                    result.setPresetSize(i, boundedSize(min, prefRowHeight, max));
                } else {
                    result.setPresetSize(i, prefRowHeight);
                }
            } else if (constrMinH > 0){
                result.setSize(i, snapSizeY(constrMinH));
            }
        }
        List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            int start = getNodeRowIndex(child);
            int end = getNodeRowEndConvertRemaining(child);
            double childPrefAreaHeight = computeChildPrefAreaHeight(child, isNodePositionedByBaseline(child) ? rowPrefBaselineComplement[start] : -1, getMargin(child),
                    widths == null ? -1 : getTotalWidthOfNodeColumns(child, widths));
            if (start == end && !result.isPreset(start)) {
                double min = getRowMinHeight(start);
                double max = getRowMaxHeight(start);
                result.setMaxSize(start, boundedSize(min < 0 ? 0 : min, childPrefAreaHeight, max < 0 ? Double.MAX_VALUE : max));
            } else if (start != end){
                result.setMaxMultiSize(start, end + 1, childPrefAreaHeight);
            }
        }
        return result;
    }

    private CompositeSize computeMinHeights(double[] widths) {
        CompositeSize result;
        if (widths == null) {
            if (rowMinHeight != null) {
                return rowMinHeight;
            }
            rowMinHeight = createCompositeRows(0);
            result = rowMinHeight;
        } else {
            result = createCompositeRows(0);
        }

        final ObservableList<RowConstraints> rowConstr = getRowConstraints();
        CompositeSize prefHeights = null;
        for (int i = 0; i < rowConstr.size(); ++i) {
            final double constrMinH = rowConstr.get(i).getMinHeight();
            if (constrMinH == USE_PREF_SIZE) {
                if (prefHeights == null) {
                    prefHeights = computePrefHeights(widths);
                }
                result.setPresetSize(i, prefHeights.getSize(i));
            } else if (constrMinH != USE_COMPUTED_SIZE) {
                result.setPresetSize(i, snapSizeY(constrMinH));
            }
        }
        List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            int start = getNodeRowIndex(child);
            int end = getNodeRowEndConvertRemaining(child);
            double childMinAreaHeight = computeChildMinAreaHeight(child, isNodePositionedByBaseline(child) ? rowMinBaselineComplement[start] : -1, getMargin(child),
                             widths == null ? -1 : getTotalWidthOfNodeColumns(child, widths));
            if (start == end && !result.isPreset(start)) {
                result.setMaxSize(start, childMinAreaHeight);
            } else if (start != end){
                result.setMaxMultiSize(start, end + 1, childMinAreaHeight);
            }
        }
        return result;
    }

    private double getTotalHeightOfNodeRows(Node child, double[] heights) {
        if (getNodeRowSpan(child) == 1) {
            return heights[getNodeRowIndex(child)];
        } else {
            double total = 0;
            for (int i = getNodeRowIndex(child), last = getNodeRowEndConvertRemaining(child); i <= last; ++i) {
                total += heights[i];
            }
            return total;
        }
    }

    private CompositeSize computeMaxWidths() {
        if (columnMaxWidth == null) {
            columnMaxWidth = createCompositeColumns(Double.MAX_VALUE);// Do not restrict the column (to allow grow). The
                                                                      // Nodes will be restricted to their computed size
                                                                      // in Region.layoutInArea call
            final ObservableList<ColumnConstraints> columnConstr = getColumnConstraints();
            CompositeSize prefWidths = null;
            for (int i = 0; i < columnConstr.size(); ++i) {
                final ColumnConstraints curConstraint = columnConstr.get(i);
                final double constrMaxW = curConstraint.getMaxWidth();
                if (constrMaxW == USE_PREF_SIZE) {
                    if (prefWidths == null) {
                        prefWidths = computePrefWidths(null);
                    }
                    columnMaxWidth.setPresetSize(i, prefWidths.getSize(i));
                } else if (constrMaxW != USE_COMPUTED_SIZE) {
                    double maxColumnWidth = snapSizeX(constrMaxW);
                    final double constrMinW = curConstraint.getMinWidth();
                    if (constrMinW >= 0) {
                        final double min = snapSizeX(constrMinW);
                        columnMaxWidth.setPresetSize(i, boundedSize(min, maxColumnWidth, maxColumnWidth));
                    } else {
                        columnMaxWidth.setPresetSize(i, maxColumnWidth);
                    }
                }
            }
        }
        return columnMaxWidth;
    }

    private CompositeSize computePrefWidths(double[] heights) {
        CompositeSize result;
        if (heights == null) {
            if (columnPrefWidth != null) {
                return columnPrefWidth;
            }
            columnPrefWidth = createCompositeColumns(0);
            result = columnPrefWidth;
        } else {
            result = createCompositeColumns(0);
        }

        final ObservableList<ColumnConstraints> columnConstr = getColumnConstraints();
        for (int i = 0; i < columnConstr.size(); ++i) {
            final ColumnConstraints curConstraint = columnConstr.get(i);
            final double constrPrefW = curConstraint.getPrefWidth();
            final double constrMinW = curConstraint.getMinWidth();
            if (constrPrefW != USE_COMPUTED_SIZE) {
                final double prefColumnWidth = snapSizeX(constrPrefW);
                final double constrMaxW = curConstraint.getMaxWidth();
                if (constrMinW >= 0 || constrMaxW >= 0) {
                    double min = (constrMinW < 0 ? 0 : snapSizeX(constrMinW));
                    final double max = (constrMaxW < 0 ? Double.POSITIVE_INFINITY : snapSizeX(constrMaxW));
                    result.setPresetSize(i, boundedSize(min < 0 ? 0 : min,
                            prefColumnWidth,
                            max < 0 ? Double.POSITIVE_INFINITY : max));
                } else {
                    result.setPresetSize(i, prefColumnWidth);
                }
            } else if (constrMinW > 0){
                result.setSize(i, snapSizeX(constrMinW));
            }
        }
        List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            int start = getNodeColumnIndex(child);
            int end = getNodeColumnEndConvertRemaining(child);
            if (start == end && !result.isPreset(start)) {
                double min = getColumnMinWidth(start);
                double max = getColumnMaxWidth(start);
                result.setMaxSize(start, boundedSize(min < 0 ? 0 : min, computeChildPrefAreaWidth(child,
                        getBaselineComplementForChild(child), getMargin(child),
                        heights == null ? -1 : getTotalHeightOfNodeRows(child, heights), false),
                        max < 0 ? Double.MAX_VALUE : max));
            } else if (start != end) {
                result.setMaxMultiSize(start, end + 1, computeChildPrefAreaWidth(child, getBaselineComplementForChild(child),
                        getMargin(child),
                        heights == null ? -1 : getTotalHeightOfNodeRows(child, heights), false));
            }
        }
        return result;
    }

    private CompositeSize computeMinWidths(double[] heights) {
        CompositeSize result;
        if (heights == null) {
            if (columnMinWidth != null) {
                return columnMinWidth;
            }
            columnMinWidth = createCompositeColumns(0);
            result = columnMinWidth;
        } else {
            result = createCompositeColumns(0);
        }

        final ObservableList<ColumnConstraints> columnConstr = getColumnConstraints();
        CompositeSize prefWidths = null;
        for (int i = 0; i < columnConstr.size(); ++i) {
            final double constrMinW = columnConstr.get(i).getMinWidth();
            if (constrMinW == USE_PREF_SIZE) {
                if (prefWidths == null) {
                    prefWidths = computePrefWidths(heights);
                }
                result.setPresetSize(i, prefWidths.getSize(i));
            } else if (constrMinW != USE_COMPUTED_SIZE) {
                result.setPresetSize(i, snapSizeX(constrMinW));
            }
        }
        List<Node> managed = getManagedChildren();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            int start = getNodeColumnIndex(child);
            int end = getNodeColumnEndConvertRemaining(child);
            if (start == end && !result.isPreset(start)) {
                result.setMaxSize(start, computeChildMinAreaWidth(child, getBaselineComplementForChild(child),
                        getMargin(child),
                        heights == null ? -1 : getTotalHeightOfNodeRows(child, heights),false));
            } else if (start != end){
                result.setMaxMultiSize(start, end + 1, computeChildMinAreaWidth(child, getBaselineComplementForChild(child),
                        getMargin(child),
                        heights == null ? -1 : getTotalHeightOfNodeRows(child, heights), false));
            }
        }
        return result;
    }

    private CompositeSize computeHeightsToFit(double height) {
        assert(height != -1);
        final CompositeSize heights;
        if (rowPercentTotal == 100) {
            // all rows defined by percentage, no need to compute pref heights
            heights = createCompositeRows(0);
        } else {
            heights = (CompositeSize) computePrefHeights(null).clone();
        }
        adjustRowHeights(heights, height);
        return heights;
    }

    private CompositeSize computeWidthsToFit(double width) {
        assert(width != -1);
        final CompositeSize widths;
        if (columnPercentTotal == 100) {
            // all columns defined by percentage, no need to compute pref widths
            widths = createCompositeColumns(0);
        } else {
            widths = (CompositeSize) computePrefWidths(null).clone();
        }
        adjustColumnWidths(widths, width);
        return widths;
    }

    /**
     *
     * @return null unless one of its children has a content bias.
     */
    @Override public Orientation getContentBias() {
        computeGridMetrics();
        return bias;
    }

    @Override public void requestLayout() {
        // RT-18878: Do not update metrics dirty if we are performing layout.
        // If metricsDirty is set true during a layout pass the next call to computeGridMetrics()
        // will clear all the cell bounds resulting in out of date info until the
        // next layout pass.
        if (performingLayout) {
            return;
        } else if (metricsDirty) {
            super.requestLayout();
            return;
        }
        metricsDirty = true;
        bias = null;
        rowGrow = null;
        rowMinHeight = rowPrefHeight = rowMaxHeight = null;
        columnGrow = null;
        columnMinWidth = columnPrefWidth = columnMaxWidth = null;
        rowMinBaselineComplement = rowPrefBaselineComplement = rowMaxBaselineComplement = null;
        super.requestLayout();
    }

    @Override protected void layoutChildren() {
        performingLayout = true;
        try {
            final double snaphgap = snapSpaceX(getHgap());
            final double snapvgap = snapSpaceY(getVgap());
            final double top = snapSpaceY(getInsets().getTop());
            final double bottom = snapSpaceY(getInsets().getBottom());
            final double left = snapSpaceX(getInsets().getLeft());
            final double right = snapSpaceX(getInsets().getRight());

            final double width = getWidth();
            final double height = getHeight();
            final double contentHeight = height - top - bottom;
            final double contentWidth = width - left - right;
            double columnTotal;
            double rowTotal;
            computeGridMetrics();

            Orientation contentBias = getContentBias();
            CompositeSize heights;
            final CompositeSize widths;
            if (contentBias == null) {
                heights = (CompositeSize) computePrefHeights(null).clone();
                widths = (CompositeSize) computePrefWidths(null).clone();
                rowTotal = adjustRowHeights(heights, height);
                columnTotal = adjustColumnWidths(widths, width);
            } else if (contentBias == Orientation.HORIZONTAL) {
                widths = (CompositeSize) computePrefWidths(null).clone();
                columnTotal = adjustColumnWidths(widths, width);
                heights = computePrefHeights(widths.asArray());
                rowTotal = adjustRowHeights(heights, height);
            } else {
                heights = (CompositeSize) computePrefHeights(null).clone();
                rowTotal = adjustRowHeights(heights, height);
                widths = computePrefWidths(heights.asArray());
                columnTotal = adjustColumnWidths(widths, width);
            }

            final double x = left + computeXOffset(contentWidth, columnTotal, getAlignmentInternal().getHpos());
            final double y = top + computeYOffset(contentHeight, rowTotal, getAlignmentInternal().getVpos());
            final List<Node> managed = getManagedChildren();

            double[] baselineOffsets = createDoubleArray(numRows, -1);

            for (int i = 0, size = managed.size(); i < size; i++) {
                final Node child = managed.get(i);
                final int rowIndex = getNodeRowIndex(child);
                int columnIndex = getNodeColumnIndex(child);
                int colspan = getNodeColumnSpan(child);
                if (colspan == REMAINING) {
                    colspan = widths.getLength() - columnIndex;
                }
                int rowspan = getNodeRowSpan(child);
                if (rowspan == REMAINING) {
                    rowspan = heights.getLength() - rowIndex;
                }
                double areaX = x;
                for (int j = 0; j < columnIndex; j++) {
                    areaX += widths.getSize(j) + snaphgap;
                }
                double areaY = y;
                for (int j = 0; j < rowIndex; j++) {
                    areaY += heights.getSize(j) + snapvgap;
                }
                double areaW = widths.getSize(columnIndex);
                for (int j = 2; j <= colspan; j++) {
                    areaW += widths.getSize(columnIndex + j - 1) + snaphgap;
                }
                double areaH = heights.getSize(rowIndex);
                for (int j = 2; j <= rowspan; j++) {
                    areaH += heights.getSize(rowIndex + j - 1) + snapvgap;
                }

                HPos halign = getHalignment(child);
                VPos valign = getValignment(child);
                Boolean fillWidth = isFillWidth(child);
                Boolean fillHeight = isFillHeight(child);

                if (halign == null) {
                    halign = getColumnHalignment(columnIndex);
                }
                if (valign == null) {
                    valign = getRowValignment(rowIndex);
                }
                if (fillWidth == null) {
                    fillWidth = shouldColumnFillWidth(columnIndex);
                }
                if (fillHeight == null) {
                    fillHeight = shouldRowFillHeight(rowIndex);
                }

                double baselineOffset = 0;
                if (valign == VPos.BASELINE) {
                    if (baselineOffsets[rowIndex] == -1) {
                        baselineOffsets[rowIndex] = getAreaBaselineOffset(rowBaseline[rowIndex],
                                marginAccessor,
                                t -> {
                                    Node n = rowBaseline[rowIndex].get(t);
                                    int c = getNodeColumnIndex(n);
                                    int cs = getNodeColumnSpan(n);
                                    if (cs == REMAINING) {
                                        cs = widths.getLength() - c;
                                    }
                                    double w = widths.getSize(c);
                                    for (int j = 2; j <= cs; j++) {
                                        w += widths.getSize(c + j - 1) + snaphgap;
                                    }
                                    return w;
                                },
                                areaH,
                                t -> {
                                    Boolean b = isFillHeight(child);
                                    if (b != null) {
                                        return b;
                                    }
                                    return shouldRowFillHeight(getNodeRowIndex(child));
                                }, rowMinBaselineComplement[rowIndex]
                        );
                    }
                    baselineOffset = baselineOffsets[rowIndex];
                }

                Insets margin = getMargin(child);
                layoutInArea(child, areaX, areaY, areaW, areaH,
                        baselineOffset,
                        margin,
                        fillWidth, fillHeight,
                        halign, valign);
            }
            layoutGridLines(widths, heights, x, y, rowTotal, columnTotal);
            currentHeights = heights;
            currentWidths = widths;
        } finally {
            performingLayout = false;
        }
    }

    private double adjustRowHeights(final CompositeSize heights, double height) {
        assert(height != -1);
        final double snapvgap = snapSpaceY(getVgap());
        final double top = snapSpaceY(getInsets().getTop());
        final double bottom = snapSpaceY(getInsets().getBottom());
        final double vgaps = snapvgap * (getNumberOfRows() - 1);
        final double contentHeight = height - top - bottom;

        // if there are percentage rows, give them their percentages first
        if (rowPercentTotal > 0) {
            double remainder = 0;
            for (int i = 0; i < rowPercentHeight.length; i++) {
                if (rowPercentHeight[i] >= 0) {
                    double size = (contentHeight - vgaps) * (rowPercentHeight[i]/100);
                    double floor = Math.floor(size);
                    remainder += size - floor;

                    // snap size to integer boundary based on the computed remainder as we loop through the rows.
                    size = floor;
                    if (remainder >= 0.5) {
                        size++;
                        remainder = (-1.0) + remainder;
                    }
                    heights.setSize(i, size);
                }
            }
        }
        double rowTotal = heights.computeTotal();
        if (rowPercentTotal < 100) {
            double heightAvailable = height - top - bottom - rowTotal;
            // now that both fixed and percentage rows have been computed, divy up any surplus or deficit
            if (heightAvailable != 0) {
                // maybe grow or shrink row heights
                double remaining = growToMultiSpanPreferredHeights(heights, heightAvailable);
                remaining = growOrShrinkRowHeights(heights, Priority.ALWAYS, remaining);
                remaining = growOrShrinkRowHeights(heights, Priority.SOMETIMES, remaining);
                rowTotal += (heightAvailable - remaining);
            }
        }

        return rowTotal;
    }

    private double growToMultiSpanPreferredHeights(CompositeSize heights, double extraHeight) {
        if (extraHeight <= 0) {
            return extraHeight;
        }

        Set<Integer> rowsAlways = new TreeSet<>();
        Set<Integer> rowsSometimes = new TreeSet<>();
        Set<Integer> lastRows = new TreeSet<>();
        for (Entry<Interval, Double> ms : heights.multiSizes()) {
            final Interval interval = ms.getKey();
            for (int i = interval.begin; i < interval.end; ++i) {
                if (rowPercentHeight[i] < 0) {
                    switch (rowGrow[i]) {
                        case ALWAYS:
                            rowsAlways.add(i);
                            break;
                        case SOMETIMES:
                            rowsSometimes.add(i);
                            break;
                    }
                }
            }
            if (rowPercentHeight[interval.end - 1] < 0) {
                lastRows.add(interval.end - 1);
            }
        }

        double remaining = extraHeight;

        while (rowsAlways.size() > 0 && remaining > rowsAlways.size()) {
            double rowPortion = Math.floor(remaining / rowsAlways.size());
            for (Iterator<Integer> it = rowsAlways.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfRow = getRowMaxHeight(i);
                double prefOfRow = getRowPrefHeight(i);
                double actualPortion = rowPortion;

                for (Entry<Interval, Double> ms : heights.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.contains(i)) {
                        int intervalRows = 0;
                        for (int j = interval.begin; j < interval.end; ++j) {
                            if (rowsAlways.contains(j)) {
                                intervalRows++;
                            }
                        }
                        double curLength = heights.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.floor(Math.max(0, (ms.getValue() - curLength) / intervalRows)),
                                actualPortion);
                    }
                }

                final double current = heights.getSize(i);
                double bounded = maxOfRow >= 0 ? boundedSize(0, current + actualPortion, maxOfRow) :
                        maxOfRow == USE_PREF_SIZE && prefOfRow > 0 ? boundedSize(0, current + actualPortion, prefOfRow) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                heights.setSize(i, bounded);
            }
        }

        while (rowsSometimes.size() > 0 && remaining > rowsSometimes.size()) {
            double colPortion = Math.floor(remaining / rowsSometimes.size());
            for (Iterator<Integer> it = rowsSometimes.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfRow = getRowMaxHeight(i);
                double prefOfRow = getRowPrefHeight(i);
                double actualPortion = colPortion;

                for (Entry<Interval, Double> ms : heights.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.contains(i)) {
                        int intervalRows = 0;
                        for (int j = interval.begin; j < interval.end; ++j) {
                            if (rowsSometimes.contains(j)) {
                                intervalRows++;
                            }
                        }
                        double curLength = heights.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.floor(Math.max(0, (ms.getValue() - curLength) / intervalRows)),
                                actualPortion);
                    }
                }

                final double current = heights.getSize(i);
                double bounded = maxOfRow >= 0 ? boundedSize(0, current + actualPortion, maxOfRow) :
                        maxOfRow == USE_PREF_SIZE && prefOfRow > 0 ? boundedSize(0, current + actualPortion, prefOfRow) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                heights.setSize(i, bounded);
            }
        }


        while (lastRows.size() > 0 && remaining > lastRows.size()) {
            double colPortion = Math.floor(remaining / lastRows.size());
            for (Iterator<Integer> it = lastRows.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfRow = getRowMaxHeight(i);
                double prefOfRow = getRowPrefHeight(i);
                double actualPortion = colPortion;

                for (Entry<Interval, Double> ms : heights.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.end - 1 == i) {
                        double curLength = heights.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.max(0, ms.getValue() - curLength),
                                actualPortion);
                    }
                }

                final double current = heights.getSize(i);
                double bounded = maxOfRow >= 0 ? boundedSize(0, current + actualPortion, maxOfRow) :
                        maxOfRow == USE_PREF_SIZE && prefOfRow > 0 ? boundedSize(0, current + actualPortion, prefOfRow) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                heights.setSize(i, bounded);
            }
        }
        return remaining;
    }

    private double growOrShrinkRowHeights(CompositeSize heights, Priority priority, double extraHeight) {
        final boolean shrinking = extraHeight < 0;
        List<Integer> adjusting = new ArrayList<>();

        for (int i = 0; i < rowGrow.length; i++) {
            if (rowPercentHeight[i] < 0 && (shrinking || rowGrow[i] == priority)) {
                adjusting.add(i);
            }
        }

        double available = extraHeight; // will be negative in shrinking case
        boolean handleRemainder = false;
        double portion = 0;

        // RT-25684: We have to be careful that when subtracting change
        // that we don't jump right past 0 - this leads to an infinite
        // loop
        final boolean wasPositive = available >= 0.0;
        boolean isPositive = wasPositive;

        CompositeSize limitSize = shrinking? computeMinHeights(null) :
                            computeMaxHeights();
        while (available != 0 && wasPositive == isPositive && adjusting.size() > 0) {
            if (!handleRemainder) {
                portion = available > 0 ? Math.floor(available / adjusting.size()) :
                        Math.ceil(available / adjusting.size()); // negative in shrinking case
            }
            if (portion != 0) {
                for (Iterator<Integer> i = adjusting.iterator(); i.hasNext();) {
                    final int index = i.next();
                    double limit = snapSpaceY(limitSize.getProportionalMinOrMaxSize(index, shrinking))
                            - heights.getSize(index); // negative in shrinking case
                    if (shrinking && limit > 0
                            || !shrinking && limit < 0) { // Limit completely if current size
                                                 // (originally based on preferred) already passed the computed limit
                        limit = 0;
                    }
                    final double change = Math.abs(limit) <= Math.abs(portion)? limit : portion;
                    heights.addSize(index, change);
                    available -= change;
                    isPositive = available >= 0.0;
                    if (Math.abs(change) < Math.abs(portion)) {
                        i.remove();
                    }
                    if (available == 0) {
                        break;
                    }
                }
             } else {
                // Handle the remainder
                portion = (int)(available) % adjusting.size();
                if (portion == 0) {
                    break;
                } else {
                    // We have a remainder evenly distribute it.
                    portion = shrinking ? -1 : 1;
                    handleRemainder = true;
                }
            }
        }

        return available; // might be negative in shrinking case
    }

    private double adjustColumnWidths(final CompositeSize widths, double width) {
        assert(width != -1);
        final double snaphgap = snapSpaceX(getHgap());
        final double left = snapSpaceX(getInsets().getLeft());
        final double right = snapSpaceX(getInsets().getRight());
        final double hgaps = snaphgap * (getNumberOfColumns() - 1);
        final double contentWidth = width - left - right;

        // if there are percentage rows, give them their percentages first
        if (columnPercentTotal > 0) {
            double remainder = 0;
            for (int i = 0; i < columnPercentWidth.length; i++) {
                if (columnPercentWidth[i] >= 0) {
                    double size = (contentWidth - hgaps) * (columnPercentWidth[i]/100);
                    double floor = Math.floor(size);
                    remainder += size - floor;

                    // snap size to integer boundary based on the computed remainder as we loop through the columns.
                    size = floor;
                    if (remainder >= 0.5) {
                        size++;
                        remainder = (-1.0) + remainder;
                    }
                    widths.setSize(i, size);
                }
            }
        }

        double columnTotal = widths.computeTotal();
        if (columnPercentTotal < 100) {
            double widthAvailable = width - left - right - columnTotal;
            // now that both fixed and percentage rows have been computed, divy up any surplus or deficit
            if (widthAvailable != 0) {
                // maybe grow or shrink row heights
                double remaining = growToMultiSpanPreferredWidths(widths, widthAvailable);
                remaining = growOrShrinkColumnWidths(widths, Priority.ALWAYS, remaining);
                remaining = growOrShrinkColumnWidths(widths, Priority.SOMETIMES, remaining);
                columnTotal += (widthAvailable - remaining);
            }
        }
        return columnTotal;
    }

    private double growToMultiSpanPreferredWidths(CompositeSize widths, double extraWidth) {
        if (extraWidth <= 0) {
            return extraWidth;
        }

        Set<Integer> columnsAlways = new TreeSet<>();
        Set<Integer> columnsSometimes = new TreeSet<>();
        Set<Integer> lastColumns = new TreeSet<>();
        for (Entry<Interval, Double> ms : widths.multiSizes()) {
            final Interval interval = ms.getKey();
            for (int i = interval.begin; i < interval.end; ++i) {
                if (columnPercentWidth[i] < 0) {
                    switch (columnGrow[i]) {
                        case ALWAYS:
                            columnsAlways.add(i);
                            break;
                        case SOMETIMES:
                            columnsSometimes.add(i);
                            break;
                    }
                }
            }
            if (columnPercentWidth[interval.end - 1] < 0) {
                lastColumns.add(interval.end - 1);
            }
        }

        double remaining = extraWidth;

        while (columnsAlways.size() > 0 && remaining > columnsAlways.size()) {
            double colPortion = Math.floor(remaining / columnsAlways.size());
            for (Iterator<Integer> it = columnsAlways.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfColumn = getColumnMaxWidth(i);
                double prefOfColumn = getColumnPrefWidth(i);
                double actualPortion = colPortion;

                for (Entry<Interval, Double> ms : widths.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.contains(i)) {
                        int intervalColumns = 0;
                        for (int j = interval.begin; j < interval.end; ++j) {
                            if (columnsAlways.contains(j)) {
                                intervalColumns++;
                            }
                        }
                        double curLength = widths.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.floor(Math.max(0, (ms.getValue() - curLength) / intervalColumns)),
                                actualPortion);
                    }
                }

                final double current = widths.getSize(i);
                double bounded = maxOfColumn >= 0 ? boundedSize(0, current + actualPortion, maxOfColumn) :
                        maxOfColumn == USE_PREF_SIZE && prefOfColumn > 0 ? boundedSize(0, current + actualPortion, prefOfColumn) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                widths.setSize(i, bounded);
            }
        }

        while (columnsSometimes.size() > 0 && remaining > columnsSometimes.size()) {
            double colPortion = Math.floor(remaining / columnsSometimes.size());
            for (Iterator<Integer> it = columnsSometimes.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfColumn = getColumnMaxWidth(i);
                double prefOfColumn = getColumnPrefWidth(i);
                double actualPortion = colPortion;

                for (Entry<Interval, Double> ms : widths.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.contains(i)) {
                        int intervalColumns = 0;
                        for (int j = interval.begin; j < interval.end; ++j) {
                            if (columnsSometimes.contains(j)) {
                                intervalColumns++;
                            }
                        }
                        double curLength = widths.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.floor(Math.max(0, (ms.getValue() - curLength) / intervalColumns)),
                                actualPortion);
                    }
                }

                final double current = widths.getSize(i);
                double bounded = maxOfColumn >= 0 ? boundedSize(0, current + actualPortion, maxOfColumn) :
                        maxOfColumn == USE_PREF_SIZE && prefOfColumn > 0 ? boundedSize(0, current + actualPortion, prefOfColumn) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                widths.setSize(i, bounded);
            }
        }


        while (lastColumns.size() > 0 && remaining > lastColumns.size()) {
            double colPortion = Math.floor(remaining / lastColumns.size());
            for (Iterator<Integer> it = lastColumns.iterator(); it.hasNext();) {
                int i = it.next();
                double maxOfColumn = getColumnMaxWidth(i);
                double prefOfColumn = getColumnPrefWidth(i);
                double actualPortion = colPortion;

                for (Entry<Interval, Double> ms : widths.multiSizes()) {
                    final Interval interval = ms.getKey();
                    if (interval.end - 1 == i) {
                        double curLength = widths.computeTotal(interval.begin, interval.end);
                        actualPortion = Math.min(Math.max(0, ms.getValue() - curLength),
                                actualPortion);
                    }
                }

                final double current = widths.getSize(i);
                double bounded = maxOfColumn >= 0 ? boundedSize(0, current + actualPortion, maxOfColumn) :
                        maxOfColumn == USE_PREF_SIZE && prefOfColumn > 0 ? boundedSize(0, current + actualPortion, prefOfColumn) :
                        current + actualPortion;
                final double portionUsed = bounded - current;
                remaining -= portionUsed;
                if (portionUsed != actualPortion || portionUsed == 0) {
                    it.remove();
                }
                widths.setSize(i, bounded);
            }
        }
        return remaining;
    }

    private double growOrShrinkColumnWidths(CompositeSize widths, Priority priority, double extraWidth) {
        if (extraWidth == 0) {
            return 0;
        }
        final boolean shrinking = extraWidth < 0;
        List<Integer> adjusting = new ArrayList<>();

        for (int i = 0; i < columnGrow.length; i++) {
            if (columnPercentWidth[i] < 0 && (shrinking || columnGrow[i] == priority)) {
                adjusting.add(i);
            }
        }

        double available = extraWidth; // will be negative in shrinking case
        boolean handleRemainder = false;
        double portion = 0;

        // RT-25684: We have to be careful that when subtracting change
        // that we don't jump right past 0 - this leads to an infinite
        // loop
        final boolean wasPositive = available >= 0.0;
        boolean isPositive = wasPositive;

        CompositeSize limitSize = shrinking? computeMinWidths(null) :
                            computeMaxWidths();
        while (available != 0 && wasPositive == isPositive && adjusting.size() > 0) {
            if (!handleRemainder) {
                portion = available > 0 ? Math.floor(available / adjusting.size()) :
                        Math.ceil(available / adjusting.size()); // negative in shrinking case
            }
            if (portion != 0) {
                for (Iterator<Integer> i = adjusting.iterator(); i.hasNext();) {
                    final int index = i.next();
                    double limit = snapSpaceX(limitSize.getProportionalMinOrMaxSize(index, shrinking))
                            - widths.getSize(index); // negative in shrinking case
                    if (shrinking && limit > 0
                            || !shrinking && limit < 0) { // Limit completely if current size
                                                 // (originally based on preferred) already passed the computed limit
                        limit = 0;
                    }
                    final double change = Math.abs(limit) <= Math.abs(portion)? limit : portion;
                    widths.addSize(index, change);
                    available -= change;
                    isPositive = available >= 0.0;
                    if (Math.abs(change) < Math.abs(portion)) {
                        i.remove();
                    }
                    if (available == 0) {
                        break;
                    }
                }
            } else {
                // Handle the remainder
                portion = (int)(available) % adjusting.size();
                if (portion == 0) {
                    break;
                } else {
                    // We have a remainder evenly distribute it.
                    portion = shrinking ? -1 : 1;
                    handleRemainder = true;
                }
            }
        }

        return available; // might be negative in shrinking case
    }

    private void layoutGridLines(CompositeSize columnWidths, CompositeSize rowHeights, double x, double y, double columnHeight, double rowWidth) {
        if (!isGridLinesVisible()) {
            return;
        }
        if (!gridLines.getChildren().isEmpty()) {
            gridLines.getChildren().clear();
        }
        double hGap = snapSpaceX(getHgap());
        double vGap = snapSpaceY(getVgap());

        // create vertical lines
        double linex = x;
        double liney = y;
        for (int i = 0; i <= columnWidths.getLength(); i++) {
             gridLines.getChildren().add(createGridLine(linex, liney, linex, liney + columnHeight));
             if (i > 0 && i < columnWidths.getLength() && hGap != 0) {
                 linex += hGap;
                 gridLines.getChildren().add(createGridLine(linex, liney, linex, liney + columnHeight));
             }
             if (i < columnWidths.getLength()) {
                 linex += columnWidths.getSize(i);
             }
        }
        // create horizontal lines
        linex = x;
        for (int i = 0; i <= rowHeights.getLength(); i++) {
            gridLines.getChildren().add(createGridLine(linex, liney, linex + rowWidth, liney));
            if (i > 0 && i < rowHeights.getLength() && vGap != 0) {
                liney += vGap;
                gridLines.getChildren().add(createGridLine(linex, liney, linex + rowWidth, liney));
            }
            if (i < rowHeights.getLength()) {
                liney += rowHeights.getSize(i);
            }
        }
    }

    private Line createGridLine(double startX, double startY, double endX, double endY) {
         Line line = new Line();
         line.setStartX(startX);
         line.setStartY(startY);
         line.setEndX(endX);
         line.setEndY(endY);
         line.setStroke(GRID_LINE_COLOR);
         line.setStrokeDashOffset(GRID_LINE_DASH);

         return line;
    }

    /**
     * Returns a string representation of this {@code GridPane} object.
     * @return a string representation of this {@code GridPane} object.
     */
    @Override public String toString() {
        return "Grid hgap="+getHgap()+", vgap="+getVgap()+", alignment="+getAlignment();
    }

    private CompositeSize createCompositeRows(double initSize) {
        return new CompositeSize(getNumberOfRows(), rowPercentHeight, rowPercentTotal,
                snapSpaceY(getVgap()), initSize);
    }

    private CompositeSize createCompositeColumns(double initSize) {
        return new CompositeSize(getNumberOfColumns(), columnPercentWidth, columnPercentTotal,
                snapSpaceX(getHgap()), initSize);
    }

    private int getNodeRowEndConvertRemaining(Node child) {
        int rowSpan = getNodeRowSpan(child);
        return rowSpan != REMAINING? getNodeRowIndex(child) + rowSpan - 1 : getNumberOfRows() - 1;
    }

    private int getNodeColumnEndConvertRemaining(Node child) {
        int columnSpan = getNodeColumnSpan(child);
        return columnSpan != REMAINING? getNodeColumnIndex(child) + columnSpan - 1 : getNumberOfColumns() - 1;
    }


    // This methods are inteded to be used by GridPaneDesignInfo
    private CompositeSize currentHeights;
    private CompositeSize currentWidths;

    double[][] getGrid() {
        if (currentHeights == null || currentWidths == null) {
            return null;
        }
        return new double[][] {currentWidths.asArray(), currentHeights.asArray()};
    }

    /***************************************************************************
     *                                                                         *
     *                         Stylesheet Handling                             *
     *                                                                         *
     **************************************************************************/

    /*
     * Super-lazy instantiation pattern from Bill Pugh.
     */
     private static class StyleableProperties {

         private static final CssMetaData<GridPane,Boolean> GRID_LINES_VISIBLE =
             new CssMetaData<GridPane,Boolean>("-fx-grid-lines-visible",
                 BooleanConverter.getInstance(), Boolean.FALSE) {

            @Override
            public boolean isSettable(GridPane node) {
                return node.gridLinesVisible == null ||
                        !node.gridLinesVisible.isBound();
            }

            @Override
            public StyleableProperty<Boolean> getStyleableProperty(GridPane node) {
                return (StyleableProperty<Boolean>)node.gridLinesVisibleProperty();
            }
         };

         private static final CssMetaData<GridPane,Number> HGAP =
             new CssMetaData<GridPane,Number>("-fx-hgap",
                 SizeConverter.getInstance(), 0.0){

            @Override
            public boolean isSettable(GridPane node) {
                return node.hgap == null || !node.hgap.isBound();
            }

            @Override
            public StyleableProperty<Number> getStyleableProperty(GridPane node) {
                return (StyleableProperty<Number>)node.hgapProperty();
            }

         };

         private static final CssMetaData<GridPane,Pos> ALIGNMENT =
             new CssMetaData<GridPane,Pos>("-fx-alignment",
                 new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT) {

            @Override
            public boolean isSettable(GridPane node) {
                return node.alignment == null || !node.alignment.isBound();
            }

            @Override
            public StyleableProperty<Pos> getStyleableProperty(GridPane node) {
                return (StyleableProperty<Pos>)node.alignmentProperty();
            }

         };

         private static final CssMetaData<GridPane,Number> VGAP =
             new CssMetaData<GridPane,Number>("-fx-vgap",
                 SizeConverter.getInstance(), 0.0){

            @Override
            public boolean isSettable(GridPane node) {
                return node.vgap == null || !node.vgap.isBound();
            }

            @Override
            public StyleableProperty<Number> getStyleableProperty(GridPane node) {
                return (StyleableProperty<Number>)node.vgapProperty();
            }

         };

         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
         static {

            final List<CssMetaData<? extends Styleable, ?>> styleables =
                    new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
            styleables.add(GRID_LINES_VISIBLE);
            styleables.add(HGAP);
            styleables.add(ALIGNMENT);
            styleables.add(VGAP);

            STYLEABLES = Collections.unmodifiableList(styleables);
         }
    }

    /**
     * @return The CssMetaData associated with this class, which may include the
     * CssMetaData of its superclasses.
     * @since JavaFX 8.0
     */
    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

    /**
     * {@inheritDoc}
     *
     * @since JavaFX 8.0
     */


    @Override
    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        return getClassCssMetaData();
    }

    private static final class Interval implements Comparable<Interval> {

        public final int begin;
        public final int end;

        public Interval(int begin, int end) {
            this.begin = begin;
            this.end = end;
        }

        @Override
        public int compareTo(Interval o) {
            return begin != o.begin ? begin - o.begin : end - o.end;
        }

        private boolean contains(int position) {
            return begin <= position && position < end;
        }

        private int size() {
            return end - begin;
        }

    }

    private static final class CompositeSize implements Cloneable {

        // These variables will be modified during the computations
        double singleSizes[];
        private SortedMap<Interval, Double> multiSizes;
        private BitSet preset;

        // Preset metrics for this dimension
        private final double fixedPercent[];
        private final double totalFixedPercent;
        private final double gap;

        public CompositeSize(int capacity, double fixedPercent[], double totalFixedPercent, double gap, double initSize) {
            singleSizes = new double[capacity];
            Arrays.fill(singleSizes, initSize);

            this.fixedPercent = fixedPercent;
            this.totalFixedPercent = totalFixedPercent;
            this.gap = gap;
        }

        private void setSize(int position, double size) {
            singleSizes[position] = size;
        }

        private void setPresetSize(int position, double size) {
            setSize(position, size);
            if (preset == null) {
                preset = new BitSet(singleSizes.length);
            }
            preset.set(position);
        }

        private boolean isPreset(int position) {
            if (preset == null) {
                return false;
            }
            return preset.get(position);
        }

        private void addSize(int position, double change) {
            singleSizes[position] = singleSizes[position] + change;
        }

        private double getSize(int position) {
            return singleSizes[position];
        }

        private void setMaxSize(int position, double size) {
            singleSizes[position] = Math.max(singleSizes[position], size);
        }

        private void setMultiSize(int startPosition, int endPosition, double size) {
            if (multiSizes == null) {
                multiSizes = new TreeMap<>();
            }
            Interval i = new Interval(startPosition, endPosition);
            multiSizes.put(i, size);
        }

        private Iterable<Entry<Interval, Double>> multiSizes() {
            if (multiSizes == null) {
                return Collections.EMPTY_LIST;
            }
            return multiSizes.entrySet();
        }

        private void setMaxMultiSize(int startPosition, int endPosition, double size) {
            if (multiSizes == null) {
                multiSizes = new TreeMap<>();
            }
            Interval i = new Interval(startPosition, endPosition);
            Double sz = multiSizes.get(i);
            if (sz == null) {
                multiSizes.put(i, size);
            } else {
                multiSizes.put(i, Math.max(size, sz));
            }
        }

        private double getProportionalMinOrMaxSize(int position, boolean min) {
            double result = singleSizes[position];
            if (!isPreset(position) && multiSizes != null) {
                for (Interval i : multiSizes.keySet()) {
                    if (i.contains(position)) {
                        double segment = multiSizes.get(i) / i.size();
                        double propSize = segment;
                        for (int j = i.begin; j < i.end; ++j) {
                            if (j != position) {
                                if (min ? singleSizes[j] > segment : singleSizes[j] < segment) {
                                    propSize += segment - singleSizes[j];
                                }
                            }
                        }
                        result = min ? Math.max(result, propSize) : Math.min(result, propSize);
                    }
                }
            }
            return result;
        }

        private double computeTotal(final int from, final int to) {
            double total = gap * (to - from - 1);
            for (int i = from; i < to; ++i) {
                total += singleSizes[i];
            }
            return total;
        }

        private double computeTotal() {
            return computeTotal(0, singleSizes.length);
        }

        private boolean allPreset(int begin, int end) {
            if (preset == null) {
                return false;
            }
            for (int i = begin; i < end; ++i) {
                if (!preset.get(i)) {
                    return false;
                }
            }
            return true;
        }

        private double computeTotalWithMultiSize() {
            double total = computeTotal();
            if (multiSizes != null) {
                for (Entry<Interval, Double> e: multiSizes.entrySet()) {
                    final Interval i = e.getKey();
                    if (!allPreset(i.begin, i.end)) {
                        double subTotal = computeTotal(i.begin, i.end);
                        if (e.getValue() > subTotal) {
                            total += e.getValue() - subTotal;
                        }
                    }
                }
            }
            if (totalFixedPercent > 0) {
                double totalNotFixed = 0;
                // First, remove the sizes that are fixed to be 0
                for (int i = 0; i < fixedPercent.length; ++i) {
                    if (fixedPercent[i] == 0) {
                        total -= singleSizes[i];
                    }
                }
                for (int i = 0; i < fixedPercent.length; ++i) {
                    if (fixedPercent[i] > 0) {
                        // Grow the total so that every size at it's value corresponds at least to it's fixedPercent of the total
                        // i.e. total * fixedPercent[i] >= singleSizes[i]
                        total = Math.max(total, singleSizes[i] * (100 / fixedPercent[i]));
                    } else if (fixedPercent[i] < 0){
                        totalNotFixed += singleSizes[i];
                    }
                }
                if (totalFixedPercent < 100) {
                    total = Math.max(total, totalNotFixed * 100 / (100 - totalFixedPercent));
                }
            }
            return total;
        }

        private int getLength() {
            return singleSizes.length;
        }

        @Override
        protected Object clone() {
            try {
            CompositeSize clone = (CompositeSize) super.clone();
            clone.singleSizes = clone.singleSizes.clone();
            if (multiSizes != null)
                clone.multiSizes = new TreeMap<>(clone.multiSizes);
            return clone;
            } catch (CloneNotSupportedException ex) {
                throw new RuntimeException(ex);
            }
        }

        private double[] asArray() {
            return singleSizes;
        }

    }

    /**
     * Returns the number of rows in this GridPane.
     *
     * @return the row count
     * @since 9
     */
    public final int getRowCount() {
        int nRows = this.getRowConstraints().size();
        for (int i = 0; i < this.getChildren().size(); i++) {
            Node child = this.getChildren().get(i);
            if (child.isManaged()) {
                int rowIndex = GridPane.getNodeRowIndex(child);
                int rowEnd = GridPane.getNodeRowEnd(child);
                nRows = Math.max(nRows, (rowEnd != GridPane.REMAINING? rowEnd : rowIndex) + 1);
            }
        }
        return nRows;
    }

    /**
     * Returns the number of columns in this GridPane.
     *
     * @return the column count
     * @since 9
     */
    public final int getColumnCount() {
        int nColumns = this.getColumnConstraints().size();
        for (int i = 0; i < this.getChildren().size(); i++) {
            Node child = this.getChildren().get(i);
            if (child.isManaged()) {
                int columnIndex = GridPane.getNodeColumnIndex(child);
                int columnEnd = GridPane.getNodeColumnEnd(child);
                nColumns = Math.max(nColumns, (columnEnd != GridPane.REMAINING? columnEnd : columnIndex) + 1);
            }
        }
        return nColumns;
    }

    /**
     * Returns the bounds of the cell at the specified column and row position.
     *
     * @param columnIndex the column index position for the cell within this
     * GridPane, counting from 0
     * @param rowIndex the row index position for the cell within this GridPane,
     * counting from 0
     * @return the bounds of the cell at columnIndex and rowIndex.
     * @since 9
     */
    public final Bounds getCellBounds(int columnIndex, int rowIndex) {
        final double snaphgap = this.snapSpaceX(this.getHgap());
        final double snapvgap = this.snapSpaceY(this.getVgap());
        final double top = this.snapSpaceY(this.getInsets().getTop());
        final double right = this.snapSpaceX(this.getInsets().getRight());
        final double bottom = this.snapSpaceY(this.getInsets().getBottom());
        final double left = this.snapSpaceX(this.getInsets().getLeft());
        final double gridPaneHeight = this.snapSizeY(this.getHeight()) - (top + bottom);
        final double gridPaneWidth = this.snapSizeX(this.getWidth()) - (left + right);

        // Compute grid. Result contains two double arrays, first for columns, second for rows
        double[] columnWidths;
        double[] rowHeights;

        double[][] grid = this.getGrid();
        if (grid == null) {
            rowHeights = new double[] {0};
            rowIndex = 0;
            columnWidths = new double[] {0};
            columnIndex = 0;
        } else {
            columnWidths = grid[0];
            rowHeights = grid[1];
        }

        // Compute the total row height
        double rowTotal = 0;
        for (int i = 0; i < rowHeights.length; i++) {
            rowTotal += rowHeights[i];
        }
        rowTotal += ((rowHeights.length - 1) * snapvgap);

        // Adjust for alignment
        double minY = top + Region.computeYOffset(gridPaneHeight, rowTotal, this.getAlignment().getVpos());
        double height = rowHeights[rowIndex];
        for (int j = 0; j < rowIndex; j++) {
            minY += rowHeights[j] + snapvgap;
        }

        // Compute the total column width
        double columnTotal = 0;
        for (int i = 0; i < columnWidths.length; i++) {
            columnTotal += columnWidths[i];
        }
        columnTotal += ((columnWidths.length - 1) * snaphgap);

        // Adjust for alignment
        double minX = left + Region.computeXOffset(gridPaneWidth, columnTotal, this.getAlignment().getHpos());
        double width = columnWidths[columnIndex];
        for (int j = 0; j < columnIndex; j++) {
            minX += columnWidths[j] + snaphgap;
        }

        return new BoundingBox(minX, minY, width, height);
    }

}
