/*
 * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package javax.swing.text;

import java.io.PrintStream;
import java.util.Vector;
import java.awt.*;
import javax.swing.event.DocumentEvent;
import javax.swing.SizeRequirements;

/**
 * A view that arranges its children into a box shape by tiling
 * its children along an axis.  The box is somewhat like that
 * found in TeX where there is alignment of the
 * children, flexibility of the children is considered, etc.
 * This is a building block that might be useful to represent
 * things like a collection of lines, paragraphs,
 * lists, columns, pages, etc.  The axis along which the children are tiled is
 * considered the major axis.  The orthogonal axis is the minor axis.
 * <p>
 * Layout for each axis is handled separately by the methods
 * <code>layoutMajorAxis</code> and <code>layoutMinorAxis</code>.
 * Subclasses can change the layout algorithm by
 * reimplementing these methods.    These methods will be called
 * as necessary depending upon whether or not there is cached
 * layout information and the cache is considered
 * valid.  These methods are typically called if the given size
 * along the axis changes, or if <code>layoutChanged</code> is
 * called to force an updated layout.  The <code>layoutChanged</code>
 * method invalidates cached layout information, if there is any.
 * The requirements published to the parent view are calculated by
 * the methods <code>calculateMajorAxisRequirements</code>
 * and  <code>calculateMinorAxisRequirements</code>.
 * If the layout algorithm is changed, these methods will
 * likely need to be reimplemented.
 *
 * @author  Timothy Prinzing
 */
public class BoxView extends CompositeView {

    /**
     * Constructs a <code>BoxView</code>.
     *
     * @param elem the element this view is responsible for
     * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     */
    public BoxView(Element elem, int axis) {
        super(elem);
        tempRect = new Rectangle();
        this.majorAxis = axis;

        majorOffsets = new int[0];
        majorSpans = new int[0];
        majorReqValid = false;
        majorAllocValid = false;
        minorOffsets = new int[0];
        minorSpans = new int[0];
        minorReqValid = false;
        minorAllocValid = false;
    }

    /**
     * Fetches the tile axis property.  This is the axis along which
     * the child views are tiled.
     *
     * @return the major axis of the box, either
     *  <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     *
     * @since 1.3
     */
    public int getAxis() {
        return majorAxis;
    }

    /**
     * Sets the tile axis property.  This is the axis along which
     * the child views are tiled.
     *
     * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     *
     * @since 1.3
     */
    public void setAxis(int axis) {
        boolean axisChanged = (axis != majorAxis);
        majorAxis = axis;
        if (axisChanged) {
            preferenceChanged(null, true, true);
        }
    }

    /**
     * Invalidates the layout along an axis.  This happens
     * automatically if the preferences have changed for
     * any of the child views.  In some cases the layout
     * may need to be recalculated when the preferences
     * have not changed.  The layout can be marked as
     * invalid by calling this method.  The layout will
     * be updated the next time the <code>setSize</code> method
     * is called on this view (typically in paint).
     *
     * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     *
     * @since 1.3
     */
    public void layoutChanged(int axis) {
        if (axis == majorAxis) {
            majorAllocValid = false;
        } else {
            minorAllocValid = false;
        }
    }

    /**
     * Determines if the layout is valid along the given axis.
     * @return if the layout is valid along the given axis
     *
     * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     *
     * @since 1.4
     */
    protected boolean isLayoutValid(int axis) {
        if (axis == majorAxis) {
            return majorAllocValid;
        } else {
            return minorAllocValid;
        }
    }

    /**
     * Paints a child.  By default
     * that is all it does, but a subclass can use this to paint
     * things relative to the child.
     *
     * @param g the graphics context
     * @param alloc the allocated region to paint into
     * @param index the child index, &gt;= 0 &amp;&amp; &lt; getViewCount()
     */
    protected void paintChild(Graphics g, Rectangle alloc, int index) {
        View child = getView(index);
        child.paint(g, alloc);
    }

    // --- View methods ---------------------------------------------

    /**
     * Invalidates the layout and resizes the cache of
     * requests/allocations.  The child allocations can still
     * be accessed for the old layout, but the new children
     * will have an offset and span of 0.
     *
     * @param index the starting index into the child views to insert
     *   the new views; this should be a value &gt;= 0 and &lt;= getViewCount
     * @param length the number of existing child views to remove;
     *   This should be a value &gt;= 0 and &lt;= (getViewCount() - offset)
     * @param elems the child views to add; this value can be
     *   <code>null</code>to indicate no children are being added
     *   (useful to remove)
     */
    public void replace(int index, int length, View[] elems) {
        super.replace(index, length, elems);

        // invalidate cache
        int nInserted = (elems != null) ? elems.length : 0;
        majorOffsets = updateLayoutArray(majorOffsets, index, nInserted);
        majorSpans = updateLayoutArray(majorSpans, index, nInserted);
        majorReqValid = false;
        majorAllocValid = false;
        minorOffsets = updateLayoutArray(minorOffsets, index, nInserted);
        minorSpans = updateLayoutArray(minorSpans, index, nInserted);
        minorReqValid = false;
        minorAllocValid = false;
    }

