/* AbstractListModel.java

	Purpose:
		
	Description:
		
	History:
		Thu Aug 18 15:19:43     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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.io.Serializables;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;

/**
 * A skeletal implementation for {@link ListModel} and {@link Selectable}
 * 
 * @author tomyeh
 */
public abstract class AbstractListModel<E>
		implements ListModel<E>, Selectable<E>, java.io.Serializable, PageableModel {

	private static final Logger log = LoggerFactory.getLogger(AbstractListModel.class);

	private transient List<ListDataListener> _listeners = new ArrayList<ListDataListener>();
	private transient List<PagingListener> _pagingListeners = new ArrayList<PagingListener>();

	/** The current selection. */
	protected transient Set<E> _selection;
	private SelectionControl<E> _ctrl;
	private boolean _multiple;

	protected AbstractListModel() {
		_selection = newEmptySelection();
		_ctrl = new DefaultSelectionControl<E>(this);
	}

	public List<ListDataListener> getListDataListeners() {
		return _listeners;
	}

	/**
	 * Fires a {@link ListDataEvent} for all registered listener (thru
	 * {@link #addListDataListener}.
	 * 
	 * <p>
	 * Note: you can invoke this method only in an event listener.
	 */
	protected void fireEvent(int type, int index0, int index1) {
		final ListDataEvent evt = new ListDataEvent(this, type, index0, index1);
		for (ListDataListener l : _listeners)
			l.onChange(evt);
	}

	// -- ListModel --//
	/** {@inheritDoc} */
	public void addListDataListener(ListDataListener l) {
		if (l == null)
			throw new NullPointerException();
		_listeners.add(l);
	}

	/** {@inheritDoc} */
	public void removeListDataListener(ListDataListener l) {
		_listeners.remove(l);
	}

	//Selectable//
	/** {@inheritDoc} */
	public Set<E> getSelection() {
		return Collections.unmodifiableSet(_selection);
	}

	/** {@inheritDoc} */
	public void setSelection(Collection<? extends E> selection) {
		if (isSelectionChanged(selection)) {
			if (!_multiple && selection.size() > 1)
				throw new IllegalArgumentException("Only one selection is allowed, not " + selection);
			_selection.clear();
			_selection.addAll(selection);
			if (selection.isEmpty()) {
				fireSelectionEvent(null);
			} else
				fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
		}
	}

	private boolean isSelectionChanged(Collection<? extends E> selection) {
		if (_selection.size() != selection.size())
			return true;

		for (final E e : selection)
			if (!_selection.contains(e))
				return true;
		return false;
	}

	/** {@inheritDoc} */
	public boolean isSelected(Object obj) {
		return !isSelectionEmpty() && (_selection.size() == 1 ? Objects.equals(_selection.iterator().next(), obj)
				: _selection.contains(obj));
	}

	/** {@inheritDoc} */
	public boolean isSelectionEmpty() {
		return _selection.isEmpty();
	}

	/** {@inheritDoc} */
	public boolean addToSelection(E obj) {
		if (_selection.add(obj)) {
			if (!_multiple) {
				_selection.clear();
				_selection.add(obj);
			}
			fireSelectionEvent(obj);
			return true;
		}
		return false;
	}

	/** {@inheritDoc} */
	public boolean removeFromSelection(Object obj) {
		if (_selection.remove(obj)) {
			fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
			return true;
		}
		return false;
	}

	/** {@inheritDoc} */
	public void clearSelection() {
		if (!_selection.isEmpty()) {
			_selection.clear();
			fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
		}
	}

	/**
	 * Selectable's implementor use only.
	 * <p> Fires a selection event for component to scroll into view. The override
	 * subclass must put the index0 of {@link #fireEvent(int, int, int)} as 
	 * the view index to scroll. By default, the value -1 is assumed which means
	 * no scroll into view.
	 * <p> The method is invoked when both methods are invoked. {@link #addToSelection(Object)}
	 * and {@link #setSelection(Collection)}.
	 * @param e selected object.
	 */
	protected void fireSelectionEvent(E e) {
		fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
	}

	/**Removes the selection of the given collection.
	 */
	protected void removeAllSelection(Collection<?> c) {
		// B60-ZK-1126
		// Notify selection has been changed
		if (_selection.removeAll(c)) {
			fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
		}
	}

	/**Removes the selection that doesn't belong to the given collection.
	 */
	protected void retainAllSelection(Collection<?> c) {
		// B60-ZK-1126
		// Notify selection has been changed
		if (_selection.retainAll(c)) {
			fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
		}
	}

	/** {@inheritDoc} */
	public boolean isMultiple() {
		return _multiple;
	}

	/** {@inheritDoc} */
	public void setMultiple(boolean multiple) {
		if (_multiple != multiple) {
			_multiple = multiple;
			fireEvent(ListDataEvent.MULTIPLE_CHANGED, -1, -1);

			if (!multiple && _selection.size() > 1) {
				E v = _selection.iterator().next();
				_selection.clear();
				_selection.add(v);
				fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
			}
		}
	}

	public void setSelectionControl(SelectionControl ctrl) {
		_ctrl = ctrl;
	}

	public SelectionControl getSelectionControl() {
		return _ctrl;
	}

	/**
	 * A default selection control implementation for {@link AbstractListModel},
	 * by default it assumes all elements are selectable.
	 * <p>Note: the implementation is not used for a huge data model, if in this case,
	 * please implement your own one to speed up.</p>
	 * @since 8.0.0
	 */
	public static class DefaultSelectionControl<E> implements SelectionControl<E> {
		private AbstractListModel<E> model;

		public DefaultSelectionControl(AbstractListModel<E> model) {
			this.model = model;
		}

		public boolean isSelectable(E e) {
			return true;
		}

		public void setSelectAll(boolean selectAll) {
			if (selectAll) {
				List<E> all = new LinkedList<>();
				for (int i = 0, j = model.getSize(); i < j; i++) {
					E o = model.getElementAt(i);
					if (isSelectable(o)) // check whether it can be selectable or not
						all.add(o);
				}

				// avoid scroll into view at client side.
				model.fireEvent(ListDataEvent.DISABLE_CLIENT_UPDATE, -1, -1);
				try {
					((Selectable<E>) model).setSelection(all);
				} finally {
					model.fireEvent(ListDataEvent.ENABLE_CLIENT_UPDATE, -1, -1);
				}
			} else {
				((Selectable) model).clearSelection();
			}
		}

		public boolean isSelectAll() {
			for (int i = 0, j = model.getSize(); i < j; i++) {
				E o = model.getElementAt(i);
				if (isSelectable(o) && !((Selectable) model).isSelected(o))
					return false;
			}
			return true;
		}
	}

	/** Instantiation an empty set of the section.
	 * It is used to initialize {@link #_selection}.
	 * <p>By default, it instantiates an instance of LinkedHashSet.
	 * The deriving class might override to instantiate a different class.
	 */
	protected Set<E> newEmptySelection() {
		return new LinkedHashSet<E>();
	}

	/** Writes {@link #_selection}.
	 * <p>Default: write it directly. Override it if E is not serializable.
	 */
	protected void writeSelection(java.io.ObjectOutputStream s) throws java.io.IOException {
		s.writeObject(_selection);
	}

	/** Reads back {@link #_selection}.
	 * <p>Default: write it directly. Override it if E is not serializable.
	 */
	@SuppressWarnings("unchecked")
	protected void readSelection(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		_selection = (Set<E>) s.readObject();
	}

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

		writeSelection(s);
		Serializables.smartWrite(s, _listeners);
		Serializables.smartWrite(s, _pagingListeners);
	}

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

		readSelection(s);
		_listeners = new ArrayList<ListDataListener>();
		Serializables.smartRead(s, _listeners);
		_pagingListeners = new ArrayList<PagingListener>();
		Serializables.smartRead(s, _pagingListeners);
	}

	@SuppressWarnings("unchecked")
	public Object clone() {
		final AbstractListModel clone;
		try {
			clone = (AbstractListModel) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new InternalError();
		}
		clone._listeners = new ArrayList<ListDataListener>();
		clone._pagingListeners = new ArrayList<PagingListener>();
		clone._selection = clone.newEmptySelection();
		clone._selection.addAll(_selection);
		return clone;
	}

	// Pageable //
	private int _pageSize = -1;
	private int _activePage = -1;

	// Pageable // ZK-1696
	public int getPageSize() {
		return _pageSize;
	}

	public int getTotalSize() {
		return getSize();
	}

	public void setPageSize(int size) throws WrongValueException {
		if (size < 0) {
			throw new WrongValueException("page size should >= 0");
		}
		
		if (_pageSize == size) return; //no change
		
		int oldPageSize = _pageSize;
		_pageSize = size;
		
		//ZK-3173: increased page size might causes active page to exceed page count
		//need to correct the active page value before sending out paging event
		if (size > oldPageSize) {
			int maxPageIndex = getPageCount() - 1;
			if (_activePage > maxPageIndex) {
				_activePage = maxPageIndex;
			}
		}
		
		for (PagingListener p : _pagingListeners) {
			try {
				p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, _activePage));
			} catch (Exception e) {
				log.warn("Failed to publish internal paging event", e);
			}
		}
	}

	public int getPageCount() {
		int size = getSize();
		if (size > 0 && _pageSize > 0) { //_pageSize default value is -1
			int pageCount = size / _pageSize;
			if (size % _pageSize == 0) {
				return pageCount;
			} else {
				return pageCount + 1;
			}
		} else {
			return 1;
		}
	}

	public int getActivePage() {
		return _activePage;
	}

	public void setActivePage(int pg) throws WrongValueException {
		if (pg < 0) {
			throw new WrongValueException("active page index should >= 0");
		}
		if (pg >= getPageCount()) {
			pg = getPageCount() - 1; //set to last valid page
		}
		
		if (_activePage == pg) return; //no change
		
		_activePage = pg;
		for (PagingListener p : _pagingListeners) {
			try {
				p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, pg));
			} catch (Exception e) {
				log.warn("Failed to publish internal paging event", e);
			}
		}
	}

	public void addPagingEventListener(PagingListener l) {
		if (l == null)
			throw new NullPointerException();
		_pagingListeners.add(l);
	}

	public void removePagingEventListener(PagingListener l) {
		_pagingListeners.remove(l);
	}
}
