from discord.ext import commands
import discord

from . import utils

import re
import asyncio


class Roles:
    """Class to handle management of roles on the server"""

    def __init__(self, bot):
        self.bot = bot

    @commands.group(aliases=['roles'], invoke_without_command=True)
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def role(self, ctx):
        """This command can be used to modify the roles on the server.
        Pass no subcommands and this will print the roles currently available on this server

        EXAMPLE: !role
        RESULT: A list of all your roles"""
        # Simply get a list of all roles in this server and send them
        entries = [r.name for r in ctx.guild.role_hierarchy[:-1]]
        if len(entries) == 0:
            await ctx.send("You do not have any roles setup on this server, other than the default role!")
            return

        try:
            pages = utils.Pages(self.bot, message=ctx.message, entries=entries)
            await pages.paginate()
        except utils.CannotPaginate as e:
            await ctx.send(str(e))

    @role.command(name='remove')
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def remove_role(self, ctx):
        """Use this to remove roles from a number of members

        EXAMPLE: !role remove @Jim @Bot @Joe
        RESULT: A follow-along to remove the role(s) you want to, from these 3 members"""
        # No use in running through everything if the bot cannot manage roles
        if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
            await ctx.send("I can't manage roles in this server, do you not trust  me? :c")
            return
        check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel

        server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
        # First get the list of all mentioned users
        members = ctx.message.mentions
        # If no users are mentioned, ask the author for a list of the members they want to remove the role from
        if len(members) == 0:
            await ctx.send("Please provide the list of members you want to remove a role from")
            try:
                msg = await self.bot.wait_for('message', check=check, timeout=60)
            except asyncio.TimeoutError:
                await ctx.send("You took too long. I'm impatient, don't make me wait")
                return
            if len(msg.mentions) == 0:
                await ctx.send("I cannot remove a role from someone if you don't provide someone...")
                return
            # Override members if everything has gone alright, and then continue
            members = msg.mentions

        # This allows the user to remove multiple roles from the list of users, if they want.
        await ctx.send("Alright, please provide the roles you would like to remove from this member. "
                       "Make sure the roles, if more than one is provided, are separate by commas. "
                       "Here is a list of this server's roles:"
                       "```\n{}```".format("\n".join([r.name for r in server_roles])))
        try:
            msg = await self.bot.wait_for('message', check=check, timeout=60)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return

        # Split the content based on commas, using regex so we can split if a space was not provided or if it was
        role_names = re.split(', ?', msg.content)
        roles = []
        # This loop is just to get the actual role objects based on the name
        for role in role_names:
            _role = discord.utils.get(server_roles, name=role)
            if _role is not None:
                roles.append(_role)

        # If no valid roles were given, let them know that and return
        if len(roles) == 0:
            await ctx.send("Please provide a valid role next time!")
            return

        # Otherwise, remove the roles from each member given
        for member in members:
            await member.remove_roles(*roles)
        await ctx.send("I have just removed the following roles:```\n{}``` from the following members:"
                       "```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))

    @role.command(name='add', aliases=['give', 'assign'])
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def add_role(self, ctx):
        """Use this to add a role to multiple members.
        Provide the list of members, and I'll ask for the role
        If no members are provided, I'll first ask for them

        EXAMPLE: !role add @Bob @Joe @jim
        RESULT: A follow along to add the roles you want to these 3"""
        # No use in running through everything if the bot cannot manage roles
        if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
            await ctx.send("I can't manage roles in this server, do you not trust  me? :c")
            return
        check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel

        # This is exactly the same as removing roles, except we call add_roles instead.
        server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
        members = ctx.message.mentions
        if len(members) == 0:
            await ctx.send("Please provide the list of members you want to add a role to")
            try:
                msg = await self.bot.wait_for('message', check=check, timeout=60)
            except asyncio.TimeoutError:
                await ctx.send("You took too long. I'm impatient, don't make me wait")
                return
            if len(msg.mentions) == 0:
                await ctx.send("I cannot add a role to someone if you don't provide someone...")
                return
            members = msg.mentions

        await ctx.send("Alright, please provide the roles you would like to add to this member. "
                       "Make sure the roles, if more than one is provided, are separate by commas. "
                       "Here is a list of this server's roles:"
                       "```\n{}```".format("\n".join([r.name for r in server_roles])))
        try:
            msg = await self.bot.wait_for('message', check=check, timeout=60)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return
        role_names = re.split(', ?', msg.content)
        roles = []
        for role in role_names:
            _role = discord.utils.get(server_roles, name=role)
            if _role is not None:
                roles.append(_role)

        if len(roles) == 0:
            await ctx.send("Please provide a valid role next time!")
            return

        for member in members:
            await member.add_roles(*roles)
        await ctx.send("I have just added the following roles:```\n{}``` to the following members:"
                       "```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))

    @role.command(name='delete')
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def delete_role(self, ctx, *, role: discord.Role = None):
        """This command can be used to delete one of the roles from the server

        EXAMPLE: !role delete StupidRole
        RESULT: No more role called StupidRole"""
        # No use in running through everything if the bot cannot manage roles
        if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
            await ctx.send("I can't delete roles in this server, do you not trust  me? :c")
            return

        # If no role was given, get the current roles on the server and ask which ones they'd like to remove
        if role is None:
            server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]

            await ctx.send(
                "Which role would you like to remove from the server? Here is a list of this server's roles:"
                "```\n{}```".format("\n".join([r.name for r in server_roles])))

            # For this method we're only going to delete one role at a time
            # This check attempts to find a role based on the content provided, if it can't find one it returns None
            # We can use that fact to simply use just that as our check
            def check(m):
                if m.author == ctx.message.author and m.channel == ctx.message.channel:
                    return discord.utils.get(server_roles, name=m.content) is not None
                else:
                    return False

            try:
                msg = await self.bot.wait_for('message', timeout=60, check=check)
            except asyncio.TimeoutError:
                await ctx.send("You took too long. I'm impatient, don't make me wait")
                return
            # If we have gotten here, based on our previous check, we know that the content provided is a valid role.
            # Due to that, no need for any error checking here
            role = discord.utils.get(server_roles, name=msg.content)

        await role.delete()
        await ctx.send("I have just removed the role {} from this server".format(role.name))

    @role.command(name='create')
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def create_role(self, ctx):
        """This command can be used to create a new role for this server
        A prompt will follow asking what settings you would like for this new role
        I'll then ask if you'd like to set anyone to use this role

        EXAMPLE: !role create
        RESULT: A follow along in order to create a new role"""
        # No use in running through everything if the bot cannot create the role
        if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
            await ctx.send("I can't create roles in this server, do you not trust  me? :c")
            return

        # Save a couple variables that will be used repeatedly
        author = ctx.message.author
        server = ctx.message.guild
        channel = ctx.message.channel

        # A couple checks that will be used in the wait_for_message's
        def num_seperated_check(m):
            if m.author == author and m.channel == channel:
                return re.search("(\d(, ?| )?|[nN]one)", m.content) is not None
            else:
                return False

        def yes_no_check(m):
            if m.author == author and m.channel == channel:
                return re.search("(yes|no)", m.content.lower()) is not None
            else:
                return False

        def members_check(m):
            if m.author == author and m.channel == channel:
                return len(m.mentions) > 0
            else:
                return False

        author_check = lambda m: m.author == author and m.channel == channel

        # Start the checks for the role, get the name of the role first
        await ctx.send(
            "Alright! I'm ready to create a new role, please respond with the name of the role you want to create")
        try:
            msg = await self.bot.wait_for('message', timeout=60.0, check=author_check)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return
        name = msg.content

        # Print a list of all the permissions available, then ask for which ones need to be active on this new role
        all_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
        fmt = "\n".join("{}) {}".format(i, perm) for i, perm in enumerate(all_perms))
        await ctx.send("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
                       "the numbers, seperated by commas, of the permissions you want this role to have.\n"
                       "```\n{}```".format(fmt))
        # For this we're going to give a couple extra minutes before we timeout
        # as it might take a bit to figure out which permissions they want
        try:
            msg = await self.bot.wait_for('message', timeout=180.0, check=num_seperated_check)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return

        # Check if any integer's were provided that are within the length of the list of permissions
        num_permissions = [int(i) for i in re.split(' ?,?', msg.content) if i.isdigit() and int(i) < len(all_perms)]

        # Check if this role should be in a separate section on the sidebard, i.e. hoisted
        await ctx.send("Do you want this role to be in a separate section on the sidebar? (yes or no)")
        try:
            msg = await self.bot.wait_for('message', timeout=60.0, check=yes_no_check)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return
        hoist = True if msg.content.lower() == "yes" else False

        # Check if this role should be able to be mentioned
        await ctx.send("Do you want this role to be mentionable? (yes or no)")
        try:
            msg = await self.bot.wait_for('message', timeout=60.0, check=yes_no_check)
        except asyncio.TimeoutError:
            await ctx.send("You took too long. I'm impatient, don't make me wait")
            return
        mentionable = True if msg.content.lower() == "yes" else False

        # Ready to actually create the role
        # First create a permissions object based on the numbers provided
        perms = discord.Permissions.none()
        for index in num_permissions:
            setattr(perms, all_perms[index], True)

        payload = {
            'name': name,
            'permissions': perms,
            'hoist': hoist,
            'mentionable': mentionable
        }
        # Create the role, and wait a second, sometimes it goes too quickly and we get a role with 'new role' to print
        role = await server.create_role(**payload)
        await asyncio.sleep(1)
        await ctx.send("We did it! You just created the new role {}\nIf you want to add this role"
                       " to some people, mention them now".format(role.name))
        try:
            msg = await self.bot.wait_for('message', timeout=60.0, check=members_check)
        except asyncio.TimeoutError:
            # There's no need to mention the users, so don't send a failure message if they didn't, just return
            return

        # Otherwise members were mentioned, add the new role to them now
        for member in msg.mentions:
            await member.add_roles(role)

        fmt = "\n".join(m.display_name for m in msg.mentions)
        await ctx.send("I have just added the role {} to: ```\n{}```".format(name, fmt))

    @commands.group(invoke_without_command=True)
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def assign(self, ctx, *role: discord.Role):
        """Assigns the provided role(s) to you, if they can be assigned

        EXAMPLE: !assign me Member
        RESULT: You now have the Member role"""
        if not ctx.message.guild.me.guild_permissions.manage_roles:
            await ctx.send("I need to have manage roles permissions to assign roles")
            return

        author = ctx.message.author
        key = str(ctx.message.guild.id)
        self_assignable_roles = self.bot.db.load('server_settings', key=key, pluck='self_assignable_roles') or []

        if len(self_assignable_roles) == 0:
            await ctx.send("There are no self-assignable roles on this server")
            return

        fmt = ""
        roles = [r for r in role if str(r.id) in self_assignable_roles]
        fmt += "\n".join(["Successfully added {}".format(r.name)
                          if str(r.id) in self_assignable_roles else
                          "{} is not available to be self-assigned".format(r.name)
                          for r in role])

        try:
            await author.add_roles(*roles)
            await ctx.send(fmt)
        except discord.HTTPException:
            await ctx.send("I cannot assign roles to you {}".format(author.mention))

    @commands.command()
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def unassign(self, ctx, *role: discord.Role):
        """Unassigns the provided role(s) to you, if they can be assigned

        EXAMPLE: !unassign Member
        RESULT: You now no longer have the Member role"""
        if not ctx.message.guild.me.guild_permissions.manage_roles:
            await ctx.send("I need to have manage roles permissions to assign roles")
            return

        author = ctx.message.author
        key = str(ctx.message.guild.id)
        self_assignable_roles = self.bot.db.load('server_settings', key=key, pluck='self_assignable_roles') or []

        if len(self_assignable_roles) == 0:
            await ctx.send("There are no self-assignable roles on this server")
            return

        fmt = ""
        roles = [r for r in role if str(r.id) in self_assignable_roles]
        fmt += "\n".join(["Successfully removed {}".format(r.name)
                          if str(r.id) in self_assignable_roles else
                          "{} is not available to be self-assigned".format(r.name)
                          for r in role])

        try:
            await author.remove_roles(*roles)
            await ctx.send(fmt)
        except discord.HTTPException:
            await ctx.send("I cannot remove roles from you {}".format(author.mention))

    @assign.command(name='add')
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def _add_assigns(self, ctx, *role: discord.Role):
        """Adds the provided role(s) to the list of available self-assignable roles

        EXAMPLE: !assigns Member NSFW
        RESULT: Allows users to self-assign the roles Member, and NSFW"""
        roles = [str(r.id) for r in role]
        key = str(ctx.message.guild.id)

        self_assignable_roles = self.bot.db.load('server_settings', key=key, pluck='self_assignable_roles') or []
        self_assignable_roles.extend(roles)
        self_assignable_roles = list(set(self_assignable_roles))
        entry = {
            'server_id': key,
            'self_assignable_roles': self_assignable_roles
        }

        self.bot.db.save('server_settings', entry)

        if len(roles) == 1:
            fmt = "Successfully added {} as a self-assignable role".format(role[0].name)
        else:
            fmt = "Succesfully added the following roles as self-assignable:\n{}".format(
                "\n".join(["**{}**".format(r.name) for r in role])
            )
        await ctx.send(fmt)

    @assign.command(name='list')
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def _list_assigns(self, ctx):
        """Lists the roles that can be self-assigned

        EXAMPLE: !assigns list
        RESUL: A list of all the self-assignable roles"""
        key = str(ctx.message.guild.id)
        self_assignable_roles = self.bot.db.load('server_settings', key=key, pluck='self_assignable_roles') or []
        if len(self_assignable_roles) == 0:
            await ctx.send("There are no self-assignable roles on this server")
            return

        roles = []
        for role_id in self_assignable_roles:
            role = discord.utils.get(ctx.message.guild.roles, id=int(role_id))
            if role:
                roles.append(role.name)

        if len(roles) == 0:
            await ctx.send("There are no self-assignable roles on this server")
            return

        try:
            pages = utils.Pages(self.bot, message=ctx.message, entries=roles)
            await pages.paginate()
        except utils.CannotPaginate as e:
            await ctx.send(str(e))

    @assign.command(name='remove', aliases=['delete'])
    @commands.guild_only()
    @utils.custom_perms(manage_roles=True)
    @utils.check_restricted()
    async def _delete_assigns(self, ctx, *role: discord.Role):
        """Removes the provided role(s) from the list of available self-assignable roles

        EXAMPLE: !assigns remove Member NSFW
        RESULT: Removes the ability for users to self-assign the roles Member, and NSFW"""
        key = str(ctx.message.guild.id)
        self_assignable_roles = self.bot.db.load('server_settings', key=key, pluck='self_assignable_roles') or []
        if len(self_assignable_roles) == 0:
            await ctx.send("There are no self-assignable roles on this server")
            return

        fmt = ""
        for r in role:
            rid = str(r.id)
            try:
                self_assignable_roles.remove(rid)
            except ValueError:
                fmt += "\n{} is not a self-assignable role".format(r.name)
            else:
                fmt += "\n{} is no longer a self-assignable role".format(r.name)

        update = {
            'self_assignable_roles': self_assignable_roles,
            'server_id': key
        }
        self.bot.db.save('server_settings', update)
        await ctx.send(fmt)


def setup(bot):
    bot.add_cog(Roles(bot))
