package me.km.snuviscript;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import me.hammerle.snuviscript.code.Script;
import me.hammerle.snuviscript.code.SnuviUtils;
import me.hammerle.snuviscript.inputprovider.Variable;
import me.km.utils.Utils;
import me.km.entities.EntityHuman;
import me.km.entities.EntityItemProjectile;
import me.km.events.CommandEvent;
import me.km.inventory.ModInventory;
import me.km.permissions.PermissionManager;
import me.km.utils.Location;
import net.minecraft.command.ICommandSource;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.passive.SheepEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.ClickType;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.management.PlayerList;
import net.minecraft.util.Hand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.math.EntityRayTraceResult;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraftforge.event.ServerChatEvent;
import net.minecraftforge.event.entity.EntityMountEvent;
import net.minecraftforge.event.entity.ProjectileImpactEvent;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.event.entity.living.*;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.entity.player.EntityItemPickupEvent;
import net.minecraftforge.event.entity.player.FillBucketEvent;
import net.minecraftforge.event.entity.player.ItemFishedEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.event.world.ExplosionEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;

public class ScriptEvents
{              
    private static void setLivingEntity(Script sc, LivingEntity ent)
    {
        sc.setVar("living_entity", ent);
    }

    private final Scripts scripts;
    private final MinecraftServer server;
    private final PermissionManager perms;
    
    public ScriptEvents(Scripts scripts, MinecraftServer server, PermissionManager perms)
    {
        this.scripts = scripts;
        this.server = server;
        this.perms = perms;
    }
            
    // -------------------------------------------------------------------------
    // basics
    // -------------------------------------------------------------------------
    
