
/*
 * Copyright 2011-2022 Cuberite Contributors
 *
 * 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.
 */

#include "ClientHandle.h"
#include "CMDUtils.h"
#include "Server.h"

bool HandleClearCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    cPlayerListCallback ClearInventory = [&](cPlayer& other_player) -> bool {
        other_player.GetInventory().Clear();
        if (a_Split.size() > 1) {
            CommandSendMessageSuccess(a_Player, a_Output, "You cleared the inventory of player \"" + other_player.GetName() + "\"");
        } else
            other_player.SendMessageSuccess("You cleared your own inventory");
        return false;
    };

    if (a_Split.size() < 2) {
        if (!a_Player) {
            a_Output->Out("Usage: " + a_Split[0] + " <player>");
        } else
            ClearInventory(*a_Player);
    } else if (!a_Player || a_Player->HasPermission("core.admin.clear")) {
        if (a_Split[1] == "" || !cRoot::Get()->FindAndDoWithPlayer(a_Split[1], ClearInventory)) 
            CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
    }
    return true;
}


bool HandleEffectCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    if (a_Split.size() < 3) {
		CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <player> <effect> [seconds] [amplifier] OR " + a_Split[0] + " <player> clear");
        return true;
    }
    bool all_players = a_Split[1] == "@a";
    if (a_Split[2] != "clear") {
        int effect_id = 0;
        int seconds = 30;
        int amplifier = 0;
        bool invalid_effect = false;
        try { // effect
            effect_id = std::stoi(a_Split[2]);
        } catch (...) {
            invalid_effect = true;
        }
        if (invalid_effect || effect_id < 1 || effect_id > 23) {
			CommandSendMessageFailure(a_Player, a_Output,  "Invalid effect ID \"" + a_Split[2] + "\"");
            return true;
        }
                
        if (a_Split.size() >= 4) { // duration
            try { 
                seconds = std::stoi(a_Split[3]); 
            } catch (...) {
			    CommandSendMessageFailure(a_Player, a_Output, "Invalid duration \"" + a_Split[4] + "\"");
            }
        }
        if (seconds < 0) { 
			CommandSendMessageFailure(a_Player, a_Output, "The duration in seconds must be at least 0");
            return true;
        } else if (seconds > 1000000) {
			CommandSendMessageFailure(a_Player, a_Output, "The duration in seconds must be at most 1000000");
            return true;
        }

        if (a_Split.size() >= 5) { //amplifier
            try { 
                amplifier = std::stoi(a_Split[4]); 
            } catch (...) {
			    CommandSendMessageFailure(a_Player, a_Output, "Invalid amplification amount \"" + a_Split[4] + "\"");
                return true;
            }
        }
        if (amplifier < 0) { 
			CommandSendMessageFailure(a_Player, a_Output, "The amplification amount must be at least 0");
            return true;
        } else if (amplifier > 255) {
			CommandSendMessageFailure(a_Player, a_Output, "The amplification amount must be at most 255");
            return true;
        }
        auto ApplyEffect = [&](cPlayer& other_player) {
            other_player.AddEntityEffect(static_cast<cEntityEffect::eType>(effect_id), seconds * 20, amplifier);
            if (!all_players) 
			    CommandSendMessageSuccess(a_Player, a_Output, "Successfully added effect to player \"" + other_player.GetName() + "\" for " + std::to_string(seconds) + " seconds");
            return false;
        };

        if (all_players) {
            cRoot::Get()->ForEachPlayer(ApplyEffect);
			CommandSendMessageSuccess(a_Player, a_Output, "Successfully added effect to every player for " + std::to_string(seconds) + " seconds");
        } else if (!cRoot::Get()->FindAndDoWithPlayer(a_Split[1], ApplyEffect))
			CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
        
    } else {
        auto RemoveEffects = [&](cPlayer& other_player) {
            other_player.ClearEntityEffects();
            if (!all_players) 
			    CommandSendMessageSuccess(a_Player, a_Output, "Successfully removed effects from player \"" + other_player.GetName() + "\"");
            return false;
        };
        if (all_players) {
            cRoot::Get()->ForEachPlayer(RemoveEffects);
			CommandSendMessageSuccess(a_Player, a_Output, "Successfully removed effects from every player");
        } else if (!cRoot::Get()->FindAndDoWithPlayer(a_Split[1], RemoveEffects))
			CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
    }
    return true;
}

