/*******************************************************************************
 * Copyright (c) 2006, 2015 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.ui.viewmodel.datamodel;

import java.util.List;
import java.util.concurrent.RejectedExecutionException;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.Immutable;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.VMChildrenUpdate;
import org.eclipse.cdt.dsf.ui.viewmodel.VMDelta;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenCountUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IHasChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;

/**
 * View model node based on a single IDMContext type. All the elements created
 * by this node are of that type.
 *
 * @since 1.0
 */
abstract public class AbstractDMVMNode extends AbstractVMNode {

	/**
	 * IVMContext implementation used for this schema node.
	 */
	@Immutable
	protected class DMVMContext extends AbstractVMContext implements IDMVMContext {
		private final IDMContext fDmc;

		public DMVMContext(IDMContext dmc) {
			super(AbstractDMVMNode.this);
			assert dmc != null;
			fDmc = dmc;
		}

		@Override
		public IDMContext getDMContext() {
			return fDmc;
		}

		/**
		 * The IAdaptable implementation.  If the adapter is the DM context,
		 * return the context, otherwise delegate to IDMContext.getAdapter().
		 */
		@SuppressWarnings("unchecked")
		@Override
		public <T> T getAdapter(Class<T> adapter) {
			T superAdapter = super.getAdapter(adapter);
			if (superAdapter != null) {
				return superAdapter;
			} else {
				// Delegate to the Data Model to find the context.
				if (adapter.isInstance(fDmc)) {
					return (T) fDmc;
				} else {
					return fDmc.getAdapter(adapter);
				}
			}
		}

		@Override
		public boolean equals(Object other) {
			if (!(other instanceof AbstractDMVMNode.DMVMContext))
				return false;
			DMVMContext otherVmc = (DMVMContext) other;
			return AbstractDMVMNode.this.equals(otherVmc.getVMNode()) && fDmc.equals(otherVmc.fDmc);
		}

		@Override
		public int hashCode() {
			return AbstractDMVMNode.this.hashCode() + fDmc.hashCode();
		}

		@Override
		public String toString() {
			return fDmc.toString();
		}
	}

	private DsfSession fSession;

	private DsfServicesTracker fServicesTracker;

	/**
	 * Concrete class type that the elements of this schema node are based on.
	 * This type is used by the standard event processing logic to find the
	 * element in the event which is managed by this VM node.
	 *
	 * @see #getContextsForEvent(VMDelta, Object, DataRequestMonitor)
	 */
	private Class<? extends IDMContext> fDMCClassType;

	/**
	 * Constructor initializes instance data, except for the child nodes.
	 * Child nodes must be initialized by calling setChildNodes()
	 * @param session
	 * @param dmcClassType
	 * @see #setChildNodes(IVMNode[])
	 */
	public AbstractDMVMNode(AbstractDMVMProvider provider, DsfSession session,
			Class<? extends IDMContext> dmcClassType) {
		super(provider);
		fSession = session;
		fServicesTracker = new DsfServicesTracker(DsfUIPlugin.getBundleContext(), session.getId());
		fDMCClassType = dmcClassType;
	}

	@Override
	public void dispose() {
		fServicesTracker.dispose();
		super.dispose();
	}

	@Override
	public void getContextsForEvent(VMDelta parentDelta, Object event, DataRequestMonitor<IVMContext[]> rm) {
		if (event instanceof IDMEvent<?>) {
			IDMEvent<?> dmEvent = (IDMEvent<?>) event;
			IDMContext dmc = DMContexts.getAncestorOfType(dmEvent.getDMContext(), fDMCClassType);
			if (dmc != null) {
				rm.setData(new IVMContext[] { createVMContext(dmc) });
				rm.done();
				return;
			}
		}
		super.getContextsForEvent(parentDelta, event, rm);
	}

	protected AbstractDMVMProvider getDMVMProvider() {
		return (AbstractDMVMProvider) getVMProvider();
	}

	protected DsfSession getSession() {
		return fSession;
	}

	protected DsfServicesTracker getServicesTracker() {
		return fServicesTracker;
	}

