/* Tabbox.java

	Purpose:

	Description:

	History:
		Tue Jul 12 10:42:31     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
 */
package org.zkoss.zul;

import java.util.Iterator;

import org.zkoss.io.Serializables;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Deferrable;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.impl.TabboxEngine;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;

/**
 * A tabbox.
 *
 * <p>
 * Event:
 * <ol>
 * <li>org.zkoss.zk.ui.event.SelectEvent is sent when user changes the tab.</li>
 * </ol>
 *
 * <p>
 * Mold:
 * <dl>
 * <dt>default</dt>
 * <dd>The default tabbox.</dd>
 * <dt>accordion</dt>
 * <dd>The accordion tabbox.</dd>
 * </dl>
 *
 * <p>{@link Toolbar} only works in the horizontal default mold and
 * the {@link #isTabscroll()} to be true. (since 3.6.3)
 *  
 * <p>Default {@link #getZclass}: z-tabbox. (since 3.5.0)
 *
 * <p>
 * Besides creating {@link Tab}  and {@link Tabpanel} programmatically, you could
 * assign a data model (a {@link ListModel} to a Tabbox via {@link #setModel(ListModel)}
 * and then the tabbox will retrieve data via {@link ListModel#getElementAt} when
 * necessary. (since 7.0.0) [ZK EE]
 *
 * <p>
 * Besides assign a list model, you could assign a renderer (a
 * {@link TabboxRenderer} instance) to a Tabbox, such that the Tabbox will
 * use this renderer to render the data returned by
 * {@link ListModel#getElementAt}. If not assigned, the default renderer, which
 * assumes a label per Tab and Tabpanel, is used. In other words, the default renderer
 * adds a label to a Tab and Tabpanel by calling toString against the object returned by
 * {@link ListModel#getElementAt}  (since 7.0.0) [ZK EE]
 *
 * <p>To retrieve what are selected in Tabbox with a {@link Selectable}
 * {@link ListModel}, you shall use {@link Selectable#getSelection} to get what
 * is currently selected object in {@link ListModel} rather than using
 * {@link Tabbox#getSelectedTab()}. That is, you shall operate on the data of
 * the {@link ListModel} rather than on the {@link Tab} of the {@link Tabbox}
 * if you use the {@link Selectable} {@link ListModel}.  (since 7.0.0) [ZK EE]
 *
 * <pre><code>
 * Set selection = ((Selectable)getModel()).getSelection();
 * </code></pre>
 * 
 * @author tomyeh
 */
public class Tabbox extends XulElement {
	private transient Tabs _tabs;
	private transient Toolbar _toolbar;
	private transient Tabpanels _tabpanels;
	private transient Tab _seltab;
	private String _panelSpacing;
	private String _orient = "top";
	private boolean _tabscroll = true;
	private boolean _maximalHeight = false;
	/** The event listener used to listen onSelect for each tab. */
	/* package */transient EventListener<Event> _listener;

	private transient ListModel<?> _model;
	private transient ListDataListener _dataListener;
	private transient TabboxRenderer<?> _renderer;
	private transient TabboxEngine _engine;

	public Tabbox() {
		init();
	}

	private void init() {
		_listener = new Listener();
	}

	/** Returns the implementation tabbox engine.
	 * @exception UiException if failed to load the engine.
	 * @since 7.0.0
	 */
	public TabboxEngine getEngine() throws UiException {
		if (_engine == null)
			_engine = newTabboxEngine();
		return _engine;
	}

	/** Sets the tabbox engine for {@link ListModel}
	 * @since 7.0.0
	 */
	public void setEngine(TabboxEngine engine) {
		if (_engine != engine) {
			_engine = engine;
		}

		//Always redraw, if any
		postOnInitRender();
	}

