/* ListboxDataLoader.java
{{IS_NOTE
	Purpose:

	Description:

	History:
		Nov 23, 2009 2:53:30 PM, Created by henrichen
}}IS_NOTE

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

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

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

import org.zkoss.lang.Objects;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.DeferredValue;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.ForEachStatus;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.Attributes;
import org.zkoss.zul.Frozen;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listfoot;
import org.zkoss.zul.Listgroup;
import org.zkoss.zul.ListgroupRendererExt;
import org.zkoss.zul.Listgroupfoot;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.ListitemRendererExt;
import org.zkoss.zul.Paging;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.GroupingInfo;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.impl.GroupsListModel.GroupDataInfo;

/**
 * Generic {@link Listbox} data loader.
 * @author henrichen
 * @since 5.0.0
 */
public class ListboxDataLoader implements DataLoader, Cropper { //no need to serialize since Listbox assumes it
	private Listbox _listbox;

	//--DataLoader--//
	public void init(Component owner, int offset, int limit) {
		_listbox = (Listbox) owner;
	}

	public void reset() {
		//do nothing
	}

	public final Component getOwner() {
		return _listbox;
	}

	public int getOffset() {
		return 0;
	}

	public int getLimit() {
		return _listbox.getRows() > 0 ? _listbox.getRows() + 5 : 50;
	}

	public int getTotalSize() {
		final ListModel model = _listbox.getModel();
		return model != null ? model.getSize() : _listbox.getVisibleItemCount();
	}

	private int INVALIDATE_THRESHOLD = -1;
	private Boolean SELECTIVE_COMPONENT_UPDATE = null; // since 10.2.0

	/**
	 * updates the status of the changed group.
	 * @param event
	 * @since 8.0.4
	 */
	public void doGroupsDataChange(GroupsDataEvent event) {
		if (event.getType() == GroupsDataEvent.GROUPS_OPENED) {
			GroupsListModel groupsListModel = ((GroupsListModel) _listbox.getModel());
			int groupIndex = event.getGroupIndex();
			int offset = groupsListModel.getGroupOffset(groupIndex);
			((Listgroup) _listbox.getItems().get(offset - getOffset())).setOpen(event.getModel().isGroupOpened(groupIndex));
		}
	}

