/*******************************************************************************
 * Copyright (c) 2009, 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.numberformat;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.CountingRequestMonitor;
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.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.IDMVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.IPropertiesUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;

import com.ibm.icu.text.MessageFormat;

/**
 * A helper class for View Model Node implementations that support elements
 * to be formatted using different number formats.  The various static methods in
 * this class handle populating the properties of an IPropertiesUpdate using data
 * retrieved from a DSF service implementing {@link IFormattedValues} interface.
 *
 * @see org.eclipse.cdt.dsf.ui.viewmodel.properties.IElementPropertiesProvider
 * @see org.eclipse.cdt.dsf.debug.service.IFormattedValues
 *
 * @since 2.0
 */
public class FormattedValueVMUtil {

	/**
	 * Cache to avoid creating many duplicate strings of formats and properties.
	 */
	private static Map<String, Map<String, String>> fFormatProperties = Collections
			.synchronizedMap(new TreeMap<String, Map<String, String>>());

	/**
	 * Common map of user-readable labels for format IDs.
	 */
	private static Map<String, String> fFormatLabels = new HashMap<>(8);

	static {
		setFormatLabel(IFormattedValues.NATURAL_FORMAT,
				MessagesForNumberFormat.FormattedValueVMUtil_Natural_format__label);
		setFormatLabel(IFormattedValues.HEX_FORMAT, MessagesForNumberFormat.FormattedValueVMUtil_Hex_format__label);
		setFormatLabel(IFormattedValues.DECIMAL_FORMAT,
				MessagesForNumberFormat.FormattedValueVMUtil_Decimal_format__label);
		setFormatLabel(IFormattedValues.OCTAL_FORMAT, MessagesForNumberFormat.FormattedValueVMUtil_Octal_format__label);
		setFormatLabel(IFormattedValues.BINARY_FORMAT,
				MessagesForNumberFormat.FormattedValueVMUtil_Binary_format__label);
		setFormatLabel(IFormattedValues.STRING_FORMAT,
				MessagesForNumberFormat.FormattedValueVMUtil_String_format__label);
	}

	/**
	 * Adds a user-readable label for a given format ID.  If a given view model has a custom format ID, it can
	 * add its label to the map of format IDs using this method.
	 *
	 * @param formatId Format ID to set the label for.
	 * @param label User-readable label for a format.
	 */
	public static void setFormatLabel(String formatId, String label) {
		fFormatLabels.put(formatId, label);
	}

	/**
	 * Returns a user readable label for a given format ID.
	 */
	public static String getFormatLabel(String formatId) {
		String label = fFormatLabels.get(formatId);
		if (label != null) {
			return label;
		} else {
			return MessageFormat.format(MessagesForNumberFormat.FormattedValueVMUtil_Other_format__format_text,
					new Object[] { formatId });
		}
	}

	/**
	 * Returns an element property representing an element value in a given format.

	 * @deprecated Replaced by {@link #getPropertyForFormatId(String, String)}
	 */
	@Deprecated
	public static String getPropertyForFormatId(String formatId) {
		return getPropertyForFormatId(formatId, ""); //$NON-NLS-1$
	}

	/**
	 * Returns an element property representing an element value in a given format.
	 *
	 * @param Format ID to create the property for.
	 * @param prefix The prefix for the property that is used to distinguish
	 * it from other number format values in a given property map.   May be
	 * <code>null</code> or an empty string if no prefix is used.
	 * @return The generated property name.
	 *
	 * @since 2.2
	 */
	public static String getPropertyForFormatId(String formatId, String prefix) {
		if (formatId == null) {
			return null;
		}
		if (prefix == null) {
			prefix = ""; //$NON-NLS-1$
		}
		synchronized (fFormatProperties) {
			Map<String, String> formatsMap = getFormatsMap(prefix);
			String property = formatsMap.get(formatId);
			if (property == null) {
				property = (prefix + IDebugVMConstants.PROP_FORMATTED_VALUE_BASE + "." + formatId).intern(); //$NON-NLS-1$
				formatsMap.put(formatId, property);
			}
			return property;
		}
	}

	private static Map<String, String> getFormatsMap(String prefix) {
		synchronized (fFormatProperties) {
			Map<String, String> prefixMap = fFormatProperties.get(prefix);
			if (prefixMap == null) {
				prefixMap = new TreeMap<>();
				fFormatProperties.put(prefix, prefixMap);
			}
			return prefixMap;
		}
	}