	/** Instantiates the default tabbox engine.
	 * It is called, if {@link #setEngine} is not called with non-null
	 * engine.
	 *
	 * <p>By default, it looks up the library property called
	 * org.zkoss.zul.tabbox.engine.class.
	 * If found, the value is assumed to be
	 * the class name of the tabbox engine (it must implement
	 * {@link TabboxEngine}).
	 * If not found, {@link UiException} is thrown.
	 *
	 * <p>Derived class might override this method to provide your
	 * own default class.
	 *
	 * @exception UiException if failed to instantiate the engine
	 * @since 7.0.0
	 */
	protected TabboxEngine newTabboxEngine() throws UiException {
		final String PROP = "org.zkoss.zul.tabbox.engine.class";
		final String klass = Library.getProperty(PROP);
		if (klass == null)
			throw new UiException("Library property,  " + PROP + ", required");

		final Object v;
		try {
			v = Classes.newInstanceByThread(klass);
		} catch (Exception ex) {
			throw UiException.Aide.wrap(ex);
		}
		if (!(v instanceof TabboxEngine))
			throw new UiException(TabboxEngine.class + " must be implemented by " + v);
		return (TabboxEngine) v;
	}

	@SuppressWarnings("unchecked")
	/**
	 * Returns the selectable model, if any.
	 * @since 7.0.0
	 */
	public Selectable<Object> getSelectableModel() {
		return (Selectable<Object>) _model;
	}

	private static boolean disableFeature() {
		return !WebApps.getFeature("ee");
	}

	/**
	 * Sets the list model associated with this t. If a non-null model
	 * is assigned, no matter whether it is the same as the previous, it will
	 * always cause re-render. [ZK EE]
	 * 
	 * @param model
	 *            the list model to associate, or null to dissociate any
	 *            previous model.
	 * @exception UiException
	 *                if failed to initialize with the model
	 * @since 7.0.0
	 */
	public void setModel(ListModel<?> model) {
		if (disableFeature())
			throw new IllegalAccessError("ZK EE version only!");
		if (model != null) {
			if (!(model instanceof Selectable))
				throw new UiException(model.getClass() + " must implement " + Selectable.class);

			if (_model != model) {
				if (_model != null) {
					_model.removeListDataListener(_dataListener);
				}
				_model = model;
				_seltab = null;
				initDataListener();
				postOnInitRender();
			}
		} else if (_model != null) {
			_model.removeListDataListener(_dataListener);
			_model = null;
			invalidate();
		}
	}

	private void initDataListener() {
		if (_dataListener == null)
			_dataListener = new ListDataListener() {
				public void onChange(ListDataEvent event) {
					getEngine().doDataChange(Tabbox.this, event);
				}
			};
		_model.addListDataListener(_dataListener);
	}

	/**
	 * Returns the renderer to render each tab and tabpanel, or null if the default renderer
	 * is used.
	 * @since 7.0.0
	 */
	@SuppressWarnings("unchecked")
	public <T> TabboxRenderer<T> getTabboxRenderer() {
		return (TabboxRenderer<T>) _renderer;
	}

	/**
	 * Sets the renderer which is used to render each tab and tabpanel if {@link #getModel}
	 * is not null. [ZK EE]
	 * 
	 * <p>
	 * Note: changing a render will not cause the tabbox to re-render. If you
	 * want it to re-render, you could assign the same model again (i.e.,
	 * setModel(null) and than setModel(oldModel)), or fire an {@link ListDataEvent} event.
	 * 
	 * @param renderer
	 *            the renderer, or null to use the default.
	 * @exception UiException
	 *                if failed to initialize with the model
	 * @since 7.0.0
	 */
	public void setTabboxRenderer(TabboxRenderer<?> renderer) {
		if (disableFeature())
			throw new IllegalAccessError("ZK EE version only!");
		if (_renderer != renderer) {
			_renderer = renderer;
			postOnInitRender();
		}
	}