    /**
     * Resizes the given layout array to match the new number of
     * child views.  The current number of child views are used to
     * produce the new array.  The contents of the old array are
     * inserted into the new array at the appropriate places so that
     * the old layout information is transferred to the new array.
     *
     * @param oldArray the original layout array
     * @param offset location where new views will be inserted
     * @param nInserted the number of child views being inserted;
     *          therefore the number of blank spaces to leave in the
     *          new array at location <code>offset</code>
     * @return the new layout array
     */
    int[] updateLayoutArray(int[] oldArray, int offset, int nInserted) {
        int n = getViewCount();
        int[] newArray = new int[n];

        System.arraycopy(oldArray, 0, newArray, 0, offset);
        System.arraycopy(oldArray, offset,
                         newArray, offset + nInserted, n - nInserted - offset);
        return newArray;
    }

    /**
     * Forwards the given <code>DocumentEvent</code> to the child views
     * that need to be notified of the change to the model.
     * If a child changed its requirements and the allocation
     * was valid prior to forwarding the portion of the box
     * from the starting child to the end of the box will
     * be repainted.
     *
     * @param ec changes to the element this view is responsible
     *  for (may be <code>null</code> if there were no changes)
     * @param e the change information from the associated document
     * @param a the current allocation of the view
     * @param f the factory to use to rebuild if the view has children
     * @see #insertUpdate
     * @see #removeUpdate
     * @see #changedUpdate
     * @since 1.3
     */
    protected void forwardUpdate(DocumentEvent.ElementChange ec,
                                 DocumentEvent e, Shape a, ViewFactory f) {
        boolean wasValid = isLayoutValid(majorAxis);
        super.forwardUpdate(ec, e, a, f);

        // determine if a repaint is needed
        if (wasValid && (! isLayoutValid(majorAxis))) {
            // Repaint is needed because one of the tiled children
            // have changed their span along the major axis.  If there
            // is a hosting component and an allocated shape we repaint.
            Component c = getContainer();
            if ((a != null) && (c != null)) {
                int pos = e.getOffset();
                int index = getViewIndexAtPosition(pos);
                Rectangle alloc = getInsideAllocation(a);
                if (majorAxis == X_AXIS) {
                    alloc.x += majorOffsets[index];
                    alloc.width -= majorOffsets[index];
                } else {
                    alloc.y += minorOffsets[index];
                    alloc.height -= minorOffsets[index];
                }
                c.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
            }
        }
    }

    /**
     * This is called by a child to indicate its
     * preferred span has changed.  This is implemented to
     * throw away cached layout information so that new
     * calculations will be done the next time the children
     * need an allocation.
     *
     * @param child the child view
     * @param width true if the width preference should change
     * @param height true if the height preference should change
     */
    public void preferenceChanged(View child, boolean width, boolean height) {
        boolean majorChanged = (majorAxis == X_AXIS) ? width : height;
        boolean minorChanged = (majorAxis == X_AXIS) ? height : width;
        if (majorChanged) {
            majorReqValid = false;
            majorAllocValid = false;
        }
        if (minorChanged) {
            minorReqValid = false;
            minorAllocValid = false;
        }
        super.preferenceChanged(child, width, height);
    }

    /**
     * Gets the resize weight.  A value of 0 or less is not resizable.
     *
     * @param axis may be either <code>View.X_AXIS</code> or
     *          <code>View.Y_AXIS</code>
     * @return the weight
     * @throws IllegalArgumentException for an invalid axis
     */
    public int getResizeWeight(int axis) {
        checkRequests(axis);
        if (axis == majorAxis) {
            if ((majorRequest.preferred != majorRequest.minimum) ||
                (majorRequest.preferred != majorRequest.maximum)) {
                return 1;
            }
        } else {
            if ((minorRequest.preferred != minorRequest.minimum) ||
                (minorRequest.preferred != minorRequest.maximum)) {
                return 1;
            }
        }
        return 0;
    }

