/*
 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

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.css.CssMetaData;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.css.converters.SizeConverter;
import java.util.Iterator;
import java.util.function.Function;
import javafx.css.Styleable;
import javafx.geometry.HPos;
import javafx.util.Callback;



/**
 * HBox lays out its children in a single horizontal row.
 * If the hbox has a border and/or padding set, then the contents will be layed
 * out within those insets.
 * <p>
 * HBox example:
 * <pre><code>
 *     HBox hbox = new HBox(8); // spacing = 8
 *     hbox.getChildren().addAll(new Label("Name:), new TextBox());
 * </code></pre>
 *
 * HBox will resize children (if resizable) to their preferred widths and uses its
 * fillHeight property to determine whether to resize their heights to
 * fill its own height or keep their heights to their preferred (fillHeight defaults to true).
 * The alignment of the content is controlled by the alignment property,
 * which defaults to Pos.TOP_LEFT.
 * <p>
 * If an hbox is resized larger than its preferred width, by default it will keep
 * children to their preferred widths, leaving the extra space unused.  If an
 * application wishes to have one or more children be allocated that extra space
 * it may optionally set an hgrow constraint on the child.  See "Optional Layout
 * Constraints" for details.
 * <p>
 * HBox lays out each managed child regardless of the child's
 * visible property value; unmanaged children are ignored.</p>
 *
 * <h4>Resizable Range</h4>
 *
 * An hbox's parent will resize the hbox within the hbox's resizable range
 * during layout.   By default the hbox computes this range based on its content
 * as outlined in the table below.
 * <table border="1">
 * <tr><td></td><th>width</th><th>height</th></tr>
 * <tr><th>minimum</th>
 * <td>left/right insets plus the sum of each child's min width plus spacing between each child.</td>
 * <td>top/bottom insets plus the largest of the children's min heights.</td></tr>
 * <tr><th>preferred</th>
 * <td>left/right insets plus the sum of each child's pref width plus spacing between each child.</td>
 * <td>top/bottom insets plus the largest of the children's pref heights.</td></tr>
 * <tr><th>maximum</th>
 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
 * </table>
 * <p>
 * An hbox'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>
 * HBox 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>hbox.setPrefWidth(400);</b>
 * </code></pre>
 * Applications may restore the computed values by setting these properties back
 * to USE_COMPUTED_SIZE.
 * <p>
 * HBox does not clip its content by default, so it is possible that childrens'
 * bounds may extend outside its own bounds if a child's min size prevents it from
 * being fit within the hbox.</p>
 *
 * <h4>Optional Layout Constraints</h4>
 *
 * An application may set constraints on individual children to customize HBox's layout.
 * For each constraint, HBox provides a static method for setting it on the child.
 * <p>
 * <table border="1">
 * <tr><th>Constraint</th><th>Type</th><th>Description</th></tr>
 * <tr><td>hgrow</td><td>javafx.scene.layout.Priority</td><td>The horizontal grow priority for the child.</td></tr>
 * <tr><td>margin</td><td>javafx.geometry.Insets</td><td>Margin space around the outside of the child.</td></tr>
 * </table>
 * <p>
 * For example, if an hbox needs the TextField to be allocated all extra space:
 * <pre><code>
 *     HBox hbox = new HBox();
 *     TextField field = new TextField();
 *     <b>HBox.setHgrow(field, Priority.ALWAYS);</b>
 *     hbox.getChildren().addAll(new Label("Search:"), field, new Button("Go"));
 * </code></pre>
 *
 * If more than one child has the same grow priority set, then the hbox will
 * allocate equal amounts of space to each.  HBox will only grow a child up to
 * its maximum width, so if the child has a max width other than Double.MAX_VALUE,
 * the application may need to override the max to allow it to grow.
 * For example:
 * <pre><code>
 *     HBox hbox = new HBox();
 *     Button button1 = new Button("Add");
 *     Button button2 = new Button("Remove");
 *     <b>HBox.setHgrow(button1, Priority.ALWAYS);
 *     HBox.setHgrow(button2, Priority.ALWAYS);
 *     button1.setMaxWidth(Double.MAX_VALUE);
 *     button2.setMaxWidth(Double.MAX_VALUE);</b>
 *     hbox.getChildren().addAll(button1, button2);
 * </code></pre>
 * @since JavaFX 2.0
 */
