/*
 * @(#)AbstractComponentDecorator.java   2010.01.21 at 09:34:55 PST
 *
 * Copyright 2009 MBARI
 *
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */



package mbarix4j.swing;

import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;

/**
 * Provide a method for consistently augmenting the appearance of a given
 * component by painting something on it <i>after</i> the component itself
 * gets painted.  If not explicitly removed via {@link #dispose}, an instance
 * of this object will live as long as its target component.<p>
 * By default, the decorator matches the location and size of the decorated
 * component, but the bounds can be adjusted by overriding
 * {@link #getDecorationBounds()}.  The {@link #synch()} method should be called
 * whenever the bounds returned by {@link #getDecorationBounds()} would change.
 * <p>
 * The decoration is clipped to the bounds set on the decoration, which does
 * not necessarily need to be the same as the decorated component bounds.  The
 * decoration may extend beyond the decorated component bounds, or it may be
 * reduced to a smaller region.
 */

// NOTE: OSX 1.6 lacks hierarchy events that w32 sends on layer changes

// TODO: should probably do some locking on Component.getTreeLock()
// TODO: synch underlying cursor when decorator covers more than
// one component; the cursor should change if the decoration exceeds the
// component's bounds (add mouse listener)
public abstract class AbstractComponentDecorator {

    // Disabled for now, since it doesn't work properly
    private static final int BOTTOM = -1;

    /**  */
    public static final Rectangle DEFAULT_BOUNDS = null;

    /**  */
    public static final int TOP = 0;
    private static Field nComponents;

    static {
        try {
            nComponents = Container.class.getDeclaredField("ncomponents");
            nComponents.setAccessible(true);
        }
        catch (Exception e) {
            nComponents = null;
        }
    }

    /** Account for the difference between the decorator actual origin
     * and the logical origin we want to pass to the {@link #paint} method.
     */
    private Point originOffset = new Point(0, 0);
    private Rectangle bounds;
    private JComponent component;
    private int layerOffset;
    private Component layerRoot;
    private Listener listener;
    private Painter painter;
    private Container parent;
    private int position;

    /**
     * Create a decorator for the given component.
     *
     * @param c
     */
    public AbstractComponentDecorator(JComponent c) {
        this(c, 1);
    }

    /** Create a decorator for the given component, indicating the layer
     * offset from the target component.  Negative values mean the decoration
     * is painted <em>before</em> the target component is painted.
     *
     * @param c
     * @param layerOffset
     */
    public AbstractComponentDecorator(JComponent c, int layerOffset) {
        this(c, layerOffset, TOP);
    }

    /** Create a decorator with the given position within its layer.
     * Use {@link #TOP} to cover other decorations, or {@link #BOTTOM}
     * to be covered by other decorations.<p>
     * WARNING: BOTTOM doesn't currently work, probably a JLayeredPane bug
     * in either the code or documentation.
     * @see JLayeredPane
     *
     * @param c
     * @param layerOffset
     * @param position
     */
    public AbstractComponentDecorator(JComponent c, int layerOffset, int position) {
        component = c;
        this.layerOffset = layerOffset;
        this.position = position;
        this.bounds = DEFAULT_BOUNDS;
        parent = c.getParent();
        painter = new Painter();
        listener = new Listener();
        component.addHierarchyListener(listener);
        component.addComponentListener(listener);
        attach();
    }

