/*******************************************************************************
 * Copyright (c) 2008, 2017 Matthew Hall and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Matthew Hall - initial API and implementation (bug 194734)
 *     Matthew Hall - bugs 262269, 265561, 262287, 268688, 278550
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 ******************************************************************************/

package org.eclipse.core.internal.databinding.property.value;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.IStaleListener;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.list.AbstractObservableList;
import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.list.ListDiff;
import org.eclipse.core.databinding.observable.list.ListDiffEntry;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.value.ValueDiff;
import org.eclipse.core.databinding.property.INativePropertyListener;
import org.eclipse.core.databinding.property.IPropertyObservable;
import org.eclipse.core.databinding.property.ISimplePropertyListener;
import org.eclipse.core.databinding.property.SimplePropertyEvent;
import org.eclipse.core.databinding.property.value.SimpleValueProperty;
import org.eclipse.core.internal.databinding.identity.IdentityMap;
import org.eclipse.core.internal.databinding.identity.IdentityObservableSet;
import org.eclipse.core.internal.databinding.identity.IdentitySet;

/**
 * @param <S>
 *            type of the source object
 * @param <M>
 *            type of the elements in the master list
 * @param <T>
 *            type of the elements in the list, being the type of the value of
 *            the detail property
 * @since 1.2
 */