public class HBox extends Pane {

    private boolean biasDirty = true;
    private boolean performingLayout = false;
    private double minBaselineComplement = Double.NaN;
    private double prefBaselineComplement = Double.NaN;
    private Orientation bias;
    private double[][] tempArray;

    /********************************************************************
     *  BEGIN static methods
     ********************************************************************/
    private static final String MARGIN_CONSTRAINT = "hbox-margin";
    private static final String HGROW_CONSTRAINT = "hbox-hgrow";

    /**
     * Sets the horizontal grow priority for the child when contained by an hbox.
     * If set, the hbox will use the priority to allocate additional space if the
     * hbox is resized larger than it's preferred width.
     * If multiple hbox children have the same horizontal grow priority, then the
     * extra space will be split evening between them.
     * If no horizontal grow priority is set on a child, the hbox will never
     * allocate it additional horizontal space if available.
     * Setting the value to null will remove the constraint.
     * @param child the child of an hbox
     * @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 an hbox
     * @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 margin for the child when contained by an hbox.
     * If set, the hbox will layout the child with the margin space around it.
     * Setting the value to null will remove the constraint.
     * @param child the child mode of the hbox
     * @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 an hbox
     * @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 static final Callback<Node, Insets> marginAccessor = n -> getMargin(n);

    /**
     * Removes all hbox constraints from the child node.
     * @param child the child node
     */
    public static void clearConstraints(Node child) {
        setHgrow(child, null);
        setMargin(child, null);
    }

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

    /**
     * Creates an HBox layout with spacing = 0.
     */
    public HBox() {
        super();
    }

    /**
     * Creates an HBox layout with the specified spacing between children.
     * @param spacing the amount of horizontal space between each child
     */
    public HBox(double spacing) {
        this();
        setSpacing(spacing);
    }

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

    /**
     * Creates an HBox layout with the specified spacing between children.
     * @param spacing the amount of horizontal space between each child
     * @param children The initial set of children for this pane.
     * @since JavaFX 8.0
     */
    public HBox(double spacing, Node... children) {
        this();
        setSpacing(spacing);
        getChildren().addAll(children);
    }

