/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.swing.tabcontrol;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleRole;
import javax.swing.event.ChangeEvent;
import org.netbeans.swing.tabcontrol.event.TabActionEvent;
import org.netbeans.swing.tabcontrol.plaf.AquaEditorTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.AquaViewTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.BasicSlidingTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.WinClassicEditorTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.WinClassicViewTabDisplayerUI;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleSelection;
import javax.swing.event.ChangeListener;
import org.netbeans.swing.tabcontrol.plaf.ToolbarTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.WinXPEditorTabDisplayerUI;
import org.netbeans.swing.tabcontrol.plaf.WinXPViewTabDisplayerUI;


/**
 * A Component which displays tabs supplied by a TabDataModel.  This is 
 * essentially the upper (or lower) portion of a tabbed pane, without the
 * part that displays components.  It can be used to provide tab-like
 * selection over a data model containing anything, not just components.
 * <p>
 * It has a three display modes (more fully described in the overview for
 * <a href="TabbedContainer.html">TabbedContainer</a>), to provide different
 * styles of tab display, such as scrolling tabs and others.
 * <p>
 * TabDisplayer is completely model driven - the class itself is little more
 * than an aggregation point for a data model, a selection model, and so forth.
 * The logic that allows it to operate is implemented in the UI delegates,
 * which are installed by (and can be replaced via) the standard Swing 
 * UIManager mechanisms.
 * <p>
 * Some TabDisplayer UI's support an <i>orientation</i> property, which is provided
 * via the client property <code>PROP_ORIENTATION</code>.
 *
 * @author Tim Boudreau
 */
public final class TabDisplayer extends JComponent implements Accessible {
    
    private boolean initialized = false;
    private TabDataModel model;
    private SingleSelectionModel sel = null;
    private boolean active;
    private final int type;

    /**
     * Displayer type for view tabs, which do not scroll and simply divide the
     * available space between themselves.  The value of this field is mapped to
     * TabbedContainer.TYPE_VIEW
     */
    public static final int TYPE_VIEW = TabbedContainer.TYPE_VIEW;
    /**
     * Displayer type for editor tabs, which scroll (typically - depends on what
     * the UI does).  The value of this field is mapped to
     * TabbedContainer.TYPE_EDITOR
     */
    public static final int TYPE_EDITOR = TabbedContainer.TYPE_EDITOR;
    
    public static final int TYPE_SLIDING = TabbedContainer.TYPE_SLIDING;
    
    public static final int TYPE_TOOLBAR = TabbedContainer.TYPE_TOOLBAR;
    
    /**
     * Property indicating the tab displayer should be painted as
     * &quot;active&quot;. This is typically used to indicate keyboard focus.
     * The valud of this field is mapped to TabbedContainer.PROP_ACTIVE
     */
    public static final String PROP_ACTIVE = TabbedContainer.PROP_ACTIVE;


    /**
     * Action command indicating that the action event signifies the user
     * clicking the Close button on a tab.
     */
    public static final String COMMAND_CLOSE = TabbedContainer.COMMAND_CLOSE;

    /**
     * Action command indicating that the action event fired signifies the user
     * selecting a tab
     */
    public static final String COMMAND_SELECT = TabbedContainer.COMMAND_SELECT;

    /**
     * Action command indicating that the action event fired signifies the user
     * requesting a popup menu over a tab
     */
    public static final String COMMAND_POPUP_REQUEST = TabbedContainer.COMMAND_POPUP_REQUEST;

    /**
     * Action command indicating that the action event fired signifies the user
     * has double clicked a tab
     */
    public static final String COMMAND_MAXIMIZE = TabbedContainer.COMMAND_MAXIMIZE;

    /**
     * Action command indicating that the action event fired signifies the user
     * has shift-clicked the close button on a tab
     */
    public static final String COMMAND_CLOSE_ALL = TabbedContainer.COMMAND_CLOSE_ALL; //NOI18N

    /**
     * Action command indicating that the action event fired signifies the user
     * has alt-clicked the close button on a tab
     */
    public static final String COMMAND_CLOSE_ALL_BUT_THIS = TabbedContainer.COMMAND_CLOSE_ALL_BUT_THIS; //NOI18N

    /**
     * Action command indicating that the action event signifies the user
     * clicking the Pin button on a tab.
     */
    public static final String COMMAND_ENABLE_AUTO_HIDE = TabbedContainer.COMMAND_ENABLE_AUTO_HIDE; //NOI18N

