//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.eventcore;

import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * Base class for all events. An event is described by its {@link #type} and its {@link #target}.
 * An event may be provided with a timestamp and further parameters.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 * 
 * @param <T>
 *            Can be used to declare that events belong to a specific platform without subclassing.
 */
public final class Event implements Serializable {

    /**
     * <p>
     * Id for object serialization.
     * </p>
     */
    private static final long serialVersionUID = 1L;

    /**
     * <p>
     * Global start event that can be used to indicate the start of a sequence.
     * </p>
     */
    public static final Event STARTEVENT = new Event(new StringEventType("START"));

    /**
     * <p>
     * Global end event that can be used to indicate the end of a sequence.
     */
    public static final Event ENDEVENT = new Event(new StringEventType("END"));

    /**
     * <p>
     * Type of the event.
     * </p>
     */
    private IEventType type;

    /**
     * <p>
     * Target of the event.
     * </p>
     */
    private IEventTarget target = null;
    
    /**
     * <p>
     * Timestamp of when the event took place.
     * </p>
     */
    private long timestamp = Long.MIN_VALUE;
    
    /**
     * <p>
     * Map with further parameters of the event, which are not belonging to the type or target.
     * </p>
     */
    private Map<String, String> parameters;

    /**
     * <p>
     * List of {@link IReplayable}s of type T that describes the replay of an event. The
     * {@link IReplayable}s can be interpreted as <it>sub-events</it> on the platform level that
     * make up the abstract event.
     * </p>
     */
    private List<IReplayable> replay;

    /**
     * <p>
     * Constructor. Creates a new Event with a given type. The type must not be null.
     * </p>
     * 
     * @param type
     *            type of the event
     */
    public Event(IEventType type) {
        if (type == null) {
            throw new IllegalArgumentException("Event type must not be null");
        }
        this.type = type;
    }

    /**
     * <p>
     * Constructor. Creates a new Event with a given type and target. The type must not be null.
     * </p>
     * 
     * @param type
     *            type of the event
     * @param target
     *            target of the event
     */
    public Event(IEventType type, IEventTarget target) {
        this(type);
        this.target = target;
    }

    /**
     * <p>
     * Two events are equal, if their {@link #type} and {@link #target} are equal. The timestamp
     * and other parameters are ignored.
     * </p>
     * <p>
     * See {@link Object#equals(Object)} for further information.
     * </p>
     * 
     * @param other
     *            Event that is compared to this
     * @return true, if events are equal, false otherwise
     */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof Event) {
            Event otherEvent = (Event) other;
            if (target != null) {
                return type.equals(otherEvent.type) && target.equals(otherEvent.target);
            }
            else {
                return type.equals(otherEvent.type) && otherEvent.target == null;
            }
        }
        return false;
    }

    /**
     * <p>
     * Returns {@link #getId()} as String representation of the event.
     * </p>
     * 
     * @return String representation of the event
     */
    @Override
    public String toString() {
        return getId();
    }

    /**
     * <p>
     * Returns the {@link #target} of the event.
     * </p>
     * 
     * @return {@link #target} of the event
     */
    public IEventTarget getTarget() {
        return target;
    }

    /**
     * <p>
     * Returns the {@link #type} of the event.
     * </p>
     * 
     * @return {@link #type} of the event
     */
    public IEventType getType() {
        return type;
    }
    
    /**
     * <p>
     * Returns the combination of {@link #type} and {@link #target} as id.
     * </p>
     *
     * @return string of the form (type,target)
     */
    public String getId() {
        String id = type.toString();
        if( target!=null ) {
            id += "." + target.getStringIdentifier();
        }
        return id;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        int multiplier = 17;
        int hash = 42;
        if (type != null) {
            hash = multiplier * hash + type.hashCode();
        }
        if (target != null) {
            hash = multiplier * hash + target.hashCode();
        }
        return hash;
    }

    /**
     * <p>
     * Sets the target of the event. Once set, the target cannot be changed.
     * </p>
     * 
     * @param target
     *            target of the event
     * @return true, if target was changed, false otherwise
     */
    public boolean setTarget(IEventTarget target) {
        if (this.target != null) {
            return false;
        }
        this.target = target;
        return true;
    }

    /**
     * <p>
     * Adds a new {@link IReplayable} of type T to the replay sequence.
     * </p>
     * 
     * @param replayable
     *            element that is added to the sequence
     * @throws IllegalArgumentException
     *             thrown is replayable is null
     */
    public void addReplayable(IReplayable replayable) {
        if (replayable == null) {
            throw new IllegalArgumentException("replayble must not be null");
        }
        
        if (replay == null) {
            replay = new LinkedList<IReplayable>();
        }
        
        replay.add(replayable);
    }

    /**
     * <p>
     * Adds a {@link List} of {@link IReplayable} to the replay sequence.
     * </p>
     * 
     * @param generatedReplaySeq
     *            {@link List} that is added to the sequence
     * @throws IllegalArgumentException
     *             thrown if generatedReplaySeq is null
     */
    public void addReplayableSequence(List<? extends IReplayable> generatedReplaySeq) {
        if (generatedReplaySeq == null) {
            throw new IllegalArgumentException("generatedReplaySeq must not be null");
        }
        
        if (replay == null) {
            replay = new LinkedList<IReplayable>();
        }
        
        replay.addAll(generatedReplaySeq);
    }
    
    /**
     * <p>
     * Adds a parameter to the event or sets it to a new value. The key must not be null. If a
     * parameter with the specified key already exists, its value is replaced with the new one.
     * If the value is null, the parameter with the specified key is removed, if it exists.
     * </p>
     * 
     * @param key   the key of the parameter
     * @param value the value of the parameter
     * 
     * @throws IllegalArgumentException
     *             if the provided key is null
     */
    public void setParameter(String key, String value) {
        if (key == null) {
            throw new IllegalArgumentException("key must not be null");
        }
        
        if (parameters == null) {
            parameters = new HashMap<String, String>();
        }
        
        if (value == null) {
            parameters.remove(key);
        }
        else {
            parameters.put(key, value);
        }
    }

    /**
     * <p>
     * Returns the event parameter with the specified key if it exists or null if not. The key
     * must not be null.
     * </p>
     * 
     * @param key the key of the parameter to be returned
     * 
     * @return the value of the parameter with the specified key or null if there is no parameter
     *         with that key
     * 
     * @throws IllegalArgumentException
     *             if the provided key is null
     */
    public String getParameter(String key) {
        if (key == null) {
            throw new IllegalArgumentException("key must not be null");
        }
        
        if (parameters != null) {
            return parameters.get(key);
        }
        else {
            return null;
        }
    }

    /**
     * <p>
     * Sets the timestamp of the event, i.e. when the event occurred. A timestamp for events is
     * optional. Therefore it is also ignored by the {@link #equals(Object)}-method. A timestamp
     * with a value smaller 0 means, that now timestamp for the event exists.
     * </p>
     * 
     * @param timestamp the new value for the timestamp
     */
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * <p>
     * Returns the timestamp of the event or a value lower than 0 if no timestamp for the event
     * exists.
     * </p>
     * 
     * @return the timestamp of the event or a value lower than 0 if no timestamp exists
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * <p>
     * Returns a the list of replay events.
     * </p>
     * <p>
     * The return value is a copy of the list used internally!
     * </p>
     * 
     * @return list of replay events.
     */
    public List<IReplayable> getReplayables() {
        if (replay != null) {
            return new LinkedList<IReplayable>(replay);
        }
        else {
            return new LinkedList<IReplayable>();
        }
    }
}
