/*
 * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolox.swing;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Rectangle;

import javax.swing.JScrollPane;
import javax.swing.ScrollPaneLayout;
import javax.swing.border.Border;

import edu.umd.cs.piccolo.util.PBounds;

/**
 * A subclass of ScrollPaneLayout that looks at the Viewport for sizing
 * information rather than View. Also queries the Viewport for sizing
 * information after each decision about scrollbar visiblity.
 * 
 * @author Lance Good
 */
public class PScrollPaneLayout extends ScrollPaneLayout {
    private static final long serialVersionUID = 1L;

    /**
     * MODIFIED FROM javax.swing.ScrollPaneLayout.layoutContainer.
     * 
     * This is largely the same as ScrollPaneLayout.layoutContainer but obtains
     * the preferred view size from the viewport rather than directly from the
     * view so the viewport can get the preferred size from the PScrollDirector
     * 
     * @param parent the Container to lay out
     */
    public void layoutContainer(final Container parent) {
        // Sync the (now obsolete) policy fields with the JScrollPane.
        if (!(parent instanceof JScrollPane)) {
            throw new IllegalArgumentException("layoutContainer may only be applied to JScrollPanes");
        }
        final JScrollPane scrollPane = (JScrollPane) parent;
        vsbPolicy = scrollPane.getVerticalScrollBarPolicy();
        hsbPolicy = scrollPane.getHorizontalScrollBarPolicy();

        final Rectangle availR = scrollPane.getBounds();
        availR.setLocation(0, 0);

        final Insets insets = parent.getInsets();
        availR.x = insets.left;
        availR.y = insets.top;
        availR.width -= insets.left + insets.right;
        availR.height -= insets.top + insets.bottom;

        // Get the scrollPane's orientation.
        final boolean leftToRight = scrollPane.getComponentOrientation().isLeftToRight();

        /*
         * If there's a visible column header remove the space it needs from the
         * top of availR. The column header is treated as if it were fixed
         * height, arbitrary width.
         */
        final Rectangle colHeadR = new Rectangle(0, availR.y, 0, 0);

        if (colHead != null && colHead.isVisible()) {
            final int colHeadHeight = colHead.getPreferredSize().height;
            colHeadR.height = colHeadHeight;
            availR.y += colHeadHeight;
            availR.height -= colHeadHeight;
        }

        /*
         * If there's a visible row header remove the space it needs from the
         * left or right of availR. The row header is treated as if it were
         * fixed width, arbitrary height.
         */
        final Rectangle rowHeadR = new Rectangle(0, 0, 0, 0);

        if (rowHead != null && rowHead.isVisible()) {
            final int rowHeadWidth = rowHead.getPreferredSize().width;
            rowHeadR.width = rowHeadWidth;
            availR.width -= rowHeadWidth;
            if (leftToRight) {
                rowHeadR.x = availR.x;
                availR.x += rowHeadWidth;
            }
            else {
                rowHeadR.x = availR.x + availR.width;
            }
        }

        /*
         * If there's a JScrollPane.viewportBorder, remove the space it occupies
         * for availR.
         */
        final Border viewportBorder = scrollPane.getViewportBorder();
        Insets vpbInsets;
        if (viewportBorder != null) {
            vpbInsets = viewportBorder.getBorderInsets(parent);
            availR.x += vpbInsets.left;
            availR.y += vpbInsets.top;
            availR.width -= vpbInsets.left + vpbInsets.right;
            availR.height -= vpbInsets.top + vpbInsets.bottom;
        }
        else {
            vpbInsets = new Insets(0, 0, 0, 0);
        }

        /*
         * At this point availR is the space available for the viewport and
         * scrollbars. rowHeadR is correct except for its height and y and
         * colHeadR is correct except for its width and x. Once we're through
         * computing the dimensions of these three parts we can go back and set
         * the dimensions of rowHeadR.height, rowHeadR.y, colHeadR.width,
         * colHeadR.x and the bounds for the corners.
         * 
         * We'll decide about putting up scrollbars by comparing the viewport
         * views preferred size with the viewports extent size (generally just
         * its size). Using the preferredSize is reasonable because layout
         * proceeds top down - so we expect the viewport to be layed out next.
         * And we assume that the viewports layout manager will give the view
         * it's preferred size.
         */
        Dimension extentSize = getExtentSize(availR);

        final PBounds cameraBounds = new PBounds(0, 0, extentSize.getWidth(), extentSize.getHeight());

        // LEG: Modification to ask the viewport for the view size rather
        // than asking the view directly
        Dimension viewPrefSize = getViewSize(cameraBounds);

        /*
         * If there's a vertical scrollbar and we need one, allocate space for
         * it (we'll make it visible later). A vertical scrollbar is considered
         * to be fixed width, arbitrary height.
         */
        final Rectangle vsbR = new Rectangle(0, availR.y - vpbInsets.top, 0, 0);

        boolean vsbNeeded;
        if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) {
            vsbNeeded = true;
        }
        else if (vsbPolicy == VERTICAL_SCROLLBAR_NEVER) {
            vsbNeeded = false;
        }
        else { // vsbPolicy == VERTICAL_SCROLLBAR_AS_NEEDED

            vsbNeeded = viewPrefSize.height > extentSize.height;
        }