    protected void attach() {
        if (layerRoot != null) {
            layerRoot.removePropertyChangeListener(listener);
            layerRoot = null;
        }

        RootPaneContainer rpc = (RootPaneContainer) SwingUtilities.getAncestorOfClass(RootPaneContainer.class,
            component);
        if ((rpc != null) && SwingUtilities.isDescendingFrom(component, rpc.getLayeredPane())) {
            JLayeredPane lp = rpc.getLayeredPane();
            Component layeredChild = component;
            int layer = JLayeredPane.DRAG_LAYER.intValue();
            if (this instanceof BackgroundPainter) {
                layer = ((BackgroundPainter) this).layer;
                painter.setDecoratedLayer(layer);
            }
            else if (layeredChild == lp) {

                // Is the drag layer the best layer to use when decorating
                // the layered pane?
                painter.setDecoratedLayer(layer);
            }
            else {
                while (layeredChild.getParent() != lp) {
                    layeredChild = layeredChild.getParent();
                }

                int base = lp.getLayer(layeredChild);

                // NOTE: JLayeredPane doesn't properly repaint an overlapping
                // child when an obscured child calls repaint() if the two
                // are in the same layer, so we use the next-higher layer
                // instead of simply using a different position within the
                // layer.
                layer = base + layerOffset;

                if (layerOffset < 0) {
                    BackgroundPainter bp = (BackgroundPainter) lp.getClientProperty(BackgroundPainter.key(base));
                    if (bp == null) {
                        bp = new BackgroundPainter(lp, base);
                    }
                }

                painter.setDecoratedLayer(base);
                layerRoot = layeredChild;
                layerRoot.addPropertyChangeListener(listener);
            }

            lp.add(painter, new Integer(layer), position);
        }
        else {

            // Always detach when the target component's window is null
            // or is not a suitable container,
            // otherwise we might prevent GC of the component
            Container parent = painter.getParent();
            if (parent != null) {
                parent.remove(painter);
            }
        }

        // Track size changes in the decorated component's parent
        if (parent != null) {
            parent.removeComponentListener(listener);
        }

        parent = component.getParent();

        if (parent != null) {
            parent.addComponentListener(listener);
        }

        synch();
    }

    /** Adjust the painting offsets and size of the decoration to
     * account for ancestor clipping.  This might be due to scroll panes
     * or having the decoration lie outside the parent layered pane.
     */
    protected Rectangle clipDecorationBounds(Rectangle decorated) {

        // Amount we have to translate the Graphics context
        originOffset.x = decorated.x;
        originOffset.y = decorated.y;

        // If the the component is obscured (by a viewport or some
        // other means), use the painter bounds to clip to the visible
        // bounds.  Doing may change the actual origin, so adjust our
        // origin offset accordingly
        Rectangle visible = getClippingRect(component, decorated);
        Rectangle clipRect = decorated.intersection(visible);
        if (decorated.x < visible.x) {
            originOffset.x += visible.x - decorated.x;
        }

        if (decorated.y < visible.y) {
            originOffset.y += visible.y - decorated.y;
        }

        return clipRect;
    }

    /** Stop decorating. */
    public void dispose() {
        component.removeHierarchyListener(listener);
        component.removeComponentListener(listener);

        if (parent != null) {
            parent.removeComponentListener(listener);
            parent = null;
        }

        if (layerRoot != null) {
            layerRoot.removePropertyChangeListener(listener);
            layerRoot = null;
        }

        Container painterParent = painter.getParent();
        if (painterParent != null) {
            Rectangle bounds = painter.getBounds();
            painterParent.remove(painter);
            painterParent.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
        }

        component.repaint();
        component = null;
    }

    /** Return any clipping rectangle detected above the given component,
     * in the coordinate space of the given component.  The given rectangle
     * is desired to be visible.
     */
    private Rectangle getClippingRect(Container component, Rectangle desired) {
        Rectangle visible = (component instanceof JComponent)
                            ? ((JComponent) component).getVisibleRect()
                            : new Rectangle(0, 0, component.getWidth(), component.getHeight());
        Rectangle clip = new Rectangle(desired);
        if ((desired.x >= visible.x) && (desired.y >= visible.y) &&
                (desired.x + desired.width <= visible.x + visible.width) &&
                (desired.y + desired.height <= visible.y + visible.height)) {

            // desired rect is within the current clip rect
        }
        else if (component.getParent() != null) {

            // Only apply the clip if it is actually smaller than the
            // component's visible area
            if ((component != painter.getParent()) &&
                    ((visible.x > 0) || (visible.y > 0) || (visible.width < component.getWidth()) ||
                     (visible.height < component.getHeight()))) {

                // Don't alter the original rectangle
                desired = new Rectangle(desired);
                desired.x = Math.max(desired.x, visible.x);
                desired.y = Math.max(desired.y, visible.y);
                desired.width = Math.min(desired.width, visible.x + visible.width - desired.x);
                desired.height = Math.min(desired.height, visible.y + visible.height - desired.y);

                // Check for clipping further up the hierarchy
                desired.x += component.getX();
                desired.y += component.getY();
                clip = getClippingRect(component.getParent(), desired);
                clip.x -= component.getX();
                clip.y -= component.getY();
            }
        }

        return clip;
    }

    protected JComponent getComponent() {
        return component;
    }

