/*******************************************************************************
 * Copyright (c) 2009, 2014 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
 *     Winnie Lai (Texas Instruments) - Individual Element Number Format (Bug 202556)
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel.numberformat;

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

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.CountingRequestMonitor;
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.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.FormattedValueDMContext;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.FormattedValueDMData;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.IFormattedDataDMContext;
import org.eclipse.cdt.dsf.debug.ui.viewmodel.IDebugVMConstants;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfServices;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.dsf.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.IDMVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.IPropertiesUpdate;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.PropertiesUpdateStatus;
import org.eclipse.cdt.dsf.ui.viewmodel.update.ICacheEntry;
import org.eclipse.cdt.dsf.ui.viewmodel.update.ICachingVMProviderExtension2;
import org.eclipse.cdt.dsf.ui.viewmodel.update.IVMUpdatePolicyExtension;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.util.tracker.ServiceTracker;

/**
 * A helper class for View Model Node implementations that support elements
 * to be formatted using different number formats.  This object can be
 * instantiated by a VM node to retrieve formatted values from a given service
 * using given DMC type.
 * <p>
 * Note: This class is a replacement for the {@link FormattedValueVMUtil#updateFormattedValues(IPropertiesUpdate[], IFormattedValues, Class, RequestMonitor)}
 * static method.  This new implementation retrieves cached values if they are
 * available in the VM Cache.
 * </p>
 *
 * @see FormattedValueVMUtil
 * @see org.eclipse.cdt.dsf.ui.viewmodel.properties.IElementPropertiesProvider
 * @see org.eclipse.cdt.dsf.debug.service.IFormattedValues
 *
 * @since 2.0
 */
public class FormattedValueRetriever {

	private final IVMNode fNode;
	private final ICachingVMProviderExtension2 fCache;
	private final IElementFormatProvider fElementFormatProvider;
	private final ServiceTracker fServiceTracker;
	private final Class<? extends IFormattedDataDMContext> fDmcType;
	private final String fPropertyPrefix;

	private final String PROP_AVAILABLE_FORMATS;
	private final String PROP_ACTIVE_FORMAT;
	private final String PROP_ACTIVE_FORMAT_VALUE;
	private final String PROP_BASE;

	public FormattedValueRetriever(IVMNode node, DsfSession session, Class<?> serviceClass,
			Class<? extends IFormattedDataDMContext> dmcType) {
		this(node, createFilter(session, serviceClass), dmcType, null);
	}

	public FormattedValueRetriever(IVMNode node, DsfSession session, Class<?> serviceClass,
			Class<? extends IFormattedDataDMContext> dmcType, String propertyPrefix) {
		this(node, createFilter(session, serviceClass), dmcType, propertyPrefix);
	}

	public FormattedValueRetriever(IVMNode node, Filter filter, Class<? extends IFormattedDataDMContext> dmcType,
			String propertyPrefix) {
		fNode = node;
		fCache = (ICachingVMProviderExtension2) node.getVMProvider();
		IVMProvider vmprovider = fNode.getVMProvider();
		fElementFormatProvider = vmprovider instanceof IElementFormatProvider ? (IElementFormatProvider) vmprovider
				: null;
		fServiceTracker = new ServiceTracker(DsfUIPlugin.getBundleContext(), filter, null);
		fServiceTracker.open();
		fDmcType = dmcType;
		if (propertyPrefix == null) {
			propertyPrefix = ""; //$NON-NLS-1$
		}
		fPropertyPrefix = propertyPrefix;
		PROP_AVAILABLE_FORMATS = (fPropertyPrefix + IDebugVMConstants.PROP_FORMATTED_VALUE_AVAILABLE_FORMATS).intern();
		PROP_ACTIVE_FORMAT = (fPropertyPrefix + IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT).intern();
		PROP_ACTIVE_FORMAT_VALUE = (fPropertyPrefix + IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE)
				.intern();
		PROP_BASE = (fPropertyPrefix + IDebugVMConstants.PROP_FORMATTED_VALUE_BASE).intern();
	}