	/**
	 * Sets the renderer by use of a class name. It creates an instance
	 * automatically. [ZK EE]
	 * 
	 * @since 7.0.0
	 * @see #setTabboxRenderer(TabboxRenderer)
	 */
	public void setTabboxRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
			IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
		if (clsnm != null)
			setTabboxRenderer((TabboxRenderer<?>) Classes.newInstanceByThread(clsnm));
	}

	public void onInitRender() {
		if (disableFeature())
			throw new IllegalAccessError("ZK EE version only!");
		removeAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED);
		doInitRenderer();
		invalidate();
	}

	private void doInitRenderer() {
		if (disableFeature())
			throw new IllegalAccessError("ZK EE version only!");
		getEngine().doInitRenderer(this);
		Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the tabbox when items have been rendered.

	}

	/**
	 * Component internal use only.
	 * @since 7.0.0
	 */
	public void postOnInitRender() {
		if (getAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED) == null) {
			setAttribute(TabboxEngine.ATTR_ON_INIT_RENDER_POSTED, Boolean.TRUE);
			Events.postEvent("onInitRender", this, null);
		}
	}

	/**
	 * Returns the model associated with this selectbox, or null if this
	 * selectbox is not associated with any list data model.
	 */
	@SuppressWarnings("unchecked")
	public <T> ListModel<T> getModel() {
		return (ListModel<T>) _model;
	}

	/**
	 * Returns whether it is in the accordion mold.
	 */
	/* package */boolean inAccordionMold() {
		return getMold().startsWith("accordion");
	}

	/**
	 * Returns the tabs that this tabbox owns.
	 */
	public Tabs getTabs() {
		return _tabs;
	}

	/**
	 * Returns the auxiliary toolbar that this tabbox owns.
	 * 
	 * @since 3.6.3
	 */
	public Toolbar getToolbar() {
		return _toolbar;
	}

	/**
	 * Returns the tabpanels that this tabbox owns.
	 */
	public Tabpanels getTabpanels() {
		return _tabpanels;
	}

	/**
	 * Returns whether the tab scrolling is enabled.
	 * Default: true.
	 * @since 3.5.0
	 */
	public boolean isTabscroll() {
		return _tabscroll;
	}

	/**
	 * Sets whether to enable the tab scrolling.
	 * When enabled, if tab list is wider than tab bar, left, right arrow will appear.
	 * @since 3.5.0
	 */
	public void setTabscroll(boolean tabscroll) {
		if (_tabscroll != tabscroll) {
			_tabscroll = tabscroll;
			smartUpdate("tabscroll", _tabscroll);
		}
	}

	/**
	 * Returns whether to use maximum height of all tabpanel in initial phase.
	 * <p> 
	 * Default: false.
	 * @since 7.0.0
	 */
	public boolean isMaximalHeight() {
		return _maximalHeight;
	}

	/**
	 * Sets whether to use maximum height of all tabpanel in initial phase.
	 * <p>
	 * The Client ROD feature will be disabled if it is set to true.
	 * @since 7.0.0
	 */
	public void setMaximalHeight(boolean maximalHeight) {
		if (_maximalHeight != maximalHeight) {
			_maximalHeight = maximalHeight;
			smartUpdate("maximalHeight", _maximalHeight);
		}
	}

	/**
	 * Returns the spacing between {@link Tabpanel}. This is used by certain
	 * molds, such as accordion.
	 * <p>
	 * Default: null (no spacing).
	 */
	public String getPanelSpacing() {
		return _panelSpacing;
	}

	/**
	 * Sets the spacing between {@link Tabpanel}. This is used by certain molds,
	 * such as accordion.
	 */
	public void setPanelSpacing(String panelSpacing) {
		if (panelSpacing != null && panelSpacing.length() == 0)
			panelSpacing = null;

		if (!Objects.equals(_panelSpacing, panelSpacing)) {
			_panelSpacing = panelSpacing;
			smartUpdate("panelSpacing", _panelSpacing);
		}
	}

	/**
	 * Returns the selected index.
	 */
	public int getSelectedIndex() {
		return _seltab != null ? _seltab.getIndex() : -1;
	}

	/**
	 * Sets the selected index.
	 */
	public void setSelectedIndex(int j) {
		final Tabs tabs = getTabs();
		if (tabs == null)
			throw new IllegalStateException("No tab at all");
		if (j >= 0)
			setSelectedTab((Tab) tabs.getChildren().get(j));
		else
			setSelectedTab((Tab) tabs.getFirstChild()); // keep the first one selected.
	}

	/**
	 * Returns the selected tab panel.
	 */
	public Tabpanel getSelectedPanel() {
		return _seltab != null ? _seltab.getLinkedPanel() : null;
	}

	/**
	 * Sets the selected tab panel.
	 */
	public void setSelectedPanel(Tabpanel panel) {
		if (panel == null)
			throw new IllegalArgumentException("null tabpanel");
		if (panel.getTabbox() != this)
			throw new UiException("Not a child: " + panel);
		final Tab tab = panel.getLinkedTab();
		if (tab != null)
			setSelectedTab(tab);
	}

	/**
	 * Returns the selected tab.
	 */
	public Tab getSelectedTab() {
		return _seltab;
	}

	/**
	 * Sets the selected tab.
	 */
	public void setSelectedTab(Tab tab) {
		selectTabDirectly(tab, false);
	}

	/** Sets the selected tab. */
	/* packge */void selectTabDirectly(Tab tab, boolean byClient) {
		if (tab == null)
			throw new IllegalArgumentException("null tab");
		if (tab.getTabbox() != this)
			throw new UiException("Not my child: " + tab);
		if (tab != _seltab) {
			if (_seltab != null)
				_seltab.setSelectedDirectly(false);

			try {
				// avoid recursive invoking
				if (getAttribute(TabboxEngine.ATTR_CHANGING_SELECTION) == null) {
					setAttribute(TabboxEngine.ATTR_CHANGING_SELECTION, Boolean.TRUE);
					_seltab = tab;
					_seltab.setSelectedDirectly(true);
					if (byClient && _model != null) {
						Selectable<Object> sm = getSelectableModel();
						if (!sm.isSelected(_model.getElementAt(_seltab.getIndex()))) {
							sm.clearSelection();
							sm.addToSelection(_model.getElementAt(_seltab.getIndex()));
						}
					}
				}
			} finally {
				removeAttribute(TabboxEngine.ATTR_CHANGING_SELECTION);
			}
			if (!byClient)
				smartUpdate("selectedTab", _seltab);
		}
	}

	/**
	 * Returns the orient.
	 *
	 * <p>
	 * Default: "top".
	 *
	 * <p>
	 * Note: only the default mold supports it (not supported if accordion).
	 */
	public String getOrient() {
		return this._orient;
	}

	/**
	 * Sets the mold.
	 *
	 * @param mold default , accordion and accordion-lite
	 *
	 */
	public void setMold(String mold) {
		if (isVertical()) {
			if (mold.startsWith("accordion")) {
				throw new WrongValueException("Unsupported vertical orient in mold : " + mold);
			} else {
				super.setMold(mold);
			}
		} else {
			super.setMold(mold);
		}
	}

	/**
	 * Sets the orient.
	 *
	 * @param orient either "top", "left", "bottom or "right".
	 * @since 7.0.0 "horizontal" is renamed to "top" and "vertical" is renamed to "left".
	 */
	public void setOrient(String orient) throws WrongValueException {
		if (!"horizontal".equals(orient) && !"top".equals(orient) && !"bottom".equals(orient)
				&& !"vertical".equals(orient) && !"right".equals(orient) && !"left".equals(orient))
			throw new WrongValueException("Unknow orient : " + orient);
		if (inAccordionMold())
			throw new WrongValueException("Unsupported vertical orient in mold : " + getMold());
		if (_toolbar != null && ("vertical".equals(orient) || "right".equals(orient) || "left".equals(orient)))
			throw new WrongValueException("Unsupported vertical orient when there is a toolbar");
		if (!Objects.equals(_orient, orient)) {
			if ("horizontal".equals(orient))
				this._orient = "top";
			else if ("vertical".equals(orient))
				this._orient = "left";
			else
				this._orient = orient;
			smartUpdate("orient", _orient);
		}
	}

	/**
	 * Returns whether it is a horizontal tabbox.
	 *
	 * @since 3.0.3
	 */
	public boolean isHorizontal() {
		String orient = getOrient();
		return "horizontal".equals(orient) || "top".equals(orient) || "bottom".equals(orient);
	}

	/**
	 * Returns whether it is the top orientation.
	 * @since 7.0.0
	 */
	public boolean isTop() {
		String orient = getOrient();
		return "horizontal".equals(orient) || "top".equals(orient);
	}

	/**
	 * Returns whether it is the bottom orientation.
	 * @since 7.0.0
	 */
	public boolean isBottom() {
		return "bottom".equals(getOrient());
	}

	/**
	 * Returns whether it is a vertical tabbox.
	 *
	 * @since 3.0.3
	 */
	public boolean isVertical() {
		String orient = getOrient();
		return "vertical".equals(orient) || "right".equals(orient) || "left".equals(orient);
	}

	/**
	 * Returns whether it is the left orientation.
	 *
	 * @since 7.0.0
	 */
	public boolean isLeft() {
		String orient = getOrient();
		return "vertical".equals(orient) || "left".equals(orient);
	}

	/**
	 * Returns whether it is the right orientation.
	 *
	 * @since 7.0.0
	 */
	public boolean isRight() {
		return "right".equals(getOrient());
	}

	public String getZclass() {
		return _zclass == null ? "z-tabbox" : _zclass;
	}

	//ZK-3678: Provide a switch to enable/disable iscroll
	/*package*/ boolean isNativeScrollbar() {
		return Utils.testAttribute(this, "org.zkoss.zul.nativebar", true, true);
	}
	// -- Component --//
	public void beforeChildAdded(Component child, Component refChild) {
		if (child instanceof Toolbar) {
			if (_toolbar != null && _toolbar != child)
				throw new UiException("Only one Toolbar is allowed: " + this);
			if (this.isVertical()) //ZK-4270: meaningful message to the developer indicating incorrect usage
				throw new UiException("Toolbar is allowed only when the tabbox is horizontal." + this);
		} else if (child instanceof Tabs) {
			if (_tabs != null && _tabs != child)
				throw new UiException("Only one tabs is allowed: " + this);
		} else if (child instanceof Tabpanels) {
			if (_tabpanels != null && _tabpanels != child)
				throw new UiException("Only one tabpanels is allowed: " + this);
		} else {
			throw new UiException("Unsupported child for tabbox: " + child);
		}
		super.beforeChildAdded(child, refChild);
	}

	public boolean insertBefore(Component child, Component refChild) {
		if (child instanceof Tabs) {
			if (super.insertBefore(child, refChild)) {
				_tabs = (Tabs) child;
				for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
					final Tab tab = (Tab) it.next();
					if (tab.isSelected()) {
						_seltab = tab;
						break;
					}
				}

				addTabsListeners();
				return true;
			}
		} else if (child instanceof Tabpanels) {
			if (super.insertBefore(child, refChild)) {
				_tabpanels = (Tabpanels) child;
				return true;
			}
		} else if (child instanceof Toolbar) {
			if (super.insertBefore(child, refChild)) {
				_toolbar = (Toolbar) child;
				return true;
			}
		} else {
			return super.insertBefore(child, refChild);
			//impossible but make it more extensible
		}
		return false;
	}

	public void onChildRemoved(Component child) {
		if (_tabs == child) {
			removeTabsListeners();
			_tabs = null;
			_seltab = null;
		} else if (_tabpanels == child) {
			_tabpanels = null;
		} else if (_toolbar == child) {
			_toolbar = null;
		}
		super.onChildRemoved(child);
	}

	/** Removes _listener from all {@link Tab} instances. */
	private void removeTabsListeners() {
		if (_tabs != null) {
			for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
				final Tab tab = (Tab) it.next();
				tab.removeEventListener(Events.ON_SELECT, _listener);
			}
		}
	}

	/** Adds _listener to all {@link Tab} instances. */
	private void addTabsListeners() {
		if (_tabs != null) {
			for (Iterator<Component> it = _tabs.getChildren().iterator(); it.hasNext();) {
				final Tab tab = (Tab) it.next();
				tab.addEventListener(Events.ON_SELECT, _listener);
			}
		}
	}

	protected void clearSelectedTab() {
		_seltab = null;
	}

	// Cloneable//
	public Object clone() {
		final Tabbox clone = (Tabbox) super.clone();

		clone.removeTabsListeners();
		clone.init();

		int cnt = 0;
		if (clone._tabs != null)
			++cnt;
		if (clone._toolbar != null)
			++cnt;
		if (clone._tabpanels != null)
			++cnt;
		if (cnt > 0)
			clone.afterUnmarshal(cnt);

		return clone;
	}

	private void afterUnmarshal(int cnt) {
		for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
			final Object child = it.next();
			if (child instanceof Tabs) {
				_tabs = (Tabs) child;
				for (Iterator<Component> e = _tabs.getChildren().iterator(); e.hasNext();) {
					final Tab tab = (Tab) e.next();
					if (tab.isSelected()) {
						_seltab = tab;
						break;
					}
				}
				if (--cnt == 0)
					break;
			} else if (child instanceof Toolbar) {
				_toolbar = (Toolbar) child;
				if (--cnt == 0)
					break;
			} else if (child instanceof Tabpanels) {
				_tabpanels = (Tabpanels) child;
				if (--cnt == 0)
					break;
			}
		}

		addTabsListeners();
	}

	// -- Serializable --//
	private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
		s.defaultWriteObject();

		willSerialize(_model);
		Serializables.smartWrite(s, _model);
		willSerialize(_renderer);
		Serializables.smartWrite(s, _renderer);
	}

	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();

		_model = (ListModel) s.readObject();
		didDeserialize(_model);
		_renderer = (TabboxRenderer) s.readObject();
		didDeserialize(_renderer);

		init();
		afterUnmarshal(-1);

		if (_model != null) {
			initDataListener();
		}
	}

	private class Listener implements EventListener<Event>, Deferrable {
		public void onEvent(Event event) {
			Events.sendEvent(Tabbox.this, event);
		}

		public boolean isDeferrable() {
			return !Events.isListened(Tabbox.this, Events.ON_SELECT, true);
		}
	}

	protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
		super.renderProperties(renderer);
		if (_panelSpacing != null)
			render(renderer, "panelSpacing", _panelSpacing);
		if (!isTop())
			render(renderer, "orient", _orient);
		if (!_tabscroll)
			renderer.render("tabscroll", _tabscroll);
		if (_maximalHeight) {
			renderer.render("z$rod", false);
			renderer.render("maximalHeight", _maximalHeight);
		}
		//ZK-3678: Provide a switch to enable/disable iscroll
		if (!isNativeScrollbar())
			renderer.render("_nativebar", false);
	}

	public void onPageAttached(Page newpage, Page oldpage) {
		super.onPageAttached(newpage, oldpage);
		if (_model != null) {
			postOnInitRender();
			if (_dataListener != null) {
				_model.removeListDataListener(_dataListener);
				_model.addListDataListener(_dataListener);
			}
		}
	}

	public void onPageDetached(Page page) {
		super.onPageDetached(page);
		if (_model != null && _dataListener != null)
			_model.removeListDataListener(_dataListener);
	}
}
