package de.hsadmin.core.model;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.persistence.EntityManager;

import de.hsadmin.mods.user.UnixUser;

public abstract class AbstractEntity {
	
	/**
	 * trims whitespace from both ends, but a null remains null.
	 * @param field
	 * @return
	 */
	public static String trim(String field) {
		if (field == null)
			return field;
		return field.trim();
	}

	/**
	 *  trims whitespace from both ends, returns null for a resulting empty
	 *  string or null.
	 * @param field
	 * @return
	 */
	public static String trimToNull(String field) {
		if (field == null)
			return field;
		field = field.trim();
		if (field.length() == 0)
			return null;
		return field;
	}

	/**
	 *  trims whitespace from both ends, returns an empty string for null
	 * @param field
	 * @return
	 */
	public static String trimToEmpty(String field) {
		if (field == null)
			return "";
		field = field.trim();
		return field;
	}

	/**
	 * Creates a JPA-QL query string from a human readable string representing a
	 * key.
	 * 
	 * Such a static method is to be implemented by every entity class which has
	 * a human readable key different from "id".
	 * 
	 * @param humanKey
	 *            A human readable key representing the entity. E.g.
	 *            "info@example.com" for an E-Mail-Address.
	 * 
	 * @return jpaQLQuery the matching JPA-QL query
	 */
	public static String createQueryFromStringKey(String humanKey) throws HSAdminException {
		return "id=" + humanKey;
	}

	/**
	 * escapes a String so that it can be used for SQL statements
	 * @param str a sting that should be usable as a String in an SQLStatement
	 * @return the escapedString
	 */
	public static String escapeString(String str) {
		str = str.replaceAll("\"", "\\\"");
		str = str.replaceAll("\'", "\\'");
		return str;
	}

	/**
	 * Calls the static method of the Entity subclass to get a query
	 * restriction.
	 * 
	 * @return a restricting JPA-QL expression to limit access to entities
	 */
	public static String restriction(Class<?> entityClass, UnixUser loginUser) {
		// hostmasters don't get any restriction
		if (loginUser.hasHostmasterRole())
			return null;
	
		// find a static method which creates the query
		java.lang.reflect.Method method = null;
		try {
			method = entityClass.getDeclaredMethod("restriction");
		} catch (SecurityException e) {
			throw new RuntimeException(
					"unexpected SecurityException in restriction()");
		} catch (NoSuchMethodException e) {
			// don't grant any access
			return "FALSE";
		}
	
		// get the restriction from a static method of the Entity class
		String restriction = "FALSE";
		try {
			restriction = (String) method.invoke(null);
		} catch (Exception e) {
			throw new RuntimeException(
					"unexpected Exception in Entity-restriction() implementation");
		}
		return restriction;
	}

	/**
	 * Creates a human readable string representing a key to this entity.
	 * 
	 * @return humanReadableString A human readable string which can be used as
	 *         a key to this entity. This string can also be used with the
	 *         static method createQueryFromStringKey which has to be supported
	 *         by all entity
	 */
	public abstract String createStringKey();

	/**
	 * returns true if entity is not yet stored to the database, false otherwise.
	 * @return
	 */
	public abstract boolean isNew();

	/**
	 * returns the unique database if
	 */
	public abstract long id();

	/**
	 * returns true if the argument entity has the same database id
	 * @param entity
	 * @return
	 */
	public boolean sameIdAs(AbstractEntity entity) {
		if (entity == null)
			return false;
		if (getClass() != entity.getClass())
			return false;
		return id() == entity.id();
	}

	/**
	 * returns true if entity is not yet stored to the database, false otherwise.
	 * @return
	 */
	@javax.persistence.Transient
	public boolean getNew() {
		return isNew();
	}

	/**
	 * Initializes a newly created entity.
	 * 
	 * This method makes it possible to initialize fields of entities using the
	 * login user and database access.
	 * @param em
	 * @param loginUser
	 */
	public void initialize(EntityManager em, UnixUser loginUser) {
	}

	/**
	 *  completes the (new) entity before it gets stored to the database
	 * @param em
	 * @param loginUser
	 */
	public void complete(EntityManager em, UnixUser loginUser) {
	}

	/**
	 * merges this entity with the database, keeping transient members which are
	 * otherwise nulled.
	 * 
	 * Usually subclasses load the attached entity with same id and update all
	 * fields to which the login user has access, but no other fields. Then the
	 * attached entity is returned instead of this entity.
	 * @param em
	 * @param loginUser
	 */
	public AbstractEntity merge(EntityManager em, UnixUser loginUser) {
		return em.merge(this);
	}

	/**
	 * determines whether the given user has full read access on all merged
	 * fields of this entity
	 * @param loginUser
	 * @return
	 */
	public boolean isReadAllowedFor(UnixUser loginUser) {
		return loginUser.hasHostmasterRole();
	}

	/**
	 * determines whether the given user has full write access on all merged 
	 * fields of this entity
	 * @param loginUser
	 * @return
	 */
	public boolean isWriteAllowedFor(UnixUser loginUser) {
		return loginUser.hasHostmasterRole();
	}

	/**
	 * returns the hive name in which this entity resists or null if entity 
	 * is new or does not need a hive.
	 * @return
	 */
	public String getHiveName() {
		return null;
	}

	/**
	 * returns the UnixUser who owns this entity (and thus has broad access to it
	 * @param em
	 * @return
	 */
	public abstract UnixUser owningUser(EntityManager em);

	/**
	 * Clonable::clone did result in javasisst errors (duplicate method) since
	 * JBoss 5.1.
	 * This is just for testing, it does not use the proper class loader!
	 */
	public <T extends AbstractEntity> T replicate() {
		try {
			ByteArrayOutputStream baOut = new ByteArrayOutputStream();
			ObjectOutputStream oOut = new ObjectOutputStream(baOut);
			oOut.writeObject(this);
			byte[] buffer = baOut.toByteArray();

			ByteArrayInputStream baIn = new ByteArrayInputStream(buffer);
			ObjectInputStream oIn = new ObjectInputStream(baIn);

			@SuppressWarnings("unchecked")
			T clone = (T) oIn.readObject();
			return clone;
		} catch (Exception exc) {
			throw new RuntimeException(exc);
		}
	}

}