    /**
     * Sets the size of the view along an axis.  This should cause
     * layout of the view along the given axis.
     *
     * @param axis may be either <code>View.X_AXIS</code> or
     *          <code>View.Y_AXIS</code>
     * @param span the span to layout to >= 0
     */
    void setSpanOnAxis(int axis, float span) {
        if (axis == majorAxis) {
            if (majorSpan != (int) span) {
                majorAllocValid = false;
            }
            if (! majorAllocValid) {
                // layout the major axis
                majorSpan = (int) span;
                checkRequests(majorAxis);
                layoutMajorAxis(majorSpan, axis, majorOffsets, majorSpans);
                majorAllocValid = true;

                // flush changes to the children
                updateChildSizes();
            }
        } else {
            if (((int) span) != minorSpan) {
                minorAllocValid = false;
            }
            if (! minorAllocValid) {
                // layout the minor axis
                minorSpan = (int) span;
                checkRequests(axis);
                layoutMinorAxis(minorSpan, axis, minorOffsets, minorSpans);
                minorAllocValid = true;

                // flush changes to the children
                updateChildSizes();
            }
        }
    }

    /**
     * Propagates the current allocations to the child views.
     */
    void updateChildSizes() {
        int n = getViewCount();
        if (majorAxis == X_AXIS) {
            for (int i = 0; i < n; i++) {
                View v = getView(i);
                v.setSize((float) majorSpans[i], (float) minorSpans[i]);
            }
        } else {
            for (int i = 0; i < n; i++) {
                View v = getView(i);
                v.setSize((float) minorSpans[i], (float) majorSpans[i]);
            }
        }
    }

    /**
     * Returns the size of the view along an axis.  This is implemented
     * to return zero.
     *
     * @param axis may be either <code>View.X_AXIS</code> or
     *          <code>View.Y_AXIS</code>
     * @return the current span of the view along the given axis, >= 0
     */
    float getSpanOnAxis(int axis) {
        if (axis == majorAxis) {
            return majorSpan;
        } else {
            return minorSpan;
        }
    }

    /**
     * Sets the size of the view.  This should cause
     * layout of the view if the view caches any layout
     * information.  This is implemented to call the
     * layout method with the sizes inside of the insets.
     *
     * @param width the width &gt;= 0
     * @param height the height &gt;= 0
     */
    public void setSize(float width, float height) {
        layout(Math.max(0, (int)(width - getLeftInset() - getRightInset())),
               Math.max(0, (int)(height - getTopInset() - getBottomInset())));
    }

    /**
     * Renders the <code>BoxView</code> using the given
     * rendering surface and area
     * on that surface.  Only the children that intersect
     * the clip bounds of the given <code>Graphics</code>
     * will be rendered.
     *
     * @param g the rendering surface to use
     * @param allocation the allocated region to render into
     * @see View#paint
     */
    public void paint(Graphics g, Shape allocation) {
        Rectangle alloc = (allocation instanceof Rectangle) ?
                           (Rectangle)allocation : allocation.getBounds();
        int n = getViewCount();
        int x = alloc.x + getLeftInset();
        int y = alloc.y + getTopInset();
        Rectangle clip = g.getClipBounds();
        for (int i = 0; i < n; i++) {
            tempRect.x = x + getOffset(X_AXIS, i);
            tempRect.y = y + getOffset(Y_AXIS, i);
            tempRect.width = getSpan(X_AXIS, i);
            tempRect.height = getSpan(Y_AXIS, i);
            int trx0 = tempRect.x, trx1 = trx0 + tempRect.width;
            int try0 = tempRect.y, try1 = try0 + tempRect.height;
            int crx0 = clip.x, crx1 = crx0 + clip.width;
            int cry0 = clip.y, cry1 = cry0 + clip.height;
            // We should paint views that intersect with clipping region
            // even if the intersection has no inside points (is a line).
            // This is needed for supporting views that have zero width, like
            // views that contain only combining marks.
            if ((trx1 >= crx0) && (try1 >= cry0) && (crx1 >= trx0) && (cry1 >= try0)) {
                paintChild(g, tempRect, i);
            }
        }
    }

    /**
     * Fetches the allocation for the given child view.
     * This enables finding out where various views
     * are located.  This is implemented to return
     * <code>null</code> if the layout is invalid,
     * otherwise the superclass behavior is executed.
     *
     * @param index the index of the child, &gt;= 0 &amp;&amp; &gt; getViewCount()
     * @param a  the allocation to this view
     * @return the allocation to the child; or <code>null</code>
     *          if <code>a</code> is <code>null</code>;
     *          or <code>null</code> if the layout is invalid
     */
    public Shape getChildAllocation(int index, Shape a) {
        if (a != null) {
            Shape ca = super.getChildAllocation(index, a);
            if ((ca != null) && (! isAllocationValid())) {
                // The child allocation may not have been set yet.
                Rectangle r = (ca instanceof Rectangle) ?
                    (Rectangle) ca : ca.getBounds();
                if ((r.width == 0) && (r.height == 0)) {
                    return null;
                }
            }
            return ca;
        }
        return null;
    }

