/*******************************************************************************
 * Copyright (c) 2006, 2018 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.debug.internal.ui.viewers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.internal.ui.elements.adapters.AsynchronousDebugLabelAdapter;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxyFactory;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IStatusMonitor;
import org.eclipse.debug.internal.ui.viewers.provisional.AsynchronousContentAdapter;
import org.eclipse.debug.internal.ui.viewers.provisional.IAsynchronousContentAdapter;
import org.eclipse.debug.internal.ui.viewers.provisional.IAsynchronousLabelAdapter;
import org.eclipse.debug.internal.ui.viewers.provisional.IChildrenRequestMonitor;
import org.eclipse.debug.internal.ui.viewers.provisional.ILabelRequestMonitor;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;

/**
 * Model for an asynchronous viewer
 *
 * @since 3.2
 */
public abstract class AsynchronousModel {

	private ModelNode fRoot; // root node
	private Map<Object, ModelNode[]> fElementToNodes = new HashMap<>(); // map
																							// of
																							// element
																							// to
																							// corresponding
																							// tree
																							// nodes
																							// (list)
	private Map<Object, IModelProxy> fModelProxies = new HashMap<>(); // map
																							// of
																							// installed
																							// model
																							// proxies,
																							// by
																							// element
	private AsynchronousViewer fViewer; // viewer this model works for
	private boolean fDisposed = false; // whether disposed

	class EmptyContentAdapter extends AsynchronousContentAdapter {

		@Override
		protected Object[] getChildren(Object parent, IPresentationContext context) throws CoreException {
			return EMPTY;
		}

		@Override
		protected boolean hasChildren(Object element, IPresentationContext context) throws CoreException {
			return false;
		}

		@Override
		protected boolean supportsPartId(String id) {
			return true;
		}
	}

	protected IAsynchronousContentAdapter fEmptyContentAdapter = new EmptyContentAdapter();

	/**
	 * List of requests currently being performed.
	 */
	private List<IStatusMonitor> fPendingUpdates = new ArrayList<>();

	/**
	 * List of pending viewer updates
	 */
	private List<IStatusMonitor> fViewerUpdates = new ArrayList<>();

	/**
	 * Constructs a new empty tree model
	 *
	 * @param viewer associated viewer
	 */
	public AsynchronousModel(AsynchronousViewer viewer) {
		fViewer = viewer;
		if (DebugUIPlugin.DEBUG_MODEL) {
			StringBuilder buffer = new StringBuilder();
			buffer.append("MODEL CREATED for: "); //$NON-NLS-1$
			buffer.append(fViewer);
			buffer.append(" ("); //$NON-NLS-1$
			buffer.append(this);
			buffer.append(")"); //$NON-NLS-1$
			DebugUIPlugin.trace(buffer.toString());
		}
	}

	/**
	 * Initializes this model. Called once after creation.
	 *
	 * @param root root element or <code>null</code>
	 */
	public void init(Object root) {
		if (root != null) {
			fRoot = new ModelNode(null, root);
			mapElement(root, fRoot);
		}
	}

	protected AsynchronousViewer getViewer() {
		return fViewer;
	}

	/**
	 * Disposes this model
	 */
	public synchronized void dispose() {
		if (DebugUIPlugin.DEBUG_MODEL) {
			StringBuilder buffer = new StringBuilder();
			buffer.append("MODEL DISPOSED for: "); //$NON-NLS-1$
			buffer.append(fViewer);
			buffer.append(" ("); //$NON-NLS-1$
			buffer.append(this);
			buffer.append(")"); //$NON-NLS-1$
			DebugUIPlugin.trace(buffer.toString());
		}
		fDisposed = true;
		cancelPendingUpdates();
		disposeAllModelProxies();
		ModelNode rootNode = getRootNode();
		if (rootNode != null) {
			rootNode.dispose();
		}
		fElementToNodes.clear();
	}

	/**
	 * Returns whether this model has been disposed
	 * @return <code>true</code> if the model is disposed <code>false</code> otherwise
	 */
	public synchronized boolean isDisposed() {
		return fDisposed;
	}

	/**
	 * Cancels all pending update requests.
	 */
	protected synchronized void cancelPendingUpdates() {
		Iterator<IStatusMonitor> updates = fPendingUpdates.iterator();
		while (updates.hasNext()) {
			IStatusMonitor update = updates.next();
			updates.remove();
			update.setCanceled(true);
		}
		fPendingUpdates.clear();
	}

