/*
 *  ToolBarHandler.java
 *  2004-01-10
 */

package com.ramussoft.pb.frames.dlayout;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;

import javax.swing.BorderFactory;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.event.MouseInputAdapter;
import javax.swing.plaf.ComponentUI;

class Handler {

    private Constraints ourConstraints = new Constraints();

    private JToolBar ourToolBar = null;
    private ToolBarLayout ourDockLayout = null;
    private JDialog ourFloatFrame = null;
    private DraggingWindow ourDraggingWindow = null;

    private ToolBarDragListener ourDragListener = null;
    private UIChangeListener ourUIListener = null;

    private boolean ourToolBarIsDragging = false;
    private boolean ourToolBarShouldFloat = false;
    private static boolean ourVersionIsCompatible = false;

    public static final String TOOL_BAR_HANDLER_KEY = "ToolBarHandler";

    /**
     * Creates a ToolBarHandler for the specified toolbar to be docked within
     * the specified container.
     */
    public Handler(final JToolBar toolbar, final ToolBarLayout layout) {
        ourToolBar = toolbar;
        ourDockLayout = layout;

        // Initially the value of the static variable ourVersionIsCompatible is
        // false so that for compatible versions (1.3 and above) this check is
        // only performed once...
        if (!ourVersionIsCompatible) {
            final String specVersion = System
                    .getProperty("java.specification.version");
            try {
                final float ver = Float.parseFloat(specVersion);
                if (ver > 1.2f)
                    ourVersionIsCompatible = true;
            } catch (final Exception ex) {
                // Assume the version is not high enough,
                // leave ourVersionIsCompatible = false
            }
        }

        if (ourVersionIsCompatible) {
            ourDragListener = new ToolBarDragListener();
            ourUIListener = new UIChangeListener();
            installListeners();
        }
    }

    /**
     * Sets the DockingConstraints for this handler representing the edge and
     * indicies at which this handler's toolbar should be docked.
     */
    public void setConstraints(final Constraints constraints) {
        ourConstraints = constraints;
    }

    /**
     * Returns this handler's DockingConstraints representing the edge and
     * indicies at which this handler's toolbar should be docked.
     */
    public Constraints getConstraints() {
        return ourConstraints;
    }

    /**
     * Gets the edge in which the assocated toolbar should be docked.
     */
    public int getDockEdge() {
        return ourConstraints.getEdge();
    }

    /**
     * Sets the edge in which the associated toolbar should be docked.
     */
    public void setDockEdge(final int edge) {
        ourConstraints.setEdge(edge);

    }

    /**
     * Gets the index at which the associated toolbar should be docked within
     * its edge.
     */
    public int getDockIndex() {
        return ourConstraints.getIndex();
    }

    /**
     * Sets the index at which the associated toolbar should be docked within
     * its edge.
     */
    public void setDockIndex(final int index) {
        ourConstraints.setIndex(index);
    }

    /**
     * Gets the row index at which the associated toolbar should be docked
     * within its edge. Not all dock boundary styles will support control over
     * the row index.
     */
    public int getRowIndex() {
        return ourConstraints.getRow();
    }

    /**
     * Sets the row index at which the associated toolbar should be docked
     * within its edge. Not all dock boundary styles will support control over
     * the row index.
     */
    public void setRowIndex(final int index) {
        ourConstraints.setRow(index);
    }

    /**
     * Hides the associated toolbar by removing it from its dock or by closing
     * its client floating frame.
     */
    public void hideToolBar() {
        final Container target = ourDockLayout.getTargetContainer();
        target.remove(ourToolBar);
        final JDialog floatFrame = getFloatingFrame();
        if (floatFrame != null) {
            floatFrame.setVisible(false);
            floatFrame.getContentPane().remove(ourToolBar);
        }

        target.validate();
        target.repaint();
    }

    /**
     * Docks the associated toolbar at its last visible location.
     */
    public void dockToolBar() {
        dockToolBar(getDockEdge());
    }

    /**
     * Docks the associated toolbar at the specified edge.
     */
    public void dockToolBar(final int edge) {
        dockToolBar(edge, getRowIndex(), getDockIndex());
    }

