/*
 * LazyTreeMap.java - This file is part of the Jakstab project.
 * Copyright 2007-2015 Johannes Kinder <jk@jakstab.org>
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, see <http://www.gnu.org/licenses/>.
 */
package org.jakstab.util;

import java.util.*;

import org.jakstab.util.Logger;

/**
 * @author Johannes Kinder
 */
public class LazyTreeMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {

	@SuppressWarnings("unused")
	private static final Logger logger = Logger.getLogger(LazyTreeMap.class);
	
	private class ReferenceCountingTreeMap<L, W> extends TreeMap<L, W> {
		private static final long serialVersionUID = -6043771577607149364L;
		private int referenceCount;
		private int hashCode = 0;
		
		public ReferenceCountingTreeMap() {
			super();
			referenceCount = 1;
		}

		public ReferenceCountingTreeMap(ReferenceCountingTreeMap<L, W> innerMap) {
			super(innerMap);
			referenceCount = 1;
		}
		
		public void addRef() {
			referenceCount++;
		}
		
		public void release() {
			referenceCount--;
		}
		
		public boolean isShared() {
			return referenceCount > 1;
		}
	}
	
	private ReferenceCountingTreeMap<K, V> innerMap;
	private transient Set<Map.Entry<K, V>> entries;
	
	public LazyTreeMap() {
		innerMap = new ReferenceCountingTreeMap<K, V>();
	}
	
	public LazyTreeMap(LazyTreeMap<K, V> other) {
		innerMap = other.innerMap;
		innerMap.addRef();
	}
	
	private boolean makeExclusive() {
		// makeExclsusive is called before all updates to inner map, 
		// so reset cached hashcode
		innerMap.hashCode = 0;
		
		if (innerMap.isShared()) {
			innerMap.release();
			innerMap = new ReferenceCountingTreeMap<K, V>(innerMap);
			assert !innerMap.isShared();
			return true;
		}
		return false;
	}
	
	@Override
	public void clear() {
		makeExclusive();
		innerMap.clear();
	}

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

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

	@Override
	public V remove(Object key) {
		makeExclusive();
		return innerMap.remove(key);
	}

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

	@Override
	public boolean containsKey(Object key) {
		return innerMap.containsKey(key);
	}

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

	@Override
	public boolean equals(Object o) {
		if (o == null)
			return false;
		LazyTreeMap<?, ?> other = (LazyTreeMap<?, ?>)o;
		return innerMap.equals(other.innerMap);
	}

	@Override
	public int hashCode() {
		if (innerMap.hashCode == 0)
			innerMap.hashCode = innerMap.hashCode();
		return innerMap.hashCode;
	}

	@Override
	public boolean isEmpty() {
		return innerMap.isEmpty();
	}

	@Override
	protected void finalize() throws Throwable {
		innerMap.release();
		super.finalize();
	}

	@Override
	public Set<Map.Entry<K, V>> entrySet() {
		if (entries == null)
			entries = new EntrySet();
		return entries;
	}

	private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
		@Override
		public Iterator<Map.Entry<K, V>> iterator() {
			return new MapEntryIterator();
		}

		@Override
		public boolean contains(Object o) {
			Map.Entry<?, ?> entry = (Map.Entry<?, ?>)o;
			V value = get(entry.getKey());
			if (value == null)
				return entry.getValue() == null;
			else
				return value.equals(entry.getValue());
		}

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

	}
	
	private final class MapEntryIterator implements Iterator<Map.Entry<K,V>> {
		private Iterator<Map.Entry<K,V>> innerIt = 
			innerMap.entrySet().iterator();
		private Map.Entry<K, V> last = null;

		@Override
		public boolean hasNext() {
			return innerIt.hasNext();
		}

		@Override
		public Map.Entry<K, V> next() {
			last = innerIt.next();
			return last;
		}

		@Override
		public void remove() {
			if (makeExclusive()) {
				// yes, we copied the map, so refresh iterator
				innerIt = innerMap.entrySet().iterator();
				// fast forward to old position
				if (last != null) {
					while (!last.equals(innerIt.next()))
						assert innerIt.hasNext();
				}
			}
			innerIt.remove();
		}
	}

}
