/* Treeitem.java

	Purpose:

	Description:

	History:
		Wed Jul  6 18:56:15     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.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.OpenEvent;
import org.zkoss.zk.ui.ext.Scope;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.ext.TreeOpenableModel;
import org.zkoss.zul.impl.XulElement;

/**
 * A treeitem.
 *
 * <p>Event:
 * <ol>
 * <li>onOpen is sent when a tree item is opened or closed by user.</li>
 * <li>onDoubleClick is sent when user double-clicks the treeitem.</li>
 * <li>onRightClick is sent when user right-clicks the treeitem.</li>
 * </ol>
 *
 * @author tomyeh
 */
public class Treeitem extends XulElement implements org.zkoss.zk.ui.ext.Disable {
	private transient Treerow _treerow;
	private transient Treechildren _treechildren;
	private Object _value;
	private boolean _open = true;
	private boolean _selected;
	private boolean _disabled;
	private boolean _selectable = true;

	/** whether the content of this item is loaded; used if
	 * the tree owning this item is using a tree model.
	 */
	private boolean _loaded;
	private boolean _rendered;

	static {
		addClientEvent(Treeitem.class, Events.ON_OPEN, CE_IMPORTANT);
	}

	public Treeitem() {
	}

	public Treeitem(String label) {
		setLabel(label);
	}

	public Treeitem(String label, Object value) {
		setLabel(label);
		setValue(value);
	}

	/**
	 * Returns whether it is selectable.
	 * <p>Default: true.</p>
	 * @since 8.0.0
	 */
	public boolean isSelectable() {
		return _selectable;
	}

	/** Sets whether it is selectable.
	 *
	 * <p>If the tree is in a checkmark mode, the selectable state will affect
	 * the checkable icon to display or not.</p>
	 * <p>Default: true.</p>
	 * @param selectable
	 */
	public void setSelectable(boolean selectable) {
		if (_selectable != selectable) {
			_selectable = selectable;

			// non-checkable cannot be selected
			if (!_selectable)
				setSelected(false);
			smartUpdate("selectable", selectable);
		}
	}

	/**
	 * Unload the tree item
	 * <p>To load the tree item, with
	 * {@link Tree#renderItem(Treeitem)}, {@link Tree#renderItem(Treeitem, Object)}, or {@link Tree#renderItems(java.util.Set)}
	 *
	 * @since 3.0.4
	 */
	public void unload() {
		if (isLoaded()) {
			//Clean its children
			if (getTreechildren() != null)
				getTreechildren().getChildren().clear();
			//Set the load status to unloaded
			setLoaded(false);
			//Change the "+/-" sign icon
			setOpen(false);
		}
	}

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

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

	/**
	 * Returns true whether this tree item is rendered. Unlike {@link #isLoaded()}
	 * which is used to check whether all children of this tree item are loaded.
	 * <p>Default: false
	 * @since 7.0.0
	 */
	public boolean isRendered() {
		if (_rendered) return true;
		Tree tree = getTree();
		if (tree != null) {
			return tree.getModel() == null;
		}
		return false;
	}

	// Component developer use only. (since 7.0.0)
	/*package*/ void setRendered(boolean rendered) {
		if (_rendered != rendered) {
			_rendered = rendered;
			smartUpdate("_loaded", _rendered);
		}
	}

	/**
	 * Return true whether all children of this tree item, if any, is loaded
	 * @return true whether all children of this tree item is loaded
	 * @since 3.0.0
	 */
	public boolean isLoaded() {
		return _loaded;
	}

	/**
	 * Sets whether all children of this tree item, if any, is loaded.
	 * @since 3.0.0
	 */
	/*package*/ void setLoaded(boolean loaded) {
		if (_loaded != loaded) {
			_loaded = loaded;

			smartUpdate("_loadedChildren", _loaded);
		}
	}

	/**
	 * Please use {@link Treecell} or {@link Tree} instead.
	 * @deprecated as of release 7.0.3.
	 */
	public void setStubonly(String stubonly) {
		// Don't remove this method, it's to override super.setStubonly().
		super.setStubonly(stubonly);
	}

