/*
 * Created on Feb 26, 2005
 */
package org.flexdock.view;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPanel;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.DockingStrategy;
import org.flexdock.docking.defaults.DefaultDockingStrategy;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.event.DockingListener;
import org.flexdock.docking.props.DockablePropertySet;
import org.flexdock.docking.props.PropertyManager;
import org.flexdock.plaf.PlafManager;
import org.flexdock.plaf.theme.ViewUI;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.ResourceManager;
import org.flexdock.util.SwingUtility;

/**
 * The {@code View} class is slightly incompatible with {@code JComponent}.
 * Similar to JFC/Swing top-level containers, a {@code View} contains only a
 * content pane {@code Container} and a {@code Titlebar}. The <b>content pane</b>
 * should contain all the components displayed by the {@code View}. As a
 * convenience {@code add} and its variants, {@code remove(Component)} and
 * {@code setLayout} have been overridden to forward to the
 * {@code contentPane} as necessary. This means you can write:
 *
 * <pre>
 * view.add(child);
 * </pre>
 *
 * And the child will be added to the contentPane. The content pane will always
 * be non-null. Attempting to set it to null will cause the View to throw an
 * exception. The default content pane will not have a layout manager set.
 *
 * @see javax.swing.JFrame
 * @see javax.swing.JRootPane
 *
 * @author Christopher Butler
 * @author Karl Schaefer
 */
public class View extends JComponent implements Dockable, DockingConstants {
    protected class ViewLayout implements LayoutManager2, Serializable {
        /**
         * Returns the amount of space the layout would like to have.
         *
         * @param parent
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's preferred size
         */
        public Dimension preferredLayoutSize(Container parent) {
            Dimension rd, tpd;
            Insets i = getInsets();

            if (contentPane != null) {
                rd = contentPane.getPreferredSize();
            } else {
                rd = parent.getSize();
            }
            if (titlepane != null && titlepane.isVisible()) {
                tpd = titlepane.getPreferredSize();
            } else {
                tpd = new Dimension(0, 0);
            }
            return new Dimension(Math.max(rd.width, tpd.width) + i.left
                                 + i.right, rd.height + tpd.height + i.top + i.bottom);
        }

        /**
         * Returns the minimum amount of space the layout needs.
         *
         * @param parent
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's minimum size
         */
        public Dimension minimumLayoutSize(Container parent) {
            Dimension rd, tpd;
            Insets i = getInsets();
            if (contentPane != null) {
                rd = contentPane.getMinimumSize();
            } else {
                rd = parent.getSize();
            }
            if (titlepane != null && titlepane.isVisible()) {
                tpd = titlepane.getMinimumSize();
            } else {
                tpd = new Dimension(0, 0);
            }
            return new Dimension(Math.max(rd.width, tpd.width) + i.left
                                 + i.right, rd.height + tpd.height + i.top + i.bottom);
        }

        /**
         * Returns the maximum amount of space the layout can use.
         *
         * @param target
         *            the Container for which this layout manager is being used
         * @return a Dimension object containing the layout's maximum size
         */
        public Dimension maximumLayoutSize(Container target) {
            Dimension rd, tpd;
            Insets i = getInsets();
            if (titlepane != null && titlepane.isVisible()) {
                tpd = titlepane.getMaximumSize();
            } else {
                tpd = new Dimension(0, 0);
            }
            if (contentPane != null) {
                rd = contentPane.getMaximumSize();
            } else {
                // This is silly, but should stop an overflow error
                rd = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE - i.top
                                   - i.bottom - tpd.height - 1);
            }
            return new Dimension(Math.min(rd.width, tpd.width) + i.left
                                 + i.right, rd.height + tpd.height + i.top + i.bottom);
        }

        /**
         * Instructs the layout manager to perform the layout for the specified
         * container.
         *
         * @param parent
         *            the Container for which this layout manager is being used
         */
        public void layoutContainer(Container parent) {
            Rectangle b = parent.getBounds();
            Insets i = getInsets();
            int contentY = 0;
            int w = b.width - i.right - i.left;
            int h = b.height - i.top - i.bottom;

            if (titlepane != null && titlepane.isVisible()) {
                Dimension mbd = titlepane.getPreferredSize();
                titlepane.setBounds(0, 0, w, mbd.height);
                contentY += mbd.height;
            }
            if (contentPane != null) {
                contentPane.setBounds(0, contentY, w, h - contentY);
            }
        }