    /** Return the bounds, relative to the decorated component, of the
     * decoration.  The default covers the entire component.  Note that
     * this method will be called from the constructor, so be careful
     * when overriding and referencing derived class state.
     */
    protected Rectangle getDecorationBounds() {
        return (bounds != DEFAULT_BOUNDS) ? bounds : new Rectangle(0, 0, component.getWidth(), component.getHeight());
    }

    protected JComponent getPainter() {
        return painter;
    }

    /** Return the currently set default tooltip text.
     * @see JComponent#setToolTipText
     * @return
     */
    public String getToolTipText() {
        return painter.getToolTipText();
    }

    /** Provide for different tool tips depending on the actual location
     * over the decoration.  Note that if you <em>only</em> override this
     * method, you must also invoke {@link #setToolTipText(String)} with
     * a non-<span class="javakeyword">null</span> argument.
     * @see JComponent#getToolTipText(MouseEvent)
     *
     * @param e
     * @return
     */
    public String getToolTipText(MouseEvent e) {
        return getToolTipText();
    }

    /** Indicate whether any of the decoration is visible.  The decoration
     * may be clipped by ancestor scroll panes or by being moved outside
     * if the visible region of its parent window.
     * @return
     */
    public boolean isVisible() {
        return painter.isVisible();
    }

    /** Define the decoration's appearance.  The point (0,0) represents
     * the upper left corner of the decorated component.
     * The default clip mask will be the extents of the decoration bounds, as
     * indicated by {@link #getDecorationBounds()}, which defaults to the
     * decorated component bounds.
     *
     * @param g
     */
    public abstract void paint(Graphics g);

    /** Force a refresh of the underlying component and its decoration. */
    public void repaint() {
        JLayeredPane p = (JLayeredPane) painter.getParent();
        if (p != null) {
            p.repaint(painter.getBounds());
        }
    }

    /**
     * Set the cursor to appear anywhere over the decoration bounds.
     *
     * @param cursor
     */
    public void setCursor(Cursor cursor) {
        painter.setCursor(cursor);
    }

    /** Change the bounds of the decoration, relative to the decorated
     * component.  The special value {@link #DEFAULT_BOUNDS} means the bounds
     * will track the component bounds.
     *
     * @param bounds
     */
    public void setDecorationBounds(Rectangle bounds) {
        if (bounds == DEFAULT_BOUNDS) {
            this.bounds = bounds;
        }
        else {
            this.bounds = new Rectangle(bounds);
        }

        synch();
    }

    /** Change the bounds of the decoration, relative to the decorated
     * component.
     *
     * @param x
     * @param y
     * @param w
     * @param h
     */
    public void setDecorationBounds(int x, int y, int w, int h) {
        setDecorationBounds(new Rectangle(x, y, w, h));
    }

    protected void setPainterBounds(int x, int y, int w, int h) {
        painter.setLocation(x, y);
        painter.setSize(w, h);
        repaint();
    }

    /** Set the text to be displayed when the mouse is over the decoration.
     * @see JComponent#setToolTipText(String)
     *
     * @param text
     */
    public void setToolTipText(String text) {
        painter.setToolTipText(text);
    }

    /**
     * Use this to change the visibility of the decoration.
     *
     * @param visible
     */
    public void setVisible(boolean visible) {
        painter.setVisible(visible);
    }

    /** Ensure the size of the decorator matches the current
     * decoration bounds with appropriate clipping to viewports.
     */
    protected void synch() {
        Container painterParent = painter.getParent();
        if (painterParent != null) {
            Rectangle decorated = getDecorationBounds();
            Rectangle clipRect = clipDecorationBounds(decorated);

            Point pt = SwingUtilities.convertPoint(component, clipRect.x, clipRect.y, painterParent);
            if ((clipRect.width <= 0) || (clipRect.height <= 0)) {
                setPainterBounds(-1, -1, 0, 0);
                setVisible(false);
            }
            else {
                setPainterBounds(pt.x, pt.y, clipRect.width, clipRect.height);
                setVisible(true);
            }

            painterParent.repaint();
        }
    }

    /**
     * @return
     */
    @Override
    public String toString() {
        return super.toString() + " on " + getComponent();
    }

    private static boolean useSimpleBackground() {
        return nComponents == null;
    }

