package com.googlecode.objectify;

import com.googlecode.objectify.impl.ref.LiveRef;

import java.io.Serializable;


/**
 * <p>Ref<?> is a Key<?> which allows the entity value to be fetched directly.</p>
 *
 * <p>Note that the methods might or might not throw runtime exceptions related to datastore operations;
 * ConcurrentModificationException, DatastoreTimeoutException, DatastoreFailureException, and DatastoreNeedIndexException.
 * Some Refs hide datastore operations that could throw these exceptions.</p>
 *
 * @author Jeff Schnitzer <jeff@infohazard.org>
 */
abstract public class Ref<T> implements Serializable, Comparable<Ref<T>>
{
	private static final long serialVersionUID = 1L;

	/** Key.create(Blah.class, id) is easier to type than new Key<Blah>(Blah.class, id) */
	public static <T> Ref<T> create(Key<T> key) {
		if (key == null)
			throw new NullPointerException("Cannot create a Ref from a null key");

		return new LiveRef<>(key);
	}

	/** Creates a Ref from a registered pojo entity */
	public static <T> Ref<T> create(T value) {
		Key<T> key = Key.create(value);
		return create(key);
	}

	/** The key associated with this ref */
	protected Key<T> key;

	/** For GWT serialization */
	protected Ref() {}

	/**
	 * Create a Ref based on the key, with the specified session
	 */
	protected Ref(Key<T> key) {
		if (key == null)
			throw new NullPointerException("Cannot create a Ref for a null key");

		this.key = key;
	}

	/**
	 * @return the key associated with this Ref
	 */
	public Key<T> key() {
		return key;
	}

	/**
	 * Obtain the entity value associated with the key. Will pull from session if present, otherwise will
	 * fetch from the datastore.
	 *
	 * @return the entity referenced, or null if the entity was not found
	 */
	abstract public T get();

	/**
	 * If an entity has been loaded into the session or is otherwise available, this will return true.
	 * Calls to get() will not require a trip to backing store.
	 * Note that even when loaded, get() can still return null if there is no entity which corresponds to the key.
	 *
	 * @return true if the value is in the session or otherwise immediately available; false if get() will
	 * require a trip to the datastore or memcache.
	 */
	abstract public boolean isLoaded();

	/**
	 * This method exists to facilitate serialization via javabeans conventions. Unlike get(),
	 * it will return null if isLoaded() is false.
	 *
	 * @return the entity referenced, or null if either the entity was not found or isLoaded() is false.
	 */
	final public T getValue() {
		return isLoaded() ? get() : null;
	}

	/**
	 * Same as key() but conforms to JavaBeans conventions in case this is being processed by a JSON
	 * converter or expression language.
	 */
	final public Key<T> getKey() {
		return key();
	}

	/**
	 * Obtain the entity value, throwing an exception if the entity was not found.
	 *
	 * @return the entity referenced. Never returns null.
	 * @throws NotFoundException if the specified entity was not found
	 */
	final public T safe() throws NotFoundException {
		T t = this.get();
		if (t == null)
			throw new NotFoundException(key());
		else
			return t;
	}

	/** Comparison is based on key */
	@Override
	public int compareTo(Ref<T> o) {
		return this.key().compareTo(o.key());
	}

	/** Equality comparison is based on key equivalence */
	@Override
	public boolean equals(Object obj) {
		return obj != null && obj instanceof Ref && key().equals(((Ref<?>)obj).key());
	}

	/** Type-safe comparison for key equivalence */
	public boolean equivalent(Ref<T> other) {
		return equals(other);
	}

	/** Type safe comparison for key equivalence */
	public boolean equivalent(Key<T> other) {
		return key().equivalent(other);
	}

	/** Hash code is simply that of key */
	@Override
	public int hashCode() {
		return key().hashCode();
	}

	/** Renders some info about the key */
	@Override
	public String toString() {
		return this.getClass().getSimpleName() + "(" + key() + ")";
	}
}