/* Selectbox.java

	Purpose:
		
	Description:
		
	History:
		Fri Sep 30 10:53:25 TST 2011, Created by jumperchen

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

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

import java.util.LinkedHashSet;
import java.util.Set;

import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.ext.Blockable;
import org.zkoss.zk.ui.ext.Disable;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zk.ui.util.ForEachStatus;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.ext.Selectable;

/**
 * A light weight dropdown list.
 * <p>
 * Default {@link #getZclass}: z-selectbox. It does not create child widgets for
 * each data, so the memory usage is much lower at the server. However, the
 * implementation is based on HTML SELECT and OPTION tags, so the functionality
 * is not as rich as {@link Listbox}.
 * 
 * @author jumperchen
 * @since 6.0.0
 */
@SuppressWarnings("serial")
public class Selectbox extends HtmlBasedComponent implements Disable {

	private String _name;
	private boolean _disabled;
	private int _jsel = -1;
	private transient ListModel<?> _model;
	private transient ListDataListener _dataListener;
	private transient ItemRenderer<?> _renderer;
	private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.onInitLaterPosted";

	private transient boolean _childable;
	private transient String[] _tmpdatas;

	static {
		addClientEvent(Selectbox.class, Events.ON_SELECT, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
		addClientEvent(Selectbox.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
		addClientEvent(Selectbox.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
	}

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

	/**
	 * Returns the index of the selected item (-1 if no one is selected).
	 */
	public int getSelectedIndex() {
		return _jsel;
	}

	/**
	 * Selects the item with the given index.
	 */
	public void setSelectedIndex(int jsel) {
		if (jsel < -1)
			jsel = -1;
		if (jsel != _jsel) {
			_jsel = jsel;
			smartUpdate("selectedIndex", jsel);
		}
	}

	/**
	 * Returns the renderer to render each item, or null if the default renderer
	 * is used.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public <T> ItemRenderer<T> getItemRenderer() {
		return (ItemRenderer) _renderer;
	}

	/**
	 * Sets the renderer which is used to render each item if {@link #getModel}
	 * is not null.
	 * 
	 * <p>
	 * Note: changing a render will not cause the selectbox 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
	 */
	public void setItemRenderer(ItemRenderer<?> renderer) {
		if (_renderer != renderer) {
			_renderer = renderer;
			postOnInitRender(); // Bug ZK-2607
			invalidate();
		}
	}

	/**
	 * Sets the renderer by use of a class name. It creates an instance
	 * automatically.
	 */
	@SuppressWarnings("rawtypes")
	public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
			IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
		if (clsnm != null)
			setItemRenderer((ItemRenderer) Classes.newInstanceByThread(clsnm));
	}

	/**
	 * Returns whether it is disabled.
	 * <p>
	 * Default: false.
	 */
	public boolean isDisabled() {
		return _disabled;
	}

	protected boolean isChildable() {
		return _childable;
	}

	/**
	 * Sets whether it is disabled.
	 */
	public void setDisabled(boolean disabled) {
		if (_disabled != disabled) {
			_disabled = disabled;
			smartUpdate("disabled", _disabled);
		}
	}

	/**
	 * Returns the name of this component.
	 * <p>
	 * Default: null.
	 * <p>
	 * The name is used only to work with "legacy" Web application that handles
	 * user's request by servlets. It works only with HTTP/HTML-based browsers.
	 * It doesn't work with other kind of clients.
	 * <p>
	 * Don't use this method if your application is purely based on ZK's
	 * event-driven model.
	 */
	public String getName() {
		return _name;
	}

	/**
	 * Sets the name of this component.
	 * <p>
	 * The name is used only to work with "legacy" Web application that handles
	 * user's request by servlets. It works only with HTTP/HTML-based browsers.
	 * It doesn't work with other kind of clients.
	 * <p>
	 * Don't use this method if your application is purely based on ZK's
	 * event-driven model.
	 * 
	 * @param name
	 *            the name of this component.
	 */
	public void setName(String name) {
		if (name != null && name.length() == 0)
			name = null;
		if (!Objects.equals(_name, name)) {
			_name = name;
			smartUpdate("name", name);
		}
	}

	private void initDataListener() {
		if (_dataListener == null)
			_dataListener = new ListDataListener() {
				public void onChange(ListDataEvent event) {
					switch (event.getType()) {
					case ListDataEvent.SELECTION_CHANGED:
						doSelectionChanged();
						return; //nothing changed so need to rerender
					case ListDataEvent.MULTIPLE_CHANGED:
						return; //nothing to do
					}
					postOnInitRender();
				}
			};
		_model.addListDataListener(_dataListener);
	}

	private void doSelectionChanged() {
		final Selectable<Object> smodel = getSelectableModel();
		if (smodel.isSelectionEmpty()) {
			if (_jsel >= 0)
				setSelectedIndex(-1);
			return;
		}

		if (_jsel >= 0 && smodel.isSelected(_model.getElementAt(_jsel)))
			return; //nothing changed

		for (int i = 0, sz = _model.getSize(); i < sz; i++) {
			if (smodel.isSelected(_model.getElementAt(i))) {
				setSelectedIndex(i);
				return; //done
			}
		}
		setSelectedIndex(-1); //just in case
	}

	@SuppressWarnings("unchecked")
	private Selectable<Object> getSelectableModel() {
		return (Selectable<Object>) _model;
	}

	/**
	 * Sets the list model associated with this selectbox. If a non-null model
	 * is assigned, no matter whether it is the same as the previous, it will
	 * always cause re-render.
	 * 
	 * @param model
	 *            the list model to associate, or null to dissociate any
	 *            previous model.
	 * @exception UiException
	 *                if failed to initialize with the model
	 */
	public void setModel(ListModel<?> model) {
		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;
				_jsel = -1; //Bug ZK-1418: clear select index since model is changed.
				initDataListener();
				postOnInitRender();
			}
		} else if (_model != null) {
			_model.removeListDataListener(_dataListener);
			_model = null;
			invalidate();
		}
	}