    /**
     * Action command indicating that the action event signifies the user
     * clicking the Pin button on a tab.
     */
    public static final String COMMAND_DISABLE_AUTO_HIDE = TabbedContainer.COMMAND_DISABLE_AUTO_HIDE; //NOI18N

    /**
     * UIManager key for the UI Delegate to be used for &quot;editor&quot; style TabbedContainers
     */
    public static final String EDITOR_TAB_DISPLAYER_UI_CLASS_ID = "EditorTabDisplayerUI"; //NOI18N

    /**
     * UIManager key for the UI Delegate to be used for &quot;view&quot; style TabbedContainers
     */
    public static final String VIEW_TAB_DISPLAYER_UI_CLASS_ID = "ViewTabDisplayerUI"; //NOI18N
    
    /**
     * UIManager key for the UI delegate to be used in &quot;sliding&quot; style
     * containers */
    public static final String SLIDING_TAB_DISPLAYER_UI_CLASS_ID = "SlidingTabDisplayerUI"; //NOI18N

    /**
     * UIManager key for the UI delegate to be used for toolbar style tabs
     */
    public static final String TOOLBAR_TAB_DISPLAYER_UI_CLASS_ID = "ToolbarTabDisplayerUI"; //NOI18N
    
    /** Client property to indicate the orientation, which determines what
     * side the tabs are displayed on.  Currently this is only honored by
     * the sliding tabs ui delegate. */
    public static final String PROP_ORIENTATION = "orientation"; //NOI18N

    /** Client property value to display tabs on the left side of the control.
     */
    public static final Object ORIENTATION_EAST = "east"; //NOI18N
    /** Client property value to display tabs on the right side of the control 
     */
    public static final Object ORIENTATION_WEST = "west"; //NOI18N
    /** Client property value to display tabs on the top edge of the control 
     */
    public static final Object ORIENTATION_NORTH = "north"; //NOI18N
    /** Client property value to display tabs on the bottom edge of the control 
     */
    public static final Object ORIENTATION_SOUTH = "south"; //NOI18N
    /** Client property value for pin button to have neutral orientation 
     */
    public static final Object ORIENTATION_CENTER = "center"; //NOI18N
    
    /** Client property value for pin button to be invisible 
     */
    public static final Object ORIENTATION_INVISIBLE = "invisible"; //NOI18N
    
    
    /**
     * Utility field holding list of ActionListeners.
     */
    private transient List actionListenerList;
    
    /** Info about global positioning of this tab control 
     * or null if no global location info is needed */
    private LocationInformer locationInformer = null;

    private boolean showClose = !Boolean.getBoolean(
        "nb.tabs.suppressCloseButton"); //NOI18N
    
    public TabDisplayer () {
        this (new DefaultTabDataModel(), TYPE_VIEW);
    }
    
    /**
     * Creates a new instance of TabDisplayer
     */
    public TabDisplayer(TabDataModel model, int type) {
        this (model, type, null);
    }
    
    /**
     * Creates a new instance of TabDisplayer
     */
    public TabDisplayer(TabDataModel model, int type, LocationInformer locationInformer) {
        switch (type) {
            case TYPE_VIEW:
            case TYPE_EDITOR:
            case TYPE_SLIDING:
            case TYPE_TOOLBAR:
                break;
            default :
                throw new IllegalArgumentException("Unknown UI type: " + type); //NOI18N
        }
        this.model = model;
        this.type = type;
        this.locationInformer = locationInformer;
        putClientProperty (PROP_ORIENTATION, ORIENTATION_NORTH);
        initialized = true;
        updateUI();
        setFocusable(false);
//        Color fillC = (Color)UIManager.get("nb_workplace_fill"); //NOI18N
//        if (fillC != null) setBackground (fillC);
    }

    public final TabDisplayerUI getUI() {
        return (TabDisplayerUI) ui;
    }

    /** Overridden to block the call from the superclass constructor, which
     * comes before the <code>type</code> property is initialized.  Provides
     * a reasonable fallback UI for use on unknown look and feels.
     */
    public final void updateUI() {
        if (!initialized) {
            return;
        }
        
        if (type == TYPE_TOOLBAR) {
            setUI (new ToolbarTabDisplayerUI(this));
            return;
        } else if (type == TYPE_SLIDING) {
            setUI (new BasicSlidingTabDisplayerUI(this));
            return;
        }
        
        ComponentUI ui = null;
        if (UIManager.get(getUIClassID()) != null) { //Avoid Error stack trace
            try {
                ui = UIManager.getUI(this);
            } catch (Error error) {
                System.err.println("Could not load a UI for " + getUIClassID() + 
                    " - missing class?");
            }
        } else {
            ui = findUIStandalone();
        }
        
        if (ui == null) {
            ui = getType() == TYPE_VIEW ?
                    WinClassicViewTabDisplayerUI.createUI(this) :
                    WinClassicEditorTabDisplayerUI.createUI(this);
        }
        setUI((TabDisplayerUI) ui);
        
    }
    