    private void handleEvent(Event e, String event, Consumer<Script> before, Consumer<Script> after)
    {      
        if(e.isCancelable())
        {
            scripts.getScriptManager().callEvent(event, sc -> 
            {
                before.accept(sc);
                sc.setVar("cancel", e.isCanceled());
            }, sc -> 
            {
                after.accept(sc);
                handleVar(sc, event, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            });
        }
        else
        {
            scripts.getScriptManager().callEvent(event, before, after);
        }
    }
    
    private void handleEvent(PlayerEntity p, String event, Consumer<Script> before, Consumer<Script> after)
    {      
        scripts.getScriptManager().callEvent(event, (sc) -> 
        {
            ScriptVars.setPlayerVars(sc, p); 
            before.accept(sc);
        }, after);
        
        if(p != null)
        {
            Script data = scripts.getScript(p);
            if(data != null)
            {
                scripts.getScriptManager().callEvent(event, data, (sc) -> 
                {
                    ScriptVars.setPlayerVars(sc, p); 
                    before.accept(sc);
                }, after);
            }
        }      
    }

    private void handleEvent(PlayerEntity p, String event, Consumer<Script> before)
    {      
        handleEvent(p, event, before, null);
    }
    
    private void handleVar(Script sc, String event, String name, Consumer<Variable> c)
    {
        try
        {
            ifVarNotNull(sc, name, c);
        }
        catch(Exception ex)
        {
            scripts.getLogger().print(String.format("invalid var in '%s' event", event), ex, null, sc.getName(), sc, sc.getActiveSourceLine());
        }
    }      
    
    private void ifVarNotNull(Script sc, String name, Consumer<Variable> c)
    {
        Variable v = sc.getVar(name);
        if(v != null)
        {
            c.accept(v);
        }
    }
    
    private void simpleCancel(Script sc, Event e, String name)
    {
        try
        {
            ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
        }
        catch(Exception ex)
        {
            scripts.getLogger().print(String.format("invalid var in '%s' event", name), ex, null, sc.getName(), sc, sc.getActiveSourceLine());
        }
    }
         
    // -------------------------------------------------------------------------
    // events
    // -------------------------------------------------------------------------

    public void onPlayerDataTick(PlayerEntity p, String var)
    {      
        handleEvent(p, "player_data_tick", sc -> 
        {
            sc.setVar("var", var);
        });
    }
    
    public void onPlayerMove(PlayerEntity p, int id)
    {      
        handleEvent(p, "player_move", sc -> 
        {
            sc.setVar("id", (double) id);
        });
    }
    
    public boolean onInventoryClick(Script script, ITextComponent text, ModInventory inv, int slot, ClickType click, PlayerEntity p)
    {
        scripts.getScriptManager().callEvent("inv_click", script, sc -> 
        {
            ScriptVars.setPlayerVars(sc, p); 
            sc.setVar("inv", inv);
            sc.setVar("inv_id", (double) inv.getModId());
            sc.setVar("inv_name", text.getFormattedText());
            sc.setVar("inv_slot", (double) slot);
            ScriptVars.setItemVars(sc, inv.getStackInSlot(slot));
            sc.setVar("cancel", false); 
        }, null);
        Variable v = script.getVar("cancel");
        return v != null && v.getBoolean(script);
    }

    public void onInventoryClose(Script script, ITextComponent text, ModInventory inv, PlayerEntity p)
    {
        scripts.getScriptManager().callEvent("inv_close", script, sc -> 
        {
            ScriptVars.setPlayerVars(sc, p); 
            sc.setVar("inv", inv);
            sc.setVar("inv_id", (double) inv.getModId());
            sc.setVar("inv_name", text.getFormattedText());
        }, null);
    }
    
    public void onHumanHurt(PlayerEntity p, EntityHuman h)
    {
        handleEvent(p, "human_hurt", sc -> ScriptVars.setEntityVars(sc, h));
    }
    
    @SubscribeEvent
    public void onPlayerPostRespawn(PlayerEvent.PlayerRespawnEvent e)
    {
        handleEvent(e.getPlayer(), "player_post_respawn", sc -> {});
    }
    
    public void onPlayerPreRespawn(PlayerEntity p)
    {
        handleEvent(p, "player_pre_respawn", sc -> {});
    }
    
    @SubscribeEvent
    public void onOldPlayerDamage(LivingHurtEvent e)
    {        
        if(!(e.getEntityLiving() instanceof ServerPlayerEntity))
        {
            return;
        }
        PlayerEntity p = (PlayerEntity) e.getEntityLiving();  
        handleEvent(p, "player_hurt", (sc) -> 
        {
            sc.setVar("player_killed", p.getHealth() <= e.getAmount());   
            sc.setVar("player_damage", (double) e.getAmount());   
            sc.setVar("player_damage_cause", e.getSource());
            PlayerEntity ent = Utils.getDamager(e.getSource());
            if(ent != null)
            {
                sc.setVar("player_involved", true);
                ScriptVars.setSecPlayer(sc, ent);
            }
            else
            {
                sc.setVar("player_involved", false);
            }
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            try
            {
                ifVarNotNull(sc, "player_damage", v -> e.setAmount(v.getFloat(sc)));
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'player_hurt' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    } 
    
    @SubscribeEvent
    public void onOldEntityDamage(LivingHurtEvent e)
    {        
        PlayerEntity p = Utils.getDamager(e.getSource());
        if(p == null)
        {
            return;
        }
        handleEvent(p, "entity_hurt", (sc) -> 
        {
            sc.setVar("entity_killed", e.getEntityLiving().getHealth() <= e.getAmount());        
            ScriptVars.setEntityVars(sc, e.getEntity()); 
            sc.setVar("entity_damage", (double) e.getAmount());   
            sc.setVar("entity_damage_cause", e.getSource());
            sc.setVar("cancel", e.isCanceled());   
        }, (sc) -> 
        {
            try
            {
                ifVarNotNull(sc, "entity_damage", v -> e.setAmount(v.getFloat(sc)));
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'entity_hurt' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    }
    
    @SubscribeEvent
    public void onEntityDamage(LivingHurtEvent e)
    {        
        handleEvent(e, "living_hurt", (sc) -> 
        {
            setLivingEntity(sc, e.getEntityLiving());
            sc.setVar("damage_source", e.getSource());  
            sc.setVar("damage_amount", (double) e.getAmount());  
        }, (sc) -> 
        {
            handleVar(sc, "living_hurt", "damage_amount", v -> e.setAmount(v.getFloat(sc)));
        });
    } 
    
    @SubscribeEvent
    public void onPlayerAttack(LivingAttackEvent e)
    {        
        LivingEntity liv = e.getEntityLiving();
        if(liv instanceof PlayerEntity)
        {
            PlayerEntity p = (PlayerEntity) liv;
            handleEvent(p, "player_is_attacked", (sc) -> 
            {
                sc.setVar("player_killed", p.getHealth() <= e.getAmount());   
                sc.setVar("player_damage_cause", e.getSource());
                PlayerEntity ent = Utils.getDamager(e.getSource());
                if(ent != null)
                {
                    sc.setVar("player_involved", true);
                    ScriptVars.setSecPlayer(sc, ent);
                }
                else
                {
                    sc.setVar("player_involved", false);
                }
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                simpleCancel(sc, e, "player_is_attacked");
            });
        }
        else
        {
            PlayerEntity p = Utils.getDamager(e.getSource());
            if(p != null)
            {
                handleEvent(p, "player_attacks", (sc) -> 
                {
                    ScriptVars.setEntityVars(sc, liv);
                    sc.setVar("cancel", e.isCanceled()); 
                }, (sc) -> 
                {
                    simpleCancel(sc, e, "player_attacks");
                });
            }
        }
    } 
    
    @SubscribeEvent
    public void onOldPlayerHeal(LivingHealEvent e)
    {        
        if(e.getEntityLiving() instanceof PlayerEntity)
        {
            handleEvent((PlayerEntity) e.getEntityLiving(), "player_heal", (sc) -> 
            {
                sc.setVar("heal", (double) e.getAmount());
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                try
                {
                    ifVarNotNull(sc, "heal", v -> e.setAmount(v.getFloat(sc)));
                    ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
                }
                catch(Exception ex)
                {
                    scripts.getLogger().print("invalid var in 'player_heal' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
                }
            });
        }
    } 
    
    @SubscribeEvent
    public void onLivingHeal(LivingHealEvent e)
    {        
        handleEvent(e, "living_heal", (sc) -> 
        {
            setLivingEntity(sc, e.getEntityLiving());
            sc.setVar("heal_amount", (double) e.getAmount());  
        }, (sc) -> 
        {
            handleVar(sc, "living_heal", "heal_amount", v -> e.setAmount(v.getFloat(sc)));
        });
    } 
    
    @SubscribeEvent
    public void onEntityDamage(LivingHealEvent e)
    {        
        if(e.getEntityLiving() instanceof PlayerEntity)
        {
            handleEvent((PlayerEntity) e.getEntityLiving(), "player_heal", (sc) -> 
            {
                sc.setVar("heal", (double) e.getAmount());
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                try
                {
                    ifVarNotNull(sc, "heal", v -> e.setAmount(v.getFloat(sc)));
                    ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
                }
                catch(Exception ex)
                {
                    scripts.getLogger().print("invalid var in 'player_heal' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
                }
            });
        }
    } 
    
    @SubscribeEvent
    public void onLivingDeath(LivingDeathEvent e)
    {        
        if(!(e.getEntityLiving() instanceof PlayerEntity))
        {
            PlayerEntity p = Utils.getDamager(e.getSource());
            handleEvent(p, "entity_death", (sc) -> 
            {
                sc.setVar("entity_damage_cause", e.getSource());
                LivingEntity ent = e.getEntityLiving();
                ScriptVars.setEntityVars(sc, ent);
                sc.setVar("player_involved", p != null);
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                simpleCancel(sc, e, "entity_death");
            });
            return;
        }
        PlayerEntity p = (PlayerEntity) e.getEntity();    
        handleEvent(p, "player_death", (sc) -> 
        {
            sc.setVar("clear", false);
            sc.setVar("player_damage_cause", e.getSource());
            PlayerEntity ent = Utils.getDamager(e.getSource());
            if(ent != null)
            {
                ScriptVars.setSecPlayer(sc, ent);
            }
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            try
            {
                Variable clear = sc.getVar("clear");
                if(clear != null && clear.getBoolean(sc))
                {
                    p.inventory.clear();
                }
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'player_death' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    } 
    
    @SubscribeEvent
    public void onEntityDrop(LivingDropsEvent e)
    {             
        try
        {
            PlayerEntity p = Utils.getDamager(e.getSource());
            handleEvent(p, "entity_drop", (sc) -> 
            {
                sc.setVar("drops", e.getDrops());
                ScriptVars.setEntityVars(sc, e.getEntityLiving());
                sc.setVar("player_involved", p != null);
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                simpleCancel(sc, e, "entity_drop");
            });
        }
        catch(NullPointerException ex)
        {
            scripts.getLogger().print(ex);
        }
    } 
            
    @SubscribeEvent
    public void onPlayerDrop(LivingDropsEvent e)
    {             
        if(!(e.getEntityLiving() instanceof PlayerEntity))
        {
            return;
        }
        handleEvent((PlayerEntity) e.getEntityLiving(), "player_drop", (sc) -> 
        {
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "player_drop");
        });
    } 
    
    @SubscribeEvent
    public void onProjectileHit(ProjectileImpactEvent e)
    {        
        final Entity hitEntity;
        final Location loc;
        
        RayTraceResult ray = e.getRayTraceResult();
        switch(ray.getType())
        {
            case ENTITY:
                hitEntity = ((EntityRayTraceResult) e.getRayTraceResult()).getEntity();
                loc = null;
                break;
            case BLOCK:
                loc = new Location(e.getEntity().world, ((BlockRayTraceResult) e.getRayTraceResult()).getPos());
                hitEntity = null;
                break;
            default:
                return;
        }
        
        handleEvent(Utils.getPlayerFromProjectile(e.getEntity()), "throw_hit", (sc) -> 
        {
            sc.setVar("entity_hit", hitEntity);
            sc.setVar("loc_hit", loc);
            ScriptVars.setEntityVars(sc, e.getEntity());
        });   
    }
    
    public void onEntityItemProjectileHit(EntityItemProjectile ent, PlayerEntity p, ItemStack stack, Entity hitEntity, BlockPos pos)
    {        
        Location loc;
        if(pos == null)
        {
            loc = null;
        }
        else
        {
            loc = new Location(ent.world, pos);
        }
        handleEvent(p, "item_hit", (sc) -> 
        {
            ScriptVars.setEntityVars(sc, ent);
            ScriptVars.setItemVars(sc, stack);
            sc.setVar("entity_hit", hitEntity);
            sc.setVar("loc_hit", loc);
        });      
    }
    
    @SubscribeEvent
    public void onEntityShear(PlayerInteractEvent.EntityInteract e)
    {              
        if(e.getHand() == Hand.OFF_HAND || !(e.getTarget() instanceof SheepEntity))
        {
            return;
        }
        PlayerEntity p = e.getPlayer();   
        if(p.getHeldItemMainhand().getItem() != Items.SHEARS)
        {
            return;
        }
        SheepEntity sheep = (SheepEntity) e.getTarget();
        handleEvent(p, "entity_shear", (sc) -> 
        {
            ScriptVars.setEntityVars(sc, sheep);
            sc.setVar("entity_sheared", sheep.getSheared());
            sc.setVar("entity_color", sheep.getFleeceColor().toString());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "entity_shear");
        });
    }
       
    @SubscribeEvent
    public void onBlockBreak(BlockEvent.BreakEvent e)
    {        
        handleEvent(e.getPlayer(), "block_break", (sc) -> 
        {
            ScriptVars.setBlockVars(sc, e.getWorld(), e.getPos());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "block_break");
        });
    }
    
    @SubscribeEvent
    public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent e)
    {    
        PlayerEntity p = e.getPlayer();
        if(p == null)
        {
            return;
        }
        handleEvent(p, "player_login", (sc) -> 
        {       
            PlayerList list = server.getPlayerList();
            sc.setVar("is_banned", list.getBannedPlayers().isBanned(p.getGameProfile()));
            sc.setVar("is_whitelisted", list.getWhitelistedPlayers().isWhitelisted(p.getGameProfile()));
        });
    }
    
    @SubscribeEvent
    public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent e)
    {    
        PlayerEntity p = e.getPlayer();
        if(p == null || e.getPlayer().ticksExisted < 20)
        {
            return;
        }
        handleEvent(p, "player_logout", sc -> {});
    }
    
    @SubscribeEvent
    public void onBucketFill(FillBucketEvent e)
    {     
        handleEvent(e.getPlayer(), "bucket_use", (sc) -> 
        {
            RayTraceResult ray = e.getTarget();
            if(ray != null && ray.hitInfo != null && ray.hitInfo instanceof BlockPos)
            {
                BlockPos pos = (BlockPos) ray.hitInfo;
                sc.setVar("has_block", true); 
                ScriptVars.setBlockVars(sc, e.getWorld(), pos);
            }
            else
            {
                sc.setVar("has_block", false); 
            }
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "bucket_use");
        });
    } 
       
    @SubscribeEvent
    public void onBlockPlace(BlockEvent.EntityPlaceEvent e)
    {
        if(!(e.getEntity() instanceof PlayerEntity))
        {
            return;
        }
        handleEvent((PlayerEntity) e.getEntity(), "block_place", (sc) -> 
        {
            sc.setVar("block_type_after", e.getPlacedBlock().getBlock().getRegistryName());
            ScriptVars.setBlockVars(sc, e.getWorld(), e.getPos());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "block_place");
        });
    }    
    
    @SubscribeEvent
    public void onRightClickBlock(PlayerInteractEvent.RightClickBlock e)
    {
        if(e.getHand() == Hand.OFF_HAND)
        {
            return;
        }
        handleEvent(e.getPlayer(), "block_click", (sc) -> 
        {
            sc.setVar("action", "right");
            ScriptVars.setBlockVars(sc, e.getWorld(), e.getPos());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "block_click");
        });       
    }
    
    @SubscribeEvent
    public void onLeftClickBlock(PlayerInteractEvent.LeftClickBlock e)
    {
        if(e.getHand() == Hand.OFF_HAND)
        {
            return;
        }
        handleEvent(e.getPlayer(), "block_click", (sc) -> 
        {
            sc.setVar("action", "left");
            ScriptVars.setBlockVars(sc, e.getWorld(), e.getPos());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "block_click");
        });       
    }
    
    @SubscribeEvent(receiveCanceled = true)
    public void onEntityClick(PlayerInteractEvent.EntityInteract e)
    {
        if(e.getHand() != Hand.OFF_HAND)
        {
            handleEvent(e.getPlayer(), "entity_click", (sc) -> 
            {
                ScriptVars.setEntityVars(sc, e.getTarget());
                sc.setVar("cancel", e.isCanceled()); 
            }, (sc) -> 
            {
                simpleCancel(sc, e, "entity_click");
            });
        }
    }
    
    public void onEmptyLeftClick(PlayerEntity p)
    {
        handleEvent(p, "left_click_air", (sc) -> 
        {
        });
    }
    
    @SubscribeEvent
    public void onItemClick(PlayerInteractEvent.RightClickItem e)
    {        
        handleEvent(e.getPlayer(), "item_air_click", (sc) -> 
        {
            ScriptVars.setItemVars(sc, e.getItemStack());
            sc.setVar("hand", e.getHand().toString());
            sc.setVar("cancel", e.isCanceled());  
        }, (sc) -> 
        {
            simpleCancel(sc, e, "item_air_click");
        });
    }
    
    @SubscribeEvent
    public void onItemUseStart(LivingEntityUseItemEvent.Start e)
    {        
        if(!(e.getEntityLiving() instanceof PlayerEntity))
        {
            return;
        }
        handleEvent((PlayerEntity) e.getEntityLiving(), "item_use_start", (sc) -> 
        {
            sc.setVar("duration", (double) e.getDuration());
            sc.setVar("cancel", e.isCanceled());  
        }, (sc) -> 
        {
            try
            {
                ifVarNotNull(sc, "duration", v -> e.setDuration(v.getInt(sc)));
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'item_use_start' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    }
    
    @SubscribeEvent
    public void onConsuming(LivingEntityUseItemEvent.Finish e)
    {
        if(!(e.getEntityLiving() instanceof PlayerEntity))
        {
            return;
        }
        handleEvent((PlayerEntity) e.getEntityLiving(), "item_use_finish", (sc) -> 
        {
            sc.setVar("result_stack", e.getResultStack());  
        });
    }
    
    @SubscribeEvent
    public void onFishing(ItemFishedEvent e)
    {
        handleEvent(e.getPlayer(), "fishing", (sc) -> 
        {
            sc.setVar("drops", e.getDrops());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "fishing");
        });
    }
    
    @SubscribeEvent
    public void onCrafting(PlayerEvent.ItemCraftedEvent e)
    {  
        handleEvent(e.getPlayer(), "craft", (sc) -> 
        {
            ScriptVars.setItemVars(sc, e.getCrafting());
        });   
    } 
    
    @SubscribeEvent
    public void onItemDrop(ItemTossEvent e)
    {
        handleEvent(e.getPlayer(), "player_toss", (sc) -> 
        {
            ScriptVars.setItemVars(sc, e.getEntityItem().getItem());   
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "player_toss");
        });
    }
    
    @SubscribeEvent
    public void onItemPickup(EntityItemPickupEvent e)
    {
        handleEvent(e.getPlayer(), "player_pickup", (sc) -> 
        {
            ScriptVars.setEntityVars(sc, e.getItem()); 
            ScriptVars.setItemVars(sc, e.getItem().getItem());  
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "player_pickup");
        });
    }
    
    @SubscribeEvent
    public void onVehicleEnter(EntityMountEvent e)
    {
        if(!(e.getEntityMounting() instanceof PlayerEntity))
        {
            return;
        }
        PlayerEntity p = (PlayerEntity) e.getEntityMounting();
        handleEvent(p, "entity_mount", (sc) -> 
        {
            sc.setVar("mounting", e.isMounting()); 
            ScriptVars.setEntityVars(sc, e.getEntityBeingMounted()); 
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "entity_mount");
        });
    }
    
    @SubscribeEvent
    public void onPlayerUsePortal(PlayerEvent.PlayerChangedDimensionEvent e)
    {
        handleEvent(e.getPlayer(), "portal", (sc) -> 
        {
            sc.setVar("from", e.getFrom().getRegistryName().getPath());
            sc.setVar("to", e.getTo().getRegistryName().getPath());
        });
    }
    
    public void onCommand(CommandEvent e)
    {
        handleEvent(e.getPlayer(), "command", (sc) -> 
        {
            sc.setVar("command", e.getName()); 
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            try
            {
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'command' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    }
    
    public void onCustomCommand(PlayerEntity p, String command, String[] args)
    {
        handleEvent(p, "custom_command", (sc) -> 
        {
            sc.setVar("command", command);
            if(args.length == 0)
            {
                sc.setVar("args", new ArrayList<>());
            }
            else
            {
                sc.setVar("args", Arrays.stream(args).map(s -> SnuviUtils.convert(s)).collect(Collectors.toList()));
            }
        });
    }
    
    public void onFunctionKey(ServerPlayerEntity p, int key)
    {        
        handleEvent(p, "function_key", (sc) -> 
        {
            sc.setVar("key", (double) key);
        });      
    }
    
    @SubscribeEvent
    public void onChatEvent(ServerChatEvent e)
    {    
        handleEvent(e.getPlayer(), "chat", (sc) -> 
        {
            String text = e.getMessage();
            if(perms.hasPermission(e.getPlayer(), "color"))
            {
                text = text.replace('&', '§');
            }
            else
            {
                text = text.replaceAll("&.", "");
            }
            sc.setVar("message", text);
            sc.setVar("cancel", e.isCanceled());
        }, (sc) -> 
        {
            try
            {
                ifVarNotNull(sc, "message", v -> e.setComponent(new StringTextComponent(v.getString(sc))));
                ifVarNotNull(sc, "cancel", v -> e.setCanceled(v.getBoolean(sc)));
            }
            catch(Exception ex)
            {
                scripts.getLogger().print("invalid var in 'chat' event", ex, null, sc.getName(), sc, sc.getActiveSourceLine());
            }
        });
    }

    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public void onExplosion(ExplosionEvent.Start e)
    {
        e.setCanceled(true);
        handleEvent(e, "explosion", sc -> 
        {
            sc.setVar("location", new Location(e.getWorld(), e.getExplosion().getPosition()));
        }, null);
    }
    
    private static String getName(ICommandSource cs)
    {
        if(cs instanceof PlayerEntity)
        {
            return ((PlayerEntity) cs).getName().getFormattedText();
        }
        else if(cs instanceof MinecraftServer)
        {
            return "Server";
        }
        return null;
    }
    
    public void onMissingCommand(ICommandSource cs, String command)
    {   
        PlayerEntity p = null;
        if(cs instanceof PlayerEntity)
        {
            p = (PlayerEntity) cs;
        }
        
        handleEvent(p, "missing_command", (sc) -> 
        {
            sc.setVar("command_name", command);
            sc.setVar("sender_name", getName(cs));
        });
    }
    
    public void onMissingPermission(ICommandSource cs, String command)
    {    
        PlayerEntity p = null;
        if(cs instanceof PlayerEntity)
        {
            p = (PlayerEntity) cs;
        }
        
        handleEvent(p, "missing_perm", (sc) -> 
        {
            sc.setVar("command_name", command);
            sc.setVar("sender_name", getName(cs));
        });
    }
    
    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public void onEntityHit(AttackEntityEvent e)
    {
        handleEvent(e.getPlayer(), "player_attack_entity", (sc) -> 
        {
            ScriptVars.setEntityVars(sc, e.getTarget());
            sc.setVar("cancel", e.isCanceled()); 
        }, (sc) -> 
        {
            simpleCancel(sc, e, "player_attack_entity");
        });
    }
}
