/*
* Copyright (C) 2008 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.collect;

import static com.google.common.collect.CollectPreconditions.checkEntryNotNull;

import javax.annotation.Nullable;

import com.google.common.annotations.GwtCompatible;
import com.google.common.collect.ImmutableMapEntry.TerminalEntry;

/**
 * Implementation of {@link ImmutableMap} with two or more entries.
 *
 * @author Jesse Wilson
 * @author Kevin Bourrillion
 * @author Gregory Kick
 */
@GwtCompatible(serializable = true, emulated = true)
final class RegularImmutableMap<K, V> extends ImmutableMap<K, V> {

	// entries in insertion order
	private final transient ImmutableMapEntry<K, V>[] entries;
	// array of linked lists of entries
	private final transient ImmutableMapEntry<K, V>[] table;
	// 'and' with an int to get a table index
	private final transient int mask;

	RegularImmutableMap(TerminalEntry<?, ?>... theEntries) {
		this(theEntries.length, theEntries);
	}

	/**
	 * Constructor for RegularImmutableMap that takes as input an array of
	 * {@code TerminalEntry} entries. Assumes that these entries have already been
	 * checked for null.
	 * 
	 * <p>
	 * This allows reuse of the entry objects from the array in the actual
	 * implementation.
	 */
	RegularImmutableMap(int size, TerminalEntry<?, ?>[] theEntries) {
		entries = createEntryArray(size);
		int tableSize = Hashing.closedTableSize(size, MAX_LOAD_FACTOR);
		table = createEntryArray(tableSize);
		mask = tableSize - 1;
		for (int entryIndex = 0; entryIndex < size; entryIndex++) {
			@SuppressWarnings("unchecked")
			TerminalEntry<K, V> entry = (TerminalEntry<K, V>) theEntries[entryIndex];
			K key = entry.getKey();
			int tableIndex = Hashing.smear(key.hashCode()) & mask;
			@Nullable
			ImmutableMapEntry<K, V> existing = table[tableIndex];
			// prepend, not append, so the entries can be immutable
			ImmutableMapEntry<K, V> newEntry = (existing == null) ? entry
					: new NonTerminalMapEntry<K, V>(entry, existing);
			table[tableIndex] = newEntry;
			entries[entryIndex] = newEntry;
			checkNoConflictInBucket(key, newEntry, existing);
		}
	}

	/**
	 * Constructor for RegularImmutableMap that makes no assumptions about the input
	 * entries.
	 */
	RegularImmutableMap(Entry<?, ?>[] theEntries) {
		int size = theEntries.length;
		entries = createEntryArray(size);
		int tableSize = Hashing.closedTableSize(size, MAX_LOAD_FACTOR);
		table = createEntryArray(tableSize);
		mask = tableSize - 1;
		for (int entryIndex = 0; entryIndex < size; entryIndex++) {
			@SuppressWarnings("unchecked") // all our callers carefully put in only Entry<K, V>s
			Entry<K, V> entry = (Entry<K, V>) theEntries[entryIndex];
			K key = entry.getKey();
			V value = entry.getValue();
			checkEntryNotNull(key, value);
			int tableIndex = Hashing.smear(key.hashCode()) & mask;
			@Nullable
			ImmutableMapEntry<K, V> existing = table[tableIndex];
			// prepend, not append, so the entries can be immutable
			ImmutableMapEntry<K, V> newEntry = (existing == null) ? new TerminalEntry<K, V>(key, value)
					: new NonTerminalMapEntry<K, V>(key, value, existing);
			table[tableIndex] = newEntry;
			entries[entryIndex] = newEntry;
			checkNoConflictInBucket(key, newEntry, existing);
		}
	}

	private void checkNoConflictInBucket(K key, ImmutableMapEntry<K, V> entry, ImmutableMapEntry<K, V> bucketHead) {
		for (; bucketHead != null; bucketHead = bucketHead.getNextInKeyBucket()) {
			checkNoConflict(!key.equals(bucketHead.getKey()), "key", entry, bucketHead);
		}
	}

	private static final class NonTerminalMapEntry<K, V> extends ImmutableMapEntry<K, V> {
		private final ImmutableMapEntry<K, V> nextInKeyBucket;

		NonTerminalMapEntry(K key, V value, ImmutableMapEntry<K, V> nextInKeyBucket) {
			super(key, value);
			this.nextInKeyBucket = nextInKeyBucket;
		}

		NonTerminalMapEntry(ImmutableMapEntry<K, V> contents, ImmutableMapEntry<K, V> nextInKeyBucket) {
			super(contents);
			this.nextInKeyBucket = nextInKeyBucket;
		}

		@Override
		ImmutableMapEntry<K, V> getNextInKeyBucket() {
			return nextInKeyBucket;
		}

		@Override
		@Nullable
		ImmutableMapEntry<K, V> getNextInValueBucket() {
			return null;
		}

	}

	/**
	 * Closed addressing tends to perform well even with high load factors. Being
	 * conservative here ensures that the table is still likely to be relatively
	 * sparse (hence it misses fast) while saving space.
	 */
	private static final double MAX_LOAD_FACTOR = 1.2;

	/**
	 * Creates an {@code ImmutableMapEntry} array to hold parameterized entries. The
	 * result must never be upcast back to ImmutableMapEntry[] (or Object[], etc.),
	 * or allowed to escape the class.
	 */
	@SuppressWarnings("unchecked") // Safe as long as the javadocs are followed
	private ImmutableMapEntry<K, V>[] createEntryArray(int size) {
		return new ImmutableMapEntry[size];
	}

	@Override
	public V get(@Nullable Object key) {
		if (key == null) {
			return null;
		}
		int index = Hashing.smear(key.hashCode()) & mask;
		for (ImmutableMapEntry<K, V> entry = table[index]; entry != null; entry = entry.getNextInKeyBucket()) {
			K candidateKey = entry.getKey();

			/*
			 * Assume that equals uses the == optimization when appropriate, and that it
			 * would check hash codes as an optimization when appropriate. If we did these
			 * things, it would just make things worse for the most performance-conscious
			 * users.
			 */
			if (key.equals(candidateKey)) {
				return entry.getValue();
			}
		}
		return null;
	}

	@Override
	public int size() {
		return entries.length;
	}

	@Override
	boolean isPartialView() {
		return false;
	}

	@Override
	ImmutableSet<Entry<K, V>> createEntrySet() {
		return new EntrySet();
	}

	@SuppressWarnings("serial") // uses writeReplace(), not default serialization
	private class EntrySet extends ImmutableMapEntrySet<K, V> {
		@Override
		ImmutableMap<K, V> map() {
			return RegularImmutableMap.this;
		}

		@Override
		public UnmodifiableIterator<Entry<K, V>> iterator() {
			return asList().iterator();
		}

		@Override
		ImmutableList<Entry<K, V>> createAsList() {
			return new RegularImmutableAsList<Entry<K, V>>(this, entries);
		}
	}

	// This class is never actually serialized directly, but we have to make the
	// warning go away (and suppressing would suppress for all nested classes too)
	private static final long serialVersionUID = 0;
}