    /**
     * Allows the tabcontrol to find the correct UI if the plaf library is
     * not present (no UI class defined in UIManager).
     */
    private ComponentUI findUIStandalone() {
        ComponentUI result = null;
        String lf = UIManager.getLookAndFeel().getID();
        switch (type) {
            case TYPE_VIEW :
                if ("Aqua".equals(lf)) { //NOI18N
                    result = AquaViewTabDisplayerUI.createUI(this);
                } else if ("Windows".equals(lf)) { //NOI18N
                    result = isXPLF() ? 
                        WinXPViewTabDisplayerUI.createUI(this) :
                        WinClassicViewTabDisplayerUI.createUI(this);
                }
                break;
            case TYPE_EDITOR :
                if ("Aqua".equals(lf)) { //NOI18N
                    result = AquaEditorTabDisplayerUI.createUI(this);
                } else if ("Windows".equals(lf)) { //NOI18N
                    result = isXPLF() ? 
                        WinXPEditorTabDisplayerUI.createUI(this) :
                        WinClassicEditorTabDisplayerUI.createUI(this);
                }
                break;
        }
        return result;
    }
    
    /** Finds if windows LF with XP theme is active.
     * @return true if windows LF and XP theme is active, false otherwise */
    private static boolean isXPLF () {
        Boolean isXP = (Boolean)Toolkit.getDefaultToolkit().
                        getDesktopProperty("win.xpstyle.themeActive"); //NOI18N
        return isXP == null ? false : isXP.booleanValue();
    }
    

    /** Returns an different UIClassID depending on the value of the <code>type</code>
     * property. */
    public String getUIClassID() {
        switch (getType()) {
            case TYPE_VIEW : return VIEW_TAB_DISPLAYER_UI_CLASS_ID;
            case TYPE_EDITOR : return EDITOR_TAB_DISPLAYER_UI_CLASS_ID;
            case TYPE_SLIDING : return SLIDING_TAB_DISPLAYER_UI_CLASS_ID;
            case TYPE_TOOLBAR : return TOOLBAR_TAB_DISPLAYER_UI_CLASS_ID;
            default :
                throw new IllegalArgumentException ("Unknown UI type: " + 
                    getType());
        }
    }

    /**
     * Returns whether this control uses the view tab look or the scrolling
     * editor tab look.  This is set in the constructor.
     */
    public final int getType() {
        return type;
    }

    public final Dimension getPreferredSize() {
        return getUI().getPreferredSize(this);
    }

    public final Dimension getMinimumSize() {
        return getUI().getMinimumSize(this);
    }
    
    /**
     * Cause the specified tab to flash or otherwise call attention to itself
     * without changing selection or focus.  Supported by VIEW and EDITOR type
     * UIs.
     */
    public final void requestAttention (int tab) {
        getUI().requestAttention(tab);
    }
    
    /**
     * Cause a tab, if blinking, to stop.
     */
    public final void cancelRequestAttention (int tab) {
        getUI().cancelRequestAttention (tab);
    }
    
    public final boolean requestAttention (TabData data) {
        int idx = getModel().indexOf(data);
        boolean result = idx >= 0;
        if (result) {
            requestAttention (idx);
        }
        return result;
    }

    /**
     * Accessor only for TabDisplayerUI when installing the UI
     */
    void setSelectionModel(SingleSelectionModel sel) {
        this.sel = sel;
    }

    /** Get the selection model, which determines which tab is selected.
     * To change the selection, get the selection model and call 
     * setSelectedIndex(). */
    public SingleSelectionModel getSelectionModel() {
        return sel;
    }

    /** Get the data model that defines the contents which are displayed */
    public final TabDataModel getModel() {
        return model;
    }

    /** Set the active state of the component */
    public final void setActive(boolean active) {
        if (active != this.active) {
            this.active = active;
            firePropertyChange(PROP_ACTIVE, !active, active); //NOI18N
        }
    }