	public void onInitRender() {
		removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
		onInitRenderNow();
		invalidate();
	}

	public void onInitRenderNow() {
		if (_model != null) {
			_tmpdatas = new String[_model.getSize()];
			final boolean old = _childable;
			try {
				_childable = true;
				final ItemRenderer<Object> renderer = getRealRenderer();
				final Selectable<Object> smodel = getSelectableModel();
				_jsel = -1;
				for (int i = 0, sz = _model.getSize(); i < sz; i++) {
					final Object value = _model.getElementAt(i);
					if (_jsel < 0 && smodel.isSelected(value))
						_jsel = i;
					_tmpdatas[i] = renderer.render(this, value, i);
				}
			} catch (Exception e) {
				throw UiException.Aide.wrap(e);
			} finally {
				//clear possible children created in renderer
				_childable = old;
				getChildren().clear();
			}
		}
	}

	private void postOnInitRender() {
		if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
			setAttribute(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", "rawtypes" })
	public <T> ListModel<T> getModel() {
		return (ListModel) _model;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public <T> ItemRenderer<T> getRealRenderer() {
		final ItemRenderer renderer = getItemRenderer();
		return renderer != null ? renderer : (ItemRenderer<T>) _defRend;
	}

	private static final ItemRenderer<Object> _defRend = new ItemRenderer<Object>() {
		public String render(final Component owner, final Object data, final int index) {
			final Selectbox self = (Selectbox) owner;
			final Template tm = self.getTemplate("model");
			if (tm == null)
				return Objects.toString(data);
			else {
				final Component[] items = ShadowElementsCtrl
						.filterOutShadows(tm.create(owner, null, new VariableResolver() {
					public Object resolveVariable(String name) {
						if ("each".equals(name)) {
							return data;
						} else if ("forEachStatus".equals(name)) {
							return new ForEachStatus() {

								public ForEachStatus getPrevious() {
									return null;
								}

								public Object getEach() {
									return getCurrent();
								}

								public int getIndex() {
									return index;
								}

								public Integer getBegin() {
									return 0;
								}

								public Integer getEnd() {
									return ((Selectbox) owner).getModel().getSize();
								}

								public Object getCurrent() {
									return data;
								}

								public boolean isFirst() {
									return getCount() == 1;
								}

								public boolean isLast() {
									return getIndex() + 1 == getEnd();
								}

								public Integer getStep() {
									return null;
								}

								public int getCount() {
									return getIndex() + 1;
								}
							};
						} else {
							return null;
						}
					}
				}, null));
				if (items.length != 1)
					throw new UiException("The model template must have exactly one item, not " + items.length);
				if (!(items[0] instanceof Label))
					throw new UiException("The model template can only support Label component, not " + items[0]);
				items[0].detach(); //remove the label from owner
				return ((Label) items[0]).getValue();
			}
		}
	};

	// -- ComponentCtrl --//
	public void invalidate() {
		// post onInitRender to rerender content if not done it before
		prepareDatas();

		super.invalidate();
	}

	// ZK-948 need render data when change parent or attach to page
	public void setParent(Component parent) {
		super.setParent(parent);
		if (parent != null) {
			prepareDatas();
		}
	}

	// ZK-948 
	public void onPageAttached(Page newpage, Page oldpage) {
		super.onPageAttached(newpage, oldpage);
		prepareDatas();
		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);
	}

	private void prepareDatas() {
		if (_tmpdatas == null && _model != null && _model.getSize() > 0) {
			// post onInitRender to rerender content
			postOnInitRender();
		}
	}

	protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
		super.renderProperties(renderer);

		render(renderer, "name", _name);
		render(renderer, "disabled", isDisabled());
		renderer.render("selectedIndex", _jsel);

		if (_tmpdatas != null) {
			render(renderer, "items", _tmpdatas);
			// B65-ZK-2379 _tmpdatas = null; //purge the data
		}
	}

	@SuppressWarnings("unchecked")
	public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
		final String cmd = request.getCommand();
		if (cmd.equals(Events.ON_SELECT)) {
			// ZK-2148: should check if model exists
			final Object prevSelected = _jsel >= 0 && _model != null ? _model.getElementAt(_jsel) : null;
			_jsel = ((Integer) request.getData().get("")).intValue();
			final Integer index = ((Integer) request.getData().get(""));
			final Set<Object> selObjs = new LinkedHashSet<Object>();

			// ZK-2148: should check if model exists
			if (_model != null) {
				if (index >= 0)
					selObjs.add(_model.getElementAt(index));
				getSelectableModel().setSelection(selObjs);
			}

			if (prevSelected != null) {
				final Set<Object> prevSeldObjs = new LinkedHashSet<Object>(1);
				prevSeldObjs.add(prevSelected);

				Events.postEvent(new SelectEvent(Events.ON_SELECT, this, null, null, null, selObjs, prevSeldObjs,
						prevSeldObjs, null, index, 0));
			} else {
				Events.postEvent(
						new SelectEvent(Events.ON_SELECT, this, null, null, null, selObjs, null, null, null, index, 0));
			}
		} else // ZK-1053
			super.service(request, everError);
	}

