/*
 * @(#)ZooGroup.java    1.1  23 August 2001
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.gui.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;

/**
 * <P>Associative collection of components contained 
 * within a <CODE>{@link Zoo Zoo}</CODE>.
 *
 * Groups may be nested to an arbitrary depth.</P>
 *
 * <P>By default, a group has a transparent background
 * and its bounds are determined by the union of the bounds
 * for the components it groups.
 *
 * When a group is resized, its contained components are scaled
 * accordingly so that they maintain the same relative
 * size and location within the group.
 *
 * The minimum and maximum sizes for a group are determined
 * by minimum and maximum scaling factors that can be applied
 * to the contained components without violating
 * the minimum and maximum sizes 
 * for any of the contained components.</P>
 *
 * <P>This panel assumes an <CODE>AbsoluteLayout</CODE>.
 *
 * Behavior is undefined if this panel is set to have a layout
 * other than an <CODE>AbsoluteLayout</CODE>.</P>
 *
 * <P>A grouped component resized through direct manipulation 
 * will appear to scale in size as the group is resized,
 * whether or not the component will actually scale in size
 * when the resize is completed.
 *
 * If the parent container for the grouped component 
 * does not scale its contents, the grouped component
 * will snap back to its original size 
 * when the resize is completed.</P>
 *
 * @author  Jeff Raab
 * @version 2.2
 * @since   1.1
 * @see     Zoo
 */
public class ZooGroup extends ZooContainer {
    
    /** 
     * Previous size of the group before 
     * a direct manipulation resize was initiated. 
     */
    protected Dimension oldSize = null;
    
    /**
     * Temporary transform applied to the graphics context
     * for this group during a direct manipulation resize.
     */
    protected AffineTransform transform = new AffineTransform();

    /**
     * Creates a new group containing 
     * the given vector of components.
     *
     * @param v a vector of components to group
     */
    public ZooGroup(Vector v) {
        setOpaque(false);
        
        // get grouped components
        Component[] c = (Component[])v.toArray(new Component[0]);

        // find and set group bounds
        Rectangle bounds = getBoundingBox(c);
        if (bounds != null)
            super.setBounds(bounds);
        
        // add translated components
        for (int i = 0; i < c.length; i++) {
            c[i].setLocation(
                c[i].getX() - getX(), 
                c[i].getY() - getY());

            add(c[i]);
        }
    }
    
    ////////////////
    // Public API //
    ////////////////

    /**
     * Sets the location of this group to the given position.
     *
     * @param x the <I>x</I>-coordinate of the desired position
     * @param y the <I>y</I>-coordinate of the desired position
     * @see #setLocation(Point)
     */
    public void setLocation(int x, int y) {
        setBoundsImpl(x, y, getWidth(), getHeight());
    }

    /**
     * Sets the location of this group to the given point.
     *
     * @param p the desired point
     * @see #setLocation(int, int)
     */
    public void setLocation(Point p) {
        setBoundsImpl(p.x, p.y, getWidth(), getHeight());
    }

    /**
     * Sets the size of this group to the given width and height.
     *
     * @param width the width of the desired size
     * @param height the height of the desired size
     * @see #setSize(Dimension)
     */
    public void setSize(int width, int height) {
        setBoundsImpl(getX(), getY(), width, height);
    }

    /**
     * Sets the size of this group to the given dimension.
     *
     * @param size the desired size
     * @see #setSize(int, int)
     */
    public void setSize(Dimension size) {
        setBoundsImpl(getX(), getY(), size.width, size.height);
    }

    /**
     * Sets the bounds of this group 
     * to the given position, width and height.
     *
     * @param x the <I>x</I>-coordinate of the desired position
     * @param y the <I>y</I>-coordinate of the desired position
     * @param width the width of the desired size
     * @param height the height of the desired size
     * @see #setBounds(Rectangle)
     */
    public void setBounds(int x, int y, int width, int height) {
        setBoundsImpl(x, y, width, height);
    }

    /**
     * Sets the bounds of this group to the given rectangle.
     *
     * @param bounds the desired bounds
     * @see #setBounds(int, int, int, int)
     */
    public void setBounds(Rectangle bounds) {
        setBoundsImpl(bounds.x, bounds.y, bounds.width, bounds.height);
    }
    
    /** Called when the native peer for this group is created. */
    public void addNotify() {
        super.addNotify();
    
        updateBoundsRestrictions();
    }

    /** 
     * Paints this group to the given graphics context.
     *
     * @param g the graphics context to which to paint
     */
    public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;

        AffineTransform oldTransform = g2.getTransform();
        
        paintBorder(g);
        paintComponent(g);

        AffineTransform test = new AffineTransform(oldTransform);
        test.concatenate(new AffineTransform(transform));
        g2.setTransform(test);

        paintChildren(g);
        