    /** Provides a shared background painting mechanism for multiple
     * decorations.  This ensures that the background is only painted once
     * if more than one background decorator is applied.
     */
    private static class BackgroundPainter extends AbstractComponentDecorator {

        private String key;
        private int layer;

        /**
         * Constructs ...
         *
         * @param p
         * @param layer
         */
        public BackgroundPainter(JLayeredPane p, int layer) {
            super(p, 0, TOP);
            this.layer = layer;
            key = key(layer);
            p.putClientProperty(key, this);
        }

        /**
         */
        @Override
        public void dispose() {
            getComponent().putClientProperty(key, null);
            super.dispose();
        }

        private List findDoubleBuffered(Component root) {
            List list = new ArrayList();
            if (root.isDoubleBuffered() && (root instanceof JComponent)) {
                list.add(root);
                ((JComponent) root).setDoubleBuffered(false);
            }

            if (root instanceof Container) {
                Component[] kids = ((Container) root).getComponents();
                for (int i = 0; i < kids.length; i++) {
                    list.addAll(findDoubleBuffered(kids[i]));
                }
            }

            return list;
        }

        private List findOpaque(Component root) {
            List list = new ArrayList();
            if (root.isOpaque() && (root instanceof JComponent)) {
                list.add(root);
                ((JComponent) root).setOpaque(false);
            }

            if (root instanceof Container) {
                Component[] kids = ((Container) root).getComponents();
                for (int i = 0; i < kids.length; i++) {
                    list.addAll(findOpaque(kids[i]));
                }
            }

            return list;
        }

        private int hideChildren(Container c) {
            if (c == null) {
                return 0;
            }

            int value = c.getComponentCount();
            try {
                nComponents.set(c, new Integer(0));
            }
            catch (Exception e) {
                return c.getComponentCount();
            }

            return value;
        }

        private static String key(int layer) {
            return "backgroundPainter for layer " + layer;
        }

        /**
         * Walk the list of "background" decorators and paint them.
         *
         * @param g
         */
        public void paint(Graphics g) {

            JLayeredPane lp = (JLayeredPane) getComponent();
            Component[] kids = lp.getComponents();

            // Construct an area of the intersection of all decorators
            Area area = new Area();
            List painters = new ArrayList();
            List components = new ArrayList();
            for (int i = kids.length - 1; i >= 0; i--) {
                if (kids[i] instanceof Painter) {
                    Painter p = (Painter) kids[i];
                    if (p.isBackgroundDecoration() && (p.getDecoratedLayer() == layer) && p.isShowing()) {
                        painters.add(p);
                        area.add(new Area(p.getBounds()));
                    }
                }
                else if ((lp.getLayer(kids[i]) == layer) && (kids[i] instanceof JComponent)) {
                    components.add(kids[i]);
                }
            }

            if (painters.size() == 0) {
                dispose();
                return;
            }

            g.setClip(area);

            // Paint background for that area
            for (Iterator i = components.iterator(); i.hasNext(); ) {
                JComponent c = (JComponent) i.next();
                paintBackground(g, lp, c);
            }

            // Paint the bg decorators
            for (Iterator i = painters.iterator(); i.hasNext(); ) {
                Painter p = (Painter) i.next();
                p.paint(g.create(p.getX(), p.getY(), p.getWidth(), p.getHeight()));
            }

            // Paint foreground for the area
            for (Iterator i = components.iterator(); i.hasNext(); ) {
                JComponent c = (JComponent) i.next();
                paintForeground(g.create(c.getX(), c.getY(), c.getWidth(), c.getHeight()), c);
            }
        }

        private void paintBackground(Graphics g, Component parent, JComponent jc) {
            int x = jc.getX();
            int y = jc.getY();
            int w = jc.getWidth();
            int h = jc.getHeight();
            paintBackground(g.create(x, y, w, h), jc);
        }

        private void paintBackground(Graphics g, JComponent jc) {
            if (jc.isOpaque()) {
                if (useSimpleBackground()) {
                    g.setColor(jc.getBackground());
                    g.fillRect(0, 0, jc.getWidth(), jc.getHeight());
                }
                else {
                    int count = hideChildren(jc);
                    boolean db = jc.isDoubleBuffered();
                    if (db) {
                        jc.setDoubleBuffered(false);
                    }

                    jc.paint(g);

                    if (db) {
                        jc.setDoubleBuffered(true);
                    }

                    restoreChildren(jc, count);
                }
            }

            Component[] kids = jc.getComponents();
            for (int i = 0; i < kids.length; i++) {
                if (kids[i] instanceof JComponent) {
                    paintBackground(g, jc, (JComponent) kids[i]);
                }
            }
        }