	/**
	 * Returns a format ID based on the element property representing a
	 * formatted element value.
	 *
	 * @deprecated Replaced by {@link #getFormatFromProperty(String, String)}
	 */
	@Deprecated
	public static String getFormatFromProperty(String property) {
		return getFormatFromProperty(property, ""); //$NON-NLS-1$
	}

	/**
	 * Returns a format ID based on the element property representing a
	 * formatted element value.  This method has an additional prefix parameter
	 * which is used when multiple number formats are stored in a single
	 * property map.
	 *
	 * @param property The property to extract the format from.
	 * @param prefix The prefix for the property that is used to distinguish
	 * it from other number format values in a given property map.   May be
	 * <code>null</code> or an empty string if no prefix is used.
	 * @return The format ID.
	 *
	 * @throws IllegalArgumentException if the property is not a formatted value
	 * property.
	 *
	 * @since 2.2
	 */
	public static String getFormatFromProperty(String property, String prefix) {
		if (prefix == null) {
			prefix = ""; //$NON-NLS-1$
		}

		synchronized (fFormatProperties) {
			Map<String, String> formatsMap = getFormatsMap(prefix);
			for (Map.Entry<String, String> entry : formatsMap.entrySet()) {
				if (entry.getValue().equals(property)) {
					return entry.getKey();
				}
			}
			if (!property.startsWith(prefix)
					|| !property.startsWith(IDebugVMConstants.PROP_FORMATTED_VALUE_BASE, prefix.length())) {
				throw new IllegalArgumentException(
						"Property " + property + " is not a valid formatted value format property."); //$NON-NLS-1$//$NON-NLS-2$
			}
			String formatId = property
					.substring(prefix.length() + IDebugVMConstants.PROP_FORMATTED_VALUE_BASE.length() + 1).intern();
			formatsMap.put(formatId, property);
			return formatId;
		}
	}

	/**
	 * Returns the user-selected number format that is saved in the given
	 * presentation context.
	 */
	public static String getPreferredFormat(IPresentationContext context) {
		Object prop = context.getProperty(IDebugVMConstants.PROP_FORMATTED_VALUE_FORMAT_PREFERENCE);
		if (prop != null) {
			return (String) prop;
		}
		return IFormattedValues.NATURAL_FORMAT;
	}