    /**
     * Provides a mapping from the document model coordinate space
     * to the coordinate space of the view mapped to it.  This makes
     * sure the allocation is valid before calling the superclass.
     *
     * @param pos the position to convert &gt;= 0
     * @param a the allocated region to render into
     * @return the bounding box of the given position
     * @throws BadLocationException  if the given position does
     *  not represent a valid location in the associated document
     * @see View#modelToView
     */
    public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
        if (! isAllocationValid()) {
            Rectangle alloc = a.getBounds();
            setSize(alloc.width, alloc.height);
        }
        return super.modelToView(pos, a, b);
    }

    /**
     * Provides a mapping from the view coordinate space to the logical
     * coordinate space of the model.
     *
     * @param x   x coordinate of the view location to convert &gt;= 0
     * @param y   y coordinate of the view location to convert &gt;= 0
     * @param a the allocated region to render into
     * @return the location within the model that best represents the
     *  given point in the view &gt;= 0
     * @see View#viewToModel
     */
    public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
        if (! isAllocationValid()) {
            Rectangle alloc = a.getBounds();
            setSize(alloc.width, alloc.height);
        }
        return super.viewToModel(x, y, a, bias);
    }

    /**
     * Determines the desired alignment for this view along an
     * axis.  This is implemented to give the total alignment
     * needed to position the children with the alignment points
     * lined up along the axis orthogonal to the axis that is
     * being tiled.  The axis being tiled will request to be
     * centered (i.e. 0.5f).
     *
     * @param axis may be either <code>View.X_AXIS</code>
     *   or <code>View.Y_AXIS</code>
     * @return the desired alignment &gt;= 0.0f &amp;&amp; &lt;= 1.0f; this should
     *   be a value between 0.0 and 1.0 where 0 indicates alignment at the
     *   origin and 1.0 indicates alignment to the full span
     *   away from the origin; an alignment of 0.5 would be the
     *   center of the view
     * @throws IllegalArgumentException for an invalid axis
     */
    public float getAlignment(int axis) {
        checkRequests(axis);
        if (axis == majorAxis) {
            return majorRequest.alignment;
        } else {
            return minorRequest.alignment;
        }
    }

    /**
     * Determines the preferred span for this view along an
     * axis.
     *
     * @param axis may be either <code>View.X_AXIS</code>
     *           or <code>View.Y_AXIS</code>
     * @return   the span the view would like to be rendered into &gt;= 0;
     *           typically the view is told to render into the span
     *           that is returned, although there is no guarantee;
     *           the parent may choose to resize or break the view
     * @throws IllegalArgumentException for an invalid axis type
     */
    public float getPreferredSpan(int axis) {
        checkRequests(axis);
        float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
            getTopInset() + getBottomInset();
        if (axis == majorAxis) {
            return ((float)majorRequest.preferred) + marginSpan;
        } else {
            return ((float)minorRequest.preferred) + marginSpan;
        }
    }

    /**
     * Determines the minimum span for this view along an
     * axis.
     *
     * @param axis may be either <code>View.X_AXIS</code>
     *           or <code>View.Y_AXIS</code>
     * @return  the span the view would like to be rendered into &gt;= 0;
     *           typically the view is told to render into the span
     *           that is returned, although there is no guarantee;
     *           the parent may choose to resize or break the view
     * @throws IllegalArgumentException for an invalid axis type
     */
    public float getMinimumSpan(int axis) {
        checkRequests(axis);
        float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
            getTopInset() + getBottomInset();
        if (axis == majorAxis) {
            return ((float)majorRequest.minimum) + marginSpan;
        } else {
            return ((float)minorRequest.minimum) + marginSpan;
        }
    }

    /**
     * Determines the maximum span for this view along an
     * axis.
     *
     * @param axis may be either <code>View.X_AXIS</code>
     *           or <code>View.Y_AXIS</code>
     * @return   the span the view would like to be rendered into &gt;= 0;
     *           typically the view is told to render into the span
     *           that is returned, although there is no guarantee;
     *           the parent may choose to resize or break the view
     * @throws IllegalArgumentException for an invalid axis type
     */
    public float getMaximumSpan(int axis) {
        checkRequests(axis);
        float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
            getTopInset() + getBottomInset();
        if (axis == majorAxis) {
            return ((float)majorRequest.maximum) + marginSpan;
        } else {
            return ((float)minorRequest.maximum) + marginSpan;
        }
    }

    // --- local methods ----------------------------------------------------

    /**
     * Are the allocations for the children still
     * valid?
     *
     * @return true if allocations still valid
     */
    protected boolean isAllocationValid() {
        return (majorAllocValid && minorAllocValid);
    }

    /**
     * Determines if a point falls before an allocated region.
     *
     * @param x the X coordinate &gt;= 0
     * @param y the Y coordinate &gt;= 0
     * @param innerAlloc the allocated region; this is the area
     *   inside of the insets
     * @return true if the point lies before the region else false
     */
    protected boolean isBefore(int x, int y, Rectangle innerAlloc) {
        if (majorAxis == View.X_AXIS) {
            return (x < innerAlloc.x);
        } else {
            return (y < innerAlloc.y);
        }
    }

    /**
     * Determines if a point falls after an allocated region.
     *
     * @param x the X coordinate &gt;= 0
     * @param y the Y coordinate &gt;= 0
     * @param innerAlloc the allocated region; this is the area
     *   inside of the insets
     * @return true if the point lies after the region else false
     */
    protected boolean isAfter(int x, int y, Rectangle innerAlloc) {
        if (majorAxis == View.X_AXIS) {
            return (x > (innerAlloc.width + innerAlloc.x));
        } else {
            return (y > (innerAlloc.height + innerAlloc.y));
        }
    }

    /**
     * Fetches the child view at the given coordinates.
     *
     * @param x the X coordinate &gt;= 0
     * @param y the Y coordinate &gt;= 0
     * @param alloc the parents inner allocation on entry, which should
     *   be changed to the child's allocation on exit
     * @return the view
     */
    protected View getViewAtPoint(int x, int y, Rectangle alloc) {
        int n = getViewCount();
        if (majorAxis == View.X_AXIS) {
            if (x < (alloc.x + majorOffsets[0])) {
                childAllocation(0, alloc);
                return getView(0);
            }
            for (int i = 0; i < n; i++) {
                if (x < (alloc.x + majorOffsets[i])) {
                    childAllocation(i - 1, alloc);
                    return getView(i - 1);
                }
            }
            childAllocation(n - 1, alloc);
            return getView(n - 1);
        } else {
            if (y < (alloc.y + majorOffsets[0])) {
                childAllocation(0, alloc);
                return getView(0);
            }
            for (int i = 0; i < n; i++) {
                if (y < (alloc.y + majorOffsets[i])) {
                    childAllocation(i - 1, alloc);
                    return getView(i - 1);
                }
            }
            childAllocation(n - 1, alloc);
            return getView(n - 1);
        }
    }

    /**
     * Allocates a region for a child view.
     *
     * @param index the index of the child view to
     *   allocate, &gt;= 0 &amp;&amp; &lt; getViewCount()
     * @param alloc the allocated region
     */
    protected void childAllocation(int index, Rectangle alloc) {
        alloc.x += getOffset(X_AXIS, index);
        alloc.y += getOffset(Y_AXIS, index);
        alloc.width = getSpan(X_AXIS, index);
        alloc.height = getSpan(Y_AXIS, index);
    }

    /**
     * Perform layout on the box
     *
     * @param width the width (inside of the insets) &gt;= 0
     * @param height the height (inside of the insets) &gt;= 0
     */
    protected void layout(int width, int height) {
        setSpanOnAxis(X_AXIS, width);
        setSpanOnAxis(Y_AXIS, height);
    }

    /**
     * Returns the current width of the box.  This is the width that
     * it was last allocated.
     * @return the current width of the box
     */
    public int getWidth() {
        int span;
        if (majorAxis == X_AXIS) {
            span = majorSpan;
        } else {
            span = minorSpan;
        }
        span += getLeftInset() - getRightInset();
        return span;
    }

    /**
     * Returns the current height of the box.  This is the height that
     * it was last allocated.
     * @return the current height of the box
     */
    public int getHeight() {
        int span;
        if (majorAxis == Y_AXIS) {
            span = majorSpan;
        } else {
            span = minorSpan;
        }
        span += getTopInset() - getBottomInset();
        return span;
    }

    /**
     * Performs layout for the major axis of the box (i.e. the
     * axis that it represents). The results of the layout (the
     * offset and span for each children) are placed in the given
     * arrays which represent the allocations to the children
     * along the major axis.
     *
     * @param targetSpan the total span given to the view, which
     *  would be used to layout the children
     * @param axis the axis being laid out
     * @param offsets the offsets from the origin of the view for
     *  each of the child views; this is a return value and is
     *  filled in by the implementation of this method
     * @param spans the span of each child view; this is a return
     *  value and is filled in by the implementation of this method
     */
    protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
        /*
         * first pass, calculate the preferred sizes
         * and the flexibility to adjust the sizes.
         */
        long preferred = 0;
        int n = getViewCount();
        for (int i = 0; i < n; i++) {
            View v = getView(i);
            spans[i] = (int) v.getPreferredSpan(axis);
            preferred += spans[i];
        }

        /*
         * Second pass, expand or contract by as much as possible to reach
         * the target span.
         */

        // determine the adjustment to be made
        long desiredAdjustment = targetSpan - preferred;
        float adjustmentFactor = 0.0f;
        int[] diffs = null;

        if (desiredAdjustment != 0) {
            long totalSpan = 0;
            diffs = new int[n];
            for (int i = 0; i < n; i++) {
                View v = getView(i);
                int tmp;
                if (desiredAdjustment < 0) {
                    tmp = (int)v.getMinimumSpan(axis);
                    diffs[i] = spans[i] - tmp;
                } else {
                    tmp = (int)v.getMaximumSpan(axis);
                    diffs[i] = tmp - spans[i];
                }
                totalSpan += tmp;
            }

            float maximumAdjustment = Math.abs(totalSpan - preferred);
                adjustmentFactor = desiredAdjustment / maximumAdjustment;
                adjustmentFactor = Math.min(adjustmentFactor, 1.0f);
                adjustmentFactor = Math.max(adjustmentFactor, -1.0f);
            }

        // make the adjustments
        int totalOffset = 0;
        for (int i = 0; i < n; i++) {
            offsets[i] = totalOffset;
            if (desiredAdjustment != 0) {
                float adjF = adjustmentFactor * diffs[i];
                spans[i] += Math.round(adjF);
            }
            totalOffset = (int) Math.min((long) totalOffset + (long) spans[i], Integer.MAX_VALUE);
        }
    }

    /**
     * Performs layout for the minor axis of the box (i.e. the
     * axis orthogonal to the axis that it represents). The results
     * of the layout (the offset and span for each children) are
     * placed in the given arrays which represent the allocations to
     * the children along the minor axis.
     *
     * @param targetSpan the total span given to the view, which
     *  would be used to layout the children
     * @param axis the axis being laid out
     * @param offsets the offsets from the origin of the view for
     *  each of the child views; this is a return value and is
     *  filled in by the implementation of this method
     * @param spans the span of each child view; this is a return
     *  value and is filled in by the implementation of this method
     */
    protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
        int n = getViewCount();
        for (int i = 0; i < n; i++) {
            View v = getView(i);
            int max = (int) v.getMaximumSpan(axis);
            if (max < targetSpan) {
                // can't make the child this wide, align it
                float align = v.getAlignment(axis);
                offsets[i] = (int) ((targetSpan - max) * align);
                spans[i] = max;
            } else {
                // make it the target width, or as small as it can get.
                int min = (int)v.getMinimumSpan(axis);
                offsets[i] = 0;
                spans[i] = Math.max(min, targetSpan);
            }
        }
    }

    /**
     * Calculates the size requirements for the major axis
     * <code>axis</code>.
     *
     * @param axis the axis being studied
     * @param r the <code>SizeRequirements</code> object;
     *          if <code>null</code> one will be created
     * @return the newly initialized <code>SizeRequirements</code> object
     * @see javax.swing.SizeRequirements
     */
    protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
        // calculate tiled request
        float min = 0;
        float pref = 0;
        float max = 0;

        int n = getViewCount();
        for (int i = 0; i < n; i++) {
            View v = getView(i);
            min += v.getMinimumSpan(axis);
            pref += v.getPreferredSpan(axis);
            max += v.getMaximumSpan(axis);
        }

        if (r == null) {
            r = new SizeRequirements();
        }
        r.alignment = 0.5f;
        r.minimum = (int) min;
        r.preferred = (int) pref;
        r.maximum = (int) max;
        return r;
    }

    /**
     * Calculates the size requirements for the minor axis
     * <code>axis</code>.
     *
     * @param axis the axis being studied
     * @param r the <code>SizeRequirements</code> object;
     *          if <code>null</code> one will be created
     * @return the newly initialized <code>SizeRequirements</code> object
     * @see javax.swing.SizeRequirements
     */
    protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
        int min = 0;
        long pref = 0;
        int max = Integer.MAX_VALUE;
        int n = getViewCount();
        for (int i = 0; i < n; i++) {
            View v = getView(i);
            min = Math.max((int) v.getMinimumSpan(axis), min);
            pref = Math.max((int) v.getPreferredSpan(axis), pref);
            max = Math.max((int) v.getMaximumSpan(axis), max);
        }

        if (r == null) {
            r = new SizeRequirements();
            r.alignment = 0.5f;
        }
        r.preferred = (int) pref;
        r.minimum = min;
        r.maximum = max;
        return r;
    }

    /**
     * Checks the request cache and update if needed.
     * @param axis the axis being studied
     * @throws IllegalArgumentException if <code>axis</code> is
     *  neither <code>View.X_AXIS</code> nor <code>View.Y_AXIS</code>
     */
    void checkRequests(int axis) {
        if ((axis != X_AXIS) && (axis != Y_AXIS)) {
            throw new IllegalArgumentException("Invalid axis: " + axis);
        }
        if (axis == majorAxis) {
            if (!majorReqValid) {
                majorRequest = calculateMajorAxisRequirements(axis,
                                                              majorRequest);
                majorReqValid = true;
            }
        } else if (! minorReqValid) {
            minorRequest = calculateMinorAxisRequirements(axis, minorRequest);
            minorReqValid = true;
        }
    }

    /**
     * Computes the location and extent of each child view
     * in this <code>BoxView</code> given the <code>targetSpan</code>,
     * which is the width (or height) of the region we have to
     * work with.
     *
     * @param targetSpan the total span given to the view, which
     *  would be used to layout the children
     * @param axis the axis being studied, either
     *          <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
     * @param offsets an empty array filled by this method with
     *          values specifying the location  of each child view
     * @param spans  an empty array filled by this method with
     *          values specifying the extent of each child view
     */
    protected void baselineLayout(int targetSpan, int axis, int[] offsets, int[] spans) {
        int totalAscent = (int)(targetSpan * getAlignment(axis));
        int totalDescent = targetSpan - totalAscent;

        int n = getViewCount();

        for (int i = 0; i < n; i++) {
            View v = getView(i);
            float align = v.getAlignment(axis);
            float viewSpan;

            if (v.getResizeWeight(axis) > 0) {
                // if resizable then resize to the best fit

                // the smallest span possible
                float minSpan = v.getMinimumSpan(axis);
                // the largest span possible
                float maxSpan = v.getMaximumSpan(axis);

                if (align == 0.0f) {
                    // if the alignment is 0 then we need to fit into the descent
                    viewSpan = Math.max(Math.min(maxSpan, totalDescent), minSpan);
                } else if (align == 1.0f) {
                    // if the alignment is 1 then we need to fit into the ascent
                    viewSpan = Math.max(Math.min(maxSpan, totalAscent), minSpan);
                } else {
                    // figure out the span that we must fit into
                    float fitSpan = Math.min(totalAscent / align,
                                             totalDescent / (1.0f - align));
                    // fit into the calculated span
                    viewSpan = Math.max(Math.min(maxSpan, fitSpan), minSpan);
                }
            } else {
                // otherwise use the preferred spans
                viewSpan = v.getPreferredSpan(axis);
            }

            offsets[i] = totalAscent - (int)(viewSpan * align);
            spans[i] = (int)viewSpan;
        }
    }

    /**
     * Calculates the size requirements for this <code>BoxView</code>
     * by examining the size of each child view.
     *
     * @param axis the axis being studied
     * @param r the <code>SizeRequirements</code> object;
     *          if <code>null</code> one will be created
     * @return the newly initialized <code>SizeRequirements</code> object
     */
    protected SizeRequirements baselineRequirements(int axis, SizeRequirements r) {
        SizeRequirements totalAscent = new SizeRequirements();
        SizeRequirements totalDescent = new SizeRequirements();

        if (r == null) {
            r = new SizeRequirements();
        }

        r.alignment = 0.5f;

        int n = getViewCount();

        // loop through all children calculating the max of all their ascents and
        // descents at minimum, preferred, and maximum sizes
        for (int i = 0; i < n; i++) {
            View v = getView(i);
            float align = v.getAlignment(axis);
            float span;
            int ascent;
            int descent;

            // find the maximum of the preferred ascents and descents
            span = v.getPreferredSpan(axis);
            ascent = (int)(align * span);
            descent = (int)(span - ascent);
            totalAscent.preferred = Math.max(ascent, totalAscent.preferred);
            totalDescent.preferred = Math.max(descent, totalDescent.preferred);

            if (v.getResizeWeight(axis) > 0) {
                // if the view is resizable then do the same for the minimum and
                // maximum ascents and descents
                span = v.getMinimumSpan(axis);
                ascent = (int)(align * span);
                descent = (int)(span - ascent);
                totalAscent.minimum = Math.max(ascent, totalAscent.minimum);
                totalDescent.minimum = Math.max(descent, totalDescent.minimum);

                span = v.getMaximumSpan(axis);
                ascent = (int)(align * span);
                descent = (int)(span - ascent);
                totalAscent.maximum = Math.max(ascent, totalAscent.maximum);
                totalDescent.maximum = Math.max(descent, totalDescent.maximum);
            } else {
                // otherwise use the preferred
                totalAscent.minimum = Math.max(ascent, totalAscent.minimum);
                totalDescent.minimum = Math.max(descent, totalDescent.minimum);
                totalAscent.maximum = Math.max(ascent, totalAscent.maximum);
                totalDescent.maximum = Math.max(descent, totalDescent.maximum);
            }
        }

        // we now have an overall preferred, minimum, and maximum ascent and descent

        // calculate the preferred span as the sum of the preferred ascent and preferred descent
        r.preferred = (int)Math.min((long)totalAscent.preferred + (long)totalDescent.preferred,
                                    Integer.MAX_VALUE);

        // calculate the preferred alignment as the preferred ascent divided by the preferred span
        if (r.preferred > 0) {
            r.alignment = (float)totalAscent.preferred / r.preferred;
        }


        if (r.alignment == 0.0f) {
            // if the preferred alignment is 0 then the minimum and maximum spans are simply
            // the minimum and maximum descents since there's nothing above the baseline
            r.minimum = totalDescent.minimum;
            r.maximum = totalDescent.maximum;
        } else if (r.alignment == 1.0f) {
            // if the preferred alignment is 1 then the minimum and maximum spans are simply
            // the minimum and maximum ascents since there's nothing below the baseline
            r.minimum = totalAscent.minimum;
            r.maximum = totalAscent.maximum;
        } else {
            // we want to honor the preferred alignment so we calculate two possible minimum
            // span values using 1) the minimum ascent and the alignment, and 2) the minimum
            // descent and the alignment. We'll choose the larger of these two numbers.
            r.minimum = Math.round(Math.max(totalAscent.minimum / r.alignment,
                                          totalDescent.minimum / (1.0f - r.alignment)));
            // a similar calculation is made for the maximum but we choose the smaller number.
            r.maximum = Math.round(Math.min(totalAscent.maximum / r.alignment,
                                          totalDescent.maximum / (1.0f - r.alignment)));
        }

        return r;
    }

    /**
     * Fetches the offset of a particular child's current layout.
     * @param axis the axis being studied
     * @param childIndex the index of the requested child
     * @return the offset (location) for the specified child
     */
    protected int getOffset(int axis, int childIndex) {
        int[] offsets = (axis == majorAxis) ? majorOffsets : minorOffsets;
        return offsets[childIndex];
    }

    /**
     * Fetches the span of a particular child's current layout.
     * @param axis the axis being studied
     * @param childIndex the index of the requested child
     * @return the span (width or height) of the specified child
     */
    protected int getSpan(int axis, int childIndex) {
        int[] spans = (axis == majorAxis) ? majorSpans : minorSpans;
        return spans[childIndex];
    }

    /**
     * Determines in which direction the next view lays.
     * Consider the View at index n. Typically the <code>View</code>s
     * are laid out from left to right, so that the <code>View</code>
     * to the EAST will be at index n + 1, and the <code>View</code>
     * to the WEST will be at index n - 1. In certain situations,
     * such as with bidirectional text, it is possible
     * that the <code>View</code> to EAST is not at index n + 1,
     * but rather at index n - 1, or that the <code>View</code>
     * to the WEST is not at index n - 1, but index n + 1.
     * In this case this method would return true,
     * indicating the <code>View</code>s are laid out in
     * descending order. Otherwise the method would return false
     * indicating the <code>View</code>s are laid out in ascending order.
     * <p>
     * If the receiver is laying its <code>View</code>s along the
     * <code>Y_AXIS</code>, this will return the value from
     * invoking the same method on the <code>View</code>
     * responsible for rendering <code>position</code> and
     * <code>bias</code>. Otherwise this will return false.
     *
     * @param position position into the model
     * @param bias either <code>Position.Bias.Forward</code> or
     *          <code>Position.Bias.Backward</code>
     * @return true if the <code>View</code>s surrounding the
     *          <code>View</code> responding for rendering
     *          <code>position</code> and <code>bias</code>
     *          are laid out in descending order; otherwise false
     */
    protected boolean flipEastAndWestAtEnds(int position,
                                            Position.Bias bias) {
        if(majorAxis == Y_AXIS) {
            int testPos = (bias == Position.Bias.Backward) ?
                          Math.max(0, position - 1) : position;
            int index = getViewIndexAtPosition(testPos);
            if(index != -1) {
                View v = getView(index);
                if (v instanceof CompositeView compositeView) {
                    return compositeView.flipEastAndWestAtEnds(position,
                                                               bias);
                }
            }
        }
        return false;
    }

    // --- variables ------------------------------------------------

    int majorAxis;

    int majorSpan;
    int minorSpan;

    /*
     * Request cache
     */
    boolean majorReqValid;
    boolean minorReqValid;
    SizeRequirements majorRequest;
    SizeRequirements minorRequest;

    /*
     * Allocation cache
     */
    boolean majorAllocValid;
    int[] majorOffsets;
    int[] majorSpans;
    boolean minorAllocValid;
    int[] minorOffsets;
    int[] minorSpans;

    /** used in paint. */
    Rectangle tempRect;
}
