/** ListModelListProxy.java.

	Purpose:

	Description:

	History:
		Mon Jun 08 11:30:22 CST 2021, Created by jameschu

	Copyright (C) 2021 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.bind.proxy;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;

import org.zkoss.bind.BindContext;
import org.zkoss.bind.annotation.ImmutableElements;
import org.zkoss.bind.sys.FormBinding;
import org.zkoss.bind.sys.SavePropertyBinding;
import org.zkoss.lang.Objects;
import org.zkoss.util.Pair;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.ListModelList;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.ext.SelectionControl;
import org.zkoss.zul.ext.Sortable;

/**
 * A ListModelList Proxy
 *
 * @author jameschu
 * @since 9.6.0
 */
public class ListModelListProxy<E> extends ListModelList<E> implements Proxy, FormProxyObject, Serializable {
	private static final long serialVersionUID = 20210608113022L;
	private ListModelList<E> _cache;
	private ListModelList<E> _origin;
	private boolean _dirty;
	private boolean isImmutableElements;
	private Annotation[] _callerAnnots;
	private ProxyNode _node;

	public ListModelListProxy(ListModelList<E> origin, Annotation[] callerAnnots) {
		_origin = origin;
		_cache = initCache();
		if (callerAnnots != null) {
			for (Annotation annot : callerAnnots) {
				if (annot.annotationType().isAssignableFrom(ImmutableElements.class)) {
					isImmutableElements = true;
					break;
				}
			}
			_callerAnnots = callerAnnots;
		}
		resetFromOrigin();
	}

	private ListModelList<E> initCache() {
		ListModelList<E> cache = new ListModelList<>(getOriginObject().size());
		return cache;
	}

	public ListModelList<E> getCache() {
		return _cache;
	}

	public void onDirtyChange() {
		ProxyHelper.callOnDirtyChange(_node);
	}

	public void onDataChange(Object o) {
		ProxyHelper.callOnDataChange(_node, new Object[]{o, "."});
	}

	protected void setDirty(boolean d) {
		if (_dirty != d) {
			_dirty = d;
			onDirtyChange();
		}
		if (d)
			onDataChange(this);
	}

	//-- AbstractListModel --//
	public void addListDataListener(ListDataListener l) {
		getCache().addListDataListener(l); //initial setting
	}
	public void removeListDataListener(ListDataListener l) {
		getCache().removeListDataListener(l); //initial setting
	}

	public Set<E> getSelection() {
		return getCache().getSelection();
	}

	public void setSelection(Collection<? extends E> selection) {
		getCache().setSelection(selection);
		setDirty(true);
	}

	public boolean isSelected(Object obj) {
		return getCache().isSelected(obj);
	}

	public boolean isSelectionEmpty() {
		return getCache().isSelectionEmpty();
	}

	public boolean addToSelection(E obj) {
		boolean result = getCache().addToSelection(obj);
		if (result)
			setDirty(true);
		return result;
	}

	public boolean removeFromSelection(Object obj) {
		boolean result = getCache().removeFromSelection(obj);
		if (result)
			setDirty(true);
		return result;
	}

	public void clearSelection() {
		if (!getCache().getSelection().isEmpty()) {
			getCache().clearSelection();
			setDirty(true);
		}
	}

	public void setMultiple(boolean multiple) {
		if (getCache().isMultiple() != multiple) {
			getCache().setMultiple(multiple);
			setDirty(true);
		}
	}

	public void setSelectionControl(SelectionControl ctrl) {
		getCache().setSelectionControl(ctrl); //initial setting
	}

	public SelectionControl getSelectionControl() {
		return getCache().getSelectionControl(); //initial setting
	}

	public int getPageSize() {
		return getCache().getPageSize();
	}

	public void setPageSize(int size) throws WrongValueException {
		getCache().setPageSize(size);
		setDirty(true);
	}

	public int getPageCount() {
		return getCache().getPageCount();
	}

	public int getActivePage() {
		return getCache().getActivePage();
	}