	/**
	 * 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.
	 *
	 * @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 monitor Request monitor used to signal completion of work
	 *
	 * @deprecated This method has been replaced by the {@link FormattedValueRetriever}
	 * utility.
	 */
	@Deprecated
	@ConfinedToDsfExecutor("service.getExecutor()")
	public static void updateFormattedValues(final IPropertiesUpdate updates[], final IFormattedValues service,
			final Class<? extends IFormattedDataDMContext> dmcType, final RequestMonitor monitor) {
		// First retrieve the available formats for each update's element (if
		// needed). Store the result in a map (for internal use) and in the
		// update object (if requested). After that's done, call another method
		// to retrieve the formatted values. Note that we use a synchronized map
		// because it's updated by a request monitor with an ImmediateExecutor.
		final Map<IPropertiesUpdate, String[]> availableFormats = Collections
				.synchronizedMap(new HashMap<IPropertiesUpdate, String[]>(updates.length * 4 / 3));
		final CountingRequestMonitor countingRm = new CountingRequestMonitor(service.getExecutor(), monitor) {
			@Override
			protected void handleCompleted() {
				// Retrieve the formatted values now that we have the available formats (where needed).
				// Note that we are passing off responsibility of our parent monitor
				updateFormattedValuesWithAvailableFormats(updates, service, dmcType, availableFormats, monitor);

				// Note: we must not call the update's done method
			}
		};
		int count = 0;

		// 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.
		for (final IPropertiesUpdate update : updates) {
			if ((!update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_AVAILABLE_FORMATS)
					&& !update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT)
					&& !update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE))) {
				continue;
			}

			IFormattedDataDMContext dmc = null;
			if (update.getElement() instanceof IDMVMContext) {
				dmc = DMContexts.getAncestorOfType(((IDMVMContext) update.getElement()).getDMContext(), dmcType);
			}

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

			service.getAvailableFormats(dmc,
					new ViewerDataRequestMonitor<String[]>(ImmediateExecutor.getInstance(), update) {
						@Override
						protected void handleCompleted() {
							if (isSuccess()) {
								// Set the result (available formats) into the update object if it was requested
								if (update.getProperties()
										.contains(IDebugVMConstants.PROP_FORMATTED_VALUE_AVAILABLE_FORMATS)) {
									update.setProperty(IDebugVMConstants.PROP_FORMATTED_VALUE_AVAILABLE_FORMATS,
											getData());
								}

								// 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(getStatus());
							}
							countingRm.done();

							// 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.
						}
					});
			count++;
		}
		countingRm.setDoneCount(count);
	}

	/**
	 * @param updates
	 *            the update objects to act on. Implementation will not directly
	 *            mark these complete, but contribute towards that end by
	 *            marking [monitor] complete.
	 * @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
	 * @param monitor
	 *            Request monitor used to signal completion of work
	 */
	@ConfinedToDsfExecutor("service.getExecutor()")
	private static void updateFormattedValuesWithAvailableFormats(IPropertiesUpdate updates[], IFormattedValues service,
			Class<? extends IFormattedDataDMContext> dmcType, Map<IPropertiesUpdate, String[]> availableFormatsMap,
			final RequestMonitor monitor) {
		// 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 = null;
			if (update.getElement() instanceof IDMVMContext) {
				dmc = DMContexts.getAncestorOfType(((IDMVMContext) update.getElement()).getDMContext(), dmcType);
			}
			if (dmc == null) {
				// The error status should already be set by the calling method.
				continue;
			}

			// 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. If our caller didn't provide the
			// available formats for an update (element), then it means the
			// update doesn't contain any properties that requires us to
			// determine the active format.
			String[] availableFormats = availableFormatsMap.get(update);
			String _activeFormat = null;
			if (availableFormats != null && availableFormats.length != 0) {
				_activeFormat = getPreferredFormat(update.getPresentationContext());
				update.setProperty(IDebugVMConstants.PROP_FORMATTED_VALUE_FORMAT_PREFERENCE, _activeFormat);
				if (!isFormatAvailable(_activeFormat, availableFormats)) {
					_activeFormat = availableFormats[0];
				}
			}
			final String activeFormat = _activeFormat; // null means we don't need to know what the active format is

			if (update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_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(IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT, activeFormat);
			}

			// 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.
			boolean activeFormatValueRequested = false; // does the update object ask for PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE?
			boolean activeFormatValueHandled = false; // have we come across a specific format request that is the active format?
			if (update.getProperties().contains(IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE)) {
				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$
				activeFormatValueRequested = true; // we may end up making an additional run
			}

			for (Iterator<String> itr = update.getProperties().iterator(); itr.hasNext()
					|| (activeFormatValueRequested && !activeFormatValueHandled);) {
				String nextFormat;
				if (itr.hasNext()) {
					String propertyName = itr.next();
					if (propertyName.startsWith(IDebugVMConstants.PROP_FORMATTED_VALUE_BASE)) {
						nextFormat = FormattedValueVMUtil.getFormatFromProperty(propertyName);
						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;
				}

				final boolean _activeFormatValueRequested = activeFormatValueRequested;
				final FormattedValueDMContext formattedValueDmc = service.getFormattedValueContext(dmc, nextFormat);
				service.getFormattedExpressionValue(formattedValueDmc,
						// Here also use the ViewerDataRequestMonitor in order to propagate the update's cancel request.
						// 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()) {
									String format = formattedValueDmc.getFormatID();
									update.setProperty(FormattedValueVMUtil.getPropertyForFormatId(format),
											getData().getFormattedValue());
									if (_activeFormatValueRequested && format.equals(activeFormat)) {
										update.setProperty(IDebugVMConstants.PROP_FORMATTED_VALUE_ACTIVE_FORMAT_VALUE,
												getData().getFormattedValue());
									}
								} else {
									update.setStatus(getStatus());
								}
								countingRm.done();

								// Note: we must not call the update's done method
							}
						});
				count++;
			}
		}
		countingRm.setDoneCount(count);
	}

	private static boolean isFormatAvailable(String format, String[] availableFormats) {
		for (String availableFormat : availableFormats) {
			if (availableFormat.equals(format)) {
				return true;
			}
		}
		return false;
	}

}