        g2.setTransform(oldTransform);
    }

    ///////////////////////
    // Protected methods //
    ///////////////////////
    
    /** 
     * Returns the minimum size for this group,
     * calculated from its current size and 
     * the largest ratio of minimum size to current size
     * among the contained components.
     *
     * @see #calculateMaximumSize()
     */
    protected Dimension calculateMinimumSize() {
        Component c   = getComponent(0);
        Dimension min = c.getMinimumSize();

        // get initial ratio
        double dx = 0,
               dy = 0;

        dx = ((c.getWidth() == 0) ? 
            Double.MAX_VALUE : 
            (double)min.width / c.getWidth());
        dy = ((c.getHeight() == 0) ? 
            Double.MAX_VALUE : 
            (double)min.height / c.getHeight());
        
        // find minimum ratio for components
        for (int i = 1; i < getComponentCount(); i++) {
            c   = getComponent(i);
            min = c.getMinimumSize();
            
            dx = ((c.getWidth() == 0) ? 
                Double.MAX_VALUE : 
                Math.max((double)min.width / c.getWidth(), dx));
            dy = ((c.getHeight() == 0) ? 
                Double.MAX_VALUE : 
                Math.max((double)min.height / c.getHeight(), dy));
        }
        
        // calculate minimum width and height
        double width  = dx * getWidth(),
               height = dy * getHeight(); 

        // return calculated minimum size
        return new Dimension(
            ((width  > Integer.MAX_VALUE) ? 
                Integer.MAX_VALUE : 
                (int)(width  + 1)),
            ((height > Integer.MAX_VALUE) ? 
                Integer.MAX_VALUE : 
                (int)(height + 1)));
    }
    
    /** 
     * Returns the minimum size for this group,
     * calculated from its current size and 
     * the smallest ratio of maximum size to current size
     * among the contained components.
     *
     * @see #calculateMinimumSize()
     */
    protected Dimension calculateMaximumSize() {
        Component c   = getComponent(0);
        Dimension max = c.getMaximumSize();

        // get initial ratio
        double dx = 0,
               dy = 0;

        dx = ((c.getWidth() == 0) ? 
            Double.MAX_VALUE : 
            (double)max.width / c.getWidth());
        dy = ((c.getHeight() == 0) ? 
            Double.MAX_VALUE : 
            (double)max.height / c.getHeight());
        
        // find maximum ratio for components
        for (int i = 1; i < getComponentCount(); i++) {
            c   = getComponent(i);
            max = c.getMaximumSize();
            
            dx = ((c.getWidth() == 0) ? 
                dx : 
                Math.min((double)max.width / c.getWidth(), dx));
            dy = ((c.getHeight() == 0) ? 
                dy : 
                Math.min((double)max.height / c.getHeight(), dy));
        }
        
        // calculate maximum width and height
        double width  = dx * getWidth(),
               height = dy * getHeight(); 

        // return calculated maximum size
        return new Dimension(
            ((width  > Integer.MAX_VALUE) ? 
                Integer.MAX_VALUE : 
                (int)width),
            ((height > Integer.MAX_VALUE) ? 
                Integer.MAX_VALUE : 
                (int)height));
    }
    
    ///////////////////////////////
    // Package-protected methods //
    ///////////////////////////////

    /** Initiates a direct manipulation resize. */
    void startResize() {
        oldSize = getSize();
    }
    
    /** Completes a direct manipulation resize. */
    void endResize() {
        transform = new AffineTransform();

        // find scaling transform
        Rectangle bounds = getBounds();
        double dx = (double)bounds.width  / oldSize.width,
               dy = (double)bounds.height / oldSize.height;
        
        // scale contained components
        Component[] child = getComponents();
        for (int i = 0; i < child.length; i++) {
            bounds = child[i].getBounds();
            child[i].setBounds(
                (int)(bounds.x      * dx + 0.5),
                (int)(bounds.y      * dy + 0.5),
                (int)(bounds.width  * dx + 0.5),
                (int)(bounds.height * dy + 0.5));
            revalidate(child[i]);
        }

        oldSize = null;

        // recalculate extrema
        updateBoundsRestrictions();
    }

    /////////////////////
    // Private methods //
    /////////////////////

    /** Updates the stored minimum and maximum sizes for this group. */
    private void updateBoundsRestrictions() {
        if (getComponentCount() > 0) {
            setMinimumSize(calculateMinimumSize());
            setMaximumSize(calculateMaximumSize());
        }
    }

    /** Resizes this group and its contained components. */
    private void setBoundsImpl(int x, int y, int width, int height) {

        // if a direct manipulation resize is in effect
        if (oldSize != null) {
            transform = AffineTransform.getScaleInstance(
                (double)width  / oldSize.width,
                (double)height / oldSize.height);
        }
        
        // if a direct manipulation resize is not in effect
        else {

            // find scaling transform
            Rectangle bounds = getBounds();
            double dx = (double)width  / bounds.width,
                   dy = (double)height / bounds.height;

            // scale contained component bounds
            Component[] child = getComponents();
            for (int i = 0; i < child.length; i++) {
                bounds = child[i].getBounds();
                child[i].setBounds(
                    (int)(bounds.x      * dx + 0.5),
                    (int)(bounds.y      * dy + 0.5),
                    (int)(bounds.width  * dx + 0.5),
                    (int)(bounds.height * dy + 0.5));
                revalidate(child[i]);
            }
        }

        // set the bounds of this component
        super.setBounds(x, y, width, height);
    }

    /** Returns the union of the bounds of the given components. */
    private Rectangle getBoundingBox(Component[] c) {
        if ((c == null) || (c.length == 0))
            return null;
            
        Rectangle bounds = new Rectangle(c[0].getBounds());

        for (int i = 1; i < c.length; i++)
            bounds = bounds.union(c[i].getBounds());

        return bounds;
    }

    /** Revalidates the given component, if appropriate. */
    private void revalidate(Component c) {
        if (c instanceof JComponent) {
            JComponent jc = (JComponent)c;
            jc.revalidate();
            return;
        }

        if (c instanceof Container) {
            Container co = (Container)c;
            co.validate();
            return;
        }
    }
}