    /** Gets the &quot;active&quot; state of this component.  If the component
     * is active, most UIs will paint the selected tab differently to indicate
     * that focus is somewhere in the container */
    public final boolean isActive() {
        return active;
    }

    /**
     * Gets tooltip for the tab corresponding to the mouse event, or if no
     * tab, delegates to the default implementation.
     */
    public final String getToolTipText(MouseEvent event) {
        if (ui != null) {
            Point p = event.getPoint();
            if (event.getSource() != this) {
                Component c = (Component) event.getSource();
                p = SwingUtilities.convertPoint(c, p, this);
            }
            int index = getUI().tabForCoordinate(p);
            if (index != -1) {
                // #55101 - have special tooltips for buttons.
                String button = getUI().getTooltipForButtons(p);
                if (button != null) {
                    return button;
                }
                return getModel().getTab(index).tip;
            }
        }
        return super.getToolTipText(event);
    }

    /** Make a tab visible.  In the case of scrolling UIs, a tab is not
     * always visible.  This call will make it scroll into view */
    public final void makeTabVisible(int index) { 
        getUI().makeTabVisible(index);
    }

    /** Get the rectangle that a given tab occupies */
    public final Rectangle getTabRect(int tab, Rectangle dest) {
        if (dest == null) {
            dest = new Rectangle();
        }
        getUI().getTabRect(tab, dest);
        return dest;
    }

    public final Image getDragImage(int index) {
        return getUI().createImageOfTab(index);
    }

    /**
     * Register an ActionListener.  TabbedContainer and TabDisplayer guarantee
     * that the type of event fired will always be TabActionEvent.  There are
     * two special things about TabActionEvent: <ol> <li>There are methods on
     * TabActionEvent to find the index of the tab the event was performed on,
     * and if present, retrieve the mouse event that triggered it, for clients
     * that wish to provide different handling for different mouse buttons</li>
     * <li>TabActionEvents can be consumed.  If a listener consumes the event,
     * the UI will take no action - the selection will not be changed, the tab
     * will not be closed.  Consuming the event means taking responsibility for
     * doing whatever would normally happen automatically.  This is useful for,
     * for example, showing a dialog and possibly aborting closing a tab if it
     * contains unsaved data, for instance.</li> </ol> Action events will be
     * fired <strong>before</strong> any action has been taken to alter the
     * state of the control to match the action, so that they may be vetoed or
     * modified by consuming the event.
     *
     * @param listener The listener to register.
     */
    public final synchronized void addActionListener(ActionListener listener) {
        if (actionListenerList == null) {
            actionListenerList = new ArrayList();
        }
        actionListenerList.add(listener);
    }

    /**
     * Removes ActionListener from the list of listeners.
     *
     * @param listener The listener to remove.
     */
    public final synchronized void removeActionListener(ActionListener listener) {
        if (actionListenerList != null) {
            actionListenerList.remove(listener);
        }
    }

    public String getCommandAtPoint(Point p) {
        return getUI().getCommandAtPoint (p);
    }
    
    public void registerShortcuts(JComponent comp) {
        getUI().registerShortcuts(comp);
    }
    
    public void unregisterShortcuts(JComponent comp) {
        getUI().unregisterShortcuts(comp);
    }

    /**
     * Notifies all registered listeners about the event.
     *
     * @param event The event to be fired
     */
    protected final void postActionEvent(TabActionEvent event) {
        List list;
        synchronized (this) {
            if (actionListenerList == null) {
                return;
            }
            list = Collections.unmodifiableList(actionListenerList);
        }
        for (int i = 0; i < list.size(); i++) {
            ((ActionListener) list.get(i)).actionPerformed(event);
        }
    }

    public int tabForCoordinate(Point p) {
        return getUI().tabForCoordinate(p);
    }
    
    public LocationInformer getLocationInformer() {
        return locationInformer;
    }
    