    /**
     * The amount of horizontal space between each child in the hbox.
     */
    public final DoubleProperty spacingProperty() {
        if (spacing == null) {
            spacing = new StyleableDoubleProperty() {
                @Override
                public void invalidated() {
                    requestLayout();
                }

                @Override
                public CssMetaData getCssMetaData () {
                    return StyleableProperties.SPACING;
                }

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

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

    private DoubleProperty spacing;
    public final void setSpacing(double value) { spacingProperty().set(value); }
    public final double getSpacing() { return spacing == null ? 0 : spacing.get(); }

    /**
     * The overall alignment of children within the hbox'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<HBox, Pos> getCssMetaData() {
                    return StyleableProperties.ALIGNMENT;
                }

                @Override
                public Object getBean() {
                    return HBox.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;
    }

    /**
     * Whether or not resizable children will be resized to fill the full height of the hbox
     * or be resized to their preferred height and aligned according to the <code>alignment</code>
     * vpos value.   Note that if the hbox vertical alignment is set to BASELINE, then this
     * property will be ignored and children will be resized to their preferred heights.
     */
    public final BooleanProperty fillHeightProperty() {
        if (fillHeight == null) {
            fillHeight = new StyleableBooleanProperty(true) {
                @Override
                public void invalidated() {
                    requestLayout();
                }

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

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

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

    private BooleanProperty fillHeight;
    public final void setFillHeight(boolean value) { fillHeightProperty().set(value); }
    public final boolean isFillHeight() { return fillHeight == null ? true : fillHeight.get(); }

    private boolean shouldFillHeight() {
        return isFillHeight() && getAlignmentInternal().getVpos() != VPos.BASELINE;
    }

    /**
     *
     * @return null unless one of its children has a content bias.
     */
    @Override public Orientation getContentBias() {
        if (biasDirty) {
            bias = null;
            final List<Node> children = getManagedChildren();
            for (Node child : children) {
                Orientation contentBias = child.getContentBias();
                if (contentBias != null) {
                    bias = contentBias;
                    if (contentBias == Orientation.HORIZONTAL) {
                        break;
                    }
                }
            }
            biasDirty = false;
        }
        return bias;
    }

    @Override protected double computeMinWidth(double height) {
        Insets insets = getInsets();
        return snapSpace(insets.getLeft()) +
               computeContentWidth(getManagedChildren(), height, true) +
               snapSpace(insets.getRight());
    }

    @Override protected double computeMinHeight(double width) {
        Insets insets = getInsets();
        List<Node>managed = getManagedChildren();
        double contentHeight = 0;
        if (width != -1 && getContentBias() != null) {
            double prefWidths[][] = getAreaWidths(managed, -1, false);
            adjustAreaWidths(managed, prefWidths, width, -1);
            contentHeight = computeMaxMinAreaHeight(managed, marginAccessor, prefWidths[0], getAlignmentInternal().getVpos());
        } else {
            contentHeight = computeMaxMinAreaHeight(managed, marginAccessor, getAlignmentInternal().getVpos());
        }
        return snapSpace(insets.getTop()) +
               contentHeight +
               snapSpace(insets.getBottom());
    }

    @Override protected double computePrefWidth(double height) {
        Insets insets = getInsets();
        return snapSpace(insets.getLeft()) +
               computeContentWidth(getManagedChildren(), height, false) +
               snapSpace(insets.getRight());
    }

    @Override protected double computePrefHeight(double width) {
        Insets insets = getInsets();
        List<Node>managed = getManagedChildren();
        double contentHeight = 0;
        if (width != -1 && getContentBias() != null) {
            double prefWidths[][] = getAreaWidths(managed, -1, false);
            adjustAreaWidths(managed, prefWidths, width, -1);
            contentHeight = computeMaxPrefAreaHeight(managed, marginAccessor, prefWidths[0], getAlignmentInternal().getVpos());
        } else {
            contentHeight = computeMaxPrefAreaHeight(managed, marginAccessor, getAlignmentInternal().getVpos());
        }
        return snapSpace(insets.getTop()) +
               contentHeight +
               snapSpace(insets.getBottom());
    }

    private double[][] getAreaWidths(List<Node>managed, double height, boolean minimum) {
        // height could be -1
        double[][] temp = getTempArray(managed.size());
        final double insideHeight = height == -1? -1 : height -
                                     snapSpace(getInsets().getTop()) - snapSpace(getInsets().getBottom());
        final boolean shouldFillHeight = shouldFillHeight();
        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            Insets margin = getMargin(child);
            if (minimum) {
                temp[0][i] = computeChildMinAreaWidth(child, getMinBaselineComplement(), margin, insideHeight, shouldFillHeight);
            } else {
                temp[0][i] = computeChildPrefAreaWidth(child, getPrefBaselineComplement(), margin, insideHeight, shouldFillHeight);
            }
        }
        return temp;
    }

    private double adjustAreaWidths(List<Node>managed, double areaWidths[][], double width, double height) {
        Insets insets = getInsets();
        double top = snapSpace(insets.getTop());
        double bottom = snapSpace(insets.getBottom());

        double contentWidth = sum(areaWidths[0], managed.size()) + (managed.size()-1)*snapSpace(getSpacing());
        double extraWidth = width -
                snapSpace(insets.getLeft()) - snapSpace(insets.getRight()) - contentWidth;

        if (extraWidth != 0) {
            final double refHeight = shouldFillHeight() && height != -1? height - top - bottom : -1;
            double remaining = growOrShrinkAreaWidths(managed, areaWidths, Priority.ALWAYS, extraWidth, refHeight);
            remaining = growOrShrinkAreaWidths(managed, areaWidths, Priority.SOMETIMES, remaining, refHeight);
            contentWidth += (extraWidth - remaining);
        }
        return contentWidth;
    }

    private double growOrShrinkAreaWidths(List<Node>managed, double areaWidths[][], Priority priority, double extraWidth, double height) {
        final boolean shrinking = extraWidth < 0;
        int adjustingNumber = 0;

        double[] usedWidths = areaWidths[0];
        double[] temp = areaWidths[1];
        final boolean shouldFillHeight = shouldFillHeight();

        if (shrinking) {
            adjustingNumber = managed.size();
            for (int i = 0, size = managed.size(); i < size; i++) {
                final Node child = managed.get(i);
                temp[i] = computeChildMinAreaWidth(child, getMinBaselineComplement(), getMargin(child), height, shouldFillHeight);
            }
        } else {
            for (int i = 0, size = managed.size(); i < size; i++) {
                final Node child = managed.get(i);
                if (getHgrow(child) == priority) {
                    temp[i] = computeChildMaxAreaWidth(child, getMinBaselineComplement(), getMargin(child), height, shouldFillHeight);
                    adjustingNumber++;
                } else {
                    temp[i] = -1;
                }
            }
        }

        double available = extraWidth; // will be negative in shrinking case
        outer:while (Math.abs(available) > 1 && adjustingNumber > 0) {
            final double portion = snapPortion(available / adjustingNumber); // negative in shrinking case
            for (int i = 0, size = managed.size(); i < size; i++) {
                if (temp[i] == -1) {
                    continue;
                }
                final double limit = temp[i] - usedWidths[i]; // negative in shrinking case
                final double change = Math.abs(limit) <= Math.abs(portion)? limit : portion;
                usedWidths[i] += change;
                available -= change;
                if (Math.abs(available) < 1) {
                    break outer;
                }
                if (Math.abs(change) < Math.abs(portion)) {
                    temp[i] = -1;
                    adjustingNumber--;
                }
            }
        }

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

    private double computeContentWidth(List<Node> managedChildren, double height, boolean minimum) {
        return sum(getAreaWidths(managedChildren, height, minimum)[0], managedChildren.size())
                + (managedChildren.size()-1)*snapSpace(getSpacing());
    }

    private static double sum(double[] array, int size) {
        int i = 0;
        double res = 0;
        while (i != size) {
            res += array[i++];
        }
        return res;
    }

    @Override public void requestLayout() {
        if (performingLayout) {
            return;
        }
        biasDirty = true;
        bias = null;
        minBaselineComplement = Double.NaN;
        prefBaselineComplement = Double.NaN;
        baselineOffset = Double.NaN;
        super.requestLayout();
    }

    private double getMinBaselineComplement() {
        if (Double.isNaN(minBaselineComplement)) {
            if (getAlignmentInternal().getVpos() == VPos.BASELINE) {
                minBaselineComplement = getMinBaselineComplement(getManagedChildren());
            } else {
                minBaselineComplement = -1;
            }
        }
        return minBaselineComplement;
    }

    private double getPrefBaselineComplement() {
        if (Double.isNaN(prefBaselineComplement)) {
            if (getAlignmentInternal().getVpos() == VPos.BASELINE) {
                prefBaselineComplement = getPrefBaselineComplement(getManagedChildren());
            } else {
                prefBaselineComplement = -1;
            }
        }
        return prefBaselineComplement;
    }

    private double baselineOffset = Double.NaN;

    @Override
    public double getBaselineOffset() {
        List<Node> managed = getManagedChildren();
        if (managed.isEmpty()) {
            return BASELINE_OFFSET_SAME_AS_HEIGHT;
        }
        if (Double.isNaN(baselineOffset)) {
            VPos vpos = getAlignmentInternal().getVpos();
            if (vpos == VPos.BASELINE) {
                double max = 0;
                for (int i =0, sz = managed.size(); i < sz; ++i) {
                    final Node child = managed.get(i);
                    double offset = child.getBaselineOffset();
                    if (offset == BASELINE_OFFSET_SAME_AS_HEIGHT) {
                        baselineOffset = BASELINE_OFFSET_SAME_AS_HEIGHT;
                        break;
                    } else {
                        Insets margin = getMargin(child);
                        double top = margin != null ? margin.getTop() : 0;
                        max = Math.max(max, top + child.getLayoutBounds().getMinY() + offset);
                    }
                }
                baselineOffset = max + snappedTopInset();
            } else {
                baselineOffset = BASELINE_OFFSET_SAME_AS_HEIGHT;
            }
        }
        return baselineOffset;
    }

    @Override protected void layoutChildren() {
        performingLayout = true;
        List<Node> managed = getManagedChildren();
        Insets insets = getInsets();
        Pos align = getAlignmentInternal();
        HPos alignHpos = align.getHpos();
        VPos alignVpos = align.getVpos();
        double width = getWidth();
        double height = getHeight();
        double top = snapSpace(insets.getTop());
        double left = snapSpace(insets.getLeft());
        double bottom = snapSpace(insets.getBottom());
        double right = snapSpace(insets.getRight());
        double space = snapSpace(getSpacing());
        boolean shouldFillHeight = shouldFillHeight();

        final double[][] actualAreaWidths = getAreaWidths(managed, height, false);
        double contentWidth = adjustAreaWidths(managed, actualAreaWidths, width, height);
        double contentHeight = height - top - bottom;

        double x = left + computeXOffset(width - left - right, contentWidth, align.getHpos());
        double y = top;
        double baselineOffset = -1;
        if (alignVpos == VPos.BASELINE) {
            double baselineComplement = getMinBaselineComplement();
            baselineOffset = getAreaBaselineOffset(managed, marginAccessor, i -> actualAreaWidths[0][i],
                    contentHeight, shouldFillHeight, baselineComplement);
        }

        for (int i = 0, size = managed.size(); i < size; i++) {
            Node child = managed.get(i);
            Insets margin = getMargin(child);
            layoutInArea(child, x, y, actualAreaWidths[0][i], contentHeight,
                    baselineOffset, margin, true, shouldFillHeight,
                    alignHpos, alignVpos);
            x += actualAreaWidths[0][i] + space;
        }
        performingLayout = false;
    }

    private double[][] getTempArray(int size) {
        if (tempArray == null) {
            tempArray = new double[2][size]; // First array for the result, second for temporary computations
        } else if (tempArray[0].length < size) {
            tempArray = new double[2][Math.max(tempArray.length * 3, size)];
        }
        return tempArray;

    }

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

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

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

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

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

         };

         private static final CssMetaData<HBox,Boolean> FILL_HEIGHT =
             new CssMetaData<HBox,Boolean>("-fx-fill-height",
                 BooleanConverter.getInstance(), Boolean.TRUE) {

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

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

         };

         private static final CssMetaData<HBox,Number> SPACING =
             new CssMetaData<HBox,Number>("-fx-spacing",
                 SizeConverter.getInstance(), 0.0){

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

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

         };

         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
         static {
            final List<CssMetaData<? extends Styleable, ?>> styleables =
                new ArrayList<CssMetaData<? extends Styleable, ?>>(Pane.getClassCssMetaData());
            styleables.add(FILL_HEIGHT);
            styleables.add(ALIGNMENT);
            styleables.add(SPACING);
            STYLEABLES = Collections.unmodifiableList(styleables);
         }
    }

    /**
     * @return The CssMetaData associated with this class, which may include the
     * CssMetaData of its super classes.
     * @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();
    }

}
