/*******************************************************************************
 * Copyright (c) 2008, 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.debug.ui.viewmodel.breakpoints;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.ModelProxyInstalledEvent;
import org.eclipse.cdt.dsf.ui.viewmodel.VMChildrenUpdate;
import org.eclipse.cdt.dsf.ui.viewmodel.VMDelta;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointUIConstants;
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.IModelDelta;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.contexts.DebugContextEvent;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IWorkbenchWindow;

/**
 * Base class for breakpoint VM Nodes.  Concrete implementations must
 * implement the breakpoint object to be populated into the view.
 * Also this node only implements the content provider so sub-classes
 * must also implement a label provider, element editor, etc.
 *
 * @since 2.1
 */
public abstract class AbstractBreakpointVMNode extends AbstractVMNode {

	public AbstractBreakpointVMNode(BreakpointVMProvider provider) {
		super(provider);
	}

	/**
	 * Class that creates the element object for the corresponding breakpoints.
	 * This element object will be populated in the breakpoints view to represent
	 * the given breakpoint.
	 */
	abstract protected Object createBreakpiontElement(IBreakpoint bp);

	@Override
	public void update(final IHasChildrenUpdate[] updates) {
		for (final IHasChildrenUpdate update : updates) {
			if (!checkUpdate(update))
				continue;
			((BreakpointVMProvider) getVMProvider()).getNestingCategoryBreakpoints(update.getElementPath(),
					new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								update.setHasChilren(getData().length != 0);
							} else {
								update.setHasChilren(false);
							}
							update.done();
						}
					});
		}
	}

	@Override
	public void update(final IChildrenCountUpdate[] updates) {
		for (final IChildrenCountUpdate update : updates) {
			if (!checkUpdate(update))
				continue;
			((BreakpointVMProvider) getVMProvider()).getNestingCategoryBreakpoints(update.getElementPath(),
					new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								update.setChildCount(getData().length);
							} else {
								update.setChildCount(0);
							}
							update.done();
						}
					});
		}
	}

	@Override
	public void update(IChildrenUpdate[] updates) {
		for (final IChildrenUpdate update : updates) {
			if (!checkUpdate(update))
				continue;
			((BreakpointVMProvider) getVMProvider()).getNestingCategoryBreakpoints(update.getElementPath(),
					new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								@SuppressWarnings("unchecked")
								Comparator<Object> comparator = (Comparator<Object>) getVMProvider()
										.getPresentationContext()
										.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR);
								if (comparator != null) {
									Arrays.sort(getData(), comparator);
								}
								fillUpdateWithBreakpointElements(update, getData());
							}
							update.done();
						}
					});
		}
	}

	private void fillUpdateWithBreakpointElements(IChildrenUpdate update, IBreakpoint[] bps) {
		int updateIdx = update.getOffset() != -1 ? update.getOffset() : 0;
		int endIdx = updateIdx + (update.getLength() != -1 ? update.getLength() : bps.length);
		while (updateIdx < endIdx && updateIdx < bps.length) {
			update.setChild(createBreakpiontElement(bps[updateIdx]), updateIdx);
			updateIdx++;
		}
	}

	@Override
	public int getDeltaFlags(Object event) {
		if (event instanceof BreakpointsChangedEvent) {
			BreakpointsChangedEvent bpChangedEvent = ((BreakpointsChangedEvent) event);
			if (BreakpointsChangedEvent.Type.ADDED.equals(bpChangedEvent.getType())) {
				return IModelDelta.CONTENT | IModelDelta.SELECT | IModelDelta.EXPAND;
			}
			return IModelDelta.CONTENT;
		} else if (BreakpointVMProvider.isPresentationContextEvent(event)) {
			PropertyChangeEvent propertyEvent = (PropertyChangeEvent) event;
			if (IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION.equals(propertyEvent.getProperty())
					|| IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR.equals(propertyEvent.getProperty())) {
				return IModelDelta.CONTENT;
			} else if (IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS.equals(propertyEvent.getProperty())) {
				return IModelDelta.EXPAND | IModelDelta.CONTENT;
			} else if (IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION.equals(propertyEvent.getProperty())
					&& Boolean.TRUE.equals(propertyEvent.getNewValue())) {
				return IModelDelta.EXPAND | IModelDelta.SELECT;
			}
		} else if (event instanceof DebugContextEvent
				&& (((DebugContextEvent) event).getFlags() & DebugContextEvent.ACTIVATED) != 0) {
			int flags = IModelDelta.NO_CHANGE;
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION))) {
				flags |= IModelDelta.CONTENT;
			}
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION))) {
				flags |= IModelDelta.EXPAND | IModelDelta.SELECT;
			}
			return flags;
		} else if (event instanceof ModelProxyInstalledEvent) {
			// Upon model proxy installed, check whether we need to select a
			// breakpoint in linking with Debug view
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION))) {
				return IModelDelta.EXPAND | IModelDelta.SELECT;
			}
		}
		return 0;
	}

	@Override
	public void buildDelta(Object event, VMDelta parent, int nodeOffset, RequestMonitor rm) {
		if (event instanceof BreakpointsChangedEvent) {
			BreakpointsChangedEvent bpChangedEvent = ((BreakpointsChangedEvent) event);
			if (BreakpointsChangedEvent.Type.ADDED.equals(bpChangedEvent.getType())) {
				buildBreakpointAddedDelta(bpChangedEvent, parent, nodeOffset, rm);
				// Do not call rm.done() in this method!
				return;
			} else {
				parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
			}
		} else if (BreakpointVMProvider.isPresentationContextEvent(event)) {
			PropertyChangeEvent propertyEvent = (PropertyChangeEvent) event;
			if (IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION.equals(propertyEvent.getProperty())
					|| IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR.equals(propertyEvent.getProperty())) {
				parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
			} else if (IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS.equals(propertyEvent.getProperty())) {
				parent.setFlags(parent.getFlags() | IModelDelta.EXPAND | IModelDelta.CONTENT);
			} else if (IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION.equals(propertyEvent.getProperty())
					&& Boolean.TRUE.equals(propertyEvent.getNewValue())) {
				IWorkbenchWindow window = getVMProvider().getPresentationContext().getWindow();
				if (window != null) {
					ISelection activeContext = DebugUITools.getDebugContextManager().getContextService(window)
							.getActiveContext();
					buildTrackSelectionDelta(activeContext, parent, nodeOffset, rm);
					// Do not call rm.done() in this method!
					return;
				}
			}
		} else if (event instanceof DebugContextEvent
				&& (((DebugContextEvent) event).getFlags() & DebugContextEvent.ACTIVATED) != 0) {
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION))) {
				parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
			}
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION))) {
				buildTrackSelectionDelta(((DebugContextEvent) event).getContext(), parent, nodeOffset, rm);
				// Do not call rm.done() in this method!
				return;
			}
		} else if (event instanceof ModelProxyInstalledEvent) {
			if (Boolean.TRUE.equals(getVMProvider().getPresentationContext()
					.getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION))) {
				IWorkbenchWindow window = getVMProvider().getPresentationContext().getWindow();
				if (window != null) {
					ISelection activeContext = DebugUITools.getDebugContextManager().getContextService(window)
							.getActiveContext();
					buildTrackSelectionDelta(activeContext, parent, nodeOffset, rm);
					return;
				}
			}
		}

		rm.done();
	}

	private void buildBreakpointAddedDelta(final BreakpointsChangedEvent event, final VMDelta parent,
			final int nodeOffset, final RequestMonitor rm) {
		getVMProvider().updateNode(this, new VMChildrenUpdate(parent, getVMProvider().getPresentationContext(), -1, -1,
				new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
					@Override
					protected void handleSuccess() {
						for (int i = 0; i < event.getBreakpoints().length; i++) {
							IBreakpoint eventBp = event.getBreakpoints()[i];
							int bpIndex = findBreakpointIndex(eventBp, getData());
							if (bpIndex >= 0) {
								// Select only the first breakpoint that was added
								if (i == 0) {
									parent.addNode(getData().get(bpIndex), bpIndex, IModelDelta.SELECT);
								}
								// For all other added breakpoints, expand the parent.
								parent.setFlags(parent.getFlags() | IModelDelta.CONTENT | IModelDelta.EXPAND);
							}
						}
						rm.done();
					}
				}));
	}

	protected void buildTrackSelectionDelta(ISelection debugContext, final VMDelta parent, final int nodeOffset,
			final RequestMonitor rm) {
		assert getVMProvider() instanceof BreakpointVMProvider;

		((BreakpointVMProvider) getVMProvider()).getBreakpointsForDebugContext(debugContext,
				new DataRequestMonitor<IBreakpoint[]>(getExecutor(), rm) {
					@Override
					protected void handleSuccess() {
						if (getData().length == 0) {
							// No breakpoints to select, we're done.
							rm.done();
							return;
						}
						final IBreakpoint[] bpsToSelect = getData();

						getVMProvider().updateNode(AbstractBreakpointVMNode.this,
								new VMChildrenUpdate(parent, getVMProvider().getPresentationContext(), -1, -1,
										new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
											@Override
											protected void handleSuccess() {
												for (int i = 0; i < bpsToSelect.length; i++) {
													int bpIndex = findBreakpointIndex(bpsToSelect[i], getData());
													if (bpIndex >= 0) {
														// Select only the first breakpoint that was added
														if (i == 0) {
															parent.addNode(getData().get(bpIndex), bpIndex,
																	IModelDelta.SELECT);
														}
														// For all other added breakpoints, expand the parent.
														parent.setFlags(parent.getFlags() | IModelDelta.EXPAND);
													}
												}
												rm.done();
											}
										}));
					}

					@Override
					protected void handleErrorOrWarning() {
						rm.done();
					}
				});
	}

	private int findBreakpointIndex(IBreakpoint bp, List<Object> bpElements) {
		for (int j = 0; j < bpElements.size(); j++) {
			IBreakpoint elementBp = (IBreakpoint) DebugPlugin.getAdapter(bpElements.get(j), IBreakpoint.class);
			if (elementBp != null && elementBp.equals(bp)) {
				return j;
			}
		}
		return -1;
	}
}
