/*******************************************************************************
 * Copyright (c) 2007, 2016 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.cdt.core.parser.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Provides functionality similar to a Map, with the feature that char arrays
 * and sections of char arrays (known as slices) may be used as keys.
 *
 * This class is useful because small pieces of an existing large char[] buffer
 * can be directly used as map keys. This avoids the need to create many String
 * objects as would normally be needed as keys in a standard java.util.Map.
 * Thus performance is improved in the CDT core.
 *
 * Most methods are overloaded with two versions, one that uses a
 * section of a char[] as the key (a slice), and one that uses
 * the entire char[] as the key.
 *
 * This class is intended as a replacement for CharArrayObjectMap.
 *
 * ex:
 * char[] key = "one two three".toCharArray();
 * map.put(key, 4, 3, Integer.valueOf(99));
 * map.get(key, 4, 3); // returns 99
 * map.get("two".toCharArray()); // returns 99
 *
 * @author Mike Kucera
 *
 * @param <V>
 */
public final class CharArrayMap<V> {

	/**
	 * Wrapper class used as keys in the map. The purpose
	 * of this class is to provide implementations of
	 * equals() and hashCode() that operate on array slices.
	 *
	 * This class is private so it is assumed that the arguments
	 * passed to the constructor are legal.
	 */
	private static final class Key implements Comparable<Key> {
		private final char[] buffer;
		private final int start;
		private final int length;

		public Key(char[] buffer, int start, int length) {
			this.buffer = buffer;
			this.length = length;
			this.start = start;
		}

		/**
		 * @throws NullPointerException if buffer is null
		 */
		public Key(char[] buffer) {
			this.buffer = buffer;
			this.length = buffer.length; // throws NPE
			this.start = 0;
		}

		@Override
		public boolean equals(Object x) {
			if (this == x)
				return true;
			if (!(x instanceof Key))
				return false;

			Key k = (Key) x;
			if (length != k.length)
				return false;

			for (int i = start, j = k.start; i < length; i++, j++) {
				if (buffer[i] != k.buffer[j]) {
					return false;
				}
			}
			return true;
		}

		@Override
		public int hashCode() {
			int result = 17;
			for (int i = start; i < start + length; i++) {
				result = 37 * result + buffer[i];
			}
			return result;
		}

		@SuppressWarnings("nls")
		@Override
		public String toString() {
			String slice = new String(buffer, start, length);
			return "'" + slice + "'@(" + start + "," + length + ")";
		}

		@Override
		public int compareTo(Key other) {
			char[] b1 = buffer, b2 = other.buffer;

			for (int i = start, j = other.start; i < b1.length && j < b2.length; i++, j++) {
				if (b1[i] != b2[j])
					return b1[i] < b2[j] ? -1 : 1;
			}
			return b1.length - b2.length;
		}
	}

	/**
	 * Used to enforce preconditions.
	 * Note that the NPE thrown by mutator methods is thrown from the Key constructor.
	 *
	 * @throws IndexOutOfBoundsException if boundaries are wrong in any way
	 */
	private static void checkBoundaries(char[] chars, int start, int length) {
		if (start < 0 || length < 0 || start >= chars.length || start + length > chars.length)
			throw new IndexOutOfBoundsException("Buffer length: " + chars.length + //$NON-NLS-1$
					", Start index: " + start + //$NON-NLS-1$
					", Length: " + length); //$NON-NLS-1$
	}

	private final Map<Key, V> map;

	/**
	 * Constructs an empty CharArrayMap with default initial capacity.
	 */
	public CharArrayMap() {
		map = new HashMap<>();
	}

	/**
	 * Static factory method that constructs an empty CharArrayMap with default initial capacity,
	 * and the map will be kept in ascending key order.
	 *
	 * Characters are compared using a strictly numerical comparison; it is not locale-dependent.
	 */
	public static <V> CharArrayMap<V> createOrderedMap() {
		// TreeMap does not have a constructor that takes an initial capacity
		return new CharArrayMap<>(new TreeMap<Key, V>());
	}