	/**
	 * Installs the model proxy for the given element into this viewer
	 * if not already installed.
	 *
	 * @param element element to install an update policy for
	 */
	public synchronized void installModelProxy(Object element) {
		if (!fModelProxies.containsKey(element)) {
			IModelProxyFactory modelProxyFactory = getModelProxyFactoryAdapter(element);
			if (modelProxyFactory != null) {
				final IModelProxy proxy = modelProxyFactory.createModelProxy(element, getPresentationContext());
				if (proxy != null) {
					fModelProxies.put(element, proxy);
					Job job = new Job("Model Proxy installed notification job") {//$NON-NLS-1$
						@Override
						protected IStatus run(IProgressMonitor monitor) {
							if (!monitor.isCanceled()) {
								proxy.init(getPresentationContext());
								getViewer().modelProxyAdded(proxy);
								proxy.installed(getViewer());
							}
							return Status.OK_STATUS;
						}
					};
					job.setSystem(true);
					job.schedule();
				}
			}
		}
	}

	/**
	 * Uninstalls the model proxy installed for the given element, if any.
	 *
	 * @param element the element context
	 */
	protected synchronized void disposeModelProxy(Object element) {
		IModelProxy proxy = fModelProxies.remove(element);
		if (proxy != null) {
			getViewer().modelProxyRemoved(proxy);
			proxy.dispose();
		}
	}

	/**
	 * Unintalls all model proxies installed for this model
	 */
	private void disposeAllModelProxies() {
		synchronized(fModelProxies) {
			for (IModelProxy proxy : fModelProxies.values()) {
				getViewer().modelProxyRemoved(proxy);
				proxy.dispose();
			}
			fModelProxies.clear();
		}
	}

	/**
	 * Returns the presentation this model is installed in
	 *
	 * @return the presentation context from the backing viewer
	 */
	protected IPresentationContext getPresentationContext() {
		return fViewer.getPresentationContext();
	}

	/**
	 * Returns the model proxy factory for the given element of <code>null</code> if none.
	 *
	 * @param element element to retrieve adapters for
	 * @return model proxy factory adapter or <code>null</code>
	 */
	protected IModelProxyFactory getModelProxyFactoryAdapter(Object element) {
		IModelProxyFactory adapter = null;
		if (element instanceof IModelProxyFactory) {
			adapter = (IModelProxyFactory) element;
		} else if (element instanceof IAdaptable) {
			IAdaptable adaptable = (IAdaptable) element;
			adapter = adaptable.getAdapter(IModelProxyFactory.class);
		}
		return adapter;
	}

	/**
	 * Maps the given element to the given node.
	 *
	 * @param element the element context
	 * @param node the model node
	 */
	protected synchronized void mapElement(Object element, ModelNode node) {
		ModelNode[] nodes = getNodes(element);
		node.remap(element);
		if (nodes == null) {
			fElementToNodes.put(element, new ModelNode[] { node});
		} else {
			for (ModelNode mnode : nodes) {
				if (mnode == node) {
					return;
				}
			}
			ModelNode[] old = nodes;
			ModelNode[] newNodes = new ModelNode[old.length + 1];
			System.arraycopy(old, 0, newNodes, 0, old.length);
			newNodes[old.length] = node;
			fElementToNodes.put(element, newNodes);
		}
		installModelProxy(element);
	}

	/**
	 * Unmaps the given node from its element and widget.
	 *
	 * @param node the model node
	 */
	protected synchronized void unmapNode(ModelNode node) {
		Object element = node.getElement();
		ModelNode[] nodes = fElementToNodes.get(element);
		if (nodes == null) {
			return;
		}
		if (nodes.length == 1) {
			fElementToNodes.remove(element);
			disposeModelProxy(element);
		} else {
			for (int i = 0; i < nodes.length; i++) {
				ModelNode node2 = nodes[i];
				if (node2 == node) {
					ModelNode[] newNodes= new ModelNode[nodes.length - 1];
					System.arraycopy(nodes, 0, newNodes, 0, i);
					if (i < newNodes.length) {
						System.arraycopy(nodes, i + 1, newNodes, i, newNodes.length - i);
					}
					fElementToNodes.put(element, newNodes);
				}
			}
		}
	}

	/**
	 * Returns the nodes in this model for the given element or
	 * <code>null</code> if none.
	 *
	 * @param element model element
	 * @return associated nodes or <code>null</code>
	 */
	public synchronized ModelNode[] getNodes(Object element) {
		return fElementToNodes.get(element);
	}

	/**
	 * Returns the root node or <code>null</code>
	 *
	 * @return the root node or <code>null</code>
	 */
	public ModelNode getRootNode() {
		return fRoot;
	}

