package org.jboss.errai.jpa.client.local;

import org.jboss.errai.common.client.api.Assert;

import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;

/**
 * Holder class for a storage key: a tuple of type and identity value.
 * <p>
 * Instances of this class are immutable.
 *
 * @param <X> The entity's Java type
 * @param <T> The entity's identity type
 * @author Jonathan Fuerth <jfuerth@gmail.com>
 */
public class Key<X, T> {

  private final ErraiEntityType<X> entityType;
  private final T id;

  /**
   * Creates a key that uniquely identifies an entity of a particular type (the
   * entity with this identity does not necessarily exist, but if it did, this
   * would be its identity).
   *
   * @param entityType The type of the entity. Must not be null.
   * @param id The ID of the entity. Must not be null.
   * @throws NullPointerException if either argument is null.
   */
  public Key(ErraiEntityType<X> entityType, T id) {
    this.entityType = Assert.notNull(entityType);
    this.id = Assert.notNull(id);
  }

  /**
   * Returns a Key instance for the entity type of the given class.
   *
   * @param em
   *          The entity manager (required for looking up the EntityType for the
   *          given class). Must not be null.
   * @param entityClass
   *          The class of the entity for the key. Must not be null.
   * @param id
   *          The ID value for the entity. Must not be null.
   * @return A Key instance for the given entity type and ID value.
   * @throws NullPointerException
   *           if any argument is null.
   * @throws IllegalArgumentException
   *           if {@code entityClass} is not a known JPA entity type.
   */
  public static <X, T> Key<X, T> get(ErraiEntityManager em, Class<X> entityClass, T id) {
    ErraiEntityType<X> entityType = em.getMetamodel().entity(entityClass);
    return new Key<X, T>(entityType, id);
  }

  /**
   * Returns the entity type for this key.
   *
   * @return the entity type of the key. Never null.
   */
  public ErraiEntityType<X> getEntityType() {
    return entityType;
  }

  /**
   * Returns the ID value for this key.
   *
   * @return the ID value of the key. Never null.
   */
  public T getId() {
    return id;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result
            + ((entityType == null) ? 0 : entityType.hashCode());
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Key<?, ?> other = (Key<?, ?>) obj;
    if (entityType == null) {
      if (other.entityType != null)
        return false;
    }
    else if (!entityType.equals(other.entityType))
      return false;
    if (id == null) {
      if (other.id != null)
        return false;
    }
    else if (!id.equals(other.id))
      return false;
    return true;
  }

  @Override
  public String toString() {
    return "Key [entityType=" + entityType + ", id=" + id + "]";
  }

  /**
   * Returns a JSON string representation of this key which can be turned back
   * into a Key instance via
   * {@link Key#fromJson(ErraiEntityManager, String, boolean)}.
   *
   * @see #toJsonObject()
   * @return a JSON-formatted string. Never null.
   */
  public String toJson() {
    return toJsonObject().toString();
  }

  /**
   * Returns a JSONObject representation of this key which can be turned back
   * into a Key instance via
   * {@link Key#fromJsonObject(ErraiEntityManager, JSONObject, boolean)}.
   *
   * @see #toJson()
   * @return a JSON object that represents this key. Never null.
   */
  public JSONObject toJsonObject() {
    JSONObject keyJson = new JSONObject();
    keyJson.put("entityType", new JSONString(entityType.getJavaType().getName()));
    keyJson.put("id", JsonUtil.basicValueToJson(id));
    return keyJson;
  }

  /**
   * Returns a Key instance based on the given JSON string.
   *
   * @param em
   *          The entity manager that can be used to look up the entity type
   *          corresponding with the key.
   * @param key
   *          The key to parse.
   * @param failIfNotFound
   *          If true, and the entity type given in {@code key} is not known to
   *          {@code em}, an IllegalArgumentException will be thrown.
   * @return An instance of Key that corresponds with the entity type and ID of
   *         the given JSON string.
   */
  public static Key<?, ?> fromJson(ErraiEntityManager em, String key, boolean failIfNotFound) {
    JSONValue k;
    try {
      k = JSONParser.parseStrict(key);

    } catch (JSONException e) {
      throw new JSONException("Input: " + key, e);
    }
    return fromJsonObject(em, k.isObject(), failIfNotFound);
  }

  /**
   * Returns a Key instance based on the given JSON object.
   *
   * @param em
   *          The entity manager that can be used to look up the entity type
   *          corresponding with the key.
   * @param key
   *          The properties of the key to create.
   * @param failIfNotFound
   *          If true, and the entity type given in {@code key} is not known to
   *          {@code em}, an IllegalArgumentException will be thrown.
   * @return An instance of Key that corresponds with the entity type and ID of
   *         the given JSON object.
   */
  public static Key<?, ?> fromJsonObject(ErraiEntityManager em, JSONObject key, boolean failIfNotFound) {
    String entityClassName = key.get("entityType").isString().stringValue();
    ErraiEntityType<Object> et = em.getMetamodel().entity(entityClassName, failIfNotFound);
    if (et == null) {
      return null;
    }
    ErraiSingularAttribute<?, Object> idAttr = et.getId(Object.class);
    Object id = JsonUtil.basicValueFromJson(key.get("id"), idAttr.getJavaType());

    return new Key<Object, Object>(et, id);
  }
}