	public void setActivePage(int pg) throws WrongValueException {
		getCache().setActivePage(pg);
		setDirty(true);
	}

	public void addPagingEventListener(PagingListener l) {
		getCache().addPagingEventListener(l); //initial setting
	}

	public void removePagingEventListener(PagingListener l) {
		getCache().removePagingEventListener(l); //initial setting
	}

	//-- ListModelList --//
	public void removeRange(int fromIndex, int toIndex) {
		getCache().removeRange(fromIndex, toIndex);
		setDirty(true);
	}

	public List<E> getInnerList() {
		return getCache().getInnerList();
	}

	public int getSize() {
		return getCache().getSize();
	}

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

	public boolean add(E o) {
		if (getCache().add(createProxyObject(o))) {
			setDirty(true);
			return true;
		}
		return false;
	}

	public void add(int index, E element) {
		getCache().add(index, createProxyObject(element));
		setDirty(true);
	}

	public boolean notifyChange(E element) {
		return getCache().notifyChange(element);
	}

	public boolean addAll(Collection<? extends E> c) {
		boolean modified = false;
		Iterator<? extends E> e = c.iterator();
		while (e.hasNext()) {
			if (add(e.next()))
				modified = true;
		}
		if (modified)
			setDirty(true);
		return modified;
	}

	public boolean addAll(int index, Collection<? extends E> c) {
		boolean result = false;
		if (c.size() > 0) {
			List<E> proxyList = new LinkedList<>();
			for (E e : c)
				proxyList.add(createProxyObject(e));
			result = getCache().addAll(index, proxyList);
			setDirty(true);
		}
		return result;
	}

	public void clear() {
		getCache().clear();
		setDirty(true);
	}

	public boolean contains(Object o) {
		Iterator<E> e = iterator();
		if (o == null) {
			while (e.hasNext())
				if (testEquals(e.next(), null))
					return true;
		} else {
			while (e.hasNext())
				if (testEquals(o, e.next()))
					return true;
		}
		return false;
	}

	public boolean containsAll(Collection<?> c) {
		Iterator<?> e = c.iterator();
		while (e.hasNext())
			if (!contains(e.next()))
				return false;
		return true;
	}

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

	public int indexOf(Object o) {
		final int size = size();
		if (o == null) {
			for (int i = 0; i < size; i++)
				if (testEquals(get(i), null))
					return i;
		} else {
			for (int i = 0; i < size; i++)
				if (testEquals(o, get(i)))
					return i;
		}
		return -1;
	}

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

	public int lastIndexOf(Object o) {
		final int size = size();
		if (o == null) {
			for (int i = size - 1; i >= 0; i--)
				if (testEquals(get(i), null))
					return i;
		} else {
			for (int i = size - 1; i >= 0; i--)
				if (testEquals(o, get(i)))
					return i;
		}
		return -1;
	}

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

	public ListIterator<E> listIterator(int index) {
		return getCache().listIterator(index);
	}

	public boolean remove(Object o) {
		if (getCache().remove(createProxyObject(o))) {
			setDirty(true);
			return true;
		}
		return false;
	}

	public E remove(int index) {
		E removed = getCache().remove(index);
		setDirty(true);
		return removed;
	}

	public boolean removeAll(Collection<?> c) {
		boolean modified = false;
		Iterator<?> e = iterator();
		c = createProxyObject(c); // use a proxy object to compare
		while (e.hasNext()) {
			if (c.contains(e.next())) {
				e.remove();
				modified = true;
			}
		}
		if (modified)
			setDirty(true);
		return modified;
	}

	public boolean retainAll(Collection<?> c) {
		boolean modified = false;
		Iterator<E> e = iterator();
		c = createProxyObject(c); // use a proxy object to compare
		while (e.hasNext()) {
			if (!c.contains(e.next())) {
				e.remove();
				modified = true;
			}
		}
		if (modified)
			setDirty(true);
		return modified;
	}