	/**
	 * Please use {@link Treecell} or {@link Tree} instead.
	 * @deprecated as of release 7.0.3.
	 */
	public void setStubonly(boolean stubonly) {
		// Don't remove this method, it's to override super.setStubonly().
		super.setStubonly(stubonly);
	}

	/**
	 * return the index of this item
	 * @return the index of this item
	 * @since 5.0.9
	 */
	public int getIndex() {
		List list = this.getParent().getChildren();
		return list.indexOf(this);
	}

	/** Returns the treerow that this tree item owns (might null).
	 * Each tree items has exactly one tree row.
	 */
	public Treerow getTreerow() {
		return _treerow;
	}

	/** Returns the treechildren that this tree item owns, or null if
	 * doesn't have any child.
	 */
	public Treechildren getTreechildren() {
		return _treechildren;
	}

	/** Returns whether the element is to act as a container
	 * which can have child elements.
	 */
	public boolean isContainer() {
		return _treechildren != null;
	}

	/** Returns whether this element contains no child elements.
	 */
	public boolean isEmpty() {
		return _treechildren == null || _treechildren.getChildren().isEmpty();
	}

	/** Returns the value. It could be anything you want.
	 * <p>Default: null.
	 * <p>Note: the value is not sent to the browser, so it is OK to be
	 * anything.
	 */
	@SuppressWarnings("unchecked")
	public <T> T getValue() {
		return (T) _value;
	}

	/** Sets the value.
	 * @param value the value.
	 * Note: the value is not sent to the browser, so it is OK to be
	 * anything.
	 */
	public <T> void setValue(T value) {
		_value = value;
	}

	/** Returns whether this container is open.
	 * <p>Default: true.
	 */
	public boolean isOpen() {
		return _open;
	}

	/** Sets whether this container is open.
	 */
	public void setOpen(boolean open) {
		if (_open != open) {
			_open = open;
			//Note: _treerow might not be ready yet because it might be
			//initialized before creating child components (for ZK pages)
			smartUpdate("open", _open);
			//If the item is open, its tree has model and not rendered, render the item
			if (_treechildren != null)
				addVisibleItemCount((_open ? 1 : -1) * _treechildren.getVisibleItemCount());

			Tree tree = getTree();
			if (tree != null && tree.getModel() != null) {
				if (_open && !isLoaded())
					tree.renderItem(this);
			}
		}
	}

	/** Returns whether this item is selected.
	 */
	public boolean isSelected() {
		return _selected;
	}

	/** Returns whether this item is selected.
	 */
	public void setSelected(boolean selected) {
		if (_selected != selected) {
			final Tree tree = getTree();
			if (tree != null) {
				//Note: we don't update it here but let its parent does the job
				tree.toggleItemSelection(this);
			} else {
				_selected = selected;
			}
		}
	}

	/** Updates _selected directly and invalidate getTreerow if necessary.
	 */
	/*package*/ void setSelectedDirectly(boolean selected) {
		_selected = selected;
	}

	/** Returns the level this cell is. The root is level 0.
	 */
	public int getLevel() {
		int level = 0;
		for (Component item = this;; ++level) {
			final Component tch = item.getParent();
			if (tch == null)
				break;

			item = tch.getParent();
			if (item == null || item instanceof Tree)
				break;
		}
		return level;
	}

	/** Returns the label of the {@link Treecell} it contains, or null
	 * if no such cell.
	 */
	public String getLabel() {
		return _treerow != null ? _treerow.getLabel() : null;
	}

	/** Sets the label of the {@link Treecell} it contains.
	 *
	 * <p>If treerow and treecell are not created, we automatically create it.
	 *
	 * <p>Notice that this method will create a treerow and treecell automatically
	 * if they don't exist. Thus, you cannot attach a treerow to it again if
	 * set an image or a label.
	 */
	public void setLabel(String label) {
		autoTreerow().setLabel(label);
	}

	private Treerow autoTreerow() {
		if (_treerow == null) {
			final Treerow row = new Treerow();
			row.applyProperties();
			row.setParent(this);
		}
		return _treerow;
	}

