package com.laytonsmith.core.events;
import com.laytonsmith.PureUtilities.Common.DateUtils;
import com.laytonsmith.PureUtilities.Pair;
import com.laytonsmith.abstraction.MCPlayer;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Prefs;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CClassType;
import com.laytonsmith.core.constructs.CClosure;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.IVariable;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.CommandHelperEnvironment;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.exceptions.CRE.CREPlayerOfflineException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.exceptions.EventException;
import com.laytonsmith.core.profiler.ProfilePoint;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class represents an actually bound event. When the script runs bind(), a
* new BoundEvent is created as a closure.
*
*/
public class BoundEvent implements Comparable<BoundEvent> {
private final String eventName;
private final String id;
private final Priority priority;
private final Map<String, Construct> prefilter;
private final String eventObjName;
private Environment originalEnv;
private final ParseTree tree; //The code closure for this event
private final Driver driver; //For efficiency sake, cache it here
private static int EventID = 0;
private final Target target;
/**
* Returns a unique ID that can be used to identify an event.
* @return
*/
private static int GetUniqueID() {
synchronized (BoundEvent.class) {
return ++EventID;
}
}
/**
* This is the environment that was set at bind time, not the environment
* set during run time. The environment is cloned, so changes to the environment
* will not affect other code.
* @return
*/
public Environment getEnvironment() {
try {
return originalEnv.clone();
} catch (CloneNotSupportedException ex) {
throw new Error(ex);
}
}
/**
* Event priorities. This is sorted and events are run in a particular order.
*/
public enum Priority {
LOWEST(5),
LOW(4),
NORMAL(3),
HIGH(2),
HIGHEST(1),
MONITOR(1000);
private final int id;
private Priority(int i) {
this.id = i;
}
public int getId() {
return this.id;
}
public boolean isHigherPriority(Priority other){
return other.getId() > this.getId();
}
public boolean isLowerPriority(Priority other){
return other.getId() < this.getId();
}
}
/**
* Compares two event's IDs, and if they are the same, they should
* be the actual same event. Since only one event of a given ID exists,
* technically == should work on these events.
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof BoundEvent) {
return this.id.equals(((BoundEvent) obj).id);
} else {
return false;
}
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return "(" + eventName + ") " + id;
}
/**
* Constructs a new BoundEvent.
* @param name The name of the event
* @param options The options for this event. Contains the priority and assigned id, possibly
* @param prefilter The prefilter provided by the user
* @param eventObjName The name of the variable that should be assigned the event object
* @param env The script's environment
* @param tree The closure of the BoundEvent
* @throws EventException If the priority or id are improperly specified
*/
public BoundEvent(String name, CArray options, CArray prefilter, String eventObjName,
Environment env, ParseTree tree, Target t) throws EventException {
this.eventName = name;
if (options != null && options.containsKey("id")) {
this.id = options.get("id", t).val();
if (this.id.matches(".*?:\\d*?")) {
throw new EventException("The id given may not match the format\"string:number\"");
}
} else {
//Generate a new event id
id = name + ":" + GetUniqueID();
}
if (options != null && options.containsKey("priority")) {
try{
this.priority = Priority.valueOf(options.get("priority", t).val().toUpperCase());
} catch(IllegalArgumentException e){
throw new EventException("Priority must be one of: LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR");
}
} else {
this.priority = Priority.NORMAL;
}
this.prefilter = new HashMap<String, Construct>();
if (prefilter != null) {
for (String key : prefilter.stringKeySet()) {
this.prefilter.put(key, prefilter.get(key, Target.UNKNOWN));
}
}
this.originalEnv = env;
this.tree = tree;
if(EventList.getEvent(this.eventName) == null){
throw new EventException("No event named \"" + this.eventName + "\" is registered!");
}
this.driver = EventList.getEvent(this.eventName).driver();
this.eventObjName = eventObjName;
this.target = t;
}
public int getLineNum(){
return target.line();
}
public File getFile(){
return target.file();
}
public int getCol(){
return target.col();
}
public Target getTarget(){
return target;
}
public String getEventName() {
return eventName;
}
public String getEventObjName() {
return eventObjName;
}
public Driver getDriver() {
return driver;
}
public String getId() {
return id;
}
public Map<String, Construct> getPrefilter() {
return prefilter;
}
public Priority getPriority() {
return priority;
}
/**
* Events are sorted by priority
* @param o
* @return
*/
@Override
public int compareTo(BoundEvent o) {
if (this.getPriority().getId() < o.getPriority().getId()) {
return -1;
} else if (this.getPriority().getId() > o.getPriority().getId()) {
return 1;
} else {
return this.id.compareTo(o.id);
}
}
/**
* When the event actually occurs, this should be run, after translating the
* original event object (of whatever type it may be) into a standard map, which
* contains the event object data. It is converted into a CArray here, and then
* the script is executed with the driver's execute function.
* @param event
*/
public void trigger(ActiveEvent activeEvent) throws EventException {
try {
// GenericTree<Construct> root = new GenericTree<Construct>();
// root.setRoot(tree);
Environment env = originalEnv.clone();
CArray ca = CArray.GetAssociativeArray(Target.UNKNOWN);
for (String key : activeEvent.parsedEvent.keySet()) {
ca.set(new CString(key, Target.UNKNOWN), activeEvent.parsedEvent.get(key), Target.UNKNOWN);
}
if(activeEvent.parsedEvent.containsKey("player")){
try{
MCPlayer p = Static.GetPlayer(activeEvent.parsedEvent.get("player"), Target.UNKNOWN);
if(p != null && p.isOnline()){
env.getEnv(CommandHelperEnvironment.class).SetPlayer(p);
}
} catch(ConfigRuntimeException e){
if(!(e instanceof CREPlayerOfflineException)){
throw e;
}
//else we just leave the player to be null. It either doesn't matter here,
//or the event will add it later, manually.
}
}
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(new CClassType("array", Target.UNKNOWN), eventObjName, ca, Target.UNKNOWN));
env.getEnv(GlobalEnv.class).SetEvent(activeEvent);
activeEvent.addHistory("Triggering bound event: " + this);
try{
ProfilePoint p = env.getEnv(GlobalEnv.class).GetProfiler().start("Executing event handler for " + this.getEventName() + " defined at " + this.getTarget(), LogLevel.ERROR);
try {
this.execute(env, activeEvent);
} finally {
p.stop();
}
} catch(ConfigRuntimeException e){
//We don't know how to handle this, but we need to set the env,
//then pass it up the chain
e.setEnv(env);
throw e;
}
} catch (CloneNotSupportedException ex) {
Logger.getLogger(BoundEvent.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* Used to manually trigger an event, the underlying event is set to null.
* @param event
* @throws EventException
*/
public void manual_trigger(CArray event) throws EventException{
try {
Environment env = originalEnv.clone();
env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(new CClassType("array", Target.UNKNOWN), eventObjName, event, Target.UNKNOWN));
Map<String, Construct> map = new HashMap<>();
for(String key : event.stringKeySet()){
map.put(key, event.get(key, Target.UNKNOWN));
}
ActiveEvent activeEvent = new ActiveEvent(null);
activeEvent.setParsedEvent(map);
activeEvent.setBoundEvent(this);
env.getEnv(GlobalEnv.class).SetEvent(activeEvent);
this.execute(env, activeEvent);
} catch (CloneNotSupportedException ex) {
Logger.getLogger(BoundEvent.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void execute(Environment env, ActiveEvent activeEvent) throws EventException{
ParseTree superRoot = new ParseTree(null);
superRoot.addChild(tree);
Event myDriver = this.getEventDriver();
myDriver.execute(superRoot, this, env, activeEvent);
}
//TODO: Once ParseTree supports these again, we may bring this back
// /**
// * Returns true if this event MUST be synchronous.
// * @return
// */
// public boolean isSync(){
// return tree.isSync();
// }
//
// /**
// * Returns true if this event MUST be asynchronous.
// * @return
// */
// public boolean isAsync(){
// return tree.isAsync();
// }
public ParseTree getParseTree(){
return tree;
}
/**
* Returns the Event driver that knows how to handle this event.
* @return
*/
public Event getEventDriver(){
return EventList.getEvent(this.getDriver(), this.getEventName());
}
/**
* The bound event is essentially an ActiveEvent generator. Because bound events don't change from run to run, it doesn't
* make sense to store triggered event specific information with the bound event itself. Instead, when the event is triggered,
* an ActiveEvent is generated, stored in the environment, and then the script is triggered. This ActiveEvent contains both
* the underlying event (if needed for things like cancellation or other event manipulation) and the BoundEvent object itself
* (which can be used to get the event id and other information as needed). For convenience, the parsed event information
* is also cached here.
*/
public static class ActiveEvent{
private final BindableEvent underlyingEvent;
private Map<String, Construct> parsedEvent;
private BoundEvent boundEvent;
private Boolean cancelled;
private BoundEvent consumedAt;
private final Map<String, BoundEvent> lockedAt;
private final List<Pair<CClosure, Environment>> whenCancelled;
private final List<Pair<CClosure, Environment>> whenTriggered;
private final List<String> history;
public ActiveEvent(BindableEvent underlyingEvent){
this.underlyingEvent = underlyingEvent;
this.cancelled = null;
whenCancelled = new ArrayList<Pair<CClosure, Environment>>();
whenTriggered = new ArrayList<Pair<CClosure, Environment>>();
lockedAt = new HashMap<String, BoundEvent>();
history = new ArrayList<String>();
}
public void addHistory(String history){
if(Prefs.DebugMode()){
this.history.add(DateUtils.ParseCalendarNotation("%Y-%M-%D %h:%m.%s - ") + history);
}
}
public List<String> getHistory(){
return history;
}
public Map<String, Construct> getParsedEvent() {
return parsedEvent;
}
public BindableEvent getUnderlyingEvent() {
return underlyingEvent;
}
public BoundEvent getBoundEvent() {
return boundEvent;
}
public void setBoundEvent(BoundEvent boundEvent){
this.boundEvent = boundEvent;
}
public void setParsedEvent(Map<String, Construct> parsedEvent){
this.parsedEvent = parsedEvent;
}
public boolean isCancelled() {
//if cancelled is not null, return it. If it is null, check with the underlying event.
//If it isn't null, that means we have manually set it somewhere, so that takes precedence;
//indeed, it may not make sense to ask the event, as it may not be cancellable in the first
//place, but we can still return regardless.
if(cancelled != null){
return cancelled;
} else {
if(boundEvent.getEventDriver().isCancellable(underlyingEvent)){
return boundEvent.getEventDriver().isCancelled(underlyingEvent);
} else {
return false;
}
}
}
public void setCancelled(boolean cancelled) {
this.addHistory("Setting cancelled flag to " + cancelled + " " + boundEvent);
this.cancelled = cancelled;
try {
boundEvent.getEventDriver().cancel(underlyingEvent, cancelled);
} catch (EventException ex) {
//Ignore this exception. This is thrown if the event isn't cancellable.
//Who cares.
}
}
public Event getEventDriver(){
return this.boundEvent.getEventDriver();
}
public boolean isCancellable() {
return boundEvent.getEventDriver().isCancellable(this.underlyingEvent);
}
public void consume(){
this.addHistory("Consuming event" + boundEvent);
if(consumedAt == null){
consumedAt = boundEvent;
}
}
public boolean canReceive(){
if(consumedAt == null){
return true;
}
return consumedAt.getPriority().isLowerPriority(boundEvent.getPriority());
}
public boolean isConsumed(){
return consumedAt != null;
}
public Priority consumedAt(){
return consumedAt.getPriority();
}
public void lock(String parameter){
this.addHistory("Locking " + (parameter==null?"the whole event":parameter) + " " + boundEvent);
if(lockedAt.containsKey(null)){
return; //Everything is already locked
}
if(parameter == null && !lockedAt.containsKey(null)){
lockedAt.put(null, boundEvent); //Everything is locked now
} else if(!lockedAt.containsKey(parameter)) {
lockedAt.put(parameter, boundEvent);
}
}
public boolean isLocked(String parameter){
Priority param = lockedAt.get(parameter)==null?null:lockedAt.get(parameter).getPriority();
Priority global = lockedAt.get(parameter)==null?null:lockedAt.get(null).getPriority();
if(param == null && global == null){
return false;
} else if(param == null){
return global.isHigherPriority(boundEvent.getPriority());
} else if(global == null){
return param.isHigherPriority(boundEvent.getPriority());
} else {
if(param.isHigherPriority(global)){
return param.isHigherPriority(boundEvent.getPriority());
} else {
return global.isHigherPriority(boundEvent.getPriority());
}
}
}
public Priority lockedAt(String parameter){
Priority param = lockedAt.get(parameter)==null?null:lockedAt.get(parameter).getPriority();
Priority global = lockedAt.get(parameter)==null?null:lockedAt.get(null).getPriority();
if(param == null && global == null){
return null; //It's not locked
} else if(param == null){
return global; //It's not parameter locked, but it is globally locked
} else if(global == null){
return param; //It's not globally locked, but it is parameter locked
} else {
//It's both. The higher priority one wins.
if(param.isHigherPriority(global)){
return param;
} else {
return global;
}
}
}
public void addWhenTriggered(CClosure tree){
this.addHistory("Adding a whenTriggered callback. " + boundEvent);
try {
whenTriggered.add(new Pair<CClosure, Environment>(tree, boundEvent.originalEnv.clone()));
} catch (CloneNotSupportedException ex) {
Logger.getLogger(BoundEvent.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void addWhenCancelled(CClosure tree){
this.addHistory("Adding a whenCancelled callback. " + boundEvent);
try {
whenCancelled.add(new Pair<CClosure, Environment>(tree, boundEvent.originalEnv.clone()));
} catch (CloneNotSupportedException ex) {
Logger.getLogger(BoundEvent.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void executeTriggered(){
// for(Pair<CClosure, Env> pair : whenTriggered){
// MethodScriptCompiler.execute(pair.fst, pair.snd, null, null);
// }
}
public void executeCancelled(){
// for(Pair<CClosure, Env> pair : whenCancelled){
// MethodScriptCompiler.execute(pair.fst, pair.snd, null, null);
// }
}
}
}