// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.http;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;

/**
 * General purpose HTTP multi-map. It's optimized Linked-HashMap, designed for
 * small number of items and <code>String</code> non-null keys. It stores keys
 * in case-sensitive way, but, by default, you can read them in case-insensitive
 * way.
 */
public class HttpMultiMap<V> implements Iterable<Map.Entry<String, V>>  {

	private static final int BUCKET_SIZE = 31;
	private final boolean caseSensitive;

	@SuppressWarnings("unchecked")
	private final MapEntry<V>[] entries = new MapEntry[BUCKET_SIZE + 1];
	private final MapEntry<V> head = new MapEntry<>(-1, null, null);

	/**
	 * Creates new case-insensitive multimap.
	 */
	public static <T> HttpMultiMap<T> newCaseInsensitveMap() {
		return new HttpMultiMap<>(false);
	}
	/**
	 * Creates new case-insensitive map.
	 */
	public static <T> HttpMultiMap<T> newCaseSensitveMap() {
		return new HttpMultiMap<>(true);
	}

	protected HttpMultiMap(boolean caseSensitive) {
		head.before = head.after = head;
		this.caseSensitive = caseSensitive;
	}

	/**
	 * Calculates hash value of the input string.
	 */
	private int hash(String name) {
		int h = 0;
		for (int i = name.length() - 1; i >= 0; i--) {
			char c = name.charAt(i);
			if (!caseSensitive) {
				if (c >= 'A' && c <= 'Z') {
					c += 32;
				}
			}
			h = 31 * h + c;
		}

		if (h > 0) {
			return h;
		}
		if (h == Integer.MIN_VALUE) {
			return Integer.MAX_VALUE;
		}
		return -h;
	}

	/**
	 * Calculates bucket index from the hash.
	 */
	private static int index(int hash) {
		return hash & BUCKET_SIZE;
	}

	/**
	 * Returns <code>true</code> if two names are the same.
	 */
	private boolean eq(String name1, String name2) {
		int nameLen = name1.length();
		if (nameLen != name2.length()) {
			return false;
		}

		for (int i = nameLen - 1; i >= 0; i--) {
			char c1 = name1.charAt(i);
			char c2 = name2.charAt(i);

			if (c1 != c2) {
				if (caseSensitive) {
					return false;
				}
				if (c1 >= 'A' && c1 <= 'Z') {
					c1 += 32;
				}
				if (c2 >= 'A' && c2 <= 'Z') {
					c2 += 32;
				}
				if (c1 != c2) {
					return false;
				}
			}
		}
		return true;
	}

	// ---------------------------------------------------------------- basic

	/**
	 * Returns the number of keys. This is not the number of all elements.
	 * Not optimized.
	 */
	public int size() {
		return names().size();
	}

	/**
	 * Clears the map.
	 */
	public HttpMultiMap<V> clear() {
		for (int i = 0; i < entries.length; i++) {
			entries[i] = null;
		}
		head.before = head.after = head;
		return this;
	}

	/**
	 * Returns <code>true</code> if name exist.
	 */
	public boolean contains(String name) {
		return getEntry(name) != null;
	}

