/*
 */
package org.flexdock.docking.defaults;

import java.awt.Component;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.AbstractAction;
import javax.swing.JSplitPane;
import javax.swing.plaf.basic.BasicSplitPaneUI;

import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.SwingUtility;

/**
 * @author Christopher Butler
 */
@SuppressWarnings(value = { "serial" })
public class DockingSplitPane extends JSplitPane implements DockingConstants {
    protected DockingPort dockingPort;

    protected String region;

    protected boolean dividerLocDetermined;

    protected boolean controllerInTopLeft;

    protected double initialDividerRatio = .5;

    protected double percent = -1;

    private int dividerHashCode = -1;

    private boolean constantPercent;

    /**
     * Creates a new {@code DockingSplitPane} for the specified
     * {@code DockingPort} with the understanding that the resulting
     * {@code DockingSplitPane} will be used for docking a {@code Dockable} into
     * the {@code DockingPort's} specified {@code region}. Neither {@code port}
     * or {@code region} may be {@code null}. {@code region} must be a valid
     * docking region as defined by {@code isValidDockingRegion(String region)}.
     *
     * @param port
     *            the {@code DockingPort} for which this
     *            {@code DockingSplitPane} is to be created.
     * @param region
     *            the region within the specified {@code DockingPort} for which
     *            this {@code DockingSplitPane} is to be created.
     * @throws {@code IllegalArgumentException}
     *             if either {@code port} is {@code null} or }region} is
     *             {@code null} or invalid.
     * @see DockingManager#isValidDockingRegion(String)
     */
    public DockingSplitPane(DockingPort port, String region) {
        if (port == null)
            throw new IllegalArgumentException("'port' cannot be null.");
        if (!DockingManager.isValidDockingRegion(region))
            throw new IllegalArgumentException("'" + region
                                               + "' is not a valid region.");

        this.region = region;
        this.dockingPort = port;
        // the controlling item is in the topLeft if our new item (represented
        // by the "region" string) is NOT in the topLeft.
        controllerInTopLeft = !DockingUtility.isRegionTopLeft(region);

        // set the proper resize weight
        int weight = controllerInTopLeft ? 1 : 0;
        setResizeWeight(weight);

        addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, new PropertyChangeListener() {

                public void propertyChange(PropertyChangeEvent pce) {
                    if (constantPercent && getUI() instanceof BasicSplitPaneUI) {
                        BasicSplitPaneUI ui = (BasicSplitPaneUI) getUI();
                        if (dividerHashCode != ui.getDivider().hashCode()) {
                            dividerHashCode = ui.getDivider().hashCode();
                            ui.getDivider().addMouseListener(new MouseAdapter() {

                                    public void mouseReleased(MouseEvent e) {
                                        DockingSplitPane.this.percent = SwingUtility.getDividerProportion(DockingSplitPane.this);
                                        DockingSplitPane.this.setResizeWeight(percent);
                                    }
                                });
                        }
                    }
                }
            });