	// Cloneable//
	@SuppressWarnings("rawtypes")
	public Object clone() {
		final Selectbox clone = (Selectbox) super.clone();
		if (clone._model != null) {
			if (clone._model instanceof ComponentCloneListener) {
				final ListModel model = ((ListModel) ((ComponentCloneListener) clone._model).willClone(clone));
				if (model != null)
					clone._model = model;
			}
			clone.postOnInitRender();
			// we use the same data model but we have to create a new listener
			clone._dataListener = null;
			clone.initDataListener();
		}
		return clone;
	}

	// -- Serializable --//
	// NOTE: they must be declared as private
	private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
		s.defaultWriteObject();

		willSerialize(_model);
		s.writeObject(
				_model instanceof java.io.Serializable || _model instanceof java.io.Externalizable ? _model : null);
		willSerialize(_renderer);
		s.writeObject(_renderer instanceof java.io.Serializable || _renderer instanceof java.io.Externalizable
				? _renderer : null);
	}

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

		_model = (ListModel) s.readObject();
		didDeserialize(_model);
		_renderer = (ItemRenderer) s.readObject();
		didDeserialize(_renderer);
		if (_model != null) {
			initDataListener();
		}
	}

	public void sessionWillPassivate(Page page) {
		super.sessionWillPassivate(page);
		willPassivate(_model);
		willPassivate(_renderer);
	}

	public void sessionDidActivate(Page page) {
		super.sessionDidActivate(page);
		didActivate(_model);
		didActivate(_renderer);
		if (_model != null)
			postOnInitRender();
	}

	public Object getExtraCtrl() {
		return new ExtraCtrl();
	}

	/** A utility class to implement {@link #getExtraCtrl}.
	 * It is used only by component developers.
	 *
	 * <p>If a component requires more client controls, it is suggested to
	 * override {@link #getExtraCtrl} to return an instance that extends from
	 * this class.
	 */
	protected class ExtraCtrl extends HtmlBasedComponent.ExtraCtrl implements Blockable {
		public boolean shallBlock(AuRequest request) {
			return isDisabled() || !Components.isRealVisible(Selectbox.this);
		}
	}
}
