/*
 * @(#)Laminate.java    1.3  22 May 2002
 *
 * 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 edu.neu.ccs.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;

/**
 * Component used to "laminate" an existing component
 * by mimicing its size and location and always maintaining
 * a higher <I>z</I>-order, in order to impart mouse handling
 * without intervention by the laminated component.
 *
 * @author  Jeff Raab
 * @version 2.2
 * @since   1.1
 */
public class Laminate 
    extends JComponent 
    implements ComponentListener 
{
    /** The laminated component. */
    private Component target = null;

    /** Whether or not this laminate is activated. */
    private boolean activated = true;

    /** Whether or not this laminate is selected. */
    private boolean selected = false;

    /** Point at which a move was initiated. */
    private Point moveOffset = new Point();

    /** The mouse action adapter for this laminate. */
    private MouseActionAdapter adapter = null;

    /** 
     * Whether or not this laminate can be resized. 
     * @since 2.0
     */
    private boolean resizeable = true;
    
    /** 
     * Whether or not this laminate can be moved. 
     * @since 2.0
     */
    private boolean moveable = true;

    /** 
     * Whether or not this laminate is resizing
     * on the horizontal axis.
     */
    private boolean resizingX = false;

    /** 
     * Whether or not this laminate is resizing
     * on the vertical axis.
     */
    private boolean resizingY = false;
    
    /**
     * Constructs a laminate for the given component.
     *
     * @param c a component to laminate
     * @param isActivated whether or not this laminate is activated
     */
    public Laminate(Component c, boolean isActivated) {
        installMouseAdapter();

        setTarget(c);
        setActivated(isActivated);
    }

    ///////////////////////
    // ComponentListener //
    ///////////////////////

    /** Called when the laminated component is hidden. */
    public void componentHidden(ComponentEvent evt) {
        setVisible(target.isVisible());
    }

    /** Called when the laminated component is moved. */
    public void componentMoved(ComponentEvent evt) {
        setLocation(target.getLocation());

        getZoo().repaint();
    }

    /** Called when the laminated component is resized. */
    public void componentResized(ComponentEvent evt) {
        setSize(target.getSize());

        getZoo().repaint();
    }

    /** Called when the laminated component is shown. */
    public void componentShown(ComponentEvent evt) {
        setVisible(target.isVisible());
    }

    ////////////////
    // Public API //
    ////////////////

    /**
     * Sets the target component this laminate is covering
     * to the given component.
     *
     * If the given component is <CODE>null</CODE>, 
     * this laminate will not cover a component
     *
     * @param c a component to laminate
     * @see #getTarget()
     */
    public void setTarget(Component c) {
        if (target != null) {
            target.removeComponentListener(this);
            setActivated(false);
        }

        target = c;

        if (target != null) {
            target.addComponentListener(this);
            setActivated(true);

            setBounds(target.getBounds());
        }
    }

    /** 
     * Returns the component this laminate is covering. 
     *
     * @see #setTarget(Component)
     */
    public Component getTarget() {
        return target;
    }

    /**
     * Sets whether or not this laminate handles mouse events
     * targeted for the covered component.
     *
     * @param isActivated whether or not this laminate
     *      should handle mouse events
     * @see #isActivated()
     */
    public void setActivated(boolean isActivated) {
        if (activated != isActivated) {
            activated = isActivated;
        
            if (activated)
                adapter.addAsListenerTo(this);
            else
                adapter.removeAsListenerTo(this);
        }
    }
    
    /** 
     * Returns whether or not this laminate is activated. 
     *
     * @see #setActivated(boolean)
     */
    public boolean isActivated() {
        return activated;
    }

    /**
     * Sets whether or not this laminate is selected.
     *
     * @param isSelected whether or not this laminate
     *      is to be selected
     * @see #isSelected()
     */
    public void setSelected(boolean isSelected) {
        selected = isSelected;
        
        repaint();
    } 

    /** 
     * Returns whether or not this laminate is selected. 
     *
     * @see #setSelected(boolean)
     */
    public boolean isSelected() {
        return selected;
    }

    /** Returns the mouse action adapter for this laminate. */
    public MouseActionAdapter getMouseActionAdapter() {
        return adapter;
    }
    
    /**
     * Sets the location of this laminate and its laminated component
     * to the given position.
     *
     * @param x the <I>x</I>-coordinate of the position
     *      to which to move this laminate
     * @param y the <I>y</I>-coordinate of the position
     *      to which to move this laminate
     * @see #setLocation(Point)
     */
    public void setLocation(int x, int y) {
        setBoundsImpl(x, y, getWidth(), getHeight());
    }
     
    /**
     * Sets the location of this laminate and its laminated component
     * to the given point.
     *
     * @param p the point to which to move this laminate
     * @see #setLocation(int, int)
     */
    public void setLocation(Point p) {
        setBoundsImpl(p.x, p.y, getWidth(), getHeight());
    }

    /**
     * Sets the size of this laminate and its laminated component
     * to the given width and height.
     *
     * @param width the width to which to size this laminate
     * @param height the height to which to size this laminate
     * @see #setSize(Dimension)
     */
    public void setSize(int width, int height) {
        setBoundsImpl(getX(), getY(), width, height);
    }

    /**
     * Sets the size of this laminate and its laminated component
     * to the given size.
     *
     * @param size the size for this laminate
     * @see #setSize(int, int)
     */
    public void setSize(Dimension size) {
        setBoundsImpl(getX(), getY(), size.width, size.height);
    }

    /**
     * Sets the bounds of this laminate and its laminated component
     * to the given position, width, and height.
     *
     * @param x the <I>x</I>-coordinate 
     *      for the bounds for this laminate
     * @param y the <I>y</I>-coordinate 
     *      for the bounds for this laminate
     * @param width the width for the bounds of this laminate
     * @param height the height for the bounds of this laminate
     * @see #setBounds(Rectangle)
     */
    public void setBounds(int x, int y, int width, int height) {
        setBoundsImpl(x, y, width, height);
    }

    /**
     * Sets the bounds of this laminate and its laminated component
     * to the given bounds.
     *
     * @param bounds the new bounds for this laminate
     * @see #setBounds(int, int, int, int)
     */
    public void setBounds(Rectangle bounds) {
        setBoundsImpl(bounds.x, bounds.y, bounds.width, bounds.height);
    }
    
    /**
     * Sets whether or not this laminate 
     * can be moved to another location using direct manipulation
     * to the given value.
     *
     * The <CODE>{@link #setLocation(int, int) setLocation}</CODE>
     * method is not affected by this property.
     *
     * @param isMoveable whether or not this laminate can be moved
     * @see #isMoveable()
     * @since 2.0
     */
    public void setMoveable(boolean isMoveable) {
        moveable = isMoveable;
    }

    /**
     * Returns <CODE>true</CODE> if this laminate
     * can be moved to another location using direct manipulation
     * and <CODE>false</CODE> if it can not be moved.
     *
     * The <CODE>{@link #setLocation(int, int) setLocation}</CODE>
     * method is not affected by this property.
     *
     * @see #setMoveable(boolean)
     * @since 2.0
     */
    public boolean isMoveable() {
        return moveable && getZoo().allowsMove();
    }

    /**
     * Sets whether or not this laminate 
     * can be resized using direct manipulation,
     * to the given value.
     *
     * Methods such as
     * <CODE>{@link #setSize(Dimension) setSize}</CODE>
     * are not affected by this property.
     *
     * @param isResizeable whether or not this laminate can be sized
     * @see #isResizeable()
     * @since 2.0
     */
    public void setResizeable(boolean isResizeable) {
        resizeable = isResizeable;
    }

    /**
     * Returns <CODE>true</CODE> if this laminate
     * can be resized using direct manipulation
     * and <CODE>false</CODE> if it can not be resized.
     *
     * Methods such as
     * <CODE>{@link #setSize(Dimension) setSize}</CODE>
     * are not affected by this property.
     *
     * @see #setResizeable(boolean)
     * @since 2.0
     */
    public boolean isResizeable() {
        return resizeable && getZoo().allowsResize();
    }

    ///////////////////////
    // Protected methods //
    ///////////////////////

    /**
     * Paints a selection box around this laminate,
     * if this laminate is selected.
     *
     * @param g the graphics context to which to paint
     */
    protected void paintComponent(Graphics g) {
        if (isSelected()) {
            if (getZoo().hasFocus())
                g.setColor(getZoo().getFocusedHighlightColor());
            else
                g.setColor(getZoo().getUnfocusedHighlightColor());

            g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);

            int width  = getWidth(),
            height = getHeight();

            if (isResizableX())
                g.fillRect(width - 5, height / 2 - 2, 5, 5);

            if (isResizableY())
                g.fillRect(width / 2 - 2, height - 5, 5, 5);

            if (isResizableX() && isResizableY())
                g.fillRect(width - 5, height - 5, 5, 5);
        }
    }

    /** Returns the zoo containing this laminate. */
    protected Zoo getZoo() {
        return (Zoo)getParent();
    }

    /** Installs the mouse action adapter for this laminate. */
    protected void installMouseAdapter() {
        adapter = new MouseActionAdapter(this);

        installSelectionActions();
        installCursorActions();
        installManipulationActions();
    }

    /** 
     * Installs the mouse actions 
     * that affect selection of this laminate.
     */
    protected void installSelectionActions() {
        adapter.addMousePressedAction(new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                Zoo parent = getZoo();

                // toggle selection if shift down
                if (evt.isShiftDown()) {
                    if (isSelected())
                        parent.removeFromSelection(getTarget());
                    else
                        parent.addToSelection(getTarget());
                }

                // if shift not down, and the laminated component
                // is not only selected component, select only it
                else if (!isSelected() ||
                         getZoo().getSelectedComponentCount() != 1)
                {
                    parent.selectOnly(getTarget());
                }
            }
        });
    }

    /** 
     * Installs the mouse actions 
     * that affect the mouse cursor for this laminate.
     */
    protected void installCursorActions() {
        MouseAction rollover = new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                if (!isSelected())
                    return;

                int width  = getWidth(),
                    height = getHeight(),
                    type   = getCursor().getType();
                
                // don't change cursor in middle of operation
                if (resizingX || resizingY)
                    return;

                // check for corner resize possibility
                if (inSE(evt.getX(), evt.getY(), 5) &&
                    isResizableXY())
                {
                    type = Cursor.SE_RESIZE_CURSOR;
                }

                // check for bottom resize possibility
                else if (inS(evt.getX(), evt.getY(), 5) &&
                         isResizableY())
                {
                    type = Cursor.S_RESIZE_CURSOR;
                }

                // check for right side resize possibility
                else if (inE(evt.getX(), evt.getY(), 5) &&
                         isResizableX())
                {
                    type = Cursor.E_RESIZE_CURSOR;
                }

                // check for move possibility
                else if (isMoveable()) {
                    type = Cursor.MOVE_CURSOR;
                }

                // set the cursor if necessary
                if (getCursor().getType() != type)
                    setCursor(new Cursor(type));
            }
        };

        // add cursor change action for appropriate events
        adapter.addMouseMovedAction(rollover);
        adapter.addMouseEnteredAction(rollover);
        adapter.addMouseExitedAction(new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                if (!resizingX && !resizingY)
                    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            }
        });
    }

    /** 
     * Installs the mouse actions 
     * that affect manipulation of this laminate.
     */
    protected void installManipulationActions() {
        adapter.addMousePressedAction(new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                if (!isSelected())
                    return;

                // check for corner resize
                if (inSE(evt.getX(), evt.getY(), 5) &&
                    isResizableXY())
                {
                    setResizing(true, true);
                }

                // check for vertical resize
                else if (inS(evt.getX(), evt.getY(), 5) &&
                         isResizableY())
                {
                    setResizing(false, true);
                }

                // check for horizontal resize
                else if (inE(evt.getX(), evt.getY(), 5) &&
                         isResizableX())
                {
                    setResizing(true, false);
                }

                // otherwise a drag start
                else if (isMoveable()) {
                    startMoveAt(evt);
                }
            }
        });

        adapter.addMouseDraggedAction(new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                if (!isSelected())
                    return;
                
                if (resizingX || resizingY)
                    resizeTo(evt);
                else if (isMoveable())
                    moveTo(evt);
            }
        });

        adapter.addMouseReleasedAction(new MouseAction() {
            public void mouseActionPerformed(MouseEvent evt) {
                if (resizingX || resizingY) {
                    setResizing(false, false);

                    // finalize resize of group
                    if (getTarget() instanceof ZooGroup) {
                        ZooGroup group = (ZooGroup)getTarget();
                        group.endResize();
                    }

                    // update cursor
                    if (getTarget().contains(evt.getPoint()))
                        setCursor(new Cursor(Cursor.MOVE_CURSOR));
                    else
                        setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
                }
            }
        });
    }

    /** 
     * Sets the point at which a move is initiated
     * based on the given mouse event.
     *
     * @param evt the mouse event initiating the move 
     */
    protected void startMoveAt(MouseEvent evt) {
        moveOffset.setLocation(evt.getX(), evt.getY());
        setResizing(false, false);
    }

    /** 
     * Restricts the given proposed bounds based on the zoo
     * in which this laminate is contained.
     *
     * @param x the <I>x</I>-coordinate of the proposed bounds
     * @param y the <I>y</I>-coordinate of the proposed bounds 
     * @param width the width of the proposed bounds 
     * @param height the height of the proposed bounds  
     */
    protected Rectangle restrictBounds(
        int x, 
        int y, 
        int width, 
        int height)
    {
        Dimension size = new Dimension(width, height);

        // restrict only if appropriate
        if (getZoo() != null && 
            getZoo().isRestrictingBounds())
        {

            // retrieve zoo bounds
            Rectangle bounds = getZooInnerBounds();

            // restrict location
            x = Math.min(
                Math.max(bounds.x, x),
                bounds.x + bounds.width - target.getWidth());
            y = Math.min(
                Math.max(bounds.y, y),
                bounds.y + bounds.height - target.getHeight());

            // restrict size
            size = DimensionUtilities.max(
                size, target.getMinimumSize());
            size = DimensionUtilities.min(
                size, target.getMaximumSize());

            size.width = Math.max(0,
                Math.min(
                    size.width, 
                    bounds.x + bounds.width - target.getX()));
            size.height = Math.max(0,
                Math.min(
                    size.height, 
                    bounds.y + bounds.height - target.getY()));
        }
        
        // return the resulting rectangle
        return new Rectangle(x, y, size.width, size.height);
    }

    /** 
     * Moves this laminate and its covered component
     * based on the given mouse event.
     *
     * @param evt the mouse event initiating the move 
     */
    protected void moveTo(MouseEvent evt) {
        int x = evt.getX(),
            y = evt.getY();

        // translate based on point where mouse pressed
        x = getX() + x - moveOffset.x;
        y = getY() + y - moveOffset.y;

        // set target bounds
        target.setBounds(restrictBounds(
            x, y, target.getWidth(), target.getHeight()));
    }

    /**
     * Resizes this laminate and its covered component
     * based on the given mouse event.
     *
     * @param evt the mouse event initiating the resize
     */
    protected void resizeTo(MouseEvent evt) {
        Dimension d = new Dimension(evt.getX(), evt.getY());

        // only change width if appropriate
        if (!resizingX)
            d.width = getWidth();

        // only change height if appropriate
        if (!resizingY)
            d.height = getHeight();

        // set target bounds
        target.setBounds(restrictBounds(
            target.getX(), target.getY(), d.width, d.height));
        
        // revalidate target if necessary
        revalidateTarget();
    }

    /** 
     * Sets on which axis(es) 
     * this component is being resized.
     *
     * @param xAxis whether or not it is resizing on the x-axis
     * @param yAxis whether or not it is resizing on the y-axis
     */
    protected void setResizing(boolean xAxis, boolean yAxis) {
        resizingX = xAxis;
        resizingY = yAxis;

        // if a resize is about to happen
        if (resizingX || resizingY) {
            if (getTarget() instanceof ZooGroup) {
                ZooGroup group = (ZooGroup)getTarget();
                group.startResize();
            }
        }
    }

    /**
     * Returns <CODE>true</CODE> if the target component
     * is resizeable in the horizontal direction, 
     * and <CODE>false</CODE> if it is not.
     *
     * @see #isResizableY()
     * @see #isResizableXY()
     */
    protected boolean isResizableX() {
        return isResizeable() &&
            (target.getMinimumSize().width <
             target.getMaximumSize().width);
    }

    /**
     * Returns <CODE>true</CODE> if the target component
     * is resizeable in the vertical direction, 
     * and <CODE>false</CODE> if it is not.
     *
     * @see #isResizableX()
     * @see #isResizableXY()
     */
    protected boolean isResizableY() {
        return isResizeable() &&
           (target.getMinimumSize().height <
            target.getMaximumSize().height);
    }
    
    /**
     * Returns <CODE>true</CODE> if the target component
     * is resizeable in both the horizontal and vertical directions, 
     * and <CODE>false</CODE> if it is not.
     *
     * @see #isResizableX()
     * @see #isResizableY()
     * @since 2.1
     */
    protected boolean isResizableXY() {
        return isResizableX() &&
               isResizableY();
    }
    
    /**
     * Returns <CODE>true</CODE> if the given position
     * is in the bottom right-hand corner of this component
     * within the given constraint, 
     * and <CODE>false</CODE> otherwise.
     *
     * @see #inS(int, int, int)
     * @see #inE(int, int, int)
     */
    protected boolean inSE(int x, int y, int delta) {
        return Point2D.distance(
            x, y, getWidth(), getHeight()) <= delta;
    }

    /**
     * Returns <CODE>true</CODE> if the given position
     * is in the bottom middle of this component
     * within the given constraint, 
     * and <CODE>false</CODE> otherwise.
     *
     * @see #inSE(int, int, int)
     * @see #inE(int, int, int)
     */
    protected boolean inS(int x, int y, int delta) {
        return Point2D.distance(
            x, y, getWidth() / 2, getHeight()) <= delta;
    }

    /**
     * Returns <CODE>true</CODE> if the given position
     * is in the right-hand middle of this component
     * within the given constraint, 
     * and <CODE>false</CODE> otherwise.
     *
     * @see #inSE(int, int, int)
     * @see #inS(int, int, int)
     */
    protected boolean inE(int x, int y, int delta) {
        return Point2D.distance(
            x, y, getWidth(), getHeight() / 2) <= delta;
    }

    /////////////////////
    // Private methods //
    /////////////////////

    /** 
     * Sets the bounds for this laminate and its laminated component
     * to the given bounds.
     *
     * @param x the <I>x</I>-coordinate for this component
     * @param y the <I>y</I>-coordinate for this component
     * @param width the width for this component
     * @param height the height for this component
     */
    private void setBoundsImpl(int x, int y, int width, int height) {
        target.removeComponentListener(this);
        
        Rectangle bounds = restrictBounds(x, y, width, height);
        super.setBounds(
            bounds.x, bounds.y, bounds.width, bounds.height);
        target.setBounds(
            bounds.x, bounds.y, bounds.width, bounds.height);

        target.addComponentListener(this);
    }
    
    /**
     * Updates the laminated component 
     * by revalidating or validating it.
     */
    private void revalidateTarget() {
        if (target instanceof JComponent) {
            JComponent c = (JComponent)target;
            c.revalidate();
            return;
        }
        if (target instanceof Container) {
            Container c = (Container)target;
            c.validate();
            return;
        }
    }
    
    /** Returns the bounds of the parent zoo minus its insets. */
    private Rectangle getZooInnerBounds() {
        Dimension size   = getZoo().getSize();
        Insets    insets = getZoo().getInsets();
        
        return new Rectangle(
            insets.left,
            insets.top,
            size.width  - insets.left - insets.right,
            size.height - insets.top  - insets.bottom);
    }
}
