/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.event;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.log4j.Logger;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Constants;
import org.dspace.core.Context;

/**
 * An Event object represents a single action that changed one object in the
 * DSpace data model. An "atomic" action at the application or business-logic
 * API level may spawn many of these events.
 * <p>
 * This class includes tools to help set and use the contents of the event. Note
 * that it describes DSpace data object types in two ways: by the type
 * identifiers in the Constants class, and also by an Event-specific bitmask
 * (used by its internal filters). All public API calls use the Constants
 * version of the data model types.
 * <p>
 * Note that the type of the event itself is actually descriptive of the
 * <em>action</em> it performs: ADD, MODIFY, etc. The most significant
 * elements of the event are:
 * <ul>
 * <li>(Action) Type</li>
 * <li>Subject -- DSpace object to which the action applies, e.g. the Collection
 * to which an ADD adds a member.</li>
 * <li>Object -- optional, when present it is the other object effected by an
 * action, e.g. the Item ADDed to a Collection by an ADD.</li>
 * <li>detail -- a textual summary of what changed.  Content and its
 * significance varies by the combination of action and subject type.</li>
 * <li> - timestamp -- exact millisecond timestamp at which event was logged.</li>
 * </ul>
 * 
 * @version $Revision$
 */
public class Event implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** ---------- Constants ------------- * */

    /** Event (Action) types */
    public static final int CREATE = 1 << 0; // create new object

    public static final int MODIFY = 1 << 1; // modify object

    public static final int MODIFY_METADATA = 1 << 2; // modify object

    public static final int ADD = 1 << 3; // add content to container

    public static final int REMOVE = 1 << 4; // remove content from container

    public static final int DELETE = 1 << 5; // destroy object
    
    public static final int INSTALL = 1 << 6; // object exits workspace/flow

    /** Index of filter parts in their array: */
    public static final int SUBJECT_MASK = 0; // mask of subject types

    public static final int EVENT_MASK = 1; // mask of event type

    // XXX NOTE: keep this up to date with any changes to event (action) types.
    private static final String eventTypeText[] = { "CREATE", "MODIFY",
            "MODIFY_METADATA", "ADD", "REMOVE", "DELETE", "INSTALL" };

    /** XXX NOTE: These constants must be kept synchronized * */
    /** XXX NOTE: with ALL_OBJECTS_MASK *AND* objTypeToMask hash * */
    private static final int NONE = 0;

    private static final int BITSTREAM = 1 << Constants.BITSTREAM; // 0

    private static final int BUNDLE = 1 << Constants.BUNDLE; // 1

    private static final int ITEM = 1 << Constants.ITEM; // 2

    private static final int COLLECTION = 1 << Constants.COLLECTION; // 3

    private static final int COMMUNITY = 1 << Constants.COMMUNITY; // 4

    private static final int SITE = 1 << Constants.SITE; // 5

    private static final int GROUP = 1 << Constants.GROUP; // 6

    private static final int EPERSON = 1 << Constants.EPERSON; // 7

    private static final int ALL_OBJECTS_MASK = BITSTREAM | BUNDLE | ITEM
            | COLLECTION | COMMUNITY | SITE | GROUP | EPERSON;

    private static Map<Integer, Integer> objTypeToMask = new HashMap<Integer, Integer>();

    private static Map<Integer, Integer> objMaskToType = new HashMap<Integer, Integer>();
    static
    {
        objTypeToMask.put(Constants.BITSTREAM, BITSTREAM);
        objMaskToType.put(BITSTREAM, Constants.BITSTREAM);

        objTypeToMask.put(Constants.BUNDLE, BUNDLE);
        objMaskToType.put(BUNDLE, Constants.BUNDLE);

        objTypeToMask.put(Constants.ITEM, ITEM);
        objMaskToType.put(ITEM, Constants.ITEM);

        objTypeToMask.put(Constants.COLLECTION, COLLECTION);
        objMaskToType.put(COLLECTION, Constants.COLLECTION);

        objTypeToMask.put(Constants.COMMUNITY, COMMUNITY);
        objMaskToType.put(COMMUNITY, Constants.COMMUNITY);

        objTypeToMask.put(Constants.SITE, SITE);
        objMaskToType.put(SITE, Constants.SITE);

        objTypeToMask.put(Constants.GROUP, GROUP);
        objMaskToType.put(GROUP, Constants.GROUP);

        objTypeToMask.put(Constants.EPERSON, EPERSON);
        objMaskToType.put(EPERSON, Constants.EPERSON);
    }

    /** ---------- Event Fields ------------- * */

    /** identifier of Dispatcher that created this event (hash of its name) */
    private int dispatcher;

    /** event (action) type - above enumeration */
    private int eventType;

    /** object-type of SUBJECT - see above enumeration */
    private int subjectType;

    /** content model identifier */
    private int subjectID;

    /** object-type of SUBJECT - see above enumeration */
    private int objectType = NONE;

    /** content model identifier */
    private int objectID = -1;

    /** timestamp */
    private long timeStamp;

    /** "detail" - arbitrary field for relevant detail, */
    /** e.g. former handle for DELETE event since obj is no longer available. */
    /**
     * FIXME This field is not a complete view of the DSpaceObject that was
     * modified. Providing these objects to the consumer (e.g. by storing
     * lifecycle versions of the changed objects in the context) would provide
     * for more complex consumer abilities that are beyond our purview.
     */
    private String detail;

    /** unique key to bind together events from one context's transaction */
    private String transactionID;

    /** identity of authenticated user, i.e. context.getCurrentUser(). */
    /** Only needed in the event for marshalling for asynch event messages */
    private int currentUser = -1;

    /** copy of context's "extraLogInfo" field.  Used only for */
    /** marshalling for asynch event messages. */
    private String extraLogInfo = null;

    private BitSet consumedBy = new BitSet();

    /** log4j category */
    private static Logger log = Logger.getLogger(Event.class);

    /**
     * Constructor.
     * 
     * @param eventType
     *            action type, e.g. Event.ADD.
     * @param subjectType
     *            DSpace Object Type of subject e.g. Constants.ITEM.
     * @param subjectID
     *            database ID of subject instance.
     * @param detail
     *            detail information that depends on context.
     */
    public Event(int eventType, int subjectType, int subjectID, String detail)
    {
        this.eventType = eventType;
        this.subjectType = coreTypeToMask(subjectType);
        this.subjectID = subjectID;
        timeStamp = System.currentTimeMillis();
        this.detail = detail;
    }

    /**
     * Constructor.
     * 
     * @param eventType
     *            action type, e.g. Event.ADD.
     * @param subjectType
     *            DSpace Object Type of subject e.g. Constants.ITEM.
     * @param subjectID
     *            database ID of subject instance.
     * @param objectType
     *            DSpace Object Type of object e.g. Constants.BUNDLE.
     * @param objectID
     *            database ID of object instance.
     * @param detail
     *            detail information that depends on context.
     */
    public Event(int eventType, int subjectType, int subjectID, int objectType,
            int objectID, String detail)
    {
        this.eventType = eventType;
        this.subjectType = coreTypeToMask(subjectType);
        this.subjectID = subjectID;
        this.objectType = coreTypeToMask(objectType);
        this.objectID = objectID;
        timeStamp = System.currentTimeMillis();
        this.detail = detail;
    }

    /**
     * Compare two events. Ignore any difference in the timestamps. Also ignore
     * transactionID since that is not always set initially.
     * 
     * @param other
     *            the event to compare this one to
     * @return true if events are "equal", false otherwise.
     */
    public boolean equals(Object other)
    {
        if (other instanceof Event)
        {
            Event otherEvent = (Event)other;
            return (this.detail == null ? otherEvent.detail == null : this.detail
                    .equals(otherEvent.detail))
                    && this.eventType == otherEvent.eventType
                    && this.subjectType == otherEvent.subjectType
                    && this.subjectID == otherEvent.subjectID
                    && this.objectType == otherEvent.objectType
                    && this.objectID == otherEvent.objectID;
        }

        return false;
    }

    public int hashCode()
    {
        return new HashCodeBuilder().append(this.detail)
                                    .append(eventType)
                                    .append(subjectType)
                                    .append(subjectID)
                                    .append(objectType)
                                    .append(objectID)
                                    .toHashCode();
    }

    /**
     * Set the identifier of the dispatcher that first processed this event.
     * 
     * @param id
     *            the unique (hash code) value characteristic of the dispatcher.
     */
    public void setDispatcher(int id)
    {
        dispatcher = id;
    }

    // translate a "core.Constants" object type value to local bitmask value.
    private static int coreTypeToMask(int core)
    {
        Integer mask = objTypeToMask.get(core);
        if (mask == null)
        {
            return -1;
        }
        else
        {
            return mask.intValue();
        }
    }

    // translate bitmask object-type to "core.Constants" object type.
    private static int maskTypeToCore(int mask)
    {
        Integer core = objMaskToType.get(mask);
        if (core == null)
        {
            return -1;
        }
        else
        {
            return core.intValue();
        }
    }

    /**
     * Get the DSpace object which is the "object" of an event.
     * 
     * @return DSpaceObject or null if none can be found or no object was set.
     */
    public DSpaceObject getObject(Context context) throws SQLException
    {
        int type = getObjectType();
        int id = getObjectID();
        if (type < 0 || id < 0)
        {
            return null;
        }
        else
        {
            return DSpaceObject.find(context, type, id);
        }
    }

    /**
     * Syntactic sugar to get the DSpace object which is the "subject" of an
     * event.
     * 
     * @return DSpaceObject or null if none can be found.
     */
    public DSpaceObject getSubject(Context context) throws SQLException
    {
        return DSpaceObject.find(context, getSubjectType(), getSubjectID());
    }

    /**
     * @return database ID of subject of this event.
     */
    public int getSubjectID()
    {
        return subjectID;
    }

    /**
     * @return database ID of object of this event, or -1 if none was set.
     */
    public int getObjectID()
    {
        return objectID;
    }

    /**
     * @return type number (e.g. Constants.ITEM) of subject of this event.
     */
    public int getSubjectType()
    {
        return maskTypeToCore(subjectType);
    }

    /**
     * @return type number (e.g. Constants.ITEM) of object of this event, or -1
     *          if none was set.
     */
    public int getObjectType()
    {
        return maskTypeToCore(objectType);
    }

    /**
     * @return type of subject of this event as a String, e.g. for logging.
     */
    public String getSubjectTypeAsString()
    {
        int i = log2(subjectType);
        if (i >= 0 && i < Constants.typeText.length)
        {
            return Constants.typeText[i];
        }
        else
        {
            return "(Unknown)";
        }
    }

    /**
     * @return type of object of this event as a String, e.g. for logging.
     */
    public String getObjectTypeAsString()
    {
        int i = log2(objectType);
        if (i >= 0 && i < Constants.typeText.length)
        {
            return Constants.typeText[i];
        }
        else
        {
            return "(Unknown)";
        }
    }

    /**
     * Translate a textual DSpace Object type name into an event subject-type
     * mask. NOTE: This returns a BIT-MASK, not a numeric type value; the mask
     * is only used within the event system.
     * 
     * @param s
     *            text name of object type.
     * @return numeric value of object type or 0 for error.
     */
    public static int parseObjectType(String s)
    {
        if ("*".equals(s) || "all".equalsIgnoreCase(s))
        {
            return ALL_OBJECTS_MASK;
        }
        else
        {
            int id = Constants.getTypeID(s.toUpperCase());
            if (id >= 0)
            {
                return 1 << id;
            }
        }
        return 0;
    }

    /**
     * @return event-type (i.e. action) this event, one of the masks like
     *          Event.ADD defined above.
     */
    public int getEventType()
    {
        return eventType;
    }

    /**
     * Get the text name of event (action) type.
     * 
     * @return event-type (i.e. action) this event as a String, e.g. for
     *          logging.
     */
    public String getEventTypeAsString()
    {
        int i = log2(eventType);
        if (i >= 0 && i < eventTypeText.length)
        {
            return eventTypeText[i];
        }
        else
        {
            return "(Unknown)";
        }
    }

    /**
     * Interpret named event type.
     * 
     * @param s
     *            name of event type.
     * @return numeric value of event type or 0 for error.
     */
    public static int parseEventType(String s)
    {
        if ("*".equals(s) || "all".equalsIgnoreCase(s))
        {
            int result = 0;
            for (int i = 0; i < eventTypeText.length; ++i)
            {
                result |= (1 << i);
            }
            return result;
        }

        for (int i = 0; i < eventTypeText.length; ++i)
        {
            if (eventTypeText[i].equalsIgnoreCase(s))
            {
                return 1 << i;
            }
        }
        return 0;
    }

    /**
     * @return timestamp at which event occurred, as a count of milliseconds
     *         since the epoch (standard Java format).
     */
    public long getTimeStamp()
    {
        return timeStamp;
    }

    /**
     * @return hashcode identifier of name of Dispatcher which first dispatched
     *         this event. (Needed by asynch dispatch code.)
     */
    public int getDispatcher()
    {
        return dispatcher;
    }

    /**
     * @return value of detail element of the event.
     */
    public String getDetail()
    {
        return detail;
    }

    /**
     * @return value of transactionID element of the event.
     */
    public String getTransactionID()
    {
        return transactionID;
    }

    /**
     * Sets value of transactionID element of the event.
     * 
     * @param tid
     *            new value of transactionID.
     */
    public void setTransactionID(String tid)
    {
        transactionID = tid;
    }

    public void setCurrentUser(int uid)
    {
        currentUser = uid;
    }

    public int getCurrentUser()
    {
        return currentUser;
    }

    public void setExtraLogInfo(String info)
    {
        extraLogInfo = info;
    }

    public String getExtraLogInfo()
    {
        return extraLogInfo;
    }

    /**
     * Test whether this event would pass through a list of filters.
     * 
     * @param filters
     *            list of filter masks; each one is an Array of two ints.
     * @return true if this event would be passed through the given filter
     *         list.
     */
    public boolean pass(List<int[]> filters)
    {
        boolean result = false;

        for (int filter[] : filters)
        {
            if ((subjectType & filter[SUBJECT_MASK]) != 0 && (eventType & filter[EVENT_MASK]) != 0)
            {
                result = true;
            }
        }

        if (log.isDebugEnabled())
        {
            log.debug("Filtering event: " + "eventType="
                    + String.valueOf(eventType) + ", subjectType="
                    + String.valueOf(subjectType) + ", result="
                    + String.valueOf(result));
        }

        return result;
    }

    // dumb integer "log base 2", returns -1 if there are no 1's in number.
    private static int log2(int n)
    {
        for (int i = 0; i < 32; ++i)
        {
            if (n == 1)
            {
                return i;
            }
            else
            {
                n = n >> 1;
            }
        }
        return -1;
    }

    /**
     * Keeps track of which consumers the event has been consumed by. Should be
     * called by a dispatcher when calling consume(Context ctx, String name,
     * Event event) on an event.
     * 
     * @param consumerName
     */
    public void setBitSet(String consumerName)
    {
        consumedBy.set(EventManager.getConsumerIndex(consumerName));
    }

    /**
     * @return the set of consumers which have consumed this Event.
     */
    public BitSet getBitSet()
    {
        return consumedBy;
    }

    /**
     * @return Detailed string representation of contents of this event, to
     *          help in logging and debugging.
     */
    public String toString()
    {
        return "org.dspace.event.Event(eventType="
                + this.getEventTypeAsString()
                + ", SubjectType="
                + this.getSubjectTypeAsString()
                + ", SubjectID="
                + String.valueOf(subjectID)
                + ", ObjectType="
                + this.getObjectTypeAsString()
                + ", ObjectID="
                + String.valueOf(objectID)
                + ", TimeStamp="
                + String.valueOf(timeStamp)
                + ", dispatcher="
                + String.valueOf(dispatcher)
                + ", detail="
                + (detail == null ? "[null]" : "\"" + detail + "\"")
                + ", transactionID="
                + (transactionID == null ? "[null]" : "\"" + transactionID
                        + "\"") + ")";
    }
}