	/**
	 * Cancels any conflicting updates for children of the given item, and
	 * schedules the new update.
	 *
	 * @param update the update to schedule
	 */
	protected void requestScheduled(IStatusMonitor update) {
		AsynchronousRequestMonitor absUpdate = (AsynchronousRequestMonitor) update;
		synchronized (fPendingUpdates) {
			ListIterator<IStatusMonitor> updates = fPendingUpdates.listIterator();
			while (updates.hasNext()) {
				AsynchronousRequestMonitor pendingUpdate = (AsynchronousRequestMonitor) updates.next();
				if (absUpdate.contains(pendingUpdate)) {
					updates.remove();
					pendingUpdate.setCanceled(true);
				}
			}
			fPendingUpdates.add(update);
		}
	}

	/**
	 * Removes the update from the pending updates list.
	 *
	 * @param update the update to remove
	 */
	protected void requestComplete(IStatusMonitor update) {
		synchronized (fPendingUpdates) {
			fPendingUpdates.remove(update);
		}
	}

	/**
	 * An viewer update has been scheduled due to the following update request.
	 *
	 * @param update the update to add
	 */
	protected void viewerUpdateScheduled(IStatusMonitor update) {
		// synch viewer updates and pending updates on same lock - fPendingUpdates
		synchronized (fPendingUpdates) {
			fViewerUpdates.add(update);
		}
	}

	/**
	 * Returns the result of running the given elements through the
	 * viewers filters.
	 *
	 * @param parent parent element
	 * @param elements the elements to filter
	 * @return only the elements which all filters accept
	 */
	protected Object[] filter(Object parent, Object[] elements) {
		ViewerFilter[] filters = getViewer().getFilters();
		if (filters != null) {
			ArrayList<Object> filtered = new ArrayList<>(elements.length);
			for (Object element : elements) {
				boolean add = true;
				for (ViewerFilter filter : filters) {
					add = filter.select(getViewer(), parent, element);
					if (!add) {
						break;
					}
				}
				if (add) {
					filtered.add(element);
				}
			}
			return filtered.toArray();
		}
		return elements;
	}

	/**
	 * Refreshes the given node.
	 *
	 * @param node the model node to update
	 */
	protected void updateLabel(ModelNode node) {
		Object element = node.getElement();
		IAsynchronousLabelAdapter adapter = getLabelAdapter(element);
		if (adapter != null) {
			ILabelRequestMonitor labelUpdate = new LabelRequestMonitor(node, this);
			requestScheduled(labelUpdate);
			adapter.retrieveLabel(element, getPresentationContext(), labelUpdate);
		}
	}

	/**
	 * Returns the label adapter for the given element or <code>null</code> if none.
	 *
	 * @param element element to retrieve adapter for
	 * @return presentation adapter or <code>null</code>
	 */
	protected IAsynchronousLabelAdapter getLabelAdapter(Object element) {
		IAsynchronousLabelAdapter adapter = null;
		if (element instanceof IAsynchronousLabelAdapter) {
			adapter = (IAsynchronousLabelAdapter) element;
		} else if (element instanceof IAdaptable) {
			IAdaptable adaptable = (IAdaptable) element;
			adapter = adaptable.getAdapter(IAsynchronousLabelAdapter.class);
		}
		// if no adapter, use default (i.e. model presentation)
		if (adapter == null) {
			return new AsynchronousDebugLabelAdapter();
		}
		return adapter;
	}

	/**
	 * Returns the tree element adapter for the given element or
	 * <code>null</code> if none.
	 *
	 * @param element
	 *            element to retrieve adapter for
	 * @return presentation adapter or <code>null</code>
	 */
	protected IAsynchronousContentAdapter getContentAdapter(Object element) {
		IAsynchronousContentAdapter adapter = null;
		if (element instanceof IAsynchronousContentAdapter) {
			adapter = (IAsynchronousContentAdapter) element;
		} else if (element instanceof IAdaptable) {
			IAdaptable adaptable = (IAdaptable) element;
			adapter = adaptable.getAdapter(IAsynchronousContentAdapter.class);
		}
		return adapter;
	}

	/**
	 * Updates the children of the given node.
	 *
	 * @param parent
	 *            node of which to update children
	 */
	public void updateChildren(ModelNode parent) {
		Object element = parent.getElement();
		IAsynchronousContentAdapter adapter = getContentAdapter(element);
		if (adapter == null) {
			adapter = fEmptyContentAdapter;
		}
		if (adapter != null) {
			IChildrenRequestMonitor update = new ChildrenRequestMonitor(parent, this);
			requestScheduled(update);
			adapter.retrieveChildren(element, getPresentationContext(), update);
		}
	}