    /**
     * Docks the associated toolbar at the secified edge and indicies.
     */
    public void dockToolBar(final int edge, final int row, final int index) {
        final Container target = ourDockLayout.getTargetContainer();
        if (target == null)
            return;

        target.remove(ourToolBar);
        final JDialog floatFrame = getFloatingFrame();
        if (floatFrame != null) {
            floatFrame.setVisible(false);
            floatFrame.getContentPane().remove(ourToolBar);
        }

        ourConstraints.setEdge(edge);
        ourConstraints.setRow(row);
        ourConstraints.setIndex(index);

        target.add(ourToolBar, ourConstraints);
        ourToolBarShouldFloat = false;

        target.validate();
        target.repaint();
    }

    /**
     * Floats the associated toolbar at its natural location.
     */
    public void floatToolBar() {
        final Point p = getFloatingLocation();
        floatToolBar(p.x, p.y);
    }

    /**
     * Floats the associated toolbar at the specified screen location.
     */
    public void floatToolBar(final int x, final int y) {
        floatToolBar(x, y, false);
    }

    /**
     * Floats the associated toolbar at the specified screen location,
     * optionally centering the floating frame on this point.
     */
    public void floatToolBar(int x, int y, final boolean center) {
        final JDialog floatFrame = getFloatingFrame();
        if (floatFrame == null)
            return;

        final Container target = ourDockLayout.getTargetContainer();
        if (target != null)
            target.remove(ourToolBar);
        floatFrame.setVisible(false);
        floatFrame.getContentPane().remove(ourToolBar);

        ourToolBar.setOrientation(ToolBarLayout.HORIZONTAL);
        floatFrame.getContentPane().add(ourToolBar, BorderLayout.CENTER);
        floatFrame.pack();

        if (center) {
            x -= floatFrame.getWidth() / 2;
            y -= floatFrame.getHeight() / 2;
        }

        // x and y are given relative to screen
        floatFrame.setLocation(x, y);
        floatFrame.setTitle(ourToolBar.getName());
        floatFrame.setVisible(true);

        ourToolBarShouldFloat = true;

        if (target != null) {
            target.validate();
            target.repaint();
        }
    }

    /**
     * Gets the screen location of the associated toolbar's floating frame.
     */
    public Point getFloatingLocation() {
        final JDialog floatFrame = getFloatingFrame();
        if (floatFrame != null)
            return floatFrame.getLocation();
        else
            return new Point(0, 0);
    }

    /**
     * Sets the screen location of the associated toolbar's floating frame.
     */
    public void setFloatingLocation(final int x, final int y) {
        final JDialog floatFrame = getFloatingFrame();
        if (floatFrame != null)
            floatFrame.setLocation(x, y);
    }

    /**
     * Determines whether or not the associated toolbar should be floated.
     */
    public boolean shouldFloat() {
        return ourToolBarShouldFloat;
    }

    /**
     * Stores whether or not the associated toolbar should be floated.
     */
    public void setShouldFloat(final boolean shouldFloat) {
        ourToolBarShouldFloat = shouldFloat;
    }

    /**
     * Gets the nearest dockable DockBoundary for the specified point, or
     * returns null if the point is not a dockable location.
     */
    private DockBoundary getDockableBoundary(final Point point) {
        return ourDockLayout.getDockableBoundary(point);
    }

    /**
     * Checks to see if the DockBoundary containing the provided toolbar wishes
     * to veto the drag operation at the provided point. Some DockBoundaries may
     * manipulate the positions of the toolbars without this handler having to
     * undock and redock the toolbar.
     */
    private boolean isDraggable(final Point point, final JToolBar toolbar) {
        if (toolbar == null)
            return false;

        final DockBoundary boundary = ourDockLayout.getBoundary(point);
        if (boundary == null)
            return true;

        else if (boundary.containsToolBar(toolbar))
            return boundary.isDraggablePoint(point, toolbar);

        else
            return true;
    }

    /**
     * Returns the client frame for supporting the floating toolbar.
     */
    private JDialog getFloatingFrame() {
        if (ourFloatFrame == null) {
            final Window w = SwingUtilities.getWindowAncestor(ourDockLayout
                    .getTargetContainer());

            if (w == null)
                return null;

            Frame fr = null;
            if (w instanceof Frame)
                fr = (Frame) w;
            ourFloatFrame = new JDialog(fr);
            ourFloatFrame
                    .setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
            ourFloatFrame.addWindowListener(new FloatFrameCloseListener());
            ourFloatFrame.getContentPane().setLayout(new BorderLayout());
            ourFloatFrame.setTitle(ourToolBar.getName());
            ourFloatFrame.setResizable(false);
        }

        return ourFloatFrame;
    }