	public void doListDataChange(ListDataEvent event) {
		if (INVALIDATE_THRESHOLD == -1) {
			INVALIDATE_THRESHOLD = Utils.getIntAttribute(this.getOwner(), "org.zkoss.zul.invalidateThreshold", 10,
					true);
		}

		// ZK-5504
		if (SELECTIVE_COMPONENT_UPDATE == null) {
			SELECTIVE_COMPONENT_UPDATE = Utils.testAttribute(this.getOwner(),
					Attributes.SELECTIVE_COMPONENT_UPDATE, false,
					true);
		}
		//when this is called _model is never null
		final ListModel _model = _listbox.getModel();
		final int newsz = _model.getSize(), oldsz = _listbox.getItemCount();
		int min = event.getIndex0(), max = event.getIndex1(), cnt;

		switch (event.getType()) {
		case ListDataEvent.INTERVAL_ADDED:
			cnt = newsz - oldsz;
			if (cnt < 0)
				throw new UiException("Adding causes a smaller list?");
			if (cnt == 0) //no change, nothing to do here
				return;
			boolean isInvalidated = false;
			if ((oldsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !inPagingMold()) {
				invalidateListitems();
				isInvalidated = true;
			}
			if (min < 0)
				if (max < 0)
					min = 0;
				else
					min = max - cnt + 1;
			if (min > oldsz)
				min = oldsz;

			ListitemRenderer renderer = null;
			final Component next = min < oldsz ? _listbox.getItemAtIndex(min) : null;
			while (--cnt >= 0) {
				if (renderer == null)
					renderer = (ListitemRenderer) getRealRenderer();
				_listbox.insertBefore(newUnloadedItem(renderer, min++), next);
			}

			// Fix ZK-5468: the content of the subsequence item might be changed
			if (!SELECTIVE_COMPONENT_UPDATE && !isInvalidated && !_listbox.isInvalidated()) {
				syncModel(max, _listbox.getItemCount() - (max - min));
			}
			break;

		case ListDataEvent.INTERVAL_REMOVED:
			cnt = oldsz - newsz;
			if (cnt < 0)
				throw new UiException("Removal causes a larger list?");
			if (cnt == 0) //no change, nothing to do here
				return;
			if (min >= 0)
				max = min + cnt - 1;
			else if (max < 0)
				max = cnt - 1; //0 ~ cnt - 1
			if (max > oldsz - 1)
				max = oldsz - 1;

			boolean isInvalidated0 = false;
			if ((newsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !inPagingMold()) {
				_listbox.shallUpdateScrollPos(true);
				invalidateListitems();
				isInvalidated0 = true;
			}

			//detach from end (due to groupfoot issue)
			Component comp = _listbox.getItemAtIndex(max);
			while (--cnt >= 0) {
				Component p = comp.getPreviousSibling();
				comp.detach();
				comp = p;
			}

			// Fix ZK-5468: the content of the subsequence item might be changed
			if (!SELECTIVE_COMPONENT_UPDATE && !isInvalidated0 && !_listbox.isInvalidated()) {
				syncModel(max, _listbox.getItemCount() - (max - min));
			}
			break;

		default: //CONTENTS_CHANGED
			syncModel(min, max < 0 ? -1 : (max - min + 1));
			//TonyQ: B50-ZK-897 , listfoot disappear after clicking run button ,
			// 		   		sync logic with GridDataLoader
		}
	}

	private void invalidateListitems() {
		//Bug 3147518: avoid memory leak
		//Also better performance (outer better than remove a lot)
		final Execution execution = Executions.getCurrent();
		final String uuid = _listbox.getUuid();
		final boolean isDeferInitModel = execution != null && execution.getAttribute("zkoss.Listbox.deferInitModel_" + uuid) != null;
		if (isDeferInitModel) return; // skip while defer loading a model

		final Page page = _listbox.getPage();
		if (execution != null && execution.isAsyncUpdate(page)) {
			final UiEngine engine = page != null
				? ((WebAppCtrl) page.getDesktop().getWebApp()).getUiEngine()
				: null;
			if (engine != null) {
				engine.addSmartUpdate(_listbox, "itemsInvalid_", new DeferredRedraw(_listbox.getItems()), 10000);
				execution.setAttribute("zkoss.Listbox.invalidateListitems" + uuid, Boolean.TRUE);
			}
		}
	}

	/** Creates an new and unloaded listitem. */
	protected final Listitem newUnloadedItem(ListitemRenderer renderer, int index) {
		final ListModel model = _listbox.getModel();
		Listitem item = null;
		if (model instanceof GroupsListModel) {
			final GroupsListModel gmodel = (GroupsListModel) model;
			final GroupingInfo info = gmodel.getDataInfo(index);
			switch (info.getType()) {
			case GroupDataInfo.GROUP:
				item = newListgroup(renderer);
				((Listgroup) item).setOpen(info.isOpen());
				break;
			case GroupDataInfo.GROUPFOOT:
				item = newListgroupfoot(renderer);
				break;
			default:
				item = newListitem(renderer);
			}
		} else {
			item = newListitem(renderer);
		}
		((LoadStatus) item.getExtraCtrl()).setLoaded(false);
		((LoadStatus) item.getExtraCtrl()).setIndex(index);

		newUnloadedCell(renderer, item);
		return item;
	}

	private Listitem newListitem(ListitemRenderer renderer) {
		Listitem item = null;
		if (renderer instanceof ListitemRendererExt)
			item = ((ListitemRendererExt) renderer).newListitem(_listbox);
		if (item == null) {
			item = new Listitem();
			item.applyProperties();
		}
		return item;
	}

	private Listgroup newListgroup(ListitemRenderer renderer) {
		Listgroup group = null;
		if (renderer instanceof ListgroupRendererExt)
			group = ((ListgroupRendererExt) renderer).newListgroup(_listbox);
		if (group == null) {
			group = new Listgroup();
			group.applyProperties();
		}
		return group;
	}

	private Listgroupfoot newListgroupfoot(ListitemRenderer renderer) {
		Listgroupfoot groupfoot = null;
		if (renderer instanceof ListgroupRendererExt)
			groupfoot = ((ListgroupRendererExt) renderer).newListgroupfoot(_listbox);
		if (groupfoot == null) {
			groupfoot = new Listgroupfoot();
			groupfoot.applyProperties();
		}
		return groupfoot;
	}

	private Listcell newUnloadedCell(ListitemRenderer renderer, Listitem item) {
		Listcell cell = null;
		if (renderer instanceof ListitemRendererExt)
			cell = ((ListitemRendererExt) renderer).newListcell(item);

		if (cell == null) {
			cell = new Listcell();
			cell.applyProperties();
		}
		cell.setParent(item);
		return cell;
	}

	public Object getRealRenderer() {
		final ListitemRenderer renderer = _listbox.getItemRenderer();
		return renderer != null ? renderer : _defRend;
	}

	private static final ListitemRenderer _defRend = new ListitemRenderer() {
		public void render(final Listitem item, final Object data, final int index) {
			final Listbox listbox = (Listbox) item.getParent();
			Template tm = listbox.getTemplate("model");
			GroupingInfo info = null;
			if (item instanceof Listgroup) {
				final Template tm2 = listbox.getTemplate("model:group");
				if (tm2 != null)
					tm = tm2;
				if (listbox.getModel() instanceof GroupsListModel) {
					final GroupsListModel gmodel = (GroupsListModel) listbox.getModel();
					info = gmodel.getDataInfo(index);
				}
			} else if (item instanceof Listgroupfoot) {
				final Template tm2 = listbox.getTemplate("model:groupfoot");
				if (tm2 != null)
					tm = tm2;
			}
			if (tm == null) {
				item.setLabel(Objects.toString(data));
				item.setValue(data);
			} else {
				final GroupingInfo groupingInfo = info;
				final Component[] items = ShadowElementsCtrl
						.filterOutShadows(tm.create(listbox, item, 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 listbox.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 if ("groupingInfo".equals(name)) {
							return groupingInfo;
						} else {
							return null;
						}
					}
				}, null));
				if (items.length != 1)
					throw new UiException("The model template must have exactly one item, not " + items.length);

				final Listitem nli = (Listitem) items[0];

				//sync open state
				if (nli instanceof Listgroup && item instanceof Listgroup) {
					((Listgroup) nli).setOpen(((Listgroup) item).isOpen());
				}

				if (nli.getValue() == null) //template might set it
					nli.setValue(data);
				item.setAttribute(Attributes.MODEL_RENDERAS, nli);
				//indicate a new item is created to replace the existent one
				item.detach();
			}
		}
	};

