/*******************************************************************************
 * 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;
	}

}