	/**
	 * Creates an OSGI service filter for the given service type in a given
	 * DSF session.
	 */
	private static Filter createFilter(DsfSession session, Class<?> serviceClass) {
		try {
			return DsfUIPlugin.getBundleContext()
					.createFilter(DsfServices.createServiceFilter(serviceClass, session.getId()));
		} catch (InvalidSyntaxException e) {
			throw new RuntimeException("Unable to create service filter for " + serviceClass, e); //$NON-NLS-1$
		}
	}

	public void dispose() {
		fServiceTracker.close();
	}

	/**
	 * This method fills in the formatted value properties in the given array
	 * of property update objects using data retrieved from the given
	 * formatted values service.
	 * <p>
	 * Note: The node parameter must return a <code>ICachingVMProviderExtension2</code>
	 * through its {@link IVMNode#getVMProvider()} method.
	 *
	 * @param node This method also takes an <code>IVMNode</code> parameter
	 * which allows for retrieving the format value data from the View Model
	 * cache.  If the needed value property is cached already, the cached
	 * value will be used otherwise the properties will be retrieved from the
	 * service.
	 *
	 * @param updates The array of updates to fill in information to.  This
	 * update is used to retrieve the data model context and to write the
	 * properties into. Implementation will not directly mark these updates
	 * complete, but contribute towards that end by marking [monitor] complete.
	 *
	 * @param service The service to be used to retrieve the values from.
	 *
	 * @param dmcType The class type of the data model context.  Some updates
	 * can contain multiple formatted data data model contexts, and this
	 * method assures that there is no ambiguity in which context should be
	 * used.
	 *
	 * @param rm Request monitor used to signal completion of work
	 *
	 * @since 2.2
	 */
	@ConfinedToDsfExecutor("node.getExecutor()")
	public void update(final IPropertiesUpdate updates[], final RequestMonitor rm) {
		retrieveElementActiveFormat(updates,
				new DataRequestMonitor<Map<IPropertiesUpdate, String>>(ImmediateExecutor.getInstance(), rm) {
					@Override
					protected void handleCompleted() {
						final Map<IPropertiesUpdate, String> elementFormatMap = getData();
						final Map<IPropertiesUpdate, String[]> cachedAvailableFormatsMap = calcCachedAvailableFormatsMap(
								updates);
						if ((cachedAvailableFormatsMap != null && cachedAvailableFormatsMap.size() == updates.length)) {
							// All updates were satisfied by the cache.
							doUpdateWithAvailableFormats(updates, cachedAvailableFormatsMap, elementFormatMap, rm);
						} else {
							final IFormattedValues service = (IFormattedValues) fServiceTracker.getService();
							if (service == null) {
								rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID,
										IDsfStatusConstants.REQUEST_FAILED, "Service not available " + fServiceTracker, //$NON-NLS-1$
										null));
								rm.done();
								return;
							}
							try {
								service.getExecutor().execute(new DsfRunnable() {
									@Override
									public void run() {
										retrieveAvailableFormats(
												calcOutstandingAvailableFormatsUpdates(updates,
														cachedAvailableFormatsMap),
												new DataRequestMonitor<Map<IPropertiesUpdate, String[]>>(
														fNode.getVMProvider().getExecutor(), rm) {
													@Override
													protected void handleSuccess() {
														Map<IPropertiesUpdate, String[]> availableFormatsMap;
														if (cachedAvailableFormatsMap != null) {
															availableFormatsMap = cachedAvailableFormatsMap;
															availableFormatsMap.putAll(getData());
														} else {
															availableFormatsMap = getData();
														}
														// Retrieve the formatted values now that we have the available formats (where needed).
														// Note that we are passing off responsibility of our parent monitor
														doUpdateWithAvailableFormats(updates, availableFormatsMap,
																elementFormatMap, rm);
													}
												});
									}
								});
							} catch (RejectedExecutionException e) {
								rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID,
										IDsfStatusConstants.REQUEST_FAILED,
										"Service executor shut down " + service.getExecutor(), e)); //$NON-NLS-1$
								rm.done();
							}
						}
					}
				});
	}

	private void retrieveElementActiveFormat(final IPropertiesUpdate updates[],
			final DataRequestMonitor<Map<IPropertiesUpdate, String>> rm) {
		if (fElementFormatProvider == null) {
			rm.setData(new HashMap<IPropertiesUpdate, String>(0));
			rm.done();
			return;
		}
		Map<IPropertiesUpdate, String> cachedMap = null;
		HashSet<IPropertiesUpdate> outstanding = null;
		for (IPropertiesUpdate update : updates) {
			if (isElementFormatPropertyNeeded(update) == false) {
				continue;
			}
			String active = null;
			ICacheEntry cacheEntry = fCache.getCacheEntry(fNode, update.getViewerInput(), update.getElementPath());
			if (cacheEntry != null && cacheEntry.getProperties() != null) {
				active = (String) cacheEntry.getProperties().get(PROP_ACTIVE_FORMAT);
			}
			if (active != null) {
				if (cachedMap == null) {
					cachedMap = new HashMap<>(updates.length * 4 / 3);
				}
				cachedMap.put(update, active);
			} else {
				if (outstanding == null) {
					outstanding = new HashSet<>(updates.length * 4 / 3);
				}
				outstanding.add(update);
			}
		}
		if (outstanding == null || outstanding.size() == 0) {
			rm.setData(cachedMap == null ? new HashMap<IPropertiesUpdate, String>(0) : cachedMap);
			rm.done();
			return;
		}
		if (cachedMap == null) {
			cachedMap = new HashMap<>(updates.length * 4 / 3);
		}
		final Map<IPropertiesUpdate, String> elementFormatMap = Collections.synchronizedMap(cachedMap);
		rm.setData(elementFormatMap);
		final CountingRequestMonitor countingRm = new CountingRequestMonitor(ImmediateExecutor.getInstance(), rm);
		int count = 0;
		for (final IPropertiesUpdate update : outstanding) {
			fElementFormatProvider.getActiveFormat(update.getPresentationContext(), fNode, update.getViewerInput(),
					update.getElementPath(),
					new ViewerDataRequestMonitor<String>(ImmediateExecutor.getInstance(), update) {
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								String active = this.getData();
								if (update.getProperties().contains(PROP_ACTIVE_FORMAT)) {
									update.setProperty(PROP_ACTIVE_FORMAT, active);
								}
								elementFormatMap.put(update, active);
							}
							countingRm.done();
						}
					});
			count++;
		}
		countingRm.setDoneCount(count);

	}

	/**
	 * Retrieves the <code>PROP_FORMATTED_VALUE_AVAILABLE_FORMATS</code>
	 * property for each update and returns it in a map.  The returned
	 * map may be <code>null</code> if no cache data is available.
	 *
	 * @since 2.2
	 */
	private Map<IPropertiesUpdate, String[]> calcCachedAvailableFormatsMap(IPropertiesUpdate updates[]) {
		Map<IPropertiesUpdate, String[]> cachedAvailableFormatsMap = null; // delay creating map till needed
		for (IPropertiesUpdate update : updates) {
			ICacheEntry cacheEntry = fCache.getCacheEntry(fNode, update.getViewerInput(), update.getElementPath());
			if (cacheEntry != null && cacheEntry.getProperties() != null) {
				String[] availableFormats = (String[]) cacheEntry.getProperties().get(PROP_AVAILABLE_FORMATS);
				// Add the cached entry to the cached map even if its null.  This will help keep track
				// of whether we need to call the service for data.
				if (availableFormats != null || !isAvailableFormatsPropertyNeeded(update)) {
					if (cachedAvailableFormatsMap == null) {
						cachedAvailableFormatsMap = new HashMap<>(updates.length * 4 / 3);
					}
					cachedAvailableFormatsMap.put(update, availableFormats);
					continue;
				}
			}
		}
		return cachedAvailableFormatsMap;
	}

	/**
	 * Generates a list of updates which still need the
	 * <code>PROP_FORMATTED_VALUE_AVAILABLE_FORMATS</code> property.
	 *
	 * @since 2.2
	 */
	private List<IPropertiesUpdate> calcOutstandingAvailableFormatsUpdates(IPropertiesUpdate[] updates,
			Map<IPropertiesUpdate, String[]> cachedAvailableFormatsMap) {
		if (cachedAvailableFormatsMap != null) {
			List<IPropertiesUpdate> outstandingUpdates = new ArrayList<>(
					updates.length - cachedAvailableFormatsMap.size());
			for (IPropertiesUpdate update : updates) {
				if (!cachedAvailableFormatsMap.containsKey(update)) {
					outstandingUpdates.add(update);
				}
			}
			return outstandingUpdates;
		} else {
			return Arrays.asList(updates);
		}
	}

	/**
	 * Method to retrieve available formats for each update's element (if
	 * needed). The result is returned in a map and in the
	 * update object (if requested).
	 * <p>
	 * Note that we use a synchronized map because it's updated by a request
	 * monitor with an ImmediateExecutor.
	 *
	 * @since 2.2
	 */
	@ConfinedToDsfExecutor("service.getExecutor()")
	private void retrieveAvailableFormats(final List<IPropertiesUpdate> updates,
			final DataRequestMonitor<Map<IPropertiesUpdate, String[]>> rm) {
		IFormattedValues service = (IFormattedValues) fServiceTracker.getService();
		assert service.getExecutor().isInExecutorThread();

		final Map<IPropertiesUpdate, String[]> availableFormats = Collections
				.synchronizedMap(new HashMap<IPropertiesUpdate, String[]>(updates.size() * 4 / 3));
		rm.setData(availableFormats);
		final CountingRequestMonitor countingRm = new CountingRequestMonitor(service.getExecutor(), rm);
		int count = 0;

		for (final IPropertiesUpdate update : updates) {

			if (!isAvailableFormatsPropertyNeeded(update)) {
				continue;
			}

			IFormattedDataDMContext dmc = getFormattedDataDMContext(update);
			if (dmc == null) {
				continue;
			}

			service.getAvailableFormats(dmc,
					new ViewerDataRequestMonitor<String[]>(ImmediateExecutor.getInstance(), update) {
						/**
						 * Note we don't mark the update object done, and we
						 * avoid calling our base implementation so that it
						 * doesn't either. The completion of this request is
						 * just a step in servicing the update.
						 */
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								// Set the result (available formats) into the update object if it was requested
								if (update.getProperties().contains(PROP_AVAILABLE_FORMATS)) {
									update.setProperty(PROP_AVAILABLE_FORMATS, getData());
								}

								if (getData().length != 0) {
									// also add it to the map; we'll need to access it when querying the element's value.
									availableFormats.put(update, getData());
								} else {
									update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID,
											IDsfStatusConstants.REQUEST_FAILED,
											"No number formats available for " + update.getElement(), null)); //$NON-NLS-1$
								}
							} else {
								update.setStatus(getStatus());
							}
							countingRm.done();
						}
					});
			count++;
		}
		countingRm.setDoneCount(count);

	}

	/**
	 * This method continues retrieving formatted value properties.  It is
	 * called once the available formats are calculated for each requested
	 * update.
	 *
	 * @param availableFormatsMap Prior to calling this method, the caller
	 * queries (where necessary) the formats supported by the element in each
	 * update, and it puts that information in this map. If an entry in
	 * [updates] does not appear in this map, it means that its view-model
	 * element doesn't support any formats (very unlikely), or that the
	 * available formats aren't necessary to service the properties specified
	 * in the update
	 *
	 * @since 2.2
	 */
	@ConfinedToDsfExecutor("fNode.getExecutor()")
	private void doUpdateWithAvailableFormats(IPropertiesUpdate updates[],
			final Map<IPropertiesUpdate, String[]> availableFormatsMap,
			final Map<IPropertiesUpdate, String> elementFormatMap, final RequestMonitor rm) {
		final List<IPropertiesUpdate> outstandingUpdates = new ArrayList<>(updates.length);
		final Map<IPropertiesUpdate, List<String>> requestedFormatsMap = new HashMap<>(updates.length * 4 / 3);
		final Map<IPropertiesUpdate, String> activeFormatsMap = new HashMap<>(updates.length * 4 / 3);

		for (final IPropertiesUpdate update : updates) {
			String preferredFormat = FormattedValueVMUtil.getPreferredFormat(update.getPresentationContext());
			if (update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_FORMAT_PREFERENCE)) {
				update.setProperty(IDebugVMConstants.PROP_FORMATTED_VALUE_FORMAT_PREFERENCE, preferredFormat);
			}

			final String activeFormat = calcActiveFormat(update, preferredFormat, availableFormatsMap,
					elementFormatMap);

			if (update.getProperties().contains(PROP_ACTIVE_FORMAT)) {
				assert activeFormat != null : "Our caller should have provided the available formats if this property was specified; given available formats, an 'active' nomination is guaranteed."; //$NON-NLS-1$
				update.setProperty(PROP_ACTIVE_FORMAT, activeFormat);
			}
			List<String> requestedFormats = calcRequestedFormats(update, activeFormat, availableFormatsMap.get(update));

			ICacheEntry cacheEntry = fCache.getCacheEntry(fNode, update.getViewerInput(), update.getElementPath());
			if (cacheEntry != null && cacheEntry.getProperties() != null) {
				IVMUpdatePolicyExtension updatePolicy = getVMUpdatePolicyExtension();
				Iterator<String> itr = requestedFormats.iterator();
				while (itr.hasNext()) {
					String format = itr.next();
					String formatProperty = FormattedValueVMUtil.getPropertyForFormatId(format, fPropertyPrefix);
					Object value = cacheEntry.getProperties().get(formatProperty);
					if (value != null || !canUpdateProperty(cacheEntry, updatePolicy, formatProperty)) {
						itr.remove();
						setUpdateFormatProperty(update, activeFormat, format, value);
					}
				}
			}

			if (!requestedFormats.isEmpty()) {
				outstandingUpdates.add(update);
				requestedFormatsMap.put(update, requestedFormats);
				activeFormatsMap.put(update, activeFormat);
			}
		}
		final IFormattedValues service = (IFormattedValues) fServiceTracker.getService();
		if (service == null) {
			rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED,
					"Service not available " + fServiceTracker, null)); //$NON-NLS-1$
			rm.done();
			return;
		}
		try {
			service.getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					doUpdateWithRequestedFormats(outstandingUpdates, requestedFormatsMap, activeFormatsMap, rm);
				}
			});
		} catch (RejectedExecutionException e) {
			rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED,
					"Service executor shut down " + service.getExecutor(), e)); //$NON-NLS-1$
			rm.done();
		}
	}

	private IVMUpdatePolicyExtension getVMUpdatePolicyExtension() {
		if (fCache.getActiveUpdatePolicy() instanceof IVMUpdatePolicyExtension) {
			return (IVMUpdatePolicyExtension) fCache.getActiveUpdatePolicy();
		}
		return null;
	}

	private static boolean canUpdateProperty(ICacheEntry entry, IVMUpdatePolicyExtension updatePolicy,
			String property) {
		return !entry.isDirty() || (updatePolicy != null && updatePolicy.canUpdateDirtyProperty(entry, property));
	}

	/**
	 * Retrieves the specified formatted values from the service.
	 *
	 * @param requestedFormatsMap Map containing the formats to be retrieved
	 * and filled in for each given update.
	 * @param activeFormatsMap Map containing the active format for each given
	 * update.  The active format value needs to be set in the update using the
	 * special property <code>PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE</code>.
	 *
	 * @since 2.2
	 */
	@ConfinedToDsfExecutor("service.getExecutor()")
	private void doUpdateWithRequestedFormats(List<IPropertiesUpdate> updates,
			final Map<IPropertiesUpdate, List<String>> requestedFormatsMap,
			final Map<IPropertiesUpdate, String> activeFormatsMap, final RequestMonitor monitor) {
		IFormattedValues service = (IFormattedValues) fServiceTracker.getService();
		assert service.getExecutor().isInExecutorThread();

		// Use a single counting RM for all the requested formats for each update.
		final CountingRequestMonitor countingRm = new CountingRequestMonitor(ImmediateExecutor.getInstance(), monitor);
		int count = 0;

		for (final IPropertiesUpdate update : updates) {
			IFormattedDataDMContext dmc = getFormattedDataDMContext(update);
			if (dmc == null) {
				continue;
			}

			List<String> requestedFormats = requestedFormatsMap.get(update);
			for (String requestedFormat : requestedFormats) {
				final FormattedValueDMContext formattedValueDmc = service.getFormattedValueContext(dmc,
						requestedFormat);
				service.getFormattedExpressionValue(formattedValueDmc,
						// Here also use the ViewerDataRequestMonitor in order to propagate the update's cancel request.
						// However, when operation is complete, call the counting RM's done().
						// Use an immediate executor to avoid the possibility of a rejected execution exception.
						new ViewerDataRequestMonitor<FormattedValueDMData>(ImmediateExecutor.getInstance(), update) {
							@Override
							protected void handleCompleted() {
								if (isSuccess()) {
									setUpdateFormatProperty(update, activeFormatsMap.get(update),
											formattedValueDmc.getFormatID(), getData().getFormattedValue());
								} else {
									update.setStatus(getStatus());
								}
								// Note: we must not call the update's done method, instead call counting RM done.
								countingRm.done();

							}
						});
				count++;
			}
		}
		countingRm.setDoneCount(count);
	}

	/**
	 * Determine the 'active' value format. It is the view preference if
	 * and only if the element supports it. Otherwise it is the first
	 * format supported by the element.
	 * <p>
	 * Note: If the availableFormatsMap doesn't contain the available formats
	 * for the given update, it means the update doesn't request any properties
	 * which requires the active format to be calculated.
	 *
	 * @param update Properties update to calculate the active format for.
	 * @param availableFormatsMap The map of available formats.
	 * @param elementFormatMap The map of element active format.
	 * @return The active format, or null if active format not requested in
	 * update.
	 */
	private String calcActiveFormat(IPropertiesUpdate update, String preferredFormat,
			Map<IPropertiesUpdate, String[]> availableFormatsMap, Map<IPropertiesUpdate, String> elementFormatMap) {
		String[] availableFormats = availableFormatsMap.get(update);
		if (availableFormats != null && availableFormats.length != 0) {
			String elementFormat = elementFormatMap.get(update);
			if (elementFormat != null && isFormatAvailable(elementFormat, availableFormats)) {
				return elementFormat;
			}
			if (isFormatAvailable(preferredFormat, availableFormats)) {
				return preferredFormat;
			} else {
				return availableFormats[0];
			}
		}
		return null; // null means we don't need to know what the active format is
	}

	/**
	 * Returns <code>true</code> if the given availableFormats array contains
	 * the given format.
	 */
	private boolean isFormatAvailable(String format, String[] availableFormats) {
		for (String availableFormat : availableFormats) {
			if (availableFormat.equals(format)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Service the properties that ask for the value in a specific
	 * format. If the update request contains the property
	 * PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE, and the active format
	 * has not been explicitly requested, then we need an additional
	 * iteration to provide it.
	 */
	private List<String> calcRequestedFormats(IPropertiesUpdate update, String activeFormat,
			String[] availableFormats) {
		List<String> requestedFormats = new ArrayList<>(10);

		boolean activeFormatValueHandled = false; // have we come across a specific format request that is the active format?

		for (Iterator<String> itr = update.getProperties().iterator(); itr.hasNext()
				|| (activeFormat != null && !activeFormatValueHandled);) {
			String nextFormat;
			if (itr.hasNext()) {
				String propertyName = itr.next();
				if (propertyName.startsWith(PROP_BASE)) {
					nextFormat = FormattedValueVMUtil.getFormatFromProperty(propertyName, fPropertyPrefix);
					if (nextFormat.equals(activeFormat)) {
						activeFormatValueHandled = true;
					}
					// if we know the supported formats (we may not), then no-op if this format is unsupported
					if (availableFormats != null && !isFormatAvailable(nextFormat, availableFormats)) {
						continue;
					}
				} else {
					continue;
				}
			} else {
				// the additional iteration to handle the active format
				nextFormat = activeFormat;
				activeFormatValueHandled = true;
			}
			requestedFormats.add(nextFormat);
		}
		return requestedFormats;
	}

	/**
	 * Writes the given formatted property value into the update.  It also
	 * writes the active format property if needed.
	 * <p>
	 * If the given property value is null, this method writes an error status
	 * instead.
	 */
	private void setUpdateFormatProperty(IPropertiesUpdate update, String activeFormat, String format, Object value) {
		String formatProperty = FormattedValueVMUtil.getPropertyForFormatId(format, fPropertyPrefix);
		if (value != null) {
			update.setProperty(formatProperty, value);
			if (update.getProperties().contains(PROP_ACTIVE_FORMAT_VALUE) && format.equals(activeFormat)) {
				update.setProperty(PROP_ACTIVE_FORMAT_VALUE, value);
			}
		} else {
			IStatus staleDataStatus = DsfUIPlugin.newErrorStatus(IDsfStatusConstants.INVALID_STATE,
					"Cache contains stale data. Refresh view.", null);//$NON-NLS-1$
			if (update.getProperties().contains(PROP_ACTIVE_FORMAT_VALUE) && format.equals(activeFormat)) {
				PropertiesUpdateStatus.getPropertiesStatus(update)
						.setStatus(new String[] { PROP_ACTIVE_FORMAT_VALUE, formatProperty }, staleDataStatus);
			} else {
				PropertiesUpdateStatus.getPropertiesStatus(update).setStatus(formatProperty, staleDataStatus);
			}
		}
	}

	/**
	 * For each update, query the formats available for the update's
	 * element...but only if necessary. The available formats are necessary
	 * only if the update explicitly requests that information, or if the
	 * update is asking what the active format is or is asking for the value
	 * of the element in that format. The reason we need them in the last
	 * two cases is that we can't establish the 'active' format for an
	 * element without knowing its available formats. See
	 * updateFormattedValuesWithAvailableFormats(), as that's where we make
	 * that determination.
	 * @param update
	 * @return
	 */
	private boolean isAvailableFormatsPropertyNeeded(IPropertiesUpdate update) {
		return update.getProperties().contains(PROP_AVAILABLE_FORMATS)
				|| update.getProperties().contains(PROP_ACTIVE_FORMAT)
				|| update.getProperties().contains(PROP_ACTIVE_FORMAT_VALUE);
	}

	/**
	 * For each update, query the active format for the update's
	 * element...but only if necessary. It is necessary only if the
	 * update is asking what the active format is or is asking for the value
	 * of the element in that format.
	 * @param update
	 * @return true if needed
	 */
	private boolean isElementFormatPropertyNeeded(IPropertiesUpdate update) {
		if (fElementFormatProvider == null)
			return false;
		return update.getProperties().contains(PROP_ACTIVE_FORMAT)
				|| update.getProperties().contains(PROP_ACTIVE_FORMAT_VALUE);
	}

	/**
	 * Extracts the formatted data DMC from the update.  If update doesn't
	 * contain DMC-based elemtn, it writes an error to the update and returns
	 * <code>null</code>.
	 */
	private IFormattedDataDMContext getFormattedDataDMContext(IPropertiesUpdate update) {
		IFormattedDataDMContext dmc = null;
		if (update.getElement() instanceof IDMVMContext) {
			dmc = DMContexts.getAncestorOfType(((IDMVMContext) update.getElement()).getDMContext(), fDmcType);
		}

		if (dmc == null) {
			update.setStatus(DsfUIPlugin.newErrorStatus(IDsfStatusConstants.INVALID_HANDLE,
					"Update element did not contain a valid context: " + fDmcType, null)); //$NON-NLS-1$
		}
		return dmc;
	}
}