bool HandleKickCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    if (a_Split.size() < 2) {
		CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <player> [reason ...]");
    } else {
        auto KickPlayer = [&](cPlayer& other_player) ->bool { 
            AString reason;

            if (a_Split.size() >= 3) 
                reason = StringJoin(AStringVector(a_Split.begin() + 2, a_Split.end()), " ");

            other_player.GetClientHandle()->Kick(!reason.empty() ? reason : "You have been kicked");
            CommandSendMessageSuccess(a_Player, a_Output, "Successfully kicked player \"" + other_player.GetName() + "\"");
            return true;
        };

        if (a_Split[2].empty() || !cRoot::Get()->FindAndDoWithPlayer(a_Split[1], KickPlayer))
			CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
    }
    return true;
}

bool HandleListCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    AStringVector player_table;
    cRoot::Get()->ForEachPlayer([&](cPlayer& other_player) { player_table.push_back(other_player.GetName()); return true; });
    std::sort(player_table.begin(), player_table.end());
	CommandSendMessage(a_Player, a_Output,  "Players (" + std::to_string(player_table.size()) + "): " + StringJoin(player_table, ", ") );
    return true;
}

bool HandleRankCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    
    if (a_Split.size() < 2) {
        CommandSendMessage(a_Player, a_Output,  "Usage: " + a_Split[0] + " <player> [rank]");
        return true;
    }
    
    cUUID player_uuid = [](const AString& player_name) {
        if (cRoot::Get()->GetServer()->ShouldAuthenticate()) {
		    // The server is in online-mode, get the UUID from Mojang servers and check for validity:
		    return cRoot::Get()->GetMojangAPI().GetUUIDFromPlayerName(player_name);
	    }
	    // The server is in offline mode, generate an offline-mode UUID, no validity check is possible:
	    return cClientHandle::GenerateOfflineUUID(player_name);
    }(a_Split[1]);

    if (player_uuid.IsNil() || player_uuid.ToShortString().size() != 32) {
        CommandSendMessage(a_Player, a_Output,  "There is no player with the name \"" + a_Split[1] + "\"");
        return true;
    }

    // View the player's rank, if requested:
    if (a_Split.size() < 3) {
        const auto current_rank = cRoot::Get()->GetRankManager()->GetPlayerRankName(player_uuid);
        if (current_rank.empty())
            CommandSendMessage(a_Player, a_Output,  "Player \"" + a_Split[1] + "\" has no rank assigned to them.");
        else
            CommandSendMessage(a_Player, a_Output,  "The rank of player \"" + a_Split[1] + "\" is " + current_rank);
    } else {
	    // Change the player's rank
        if (!cRoot::Get()->GetRankManager()->RankExists(a_Split[2])) 
            CommandSendMessage(a_Player, a_Output,  "The specified rank does not exist!");
        else {
            cRoot::Get()->GetRankManager()->SetPlayerRank(player_uuid, a_Split[1], a_Split[2]);
            
            // Let the player know
            cRoot::Get()->FindAndDoWithPlayer(a_Split[1], [&](cPlayer& other_player) {
                if (a_Split[1] == other_player.GetName()) {
                    other_player.SendMessageInfo("You were assigned the rank " + a_Split[2] + " by " + ((a_Player) ? ("player \"" + a_Player->GetName() + "\"") : "the server console"));
                    other_player.LoadRank();
                }
                return true;
            });

			CommandSendMessageSuccess(a_Player, a_Output, "Player \"" + a_Split[1] + "\" is now in rank " + cRoot::Get()->GetRankManager()->GetPlayerRankName(player_uuid));
        }
    }
    return true;
}