        private void paintForeground(Graphics g, JComponent jc) {
            List opaque = findOpaque(jc);
            List db = findDoubleBuffered(jc);
            jc.paint(g);

            for (Iterator i = opaque.iterator(); i.hasNext(); ) {
                ((JComponent) i.next()).setOpaque(true);
            }

            for (Iterator i = db.iterator(); i.hasNext(); ) {
                ((JComponent) i.next()).setDoubleBuffered(true);
            }
        }

        private void restoreChildren(Container c, int count) {
            if (c != null) {
                try {
                    nComponents.set(c, new Integer(count));
                }
                catch (Exception e) {}
            }
        }

        /**
         * @return
         */
        public String toString() {
            return key + " on " + getComponent();
        }
    }


    /** Tracks changes to component configuration. */
    private final class Listener extends ComponentAdapter implements HierarchyListener, PropertyChangeListener {

        /**
         *
         * @param e
         */
        public void componentHidden(ComponentEvent e) {
            setVisible(false);
        }

        /**
         *
         * @param e
         */
        public void componentMoved(ComponentEvent e) {

            // FIXME figure out why attach works and synch doesn't
            // when painting a selection marquee over a decorated background
            attach();
        }

        /**
         *
         * @param e
         */
        public void componentResized(ComponentEvent e) {

            // FIXME figure out why attach works and synch doesn't
            // when painting a selection marquee over a decorated background
            attach();
        }

        /**
         *
         * @param e
         */
        public void componentShown(ComponentEvent e) {
            setVisible(true);
        }

        // NOTE: OSX (1.6) doesn't generate these the same as w32

        /**
         *
         * @param e
         */
        public void hierarchyChanged(HierarchyEvent e) {
            if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) != 0) {
                attach();
            }
        }

        /**
         *
         * @param e
         */
        public void propertyChange(PropertyChangeEvent e) {
            if (JLayeredPane.LAYER_PROPERTY.equals(e.getPropertyName())) {
                attach();
            }
        }
    }


    /** Used to hook into the Swing painting architecture. */
    private class Painter extends JComponent {

        private int base;
        private Cursor cursor;

        {
            setFocusable(false);
        }

        /**
         * @return
         */
        public JComponent getComponent() {
            return AbstractComponentDecorator.this.getComponent();
        }

        /** Returns the cursor of the decorated component, or the last
         * cursor set by {@link #setCursor}.
         * @return
         */
        public Cursor getCursor() {
            return (cursor != null) ? cursor : component.getCursor();
        }

        /**
         * @return
         */
        public int getDecoratedLayer() {
            return base;
        }

        /** Provide a decorator-specific tooltip, shown when within the
         * decorator's bounds.
         *
         * @param e
         * @return
         */
        public String getToolTipText(MouseEvent e) {
            return AbstractComponentDecorator.this.getToolTipText(e);
        }

        /**
         * @return
         */
        public boolean isBackgroundDecoration() {
            return layerOffset < 0;
        }

        /** Delegate to the containing decorator to perform the paint.
         *
         * @param g
         */
        public void paintComponent(Graphics g) {
            if (!component.isShowing()) {
                return;
            }

            Graphics g2 = g.create();
            g2.translate(-originOffset.x, -originOffset.y);
            AbstractComponentDecorator.this.paint(g2);
            g2.dispose();
        }

        /**
         * Set the cursor to something else.
         *
         * @param cursor
         */
        public void setCursor(Cursor cursor) {
            Cursor oldCursor = getCursor();

            // Make sure the cursor actually changed, otherwise
            // we get cursor flicker (notably on w32 title bars)
            if (((oldCursor == null) && (cursor != null)) || ((oldCursor != null) && !oldCursor.equals(cursor))) {
                this.cursor = cursor;
                super.setCursor(cursor);
            }
        }

        /**
         *
         * @param base
         */
        public void setDecoratedLayer(int base) {
            this.base = base;
        }

        /**
         * @return
         */
        @Override
        public String toString() {
            return "Painter for " + AbstractComponentDecorator.this;
        }
    }
}