        if (vsb != null && vsbNeeded) {
            adjustForVSB(true, availR, vsbR, vpbInsets, leftToRight);
            extentSize = viewport.toViewCoordinates(availR.getSize());

            // LEG: Modification because the view's preferred size needs to
            // be recomputed because the extent may have changed
            cameraBounds.setRect(0, 0, extentSize.getWidth(), extentSize.getHeight());
            viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds);
        }

        /*
         * If there's a horizontal scrollbar and we need one, allocate space for
         * it (we'll make it visible later). A horizontal scrollbar is
         * considered to be fixed height, arbitrary width.
         */
        final Rectangle hsbR = new Rectangle(availR.x - vpbInsets.left, 0, 0, 0);
        boolean hsbNeeded;
        if (hsbPolicy == HORIZONTAL_SCROLLBAR_ALWAYS) {
            hsbNeeded = true;
        }
        else if (hsbPolicy == HORIZONTAL_SCROLLBAR_NEVER) {
            hsbNeeded = false;
        }
        else { // hsbPolicy == HORIZONTAL_SCROLLBAR_AS_NEEDED
            hsbNeeded = viewPrefSize.width > extentSize.width;
        }

        if (hsb != null && hsbNeeded) {
            adjustForHSB(true, availR, hsbR, vpbInsets);

            /*
             * If we added the horizontal scrollbar then we've implicitly
             * reduced the vertical space available to the viewport. As a
             * consequence we may have to add the vertical scrollbar, if that
             * hasn't been done so already. Ofcourse we don't bother with any of
             * this if the vsbPolicy is NEVER.
             */
            if (vsb != null && !vsbNeeded && vsbPolicy != VERTICAL_SCROLLBAR_NEVER) {

                extentSize = viewport.toViewCoordinates(availR.getSize());

                // LEG: Modification because the view's preferred size needs to
                // be recomputed because the extent may have changed
                cameraBounds.setRect(0, 0, extentSize.getWidth(), extentSize.getHeight());
                viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds);

                vsbNeeded = viewPrefSize.height > extentSize.height;

                if (vsbNeeded) {
                    adjustForVSB(true, availR, vsbR, vpbInsets, leftToRight);
                }
            }
        }

        /*
         * Set the size of the viewport first, and then recheck the Scrollable
         * methods. Some components base their return values for the Scrollable
         * methods on the size of the Viewport, so that if we don't ask after
         * resetting the bounds we may have gotten the wrong answer.
         */
        if (viewport != null) {
            viewport.setBounds(availR);
        }

        /*
         * We now have the final size of the viewport: availR. Now fixup the
         * header and scrollbar widths/heights.
         */
        vsbR.height = availR.height + vpbInsets.top + vpbInsets.bottom;
        hsbR.width = availR.width + vpbInsets.left + vpbInsets.right;
        rowHeadR.height = availR.height + vpbInsets.top + vpbInsets.bottom;
        rowHeadR.y = availR.y - vpbInsets.top;
        colHeadR.width = availR.width + vpbInsets.left + vpbInsets.right;
        colHeadR.x = availR.x - vpbInsets.left;

        /*
         * Set the bounds of the remaining components. The scrollbars are made
         * invisible if they're not needed.
         */
        if (rowHead != null) {
            rowHead.setBounds(rowHeadR);
        }

        if (colHead != null) {
            colHead.setBounds(colHeadR);
        }

        if (vsb != null) {
            if (vsbNeeded) {
                vsb.setVisible(true);
                vsb.setBounds(vsbR);
            }
            else {
                vsb.setVisible(false);
            }
        }

        if (hsb != null) {
            if (hsbNeeded) {
                hsb.setVisible(true);
                hsb.setBounds(hsbR);
            }
            else {
                hsb.setVisible(false);
            }
        }

        if (lowerLeft != null) {
            if (leftToRight) {
                lowerLeft.setBounds(rowHeadR.x, hsbR.y, rowHeadR.width, hsbR.height);
            }
            else {
                lowerLeft.setBounds(vsbR.x, hsbR.y, vsbR.width, hsbR.height);
            }
        }

        if (lowerRight != null) {
            if (leftToRight) {
                lowerRight.setBounds(vsbR.x, hsbR.y, vsbR.width, hsbR.height);
            }
            else {
                lowerRight.setBounds(rowHeadR.x, hsbR.y, rowHeadR.width, hsbR.height);
            }
        }

        if (upperLeft != null) {
            if (leftToRight) {
                upperLeft.setBounds(rowHeadR.x, colHeadR.y, rowHeadR.width, colHeadR.height);
            }
            else {
                upperLeft.setBounds(vsbR.x, colHeadR.y, vsbR.width, colHeadR.height);
            }
        }

        if (upperRight != null) {
            if (leftToRight) {
                upperRight.setBounds(vsbR.x, colHeadR.y, vsbR.width, colHeadR.height);
            }
            else {
                upperRight.setBounds(rowHeadR.x, colHeadR.y, rowHeadR.width, colHeadR.height);
            }
        }
    }

    /**
     * @param cameraBounds
     * @return
     */
    private Dimension getViewSize(final PBounds cameraBounds) {
        Dimension viewPrefSize;
        if (viewport != null) {
            viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds);
        }
        else {
            viewPrefSize = new Dimension(0, 0);
        }
        return viewPrefSize;
    }

    /**
     * @param availR
     * @return
     */
    private Dimension getExtentSize(final Rectangle availR) {
        Dimension extentSize;
        if (viewport != null) {
            extentSize = viewport.toViewCoordinates(availR.getSize());
        }
        else {
            extentSize = new Dimension(0, 0);
        }
        return extentSize;
    }

    /**
     * Copied FROM javax.swing.ScrollPaneLayout.adjustForVSB.
     * 
     * This method is called from ScrollPaneLayout.layoutContainer and is
     * private in ScrollPaneLayout so it was copied here
     * 
     * @param wantsVSB whether to account for vertical scrollbar
     * @param available region to adjust
     * @param vsbR vertical scroll bar region
     * @param vpbInsets margin of vertical scroll bars
     * @param leftToRight orientation of the text LTR or RTL
     */
    protected void adjustForVSB(final boolean wantsVSB, final Rectangle available, final Rectangle vsbR,
            final Insets vpbInsets, final boolean leftToRight) {
        final int vsbWidth = vsb.getPreferredSize().width;
        if (wantsVSB) {
            available.width -= vsbWidth;
            vsbR.width = vsbWidth;

            if (leftToRight) {
                vsbR.x = available.x + available.width + vpbInsets.right;
            }
            else {
                vsbR.x = available.x - vpbInsets.left;
                available.x += vsbWidth;
            }
        }
        else {
            available.width += vsbWidth;
        }
    }

    /**
     * Copied FROM javax.swing.ScrollPaneLayout.adjustForHSB.
     * 
     * This method is called from ScrollPaneLayout.layoutContainer and is
     * private in ScrollPaneLayout so it was copied here
     * 
     * @param wantsHSB whether to account for horizontal scrollbar
     * @param available region to adjust
     * @param hsbR vertical scroll bar region
     * @param vpbInsets margin of the scroll bars
     */
    protected void adjustForHSB(final boolean wantsHSB, final Rectangle available, final Rectangle hsbR,
            final Insets vpbInsets) {
        final int hsbHeight = hsb.getPreferredSize().height;
        if (wantsHSB) {
            available.height -= hsbHeight;
            hsbR.y = available.y + available.height + vpbInsets.bottom;
            hsbR.height = hsbHeight;
        }
        else {
            available.height += hsbHeight;
        }
    }

    /**
     * The UI resource version of PScrollPaneLayout. It isn't clear why Swing
     * does this in ScrollPaneLayout but we'll do it here too just to be safe.
     */
    public static class UIResource extends PScrollPaneLayout implements javax.swing.plaf.UIResource {

        /**
         * 
         */
        private static final long serialVersionUID = 1L;
    }
}