bool HandleListRanksCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    AStringVector ranks = cRoot::Get()->GetRankManager()->GetAllRanks();
	CommandSendMessage(a_Player, a_Output, "Available ranks: " + StringJoin(ranks, (AString)cChatColor::Plain + ", ") + " (total: " + std::to_string(ranks.size()) + ")");
    return true;
}

bool HandleListGroupsCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    
    if (a_Split.size() > 2) {
        CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[1] + " [rank]");
        return true;
    }

    if (a_Split.size() < 2) {
        AStringVector groups = cRoot::Get()->GetRankManager()->GetAllGroups();
        CommandSendMessage(a_Player, a_Output, "Available groups: " + StringJoin(groups, (AString)cChatColor::Plain + ", ") + " (total: " + std::to_string(groups.size()) + ")");
    } else {
        AStringVector groups = cRoot::Get()->GetRankManager()->GetRankGroups(a_Split[1]);
        CommandSendMessage(a_Player, a_Output, "Groups in rank " + a_Split[1] + ": " + StringJoin(groups, (AString)cChatColor::Plain + ", ") + " (total: " + std::to_string(groups.size()) + ")");
    }
    return true;
}


bool HandleMeCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    if (a_Split.size() < 2 || !a_Player) 
        CommandSendMessage(a_Player, a_Output, AString((a_Player) ? "" : "(only available to players) ") + "Usage: " + a_Split[0] + " <action ...>");
    else 
        cRoot::Get()->BroadcastChat("* " + a_Player->GetName() + " " + StringJoin(AStringVector(a_Split.begin() + 1, a_Split.end()), " "));
    return true; 
}

bool HandleNumChunksCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    // List each world's chunk count into a table, sum the total chunk count:
    std::size_t total = 0;
    AStringVector response;
    response.reserve(16);

    cRoot::Get()->ForEachWorld([&](cWorld& world) {
        std::size_t numchunks = world.GetNumChunks();
        response.push_back(world.GetName() + ": " + std::to_string(numchunks) + " chunks");
        total += numchunks;
        return false;
    });
    std::sort(response.begin(), response.end());
    response.insert(response.begin(), "Number of loaded chunks:");

    response.push_back("Total: " + std::to_string(total) + " chunks");
    
    // Return the complete report:
    CommandSendMessage(a_Player, a_Output, StringJoin(response, "\n"));
    return true;
}


bool HandleOpCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
        
    if (a_Split.size() < 2) {
        CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <player>");
        return true;
    }

    AString admin_rank_name = [&]() {
        const AStringVector ranks = cRoot::Get()->GetRankManager()->GetAllRanks();
        for (const AString& r : ranks) {
            const auto permissions = cRoot::Get()->GetRankManager()->GetRankPermissions(r);
            for (const AString& p : permissions)
                if (p == "*")
                    return r;
        }
        return AString();
    }();

    if (!admin_rank_name.empty()) 
        return HandleRankCommand({"rank", a_Split[1], admin_rank_name}, a_Player, a_Command, a_Output);
        
    CommandSendMessage(a_Player, a_Output, "No admin rank found, missing * permission");
    return true;
}

bool HandlePlayersCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    AStringVector response;
    response.reserve(16);

    response.push_back("Players online:");
    cRoot::Get()->ForEachWorld([&](cWorld& world) {
        response.push_back("World " + world.GetName() + ":");
        world.ForEachPlayer([&](cPlayer& other_player){
            response.push_back("  " + other_player.GetName() + " @ " + other_player.GetIP());
            return false;
        });
        return false;
    });

    CommandSendMessage(a_Player, a_Output, StringJoin(response, "\n"));
    return true;
}