        public void addLayoutComponent(String name, Component comp) {
        }

        public void removeLayoutComponent(Component comp) {
        }

        public void addLayoutComponent(Component comp, Object constraints) {
        }

        public float getLayoutAlignmentX(Container target) {
            return 0.0f;
        }

        public float getLayoutAlignmentY(Container target) {
            return 0.0f;
        }

        public void invalidateLayout(Container target) {
        }
    }

    public static final String uiClassID = "Flexdock.view";

    public static final String ACTION_TOGGLE_NEXT = "toggleNextView";
    public static final String ACTION_TOGGLE_PREVIOUS = "togglePreviousView";

    static final DockingStrategy VIEW_DOCKING_STRATEGY = createDockingStrategy();

    private String persistentId;

    protected Titlebar titlepane;

    protected Container contentPane;

    protected boolean contentPaneCheckingEnabled;

    protected ArrayList dockingListeners;

    // protected boolean active;
    protected ArrayList dragSources;

    protected HashSet frameDragSources;

    private transient HashSet blockedActions;

    static {
        DockingManager.setDockingStrategy(View.class, VIEW_DOCKING_STRATEGY);
        PropertyManager.setDockablePropertyType(View.class, ViewProps.class);
    }

    public View(String persistentId) {
        this(persistentId, null);
    }

    public View(String persistentId, String title) {
        this(persistentId, title, null);
    }

    public View(String persistentId, String title, String tabText) {
        if (persistentId == null)
            throw new IllegalArgumentException(
                "The 'persistentId' parameter cannot be null.");

        this.persistentId = persistentId;

        dragSources = new ArrayList(1);
        frameDragSources = new HashSet(1);
        dockingListeners = new ArrayList(1);

        setContentPane(createContentPane());
        setTitlebar(createTitlebar());
        setLayout(createLayout());
        setContentPaneCheckingEnabled(true);

        if (title == null)
            title = "";
        setTitle(title);

        if (tabText == null)
            tabText = title;
        setTabText(tabText);

        addHierarchyListener(new HierarchyListener() {
            public void hierarchyChanged(HierarchyEvent e) {
                clearButtonRollovers();
            }
        });

        updateUI();

        DockingManager.registerDockable((Dockable) this);

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

    private static DockingStrategy createDockingStrategy() {
        return new DefaultDockingStrategy() {
            protected DockingPort createDockingPortImpl(DockingPort base) {
                return new Viewport();
            }
        };
    }

    public static View getInstance(String viewId) {
        Dockable view = DockingManager.getDockable(viewId);
        return view instanceof View ? (View) view : null;
    }

    protected Container createContentPane() {
        return new JPanel();
    }

    protected LayoutManager createLayout() {
        return new ViewLayout();
    }

    protected Titlebar createTitlebar() {
        Titlebar t = new Titlebar();
        t.setView(this);

        return t;
    }

    public Container getContentPane() {
        return contentPane;
    }

    public Titlebar getTitlebar() {
        return titlepane;
    }

    public DockablePropertySet getDockingProperties() {
        return PropertyManager.getDockablePropertySet(this);
    }

    public ViewProps getViewProperties() {
        return (ViewProps) getDockingProperties();
    }

    public void addAction(Action action) {
        if (titlepane != null)
            titlepane.addAction(action);
    }

    public void addAction(String action) {
        if (titlepane != null)
            titlepane.addAction(action);
    }

    public void removeActions() {
        if (titlepane != null)
            titlepane.removeAllActions();
    }

    public void setIcon(Icon icon) {
        if (titlepane != null)
            titlepane.setIcon(icon);
    }

    public void setIcon(String imgUri) {
        Icon icon = imgUri == null ? null : ResourceManager.createIcon(imgUri);
        setIcon(icon);
    }

    /**
     * Sets the content pane for this view.
     *
     * @param c
     *            the container to use as the content pane.
     * @throws IllegalArgumentException
     *             if {@code c} is {@code null} or if {@code c} is the
     *             {@code titlepane}.
     * @see #titlepane
     * @see #getTitlePane()
     */
    public void setContentPane(Container c) throws IllegalArgumentException {
        if (c == null)
            throw new IllegalArgumentException(
                "Unable to set a null content pane.");
        if (c == titlepane)
            throw new IllegalArgumentException(
                "Cannot use the same component as both content pane and titlebar.");

        if (contentPane != null) {
            remove(contentPane);
        }
        contentPane = c;
        if (contentPane != null) {
            boolean checkingEnabled = isContentPaneCheckingEnabled();
            try {
                setContentPaneCheckingEnabled(false);
                add(contentPane);
            } finally {
                setContentPaneCheckingEnabled(checkingEnabled);
            }
        }
    }

    protected String getPreferredTitlebarUIName() {
        return ui instanceof ViewUI ? ((ViewUI) ui).getPreferredTitlebarUI()
               : null;
    }

    public void setTitlebar(Titlebar titlebar) {
        if (titlebar != null && titlebar == contentPane) {
            throw new IllegalArgumentException(
                "Cannot use the same component as both content pane and titlebar.");
        }
        if (titlepane != null) {
            remove(titlepane);
            titlepane.setView(null);
            dragSources.remove(titlepane);
            frameDragSources.remove(titlepane);
            DockingManager.removeDragListeners(titlepane);
        }
        titlepane = titlebar;
        if (titlepane != null) {
            boolean checkingEnabled = isContentPaneCheckingEnabled();
            try {
                setContentPaneCheckingEnabled(false);
                add(titlepane);
            } finally {
                setContentPaneCheckingEnabled(checkingEnabled);
            }

            dragSources.add(titlepane);
            frameDragSources.add(titlepane);
            DockingManager.updateDragListeners(this);
        }

    }

    protected Component getTitlePane() {
        return titlepane;
    }

    public void setTitle(String title) {
        if (titlepane != null) {
            titlepane.setText(title);
        }
    }

    public void setTitle(String title, boolean alsoTabText) {
        setTitle(title);

        if (alsoTabText) {
            setTabText(title);
        }
    }

    public String getTitle() {
        Titlebar tbar = getTitlebar();
        return tbar == null ? null : tbar.getText();
    }

    public void updateUI() {
        setUI(PlafManager.getUI(this));
    }

    public String getUIClassID() {
        return uiClassID;
    }

    /**
     * {@inheritDoc}
     */
    protected void addImpl(Component comp, Object constraints, int index) {
        if (comp instanceof Titlebar)
            ((Titlebar) comp).setView(this);

        if (isContentPaneCheckingEnabled()) {
            getContentPane().add(comp, constraints, index);
        } else {
            super.addImpl(comp, constraints, index);
        }
    }

    public void remove(Component comp) {
        if (comp == contentPane) {
            super.remove(comp);
        } else {
            getContentPane().remove(comp);
        }
    }

    public AbstractButton getActionButton(String actionName) {
        Titlebar tbar = getTitlebar();
        return tbar == null ? null : tbar.getActionButton(actionName);
    }

    public Component getComponent() {
        return this;
    }

    public List getDragSources() {
        return dragSources;
    }

    public Set getFrameDragSources() {
        return frameDragSources;
    }

    public String getPersistentId() {
        return persistentId;
    }

    public boolean isTerritoryBlocked(Dockable dockable, String region) {
        return getDockingProperties().isTerritoryBlocked(region).booleanValue();
    }

    public void setTerritoryBlocked(String region, boolean blocked) {
        getDockingProperties().setTerritoryBlocked(region, blocked);
    }

    public String getTabText() {
        String txt = getDockingProperties().getDockableDesc();
        return txt == null ? getTitle() : txt;
    }

    public void setTabText(String tabText) {
        getDockingProperties().setDockableDesc(tabText);
    }

    public Icon getTabIcon() {
        return getDockingProperties().getTabIcon();
    }

    public void setTabIcon(Icon icon) {
        getDockingProperties().setTabIcon(icon);
    }

    public boolean dock(Dockable dockable) {
        return dock(dockable, CENTER_REGION);
    }

    public DockingPort getDockingPort() {
        return DockingManager.getDockingPort((Dockable) this);
    }

    public Dockable getSibling(String region) {
        return DefaultDockingStrategy.getSibling(this, region);
    }

    public Viewport getViewport() {
        DockingPort port = getDockingPort();
        return port instanceof Viewport ? (Viewport) port : null;
    }

    public boolean dock(Dockable dockable, String relativeRegion) {
        return DockingManager.dock(dockable, this, relativeRegion);
    }

    public boolean dock(Dockable dockable, String relativeRegion, float ratio) {
        return DockingManager.dock(dockable, this, relativeRegion, ratio);
    }

    public void setActive(boolean b) {
        getViewProperties().setActive(b);
    }

    public boolean isActive() {
        return getViewProperties().isActive().booleanValue();
    }

    public void setActiveStateLocked(boolean b) {
        getViewProperties().setActiveStateLocked(b);
    }

    public boolean isActiveStateLocked() {
        return getViewProperties().isActiveStateLocked().booleanValue();
    }

    public boolean isMinimized() {
        return DockingUtility.isMinimized(this);
    }

    public int getMinimizedConstraint() {
        return DockingUtility.getMinimizedConstraint(this);
    }

    public void addDockingListener(DockingListener listener) {
        dockingListeners.add(listener);
    }

    public DockingListener[] getDockingListeners() {
        return (DockingListener[]) dockingListeners
               .toArray(new DockingListener[0]);
    }

    public void removeDockingListener(DockingListener listener) {
        dockingListeners.remove(listener);
    }

    public void dockingCanceled(DockingEvent evt) {
    }

    public void dockingComplete(DockingEvent evt) {
        setActionBlocked(DockingConstants.PIN_ACTION, isFloating());
        if (titlepane != null)
            titlepane.revalidate();
    }

    public void dragStarted(DockingEvent evt) {
    }

    public void dropStarted(DockingEvent evt) {
    }

    public void undockingComplete(DockingEvent evt) {
        clearButtonRollovers();
    }

    public void undockingStarted(DockingEvent evt) {
    }

    private void clearButtonRollovers() {
        if (titlepane == null)
            return;

        Component[] comps = titlepane.getComponents();
        for (int i = 0; i < comps.length; i++) {
            Button button = comps[i] instanceof Button ? (Button) comps[i]
                            : null;
            if (button != null) {
                button.getModel().setRollover(false);
            }
        }
    }

    public void setActionBlocked(String actionName, boolean blocked) {
        if (actionName == null)
            return;

        Set actions = getBlockedActions();
        if (blocked)
            actions.add(actionName);
        else {
            if (actions != null)
                actions.remove(actionName);
        }
    }

    public boolean isActionBlocked(String actionName) {
        return actionName == null || blockedActions == null ? false
               : blockedActions.contains(actionName);
    }

    private HashSet getBlockedActions() {
        if (blockedActions == null)
            blockedActions = new HashSet(1);
        return blockedActions;
    }

    public boolean isFloating() {
        return DockingUtility.isFloating(this);
    }

    protected String paramString() {
        return "id=" + getPersistentId() + "," + super.paramString();
    }

    /**
     * @return the contentPaneCheckingEnabled
     */
    protected boolean isContentPaneCheckingEnabled() {
        return contentPaneCheckingEnabled;
    }

    /**
     * @param contentPaneCheckingEnabled
     *            the contentPaneCheckingEnabled to set
     */
    protected void setContentPaneCheckingEnabled(
        boolean contentPaneCheckingEnabled) {
        this.contentPaneCheckingEnabled = contentPaneCheckingEnabled;
    }

    /**
     * Sets the <code>LayoutManager</code>. Overridden to conditionally
     * forward the call to the <code>contentPane</code>.
     *
     * @param manager
     *            the <code>LayoutManager</code>
     * @see #setContentPaneCheckingEnabled
     */
    public void setLayout(LayoutManager manager) {
        if (isContentPaneCheckingEnabled()) {
            getContentPane().setLayout(manager);
        } else {
            super.setLayout(manager);
        }
    }
}
