/*
 * Java
 *
 * Copyright 2018-2020 MicroEJ Corp. All rights reserved.
 * This library is provided in source code for use, modification and test, subject to license terms.
 * Any modification of the source code will break MicroEJ Corp. warranties on the whole library.
 */
package ej.basictool.map;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.Set;

import ej.basictool.ArrayTools;

/**
 * @param <K>
 *            the type of keys maintained by this map.
 * @param <V>
 *            the type of mapped values.
 */
public abstract class AbstractWeakPackedMap<K, V> extends AbstractPackedMap<K, V> {

	/**
	 * Constructs an empty map.
	 */
	public AbstractWeakPackedMap() {
		super();
	}

	/**
	 * Constructs a map with the same mappings as the specified map.
	 *
	 * @param map
	 *            the map whose mappings are to be placed in this map.
	 * @throws NullPointerException
	 *             if the specified map is <code>null</code>.
	 */
	public AbstractWeakPackedMap(AbstractWeakPackedMap<K, V> map) {
		super(map);
	}

	@Override
	public boolean containsKey(Object key) {
		clean();
		if (key == null) {
			return false;
		}
		return super.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		clean();
		return super.containsValue(value);
	}

	@Override
	public V get(Object key) {
		clean();
		return super.get(key);
	}

	@Override
	public Set<K> keySet() {
		return new WeakPackedMapKeySet(this);
	}

	class WeakPackedMapKeySet extends PackedMapKeySet {

		public WeakPackedMapKeySet(AbstractPackedMap<K, V> map) {
			super(map);
		}

		@Override
		public Object[] toArray() {
			Object[] keysValues = AbstractWeakPackedMap.this.keysValues;
			int size = keysValues.length / 2;
			Object[] result = new Object[size];
			copy(keysValues, size, result);
			return result;
		}

		@Override
		public <T> T[] toArray(T[] a) {
			Object[] keysValues = AbstractWeakPackedMap.this.keysValues;
			int size = keysValues.length / 2;
			T[] result;
			if (a.length <= size) {
				result = a;
			} else {
				result = ArrayTools.createNewArray(a, size);
			}
			copy(keysValues, size, result);
			return result;
		}

		private void copy(Object[] keysValues, int size, Object[] result) {
			for (int i = 0; i < size; ++i) {
				result[i] = unwrapKey(keysValues[i]);
			}
		}

	}

	@Override
	public V put(K key, V value) {
		clean();
		return super.put(key, value);
	}

	@Override
	public V remove(Object key) {
		clean();
		return super.remove(key);
	}

	@Override
	public int size() {
		clean();
		return super.size();
	}

	@Override
	protected Object wrapKey(K key) {
		return new WeakPackedMapReference<>(key);
	}

	@SuppressWarnings("unchecked")
	@Override
	protected int getWrappedKeyHashCode(Object wrappedKey) {
		return ((WeakPackedMapReference<?>) wrappedKey).hashCode;
	}

	// Save the hash code of the key to avoid unwrapping the reference when browsing the packed map.
	class WeakPackedMapReference<T> extends WeakReference<T> {

		private final int hashCode;

		public WeakPackedMapReference(T referent) {
			super(referent);
			this.hashCode = referent.hashCode();
		}

		@Override
		public int hashCode() {
			return this.hashCode;
		}

	}

	@Override
	@SuppressWarnings("unchecked")
	protected K unwrapKey(Object wrappedKey) {
		K k = ((Reference<K>) wrappedKey).get();
		return k;
	}

	private void clean() {
		this.keysValues = clean(this, this.keysValues);
	}

	static <K, V> Object[] clean(AbstractPackedMap<K, V> map, Object[] keysValues) {
		int size = keysValues.length / 2;
		for (int i = 0; i < size; i++) {
			Object object = map.unwrapKey(keysValues[i]);
			if (object == null) {
				int arrayLength = keysValues.length;
				int valueIndex = (arrayLength / 2) + i;
				Object[] newArray = new Object[arrayLength - 2];
				System.arraycopy(keysValues, 0, newArray, 0, i);
				System.arraycopy(keysValues, i + 1, newArray, i, valueIndex - i - 1);
				System.arraycopy(keysValues, valueIndex + 1, newArray, valueIndex - 1, arrayLength - valueIndex - 1);
				keysValues = newArray;
				i--;
				size--;
			}
		}
		return keysValues;
	}

}