bool HandlePortalCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    if (a_Split.size() > 2 || !a_Player)  
        CommandSendMessage(a_Player, a_Output, AString((a_Player) ? "" : "(only available to players) ") + "Usage: " + a_Split[0] + " [world]");
    else if (a_Split.size() < 2)
        CommandSendMessage(a_Player, a_Output, "You are in world \"" + a_Player->GetWorld()->GetName() + "\"");
    else if (a_Player->GetWorld()->GetName() == a_Split[1])
        CommandSendMessageFailure(a_Player, a_Output, "You are already in world \"" + a_Split[1] + "\"!");
    else if (!cRoot::Get()->GetWorld(a_Split[1]))
        CommandSendMessageFailure(a_Player, a_Output, "Could not find world \"" + a_Split[1] + "\"!");
    else if (!a_Player->MoveToWorld(a_Split[1])) 
        CommandSendMessageFailure(a_Player, a_Output, "Could not move to world \"" + a_Split[1] + "\"!");
    else
        CommandSendMessageSuccess(a_Player, a_Output, "Successfully moved to world \"" + a_Split[1] + "\"! :D");
    return true;
}

bool HandleSaveAllCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    cRoot::Get()->SaveAllChunks();
    if (!a_Player)
        CommandSendMessage(nullptr, a_Output, "Saving all worlds!");
    else
        cRoot::Get()->BroadcastChat((AString)cChatColor::Rose + "[WARNING] " + cChatColor::White + "Saving all worlds!");
    return true;
}

bool HandleSayCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    if (a_Split.size() < 2) 
        CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <message ...>");
    else if (!a_Player)
        cRoot::Get()->BroadcastChat(AString(cChatColor::Gold) + "[SERVER] " + cChatColor::Yellow + StringJoin(AStringVector(a_Split.begin() + 1, a_Split.end()), " "));
    else
        cRoot::Get()->BroadcastChat("[" + a_Player->GetName() + "] " + StringJoin(AStringVector(a_Split.begin() + 1, a_Split.end()), " "));
    return true;
}

bool HandleSeedCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    cWorld* world = CommandGetWorld((a_Split.size() > 1) ? a_Split[1] : AString(), a_Player);
    if (!world)
        CommandSendMessage(a_Player, a_Output, "There is no world \"" + a_Split[1] + "\"");
    else
        CommandSendMessage(a_Player, a_Output, "Seed of world \"" + world->GetName() + "\": " + std::to_string(world->GetSeed()));
    return true;
}

bool HandleSetSpawnCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    
    if (!a_Player) {
        CommandSendMessage(nullptr, a_Output, "Command is for players only!");
        return true;
    }

    int player_x = (int)a_Player->GetPosX(), player_y = (int)a_Player->GetPosY(), player_z = (int)a_Player->GetPosZ();
    if (cWorld* world = a_Player->GetWorld(); world->SetSpawn(player_x, player_y, player_z)) {
        CommandSendMessageSuccess(a_Player, a_Output, fmt::format(FMT_STRING("Successfully changed spawn position to [X:{} Y:{} Z:{}]"), player_x, player_y, player_z));
    } else
        CommandSendMessageFailure(a_Player, a_Output, "Failed to change spawn position");
    return true;
}

bool HandleSpawnCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {

    auto MoveToSpawn = [&](cPlayer& other_player) {

        cWorld* world = other_player.GetWorld();
        if (!world) {
            CommandSendMessageFailure(a_Player, a_Output, "World for player \"" + a_Split[1] + "\" not found");
            return false;
        }

        auto spawn_x = world->GetSpawnX(), spawn_y = world->GetSpawnY(), spawn_z = world->GetSpawnZ();
        other_player.TeleportToCoords(spawn_x, spawn_y, spawn_z);

        CommandSendMessageSuccess(a_Player, a_Output, (a_Split.size() < 2) ? "Successfully moved to world spawn" 
                : ("Successfully returned player \"" + other_player.GetName() + "\" to world spawn"));
        return true;
    };

    if (a_Split.size() < 2) {
        if (!a_Player) {
            CommandSendMessage(nullptr, a_Output, "Usage: " + a_Split[0] + " <player>");
        } else {
            MoveToSpawn(*a_Player);
        }
    } else if (!a_Player || a_Player->HasPermission("core.spawn.others")) {
        if (a_Split[1] == "" || !cRoot::Get()->FindAndDoWithPlayer(a_Split[1], MoveToSpawn)) {
            CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
        }
    }
    return true;
}