	@Override
	protected boolean checkUpdate(IViewerUpdate update) {
		if (!super.checkUpdate(update))
			return false;

		// Extract the VMC from the update (whatever the update sub-class.
		Object element = update.getElement();
		if (element instanceof IDMVMContext) {
			// If update element is a DMC, check if session is still alive.
			IDMContext dmc = ((IDMVMContext) element).getDMContext();
			if (!dmc.getSessionId().equals(getSession().getId()) || !DsfSession.isSessionActive(dmc.getSessionId())) {
				handleFailedUpdate(update);
				return false;
			}
		}
		return true;
	}

	@Override
	public void update(final IHasChildrenUpdate[] updates) {
		try {
			getSession().getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					for (IHasChildrenUpdate update : updates) {
						if (!checkUpdate(update))
							continue;
						updateHasElementsInSessionThread(update);
					}
				}
			});
		} catch (RejectedExecutionException e) {
			for (IViewerUpdate update : updates) {
				handleFailedUpdate(update);
			}
		}
	}

	@ConfinedToDsfExecutor("getSession().getExecutor()")
	protected void updateHasElementsInSessionThread(final IHasChildrenUpdate update) {
		update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.NOT_SUPPORTED,
				"Not implemented, clients should call to update all children instead.", null)); //$NON-NLS-1$
		update.done();
	}

	@Override
	public void update(final IChildrenCountUpdate[] updates) {
		try {
			getSession().getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					for (IChildrenCountUpdate update : updates) {
						if (!checkUpdate(update))
							continue;
						updateElementCountInSessionThread(update);
					}
				}
			});
		} catch (RejectedExecutionException e) {
			for (IViewerUpdate update : updates) {
				handleFailedUpdate(update);
			}
		}
	}

	@ConfinedToDsfExecutor("getSession().getExecutor()")
	protected void updateElementCountInSessionThread(final IChildrenCountUpdate update) {
		update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.NOT_SUPPORTED,
				"Not implemented, clients should call to update all children instead.", null)); //$NON-NLS-1$
		update.done();
	}

	@Override
	public void update(final IChildrenUpdate[] updates) {
		try {
			getSession().getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					// After every dispatch, must check if update still valid.
					for (IChildrenUpdate update : updates) {
						if (!checkUpdate(update))
							continue;
						updateElementsInSessionThread(update);
					}
				}
			});
		} catch (RejectedExecutionException e) {
			for (IViewerUpdate update : updates) {
				handleFailedUpdate(update);
			}
		}
	}

	@ConfinedToDsfExecutor("getSession().getExecutor()")
	abstract protected void updateElementsInSessionThread(IChildrenUpdate update);

	/**
	 * Utility method that takes an array of DMC object and creates a
	 * corresponding array of IVMContext elements base on that.
	 * @param parent The parent for generated IVMContext elements.
	 * @param dmcs Array of DMC objects to build return array on.
	 * @return Array of IVMContext objects.
	 */
	protected IVMContext[] dmcs2vmcs(IDMContext[] dmcs) {
		IVMContext[] vmContexts = new IVMContext[dmcs.length];
		for (int i = 0; i < dmcs.length; i++) {
			vmContexts[i] = createVMContext(dmcs[i]);
		}
		return vmContexts;
	}

	/**
	 * Fill update request with view model contexts based on given data model contexts.
	 * Assumes that data model context elements start at index 0.
	 *
	 * @param update  the viewer update request
	 * @param dmcs  the data model contexts
	 */
	protected void fillUpdateWithVMCs(IChildrenUpdate update, IDMContext[] dmcs) {
		fillUpdateWithVMCs(update, dmcs, 0);
	}

	/**
	 * Fill update request with view model contexts based on given data model contexts.
	 *
	 * @param update  the viewer update request
	 * @param dmcs  the data model contexts
	 * @param firstIndex  the index of the first data model context
	 *
	 * @since 1.1
	 */
	protected void fillUpdateWithVMCs(IChildrenUpdate update, IDMContext[] dmcs, int firstIndex) {
		int updateIdx = update.getOffset() != -1 ? update.getOffset() : 0;
		final int endIdx = updateIdx + (update.getLength() != -1 ? update.getLength() : dmcs.length);
		int dmcIdx = updateIdx - firstIndex;
		if (dmcIdx < 0) {
			updateIdx -= dmcIdx;
			dmcIdx = 0;
		}
		while (updateIdx < endIdx && dmcIdx < dmcs.length) {
			update.setChild(createVMContext(dmcs[dmcIdx++]), updateIdx++);
		}
	}

	protected IDMVMContext createVMContext(IDMContext dmc) {
		return new DMVMContext(dmc);
	}

	/**
	 * Creates a default CompositeDMVMContext which represents the selection.
	 * This can be overridden by view model providers which for their own purposes.
	 * @param update defines the selection to be updated to
	 * @return DM Context which represent the current selection
	 */
	protected IDMContext createCompositeDMVMContext(IViewerUpdate update) {
		return new CompositeDMVMContext(update);
	}

	/**
	 * Searches for a DMC of given type in the tree path contained in given
	 * VMC.  Only a DMC in the same session will be returned.
	 * @param <V> Type of the DMC that will be returned.
	 * @param vmc VMC element to search.
	 * @param dmcType Class object for matching the type.
	 * @return DMC, or null if not found.
	 */
	protected <T extends IDMContext> T findDmcInPath(Object inputObject, TreePath path, Class<T> dmcType) {
		T retVal = null;
		for (int i = path.getSegmentCount() - 1; i >= 0; i--) {
			if (path.getSegment(i) instanceof IDMVMContext) {
				IDMContext dmc = ((IDMVMContext) path.getSegment(i)).getDMContext();
				if (dmc.getSessionId().equals(getSession().getId())) {
					retVal = DMContexts.getAncestorOfType(dmc, dmcType);
					if (retVal != null)
						break;
				}
			}
		}
		// Search the root object of the layout hierarchy.
		if (retVal == null) {
			if (inputObject instanceof ITreeSelection) {
				ITreeSelection inputSelection = (ITreeSelection) inputObject;
				if (inputSelection.getPaths().length == 1) {
					retVal = findDmcInPath(null, inputSelection.getPaths()[0], dmcType);
				}
			} else if (inputObject instanceof IStructuredSelection) {
				Object rootElement = ((IStructuredSelection) inputObject).getFirstElement();
				if (rootElement instanceof IDMVMContext) {
					retVal = DMContexts.getAncestorOfType(((IDMVMContext) rootElement).getDMContext(), dmcType);
				}
			} else if (inputObject instanceof IDMVMContext) {
				retVal = DMContexts.getAncestorOfType(((IDMVMContext) inputObject).getDMContext(), dmcType);
			}
		}

		return retVal;
	}

	/**
	 * This method looks for a specific DMC, used in a IVMNode type. If found, its index is returned, else
	 * index 0.
	 *
	 * @param nodeType the node to search on
	 * @param wantedCtx the dmc we are looking-for
	 * @param parentDelta delta for the parent VMNode
	 * @param rm request monitor
	 */
	protected void getVMCIndexForDmc(IVMNode nodetype, IDMContext wantedCtx, VMDelta parentDelta,
			DataRequestMonitor<Integer> rm) {
		final int indexFailed = 0;
		getVMProvider().updateNode(nodetype, new VMChildrenUpdate(parentDelta, getVMProvider().getPresentationContext(),
				-1, -1, new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
					@Override
					protected void handleSuccess() {
						boolean found = false;
						for (int i = 0; i < getData().size(); i++) {
							if (getData().get(i) instanceof IDMVMContext) {
								IDMVMContext vmc = (IDMVMContext) getData().get(i);
								if (vmc.getDMContext().equals(wantedCtx)) {
									rm.setData(i);
									found = true;
									break;
								}
							}
						}
						if (!found) {
							rm.setData(indexFailed);
						}
						rm.done();
					}
				}));
	}
}
