package stream.flarebot.flarebot.util.general;

import com.arsenarsen.lavaplayerbridge.PlayerManager;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Emote;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.Member;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.entities.User;
import org.apache.commons.lang3.StringUtils;
import stream.flarebot.flarebot.FlareBot;
import stream.flarebot.flarebot.FlareBotManager;
import stream.flarebot.flarebot.Getters;
import stream.flarebot.flarebot.objects.GuildWrapper;
import stream.flarebot.flarebot.util.Constants;
import stream.flarebot.flarebot.util.MessageUtils;

import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class GuildUtils {

    private static final Pattern userDiscrim = Pattern.compile(".+#[0-9]{4}");
    private static final int LEVENSHTEIN_DISTANCE = 8;

    /**
     * Gets the prefix for the specified {@link Guild}
     *
     * @param guild The {@link Guild} to get a prefix from
     * @return A char that is the guilds prefix
     */
    public static char getPrefix(Guild guild) {
        return guild == null ? Constants.COMMAND_CHAR : FlareBotManager.instance().getGuild(guild.getId()).getPrefix();
    }

    /**
     * Gets the prefix from the {@link GuildWrapper}
     *
     * @param guild The {@link GuildWrapper} that represents the guild that you want to get the prefix from
     * @return A char that is the guilds prefix
     */
    public static char getPrefix(GuildWrapper guild) {
        return guild == null ? Constants.COMMAND_CHAR : guild.getPrefix();
    }

    /**
     * Gets the number of users that the {@link Guild} has. Not including bots.
     *
     * @param guild The {@link Guild} to get the user count from
     * @return An int of the number of users
     */
    public static int getGuildUserCount(Guild guild) {
        int i = 0;
        for (Member member : guild.getMembers()) {
            if (!member.getUser().isBot()) {
                i++;
            }
        }
        return i;
    }

    /**
     * Gets a list of {@link Role} that match a string. Case doesn't matter.
     *
     * @param string The String to get a list of {@link Role} from.
     * @param guild  The {@link Guild} to get the roles from.
     * @return an empty if no role matches, otherwise a list of roles matching the string.
     */
    public static List<Role> getRole(String string, Guild guild) {
        return guild.getRolesByName(string, true);
    }

    /**
     * Gets a {@link Role} from a string. Case Doesn't matter.
     *
     * @param s       The String to get a role from
     * @param guildId The id of the {@link Guild} to get the role from
     * @return null if the role doesn't, otherwise a list of roles matching the string
     */
    public static Role getRole(String s, String guildId) {
        return getRole(s, guildId, null);
    }

    /**
     * Gets a {@link Role} that matches a string. Case doesn't matter.
     *
     * @param s       The String to get a role from
     * @param guildId The id of the {@link Guild} to get the role from
     * @param channel The channel to send an error message to if anything goes wrong.
     * @return null if the role doesn't, otherwise a list of roles matching the string
     */
    public static Role getRole(String s, String guildId, TextChannel channel) {
        Guild guild = Getters.getGuildById(guildId);
        Role role = guild.getRoles().stream()
                .filter(r -> r.getName().equalsIgnoreCase(s))
                .findFirst().orElse(null);
        if (role != null) return role;
        try {
            role = guild.getRoleById(Long.parseLong(s.replaceAll("[^0-9]", "")));
            if (role != null) return role;
        } catch (NumberFormatException | NullPointerException ignored) {
        }
        if (channel != null) {
            if (guild.getRolesByName(s, true).isEmpty()) {
                String closest = null;
                int distance = LEVENSHTEIN_DISTANCE;
                for (Role role1 : guild.getRoles().stream().filter(role1 -> FlareBotManager.instance().getGuild(guildId).getSelfAssignRoles()
                        .contains(role1.getId())).collect(Collectors.toList())) {
                    int currentDistance = StringUtils.getLevenshteinDistance(role1.getName(), s);
                    if (currentDistance < distance) {
                        distance = currentDistance;
                        closest = role1.getName();
                    }
                }
                MessageUtils.sendErrorMessage("That role does not exist! "
                        + (closest != null ? "Maybe you mean `" + closest + "`" : ""), channel);
                return null;
            } else {
                return guild.getRolesByName(s, true).get(0);
            }
        }
        return null;
    }

    /**
     * Gets a {@link User} from a string. Not case sensitive.
     * The string can eater be their name, their id, or them being mentioned.
     *
     * @param s the string to get the user from
     * @return null if the user wasn't found otherwise a {@link User}
     */
    public static User getUser(String s) {
        return getUser(s, null);
    }

    /**
     * Gets a {@link User} from a string. Not case sensitive.
     * The string can eater be their name, their id, or them being mentioned.
     *
     * @param s       The string to get the user from.
     * @param guildId The string id of the {@link Guild} to get the user from.
     * @return null if the user wasn't found otherwise a {@link User}.
     */
    public static User getUser(String s, String guildId) {
        return getUser(s, guildId, false);
    }

    /**
     * Gets a {@link User} from a string. Not case sensitive.
     * The string can eater be their name, their id, or them being mentioned.
     *
     * @param s        The string to get the user from
     * @param forceGet If you want to get the user from Discord instead of from a guild
     * @return null if the user wasn't found otherwise a {@link User}
     * @throws
     */
    public static User getUser(String s, boolean forceGet) {
        return getUser(s, null, forceGet);
    }

    /**
     * Gets a {@link User} from a string. Not case sensitive.
     * The string can eater be their name, their id, or them being mentioned.
     *
     * @param s        The string to get the user from.
     * @param guildId  The id of the {@link Guild} to get the user from.
     * @param forceGet If you want to get the user from discord instead of from a guild.
     * @return null if the user wasn't found otherwise a {@link User}.
     */
    public static User getUser(String s, String guildId, boolean forceGet) {
        Guild guild = guildId == null || guildId.isEmpty() ? null : Getters.getGuildById(guildId);
        if (userDiscrim.matcher(s).find()) {
            if (guild == null) {
                return Getters.getUserCache().stream()
                        .filter(user -> (user.getName() + "#" + user.getDiscriminator()).equalsIgnoreCase(s))
                        .findFirst().orElse(null);
            } else {
                try {
                    return guild.getMembers().stream()
                            .map(Member::getUser)
                            .filter(user -> (user.getName() + "#" + user.getDiscriminator()).equalsIgnoreCase(s))
                            .findFirst().orElse(null);
                } catch (NullPointerException ignored) {
                }
            }
        } else {
            User tmp;
            if (guild == null) {
                tmp = Getters.getUserCache().stream().filter(user -> user.getName().equalsIgnoreCase(s))
                        .findFirst().orElse(null);
            } else {
                tmp = guild.getMembers().stream()
                        .map(Member::getUser)
                        .filter(user -> user.getName().equalsIgnoreCase(s))
                        .findFirst().orElse(null);
            }
            if (tmp != null) return tmp;
            try {
                long l = Long.parseLong(s.replaceAll("[^0-9]", ""));
                if (guild == null) {
                    tmp = Getters.getUserById(l);
                } else {
                    Member temMember = guild.getMemberById(l);
                    if (temMember != null) {
                        tmp = temMember.getUser();
                    }
                }
                if (tmp != null) {
                    return tmp;
                } else if (forceGet) {
                    return Getters.retrieveUserById(l);
                }
            } catch (NumberFormatException | NullPointerException ignored) {
            }
        }
        return null;
    }

    /**
     * Gets a {@link TextChannel} from a string. Not case sensitive.
     * The string can eater be the channel name, it's id, or it being mentioned.
     *
     * @param arg The string to get the channel from.
     * @return null if the channel couldn't be found otherwise a {@link TextChannel}.
     */
    public static TextChannel getChannel(String arg) {
        return getChannel(arg, null);
    }

    /**
     * Gets a {@link TextChannel} from a string. Not case sensitive.
     * The string can eater be the channel name, it's id, or it being mentioned.
     *
     * @param channelArg The string to get the channel from
     * @param wrapper    The Guild wrapper for the {@link Guild} that you want to get the channel from
     * @return null if the channel couldn't be found otherwise a {@link TextChannel}
     */
    public static TextChannel getChannel(String channelArg, GuildWrapper wrapper) {
        try {
            long channelId = Long.parseLong(channelArg.replaceAll("[^0-9]", ""));
            return wrapper != null ? wrapper.getGuild().getTextChannelById(channelId) : Getters.getChannelById(channelId);
        } catch (NumberFormatException e) {
            if (wrapper != null) {
                List<TextChannel> tcs = wrapper.getGuild().getTextChannelsByName(channelArg, true);
                if (!tcs.isEmpty()) {
                    return tcs.get(0);
                }
            }
            return null;
        }
    }

    /**
     * Gets an {@link Emote} from an id.
     *
     * @param l the id as a long of the emote
     * @return null if the id is invalid or wasn't found, otherwise a {@link Emote}
     */
    public static Emote getEmoteById(long l) {
        return Getters.getGuildCache().stream().map(g -> g.getEmoteById(l))
                .filter(Objects::nonNull).findFirst().orElse(null);
    }

    /**
     * Gets weather or not the bot can change nick.
     * This checks for {@link Permission#NICKNAME_CHANGE}.
     * If we don't have the then it checks for {@link Permission#NICKNAME_MANAGE}.
     *
     * @param guildId The string guildid to check if we can change nick
     * @return If we change change nick
     */
    public static boolean canChangeNick(String guildId) {
        Guild guild = Getters.getGuildById(guildId);
        return guild != null &&
                (guild.getSelfMember().hasPermission(Permission.NICKNAME_CHANGE) ||
                        guild.getSelfMember().hasPermission(Permission.NICKNAME_MANAGE));
    }

    /**
     * Joins the bot to a {@link TextChannel}.
     *
     * @param channel The chanel to send an error message to in case this fails.
     * @param member  The member requesting the join. This is also how we determine what channel to join.
     */
    public static void joinChannel(TextChannel channel, Member member) {
        if (channel.getGuild().getSelfMember()
                .hasPermission(member.getVoiceState().getChannel(), Permission.VOICE_CONNECT) &&
                channel.getGuild().getSelfMember()
                        .hasPermission(member.getVoiceState().getChannel(), Permission.VOICE_SPEAK)) {
            if (member.getVoiceState().getChannel().getUserLimit() > 0 && member.getVoiceState().getChannel()
                    .getMembers().size()
                    >= member.getVoiceState().getChannel().getUserLimit() && !member.getGuild().getSelfMember()
                    .hasPermission(member.getVoiceState().getChannel(), Permission.MANAGE_CHANNEL)) {
                MessageUtils.sendErrorMessage("We can't join :(\n\nThe channel user limit has been reached and we don't have the 'Manage Channel' permission to " +
                        "bypass it!", channel);
                return;
            }
            PlayerManager musicManager = FlareBot.instance().getMusicManager();
            channel.getGuild().getAudioManager().openAudioConnection(member.getVoiceState().getChannel());
            musicManager.getPlayer(channel.getGuild().getId()).play();

            if (musicManager.getPlayer(channel.getGuild().getId()).getPaused()) {
                MessageUtils.sendWarningMessage("The music is currently paused do `{%}resume`", channel);
            }
        } else {
            MessageUtils.sendErrorMessage("I do not have permission to " + (!channel.getGuild().getSelfMember()
                    .hasPermission(member.getVoiceState()
                            .getChannel(), Permission.VOICE_CONNECT) ?
                    "connect" : "speak") + " in your voice channel!", channel);
        }
    }
}