	private CharArrayMap(Map<Key, V> map) {
		assert map != null;
		this.map = map;
	}

	/**
	 * Constructs an empty CharArrayMap with the given initial capacity.
	 * @throws IllegalArgumentException if the initial capacity is negative
	 */
	public CharArrayMap(int initialCapacity) {
		map = new HashMap<>(initialCapacity);
	}

	/**
	 * Creates a new mapping in this map, uses the given array slice as the key.
	 * If the map previously contained a mapping for this key, the old value is replaced.
	 * @throws NullPointerException if chars is null
	 * @throws IndexOutOfBoundsException if the boundaries specified by start and length are out of range
	 */
	public void put(char[] chars, int start, int length, V value) {
		checkBoundaries(chars, start, length);
		map.put(new Key(chars, start, length), value);
	}

	/**
	 * Creates a new mapping in this map, uses all of the given array as the key.
	 * If the map previously contained a mapping for this key, the old value is replaced.
	 * @throws NullPointerException if chars is null
	 */
	public void put(char[] chars, V value) {
		map.put(new Key(chars), value);
	}

	/**
	 * Returns the value to which the specified array slice is mapped in this map,
	 * or null if the map contains no mapping for this key.
	 * @throws NullPointerException if chars is null
	 * @throws IndexOutOfBoundsException if the boundaries specified by start and length are out of range
	 */
	public V get(char[] chars, int start, int length) {
		checkBoundaries(chars, start, length);
		return map.get(new Key(chars, start, length));
	}

	/**
	 * Returns the value to which the specified array is mapped in this map,
	 * or null if the map contains no mapping for this key.
	 * @throws NullPointerException if chars is null
	 */
	public V get(char[] chars) {
		return map.get(new Key(chars));
	}

	/**
	 * Removes the mapping for the given array slice if present.
	 * Returns the value object that corresponded to the key
	 * or null if the key was not in the map.
	 * @throws NullPointerException if chars is null
	 * @throws IndexOutOfBoundsException if the boundaries specified by start and length are out of range
	 */
	public V remove(char[] chars, int start, int length) {
		checkBoundaries(chars, start, length);
		return map.remove(new Key(chars, start, length));
	}

	/**
	 * Removes the mapping for the given array if present.
	 * Returns the value object that corresponded to the key
	 * or null if the key was not in the map.
	 * @throws NullPointerException if chars is null
	 */
	public V remove(char[] chars) {
		return map.remove(new Key(chars));
	}

	/**
	 * Returns true if the given key has a value associated with it in the map.
	 * @throws NullPointerException if chars is null
	 * @throws IndexOutOfBoundsException if the boundaries specified by start and length are out of range
	 */
	public boolean containsKey(char[] chars, int start, int length) {
		checkBoundaries(chars, start, length);
		return map.containsKey(new Key(chars, start, length));
	}

	/**
	 * Returns true if the given key has a value associated with it in the map.
	 * @throws NullPointerException if chars is null
	 */
	public boolean containsKey(char[] chars) {
		return map.containsKey(new Key(chars));
	}

	/**
	 * Returns true if the given value is contained in the map.
	 */
	public boolean containsValue(V value) {
		return map.containsValue(value);
	}

	/**
	 * Use this in a foreach loop.
	 */
	public Collection<V> values() {
		return map.values();
	}

	/**
	 * Returns the keys stored in the map.
	 */
	public Collection<char[]> keys() {
		Set<Key> keys = map.keySet();
		ArrayList<char[]> r = new ArrayList<>(keys.size());
		for (Key key : keys) {
			r.add(CharArrayUtils.extract(key.buffer, key.start, key.length));
		}
		return r;
	}

	/**
	 * Removes all mappings from the map.
	 */
	public void clear() {
		map.clear();
	}

	/**
	 * Returns the number of mappings.
	 */
	public int size() {
		return map.size();
	}

	/**
	 * Returns true if the map is empty.
	 */
	public boolean isEmpty() {
		return map.isEmpty();
	}

	/**
	 * Returns a String representation of the map.
	 */
	@Override
	public String toString() {
		return map.toString();
	}

}