	public void syncModel(int offset, int limit) {
		_listbox.setAttribute(Listbox.SYNCING_MODEL, Boolean.TRUE);
		try {
			syncModel0(offset, limit);
		} finally {
			//Bug ZK-2789: do not use setAttribute when actually trying to removeAttribute
			_listbox.removeAttribute(Listbox.SYNCING_MODEL);
		}
	}

	private void syncModel0(int offset, int limit) {
		int min = offset;
		int max = offset + limit - 1;

		final ListModel _model = _listbox.getModel();
		final int newsz = _model.getSize();
		final int oldsz = _listbox.getItemCount();
		final Paginal _pgi = _listbox.getPaginal();
		final boolean inPaging = inPagingMold();
		final boolean shallInvalidated = //Bug 3147518: avoid memory leak
		(min < 0 || min == 0) && (max < 0 || max >= newsz || max >= oldsz);

		int newcnt = newsz - oldsz;
		int atg = _pgi != null ? _listbox.getActivePage() : 0;
		ListitemRenderer renderer = null;
		Component next = null;
		if (oldsz > 0) {
			if (min < 0)
				min = 0;
			else if (min > oldsz - 1)
				min = oldsz - 1;
			if (max < 0)
				max = oldsz - 1;
			else if (max > oldsz - 1)
				max = oldsz - 1;
			if (min > max) {
				int t = min;
				min = max;
				max = t;
			}

			int cnt = max - min + 1; //# of affected
			if (_model instanceof GroupsListModel) {
				//detach all from end to front since groupfoot
				//must be detached before group
				newcnt += cnt; //add affected later
				if ((shallInvalidated || newcnt > INVALIDATE_THRESHOLD) && !inPaging)
					invalidateListitems();

				Component comp = _listbox.getItemAtIndex(max);
				next = comp.getNextSibling();
				while (--cnt >= 0) {
					Component p = comp.getPreviousSibling();
					comp.detach();
					comp = p;
				}
			} else { //ListModel
				int addcnt = 0;
				Component item = _listbox.getItemAtIndex(min);
				while (--cnt >= 0) {
					next = item.getNextSibling();

					if (cnt < -newcnt) { //if shrink, -newcnt > 0
						item.detach(); //remove extra
					} else if (((Listitem) item).isLoaded()) {
						if (renderer == null)
							renderer = (ListitemRenderer) getRealRenderer();

						// ZK-2450: cache selected Index and item, added them back after detach item
						if (_pgi != null && ((Listitem) item).isSelected()) {
							int index = ((Listitem) item).getIndex();
							item.detach(); // always detach
							Listitem newItem = newUnloadedItem(renderer, min);
							_listbox.insertBefore(newItem, next);
							_listbox.addItemToSelection(newItem);
						} else {
							item.detach(); //always detach
							_listbox.insertBefore(newUnloadedItem(renderer, min), next);
						}
						++addcnt;
					}
					++min;
					item = next; //B2100338.,next item could be Paging, don't use Listitem directly
				}

				if ((shallInvalidated || addcnt > INVALIDATE_THRESHOLD || addcnt + newcnt > INVALIDATE_THRESHOLD)
						&& !inPagingMold())
					invalidateListitems();
			}
		} else {
			min = 0;
		}

		for (; --newcnt >= 0; ++min) {
			if (renderer == null)
				renderer = (ListitemRenderer) getRealRenderer();
			_listbox.insertBefore(newUnloadedItem(renderer, min), next);
		}
		if (_pgi != null) {
			if (atg >= _pgi.getPageCount())
				atg = _pgi.getPageCount() - 1;
			_pgi.setActivePage(atg);
			if (_pgi.getTotalSize() != newsz)
				_pgi.setTotalSize(newsz); //Bug ZK-1601: reset total size since model size may changed.
		}
	}