	/** Returns the image of the {@link Treecell} it contains.
	 */
	public String getImage() {
		return _treerow != null ? _treerow.getImage() : null;
	}

	/** Sets the image of the {@link Treecell} it contains.
	 *
	 * <p>If treerow and treecell are not created, we automatically create it.
	 *
	 * <p>Notice that this method will create a treerow and treecell automatically
	 * if they don't exist. Thus, you cannot attach a treerow to it again if
	 * set an image or a label.
	 */
	public void setImage(String image) {
		autoTreerow().setImage(image);
	}

	/** Returns the parent tree item,
	 * or null if this item is already the top level of the tree.
	 * The parent tree item is actually the grandparent if any.
	 *
	 * @since 3.0.0
	 */
	public Treeitem getParentItem() {
		final Component p = getParent();
		final Component gp = p != null ? p.getParent() : null;
		return gp instanceof Treeitem ? (Treeitem) gp : null;
	}

	/** Returns the tree owning this item.
	 */
	public Tree getTree() {
		for (Component p = this; (p = p.getParent()) != null;)
			if (p instanceof Tree)
				return (Tree) p;
		return null;
	}

	/*package*/ boolean isRealVisible() {
		if (!isVisible())
			return false;
		Component comp = getParent();
		return comp == null
				|| ((comp instanceof Treechildren) ? ((Treechildren) comp).isRealVisible() : comp.isVisible());
	}

	//-- super --//
	public boolean setVisible(boolean visible) {
		if (isVisible() != visible) {
			smartUpdate("visible", visible);
			int count = isOpen() && _treechildren != null ? _treechildren.getVisibleItemCount() + 1 : 1;
			if (visible) {
				boolean result = super.setVisible(visible);
				addVisibleItemCount(count);
				return result;
			} else {
				addVisibleItemCount(-count);
				return super.setVisible(visible);
			}
		}
		return visible;
	}

	/**
	 * Returns the number of visible descendant {@link Treechildren}.
	 * Descendants include direct children, grand children and so on.
	 *
	 * @since 3.6.1
	 */
	public int getVisibleItemCount() {
		return isVisible() ? 1 + (_open && _treechildren != null ? _treechildren.getVisibleItemCount() : 0) : 0;
	}

	/**
	 * adds the number of the visible item to the count of its parent.
	 * @param count
	 * @since 3.0.7
	 */
	void addVisibleItemCount(int count) {
		Treechildren tc = (Treechildren) getParent();
		if (tc != null && super.isVisible())
			tc.addVisibleItemCount(count);
	}

	/**
	 * @deprecated as of release 6.0.0. To control the size of Tree related
	 * components, please refer to {@link Tree} and {@link Treecol} instead.
	 */
	public void setWidth(String width) {
		// Don't remove this method, it's to override super.setWidth().
	}

	/**
	 * @deprecated as of release 6.0.0. To control the size of Tree related
	 * components, please refer to {@link Tree} and {@link Treecol} instead.
	 */
	public void setHflex(String flex) {
		// Don't remove this method, it's to override super.setHflex().
	}

	//-- Component --//
	public void beforeParentChanged(Component parent) {
		if (parent != null && !(parent instanceof Treechildren))
			throw new UiException("Wrong parent: " + parent);
		super.beforeParentChanged(parent);
	}

	public void setParent(Component parent) {
		final Component oldp = getParent();
		if (oldp == parent)
			return; //nothing changed

		final Tree oldtree = oldp != null ? getTree() : null;
		super.setParent(parent);

		//maintain the selected status
		if (oldtree != null)
			oldtree.onTreeitemRemoved(this);
		if (parent != null) {
			final Tree tree = getTree();
			if (tree != null)
				tree.onTreeitemAdded(this);
		}
	}