    /**
     * Returns a temporary window to display which represents the estimated
     * bounds of the toolbar while it is being dragged.
     */
    private DraggingWindow getDraggingWindow() {
        if (ourDraggingWindow == null) {
            final Window w = SwingUtilities.getWindowAncestor(ourDockLayout
                    .getTargetContainer());

            if (w != null)
                ourDraggingWindow = new DraggingWindow(w);
        }

        return ourDraggingWindow;
    }

    /**
     * Releases this handler's listeners from the associated toolbar.
     */
    public void uninstallListeners() {
        // May want to restore the original listeners that were
        // stripped from the toolbar when this handler was created.
        ourToolBar.removeMouseListener(ourDragListener);
        ourToolBar.removeMouseMotionListener(ourDragListener);
        ourToolBar.removePropertyChangeListener(ourUIListener);
    }

    /**
     * Strips off the UI's mouse listeners attached to the associated toolbar
     * and replaces them with this handler's listeners.
     */
    private void installListeners() {
        if (!ourVersionIsCompatible)
            return;

        ourToolBar.removePropertyChangeListener("UI", ourUIListener);

        // Uninstall the current ui, collect the remaining listeners
        // on the toolbar, and reinstall the ui...
        final ComponentUI ui = ourToolBar.getUI();
        ui.uninstallUI(ourToolBar);
        final java.util.List mList = Arrays.asList(ourToolBar
                .getListeners(MouseListener.class));

        final java.util.List mmList = Arrays.asList(ourToolBar
                .getListeners(MouseMotionListener.class));
        ui.installUI(ourToolBar);

        // ...then remove the listeners that were added by the ui...
        final MouseListener[] ml = ourToolBar
                .getListeners(MouseListener.class);
        final MouseMotionListener[] mml = ourToolBar
                .getListeners(MouseMotionListener.class);

        for (int i = 0; i < ml.length; i++) {
            if (!mList.contains(ml[i]))
                ourToolBar.removeMouseListener(ml[i]);
        }

        for (int i = 0; i < mml.length; i++) {
            if (!mmList.contains(mml[i]))
                ourToolBar.removeMouseMotionListener(mml[i]);
        }

        // ...and add our listeners to the toolbar.
        ourToolBar.addMouseListener(ourDragListener);
        ourToolBar.addMouseMotionListener(ourDragListener);
        ourToolBar.addPropertyChangeListener("UI", ourUIListener);
    }

    /**
     * Inner class that monitors the float frame for closing.
     */
    private class FloatFrameCloseListener extends WindowAdapter {
        @Override
        public void windowClosing(final WindowEvent we) {
            dockToolBar(getDockEdge(), getRowIndex(), getDockIndex());
        }
    }

    /**
     * Inner class that listens for changes in the UI on the associated toolbar.
     * Insures that when a new UI is installed, its listeners are replaced.
     */
    private class UIChangeListener implements PropertyChangeListener {
        public void propertyChange(final PropertyChangeEvent pce) {
            installListeners();
        }
    }

    /**
     * Inner class that replaces the associated toolbar's mouse listeners and
     * handles the drag and drop behavior.
     */
    private class ToolBarDragListener extends MouseInputAdapter {
        @Override
        public void mouseDragged(final MouseEvent me) {
            ourDraggingWindow = getDraggingWindow();
            if (ourDraggingWindow == null)
                return;

            // Only allow Button 1 to perform the drag...
            if ((me.getModifiers() & InputEvent.BUTTON1_MASK) != InputEvent.BUTTON1_MASK) {
                hideDraggingWindow();
                return;
            }

            final Container target = ourDockLayout.getTargetContainer();
            if (target == null)
                return;

            Point p = me.getPoint();
            p = SwingUtilities.convertPoint(ourToolBar, p, target);

            // Make sure the DockBoundary containing this point
            // and this toolbar wishes to allow the drag operation
            // to commence or continue...
            if (!isDraggable(p, ourToolBar)) {
                hideDraggingWindow();
                return;
            }

            // Determine if this point lies within a
            // DockBoundary's dockable range...
            int orient = ToolBarLayout.HORIZONTAL;
            boolean dockable = false;

            if (!me.isControlDown()) {
                final DockBoundary dock = getDockableBoundary(p);

                if (dock != null) {
                    dockable = true;
                    orient = dock.getOrientation();
                }
            }

            // Present the dragging window at this point on the screen...
            SwingUtilities.convertPointToScreen(p, target);
            ourDraggingWindow.presentWindow(p, dockable, orient);
            ourToolBarIsDragging = true;

        }

