/* $Id: LabelledLayout.java 19034 2011-02-13 18:12:44Z bobtarling $
 *****************************************************************************
 * Copyright (c) 2009 Contributors - see below
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    bobtarling
 *****************************************************************************
 *
 * Some portions of this file was previously release using the BSD License:
 */

// Copyright (c) 2008-2009 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

package org.argouml.uml.ui;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.util.ArrayList;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.UIManager;

/**
 * This layout manager lines up components in 2 columns. All JLabels
 * are the first column and any component the JLabel is registered
 * with is in a second column next to the label. <p>
 *
 * Components are sized automatically to fill available space in the container
 * when it is resized. <p>
 * 
 * All JLabel widths will be the largest of the JLabel preferred widths (unless
 * the container is too narrow). <p>
 * 
 * The components will take up any left over width unless they are
 * restricted themselves by a maximum width. <p>
 * 
 * The height of each component is either fixed or will resize to use up any
 * available space in the container. Whether a components height is resizable
 * is determined by checking whether the preferred height of that component is
 * greater then its minimum height. This is the case for components such as
 * JList which would require to expand to show the maximum number or items. <p>
 * 
 * If a component is not to have its height resized then its preferred
 * height and minimum height should be the same. This is the case for
 * components such as JTextField and JComboBox* which should always stay the
 * same height. <p>
 * 
 * [There is known bug in JRE5 where the prefered height and minimum height of
 * a JComboBox can differ. LabelledLayout has coded a workaround for this bug.
 * See - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6255154 ] <p>
 * 
 * LabelledLayout can show multiple panels of label/component
 * pairs. The seperation of these panels is indicated by adding a
 * Seperator component to the container. Labelled layout starts
 * a new panel when detecting this Seperator. <p>
 * 
 * When there are multiple panels, each panel is given equal width.
 * The width restriction of JLabels and components described above are then
 * dependent on panel width rather than container width.
 *
 * @author Bob Tarling
 */
public class LabelledLayout implements LayoutManager, java.io.Serializable {

    private static final long serialVersionUID = -5596655602155151443L;

    /**
     * This is the horizontal gap (in pixels) which specifies the space
     * between sections.  They can be changed at any time.
     * This should be a non negative integer.
     *
     * @serial
     * @see #getHgap()
     * @see #setHgap(int)
     */
    private int hgap;
    
    /**
     * This is the vertical gap (in pixels) which specifies the space
     * between rows.  They can be changed at any time.
     * This should be a non negative integer.
     *
     * @serial
     * @see #getVgap()
     * @see #setVgap(int)
     */
    private int vgap;

    private boolean ignoreSplitters;

    /**
     * Construct a new LabelledLayout.
     */
    public LabelledLayout() {
        ignoreSplitters = false;
        hgap = 0;
        vgap = 0;
    }

    /**
     * Construct a new LabelledLayout.
     */
    public LabelledLayout(boolean ignoreSplitters) {
        this.ignoreSplitters = ignoreSplitters;
        this.hgap = 0;
        this.vgap = 0;
    }

    /**
     * Construct a new horizontal LabelledLayout with the specified
     * cell spacing.
     * @param hgap The horizontal gap between components
     * @param vgap The vertical gap between components
     */
    public LabelledLayout(int hgap, int vgap) {
        this.ignoreSplitters = false;
        this.hgap = hgap;
        this.vgap = vgap;
    }

    /** 
     * Adds the specified component with the specified name to the
     * layout. This is included to satisfy the LayoutManager interface
     * but is not actually used in this layout implementation.
     *
     * @param name the name of the component
     * @param comp the component to be added
     */
    public void addLayoutComponent(String name, Component comp) {
    }

    /** 
     * Removes the specified component from
     * the layout. This is included to satisfy the LayoutManager
     * interface but is not actually used in this layout
     * implementation.
     *
     * @param comp the component
     */
    public void removeLayoutComponent(Component comp) {
    }

    /**
     * Determines the preferred size of the container argument using
     * this labelled layout.  The preferred size is that all child
     * components are in one section at their own preferred size with
     * gaps and border indents.
     */
    public Dimension preferredLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            final Insets insets = parent.getInsets();
            int preferredWidth = 0;
            int preferredHeight = 0;
            int widestLabel = 0;

