/*******************************************************************************
 * Copyright (c) 2006, 2012 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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;

import org.eclipse.cdt.dsf.concurrent.CountingRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
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.IColumnPresentation;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IHasChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerInputUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;

/**
 * Base implementation for View Model Adapters.  The implementation uses
 * its own single-thread executor for communicating with providers and
 * layout nodes.
 *
 * @since 1.0
 */
@ThreadSafe
abstract public class AbstractVMAdapter implements IVMAdapter {

	private boolean fDisposed;

	private final Map<IPresentationContext, IVMProvider> fViewModelProviders = Collections
			.synchronizedMap(new HashMap<IPresentationContext, IVMProvider>());

	/**
	 * Constructor for the View Model session.  It is tempting to have the
	 * adapter register itself here with the session as the model adapter, but
	 * that would mean that the adapter might get accessed on another thread
	 * even before the deriving class is fully constructed.  So it it better
	 * to have the owner of this object register it with the session.
	 * @param session
	 */
	public AbstractVMAdapter() {
	}

	@Override
	@ThreadSafe
	public IVMProvider getVMProvider(IPresentationContext context) {
		synchronized (fViewModelProviders) {
			if (fDisposed)
				return null;

			IVMProvider provider = fViewModelProviders.get(context);
			if (provider == null) {
				provider = createViewModelProvider(context);
				if (provider != null) {
					fViewModelProviders.put(context, provider);
				}
			}
			return provider;
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @since 1.1
	 */
	@Override
	public IVMProvider[] getActiveProviders() {
		synchronized (fViewModelProviders) {
			return fViewModelProviders.values().toArray(new IVMProvider[fViewModelProviders.size()]);
		}
	}

	public void dispose() {
		IVMProvider[] providers = new IVMProvider[0];
		synchronized (fViewModelProviders) {
			providers = fViewModelProviders.values().toArray(new IVMProvider[fViewModelProviders.size()]);
			fViewModelProviders.clear();
			fDisposed = true;
		}

		for (final IVMProvider provider : providers) {
			try {
				provider.getExecutor().execute(() -> provider.dispose());
			} catch (RejectedExecutionException e) {
				// Not much we can do at this point.
			}
		}
	}

	/**
	 * @return whether this VM adapter is disposed.
	 *
	 * @since 1.1
	 */
	public boolean isDisposed() {
		return fDisposed;
	}

	@Override
	public void update(IHasChildrenUpdate[] updates) {
		handleUpdate(updates);
	}

	@Override
	public void update(IChildrenCountUpdate[] updates) {
		handleUpdate(updates);
	}

	@Override
	public void update(final IChildrenUpdate[] updates) {
		handleUpdate(updates);
	}

	private void handleUpdate(IViewerUpdate[] updates) {
		IVMProvider provider = getVMProvider(updates[0].getPresentationContext());
		if (provider != null) {
			updateProvider(provider, updates);
		} else {
			for (IViewerUpdate update : updates) {
				update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
						"No model provider for update " + update, null)); //$NON-NLS-1$
				update.done();
			}
		}
	}

	private void updateProvider(final IVMProvider provider, final IViewerUpdate[] updates) {
		try {
			provider.getExecutor().execute(() -> {
				if (updates instanceof IHasChildrenUpdate[]) {
					provider.update((IHasChildrenUpdate[]) updates);
				} else if (updates instanceof IChildrenCountUpdate[]) {
					provider.update((IChildrenCountUpdate[]) updates);
				} else if (updates instanceof IChildrenUpdate[]) {
					provider.update((IChildrenUpdate[]) updates);
				}
			});
		} catch (RejectedExecutionException e) {
			for (IViewerUpdate update : updates) {
				update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
						"Display is disposed, cannot complete update " + update, null)); //$NON-NLS-1$
				update.done();
			}
		}
	}

	@Override
	public IModelProxy createModelProxy(Object element, IPresentationContext context) {
		IVMProvider provider = getVMProvider(context);
		if (provider != null) {
			return provider.createModelProxy(element, context);
		}
		return null;
	}

	@Override
	public IColumnPresentation createColumnPresentation(IPresentationContext context, Object element) {
		final IVMProvider provider = getVMProvider(context);
		if (provider != null) {
			return provider.createColumnPresentation(context, element);
		}
		return null;
	}

	@Override
	public String getColumnPresentationId(IPresentationContext context, Object element) {
		final IVMProvider provider = getVMProvider(context);
		if (provider != null) {
			return provider.getColumnPresentationId(context, element);
		}
		return null;
	}

	@Override
	public void update(IViewerInputUpdate update) {
		final IVMProvider provider = getVMProvider(update.getPresentationContext());
		if (provider != null) {
			provider.update(update);
		} else {
			update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, "not supported")); //$NON-NLS-1$
			update.done();
		}
	}

	/**
	 * Creates a new View Model Provider for given presentation context.  Returns null
	 * if the presentation context is not supported.
	 */
	@ThreadSafe
	abstract protected IVMProvider createViewModelProvider(IPresentationContext context);

	/**
	 * Dispatch given event to VM providers interested in events.
	 *
	 * @since 1.1
	 */
	protected final void handleEvent(final Object event) {
		final List<IVMEventListener> eventListeners = new ArrayList<>();

		aboutToHandleEvent(event);

		for (IVMProvider vmProvider : getActiveProviders()) {
			if (vmProvider instanceof IVMEventListener) {
				eventListeners.add((IVMEventListener) vmProvider);
			}
		}

		if (!eventListeners.isEmpty()) {
			final CountingRequestMonitor crm = new CountingRequestMonitor(ImmediateExecutor.getInstance(), null) {
				@Override
				protected void handleCompleted() {
					if (isDisposed()) {
						return;
					}
					doneHandleEvent(event);
				}
			};

			int count = 0;
			for (final IVMEventListener vmEventListener : eventListeners) {
				RequestMonitor listenerRm = null;
				if (vmEventListener.shouldWaitHandleEventToComplete()) {
					listenerRm = crm;
					count++;
				} else {
					// Create a dummy request monitor for the handling of this event.
					listenerRm = new RequestMonitor(ImmediateExecutor.getInstance(), null);
				}
				final RequestMonitor finalListenerRm = listenerRm;
				vmEventListener.getExecutor().execute(new DsfRunnable() {
					@Override
					public void run() {
						vmEventListener.handleEvent(event, finalListenerRm);
					}
				});
			}
			crm.setDoneCount(count);
		} else {
			doneHandleEvent(event);
		}
	}

	/**
	 * Given event is about to be handled.
	 *
	 * @param event
	 *
	 * @since 1.1
	 */
	protected void aboutToHandleEvent(final Object event) {
	}

	/**
	 * Given event has been processed by all VM event listeners.
	 *
	 * @param event
	 *
	 * @since 1.1
	 */
	protected void doneHandleEvent(final Object event) {
	}
}