        getActionMap().put("toggleFocus", new AbstractAction() {
                public void actionPerformed(ActionEvent e) {
                    SwingUtility.toggleFocus(+1);
                }
            });
    }

    @Override
    public void setBounds(int x, int y, int w, int h) {
        super.setBounds(x, y, w, h);
        if (constantPercent) {
            setResizeWeight(percent);
            super.setDividerLocation((int) (percent * getSplitSize()));
        }
    }

    public void setConstantPercent(boolean cstPercent) {
        if (cstPercent != constantPercent) {
            constantPercent = cstPercent;
        }
    }

    public void resetToPreferredSizes() {
        Insets i = getInsets();

        if (getOrientation() == VERTICAL_SPLIT) {
            int h = getHeight() - i.top - i.bottom - getDividerSize();
            int topH = getTopComponent().getPreferredSize().height;
            int bottomH = getBottomComponent().getPreferredSize().height;
            int extraSpace = h - topH - bottomH;

            // we have more space than necessary; resize to give each at least
            // preferred size
            if (extraSpace >= 0) {
                setDividerLocation(i.top + topH
                                   + ((int) (extraSpace * getResizeWeight() + .5)));
            }

            // TODO implement shrinking excess space to ensure that one has
            // preferred and nothing more
        } else {
            int w = getWidth() - i.left - i.right - getDividerSize();
            int leftH = getLeftComponent().getPreferredSize().width;
            int rightH = getRightComponent().getPreferredSize().width;
            int extraSpace = w - leftH - rightH;

            // we have more space than necessary; resize to give each at least
            // preferred size
            if (extraSpace >= 0) {
                setDividerLocation(i.left + leftH
                                   + ((int) (extraSpace * getResizeWeight() + .5)));
            }

            // TODO implement shrinking excess space to ensure that one has
            // preferred and nothing more
        }
    }

    private int getSplitSize() {
        return getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? getWidth() : getHeight();
    }

    public void setDividerLocation(double percent) {
        this.percent = percent;
        super.setDividerLocation(percent);
        setResizeWeight(percent);
    }

    public double getPercent() {
        if (constantPercent) {
            return percent;
        }

        return -1;
    }

    protected boolean isDividerSizeProperlyDetermined() {
        if (getDividerLocation() != 0)
            return true;
        return dividerLocDetermined;
    }

    /**
     * Returns the 'oldest' {@code Component} to have been added to this
     * {@code DockingSplitPane} as a result of a docking operation. A
     * {@code DockingSplitPane} is created based upon the need to share space
     * within a {@code DockingPort} between two {@code Dockables}. This happens
     * when a new {@code Dockable} is introduced into an outer region of a
     * {@code DockingPort} that already contains a {@code Dockable}. The
     * {@code Dockable} that was in the {@code DockingPort} prior to splitting
     * the layout is the 'elder' {@code Component} and, in many circumstances,
     * may be used to control initial divider location and resize weight.
     * <p>
     * If this split pane contains {@code DockingPorts} as its child components,
     * then this method will return the {@code Component} determined by calling
     * {@code getDockedComponent()} for the {@code DockingPort} in this split
     * pane's elder region.
     * <p>
     * The elder region of this {@code DockingSplitPane} is determined using the
     * value returned from {@code getRegion()}, where {@code getRegion()}
     * indicates the docking region of the 'new' {@code Dockable} for this
     * {@code DockingSplitPane}.
     *
     * @return the 'oldest' {@code Component} to have been added to this
     *         {@code DockingSplitPane} as a result of a docking operation.
     * @see #getRegion()
     * @see DockingPort#getDockedComponent()
     */
    public Component getElderComponent() {
        Component c = controllerInTopLeft ? getLeftComponent()
            : getRightComponent();
        if (c instanceof DockingPort)
            c = ((DockingPort) c).getDockedComponent();
        return c;
    }

    /**
     * Returns the docking region for which this {@code DockingSplitPane} was
     * created. A {@code DockingSplitPane} is created based upon the need to
     * share space within a {@code DockingPort} between two {@code Dockables}.
     * This happens when a new {@code Dockable} is introduced into an outer
     * region of a {@code DockingPort} that already contains a {@code Dockable}.
     * This method returns that outer region for which this
     * {@code DockingSplitPane} was created and may be used to control the
     * orientation of the split pane. The region returned by this method will be
     * the same passed into the {@code DockingSplitPane} constructor on
     * instantiation.
     *
     * @return the docking region for which this {@code DockingSplitPane} was
     *         created.
     * @see #DockingSplitPane(DockingPort, String)
     */
    public String getRegion() {
        return region;
    }

    /**
     * Indicates whether the 'oldest' {@code Component} to have been added to
     * this {@code DockingSplitPane} as a result of a docking operation is in
     * the TOP or LEFT side of the split pane. A {@code DockingSplitPane} is
     * created based upon the need to share space within a {@code DockingPort}
     * between two {@code Dockables}. This happens when a new {@code Dockable}
     * is introduced into an outer region of a {@code DockingPort} that already
     * contains a {@code Dockable}. The {@code Dockable} that was in the
     * {@code DockingPort} prior to splitting the layout is the 'elder'
     * {@code Component} and is returned by {@code getElderComponent()}. This
     * method indicates whether or not that {@code Component} is in the TOP or
     * LEFT side of this {@code DockingSplitPane}.
     * <p>
     * The elder region of this {@code DockingSplitPane} is determined using the
     * value returned from {@code getRegion()}, where {@code getRegion()}
     * indicates the docking region of the 'new' {@code Dockable} for this
     * {@code DockingSplitPane}.
     *
     * @return {@code true} if the 'oldest' {@code Component} to have been added
     *         to this {@code DockingSplitPane} is in the TOP or LEFT side of
     *         the split pane; {@code false} otherwise.
     * @see #getElderComponent()
     * @see #getRegion()
     */
    public boolean isElderTopLeft() {
        return controllerInTopLeft;
    }

    /**
     * Overridden to ensure proper divider location on initial rendering.
     * Sometimes, a split divider location is set as a proportion before the
     * split pane itself has been fully realized in the container hierarchy.
     * This results in a layout calculation based on a proportion of zero width
     * or height, rather than the desired proportion of width or height after
     * the split pane has been fully rendered. This method ensures that default
     * {@code JSplitPane} layout behavior is deferred until after the initial
     * dimensions of this split pane have been properly determined.
     *
     * @see java.awt.Container#doLayout()
     * @see JSplitPane#setDividerLocation(double)
     */
    public void doLayout() {
        // if they setup the docking configuration while the application
        // was first starting up, then the dividerLocation was calculated before
        // the container tree was visible, sized, validated, etc, so it'll be
        // stuck at zero. in that case, redetermine the divider location now
        // that we have valid container bounds with which to work.
        if (!isDividerSizeProperlyDetermined()) {
            // make sure this can only run once so we don't get a StackOverflow
            dividerLocDetermined = true;
            setDividerLocation(initialDividerRatio);
        }
        // continue the layout
        super.doLayout();
    }

    /**
     * Releases any internal references to external objects to aid garbage
     * collection. This method is {@code public} and may be invoked manually for
     * proactive memory management. Otherwise, this method is invoked by this
     * {@code DockingSplitPane's} {@code finalize()} method.
     */
    public void cleanup() {
        dockingPort = null;
    }

    /**
     * Sets the initial divider ration for creating split panes. The default
     * value is {@code 0.5}.
     *
     * @exception IllegalArgumentException
     *                if {@code ratio} is less than 0.0 or greater than 1.0.
     * @param ratio
     *            a ratio for determining weighting between the two sides of a
     *            split pane.
     */
    public void setInitialDividerRatio(double ratio) {
        if (ratio < 0.0 || ratio > 1.0) {
            throw new IllegalArgumentException("ratio (" + ratio
                                               + ") must be between [0.0,1,0] inclusive");
        }
        initialDividerRatio = ratio;
    }

    protected void finalize() throws Throwable {
        cleanup();
        super.finalize();
    }
}
