content: package emu.grasscutter.scripts;
  
  import com.github.davidmoten.rtreemulti.RTree;
  import com.github.davidmoten.rtreemulti.geometry.Geometry;
  import emu.grasscutter.Grasscutter;
  import emu.grasscutter.data.GameData;
  import emu.grasscutter.data.excels.MonsterData;
  import emu.grasscutter.data.excels.WorldLevelData;
  import emu.grasscutter.game.entity.*;
  import emu.grasscutter.game.props.EntityType;
  import emu.grasscutter.game.world.Scene;
  import emu.grasscutter.net.proto.VisionTypeOuterClass;
  import emu.grasscutter.scripts.constants.EventType;
  import emu.grasscutter.scripts.data.*;
  import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
  import emu.grasscutter.scripts.service.ScriptMonsterTideService;
  import io.netty.util.concurrent.FastThreadLocalThread;
  import org.luaj.vm2.LuaError;
  import org.luaj.vm2.LuaValue;
  import org.luaj.vm2.lib.jse.CoerceJavaToLua;
  
  import java.util.*;
  import java.util.concurrent.*;
  import java.util.stream.Collectors;
  
  public class SceneScriptManager {
      private final Scene scene;
      private final Map<String, Integer> variables;
      private SceneMeta meta;
      private boolean isInit;
      /**
       * current triggers controlled by RefreshGroup
       */
      private final Map<Integer, Set<SceneTrigger>> currentTriggers;
      private final Map<Integer, EntityRegion> regions; // <EntityId-Region>
      private final Map<Integer,SceneGroup> sceneGroups;
      private ScriptMonsterTideService scriptMonsterTideService;
      private ScriptMonsterSpawnService scriptMonsterSpawnService;
      /**
       * blockid - loaded groupSet
       */
      private final Map<Integer, Set<SceneGroup>> loadedGroupSetPerBlock;
      public static final ExecutorService eventExecutor;
      static {
          eventExecutor = new ThreadPoolExecutor(4, 4,
                  60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000),
                  FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy());
      }
      public SceneScriptManager(Scene scene) {
          this.scene = scene;
          this.currentTriggers = new ConcurrentHashMap<>();
  
          this.regions = new ConcurrentHashMap<>();
          this.variables = new ConcurrentHashMap<>();
          this.sceneGroups = new ConcurrentHashMap<>();
          this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
          this.loadedGroupSetPerBlock = new ConcurrentHashMap<>();
  
          // TEMPORARY
          if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
              return;
          }
  
          // Create
          this.init();
      }
  
      public Scene getScene() {
          return scene;
      }
  
      public SceneConfig getConfig() {
          if (!isInit) {
              return null;
          }
          return meta.config;
      }
  
      public Map<Integer, SceneBlock> getBlocks() {
          return meta.blocks;
      }
  
      public Map<String, Integer> getVariables() {
          return variables;
      }
  
      public Set<SceneTrigger> getTriggersByEvent(int eventId) {
          return currentTriggers.computeIfAbsent(eventId, e -> new HashSet<>());
      }
      public void registerTrigger(List<SceneTrigger> triggers) {
          triggers.forEach(this::registerTrigger);
      }
      public void registerTrigger(SceneTrigger trigger) {
          getTriggersByEvent(trigger.event).add(trigger);
          Grasscutter.getLogger().debug("Registered trigger {}", trigger.name);
      }
      public void deregisterTrigger(List<SceneTrigger> triggers) {
          triggers.forEach(this::deregisterTrigger);
      }
      public void deregisterTrigger(SceneTrigger trigger) {
          getTriggersByEvent(trigger.event).remove(trigger);
      }
      public void resetTriggers(int eventId) {
          currentTriggers.put(eventId, new HashSet<>());
      }
      public void refreshGroup(SceneGroup group, int suiteIndex) {
          var suite = group.getSuiteByIndex(suiteIndex);
          if (suite == null) {
              return;
          }
          if (suite.sceneTriggers.size() > 0) {
              for (var trigger : suite.sceneTriggers) {
                  resetTriggers(trigger.event);
                  this.currentTriggers.get(trigger.event).add(trigger);
              }
          }
          spawnMonstersInGroup(group, suite);
          spawnGadgetsInGroup(group, suite);
      }
      public EntityRegion getRegionById(int id) {
          return regions.get(id);
      }
  
      public void registerRegion(EntityRegion region) {
          regions.put(region.getId(), region);
          Grasscutter.getLogger().debug("Registered region {} from group {}", region.getMetaRegion().config_id, region.getGroupId());
      }
      public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite) {
          suite.sceneRegions.stream().map(region -> new EntityRegion(this.getScene(), region))
              .forEach(this::registerRegion);
      }
      public synchronized void deregisterRegion(SceneRegion region) {
          var instance = regions.values().stream()
              .filter(r -> r.getConfigId() == region.config_id)
              .findFirst();
          instance.ifPresent(entityRegion -> regions.remove(entityRegion.getId()));
      }
  
      public Map<Integer, Set<SceneGroup>> getLoadedGroupSetPerBlock() {
          return loadedGroupSetPerBlock;
      }
  
      // TODO optimize
      public SceneGroup getGroupById(int groupId) {
          for (SceneBlock block : this.getScene().getLoadedBlocks()) {
              var group = block.groups.get(groupId);
              if (group == null) {
                  continue;
              }
  
              if (!group.isLoaded()) {
                  getScene().onLoadGroup(List.of(group));
              }
              return group;
          }
          return null;
      }
  
      private void init() {
          var meta = ScriptLoader.getSceneMeta(getScene().getId());
          if (meta == null) {
              return;
          }
          this.meta = meta;
  
          // TEMP
          this.isInit = true;
      }
  
      public boolean isInit() {
          return isInit;
      }
  
      public void loadBlockFromScript(SceneBlock block) {
          block.load(scene.getId(), meta.context);
      }
  
      public void loadGroupFromScript(SceneGroup group) {
          group.load(getScene().getId());
  
          if (group.variables != null) {
              group.variables.forEach(var -> this.getVariables().put(var.name, var.value));
          }
  
          this.sceneGroups.put(group.id, group);
      }
  
      public void checkRegions() {
          if (this.regions.size() == 0) {
              return;
          }
  
          for (var region : this.regions.values()) {
              // currently all condition_ENTER_REGION Events check for avatar, so we have no necessary to add other types of entity
              getScene().getEntities().values()
                  .stream()
                  .filter(e -> e.getEntityType() == EntityType.Avatar.getValue() && region.getMetaRegion().contains(e.getPosition()))
                  .forEach(region::addEntity);
  
              var players = region.getScene().getPlayers();
              int targetID = 0;
              if (players.size() > 0)
                  targetID = players.get(0).getUid();
  
              if (region.hasNewEntities()) {
                  Grasscutter.getLogger().trace("Call EVENT_ENTER_REGION_{}",region.getMetaRegion().config_id);
                  callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId())
                      .setSourceEntityId(region.getId())
                      .setTargetEntityId(targetID)
                  );
  
                  region.resetNewEntities();
              }
  
              for (int entityId : region.getEntities()) {
                  if (getScene().getEntityById(entityId) == null || !region.getMetaRegion().contains(getScene().getEntityById(entityId).getPosition())) {
                      region.removeEntity(entityId);
  
                  }
              }
              if (region.entityLeave()) {
                  callEvent(EventType.EVENT_LEAVE_REGION, new ScriptArgs(region.getConfigId())
                      .setSourceEntityId(region.getId())
                      .setTargetEntityId(region.getFirstEntityId())
                  );
  
                  region.resetNewEntities();
              }
          }
      }
  
      public List<EntityGadget> getGadgetsInGroupSuite(SceneGroup group, SceneSuite suite) {
          return suite.sceneGadgets.stream()
              .map(g -> createGadget(group.id, group.block_id, g))
              .filter(Objects::nonNull)
              .toList();
      }
      public List<EntityMonster> getMonstersInGroupSuite(SceneGroup group, SceneSuite suite) {
          return suite.sceneMonsters.stream()
              .map(mob -> createMonster(group.id, group.block_id, mob))
              .filter(Objects::nonNull)
              .toList();
      }
      public void addGroupSuite(SceneGroup group, SceneSuite suite) {
          // we added trigger first
          registerTrigger(suite.sceneTriggers);
  
          var toCreate = new ArrayList<GameEntity>();
          toCreate.addAll(getGadgetsInGroupSuite(group, suite));
          toCreate.addAll(getMonstersInGroupSuite(group, suite));
          addEntities(toCreate);
  
          registerRegionInGroupSuite(group, suite);
      }
      public void removeGroupSuite(SceneGroup group, SceneSuite suite) {
          deregisterTrigger(suite.sceneTriggers);
          removeMonstersInGroup(group, suite);
          removeGadgetsInGroup(group, suite);
  
          suite.sceneRegions.forEach(this::deregisterRegion);
      }
  
      public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) {
          var gadgets = group.gadgets.values();
  
          if (suite != null) {
              gadgets = suite.sceneGadgets;
          }
  
          var toCreate = gadgets.stream()
                  .map(g -> createGadget(g.group.id, group.block_id, g))
                  .filter(Objects::nonNull)
                  .toList();
          this.addEntities(toCreate);
      }
  
      public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) {
          if (suite == null || suite.sceneMonsters.size() <= 0) {
              return;
          }
          this.addEntities(suite.sceneMonsters.stream()
                  .map(mob -> createMonster(group.id, group.block_id, mob)).toList());
      }
  
      public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
          this.scriptMonsterTideService =
                  new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
  
      }
      public void unloadCurrentMonsterTide() {
          if (this.getScriptMonsterTideService() == null) {
              return;
          }
          this.getScriptMonsterTideService().unload();
      }
      public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) {
          // TODO delay
          getScene().addEntity(createMonster(group.id, group.block_id, group.monsters.get(configId)));
      }
      // Events
      public void callEvent(int eventType, ScriptArgs params) {
          /**
           * We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for every groups' trigger in every scene instances.
           * But when callEvent is called in a ScriptLib func, it may cause NPE because the inner call cleans the ThreadLocal so that outer call could not get it.
           * e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE -> (remove)
           * So we use thread pool to clean the stack to avoid this new issue.
           */
          eventExecutor.submit(() -> this.realCallEvent(eventType, params));
      }
  
      private void realCallEvent(int eventType, ScriptArgs params) {
          try {
              ScriptLoader.getScriptLib().setSceneScriptManager(this);
              Set<SceneTrigger> relevantTriggers = new HashSet<>();
              if (eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) {
                  List<SceneTrigger> relevantTriggersList = this.getTriggersByEvent(eventType).stream()
                      .filter(p -> p.condition.contains(String.valueOf(params.param1))).toList();
                  relevantTriggers = new HashSet<>(relevantTriggersList);
              } else {relevantTriggers = this.getTriggersByEvent(eventType);}
              for (SceneTrigger trigger : relevantTriggers) {
                  try {
                      ScriptLoader.getScriptLib().setCurrentGroup(trigger.currentGroup);
                      LuaValue ret = this.callScriptFunc(trigger.condition, trigger.currentGroup, params);
                      Grasscutter.getLogger().trace("Call Condition Trigger {}, [{},{},{}]", trigger.condition, params.param1, params.source_eid, params.target_eid);
                      if (ret.isboolean() && ret.checkboolean()) {
                          // the SetGroupVariableValueByGroup in tower need the param to record the first stage time
                          this.callScriptFunc(trigger.action, trigger.currentGroup, params);
                          Grasscutter.getLogger().trace("Call Action Trigger {}", trigger.action);
                          if (trigger.event == EventType.EVENT_ENTER_REGION) {
                              EntityRegion region = this.regions.values().stream().filter(p -> p.getConfigId() == params.param1).toList().get(0);
                              getScene().getPlayers().forEach(p -> p.onEnterRegion(region.getMetaRegion()));
                              deregisterRegion(region.getMetaRegion());
                          } else if (trigger.event == EventType.EVENT_LEAVE_REGION) {
                              EntityRegion region = this.regions.values().stream().filter(p -> p.getConfigId() == params.param1).toList().get(0);
                              getScene().getPlayers().forEach(p -> p.onLeaveRegion(region.getMetaRegion()));
                              deregisterRegion(region.getMetaRegion());
                          }
                          deregisterTrigger(trigger);
                      } else {
                          Grasscutter.getLogger().debug("Condition Trigger {} returned {}", trigger.condition, ret);
                      }
                      //TODO some ret do not bool
                  }finally {
                      ScriptLoader.getScriptLib().removeCurrentGroup();
                  }
              }
          }finally {
              // make sure it is removed
              ScriptLoader.getScriptLib().removeSceneScriptManager();
          }
      }
  
      private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params) {
          LuaValue funcLua = null;
          if (funcName != null && !funcName.isEmpty()) {
              funcLua = (LuaValue) group.getBindings().get(funcName);
          }
  
          LuaValue ret = LuaValue.TRUE;
  
          if (funcLua != null) {
              LuaValue args = LuaValue.NIL;
  
              if (params != null) {
                  args = CoerceJavaToLua.coerce(params);
              }
  
              ret = safetyCall(funcName, funcLua, args);
          }
          return ret;
      }
  
      public LuaValue safetyCall(String name, LuaValue func, LuaValue args) {
          try {
              return func.call(ScriptLoader.getScriptLibLua(), args);
          }catch (LuaError error) {
              ScriptLib.logger.error("[LUA] call trigger failed {},{}",name,args,error);
              return LuaValue.valueOf(-1);
          }
      }
  
      public ScriptMonsterTideService getScriptMonsterTideService() {
          return scriptMonsterTideService;
      }
  
      public ScriptMonsterSpawnService getScriptMonsterSpawnService() {
          return scriptMonsterSpawnService;
      }
  
      public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) {
          if (g.isOneoff) {
              var hasEntity = getScene().getEntities().values().stream()
                  .filter(e -> e instanceof EntityGadget)
                  .filter(e -> e.getGroupId() == g.group.id)
                  .filter(e -> e.getConfigId() == g.config_id)
                  .findFirst();
              if (hasEntity.isPresent()) {
                  return null;
              }
          }
          EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos);
  
          if (entity.getGadgetData() == null) {
              return null;
          }
  
          entity.setBlockId(blockId);
          entity.setConfigId(g.config_id);
          entity.setGroupId(groupId);
          entity.getRotation().set(g.rot);
          entity.setState(g.state);
          entity.setPointType(g.point_type);
          entity.setMetaGadget(g);
          entity.buildContent();
  
          return entity;
      }
      public EntityNPC createNPC(SceneNPC npc, int blockId, int suiteId) {
          return new EntityNPC(getScene(), npc, blockId, suiteId);
      }
      public EntityMonster createMonster(int groupId, int blockId, SceneMonster monster) {
          if (monster == null) {
              return null;
          }
  
          MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id);
  
          if (data == null) {
              return null;
          }
  
          // Calculate level
          int level = monster.level;
  
          if (getScene().getDungeonData() != null) {
              level = getScene().getDungeonData().getShowLevel();
          } else if (getScene().getWorld().getWorldLevel() > 0) {
              WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel());
  
              if (worldLevelData != null) {
                  level = worldLevelData.getMonsterLevel();
              }
          }
  
          // Spawn mob
          EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level);
          entity.getRotation().set(monster.rot);
          entity.setGroupId(groupId);
          entity.setBlockId(blockId);
          entity.setConfigId(monster.config_id);
          entity.setPoseId(monster.pose_id);
  
          this.getScriptMonsterSpawnService()
                  .onMonsterCreatedListener.forEach(action -> action.onNotify(entity));
  
          return entity;
      }
  
      public void addEntity(GameEntity gameEntity) {
          getScene().addEntity(gameEntity);
      }
  
      public void meetEntities(List<? extends GameEntity> gameEntity) {
          getScene().addEntities(gameEntity, VisionTypeOuterClass.VisionType.VISION_TYPE_MEET);
      }
  
      public void addEntities(List<? extends GameEntity> gameEntity) {
          getScene().addEntities(gameEntity);
      }
  
      public RTree<SceneBlock, Geometry> getBlocksIndex() {
          return meta.sceneBlockIndex;
      }
      public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
          var configSet = suite.sceneMonsters.stream()
                  .map(m -> m.config_id)
                  .collect(Collectors.toSet());
          var toRemove = getScene().getEntities().values().stream()
                  .filter(e -> e instanceof EntityMonster)
                  .filter(e -> e.getGroupId() == group.id)
                  .filter(e -> configSet.contains(e.getConfigId()))
                  .toList();
  
          getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
      }
      public void removeGadgetsInGroup(SceneGroup group, SceneSuite suite) {
          var configSet = suite.sceneGadgets.stream()
                  .map(m -> m.config_id)
                  .collect(Collectors.toSet());
          var toRemove = getScene().getEntities().values().stream()
                  .filter(e -> e instanceof EntityGadget)
                  .filter(e -> e.getGroupId() == group.id)
                  .filter(e -> configSet.contains(e.getConfigId()))
                  .toList();
  
          getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
      }
  }
  