/*******************************************************************************
 * Copyright (c) 2004, 2015 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.viewers.deferred;



/**
 * Allows a table to be accessed from a background thread. Provides a table-like public
 * interface that can accessed from a background thread. As updates arrive from the
 * background thread, it batches and schedules updates to the real table in the UI thread.
 * This class can be used with any widget that can be wrapped in the
 * <code>AbstractVirtualTable</code> interface.
 *
 * @since 3.1
 */
/* package */ final class ConcurrentTableUpdator {
	/**
	 * Wrapper for the real table. May only be accessed in the UI thread.
	 */
	private AbstractVirtualTable table;

	/**
	 * The array of objects that have been sent to the UI. Elements are null
	 * if they either haven't been sent yet or have been scheduled for clear.
	 * Maps indices onto elements.
	 */
	private Object[] sentObjects = new Object[0];

	/**
	 * Map of elements to object indices (inverse of the knownObjects array)
	 */
	private IntHashMap knownIndices = new IntHashMap();

	/**
	 * Contains all known objects that have been sent here from the background
	 * thread.
	 */
	private Object[] knownObjects = new Object[0];

	// Minimum length for the pendingFlushes stack
	private static final int MIN_FLUSHLENGTH = 64;

	/**
	 * Array of element indices. Contains elements scheduled to be
	 * cleared. Only the beginning of the array is used. The number
	 * of used elements is stored in lastClear
	 */
	private int[] pendingClears = new int[MIN_FLUSHLENGTH];

	/**
	 * Number of pending clears in the pendingClears array (this is normally
	 * used instead of pendingClears.length since the
	 * pendingClears array is usually larger than the actual number of pending
	 * clears)
	 */
	private int lastClear = 0;

	/**
	 * Last known visible range
	 */
	private volatile Range lastRange = new Range(0,0);

	/**
	 * True iff a UI update has been scheduled
	 */
	private volatile boolean updateScheduled;

	/**
	 * True iff this object has been disposed
	 */
	private volatile boolean disposed = false;

	/**
	 * Object that holds a start index and length. Allows
	 * the visible range to be returned as an atomic operation.
	 */
	public static final class Range {
		int start = 0;
		int length = 0;

		/**
		 * @param s
		 * @param l
		 */
		public Range(int s, int l) {
			start = s;
			length = l;
		}
	}

	/**
	 * Runnable that can be posted with an asyncExec to schedule
	 * an update to the real table.
	 */
	Runnable uiRunnable = () -> {
		updateScheduled = false;
		if(!table.getControl().isDisposed()) {
			updateTable();
		}
	};

	/**
	 * Creates a new table updator
	 *
	 * @param table real table to update
	 */
	public ConcurrentTableUpdator(AbstractVirtualTable table) {
		this.table = table;
	}

	/**
	 * Cleans up the updator object (but not the table itself).
	 */
	public void dispose() {
		disposed = true;
	}

	/**
	 * True iff this object has been disposed.
	 *
	 * @return true iff dispose() has been called
	 */
	public boolean isDisposed() {
		return disposed;
	}

	/**
	 * Returns the currently visible range
	 *
	 * @return the currently visible range
	 */
	public Range getVisibleRange() {
		return lastRange;
	}

	/**
	 * Marks the given object as dirty. Will cause it to be cleared
	 * in the table.
	 *
	 * @param toFlush
	 */
	public void clear(Object toFlush) {
		synchronized(this) {
			int currentIdx = knownIndices.get(toFlush, -1);

			// If we've never heard of this object, bail out.
			if (currentIdx == -1) {
				return;
			}

			pushClear(currentIdx);
		}

	}

	/**
	 * Sets the size of the table. Called from a background thread.
	 *
	 * @param newTotal
	 */
	public void setTotalItems(int newTotal) {
		synchronized (this) {
			if (newTotal != knownObjects.length) {
				if (newTotal < knownObjects.length) {
					// Flush any objects that are being removed as a result of the resize
					for (int i = newTotal; i < knownObjects.length; i++) {
						Object toFlush = knownObjects[i];

						if (toFlush != null) {
							knownIndices.remove(toFlush);
						}
					}
				}

				int minSize = Math.min(knownObjects.length, newTotal);

				Object[] newKnownObjects = new Object[newTotal];
				System.arraycopy(knownObjects, 0, newKnownObjects, 0, minSize);
				knownObjects = newKnownObjects;

				scheduleUIUpdate();
			}
		}
	}

	/**
	 * Pushes an index onto the clear stack
	 *
	 * @param toClear row to clear
	 */
	private void pushClear(int toClear) {

		// If beyond the end of the table
		if (toClear >= sentObjects.length) {
			return;
		}

		// If already flushed or never sent
		if (sentObjects[toClear] == null) {
			return;
		}

		// Mark as flushed
		sentObjects[toClear] = null;

		if (lastClear >= pendingClears.length) {
			int newCapacity = Math.min(MIN_FLUSHLENGTH, lastClear * 2);
			int[] newPendingClears = new int[newCapacity];
			System.arraycopy(pendingClears, 0, newPendingClears, 0, lastClear);
			pendingClears = newPendingClears;
		}

		pendingClears[lastClear++] = toClear;
	}

	/**
	 * Sets the item on the given row to the given value. May be called from a background
	 * thread. Schedules a UI update if necessary
	 *
	 * @param idx row to change
	 * @param value new value for the given row
	 */
	public void replace(Object value, int idx) {
		// Keep the synchronized block as small as possible, since the UI may
		// be waiting on it.
		synchronized(this) {
			Object oldObject = knownObjects[idx];

			if (oldObject != value) {
				if (oldObject != null) {
					knownIndices.remove(oldObject);
				}

				knownObjects[idx] = value;

				if (value != null) {
					int oldIndex = knownIndices.get(value, -1);
					if (oldIndex != -1) {
						knownObjects[oldIndex] = null;
						pushClear(oldIndex);
					}

					knownIndices.put(value, idx);
				}

				pushClear(idx);

				scheduleUIUpdate();
			}
		}
	}

	/**
	 * Schedules a UI update. Has no effect if an update has already been
	 * scheduled.
	 */
	private void scheduleUIUpdate() {
		synchronized(this) {
			if (!updateScheduled) {
				updateScheduled = true;
				if(!table.getControl().isDisposed()) {
					table.getControl().getDisplay().asyncExec(uiRunnable);
				}
			}
		}
	}


	/**
	 * Called in the UI thread by a SetData callback. Refreshes the
	 * table if necessary. Returns true iff a refresh is needed.
	 * @param includeIndex the index that should be included in the visible range.
	 */
	public void checkVisibleRange(int includeIndex) {
		int start = Math.min(table.getTopIndex() - 1, includeIndex);
		int length = Math.max(table.getVisibleItemCount(), includeIndex - start);
		Range r = lastRange;

		if (start != r.start || length != r.length) {
			updateTable();
		}
	}

	/**
	 * Updates the table. Sends any unsent items in the visible range to the table,
	 * and clears any previously-visible items that have not yet been sent to the table.
	 * Must be called from the UI thread.
	 */
	private void updateTable() {

		synchronized(this) {

			// Resize the table if necessary
			if (sentObjects.length != knownObjects.length) {
				Object[] newSentObjects = new Object[knownObjects.length];
				System.arraycopy(newSentObjects, 0, sentObjects, 0,
						Math.min(newSentObjects.length, sentObjects.length));
				sentObjects = newSentObjects;
				table.setItemCount(newSentObjects.length);
			}

			// Compute the currently visible range
			int start = Math.min(table.getTopIndex(), knownObjects.length);
			int length = Math.min(table.getVisibleItemCount(), knownObjects.length - start);
			int itemCount = table.getItemCount();

			int oldStart = lastRange.start;
			int oldLen = lastRange.length;

			// Store the visible range. Do it BEFORE sending any table.clear calls,
			// since clearing a visible row will result in a SetData callback which
			// cause another table update if the visible range is different from
			// the stored values -- this could cause infinite recursion.
			lastRange = new Range(start, length);

			// Re-clear any items in the old range that were never filled in
			for(int idx = 0; idx < oldLen; idx++) {
				int row = idx + oldStart;

				// If this item is no longer visible
				if (row < itemCount && (row < start || row >= start + length)) {

					// Note: if we wanted to be really aggressive about clearing
					// items that are no longer visible, we could clear here unconditionally.
					// The current way of doing things won't clear a row if its contents are
					// up-to-date.
					if (sentObjects[row] == null) {
						table.clear(row);
					}
				}
			}

			// Process any pending clears
			if (lastClear > 0) {
				for (int i = 0; i < lastClear; i++) {
					int row = pendingClears[i];

					if (row < sentObjects.length) {
						table.clear(row);
					}
				}

				if (pendingClears.length > MIN_FLUSHLENGTH) {
					pendingClears = new int[MIN_FLUSHLENGTH];
				}
				lastClear = 0;
			}

			// Send any unsent items in the visible range
			for (int idx = 0; idx < length; idx++) {
				int row = idx + start;

				Object obj = knownObjects[row];
				if (obj != null && obj != sentObjects[idx]) {
					table.replace(obj, row);
					sentObjects[idx] = obj;
				}
			}

		}
	}

	/**
	 * Return the array of all known objects that have been sent here from the background
	 * thread.
	 * @return the array of all known objects
	 */
	public Object[] getKnownObjects() {
		return knownObjects;
	}

}