        @Override
        public void mouseReleased(final MouseEvent me) {
            if (!ourToolBarIsDragging)
                return;

            if ((me.getModifiers() & InputEvent.BUTTON1_MASK) != InputEvent.BUTTON1_MASK) {
                return;
            }

            hideDraggingWindow();

            final Container target = ourDockLayout.getTargetContainer();
            if (target == null)
                return;

            Point p = me.getPoint();
            p = SwingUtilities.convertPoint(ourToolBar, p, target);

            DockBoundary dock = null;

            if (!me.isControlDown()) {
                dock = getDockableBoundary(p);
                if (dock != null) {
                    setDockIndex(dock.getDockIndex(p));
                    setRowIndex(dock.getRowIndex(p));
                    setDockEdge(dock.getEdge());
                }
            }

            if (dock != null) {
                dockToolBar(getDockEdge(), getRowIndex(), getDockIndex());
            } else {
                SwingUtilities.convertPointToScreen(p, target);
                floatToolBar(p.x, p.y, true);
            }

        }

        private void hideDraggingWindow() {
            if (ourDraggingWindow != null) {
                ourDraggingWindow.hideWindow();
                ourToolBarIsDragging = false;
            }
        }

    }

    /**
     * Inner class that represents the bounds of the associated toolbar while
     * dragging.
     */
    private class DraggingWindow extends JWindow {
        private Border myFloatBorder = null;
        private Border myDockBorder = null;
        private final Color myFloatColor = null;
        private final Color myDockColor = null;
        private JPanel myContent = null;

        public DraggingWindow(final Window ancestor) {
            super(ancestor);

            Color myFloatColor = UIManager
                    .getColor("ToolBar.floatingForeground");
            Color myDockColor = UIManager.getColor("ToolBar.dockingForeground");
            if (myFloatColor == null)
                myFloatColor = Color.darkGray;
            if (myDockColor == null)
                myDockColor = Color.yellow;

            myFloatBorder = BorderFactory.createLineBorder(myFloatColor, 3);
            myDockBorder = BorderFactory.createLineBorder(myDockColor, 3);

            myContent = new JPanel();
            myContent.setOpaque(true);

            myFloatColor = UIManager.getColor("ToolBar.floatingBackground");
            myDockColor = UIManager.getColor("ToolBar.dockingBackground");
            if (myFloatColor == null)
                myFloatColor = myContent.getBackground();
            if (myDockColor == null)
                myDockColor = myContent.getBackground();

            getContentPane().setLayout(new BorderLayout());
            getContentPane().add(myContent, BorderLayout.CENTER);
        }

        // Present the window at the given point for the given dockability
        // at the given dock orientation...
        public void presentWindow(final Point screenPoint, final boolean dockable,
                                  final int dockOrientation) {
            setCentroidLocation(screenPoint);

            // int tbOrientation = ourToolBar.getOrientation();
            final int orientation = dockable ? dockOrientation
                    : ToolBarLayout.HORIZONTAL;

            setSize(getPreferredToolBarSize(orientation));

            myContent.setBorder(dockable ? myDockBorder : myFloatBorder);
            myContent.setBackground(dockable ? myDockColor : myFloatColor);

            setCentroidLocation(screenPoint);

            validate();
            if (!isVisible())
                setVisible(true);
        }

        public void hideWindow() {
            setVisible(false);
        }

        private void setCentroidLocation(final Point screenPoint) {
            setLocation(screenPoint.x - getWidth() / 2, screenPoint.y
                    - getHeight() / 2);
        }

        private Dimension getPreferredToolBarSize(final int orientation) {
            final Component[] comps = ourToolBar.getComponents();
            int w = 0, h = 0;
            for (final Component element : comps) {
                final Dimension d = element.getPreferredSize();
                if (orientation == ToolBarLayout.HORIZONTAL) {
                    w += d.width;
                    h = Math.max(h, d.height);
                } else {
                    w = Math.max(w, d.width);
                    h += d.height;
                }
            }

            return new Dimension(w, h);
        }

    }
}