/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *******************************************************************************/
package org.eclipse.core.internal.resources;

import java.util.*;
import org.eclipse.core.internal.utils.IStringPoolParticipant;
import org.eclipse.core.internal.utils.StringPool;

/**
 * A specialized map implementation that is optimized for a
 * small set of interned strings as keys.  The provided keys
 * MUST be instances of java.lang.String.
 *
 * Implemented as a single array that alternates keys and values.
 */
@SuppressWarnings("unchecked")
public class MarkerAttributeMap<V> implements Map<String, V>, IStringPoolParticipant {
	protected Object[] elements = null;
	protected int count = 0;

	// 8 attribute keys, 8 attribute values
	protected static final int DEFAULT_SIZE = 16;
	protected static final int GROW_SIZE = 10;

	private static final Object[] EMPTY = new Object[0];

	/**
	 * Creates a new marker attribute map of default size
	 */
	public MarkerAttributeMap() {
		this(DEFAULT_SIZE);
	}

	/**
	 * Creates a new marker attribute map.
	 * @param initialCapacity The initial number of elements that will fit in the map.
	 */
	public MarkerAttributeMap(int initialCapacity) {
		elements = initialCapacity > 0 ? new Object[initialCapacity * 2] : EMPTY;
	}

	/**
	 * Creates a new marker attribute map of default size
	 * @param map The entries in the given map will be added to the new map.
	 */
	public MarkerAttributeMap(Map<String, ? extends V> map) {
		this(map.size());
		putAll(map);
	}

	@Override
	public void clear() {
		count = 0;
		elements = EMPTY;
	}

	@Override
	public boolean containsKey(Object key) {
		if (count == 0)
			return false;
		key = ((String) key).intern();
		for (int i = 0; i < elements.length; i = i + 2)
			if (elements[i] == key)
				return true;
		return false;
	}

	@Override
	public boolean containsValue(Object value) {
		if (count == 0)
			return false;
		for (int i = 1; i < elements.length; i = i + 2)
			if (elements[i] != null && elements[i].equals(value))
				return true;
		return false;
	}

	/**
	 * This implementation does not conform properly to the specification
	 * in the Map interface.  The returned collection will not be bound to
	 * this map and will not remain in sync with this map.
	 */
	@Override
	public Set<Entry<String, V>> entrySet() {
		return toHashMap().entrySet();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Map))
			return false;
		Map<String, V> other = (Map<String, V>) o;
		//must be same size
		if (count != other.size())
			return false;

		if (count == 0)
			return true;

		//keysets must be equal
		if (!keySet().equals(other.keySet()))
			return false;

		//values for each key must be equal
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] != null && (!elements[i + 1].equals(other.get(elements[i]))))
				return false;
		}
		return true;
	}

	@Override
	public V get(Object key) {
		if (count == 0)
			return null;
		key = ((String) key).intern();
		for (int i = 0; i < elements.length; i = i + 2)
			if (elements[i] == key)
				return (V) elements[i + 1];
		return null;
	}

	/**
	 * The capacity of the map has been exceeded, grow the array by
	 * GROW_SIZE to accomodate more entries.
	 */
	protected void grow() {
		Object[] expanded = new Object[elements.length + GROW_SIZE];
		System.arraycopy(elements, 0, expanded, 0, elements.length);
		elements = expanded;
	}

	@Override
	public int hashCode() {
		int hash = 0;
		if (count == 0)
			return hash;
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] != null) {
				hash += elements[i].hashCode();
			}
		}
		return hash;
	}

	@Override
	public boolean isEmpty() {
		return count == 0;
	}

	/**
	 * This implementation does not conform properly to the specification
	 * in the Map interface.  The returned collection will not be bound to
	 * this map and will not remain in sync with this map.
	 */
	@Override
	public Set<String> keySet() {
		Set<String> result = new HashSet<>(size());
		if (count == 0)
			return result;
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] != null) {
				result.add((String) elements[i]);
			}
		}
		return result;
	}

	@Override
	public V put(String k, V value) {
		if (k == null)
			throw new NullPointerException();
		if (value == null)
			return remove(k);
		String key = k.intern();

		if (elements.length <= (count * 2))
			grow();

		// handle the case where we don't have any attributes yet
		if (count == 0) {
			elements[0] = key;
			elements[1] = value;
			count++;
			return null;
		}

		// replace existing value if it exists
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] == key) {
				Object oldValue = elements[i + 1];
				elements[i + 1] = value;
				return (V) oldValue;
			}
		}

		// otherwise add it to the list of elements.
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] == null) {
				elements[i] = key;
				elements[i + 1] = value;
				count++;
				return null;
			}
		}
		return null;
	}

	@Override
	public void putAll(Map<? extends String, ? extends V> map) {
		for (Map.Entry<? extends String, ? extends V> e : map.entrySet())
			put(e.getKey(), e.getValue());
	}

	@Override
	public V remove(Object key) {
		if (count == 0)
			return null;
		key = ((String) key).intern();
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] == key) {
				elements[i] = null;
				Object result = elements[i + 1];
				elements[i + 1] = null;
				count--;
				return (V) result;
			}
		}
		return null;
	}

	@Override
	public int size() {
		return count;
	}

	@Override
	public void shareStrings(StringPool set) {
		//copy elements for thread safety
		Object[] array = elements;
		if (array == null)
			return;
		//don't share keys because they are already interned
		for (int i = 1; i < array.length; i = i + 2) {
			Object o = array[i];
			if (o instanceof String)
				array[i] = set.add((String) o);
			else if (o instanceof IStringPoolParticipant)
				((IStringPoolParticipant) o).shareStrings(set);
		}
	}

	/**
	 * Creates a new hash map with the same contents as this map.
	 */
	private HashMap<String, V> toHashMap() {
		HashMap<String, V> result = new HashMap<>(size());
		if (count == 0)
			return result;
		for (int i = 0; i < elements.length; i = i + 2) {
			if (elements[i] != null) {
				result.put((String) elements[i], (V) elements[i + 1]);
			}
		}
		return result;
	}

	/**
	 * This implementation does not conform properly to the specification
	 * in the Map interface.  The returned collection will not be bound to
	 * this map and will not remain in sync with this map.
	 */
	@Override
	public Collection<V> values() {
		Set<V> result = new HashSet<>(size());
		if (count == 0)
			return result;
		for (int i = 1; i < elements.length; i = i + 2) {
			if (elements[i] != null) {
				result.add((V) elements[i]);
			}
		}
		return result;
	}
}