bool HandleSpawnPointCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    
    int x, y, z;
    auto world = cRoot::Get()->GetDefaultWorld();

    if (a_Player) {
        x = a_Player->GetPosX(), y = a_Player->GetPosY(), z = a_Player->GetPosZ();
        world = a_Player->GetWorld();
    }
    
    AString response;
    auto SetSpawnPoint = [&](cPlayer& other_player) {
        if (a_Split.size() > 4) {
            if (!a_Player) 
                x = other_player.GetPosX(), y = other_player.GetPosY(), z = other_player.GetPosZ();

            try {
                x = RelativeCommandCoord(a_Split[2], x);
            } catch(...) {
                response = "'" + a_Split[2] + "' is not a valid number";
            }

            try {
                y = RelativeCommandCoord(a_Split[3], y);
            } catch(...) {
                response = "'" + a_Split[3] + "' is not a valid number";
            }

            try {
                z = RelativeCommandCoord(a_Split[4], z);
            } catch(...) {
                response = "'" + a_Split[4] + "' is not a valid number";
            }
        }
        
        if (a_Split.size() > 5) {
            world = cRoot::Get()->GetWorld(a_Split[5]);
            if (!world) 
                response = "Invalid world \"" + a_Split[5] + "\"";
        }

        if (response.empty()) {
            assert(world); // unsure if needed...
            other_player.SetBedPos(Vector3i(x, y, z), *world);
            CommandSendMessageSuccess(a_Player, a_Output, "Set the spawn point of player \"" + other_player.GetName() + "\" to (" + std::to_string(std::floor(x)) + ", " + std::to_string(std::floor(y)) + ", " + std::to_string(std::floor(z)) + ") in world \"" + world->GetName() + "\"");
        } else 
            CommandSendMessageFailure(a_Player, a_Output, response);

        return true;
    };

    if ((a_Split.size() > 2 || !a_Player) && a_Split.size() < 5)
        CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <player> <x> <y> <z> [world]");
    else if (a_Split.size() < 2)
        SetSpawnPoint(*a_Player);
    else if (a_Split[1] == "" || !cRoot::Get()->FindAndDoWithPlayer(a_Split[1], SetSpawnPoint))
        CommandSendMessageFailure(a_Player, a_Output, "Player \"" + a_Split[1] + "\" not found");
    return true;
}

bool HandleStopCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    cRoot::Get()->BroadcastChat((AString)cChatColor::Red + "[WARNING] " + cChatColor::White + "Server is terminating!");
	cRoot::Get()->QueueExecuteConsoleCommand("stop");
    return true;
}