public class ListSimpleValueObservableList<S, M extends S, T> extends AbstractObservableList<T>
		implements IPropertyObservable<SimpleValueProperty<S, T>> {
	private IObservableList<M> masterList;
	private SimpleValueProperty<S, T> detailProperty;

	private IObservableSet<M> knownMasterElements;
	private Map<M, T> cachedValues;
	private Set<M> staleElements;

	private boolean updating;

	private IListChangeListener<M> masterListener = new IListChangeListener<M>() {
		@Override
		public void handleListChange(ListChangeEvent<? extends M> event) {
			if (!isDisposed()) {
				updateKnownElements();
				fireListChange(convertDiff(event.diff));
			}
		}

		private void updateKnownElements() {
			Set<M> identityKnownElements = new IdentitySet<>(masterList);
			knownMasterElements.retainAll(identityKnownElements);
			knownMasterElements.addAll(identityKnownElements);
		}

		private ListDiff<T> convertDiff(ListDiff<? extends M> diff) {
			// Convert diff to detail value
			ListDiffEntry<? extends M>[] masterEntries = diff.getDifferences();
			List<ListDiffEntry<T>> detailEntries = new ArrayList<>(masterEntries.length);
			for (ListDiffEntry<? extends M> masterDifference : masterEntries) {
				int index = masterDifference.getPosition();
				boolean addition = masterDifference.isAddition();
				M masterElement = masterDifference.getElement();
				T elementDetailValue = detailProperty.getValue(masterElement);
				detailEntries.add(Diffs.createListDiffEntry(index, addition, elementDetailValue));
			}
			return Diffs.createListDiff(detailEntries);
		}
	};

	private IStaleListener staleListener = staleEvent -> fireStale();

	private INativePropertyListener<S> detailListener;

	/**
	 * @param masterList
	 * @param valueProperty
	 */
	public ListSimpleValueObservableList(IObservableList<M> masterList, SimpleValueProperty<S, T> valueProperty) {
		super(masterList.getRealm());
		this.masterList = masterList;
		this.detailProperty = valueProperty;

		ISimplePropertyListener<S, ValueDiff<? extends T>> listener = event -> {
			if (!isDisposed() && !updating) {
				getRealm().exec(() -> {
					@SuppressWarnings("unchecked")
					M source = (M) event.getSource();
					if (event.type == SimplePropertyEvent.CHANGE) {
						notifyIfChanged(source);
					} else if (event.type == SimplePropertyEvent.STALE) {
						boolean wasStale = !staleElements.isEmpty();
						staleElements.add(source);
						if (!wasStale)
							fireStale();
					}
				});
			}
		};
		this.detailListener = detailProperty.adaptListener(listener);
	}

	@Override
	protected void firstListenerAdded() {
		ObservableTracker.setIgnore(true);
		try {
			knownMasterElements = new IdentityObservableSet<>(getRealm(), null);
		} finally {
			ObservableTracker.setIgnore(false);
		}

		cachedValues = new IdentityMap<>();
		staleElements = new IdentitySet<>();
		knownMasterElements.addSetChangeListener(event -> {
			for (M key1 : event.diff.getRemovals()) {
				if (detailListener != null)
					detailListener.removeFrom(key1);
				cachedValues.remove(key1);
				staleElements.remove(key1);
			}
			for (M key2 : event.diff.getAdditions()) {
				cachedValues.put(key2, detailProperty.getValue(key2));
				if (detailListener != null)
					detailListener.addTo(key2);
			}
		});
		getRealm().exec(() -> {
			knownMasterElements.addAll(masterList);

			masterList.addListChangeListener(masterListener);
			masterList.addStaleListener(staleListener);
		});
	}

	@Override
	protected void lastListenerRemoved() {
		if (masterList != null) {
			masterList.removeListChangeListener(masterListener);
			masterList.removeStaleListener(staleListener);
		}
		if (knownMasterElements != null) {
			knownMasterElements.dispose();
			knownMasterElements = null;
		}
		if (cachedValues != null) {
			cachedValues.clear();
			cachedValues = null;
		}
		if (staleElements != null) {
			staleElements.clear();
			staleElements = null;
		}
	}

	@Override
	protected int doGetSize() {
		getterCalled();
		return masterList.size();
	}

	private void getterCalled() {
		ObservableTracker.getterCalled(this);
	}

	@Override
	public Object getElementType() {
		return detailProperty.getValueType();
	}

	@Override
	public T get(int index) {
		getterCalled();
		M masterElement = masterList.get(index);
		return detailProperty.getValue(masterElement);
	}

	@Override
	public boolean add(Object o) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean addAll(Collection<? extends T> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean addAll(int index, Collection<? extends T> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean contains(Object o) {
		getterCalled();

		for (M m : masterList) {
			if (Objects.equals(detailProperty.getValue(m), o))
				return true;
		}
		return false;
	}

	@Override
	public boolean isEmpty() {
		getterCalled();
		return masterList.isEmpty();
	}

	@Override
	public boolean isStale() {
		getterCalled();
		return masterList.isStale() || staleElements != null && !staleElements.isEmpty();
	}

	@Override
	public Iterator<T> iterator() {
		getterCalled();
		return new Iterator<T>() {
			Iterator<M> it = masterList.iterator();

			@Override
			public boolean hasNext() {
				getterCalled();
				return it.hasNext();
			}

			@Override
			public T next() {
				getterCalled();
				M masterElement = it.next();
				return detailProperty.getValue(masterElement);
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	@Override
	public T move(int oldIndex, int newIndex) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean remove(Object o) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	@SuppressWarnings("unchecked")
	public Object[] toArray() {
		getterCalled();
		Object[] masterElements = masterList.toArray();
		Object[] result = new Object[masterElements.length];
		for (int i = 0; i < result.length; i++) {
			// cast is always safe as we get the array from a list of type M
			result[i] = detailProperty.getValue((M) masterElements[i]);
		}
		return result;
	}

	@Override
	@SuppressWarnings("unchecked")
	public <V> V[] toArray(V[] a) {
		getterCalled();
		Object[] masterElements = masterList.toArray();
		if (a.length < masterElements.length)
			a = (V[]) Array.newInstance(a.getClass().getComponentType(), masterElements.length);
		for (int i = 0; i < masterElements.length; i++) {
			a[i] = (V) detailProperty.getValue((M) masterElements[i]);
		}
		return a;
	}

	@Override
	public void add(int index, Object o) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void clear() {
		throw new UnsupportedOperationException();
	}

	@Override
	public ListIterator<T> listIterator() {
		return listIterator(0);
	}

	@Override
	public ListIterator<T> listIterator(final int index) {
		getterCalled();
		return new ListIterator<T>() {
			ListIterator<M> it = masterList.listIterator(index);
			M lastMasterElement;
			T lastElement;
			boolean haveIterated = false;

			@Override
			public void add(Object arg0) {
				throw new UnsupportedOperationException();
			}

			@Override
			public boolean hasNext() {
				getterCalled();
				return it.hasNext();
			}

			@Override
			public boolean hasPrevious() {
				getterCalled();
				return it.hasPrevious();
			}

			@Override
			public T next() {
				getterCalled();
				lastMasterElement = it.next();
				lastElement = detailProperty.getValue(lastMasterElement);
				haveIterated = true;
				return lastElement;
			}

			@Override
			public int nextIndex() {
				getterCalled();
				return it.nextIndex();
			}

			@Override
			public T previous() {
				getterCalled();
				lastMasterElement = it.previous();
				lastElement = detailProperty.getValue(lastMasterElement);
				haveIterated = true;
				return lastElement;
			}

			@Override
			public int previousIndex() {
				getterCalled();
				return it.previousIndex();
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}

			@Override
			public void set(T o) {
				checkRealm();
				if (!haveIterated)
					throw new IllegalStateException();

				boolean wasUpdating = updating;
				updating = true;
				try {
					// detailProperty.setValue(lastElement, o); // jpp: bug!
					detailProperty.setValue(lastMasterElement, o);
				} finally {
					updating = wasUpdating;
				}

				notifyIfChanged(lastMasterElement);

				lastElement = o;
			}
		};
	}

	private void notifyIfChanged(M masterElement) {
		if (cachedValues != null) {
			T oldValue = cachedValues.get(masterElement);
			T newValue = detailProperty.getValue(masterElement);
			if (!Objects.equals(oldValue, newValue) || staleElements.contains(masterElement)) {
				cachedValues.put(masterElement, newValue);
				staleElements.remove(masterElement);
				fireListChange(indicesOf(masterElement), oldValue, newValue);
			}
		}
	}

	private int[] indicesOf(Object masterElement) {
		List<Integer> indices = new ArrayList<>();

		for (ListIterator<M> it = ListSimpleValueObservableList.this.masterList.listIterator(); it.hasNext();) {
			if (masterElement == it.next())
				indices.add(Integer.valueOf(it.previousIndex()));
		}

		int[] result = new int[indices.size()];
		for (int i = 0; i < result.length; i++) {
			result[i] = indices.get(i).intValue();
		}
		return result;
	}

	private void fireListChange(int[] indices, T oldValue, T newValue) {
		List<ListDiffEntry<T>> differences = new ArrayList<>(indices.length * 2);
		for (int index : indices) {
			differences.add(Diffs.createListDiffEntry(index, false, oldValue));
			differences.add(Diffs.createListDiffEntry(index, true, newValue));
		}
		fireListChange(Diffs.createListDiff(differences));
	}

	@Override
	public T remove(int index) {
		throw new UnsupportedOperationException();
	}

	@Override
	public T set(int index, T o) {
		checkRealm();
		M masterElement = masterList.get(index);
		T oldValue = detailProperty.getValue(masterElement);

		boolean wasUpdating = updating;
		updating = true;
		try {
			detailProperty.setValue(masterElement, o);
		} finally {
			updating = wasUpdating;
		}

		notifyIfChanged(masterElement);

		return oldValue;
	}

	@Override
	public Object getObserved() {
		return masterList;
	}

	@Override
	public SimpleValueProperty<S, T> getProperty() {
		return detailProperty;
	}

	@Override
	public synchronized void dispose() {
		if (knownMasterElements != null) {
			knownMasterElements.clear(); // detaches listeners
			knownMasterElements.dispose();
			knownMasterElements = null;
		}

		if (masterList != null) {
			masterList.removeListChangeListener(masterListener);
			masterList = null;
		}

		masterListener = null;
		detailListener = null;
		detailProperty = null;
		cachedValues = null;
		staleElements = null;

		super.dispose();
	}
}