	protected boolean inPagingMold() {
		return "paging".equals(_listbox.getMold());
	}

	protected boolean inSelectMold() {
		return "select".equals(_listbox.getMold());
	}

	public void updateModelInfo() {
		// do nothing
	}

	public void setLoadAll(boolean b) {
		// do nothing
	}

	//--Cropper--//
	public boolean isCropper() {
		return _listbox != null && inPagingMold() && _listbox.getPageSize() <= getTotalSize();
		//Single page is considered as not a cropper.
		//isCropper is called after a component is removed, so
		//we have to test >= rather than >
	}

	/** Retrieves the children available at client.
	 * <p>It can not be overridden. Rather, override {@link #getAvailableAtClient(boolean)} instead.
	 */
	public final Set<? extends Component> getAvailableAtClient() {
		return getAvailableAtClient(false);
	}

	/** Retrieves the children available at client with more control.
	 * <p>Derived class shall override this method rather than {@link #getAvailableAtClient()}.
	 * @param itemOnly whether to return only {@link Listitem} and derives.
	 * @since 5.0.10
	 */
	protected Set<? extends Component> getAvailableAtClient(boolean itemOnly) {
		final boolean invalidateListitems = Executions.getCurrent()
				.removeAttribute("zkoss.Listbox.invalidateListitems" + _listbox.getUuid()) != null;
		if (invalidateListitems && !_listbox.isInvalidated())
			return getAvailableAtClient(0, 0, itemOnly);
		if (!isCropper())
			return null;

		final Paginal pgi = _listbox.getPaginal();
		int pgsz = pgi.getPageSize();
		int ofs = pgi.getActivePage() * pgsz;
		return getAvailableAtClient(ofs, pgsz, itemOnly);
	}

	/** Retrieves the children available at the client within the given range.
	 * @param itemOnly whether to return only {@link Listitem} and derives.
	 * @since 5.0.10
	 */
	protected Set<? extends Component> getAvailableAtClient(int offset, int limit, boolean itemOnly) {
		final Set<Component> avail = new LinkedHashSet<Component>(32);
		if (!itemOnly) {
			avail.addAll(_listbox.getHeads());
			final Listfoot listfoot = _listbox.getListfoot();
			if (listfoot != null)
				avail.add(listfoot);
			final Paging paging = _listbox.getPagingChild();
			if (paging != null)
				avail.add(paging);
			final Frozen frozen = _listbox.getFrozen();
			if (frozen != null)
				avail.add(frozen);
		}

		int pgsz = limit;
		int ofs = offset;
		if (_listbox.getItemCount() > 0) {
			Component item = _listbox.getItems().get(0);
			while (item != null) {
				if (pgsz == 0)
					break;
				if (item.isVisible() && item instanceof Listitem) {
					if (--ofs < 0) {
						--pgsz;
						avail.add(item);
					}
				}
				if (item instanceof Listgroup) {
					final Listgroup g = (Listgroup) item;
					if (!g.isOpen()) {
						for (int j = 0, len = g.getItemCount(); j < len; j++)
							item = item.getNextSibling();
					}
				}
				if (item != null)
					item = item.getNextSibling();
			}
		}
		return avail;
	}

	public Component getCropOwner() {
		return _listbox;
	}

	protected static class DeferredRedraw implements DeferredValue {
		private final Collection<? extends Component> _items;

		public DeferredRedraw(Collection<? extends Component> items) {
			_items = items;
		}

		public Object getValue() {
			return ComponentsCtrl.redraw(_items);
		}
	}
}