	/**
	 * Returns <code>true</code> if map is empty.
	 */
	public boolean isEmpty() {
		return head == head.after;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, V> entry : this) {
			sb.append(entry).append('\n');
		}
		return sb.toString();
	}

	// ---------------------------------------------------------------- set/add

	private HttpMultiMap<V> _set(Iterable<Map.Entry<String, V>> map) {
		clear();
		for (Map.Entry<String, V> entry : map) {
			add(entry.getKey(), entry.getValue());
		}
		return this;
	}

	public HttpMultiMap<V> setAll(HttpMultiMap<V> multiMap) {
		return _set(multiMap);
	}

	public HttpMultiMap<V> setAll(Map<String, V> map) {
		return _set(map.entrySet());
	}

	public HttpMultiMap<V> set(final String name, final V value) {
		int h = hash(name);
		int i = index(h);
		_remove(h, i, name);
		_add(h, i, name, value);
		return this;
	}

	public HttpMultiMap<V> setAll(final String name, final Iterable<V> values) {
		int h = hash(name);
		int i = index(h);

		_remove(h, i, name);
		for (V v : values) {
			_add(h, i, name, v);
		}

		return this;
	}

	public HttpMultiMap<V> add(final String name, final V value) {
		int h = hash(name);
		int i = index(h);
		_add(h, i, name, value);
		return this;
	}

	public HttpMultiMap<V> addAll(String name, Iterable<V> values) {
		int h = hash(name);
		int i = index(h);
		for (V value : values) {
			_add(h, i, name, value);
		}
		return this;
	}

	public HttpMultiMap<V> addAll(HttpMultiMap<V> map) {
		for (Map.Entry<String, V> entry : map.entries()) {
			add(entry.getKey(), entry.getValue());
		}
		return this;
	}

	public HttpMultiMap<V> addAll(Map<String, V> map) {
		for (Map.Entry<String, V> entry : map.entrySet()) {
			add(entry.getKey(), entry.getValue());
		}
		return this;
	}

	private void _add(final int hash, final int index, final String name, final V value) {
		// update the hash table
		MapEntry<V> e = entries[index];
		MapEntry<V> newEntry;
		entries[index] = newEntry = new MapEntry<>(hash, name, value);
		newEntry.next = e;

		// update the linked list
		newEntry.addBefore(head);
	}

	// ---------------------------------------------------------------- remove

	public HttpMultiMap<V> remove(final String name) {
		int h = hash(name);
		int i = index(h);
		_remove(h, i, name);
		return this;
	}

	private void _remove(final int hash, final int index, String name) {
		MapEntry<V> e = entries[index];
		if (e == null) {
			return;
		}

		for (; ; ) {
			if (e.hash == hash && eq(name, e.key)) {
				e.remove();
				MapEntry<V> next = e.next;
				if (next != null) {
					entries[index] = next;
					e = next;
				}
				else {
					entries[index] = null;
					return;
				}
			}
			else {
				break;
			}
		}

		for (; ; ) {
			MapEntry<V> next = e.next;
			if (next == null) {
				break;
			}
			if (next.hash == hash && eq(name, next.key)) {
				e.next = next.next;
				next.remove();
			}
			else {
				e = next;
			}
		}
	}

	// ---------------------------------------------------------------- get

	/**
	 * Returns the first value from the map associated with the name.
	 * Returns <code>null</code> if name does not exist or
	 * if associated value is <code>null</code>.
	 */
	public V get(final String name) {
		Map.Entry<String, V> entry = getEntry(name);

		if (entry == null) {
			return null;
		}
		return entry.getValue();
	}

	/**
	 * Returns first entry for given name. Returns <code>null</code> if entry
	 * does not exist.
	 */
	public Map.Entry<String, V> getEntry(final String name) {
		int h = hash(name);
		int i = index(h);
		MapEntry<V> e = entries[i];
		while (e != null) {
			if (e.hash == h && eq(name, e.key)) {
				return e;
			}

			e = e.next;
		}
		return null;
	}

	/**
	 * Returns all values associated with the name.
	 */
	public List<V> getAll(final String name) {
		LinkedList<V> values = new LinkedList<>();

		int h = hash(name);
		int i = index(h);
		MapEntry<V> e = entries[i];
		while (e != null) {
			if (e.hash == h && eq(name, e.key)) {
				values.addFirst(e.getValue());
			}
			e = e.next;
		}
		return values;
	}

	// ---------------------------------------------------------------- iterate

	/**
	 * Returns iterator of all entries.
	 */
	public Iterator<Map.Entry<String, V>> iterator() {
		final MapEntry[] e = {head.after};

		return new Iterator<Map.Entry<String, V>>() {
			@Override
			public boolean hasNext() {
				return e[0] != head;
			}

			@Override
			@SuppressWarnings("unchecked")
			public Map.Entry<String, V> next() {
				if (!hasNext()) {
					throw new NoSuchElementException("No next() entry in the iteration");
				}
				MapEntry<V> next = e[0];
				e[0] = e[0].after;
				return next;
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	public Set<String> names() {
		Set<String> names = new TreeSet<>(caseSensitive ? null : String.CASE_INSENSITIVE_ORDER);

		MapEntry e = head.after;
		while (e != head) {
			names.add(e.getKey());
			e = e.after;
		}
		return names;
	}

	/**
	 * Returns all the entries of this map. Case sensitivity does not influence
	 * the returned list, it always contains all of the values.
	 */
	public List<Map.Entry<String, V>> entries() {
		List<Map.Entry<String, V>> all = new LinkedList<>();

		MapEntry<V> e = head.after;
		while (e != head) {
			all.add(e);
			e = e.after;
		}
		return all;
	}

	private static final class MapEntry<V> implements Map.Entry<String, V> {
		final int hash;
		final String key;
		V value;
		MapEntry<V> next;
		MapEntry<V> before, after;

		private MapEntry(int hash, String key, V value) {
			this.hash = hash;
			this.key = key;
			this.value = value;
		}

		void remove() {
			before.after = after;
			after.before = before;
		}

		void addBefore(MapEntry<V> e) {
			after = e;
			before = e.before;
			before.after = this;
			after.before = this;
		}

		public String getKey() {
			return key;
		}

		public V getValue() {
			return value;
		}

		public V setValue(V value) {
			V oldValue = this.value;
			this.value = value;
			return oldValue;
		}

		public String toString() {
			return getKey() + ": " + getValue();
		}
	}
}
