/* ListModelList.java

{{IS_NOTE
	Purpose:
		
	Description:
		
	History:
		Thu Nov 23 17:43:13     2006, Created by Henri Chen
}}IS_NOTE

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

{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zul;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.Sortable;

/**
 * <p>This is the {@link ListModel} as a {@link java.util.List} to be used with {@link Listbox}.
 * Add or remove the contents of this model as a List would cause the associated Listbox to change accordingly.</p> 
 *
 * <p>For more information, please refer to
 * <a href="http://books.zkoss.org/wiki/ZK_Developer%27s_Reference/MVC/Model/List_Model">ZK Developer's Reference: List Model</a>
 *
 * @author Henri Chen
 * @see ListModel
 * @see ListModelList
 * @see ListModelMap
 */
public class ListModelList<E> extends AbstractListModel<E> implements Sortable<E>, List<E>, java.io.Serializable {
	private static final long serialVersionUID = 20120206122641L;

	protected List<E> _list;

	private Comparator<E> _sorting;

	private boolean _sortDir;

	/**
	 * Constructor
	 *
	 * @param list the list to represent
	 * @param live whether to have a 'live' {@link ListModel} on top of
	 * the specified list.
	 * If false, the content of the specified list is copied.
	 * If true, this object is a 'facade' of the specified list,
	 * i.e., when you add or remove items from this ListModelList,
	 * the inner "live" list would be changed accordingly.
	 *
	 * However, it is not a good idea to modify <code>list</code>
	 * if it is passed to this method with live is true,
	 * since {@link Listbox} is not smart enough to handle it.
	 * Instead, modify it thru this object.
	 * @since 2.4.0
	 */
	public ListModelList(List<E> list, boolean live) {
		_list = live ? list : new ArrayList<E>(list);
	}

	/**
	 * Constructor.
	 */
	public ListModelList() {
		_list = new ArrayList<E>();
	}

	/**
	 * Constructor.
	 * It makes a copy of the specified collection (i.e., not live).
	 *
	 * <p>Notice that if the data is static or not shared, it is better to
	 * use <code>ListModelList(c, true)</code> instead, since
	 * making a copy is slower.
	 */
	public ListModelList(Collection<? extends E> c) {
		_list = new ArrayList<E>(c);
	}

	/**
	 * Constructor.
	 * It makes a copy of the specified array (i.e., not live).
	 * @since 2.4.1
	 */
	public ListModelList(E[] array) {
		_list = new ArrayList<E>(array.length);
		for (int j = 0; j < array.length; ++j)
			_list.add(array[j]);
	}

	/**
	 * Constructor.
	 * @param initialCapacity the initial capacity for this ListModelList.
	 */
	public ListModelList(int initialCapacity) {
		_list = new ArrayList<E>(initialCapacity);
	}

	/**
	 * Remove from fromIndex(inclusive) to toIndex(exclusive). If fromIndex equals toIndex, 
	 * this methods do nothing.
	 * @param fromIndex the begin index (inclusive) to be removed.
	 * @param toIndex the end index (exclusive) to be removed.
	 */
	public void removeRange(int fromIndex, int toIndex) {
		if (fromIndex > toIndex) {
			throw new UiException(
					"fromIndex must less than toIndex: fromIndex: " + fromIndex + ", toIndex: " + toIndex);
		}
		if (fromIndex == toIndex) {
			return;
		}
		int sz = _list.size();
		if (sz == fromIndex) {
			return;
		}
		int index = fromIndex;
		//B70-ZK-2447 : IndexOutOfBound exception occurs when using ListModelList.removeRange(0, size) with all items selected
		Set<E> removedObjs = new HashSet<E>();
		for (final Iterator<E> it = _list.listIterator(fromIndex); it.hasNext() && index < toIndex; ++index) {
			final E obj = it.next();
			removedObjs.add(obj);
			it.remove();
		}
		fireEvent(ListDataEvent.INTERVAL_REMOVED, fromIndex, index - 1);
		for (E obj : removedObjs)
			removeFromSelection(obj);
	}

	/**
	 * Get the inner real List.
	 */
	public List<E> getInnerList() {
		return _list;
	}

	//-- ListModel --//
	public int getSize() {
		return _list.size();
	}