	public Iterator<E> iterator() {
		return getCache().iterator();
	}

	public E set(int index, E element) {
		E prevElement = getCache().set(index, createProxyObject(element));
		setDirty(true);
		return prevElement;
	}

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

	public List<E> subList(int fromIndex, int toIndex) {
		return ProxyHelper.createProxyIfAny(getCache().subList(fromIndex, toIndex), _callerAnnots);
	}

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

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

	//-- Object --//
	public boolean equals(Object o) {
		return getCache().equals(o);
	}

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

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

	//-- Sortable --//
	public void sort(Comparator<E> cmpr, final boolean ascending) {
		((Sortable) getCache()).sort(cmpr, ascending);
		setDirty(true);
	}

	public void sort() {
		((Sortable) getCache()).sort();
		setDirty(true);
	}

	public String getSortDirection(Comparator<E> cmpr) {
		return ((Sortable) getCache()).getSortDirection(cmpr);
	}

	public Object clone() {
		return getCache().clone();
	}

	//-- Proxy --//
	public void setHandler(MethodHandler mi) {
		throw new UnsupportedOperationException("Not support!");
	}

	//-- FormProxyObject --//
	public ListModelList<E> getOriginObject() {
		return _origin;
	}
	public void resetFromOrigin() {
		getCache().clear();
		setDirty(false);
		for (E e : getOriginObject()) {
			getCache().add(createProxyObject(e));
		}
	}

	public void submitToOrigin(BindContext ctx) {
		Collection<E> origin = ((Collection<E>) getOriginObject());
		origin.clear();
		for (E e : _cache) {
			if (e instanceof FormProxyObject) {
				FormProxyObject proxy = (FormProxyObject) e;
				proxy.submitToOrigin(ctx);
				origin.add((E) proxy.getOriginObject());
			} else {
				origin.add(e);
			}
		}
		setDirty(false);
	}

	public boolean isFormDirty() {
		if (_dirty)
			return true;
		Iterator<?> e = iterator();
		while (e.hasNext()) {
			Object o = e.next();
			if (o instanceof FormProxyObject) {
				if (((FormProxyObject) o).isFormDirty())
					return true;
			}
		}
		return false;
	}

	public void setFormOwner(Object owner, FormBinding binding) {
		throw new IllegalAccessError("Not supported");
	}

	public void setPath(String property, ProxyNode parent) {
		if (property == null && _node != null) { // means update
			_node.setParent(parent);
		} else {
			_node = new ProxyNodeImpl(property, parent);
			for (E e : _cache) {
				if (e instanceof FormProxyObject)
					((FormProxyObject) e).setPath("[$INDEX$]", _node);
			}
		}
	}

	public void cacheSavePropertyBinding(String property, SavePropertyBinding s) {
		ProxyHelper.cacheSavePropertyBinding(_node, _node.getProperty() + property, s);
	}

	public Set<Pair<String, SavePropertyBinding>> collectCachedSavePropertyBinding() {
		throw new UnsupportedOperationException("Not support!");
	}

	protected static boolean testEquals(Object e, Object o) {
		if (e == o) {
			return true;
		}
		if (e instanceof FormProxyObject) {
			Object eo = ((FormProxyObject) e).getOriginObject();
			Object oo = o;
			if (o instanceof FormProxyObject) {
				oo = ((FormProxyObject) o).getOriginObject();
			}
			if (Objects.equals(eo, oo))
				return true;
		} else if (o instanceof FormProxyObject) {
			if (Objects.equals(e, ((FormProxyObject) o).getOriginObject()))
				return true;
		} else {
			if (Objects.equals(e, o))
				return true;
		}
		return false;
	}

	protected <T extends Object> T createProxyObject(T t) {
		T p = isImmutableElements ? t : ProxyHelper.createProxyIfAny(t);
		if (p instanceof FormProxyObject)
			((FormProxyObject) p).setPath("[$INDEX$]", _node);
		return p;
	}
}