bool HandleSummonCommand(const AStringVector& a_Split, cPlayer* a_Player, const AString& a_Command, cCommandOutputCallback* a_Output) {
    static const std::map<AString, ENUM_ITEM_TYPE> minecarts = {
        {"minecart", E_ITEM_MINECART},
        {"chest_minecart", E_ITEM_CHEST_MINECART},
        {"furnace_minecart", E_ITEM_FURNACE_MINECART},
        {"hopper_minecart", E_ITEM_MINECART_WITH_HOPPER},
        {"tnt_minecart", E_ITEM_MINECART_WITH_TNT},

        // 1.10 and below
        {"MinecartChest", E_ITEM_CHEST_MINECART},
        {"MinecartFurnace", E_ITEM_FURNACE_MINECART},
        {"MinecartHopper", E_ITEM_MINECART_WITH_HOPPER},
        {"MinecartRideable", E_ITEM_MINECART},
        {"MinecartTNT", E_ITEM_MINECART_WITH_TNT}
    };

    static const std::map<AString, eMonsterType> mobs = {
        {"bat", mtBat},
        {"blaze", mtBlaze},
        {"cave_spider", mtCaveSpider},
        {"chicken", mtChicken},
        {"cow", mtCow},
        {"creeper", mtCreeper},
        {"ender_dragon", mtEnderDragon},
        {"enderman", mtEnderman},
        {"ghast", mtGhast},
        {"giant", mtGiant},
        {"guardian", mtGuardian},
        {"horse", mtHorse},
        {"iron_golem", mtIronGolem},
        {"magma_cube", mtMagmaCube},
        {"mooshroom", mtMooshroom},
        {"ocelot", mtOcelot},
        {"pig", mtPig},
        {"rabbit", mtRabbit},
        {"sheep", mtSheep},
        {"silverfish", mtSilverfish},
        {"skeleton", mtSkeleton},
        {"slime", mtSlime},
        {"snowman", mtSnowGolem},
        {"spider", mtSpider},
        {"squid", mtSquid},
        {"villager", mtVillager},
        {"witch", mtWitch},
        {"wither", mtWither},
        {"wither_skeleton", mtWitherSkeleton},
        {"wolf", mtWolf},
        {"zombie", mtZombie},
        {"zombie_pigman", mtZombiePigman},
        {"zombie_villager", mtZombieVillager},

        // 1.10 and below
        {"Bat", mtBat},
        {"Blaze", mtBlaze},
        {"CaveSpider", mtCaveSpider},
        {"Chicken", mtChicken},
        {"Cow", mtCow},
        {"Creeper", mtCreeper},
        {"EnderDragon", mtEnderDragon},
        {"Enderman", mtEnderman},
        {"Ghast", mtGhast},
        {"Giant", mtGiant},
        {"Guardian", mtGuardian},
        {"Horse", mtHorse},
        {"LavaSlime", mtMagmaCube},
        {"MushroomCow", mtMooshroom},
        {"Ozelot", mtOcelot},
        {"Pig", mtPig},
        {"Rabbit", mtRabbit},
        {"Sheep", mtSheep},
        {"Silverfish", mtSilverfish},
        {"Skeleton", mtSkeleton},
        {"Slime", mtSlime},
        {"SnowMan", mtSnowGolem},
        {"Spider", mtSpider},
        {"Squid", mtSquid},
        {"Villager", mtVillager},
        {"VillagerGolem", mtIronGolem},
        {"Witch", mtWitch},
        {"Wither", mtWither},
        {"WitherSkeleton", mtWitherSkeleton},
        {"Wolf", mtWolf},
        {"Zombie", mtZombie},
        {"PigZombie", mtZombiePigman},
        {"ZombieVillager", mtZombieVillager}
    };

    static const std::map<AString, cProjectileEntity::eKind> projectiles = {
        {"arrow", cProjectileEntity::eKind::pkArrow},
        {"egg", cProjectileEntity::eKind::pkEgg},
        {"ender_pearl", cProjectileEntity::eKind::pkEnderPearl},
        {"fireworks_rocket", cProjectileEntity::eKind::pkFirework},
        {"fireball", cProjectileEntity::eKind::pkGhastFireball},
        {"potion", cProjectileEntity::eKind::pkSplashPotion},
        {"small_fireball", cProjectileEntity::eKind::pkFireCharge},
        {"snowball", cProjectileEntity::eKind::pkSnowball},
        {"wither_skull", cProjectileEntity::eKind::pkWitherSkull},
        {"xp_bottle", cProjectileEntity::eKind::pkExpBottle},

        // 1.10 and below
        {"Arrow", cProjectileEntity::eKind::pkArrow},
        {"Fireball", cProjectileEntity::eKind::pkGhastFireball},
        {"FireworksRocketEntity", cProjectileEntity::eKind::pkFirework},
        {"SmallFireball", cProjectileEntity::eKind::pkFireCharge},
        {"Snowball", cProjectileEntity::eKind::pkSnowball},
        {"ThrownEgg", cProjectileEntity::eKind::pkEgg},
        {"ThrownEnderpearl", cProjectileEntity::eKind::pkEnderPearl},
        {"ThrownExpBottle", cProjectileEntity::eKind::pkExpBottle},
        {"ThrownPotion", cProjectileEntity::eKind::pkSplashPotion},
        {"WitherSkull", cProjectileEntity::eKind::pkWitherSkull}
    };

    auto SpawnEntity = [&](const AString& entity_name, cWorld& world, double x, double y, double z) {
        if (entity_name == "boat" || entity_name == "Boat")
            world.SpawnBoat(Vector3d(x,y,z), cBoat::bmOak);
        else if (entity_name == "falling_block" || entity_name == "FallingSand")
            world.SpawnFallingBlock(Vector3i(x, y, z), E_BLOCK_SAND, 0); 
        else  if (entity_name == "lightning_bolt" || entity_name == "LightningBolt") 
            world.CastThunderbolt(Vector3i(x,y,z));
        else if (auto find = minecarts.find(entity_name); find != minecarts.end())
            world.SpawnMinecart(Vector3d(x,y,z), find->second);
        else if (auto find = mobs.find(entity_name); find != mobs.end())
            world.SpawnMob(x, y, z, find->second);
        else if (auto find = projectiles.find(entity_name); find != projectiles.end()) {
            auto direction = a_Player->GetLookVector() * 20;
            world.CreateProjectile(Vector3d(x,y,z), find->second, a_Player, &a_Player->GetEquippedItem(), &direction);
        } else if (entity_name == "tnt" || entity_name == "PrimedTnt")
            world.SpawnPrimedTNT(Vector3d(x,y,z));
        else if (entity_name == "xp_orb" || entity_name == "XPOrb")
            world.SpawnExperienceOrb(Vector3d(x,y,z), 1);
        else if (entity_name == "ender_crystal" || entity_name == "EnderCrystal") 
            world.SpawnEnderCrystal(Vector3d(x,y,z), false);
        else
            return false;
        return true;
    };

    if (a_Split.size() < 2 || (!a_Player && a_Split.size() < 5)) {
        CommandSendMessage(a_Player, a_Output, "Usage: " + a_Split[0] + " <entityname> [x] [y] [z]");
        return true;
    }

    double x, y, z;
    cWorld* world = cRoot::Get()->GetDefaultWorld();
    if (a_Player) {
        x = a_Player->GetPosX(), y = a_Player->GetPosY(), z = a_Player->GetPosZ();
        world = a_Player->GetWorld();
    }

    if (a_Split.size() > 4) {
        try {
            x = RelativeCommandCoord(a_Split[2], x);
        } catch(...) {
            CommandSendMessageFailure(a_Player, a_Output, "'" + a_Split[2] + "' is not a valid number");
            return true;
        }

        try {
            y = RelativeCommandCoord(a_Split[3], y);
        } catch(...) {
            CommandSendMessageFailure(a_Player, a_Output, "'" + a_Split[3] + "' is not a valid number");
            return true;
        }

        try {
            z = RelativeCommandCoord(a_Split[4], z);
        } catch(...) {
            CommandSendMessageFailure(a_Player, a_Output, "'" + a_Split[4] + "' is not a valid number");
            return true;
        }
    }
    
    assert(world); // idk, it seemed sus so this for saftey. works tho and clears with debug lol
    if (SpawnEntity(a_Split[1], *world, x, y, z))
        CommandSendMessageSuccess(a_Player, a_Output, "Successfully summoned entity at [X:" + std::to_string(std::floor(x)) + " Y:" + std::to_string(std::floor(y)) + " Z:" + std::to_string(std::floor(z)) + "]");
    else
        CommandSendMessageFailure(a_Player, a_Output, "Unknown entity '" + a_Split[1] + "'");
    return true;
}