    public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new AccessibleTabDisplayer();
        }
        return accessibleContext;
    }

    /**
     * Set whether or not the close button should be visible.
     * This can be defaulted by setting the system property
     * <code>nb.tabs.suppressCloseButton</code>.  The default is
     * true.
     */
    public final void setShowCloseButton (boolean val) {
        boolean wasShow = isShowCloseButton();
        if (wasShow != val) {
            showClose = val;
            if (isShowing()) {
                repaint();
            }
            firePropertyChange ("showCloseButton", !val, val);
        }
    }
    
    /** Find out if this displayer is set to show close buttons */
    public final boolean isShowCloseButton () {
        return showClose;
    }
    
    
    protected class AccessibleTabDisplayer extends AccessibleJComponent
                                           implements AccessibleSelection, ChangeListener {
                                               
         /**
         *  Constructs an AccessibleTabDisplayer
         */
        public AccessibleTabDisplayer() {
            super();
            getModel().addChangeListener(this);
        }

        public void stateChanged(ChangeEvent e) {
            Object o = e.getSource();
            firePropertyChange(AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY,
                               null, o);
        }

        
        /**
         * Get the role of this object.
         *
         * @return an instance of AccessibleRole describing the role of 
         *          the object
         */
        public AccessibleRole getAccessibleRole() {
            return AccessibleRole.PAGE_TAB_LIST;
        }
        
        /**
         * Returns the number of accessible children in the object.
         *
         * @return the number of accessible children in the object.
         */
        public int getAccessibleChildrenCount() {
            return getModel().size();
        }

        /**
         * Return the specified Accessible child of the object.
         *
         * @param i zero-based index of child
         * @return the Accessible child of the object
         * @exception IllegalArgumentException if index is out of bounds
         */
        public Accessible getAccessibleChild(int i) {
            if (i < 0 || i >= getModel().size()) {
                return null;
            }
            TabData data = getModel().getTab(i);
            if (data.getComponent() instanceof Accessible) {
                return (Accessible)data.getComponent();
            }
            return null;
        }
        
        

        /**
         * Gets the <code>AccessibleSelection</code> associated with
         * this object.  In the implementation of the Java 
         * Accessibility API for this class, 
	 * returns this object, which is responsible for implementing the
         * <code>AccessibleSelection</code> interface on behalf of itself.
	 * 
	 * @return this object
         */
        public AccessibleSelection getAccessibleSelection() {
           return this;
        }        
        
        /**
         * Returns the <code>Accessible</code> child contained at
         * the local coordinate <code>Point</code>, if one exists.
         * Otherwise returns the currently selected tab.
         *
         * @return the <code>Accessible</code> at the specified
         *    location, if it exists
         */
        public Accessible getAccessibleAt(Point p) {
            int tab = tabForCoordinate(p);
            if (tab == -1) {
                tab = getSelectionModel().getSelectedIndex();
            }
            return getAccessibleChild(tab);
        }        
        
        /**
         * Returns the number of Accessible children currently selected.
         * If no children are selected, the return value will be 0.
         *
         * @return the number of items currently selected.
         */
        public int getAccessibleSelectionCount() {
            return 1;
        }
        
        /**
         * Returns an Accessible representing the specified selected child
         * of the object.  If there isn't a selection, or there are
         * fewer children selected than the integer passed in, the return
         * value will be null.
         * <p>Note that the index represents the i-th selected child, which
         * is different from the i-th child.
         *
         * @param i the zero-based index of selected children
         * @return the i-th selected child
         * @see #getAccessibleSelectionCount
         */
        public Accessible getAccessibleSelection(int i) {
            // always just one selected.. -> ignore i
            int index  = getSelectionModel().getSelectedIndex();
            return getAccessibleChild(index);
        }
        
        /**
         * Determines if the current child of this object is selected.
         *
         * @return true if the current child of this object is selected; else false.
         * @param i the zero-based index of the child in this Accessible object.
         * @see AccessibleContext#getAccessibleChild
         */
        public boolean isAccessibleChildSelected(int i) {
            return i == getSelectionModel().getSelectedIndex();
        }
        
        /**
         * Adds the specified Accessible child of the object to the object's
         * selection.  If the object supports multiple selections,
         * the specified child is added to any existing selection, otherwise
         * it replaces any existing selection in the object.  If the
         * specified child is already selected, this method has no effect.
         *
         * @param i the zero-based index of the child
         * @see AccessibleContext#getAccessibleChild
         */
        public void addAccessibleSelection(int i) {
            //TODO?
        }
        
        /**
         * Removes the specified child of the object from the object's
         * selection.  If the specified item isn't currently selected, this
         * method has no effect.
         *
         * @param i the zero-based index of the child
         * @see AccessibleContext#getAccessibleChild
         */
        public void removeAccessibleSelection(int i) {
            //TODO?
        }
        
        /**
         * Clears the selection in the object, so that no children in the
         * object are selected.
         */
        public void clearAccessibleSelection() {
            //TODO?
        }
        
        /**
         * Causes every child of the object to be selected
         * if the object supports multiple selections.
         */
        public void selectAllAccessibleSelection() {
            //TODO?
        }
    }
    
}