	public void beforeChildAdded(Component child, Component refChild) {
		if (child instanceof Treerow) {
			if (_treerow != null && _treerow != child)
				throw new UiException("Only one treerow is allowed: " + this);
		} else if (child instanceof Treechildren) {
			if (_treechildren != null && _treechildren != child)
				throw new UiException("Only one treechildren is allowed: " + this);
		} else {
			throw new UiException("Unsupported child for tree item: " + child);
		}
		super.beforeChildAdded(child, refChild);
	}

	public boolean insertBefore(Component child, Component refChild) {
		if (child instanceof Treerow) {
			if (super.insertBefore(child, refChild)) {
				_treerow = (Treerow) child;
				return true;
			}
		} else if (child instanceof Treechildren) {
			if (super.insertBefore(child, refChild)) {
				_treechildren = (Treechildren) child;
				return true;
			}
		} else {
			return super.insertBefore(child, refChild);
			//impossible but more extensible
		}
		return false;
	}

	public void onChildAdded(Component child) {
		super.onChildAdded(child);
		if (_treechildren == child)
			addVisibleItemCount(_treechildren.getVisibleItemCount());
	}

	public void onChildRemoved(Component child) {
		if (child instanceof Treerow) {
			_treerow = null;
		} else if (child instanceof Treechildren) {
			if (isOpen())
				addVisibleItemCount(-_treechildren.getVisibleItemCount());
			_treechildren = null;
		}
		super.onChildRemoved(child);
	}

	// Returns whether the treeitem should be visited.
	private static boolean shallVisitTree(Tree tree, Component child) {
		final Treeitem item = (Treeitem) child;
		int count = item.isOpen() && item.getTreechildren() != null ? item.getTreechildren().getVisibleItemCount() : 0;
		Integer visited = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_COUNT);
		final Paginal pgi = tree.getPaginal();
		final int ofs = pgi.getActivePage() * pgi.getPageSize();
		int visit = visited != null ? visited.intValue() + 1 : 1;
		boolean shoulbBeVisited = ofs < visit + count;
		if (visited == null)
			visited = new Integer(shoulbBeVisited ? 1 : count + 1);
		else
			visited = new Integer(visited.intValue() + (shoulbBeVisited ? 1 : count + 1));