	public E getElementAt(int j) {
		return _list.get(j);
	}

	//-- List --//
	public void add(int index, E element) {
		_list.add(index, element);
		fireEvent(ListDataEvent.INTERVAL_ADDED, index, index);
	}
	
	public boolean add(E o) {
		int i1 = _list.size();
		boolean ret = _list.add(o);
		fireEvent(ListDataEvent.INTERVAL_ADDED, i1, i1);
		return ret;
	}

	/**
	 * Notifies a change of the same element to trigger an event of {@link ListDataEvent#CONTENTS_CHANGED}.
	 * @param element
	 * @return true if the element exists
	 * @since 8.0.0
	 */
	public boolean notifyChange(E element) {
		int i = _list.indexOf(element);
		if (i >= 0) {
			fireEvent(ListDataEvent.CONTENTS_CHANGED, i, i);
			return true;
		}
		return false;
	}

	public boolean addAll(Collection<? extends E> c) {
		int sz = c.size();
		if (sz <= 0) {
			return false;
		}
		int i1 = _list.size();
		int i2 = i1 + sz - 1;
		boolean ret = _list.addAll(c);
		fireEvent(ListDataEvent.INTERVAL_ADDED, i1, i2);
		return ret;
	}

	public boolean addAll(int index, Collection<? extends E> c) {
		int sz = c.size();
		if (sz <= 0) {
			return false;
		}
		int i2 = index + sz - 1;
		boolean ret = _list.addAll(index, c);
		fireEvent(ListDataEvent.INTERVAL_ADDED, index, i2);
		return ret;
	}

	public void clear() {
		int i2 = _list.size() - 1;
		if (i2 < 0) {
			return;
		}
		clearSelection();
		_list.clear();
		fireEvent(ListDataEvent.INTERVAL_REMOVED, 0, i2);
	}

	public boolean contains(Object elem) {
		boolean ret = _list.contains(elem);
		return ret;
	}

	public boolean containsAll(Collection<?> c) {
		return _list.containsAll(c);
	}

	public boolean equals(Object o) {
		if (this == o)
			return true;
		return _list.equals(o instanceof ListModelList<?> ? ((ListModelList<?>) o)._list : o);
	}

	public E get(int index) {
		return _list.get(index);
	}

	public int hashCode() {
		return _list.hashCode();
	}

	public String toString() {
		return _list.toString();
	}

	public int indexOf(Object elem) {
		return _list.indexOf(elem);
	}

	public boolean isEmpty() {
		return _list.isEmpty();
	}

	public Iterator<E> iterator() {
		return new Iterator<E>() {
			private Iterator<E> _it = _list.iterator();
			private E _current = null;
			private int _nextIndex;

			public boolean hasNext() {
				return _it.hasNext();
			}

			public E next() {
				_current = _it.next();
				++_nextIndex;
				return _current;
			}

			public void remove() {
				removeFromSelection(_current);
				_it.remove();
				--_nextIndex;
				fireEvent(ListDataEvent.INTERVAL_REMOVED, _nextIndex, _nextIndex);
			}
		};
	}

	public int lastIndexOf(Object elem) {
		return _list.lastIndexOf(elem);
	}

	public ListIterator<E> listIterator() {
		return listIterator(0);
	}

	public ListIterator<E> listIterator(final int index) {
		return new ListIterator<E>() {
			private ListIterator<E> _it = _list.listIterator(index);
			private E _current = null;

			public boolean hasNext() {
				return _it.hasNext();
			}

			public E next() {
				_current = _it.next();
				return _current;
			}

			public void remove() {
				removeFromSelection(_current);
				_it.remove();
				final int index = _it.nextIndex();
				fireEvent(ListDataEvent.INTERVAL_REMOVED, index, index);
			}

			public void add(E arg0) {
				final int index = _it.nextIndex();
				_it.add(arg0);
				fireEvent(ListDataEvent.INTERVAL_ADDED, index, index);
			}

			public boolean hasPrevious() {
				return _it.hasPrevious();
			}

			public int nextIndex() {
				return _it.nextIndex();
			}

			public E previous() {
				_current = _it.previous();
				return _current;
			}

			public int previousIndex() {
				return _it.previousIndex();
			}

			public void set(E arg0) {
				_it.set(arg0);
				final int index = _it.nextIndex() - 1;
				fireEvent(ListDataEvent.CONTENTS_CHANGED, index, index);
			}
		};
	}

