package emu.grasscutter.game.world;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.player.Player.SceneLoadState;
import emu.grasscutter.game.props.EnterReason;
import emu.grasscutter.game.props.EntityIdType;
import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.DungeonData;
import emu.grasscutter.data.excels.SceneData;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType;
import emu.grasscutter.scripts.data.SceneConfig;
import emu.grasscutter.server.event.player.PlayerTeleportEvent;
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.PacketDelTeamEntityNotify;
import emu.grasscutter.server.packet.send.PacketPlayerEnterSceneNotify;
import emu.grasscutter.server.packet.send.PacketScenePlayerInfoNotify;
import emu.grasscutter.server.packet.send.PacketSyncScenePlayTeamEntityNotify;
import emu.grasscutter.server.packet.send.PacketSyncTeamEntityNotify;
import emu.grasscutter.server.packet.send.PacketWorldPlayerInfoNotify;
import emu.grasscutter.server.packet.send.PacketWorldPlayerRTTNotify;
import emu.grasscutter.utils.Position;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;

public class World implements Iterable<Player> {
    private final GameServer server;
    private final Player owner;
    private final List<Player> players;
    private final Int2ObjectMap<Scene> scenes;

    private int levelEntityId;
    private int nextEntityId = 0;
    private int nextPeerId = 0;
    private int worldLevel;

    private boolean isMultiplayer;

    public World(Player player) {
        this(player, false);
    }

    public World(Player player, boolean isMultiplayer) {
        this.owner = player;
        this.server = player.getServer();
        this.players = Collections.synchronizedList(new ArrayList<>());
        this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());

        this.levelEntityId = this.getNextEntityId(EntityIdType.MPLEVEL);
        this.worldLevel = player.getWorldLevel();
        this.isMultiplayer = isMultiplayer;

