/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.viewsupport;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Widget;

import org.eclipse.core.resources.IResource;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;

/**
 * Helper class for updating error markers and other decorators that work on resources.
 * Items are mapped to their element's underlying resource.
 * Method <code>resourceChanged</code> updates all items that are affected from the changed
 * elements.
 */
public class ResourceToItemsMapper {

	public interface IContentViewerAccessor {
		void doUpdateItem(Widget item);
	}


	private static final int NUMBER_LIST_REUSE= 10;

	// map from IResource to Item or List<Item>
	private HashMap<IResource, Object> fResourceToItem;
	private Stack<List<Item>> fReuseLists;

	private IContentViewerAccessor fContentViewerAccess;

	public ResourceToItemsMapper(IContentViewerAccessor viewer) {
		fResourceToItem= new HashMap<>();
		fReuseLists= new Stack<>();

		fContentViewerAccess= viewer;
	}

	/**
	 * Must be called from the UI thread.
	 * @param changedResource Changed resource
	 */
	public void resourceChanged(IResource changedResource) {
		Object obj= fResourceToItem.get(changedResource);
		if (obj == null) {
			// not mapped
		} else if (obj instanceof Item) {
			updateItem((Item) obj);
		} else { // List of Items
			@SuppressWarnings("unchecked")
			List<Item> list= (List<Item>) obj;
			for (Item element : list) {
				updateItem(element);
			}
		}
	}

	private void updateItem(Item item) {
		if (!item.isDisposed()) {
			fContentViewerAccess.doUpdateItem(item);
		}
	}

	/**
	 * Adds a new item to the map.
	 * @param element Element to map
	 * @param item The item used for the element
	 */
	public void addToMap(Object element, Item item) {
		IResource resource= getCorrespondingResource(element);
		if (resource != null) {
			Object existingMapping= fResourceToItem.get(resource);
			if (existingMapping == null) {
				fResourceToItem.put(resource, item);
			} else if (existingMapping instanceof Item) {
				if (existingMapping != item) {
					List<Item> list= getNewList();
					list.add((Item) existingMapping);
					list.add(item);
					fResourceToItem.put(resource, list);
				}
			} else { // List
				@SuppressWarnings("unchecked")
				List<Item> list= (List<Item>) existingMapping;
				if (!list.contains(item)) {
					list.add(item);
				}
			}
		}
	}

	/**
	 * Removes an element from the map.
	 * @param element The data element
	 * @param item The table or tree item
	 */
	public void removeFromMap(Object element, Item item) {
		IResource resource= getCorrespondingResource(element);
		if (resource != null) {
			Object existingMapping= fResourceToItem.get(resource);
			if (existingMapping == null) {
				return;
			} else if (existingMapping instanceof Item) {
				fResourceToItem.remove(resource);
			} else { // List
				@SuppressWarnings("unchecked")
				List<Item> list= (List<Item>) existingMapping;
				list.remove(item);
				if (list.isEmpty()) {
					fResourceToItem.remove(resource);
					releaseList(list);
				}
			}
		}
	}

	private List<Item> getNewList() {
		if (!fReuseLists.isEmpty()) {
			return fReuseLists.pop();
		}
		return new ArrayList<>(2);
	}

	private void releaseList(List<Item> list) {
		if (fReuseLists.size() < NUMBER_LIST_REUSE) {
			fReuseLists.push(list);
		}
	}

	/**
	 * Clears the map.
	 */
	public void clearMap() {
		fResourceToItem.clear();
	}

	/**
	 * Tests if the map is empty
	 * @return Returns if there are mappings
	 */
	public boolean isEmpty() {
		return fResourceToItem.isEmpty();
	}

	/**
	 * Method that decides which elements can have error markers
	 * Returns null if an element can not have error markers.
	 * @param element The input element
	 * @return Returns the corresponding resource or null
	 */
	private static IResource getCorrespondingResource(Object element) {
		if (element instanceof IJavaElement) {
			IJavaElement elem= (IJavaElement) element;
			IResource res= elem.getResource();
			if (res == null) {
				ICompilationUnit cu= (ICompilationUnit) elem.getAncestor(IJavaElement.COMPILATION_UNIT);
				if (cu != null) {
					// elements in compilation units are mapped to the underlying resource of the original cu
					res= cu.getResource();
				}
			}
			return res;
		} else if (element instanceof IResource) {
			return (IResource) element;
		}
		return null;
	}

}