		Integer total = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_TOTAL);
		if (total == null)
			total = new Integer(count + 1);
		else
			total = new Integer(total.intValue() + count + 1);
		tree.setAttribute(Attributes.VISITED_ITEM_COUNT, visited);
		tree.setAttribute(Attributes.VISITED_ITEM_TOTAL, total);
		return shoulbBeVisited;
	}

	// Returns whether the specified should be rendered.
	private static boolean shallRenderTree(Tree tree) {
		Integer visited = (Integer) tree.getAttribute(Attributes.VISITED_ITEM_COUNT);
		final Paginal pgi = tree.getPaginal();
		final int ofs = pgi.getActivePage() * pgi.getPageSize();
		if (ofs < visited.intValue()) {
			// count the rendered item
			Integer renderedCount = (Integer) tree.getAttribute(Attributes.RENDERED_ITEM_COUNT);
			if (renderedCount == null)
				renderedCount = new Integer(1);
			else
				renderedCount = new Integer(renderedCount.intValue() + 1);
			tree.setAttribute(Attributes.RENDERED_ITEM_COUNT, renderedCount);
			return true;
		}
		return false;
	}

	protected void redrawChildren(Writer out) throws IOException {
		Tree tree = getTree();
		if (!tree.inPagingMold()) {
			super.redrawChildren(out);
		} else if (isRealVisible() && shallVisitTree(tree, this)) {
			if (shallRenderTree(tree)) {
				ComponentCtrl child = getTreerow();
				if (child != null)
					child.redraw(out);
			}
			boolean close = !isOpen();
			ComponentCtrl child = getTreechildren();
			if (child != null) {
				if (close)
					((Component) child).setAttribute(Attributes.SHALL_RENDER_ITEM, Boolean.TRUE);
				child.redraw(out);
				if (close)
					((Component) child).removeAttribute(Attributes.SHALL_RENDER_ITEM);
			}
		}
	}

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

		int cnt = 0;
		if (clone._treerow != null)
			++cnt;
		if (clone._treechildren != null)
			++cnt;
		if (cnt > 0)
			clone.afterUnmarshal(cnt);

		return clone;
	}

	private void afterUnmarshal(int cnt) {
		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			final Object child = it.next();
			if (child instanceof Treerow) {
				_treerow = (Treerow) child;
				if (--cnt == 0)
					break;
			} else if (child instanceof Treechildren) {
				_treechildren = (Treechildren) child;
				if (--cnt == 0)
					break;
			}
		}
	}

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

		afterUnmarshal(-1);
	}

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

		render(renderer, "selected", isSelected());
		render(renderer, "disabled", isDisabled());
		if (!isOpen())
			renderer.render("open", false);
		if (!isSelectable())
			renderer.render("checkable", false);
		render(renderer, "_loadedChildren", isLoaded());
		render(renderer, "_loaded", isRendered());

		if (_value instanceof String)
			render(renderer, "value", _value);
	}

	//--ComponentCtrl--//
	private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(2);

	static {
		_properties.put("_loadedChildren", new BooleanPropertyAccess() {
			public void setValue(Component cmp, Boolean loadedChildren) {
				((Treeitem) cmp).setLoaded(loadedChildren != null && loadedChildren);
			}

			public Boolean getValue(Component cmp) {
				return ((Treeitem) cmp).isLoaded();
			}
		});
		_properties.put("_loaded", new BooleanPropertyAccess() {
			public void setValue(Component cmp, Boolean rendered) {
				((Treeitem) cmp).setRendered(rendered != null && rendered);
			}

			public Boolean getValue(Component cmp) {
				return ((Treeitem) cmp).isRendered();
			}
		});
	}

	public PropertyAccess getPropertyAccess(String prop) {
		PropertyAccess pa = _properties.get(prop);
		if (pa != null)
			return pa;
		return super.getPropertyAccess(prop);
	}

	/** Processes an AU request.
	 *
	 * <p>Default: in addition to what are handled by {@link XulElement#service(Event, Scope)},
	 * it also handles onOpen.
	 * @since 5.0.0
	 */
	public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
		final String cmd = request.getCommand();
		if (cmd.equals(Events.ON_OPEN)) {
			OpenEvent evt = OpenEvent.getOpenEvent(request);

			final boolean open = evt.isOpen();

			final Tree tree = getTree();
			boolean hasOpenableModel = false;
			if (tree != null && tree.getModel() != null) {
				if (open && !isLoaded()) {
					tree.renderItem(Treeitem.this);

					// better client side performance with invalidate
					if (_treechildren != null && _treechildren.getChildren().size() >= 5)
						invalidate();
				}
				@SuppressWarnings("rawtypes")
				TreeModel model = tree.getModel();
				if (model instanceof TreeOpenableModel) {
					if (open)
						hasOpenableModel = ((TreeOpenableModel) model).addOpenPath(tree.getTreeitemPath(tree, this));
					else
						hasOpenableModel = ((TreeOpenableModel) model).removeOpenPath(tree.getTreeitemPath(tree, this));
				}
			}

			if (!hasOpenableModel && _treechildren != null && super.isVisible()) {
				if (open)
					addVisibleItemCount(_treechildren.getVisibleItemCount());
				else {
					addVisibleItemCount(-_treechildren.getVisibleItemCount());
				}
			}

			// Bug #3170417 the status should update after update the visibleItemCount
			_open = open;

			if (tree != null && tree.inPagingMold()) {
				//bug ZK-2375 clear the closed children and fire a paging event to make sure
				//everything on the current active page will be rendered correctly
				@SuppressWarnings("rawtypes")
				TreeModel model = tree.getModel();
				if (!open && model != null && WebApps.getFeature("ee")) {
					int activePage = tree.getActivePage();
					getChildren().clear();
					setRendered(false);
					setLoaded(false);
					Events.postEvent(new PagingEvent("onPagingImpl", (Paging) tree.getPaginal(), activePage));
				}
				// Bug #2838782
				tree.focus();
			}

			Events.postEvent(evt);
		} else
			super.service(request, everError);
	}
}
