/*******************************************************************************
 * Copyright (c) 2010, 2013 Andrew Gvozdev 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:
 *     Andrew Gvozdev - Initial API and implementation
 *     Tom Hochstein (Freescale) - Bug 412601 - Preprocessor Entries properties tab should list languages
 *     Serge Beauchamp (Freescale Semiconductor) - Bug 406545
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.language.settings.providers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsBroadcastingProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsEditableProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvidersKeeper;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsBaseProvider;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsManager;
import org.eclipse.cdt.core.language.settings.providers.ScannerDiscoveryLegacySupport;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.LanguageManager;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.core.settings.model.ICResourceDescription;
import org.eclipse.cdt.core.settings.model.ICSettingEntry;
import org.eclipse.cdt.core.settings.model.util.CDataUtil;
import org.eclipse.cdt.internal.ui.newui.Messages;
import org.eclipse.cdt.internal.ui.newui.StatusMessageLine;
import org.eclipse.cdt.ui.CDTSharedImages;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.newui.AbstractCPropertyTab;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TreeAdapter;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;

/**
 * This tab presents language settings entries categorized by language
 * settings providers.
 *
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
public class LanguageSettingsEntriesTab extends AbstractCPropertyTab {
	private static final String EXPORT_STR = Messages.AbstractLangsListTab_Export;
	private static final String UNEXPORT_STR = Messages.AbstractLangsListTab_Unexport;

	private static final int[] DEFAULT_ENTRIES_SASH_WEIGHTS = new int[] { 10, 30 };

	private SashForm sashFormEntries;
	private Tree treeLanguages;
	private Tree treeEntries;
	private TreeViewer treeEntriesViewer;
	private String currentLanguageId = null;
	private static String currentLanguageIdGlobal = null;

	private Button builtInCheckBox;
	private StatusMessageLine fStatusLine;

	private LanguageSettingsProvidersPage masterPropertyPage = null;

	private static final int BUTTON_ADD = 0;
	private static final int BUTTON_EDIT = 1;
	private static final int BUTTON_DELETE = 2;
	private static final int BUTTON_EXPORT = 3;
	// there is a separator instead of button #4
	private static final int BUTTON_MOVE_UP = 5;
	private static final int BUTTON_MOVE_DOWN = 6;

	private static final String[] BUTTON_LABELS = new String[7];
	{
		BUTTON_LABELS[BUTTON_ADD] = ADD_STR;
		BUTTON_LABELS[BUTTON_EDIT] = EDIT_STR;
		BUTTON_LABELS[BUTTON_DELETE] = DEL_STR;
		BUTTON_LABELS[BUTTON_EXPORT] = EXPORT_STR;
		BUTTON_LABELS[BUTTON_MOVE_UP] = MOVEUP_STR;
		BUTTON_LABELS[BUTTON_MOVE_DOWN] = MOVEDOWN_STR;
	}

	private static final String CLEAR_STR = Messages.LanguageSettingsProviderTab_Clear;

	private Map<String, List<ILanguageSettingsProvider>> initialProvidersMap = new HashMap<>();

	/**
	 * Label provider for language settings providers displayed by this tab.
	 */
	private class EntriesTreeLabelProvider extends LanguageSettingsProvidersLabelProvider {
		@Override
		protected String[] getOverlayKeys(ILanguageSettingsProvider provider) {
			String[] overlayKeys = super.getOverlayKeys(provider);
			if (provider.getName() == null) {
				return overlayKeys;
			}

			IResource rc = getResource();
			List<ICLanguageSettingEntry> entries = getSettingEntries(provider);
			if (entries == null && !(rc instanceof IProject)) {
				List<ICLanguageSettingEntry> entriesParent = getSettingEntriesUpResourceTree(provider);
				if (entriesParent != null) {
					overlayKeys[IDecoration.TOP_RIGHT] = CDTSharedImages.IMG_OVR_PARENT;
				}
			} else if (provider instanceof ILanguageSettingsBroadcastingProvider
					&& (page.isForFile() || page.isForFolder())) {
				// Assuming that the default entries for a resource are always null.
				// Using that for performance reasons. See note in performDefaults().
				List<ICLanguageSettingEntry> entriesParent = provider.getSettingEntries(null, null, currentLanguageId);
				if (entries != null && !entries.equals(entriesParent)) {
					overlayKeys[IDecoration.TOP_RIGHT] = CDTSharedImages.IMG_OVR_SETTING;
				}
			}

			ICConfigurationDescription cfgDescription = getConfigurationDescription();
			List<ILanguageSettingsProvider> initialProviders = initialProvidersMap.get(cfgDescription.getId());
			if (initialProviders != null && !initialProviders.contains(provider)) {
				overlayKeys[IDecoration.TOP_RIGHT] = CDTSharedImages.IMG_OVR_EDITED;
			}
			return overlayKeys;
		}

		@Override
		public Image getImage(Object element) {
			if (element instanceof ICLanguageSettingEntry) {
				ICConfigurationDescription cfgDescription = getConfigurationDescription();
				return LanguageSettingsImages.getImage((ICLanguageSettingEntry) element, cfgDescription);
			}

			return super.getImage(element);
		}

		@Override
		public String getText(Object element) {
			if (element instanceof ICLanguageSettingEntry) {
				ICLanguageSettingEntry entry = (ICLanguageSettingEntry) element;
				String text = entry.getName();
				if (entry.getKind() == ICSettingEntry.MACRO) {
					if ((entry.getFlags() & ICSettingEntry.UNDEFINED) == 0) {
						text = text + '=' + entry.getValue();
					}
				} else if (LanguageSettingsImages.isProjectRelative(entry)) {
					text = LanguageSettingsImages.toProjectRelative(text);
				}
				return text;
			}

			return super.getText(element);
		}
	}

	/**
	 * Content provider for setting entries tree.
	 */
	private class EntriesTreeContentProvider implements ITreeContentProvider {
		@Override
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}

		@Override
		public Object[] getChildren(Object parentElement) {
			if (parentElement instanceof Object[])
				return (Object[]) parentElement;

			if (parentElement instanceof ILanguageSettingsProvider) {
				ILanguageSettingsProvider lsProvider = (ILanguageSettingsProvider) parentElement;
				List<ICLanguageSettingEntry> entriesList = getSettingEntriesUpResourceTree(lsProvider);
				if (entriesList == null) {
					return null;
				}

				// convert to modifiable list
				entriesList = new ArrayList<>(entriesList);

				if (builtInCheckBox.getSelection() == false) {
					for (Iterator<ICLanguageSettingEntry> iter = entriesList.iterator(); iter.hasNext();) {
						ICLanguageSettingEntry entry = iter.next();
						if (entry.isBuiltIn()) {
							iter.remove();
						}
					}
				}
				return entriesList.toArray();
			}

			return null;
		}

		@Override
		public Object getParent(Object element) {
			return null;
		}

		@Override
		public boolean hasChildren(Object element) {
			Object[] children = getChildren(element);
			return children != null && children.length > 0;
		}

		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}

		@Override
		public void dispose() {
		}

	}

	/**
	 * Shortcut for getting the current resource for the property page.
	 */
	private IResource getResource() {
		return (IResource) page.getElement();
	}

	/**
	 * Shortcut for getting the current configuration description.
	 */
	private ICConfigurationDescription getConfigurationDescription() {
		return getResDesc().getConfiguration();
	}

	/**
	 * Shortcut for getting the currently selected provider.
	 */
	private ILanguageSettingsProvider getSelectedProvider() {
		ILanguageSettingsProvider provider = null;

		TreeItem[] items = treeEntries.getSelection();
		if (items.length == 1) {
			TreeItem item = items[0];
			Object itemData = item.getData();
			if (itemData instanceof ICLanguageSettingEntry) {
				item = item.getParentItem();
				if (item != null) {
					itemData = item.getData();
				}
			}
			if (itemData instanceof ILanguageSettingsProvider) {
				provider = (ILanguageSettingsProvider) itemData;
			}
		}

		return provider;
	}

	/**
	 * Shortcut for getting the currently selected setting entry.
	 */
	private ICLanguageSettingEntry getSelectedEntry() {
		ICLanguageSettingEntry entry = null;

		TreeItem[] items = treeEntries.getSelection();
		if (items.length == 1) {
			TreeItem item = items[0];
			Object itemData = item.getData();
			if (itemData instanceof ICLanguageSettingEntry) {
				entry = (ICLanguageSettingEntry) itemData;
			}
		}

		return entry;
	}

	/**
	 * Shortcut for getting setting entries for current context. {@link LanguageSettingsManager}
	 * will be checking parent resources if no settings defined for current resource.
	 *
	 * @return list of setting entries for the current context.
	 */
	private List<ICLanguageSettingEntry> getSettingEntriesUpResourceTree(ILanguageSettingsProvider provider) {
		ICConfigurationDescription cfgDescription = getConfigurationDescription();
		IResource rc = getResource();
		List<ICLanguageSettingEntry> entries = LanguageSettingsManager.getSettingEntriesUpResourceTree(provider,
				cfgDescription, rc, currentLanguageId);
		return entries;
	}

	/**
	 * Shortcut for getting setting entries for current context without checking the parent resource.
	 * @return list of setting entries for the current context.
	 */
	private List<ICLanguageSettingEntry> getSettingEntries(ILanguageSettingsProvider provider) {
		ICConfigurationDescription cfgDescription = getConfigurationDescription();
		IResource rc = getResource();
		return provider.getSettingEntries(cfgDescription, rc, currentLanguageId);
	}

	/**
	 * Store original providers to be able to tell whether they were changed by user.
	 */
	private void trackInitialSettings() {
		if (!page.isForPrefs()) {
			ICConfigurationDescription[] cfgDescriptions = page.getCfgsEditable();
			for (ICConfigurationDescription cfgDescription : cfgDescriptions) {
				if (cfgDescription instanceof ILanguageSettingsProvidersKeeper) {
					String cfgId = cfgDescription.getId();
					List<ILanguageSettingsProvider> initialProviders = ((ILanguageSettingsProvidersKeeper) cfgDescription)
							.getLanguageSettingProviders();
					initialProvidersMap.put(cfgId, initialProviders);
				}
			}
		}
	}

	/**
	 * Create UI control for languages.
	 */
	private void createTreeForLanguages(Composite parent) {
		treeLanguages = new Tree(parent, SWT.BORDER | SWT.SINGLE | SWT.H_SCROLL);
		treeLanguages.setLayoutData(new GridData(GridData.FILL_VERTICAL));
		treeLanguages.setHeaderVisible(true);

		treeLanguages.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				TreeItem[] items = treeLanguages.getSelection();
				if (items.length > 0) {
					currentLanguageId = (String) items[0].getData();
					currentLanguageIdGlobal = currentLanguageId;
					updateTreeForEntries(null, null);
				}
			}
		});

		final TreeColumn columnLanguages = new TreeColumn(treeLanguages, SWT.NONE);
		columnLanguages.setText(Messages.AbstractLangsListTab_Languages);
		columnLanguages.setWidth(200);
		columnLanguages.setResizable(false);
		columnLanguages.setToolTipText(Messages.AbstractLangsListTab_Languages);

		treeLanguages.addPaintListener(new PaintListener() {
			@Override
			public void paintControl(PaintEvent e) {
				int x = treeLanguages.getBounds().width - 5;
				if (columnLanguages.getWidth() != x) {
					columnLanguages.setWidth(x);
				}
			}
		});
	}

	/**
	 * Create tree for providers and their entries.
	 */
	private void createTreeForEntries(Composite parent) {
		treeEntries = new Tree(parent, SWT.BORDER | SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
		treeEntries.setLayoutData(new GridData(GridData.FILL_VERTICAL | GridData.GRAB_HORIZONTAL));
		treeEntries.setHeaderVisible(true);
		treeEntries.setLinesVisible(true);

		final TreeColumn treeCol = new TreeColumn(treeEntries, SWT.NONE);
		treeEntries.addTreeListener(new TreeAdapter() {
			@Override
			public void treeExpanded(TreeEvent e) {
				treeEntries.getDisplay().asyncExec(() -> {
					treeCol.pack();
				});
			}
		});

		treeCol.setText(Messages.LanguageSettingsProviderTab_SettingEntries);

		treeCol.setWidth(200);
		treeCol.setToolTipText(Messages.LanguageSettingsProviderTab_SettingEntriesTooltip);

		treeEntriesViewer = new TreeViewer(treeEntries);
		treeEntriesViewer.setContentProvider(new EntriesTreeContentProvider());
		treeEntriesViewer.setLabelProvider(new EntriesTreeLabelProvider());

		treeEntriesViewer.setUseHashlookup(true);

		treeEntries.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				if (treeLanguages.getSelectionCount() == 0) {
					selectLanguage(currentLanguageId);
				}
				updateStatusLine();
				updateButtons();
			}

			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
				if (buttonIsEnabled(BUTTON_EDIT) && treeEntries.getSelection().length > 0)
					buttonPressed(BUTTON_EDIT);
			}
		});

	}

	/**
	 * Create sash form.
	 */
	private void createSashForm() {
		sashFormEntries = new SashForm(usercomp, SWT.HORIZONTAL);

		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.horizontalSpan = 2;
		gd.grabExcessVerticalSpace = true;
		sashFormEntries.setLayoutData(gd);

		GridLayout layout = new GridLayout();
		sashFormEntries.setLayout(layout);

		createTreeForLanguages(sashFormEntries);
		createTreeForEntries(sashFormEntries);

		sashFormEntries.setWeights(DEFAULT_ENTRIES_SASH_WEIGHTS);
	}

	/**
	 * Create check-box to control whether to show built-in entries or not.
	 */
	private void createBuiltInsCheckBox() {
		builtInCheckBox = setupCheck(usercomp, Messages.AbstractLangsListTab_ShowBuiltin, 1, GridData.FILL_HORIZONTAL);
		builtInCheckBox.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				updateTreeForEntries(null, null);
			}
		});
		builtInCheckBox.setSelection(true);
		builtInCheckBox.setEnabled(true);
	}

	@Override
	public void createControls(Composite parent) {
		super.createControls(parent);
		currentLanguageId = null;

		usercomp.setLayout(new GridLayout());
		GridData gd = (GridData) usercomp.getLayoutData();
		// Discourage settings entry table from trying to show all its items at once, see bug 264330
		gd.heightHint = 1;

		if (page instanceof LanguageSettingsProvidersPage) {
			masterPropertyPage = (LanguageSettingsProvidersPage) page;
		}

		trackInitialSettings();

		createSashForm();
		fStatusLine = new StatusMessageLine(usercomp, SWT.LEFT, 2);
		createBuiltInsCheckBox();

		enableTabControls(masterPropertyPage.isLanguageSettingsProvidersEnabled());

		initButtons(BUTTON_LABELS);
		updateData(getResDesc());
	}

	/**
	 * Gray out or restore all controls except enabling check-box.
	 */
	private void enableTabControls(boolean enable) {
		sashFormEntries.setEnabled(enable);
		treeLanguages.setEnabled(enable);
		treeEntries.setEnabled(enable);
		builtInCheckBox.setEnabled(enable);

		buttoncomp.setEnabled(enable);

		if (enable) {
			updateTreeForEntries(null, null);
		} else {
			buttonSetEnabled(BUTTON_ADD, false);
			buttonSetEnabled(BUTTON_EDIT, false);
			buttonSetEnabled(BUTTON_DELETE, false);
			buttonSetEnabled(BUTTON_EXPORT, false);
			buttonSetEnabled(BUTTON_MOVE_UP, false);
			buttonSetEnabled(BUTTON_MOVE_DOWN, false);
		}
	}

	/**
	 * Check if "Export" button is in "Export" mode or "Unexport" mode.
	 */
	private boolean isExportMode(ILanguageSettingsProvider provider, ICLanguageSettingEntry entry) {
		List<ICLanguageSettingEntry> entries = getSettingEntriesUpResourceTree(provider);

		boolean isAllowedToEdit = provider instanceof ILanguageSettingsEditableProvider
				&& LanguageSettingsProviderAssociationManager.isAllowedToEditEntries(provider);

		boolean canExport = isAllowedToEdit && entries != null && entries.size() > 0;
		boolean isExported = false;
		if (canExport) {
			if (entry != null) {
				isExported = (entry.getFlags() & ICSettingEntry.EXPORTED) == ICSettingEntry.EXPORTED;
			} else if (entries != null) {
				for (ICLanguageSettingEntry ent : entries) {
					isExported = (ent.getFlags() & ICSettingEntry.EXPORTED) == ICSettingEntry.EXPORTED;
					// in case of mixed exports suggest to "Export"
					if (!isExported) {
						break;
					}
				}
			}
		}

		return !isExported;
	}

	/**
	 * Updates state for all buttons. Called when table selection changes.
	 */
	@Override
	protected void updateButtons() {
		ILanguageSettingsProvider provider = getSelectedProvider();
		ICLanguageSettingEntry entry = getSelectedEntry();
		List<ICLanguageSettingEntry> entries = getSettingEntriesUpResourceTree(provider);

		boolean isEntrySelected = (entry != null);
		boolean isProviderSelected = !isEntrySelected && (provider != null);

		boolean isAllowedToEdit = provider instanceof ILanguageSettingsEditableProvider
				&& LanguageSettingsProviderAssociationManager.isAllowedToEditEntries(provider);

		boolean isAllowedToClear = provider instanceof ILanguageSettingsEditableProvider
				&& LanguageSettingsProviderAssociationManager.isAllowedToClear(provider);

		boolean canAdd = isAllowedToEdit;
		boolean canEdit = isAllowedToEdit && isEntrySelected;
		boolean canDelete = isAllowedToEdit && isEntrySelected;
		boolean canClear = isAllowedToClear && isProviderSelected && entries != null && entries.size() > 0;
		boolean canExport = isAllowedToEdit && entries != null && entries.size() > 0;
		boolean suggestExport = isExportMode(provider, entry);

		boolean canMoveUp = false;
		boolean canMoveDown = false;
		if (isAllowedToEdit && isEntrySelected && entries != null) {
			int last = entries.size() - 1;
			int pos = getExactIndex(entries, entry);

			if (pos >= 0 && pos <= last) {
				canMoveUp = (pos != 0);
				canMoveDown = (pos != last);
			}
		}

		buttonSetText(BUTTON_DELETE, isProviderSelected ? CLEAR_STR : DEL_STR);
		buttonSetText(BUTTON_EXPORT, suggestExport ? EXPORT_STR : UNEXPORT_STR);

		buttonSetEnabled(BUTTON_ADD, canAdd);
		buttonSetEnabled(BUTTON_EDIT, canEdit);
		buttonSetEnabled(BUTTON_DELETE, canDelete || canClear);
		buttonSetEnabled(BUTTON_EXPORT, canExport);

		buttonSetEnabled(BUTTON_MOVE_UP, canMoveUp);
		buttonSetEnabled(BUTTON_MOVE_DOWN, canMoveDown);
	}

	/**
	 * Displays warning message - if any - for selected language settings entry.
	 */
	private void updateStatusLine() {
		IStatus status = null;
		if (masterPropertyPage.isLanguageSettingsProvidersEnabled()) {
			ICConfigurationDescription cfgDescription = getConfigurationDescription();
			status = LanguageSettingsImages.getStatus(getSelectedEntry(), cfgDescription);
		}
		if (status == null || status == Status.OK_STATUS) {
			ILanguageSettingsProvider provider = getSelectedProvider();
			boolean isAllowedEditing = provider instanceof ILanguageSettingsEditableProvider
					&& LanguageSettingsProviderAssociationManager.isAllowedToEditEntries(provider);
			if (provider != null && !isAllowedEditing) {
				status = new Status(IStatus.INFO, CUIPlugin.PLUGIN_ID,
						Messages.LanguageSettingsEntriesTab_Entries_Not_Editable);
			}
		}
		if (status == null || status == Status.OK_STATUS) {
			if (treeLanguages.getItemCount() <= 0) {
				status = new Status(IStatus.ERROR, CUIPlugin.PLUGIN_ID,
						Messages.LanguageSettingsEntriesTab_Cannot_Determine_Languages);
			}
		}
		fStatusLine.setErrorStatus(status);
	}

	/**
	 * Handle buttons
	 */
	@Override
	public void buttonPressed(int buttonIndex) {
		ILanguageSettingsProvider selectedProvider = getSelectedProvider();
		ICLanguageSettingEntry selectedEntry = getSelectedEntry();

		switch (buttonIndex) {
		case BUTTON_ADD:
			performAdd(selectedProvider);
			break;
		case BUTTON_EDIT:
			performEdit(selectedProvider, selectedEntry);
			break;
		case BUTTON_DELETE:
			performDelete(selectedProvider, selectedEntry);
			break;
		case BUTTON_EXPORT:
			performExport(selectedProvider, selectedEntry);
			break;
		case BUTTON_MOVE_UP:
			performMoveUp(selectedProvider, selectedEntry);
			break;
		case BUTTON_MOVE_DOWN:
			performMoveDown(selectedProvider, selectedEntry);
			break;
		default:
		}
		treeEntries.setFocus();
	}

	/**
	 * That method returns exact position of an element in the list.
	 * Note that {@link List#indexOf(Object)} returns position of the first element
	 * equals to the given one, not exact element.
	 *
	 * @param entries
	 * @param entry
	 * @return exact position of the element or -1 of not found.
	 */
	private int getExactIndex(List<ICLanguageSettingEntry> entries, ICLanguageSettingEntry entry) {
		if (entries != null) {
			for (int i = 0; i < entries.size(); i++) {
				if (entries.get(i) == entry)
					return i;
			}
		}
		return -1;
	}

	/**
	 * Find TreeItem associated with a provider.
	 */
	private TreeItem findProviderItem(String id) {
		TreeItem[] providerItems = treeEntries.getItems();
		for (TreeItem providerItem : providerItems) {
			Object providerItemData = providerItem.getData();
			if (providerItemData instanceof ILanguageSettingsProvider) {
				ILanguageSettingsProvider provider = (ILanguageSettingsProvider) providerItemData;
				if (provider.getId().equals(id)) {
					return providerItem;
				}
			}
		}
		return null;
	}

	/**
	 * Find TreeItem associated with a provider's entry.
	 */
	private TreeItem findEntryItem(String providerId, ICLanguageSettingEntry entry) {
		TreeItem[] providerItems = treeEntries.getItems();
		for (TreeItem providerItem : providerItems) {
			Object providerItemData = providerItem.getData();
			if (providerItemData instanceof ILanguageSettingsProvider) {
				ILanguageSettingsProvider provider = (ILanguageSettingsProvider) providerItemData;
				if (provider.getId().equals(providerId)) {
					TreeItem[] entryItems = providerItem.getItems();
					for (TreeItem entryItem : entryItems) {
						Object entryItemData = entryItem.getData();
						if (entryItemData == entry)
							return entryItem;
					}
				}
			}
		}
		return null;
	}

	/**
	 * Select language settings entry item in the tree.
	 */
	private void selectItem(String providerId, ICLanguageSettingEntry entry) {
		TreeItem providerItem = findProviderItem(providerId);
		if (providerItem != null) {
			treeEntries.select(providerItem);
			if (providerItem.getItems().length > 0) {
				treeEntries.showItem(providerItem.getItems()[0]);
			}
			TreeItem entryItem = findEntryItem(providerId, entry);
			if (entryItem != null) {
				treeEntries.showItem(entryItem);
				treeEntries.select(entryItem);
			}
			updateStatusLine();
		}
	}

	/**
	 * Add language settings entry.
	 */
	private void addEntry(ILanguageSettingsProvider provider, ICLanguageSettingEntry entry) {
		if (provider != null && entry != null) {
			String providerId = provider.getId();

			List<ICLanguageSettingEntry> entries = getEntriesShownToUser(provider);
			ICLanguageSettingEntry selectedEntry = getSelectedEntry();
			int pos = getExactIndex(entries, selectedEntry);
			entries.add(pos + 1, entry);

			saveEntries(provider, entries);
			updateTreeForEntries(providerId, entry);
		}
	}

	/**
	 * Save entries into provider considering resource parent.
	 */
	private void saveEntries(ILanguageSettingsProvider provider, List<ICLanguageSettingEntry> entries) {
		if (provider instanceof ILanguageSettingsEditableProvider) {
			ICConfigurationDescription cfgDescription = getConfigurationDescription();
			IResource rc = getResource();
			if (entries != null && rc != null) {
				IContainer parent = rc instanceof IProject ? null : rc.getParent();
				List<ICLanguageSettingEntry> parentEntries = LanguageSettingsManager
						.getSettingEntriesUpResourceTree(provider, cfgDescription, parent, currentLanguageId);
				if (entries.equals(parentEntries)) {
					// to use parent entries instead
					entries = null;
				}
			}
			((ILanguageSettingsEditableProvider) provider).setSettingEntries(cfgDescription, rc, currentLanguageId,
					entries);
		}
	}

	/**
	 * Get list of setting entries shown to user. If current resource has no entries assigned the parent
	 * resource is inspected.
	 */
	private List<ICLanguageSettingEntry> getEntriesShownToUser(ILanguageSettingsProvider provider) {
		ICConfigurationDescription cfgDescription = getConfigurationDescription();
		IResource rc = getResource();
		List<ICLanguageSettingEntry> entries = provider.getSettingEntries(cfgDescription, rc, currentLanguageId);
		if (entries == null) {
			entries = getSettingEntriesUpResourceTree(provider);
		}
		entries = new ArrayList<>(entries);
		return entries;
	}

	/**
	 * Call dialog to add settings entry.
	 */
	private void performAdd(ILanguageSettingsProvider selectedProvider) {
		if (selectedProvider instanceof ILanguageSettingsEditableProvider) {
			ICConfigurationDescription cfgDescription = getConfigurationDescription();
			ICLanguageSettingEntry selectedEntry = getSelectedEntry();
			LanguageSettingEntryDialog addDialog = new LanguageSettingEntryDialog(usercomp.getShell(), cfgDescription,
					selectedEntry, true);
			if (addDialog.open()) {
				ICLanguageSettingEntry settingEntry = addDialog.getEntries()[0];
				if (settingEntry != null) {
					selectedProvider = getWorkingCopy((ILanguageSettingsEditableProvider) selectedProvider);
					addEntry(selectedProvider, settingEntry);
				}
			}
		}
	}

	/**
	 * Return working copy of the provider to edit in current session. If the supplied provider is already
	 * the working copy return it. If not, create a copy to be edited.
	 */
	private ILanguageSettingsEditableProvider getWorkingCopy(ILanguageSettingsEditableProvider provider) {
		ICConfigurationDescription cfgDescription = getConfigurationDescription();
		List<ILanguageSettingsProvider> initialProviders = initialProvidersMap.get(cfgDescription.getId());
		if (initialProviders.contains(provider)) {
			List<ILanguageSettingsProvider> providers = new ArrayList<>(
					((ILanguageSettingsProvidersKeeper) cfgDescription).getLanguageSettingProviders());
			int pos = providers.indexOf(provider);
			if (pos >= 0) {
				try {
					provider = provider.clone();
					providers.set(pos, provider);
					((ILanguageSettingsProvidersKeeper) cfgDescription).setLanguageSettingProviders(providers);
				} catch (CloneNotSupportedException e) {
					CUIPlugin.log("Internal Error: cannot clone provider " + provider.getId(), e); //$NON-NLS-1$
				}
			} else {
				CUIPlugin.log("Internal Error: cannot find provider " + provider.getId(), new Exception()); //$NON-NLS-1$
			}
		}
		return provider;
	}

	/**
	 * Call dialog to edit settings entry.
	 */
	private void performEdit(ILanguageSettingsProvider selectedProvider, ICLanguageSettingEntry selectedEntry) {
		if (selectedProvider instanceof ILanguageSettingsEditableProvider && selectedEntry != null) {
			ICConfigurationDescription cfgDecsription = getConfigurationDescription();
			LanguageSettingEntryDialog editDialog = new LanguageSettingEntryDialog(usercomp.getShell(), cfgDecsription,
					selectedEntry);
			if (editDialog.open()) {
				ICLanguageSettingEntry newEntry = editDialog.getEntries()[0];
				if (newEntry != null) {
					selectedProvider = getWorkingCopy((ILanguageSettingsEditableProvider) selectedProvider);
					replaceEntry(selectedProvider, selectedEntry, newEntry);
				}
			}

		}
	}

	/**
	 * Delete provider's entry and update UI.
	 */
	private void deleteEntry(ILanguageSettingsProvider provider, ICLanguageSettingEntry entry) {
		if (provider != null && entry != null) {
			String providerId = provider.getId();

			List<ICLanguageSettingEntry> entries = getEntriesShownToUser(provider);
			int pos = getExactIndex(entries, entry);
			entries.remove(entry);
			saveEntries(provider, entries);

			if (pos >= entries.size()) {
				pos = entries.size() - 1;
			}
			ICLanguageSettingEntry entryToSelect = (pos >= 0) ? entries.get(pos) : null;

			updateTreeForEntries(providerId, entryToSelect);
		}
	}

	/**
	 * Replace provider's entry and update UI.
	 */
	private void replaceEntry(ILanguageSettingsProvider provider, ICLanguageSettingEntry oldEntry,
			ICLanguageSettingEntry newEntry) {
		if (provider != null && oldEntry != null && newEntry != null) {
			String providerId = provider.getId();

			List<ICLanguageSettingEntry> entries = getEntriesShownToUser(provider);
			int pos = getExactIndex(entries, oldEntry);
			entries.set(pos, newEntry);

			saveEntries(provider, entries);
			updateTreeForEntries(providerId, newEntry);
		}
	}

	/**
	 * Clear all provider's entries for the given resource and update UI.
	 */
	private void clearProvider(ILanguageSettingsProvider provider) {
		if (provider != null) {
			String providerId = provider.getId();
			List<ICLanguageSettingEntry> empty = new ArrayList<>();
			saveEntries(provider, empty);
			updateTreeForEntries(providerId, null);
		}
	}

	/**
	 * Clear provider's entries for the given resource or remove one entry depending on selection.
	 */
	private void performDelete(ILanguageSettingsProvider selectedProvider, ICLanguageSettingEntry selectedEntry) {
		if (selectedProvider instanceof ILanguageSettingsEditableProvider) {
			selectedProvider = getWorkingCopy((ILanguageSettingsEditableProvider) selectedProvider);
			if (selectedEntry != null) {
				deleteEntry(selectedProvider, selectedEntry);
			} else {
				clearProvider(selectedProvider);
			}
		}
	}

	/**
	 * Change "Export" flag of provider's entry and update UI.
	 */
	private void exportEntry(ILanguageSettingsProvider provider, ICLanguageSettingEntry entry, boolean isExport) {
		if (provider instanceof ILanguageSettingsEditableProvider && entry != null) {
			int flags = entry.getFlags();
			if (isExport) {
				flags |= ICSettingEntry.EXPORTED;
			} else {
				flags &= ~ICSettingEntry.EXPORTED;
			}
			ICLanguageSettingEntry newEntry = CDataUtil.createEntry(entry, flags);
			if (newEntry != null) {
				provider = getWorkingCopy((ILanguageSettingsEditableProvider) provider);
				replaceEntry(provider, entry, newEntry);
			}
		}
	}

	/**
	 * Change "Export" flag of all provider's entries and update UI.
	 */
	private void exportAllEntries(ILanguageSettingsProvider provider, boolean isExport) {
		if (provider instanceof ILanguageSettingsEditableProvider) {
			List<ICLanguageSettingEntry> entries = getEntriesShownToUser(provider);
			if (entries.size() > 0) {
				provider = getWorkingCopy((ILanguageSettingsEditableProvider) provider);
				for (int i = 0; i < entries.size(); i++) {
					ICLanguageSettingEntry entry = entries.get(i);
					int flags = entry.getFlags();
					if (isExport) {
						flags |= ICSettingEntry.EXPORTED;
					} else {
						flags &= ~ICSettingEntry.EXPORTED;
					}
					ICLanguageSettingEntry newEntry = CDataUtil.createEntry(entry, flags);
					entries.set(i, newEntry);
				}
				saveEntries(provider, entries);
				updateTreeForEntries(provider.getId(), null);
			}
		}
	}

	/**
	 * Export or un-export provider's entry or entries.
	 */
	private void performExport(ILanguageSettingsProvider selectedProvider, ICLanguageSettingEntry selectedEntry) {
		if (selectedProvider instanceof ILanguageSettingsEditableProvider) {
			boolean isExport = isExportMode(selectedProvider, selectedEntry);
			if (selectedEntry != null) {
				exportEntry(selectedProvider, selectedEntry, isExport);
			} else {
				exportAllEntries(selectedProvider, isExport);
			}
		}
	}

	/**
	 * Move provider's entry up or down.
	 */
	private void moveEntry(ILanguageSettingsProvider provider, ICLanguageSettingEntry entry, boolean up) {
		if (provider != null && entry != null) {
			String providerId = provider.getId();

			List<ICLanguageSettingEntry> entries = getEntriesShownToUser(provider);
			int pos = getExactIndex(entries, entry);
			int newPos = up ? pos - 1 : pos + 1;
			Collections.swap(entries, pos, newPos);
			saveEntries(provider, entries);

			updateTreeForEntries(providerId, entry);
		}
	}

	/**
	 * Move provider's entry up.
	 */
	private void performMoveUp(ILanguageSettingsProvider selectedProvider, ICLanguageSettingEntry selectedEntry) {
		if (selectedEntry != null && (selectedProvider instanceof ILanguageSettingsEditableProvider)) {
			selectedProvider = getWorkingCopy((ILanguageSettingsEditableProvider) selectedProvider);
			moveEntry(selectedProvider, selectedEntry, true);
		}
	}

	/**
	 * Move provider's entry down.
	 */
	private void performMoveDown(ILanguageSettingsProvider selectedProvider, ICLanguageSettingEntry selectedEntry) {
		if (selectedEntry != null && (selectedProvider instanceof ILanguageSettingsEditableProvider)) {
			selectedProvider = getWorkingCopy((ILanguageSettingsEditableProvider) selectedProvider);
			moveEntry(selectedProvider, selectedEntry, false);
		}
	}

	/**
	 * Get list of providers to display in the settings entry tree.
	 */
	private List<ILanguageSettingsProvider> getProviders(String languageSettingId) {
		List<ILanguageSettingsProvider> itemsList = new LinkedList<>();
		IResource rc = getResource();
		ICConfigurationDescription cfgDescription = getConfigurationDescription();
		if (rc != null && cfgDescription instanceof ILanguageSettingsProvidersKeeper) {
			List<ILanguageSettingsProvider> cfgProviders = ((ILanguageSettingsProvidersKeeper) cfgDescription)
					.getLanguageSettingProviders();
			for (ILanguageSettingsProvider cfgProvider : cfgProviders) {
				ILanguageSettingsProvider rawProvider = LanguageSettingsManager.getRawProvider(cfgProvider);
				if (rawProvider instanceof LanguageSettingsBaseProvider) {
					// filter out providers incapable of providing entries for this language
					List<String> languageIds = ((LanguageSettingsBaseProvider) rawProvider).getLanguageScope();
					if (languageIds != null && !languageIds.contains(currentLanguageId)) {
						continue;
					}
				}
				itemsList.add(cfgProvider);
			}
		}
		return itemsList;
	}

	/**
	 * Re-reads and refreshes the entries tree.
	 *
	 * @param selectedProviderId - provider of the entry to select after update.
	 *    If the entry is {@code null} the provider itself will be selected.
	 * @param selectedEntry - entry to select in the tree after update.
	 */
	private void updateTreeForEntries(String selectedProviderId, ICLanguageSettingEntry selectedEntry) {
		Object[] expandedElements = treeEntriesViewer.getExpandedElements();

		List<ILanguageSettingsProvider> tableItems = getProviders(currentLanguageId);
		treeEntriesViewer.getControl().setRedraw(false);
		treeEntriesViewer.setInput(tableItems.toArray(new Object[tableItems.size()]));

		// set selection and restore expansion states
		if (selectedProviderId != null) {
			selectItem(selectedProviderId, selectedEntry);

			// find the provider that will replace selected one and replace it in expandedElements[]
			for (ILanguageSettingsProvider provider : tableItems) {
				if (provider.getId().equals(selectedProviderId)) {
					for (int i = 0; i < expandedElements.length; i++) {
						if (expandedElements[i] instanceof ILanguageSettingsProvider) {
							if (((ILanguageSettingsProvider) expandedElements[i]).getId().equals(selectedProviderId)) {
								expandedElements[i] = provider;
								break;
							}
						}
					}
					break;
				}
			}
		}
		treeEntriesViewer.setExpandedElements(expandedElements);

		treeEntriesViewer.getControl().setRedraw(true);
		treeEntriesViewer.getControl().redraw();

		updateStatusLine();
		updateButtons();
	}

	/**
	 * Re-reads and refreshes the languages tree.
	 */
	private void updateTreeForLanguages(ICResourceDescription rcDes) {
		treeLanguages.removeAll();
		currentLanguageId = null;

		List<String> languageIds = LanguageSettingsManager.getLanguages(rcDes);
		if (languageIds.size() > 1) {
			// remove null language when some real ones are defined
			languageIds.remove(null);
		} else if (languageIds.isEmpty()) {
			// if no languages are defined keep null language as "Unspecified language"
			languageIds.add(null);
		}

		// Use a TreeMap to sort the languages by name.
		// For each name we keep a list of ids in case of name overlap.
		Map<String, List<String>> map = new TreeMap<>();
		for (String langId : languageIds) {
			ILanguage language = LanguageManager.getInstance().getLanguage(langId);

			String langName = language != null ? language.getName()
					: Messages.LanguageSettingsEntriesTab_UnspecifiedLanguage;
			if (langName == null || langName.length() == 0)
				continue;
			List<String> langIds = map.get(langName);
			if (langIds == null) {
				langIds = new ArrayList<>();
				map.put(langName, langIds);
			}
			langIds.add(langId);
		}

		for (String langName : map.keySet()) {
			List<String> langIds = map.get(langName);
			for (String langId : langIds) {
				TreeItem t = new TreeItem(treeLanguages, SWT.NONE);
				if (langIds.size() == 1) {
					t.setText(0, langName);
				} else {
					StringBuilder uniqueLangName = new StringBuilder();
					uniqueLangName.append(langName).append(" [id=") //$NON-NLS-1$
							.append(langId).append("]"); //$NON-NLS-1$
					t.setText(0, uniqueLangName.toString());
				}
				t.setData(langId);
				if (currentLanguageIdGlobal != null && currentLanguageIdGlobal.equals(langId)) {
					currentLanguageId = currentLanguageIdGlobal;
					treeLanguages.setSelection(t);
				} else if (currentLanguageId == null) {
					// this selects first language on first round
					// do not select the tree item and global language selection here, only on actual click
					currentLanguageId = langId;
				}
			}
		}
	}

	/**
	 * Change selection of language.
	 */
	private void selectLanguage(String langId) {
		currentLanguageId = langId;
		currentLanguageIdGlobal = currentLanguageId;

		for (TreeItem t : treeLanguages.getItems()) {
			if ((t.getData() == null && langId == null) || t.getData().equals(langId)) {
				treeLanguages.setSelection(t);
				break;
			}
		}
	}

	/**
	 * Update the tab. Called when configuration changes.
	 */
	@Override
	public void updateData(ICResourceDescription rcDes) {
		if (!canBeVisible())
			return;

		if (rcDes != null) {
			if (page.isMultiCfg()) {
				setAllVisible(false, null);
				return;
			} else {
				setAllVisible(true, null);
			}

			updateTreeForLanguages(rcDes);
			updateTreeForEntries(null, null);

			if (masterPropertyPage != null) {
				boolean enabled = masterPropertyPage.isLanguageSettingsProvidersEnabled();
				enableTabControls(enabled);
			}
		}
		updateButtons();
	}

	@Override
	public boolean canSupportMultiCfg() {
		return false;
	}

	@Override
	protected void performDefaults() {
		// This page restores defaults for file/folder only.
		// Project and Preferences page are restored by LanguageSettingsProviderTab.
		if (page.isForFile() || page.isForFolder()) {
			// The logic below is not exactly correct as the default for a resource could be different than null.
			// It is supposed to match the one taken from extension for the same resource which in theory can be non-null.
			// However for the performance reasons for resource decorators where the same logic is used
			// we use null for resetting file/folder resource which should be correct in most cases.
			// Count that as a feature.
			ICConfigurationDescription cfgDescription = getConfigurationDescription();
			if (!(cfgDescription instanceof ILanguageSettingsProvidersKeeper)) {
				return;
			}

			boolean changed = false;
			IResource rc = getResource();
			List<ILanguageSettingsProvider> oldProviders = ((ILanguageSettingsProvidersKeeper) cfgDescription)
					.getLanguageSettingProviders();
			List<ILanguageSettingsProvider> newProviders = new ArrayList<>(oldProviders.size());

			// clear entries for a given resource for all languages where applicable
			providers: for (ILanguageSettingsProvider provider : oldProviders) {
				ILanguageSettingsEditableProvider providerCopy = null;
				if (provider instanceof ILanguageSettingsEditableProvider) {
					for (TreeItem langItems : treeLanguages.getItems()) {
						String langId = (String) langItems.getData();
						if (provider.getSettingEntries(cfgDescription, rc, langId) != null) {
							if (providerCopy == null) {
								// copy providers to be able to "Cancel" in UI
								providerCopy = LanguageSettingsManager
										.getProviderCopy((ILanguageSettingsEditableProvider) provider, true);
								if (providerCopy == null) {
									continue providers;
								}
							}
							providerCopy.setSettingEntries(cfgDescription, rc, langId, null);
							changed = true;
						}
					}
				}
				if (providerCopy != null) {
					newProviders.add(providerCopy);
				} else {
					newProviders.add(provider);
				}
			}
			if (changed) {
				((ILanguageSettingsProvidersKeeper) cfgDescription).setLanguageSettingProviders(newProviders);
				updateTreeForEntries(null, null);
			}
		}
	}

	@Override
	protected void performApply(ICResourceDescription srcRcDescription, ICResourceDescription destRcDescription) {
		if (!page.isForPrefs()) {
			ICConfigurationDescription sd = srcRcDescription.getConfiguration();
			ICConfigurationDescription dd = destRcDescription.getConfiguration();
			if (sd instanceof ILanguageSettingsProvidersKeeper && dd instanceof ILanguageSettingsProvidersKeeper) {
				List<ILanguageSettingsProvider> newProviders = ((ILanguageSettingsProvidersKeeper) sd)
						.getLanguageSettingProviders();
				((ILanguageSettingsProvidersKeeper) dd).setLanguageSettingProviders(newProviders);
			}
		}

		performOK();

		trackInitialSettings();
		updateData(getResDesc());
	}

	@Override
	protected void performOK() {
		if (masterPropertyPage != null && masterPropertyPage.isLanguageSettingsProvidersEnabled()) {
			masterPropertyPage.applyLanguageSettingsProvidersEnabled();
		}
	}

	@Override
	public boolean canBeVisible() {
		if (!ScannerDiscoveryLegacySupport.isLanguageSettingsProvidersFunctionalityEnabled(null)) {
			return false;
		}

		// filter out files not associated with any languages such as *.o
		if (page.isForFile()) {
			List<String> languageIds = LanguageSettingsManager.getLanguages(getResDesc());
			if (languageIds.isEmpty() || (languageIds.size() == 1 && languageIds.get(0) == null)) {
				// for legacy projects check if the file is associated with any CDT language
				IResource rc = getResource();
				if (rc instanceof IFile) {
					try {
						ILanguage language = LanguageManager.getInstance().getLanguageForFile((IFile) rc, null);
						return language != null;
					} catch (CoreException e) {
						// who cares, not even logging
					}
				}
				return false;
			} else {
				// for regular MBS project verify language against toolchain language
				for (String langId : languageIds) {
					if (langId != null) {
						ILanguage language = LanguageManager.getInstance().getLanguage(langId);
						if (language != null) {
							return true;
						}
					}
				}
			}
			return false;
		}

		return true;
	}

}