	/**
	 * Update this model's viewer preserving its selection.
	 *
	 * @param update the update code to run in the backing viewer's preserving selection method
	 */
	protected void preservingSelection(Runnable update) {
		getViewer().preservingSelection(update);
	}

	/**
	 * The viewer updated associated with a request is complete.
	 *
	 * @param monitor the status to remove and complete
	 */
	protected void viewerUpdateComplete(IStatusMonitor monitor) {
		// synch viewer updates and pending updates on same lock - fPendingUpdates
		synchronized (fPendingUpdates) {
			fViewerUpdates.remove(monitor);
		}
		getViewer().updateComplete(monitor);
	}

	/**
	 * An update request was cancelled
	 *
	 * @param monitor the monitor to remove
	 */
	protected void requestCanceled(AsynchronousRequestMonitor monitor) {
		synchronized (fPendingUpdates) {
			fPendingUpdates.remove(monitor);
		}
	}

	/**
	 * Whether any updates are still in progress in the model or against the viewer.
	 *
	 * @return <code>true</code> if there are pending changes <code>false</code> otherwise
	 */
	protected boolean hasPendingUpdates() {
		synchronized (fViewerUpdates) {
			return !fPendingUpdates.isEmpty() || !fViewerUpdates.isEmpty();
		}
	}

	/**
	 * Asynchronous update for add/set children request.
	 *
	 * @param parent the parent model node
	 * @param element the element context
	 */
	protected abstract void add(ModelNode parent, Object element);

	/**
	 * Notification from children request monitor
	 *
	 * @param parentNode parent node
	 * @param kids list of model elements
	 */
	protected void setChildren(final ModelNode parentNode, List<Object> kids) {
		final Object[] children = filter(parentNode.getElement(), kids.toArray());
		final AsynchronousViewer viewer = getViewer();
		ViewerComparator comparator = viewer.getComparator();
		if (comparator != null) {
			comparator.sort(viewer, children);
		}

		ModelNode[] prevKids = null;
		ModelNode[] newChildren = null;
		ModelNode[] unmap = null;

		synchronized (this) {
			if (isDisposed()) {
				return;
			}
			prevKids = parentNode.getChildrenNodes();
			if (prevKids == null) {
				newChildren = new ModelNode[children.length];
				for (int i = 0; i < children.length; i++) {
					ModelNode node = new ModelNode(parentNode, children[i]);
					mapElement(children[i], node);
					newChildren[i] = node;
				}
				parentNode.setChildren(newChildren);
			} else {
				newChildren = new ModelNode[children.length];
				unmap = new ModelNode[prevKids.length];
				for (int i = 0; i < prevKids.length; i++) {
					unmap[i] = prevKids[i];
				}
				for (int i = 0; i < children.length; i++) {
					Object child = children[i];
					boolean found = false;
					for (int j = 0; j < prevKids.length; j++) {
						ModelNode prevKid = prevKids[j];
						if (prevKid != null && child.equals(prevKid.getElement())) {
							newChildren[i] = prevKid;
							prevKids[j] = null;
							found = true;
							break;
						}
					}
					if (!found) {
						newChildren[i] = new ModelNode(parentNode, child);
						mapElement(child, newChildren[i]);
					}
				}
				for (ModelNode kid : prevKids) {
					if (kid != null) {
						kid.dispose();
						unmapNode(kid);
					}
				}
				parentNode.setChildren(newChildren);
			}
			if (DebugUIPlugin.DEBUG_MODEL) {
				DebugUIPlugin.trace("CHILDREN CHANGED: " + parentNode); //$NON-NLS-1$
				DebugUIPlugin.trace(toString());
			}
		}

		//update viewer outside the lock
		final ModelNode[] finalUnmap = unmap;
		preservingSelection(() -> {
			if (finalUnmap != null) {
				for (ModelNode element : finalUnmap) {
					viewer.unmapNode(element);
				}
			}
			viewer.nodeChildrenChanged(parentNode);
		});

	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		if (fRoot != null) {
			buf.append("ROOT: "); //$NON-NLS-1$
			append(buf, fRoot, 0);
		} else {
			buf.append("ROOT: null"); //$NON-NLS-1$
		}
		return buf.toString();
	}

	private void append(StringBuilder buf, ModelNode node, int level) {
		for (int i = 0; i < level; i++) {
			buf.append('\t');
		}
		buf.append(node);
		buf.append('\n');
		ModelNode[] childrenNodes = node.getChildrenNodes();
		if (childrenNodes != null) {
			for (ModelNode childNode : childrenNodes) {
				append(buf, childNode, level + 1);
			}
		}
	}
}