            final int componentCount = parent.getComponentCount();
            for (int i = 0; i < componentCount; ++i) {
                Component childComp = parent.getComponent(i);
                if (childComp.isVisible()
                        && !(childComp instanceof Seperator)) {
                    int childHeight = getPreferredHeight(childComp);
                    if (childComp instanceof JLabel) {
                        final JLabel jlabel = (JLabel) childComp;
                        widestLabel = 
                            Math.max(widestLabel, getPreferredWidth(jlabel));
                        childComp = jlabel.getLabelFor();
                        final int childWidth = getPreferredWidth(childComp);
                        preferredWidth = 
                            Math.max(preferredWidth, childWidth);

                        childHeight =
                            Math.min(childHeight, getPreferredHeight(jlabel));
                    }
                    preferredHeight += childHeight + this.vgap;
                }
            }
            preferredWidth += insets.left + widestLabel + insets.right;
            preferredHeight += insets.top + insets.bottom;
            return new Dimension(
                    insets.left + widestLabel + preferredWidth + insets.right,
                    preferredHeight);
        }
    }
    
    /** 
     * Required by LayoutManager.
     * 
     * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container)
     */
    public Dimension minimumLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            final Insets insets = parent.getInsets();
            int minimumHeight = insets.top + insets.bottom;

            final int componentCount = parent.getComponentCount();
            for (int i = 0; i < componentCount; ++i) {
                Component childComp = parent.getComponent(i);
                if (childComp instanceof JLabel) {
                    final JLabel jlabel = (JLabel) childComp;
                    childComp = jlabel.getLabelFor();
                    
                    final int childHeight = Math.max(
                            getMinimumHeight(childComp),
                            getMinimumHeight(jlabel));
                    minimumHeight += childHeight + this.vgap;
                }
            }
            return new Dimension(0, minimumHeight);
        }
    }

    /**
     * @see java.awt.LayoutManager#layoutContainer(java.awt.Container)
     */
    public void layoutContainer(Container parent) {
        synchronized (parent.getTreeLock()) {
            int sectionX = parent.getInsets().left;

            final ArrayList<Component> components = new ArrayList<Component>();
            final int sectionCount = getSectionCount(parent);
            final int sectionWidth = getSectionWidth(parent, sectionCount);
            int sectionNo = 0;
            for (int i = 0; i < parent.getComponentCount(); ++i) {
                final Component childComp = parent.getComponent(i);
                if (childComp instanceof Seperator) {
                    if (!this.ignoreSplitters) {
                        layoutSection(
                                parent, 
                                sectionX, 
                                sectionWidth, 
                                components, 
                                sectionNo++);
                        sectionX += sectionWidth + this.hgap;
                        components.clear();
                    }
                } else {
                    components.add(parent.getComponent(i));
                }
            }
            layoutSection(
                    parent, 
                    sectionX, 
                    sectionWidth, 
                    components, 
                    sectionNo);
        }
    }

    /** Determine the number of sections.  There is only ever one
     * section if oriented vertically.  If oriented horizontally the
     * number of sections is deduced from the number of Splitters in
     * the parent container.
     */
    private int getSectionCount(Container parent) {
        int sectionCount = 1;
        final int componentCount = parent.getComponentCount();
        if (!ignoreSplitters) {
            for (int i = 0; i < componentCount; ++i) {
                if (parent.getComponent(i) instanceof Seperator) {
                    ++sectionCount;
                }
            }
        }
        return sectionCount;
    }
    
    /**
     * Determine the width of each section from the section count.
     * This is the working width minus the gaps between sections. This
     * result is then divided equally by the section count.
     */
    private int getSectionWidth(Container parent, int sectionCount) {
        return (getUsableWidth(parent) - (sectionCount - 1) * this.hgap)
                / sectionCount;
    }

    /**
     * Determine the usable width of the parent.
     * This is the full width minus any borders.
     */
    private int getUsableWidth(Container parent) {
        final Insets insets = parent.getInsets();
        return parent.getWidth() - (insets.left + insets.right);
    }
    
    /**
     * Layout a single section
     */
    private void layoutSection(
            final Container parent,
            final int sectionX,
            final int sectionWidth,
            final ArrayList components,
            final int sectionNo) {
        final ArrayList<Integer> rowHeights = new ArrayList<Integer>();

        final int componentCount = components.size();
        if (componentCount == 0) {
            return;
        }

        int labelWidth = 0;
        int unknownHeightCount = 0;
        int totalHeight = 0;

        // Build up an array list of the heights of each label/component pair.
        // Heights of zero indicate a proportional height.
        Component previousComp = null;
        for (int i = 0; i < componentCount; ++i) {
            final Component childComp = (Component) components.get(i);
            final int childHeight;
            if (childComp instanceof JLabel) {
                final JLabel jlabel = (JLabel) childComp;
                final Component labelledComp = jlabel.getLabelFor();
                
                labelWidth = Math.max(labelWidth, getPreferredWidth(jlabel));
                
                if (labelledComp != null) {
                    ++i;
                    childHeight = getChildHeight(labelledComp);
                    if (childHeight == 0) {
                        ++unknownHeightCount;
                    }
                } else {
                    childHeight = getPreferredHeight(jlabel);
                }
                
                totalHeight += childHeight + this.vgap;
                rowHeights.add(new Integer(childHeight));
            } else {
                // to manage the case there are no label/component
                // pairs but just one component
                childHeight = getChildHeight(childComp);
                if (childHeight == 0) {
                    ++unknownHeightCount;
                }
                
                totalHeight += childHeight + this.vgap;
                rowHeights.add(new Integer(childHeight));
            }
            
            previousComp = childComp;
        }
        totalHeight -= this.vgap;
        
        final Insets insets = parent.getInsets();
        final int parentHeight = 
            parent.getHeight() - (insets.top + insets.bottom);
        // Set the child components to the heights in the array list
        // calculating the height of any proportional component on the
        // fly.  FIXME - This assumes that the JLabel and the
        // component it labels have been added to the parent component
        // consecutively.
        int y = insets.top;
        int row = 0;
        previousComp = null;
        for (int i = 0; i < componentCount; ++i) {
            Component childComp = (Component) components.get(i);
            if (childComp.isVisible()) {
                int rowHeight;
                int componentWidth = sectionWidth;
                int componentX = sectionX;
                // If the component is a JLabel which has another
                // component assigned then position/size the label and
                // calculate the size of the registered component
                if (childComp instanceof JLabel
                        && ((JLabel) childComp).getLabelFor() != null) {
                    i++; // Assumes the next child is the labelled component
                    final JLabel jlabel = (JLabel) childComp;
                    childComp = jlabel.getLabelFor();
                    jlabel.setBounds(sectionX, y, labelWidth,
                                     getPreferredHeight(jlabel));
                    componentWidth = sectionWidth - (labelWidth);
                    componentX = sectionX + labelWidth;
                }
                rowHeight = rowHeights.get(row).intValue();
                if (rowHeight == 0) {
                    try {
                        rowHeight = calculateHeight(
                                parentHeight, 
                                totalHeight, 
                                unknownHeightCount--, 
                                childComp);
                    } catch (ArithmeticException e) {
                        String lookAndFeel = 
                            UIManager.getLookAndFeel().getClass().getName();
                        throw new IllegalStateException(
                                "Division by zero laying out "
                                + childComp.getClass().getName()
                                + " on " + parent.getClass().getName()
                                + " in section " + sectionNo
                                + " using "
                                + lookAndFeel,
                                e);
                    }
                    totalHeight += rowHeight;
                }
                // Make sure the component width isn't any greater
                // than its maximum allowed width
                if (childComp.getMaximumSize() != null
                        && getMaximumWidth(childComp) < componentWidth) {
                    componentWidth = getMaximumWidth(childComp);
                }
                childComp.setBounds(componentX, y, componentWidth, rowHeight);
                y += rowHeight + this.vgap;
                ++row;
                previousComp = childComp;
            }
        }
    }
    
    /**
     * @param childComp a component
     * @return 0 for a resizable component or a positive value for its fixed
     * height
     */
    private int getChildHeight(Component childComp) {
        if (isResizable(childComp)) {
            // If the child component is resizable then
            // we don't know it's actual size yet.
            // It will be calculated later as a
            // proportion of the available left over
            // space.  For now this is flagged as zero.
            return 0;
        } else {
            // If a preferred height is not given or is
            // the same as the minimum height then fix the
            // height of this row.
            return getMinimumHeight(childComp);
        }
    }

    /**
     * A component is resizable if its minimum size is less than
     * its preferred size.
     * There is a workaround here for a bug introduced in JRE5
     * where JComboBox minimum and preferred size now differ.
     * JComboBox is not resizable.
     * Anything in a JScrollPane is considered resizable
     * @param comp the component to check for resizability.
     * @return true if the given component should be resized to take u[p empty
     * space.
     */
    private boolean isResizable(Component comp) {
        if (comp == null) {
            return false;
        }
        if (comp instanceof JComboBox) {
            return false;
        }
        if (comp.getPreferredSize() == null) {
            return false;
        }
        if (comp.getMinimumSize() == null) {
            return false;
        }
        return (getMinimumHeight(comp) < getPreferredHeight(comp));
    }

    private final int calculateHeight(
            final int parentHeight, 
            final int totalHeight,
            final int unknownHeightsLeft, 
            final Component childComp) {
        return Math.max(
                (parentHeight - totalHeight) / unknownHeightsLeft,
                getMinimumHeight(childComp));
    }
    
    private int getPreferredHeight(final Component comp) {
        return (int) comp.getPreferredSize().getHeight();
    }
    
    private int getPreferredWidth(final Component comp) {
        return (int) comp.getPreferredSize().getWidth();
    }

    private int getMinimumHeight(final Component comp) {
        return (int) comp.getMinimumSize().getHeight();
    }
    
    private int getMaximumWidth(final Component comp) {
        return (int) comp.getMaximumSize().getWidth();
    }
    
    /**
     * Create a new instance of the Separator that splits the layout in columns
     * @return the separator
     */
    public static Seperator getSeparator() {
        return new Seperator();
    }

    /**
     * @return the horizontal gaps between components
     */
    public int getHgap() {
        return this.hgap;
    }

    /**
     * Set the horizontal gaps between components
     * @param hgap the horizontal gap
     */
    public void setHgap(int hgap) {
        this.hgap = hgap;
    }

    /**
     * @return the vertical gaps between components
     */
    public int getVgap() {
        return this.vgap;
    }

    /**
     * Set the vertical gaps between components
     * @param vgap the horizontal gap
     */
    public void setVgap(int vgap) {
        this.vgap = vgap;
    }
}

class Seperator extends JPanel {
    
    private static final long serialVersionUID = -4143634500959911688L;

    Seperator() {
        super.setVisible(false);
    }
}