        this.owner.getServer().registerWorld(this);
    }

    public Player getHost() {
        return owner;
    }

    public GameServer getServer() {
        return server;
    }

    public int getLevelEntityId() {
        return levelEntityId;
    }

    public int getHostPeerId() {
        if (this.getHost() == null) {
            return 0;
        }
        return this.getHost().getPeerId();
    }

    public int getNextPeerId() {
        return ++this.nextPeerId;
    }

    public int getWorldLevel() {
        return worldLevel;
    }

    public void setWorldLevel(int worldLevel) {
        this.worldLevel = worldLevel;
    }

    public List<Player> getPlayers() {
        return players;
    }

    public Int2ObjectMap<Scene> getScenes() {
        return this.scenes;
    }

    public Scene getSceneById(int sceneId) {
        // Get scene normally
        Scene scene = this.getScenes().get(sceneId);
        if (scene != null) {
            return scene;
        }

        // Create scene from scene data if it doesnt exist
        SceneData sceneData = GameData.getSceneDataMap().get(sceneId);
        if (sceneData != null) {
            scene = new Scene(this, sceneData);
            this.registerScene(scene);
            return scene;
        }

        return null;
    }

    public int getPlayerCount() {
        return this.getPlayers().size();
    }

    public boolean isMultiplayer() {
        return isMultiplayer;
    }

    public int getNextEntityId(EntityIdType idType) {
        return (idType.getId() << 24) + ++this.nextEntityId;
    }

    public synchronized void addPlayer(Player player) {
        // Check if player already in
        if (this.getPlayers().contains(player)) {
            return;
        }

        // Remove player from prev world
        if (player.getWorld() != null) {
            player.getWorld().removePlayer(player);
        }

        // Register
        player.setWorld(this);
        this.getPlayers().add(player);

        // Set player variables
        player.setPeerId(this.getNextPeerId());
        player.getTeamManager().setEntityId(this.getNextEntityId(EntityIdType.TEAM));

        // Copy main team to multiplayer team
        if (this.isMultiplayer()) {
            player.getTeamManager().getMpTeam().copyFrom(player.getTeamManager().getCurrentSinglePlayerTeamInfo(), player.getTeamManager().getMaxTeamSize());
            player.getTeamManager().setCurrentCharacterIndex(0);
        }

        // Add to scene
        Scene scene = this.getSceneById(player.getSceneId());
        scene.addPlayer(player);

        // Info packet for other players
        if (this.getPlayers().size() > 1) {
            this.updatePlayerInfos(player);
        }
    }

    public synchronized void removePlayer(Player player) {
        // Remove team entities
        player.sendPacket(
                new PacketDelTeamEntityNotify(
                        player.getSceneId(),
                    this.getPlayers().stream().map(p -> p.getTeamManager().getEntityId()).collect(Collectors.toList())
                )
        );

        // Deregister
        this.getPlayers().remove(player);
        player.setWorld(null);

        // Remove from scene
        Scene scene = this.getSceneById(player.getSceneId());
        scene.removePlayer(player);

        // Info packet for other players
        if (this.getPlayers().size() > 0) {
            this.updatePlayerInfos(player);
        }

        // Disband world if host leaves
        if (this.getHost() == player) {
            List<Player> kicked = new ArrayList<>(this.getPlayers());
            for (Player victim : kicked) {
                World world = new World(victim);
                world.addPlayer(victim);

                victim.sendPacket(new PacketPlayerEnterSceneNotify(victim, EnterType.ENTER_TYPE_SELF, EnterReason.TeamKick, victim.getSceneId(), victim.getPosition()));
            }
        }
    }

    public void registerScene(Scene scene) {
        this.getScenes().put(scene.getId(), scene);
    }

    public void deregisterScene(Scene scene) {
        this.getScenes().remove(scene.getId());
    }

    public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
        return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
    }

    public boolean transferPlayerToScene(Player player, int sceneId, TeleportType teleportType, Position pos) {
        return this.transferPlayerToScene(player, sceneId, teleportType, null, pos);
    }

    public boolean transferPlayerToScene(Player player, int sceneId, DungeonData data) {
        return this.transferPlayerToScene(player, sceneId, TeleportType.DUNGEON, data, null);
    }

    public boolean transferPlayerToScene(Player player, int sceneId, TeleportType teleportType, DungeonData dungeonData, Position teleportTo) {
        // Call player teleport event.
        PlayerTeleportEvent event = new PlayerTeleportEvent(player, teleportType, player.getPosition(), teleportTo);
        // Call event & check if it was canceled.
        event.call(); if (event.isCanceled()) {
            return false; // Teleport was canceled.
        }

        // Set the destination.
        teleportTo = event.getDestination();

        if (GameData.getSceneDataMap().get(sceneId) == null) {
            return false;
        }

        Scene oldScene = null;

        if (player.getScene() != null) {
            oldScene = player.getScene();

            // Don't deregister scenes if the player is going to tp back into them
            if (oldScene.getId() == sceneId) {
                oldScene.setDontDestroyWhenEmpty(true);
            }

            oldScene.removePlayer(player);
        }

        Scene newScene = this.getSceneById(sceneId);
        newScene.setDungeonData(dungeonData);
        newScene.addPlayer(player);

        // Dungeon
        SceneConfig config = newScene.getScriptManager().getConfig();
        if (teleportTo == null && config != null) {
            if (config.born_pos != null) {
                teleportTo = newScene.getScriptManager().getConfig().born_pos;
            }
            if (config.born_rot != null) {
                player.getRotation().set(config.born_rot);
            }
        }

        // Set player position
        if (teleportTo == null) {
            teleportTo = player.getPosition();
        }

        player.getPosition().set(teleportTo);

        if (oldScene != null) {
            newScene.setPrevScene(oldScene.getId());
            oldScene.setDontDestroyWhenEmpty(false);
        }

        // Get enter types
        EnterType enterType = EnterType.ENTER_TYPE_JUMP;
        EnterReason enterReason = EnterReason.TransPoint;

        if (dungeonData != null) {
            enterType = EnterType.ENTER_TYPE_DUNGEON;
            enterReason = EnterReason.DungeonEnter;
        } else if (oldScene == newScene) {
            enterType = EnterType.ENTER_TYPE_GOTO;
        } else if (newScene.getSceneType() == SceneType.SCENE_HOME_WORLD) {
            // Home
            enterReason = EnterReason.EnterHome;
            enterType = EnterType.ENTER_TYPE_SELF_HOME;

        }

        // Teleport packet
        player.sendPacket(new PacketPlayerEnterSceneNotify(player, enterType, enterReason, sceneId, teleportTo));
        return true;
    }

    private void updatePlayerInfos(Player paramPlayer) {
        for (Player player : this.getPlayers()) {
            // Dont send packets if player is logging in and filter out joining player
            if (!player.hasSentLoginPackets() || player == paramPlayer) {
                continue;
            }

            // Update team of all players since max players has been changed - Probably not the best way to do it
            if (this.isMultiplayer()) {
                player.getTeamManager().getMpTeam().copyFrom(player.getTeamManager().getMpTeam(), player.getTeamManager().getMaxTeamSize());
                player.getTeamManager().updateTeamEntities(null);
            }

            // Dont send packets if player is loading into the scene
            if (player.getSceneLoadState().getValue() < SceneLoadState.INIT.getValue() ) {
                // World player info packets
                player.getSession().send(new PacketWorldPlayerInfoNotify(this));
                player.getSession().send(new PacketScenePlayerInfoNotify(this));
                player.getSession().send(new PacketWorldPlayerRTTNotify(this));

                // Team packets
                player.getSession().send(new PacketSyncTeamEntityNotify(player));
                player.getSession().send(new PacketSyncScenePlayTeamEntityNotify(player));
            }
        }
    }

    public void broadcastPacket(BasePacket packet) {
        // Send to all players - might have to check if player has been sent data packets
        for (Player player : this.getPlayers()) {
            player.getSession().send(packet);
        }
    }

    // Returns true if the world should be deleted
    public boolean onTick() {
        if (this.getPlayerCount() == 0) return true;
        this.scenes.forEach((k, scene) -> scene.onTick());
        return false;
    }

    public void close() {

    }

    @Override
    public Iterator<Player> iterator() {
        return this.getPlayers().iterator();
    }
}