	public E remove(int index) {
		E ret = _list.get(index);
		removeFromSelection(ret);
		//Bug ZK-1428: should remove the object after removeFromSelection
		//since Listbox.doSelectionChanged will try to get the object again
		_list.remove(index);
		fireEvent(ListDataEvent.INTERVAL_REMOVED, index, index);
		return ret;
	}

	public boolean remove(Object o) {
		int index = indexOf(o);
		if (index >= 0) {
			remove(index);
			return true;
		}
		return false;
	}

	public boolean removeAll(Collection<?> c) {
		if (_list == c || this == c) { // special case
			clearSelection();
			clear();
			return true;
		}
		return removePartial(c, true);
	}

	public boolean retainAll(Collection<?> c) {
		if (_list == c || this == c) { //special case
			return false;
		}
		return removePartial(c, false);
	}

	private boolean removePartial(Collection<?> c, boolean exclude) {
		boolean removed = false;
		int index = 0;
		int begin = -1;
		// B60-ZK-1126.zul
		// Remember the selections to be cleared
		List<E> selected = new ArrayList<E>();
		for (final Iterator<E> it = _list.iterator(); it.hasNext(); ++index) {
			E item = it.next();
			if (c.contains(item) == exclude) {
				if (begin < 0) {
					begin = index;
				}
				removed = true;
				it.remove();
				// B60-ZK-1126.zul
				// Removed item has been selected; remember and remove the selection later
				if (_selection.contains(item)) {
					selected.add(item);
				}
			} else {
				if (begin >= 0) {
					fireEvent(ListDataEvent.INTERVAL_REMOVED, begin, index - 1);
					index = begin; //this range removed, the index is reset to begin
					begin = -1;
				}
			}
		}
		// B60-ZK-1126.zul
		// Clear the selected items that were removed
		if (!selected.isEmpty()) {
			removeAllSelection(selected);
		}
		if (begin >= 0) {
			fireEvent(ListDataEvent.INTERVAL_REMOVED, begin, index - 1);
		}

		return removed;
	}

	public E set(int index, E element) {
		E ret = _list.set(index, element);
		fireEvent(ListDataEvent.CONTENTS_CHANGED, index, index);
		return ret;
	}

	public int size() {
		return _list.size();
	}

	public List<E> subList(int fromIndex, int toIndex) {
		List<E> list = _list.subList(fromIndex, toIndex);
		//bug 2448987: sublist must be live
		return new ListModelList<E>(list, true);
	}

	public Object[] toArray() {
		return _list.toArray();
	}

	public <T> T[] toArray(T[] a) {
		return _list.toArray(a);
	}

	//-- Sortable --//
	/** Sorts the data.
	 *
	 * @param cmpr the comparator.
	 * @param ascending whether to sort in the ascending order.
	 * It is ignored since this implementation uses cmprt to compare.
	 */
	public void sort(Comparator<E> cmpr, final boolean ascending) {
		_sorting = cmpr;
		_sortDir = ascending;
		Collections.sort(_list, cmpr);
		fireEvent(ListDataEvent.STRUCTURE_CHANGED, -1, -1);
	}

	public String getSortDirection(Comparator<E> cmpr) {
		if (Objects.equals(_sorting, cmpr))
			return _sortDir ? "ascending" : "descending";
		return "natural";
	}

	@SuppressWarnings("unchecked")
	public Object clone() {
		ListModelList<E> clone = (ListModelList<E>) super.clone();
		if (_list != null)
			clone._list = new ArrayList<E>(_list);
		return clone;
	}

	protected void fireSelectionEvent(E e) {
		fireEvent(ListDataEvent.SELECTION_CHANGED, indexOf(e), -1);
	}

	//For Backward Compatibility//
	/** @deprecated As of release 6.0.0, replaced with {@link #addToSelection}.
	 */
	public void addSelection(E obj) {
		addToSelection(obj);
	}

	/** @deprecated As of release 6.0.0, replaced with {@link #removeFromSelection}.
	 */
	public void removeSelection(Object obj) {
		removeFromSelection(obj);
	}
}