# -*- coding: utf-8 -*-

"""
The MIT License (MIT)

Copyright (c) 2015-2017 Rapptz

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""

import asyncio
import inspect
import discord
import functools

from .errors import *
from .cooldowns import Cooldown, BucketType, CooldownMapping
from .view import quoted_word
from . import converter as converters

__all__ = [ 'Command', 'Group', 'GroupMixin', 'command', 'group',
            'has_role', 'has_permissions', 'has_any_role', 'check',
            'bot_has_role', 'bot_has_permissions', 'bot_has_any_role',
            'cooldown', 'guild_only', 'is_owner', 'is_nsfw', ]

def wrap_callback(coro):
    @functools.wraps(coro)
    @asyncio.coroutine
    def wrapped(*args, **kwargs):
        try:
            ret = yield from coro(*args, **kwargs)
        except CommandError:
            raise
        except asyncio.CancelledError:
            return
        except Exception as e:
            raise CommandInvokeError(e) from e
        return ret
    return wrapped

def hooked_wrapped_callback(command, ctx, coro):
    @functools.wraps(coro)
    @asyncio.coroutine
    def wrapped(*args, **kwargs):
        try:
            ret = yield from coro(*args, **kwargs)
        except CommandError:
            ctx.command_failed = True
            raise
        except asyncio.CancelledError:
            ctx.command_failed = True
            return
        except Exception as e:
            ctx.command_failed = True
            raise CommandInvokeError(e) from e
        finally:
            yield from command.call_after_hooks(ctx)
        return ret
    return wrapped

def _convert_to_bool(argument):
    lowered = argument.lower()
    if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
        return True
    elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
        return False
    else:
        raise BadArgument(lowered + ' is not a recognised boolean option')

class Command:
    """A class that implements the protocol for a bot text command.

    These are not created manually, instead they are created via the
    decorator or functional interface.

    Attributes
    -----------
    name: str
        The name of the command.
    callback: coroutine
        The coroutine that is executed when the command is called.
    help: str
        The long help text for the command.
    brief: str
        The short help text for the command. If this is not specified
        then the first line of the long help text is used instead.
    usage: str
        A replacement for arguments in the default help text.
    aliases: list
        The list of aliases the command can be invoked under.
    enabled: bool
        A boolean that indicates if the command is currently enabled.
        If the command is invoked while it is disabled, then
        :exc:`.DisabledCommand` is raised to the :func:`.on_command_error`
        event. Defaults to ``True``.
    parent: Optional[command]
        The parent command that this command belongs to. ``None`` is there
        isn't one.
    checks
        A list of predicates that verifies if the command could be executed
        with the given :class:`.Context` as the sole parameter. If an exception
        is necessary to be thrown to signal failure, then one derived from
        :exc:`.CommandError` should be used. Note that if the checks fail then
        :exc:`.CheckFailure` exception is raised to the :func:`.on_command_error`
        event.
    description: str
        The message prefixed into the default help command.
    hidden: bool
        If ``True``\, the default help command does not show this in the
        help output.
    rest_is_raw: bool
        If ``False`` and a keyword-only argument is provided then the keyword
        only argument is stripped and handled as if it was a regular argument
        that handles :exc:`.MissingRequiredArgument` and default values in a
        regular matter rather than passing the rest completely raw. If ``True``
        then the keyword-only argument will pass in the rest of the arguments
        in a completely raw matter. Defaults to ``False``.
    ignore_extra: bool
        If ``True``\, ignores extraneous strings passed to a command if all its
        requirements are met (e.g. ``?foo a b c`` when only expecting ``a``
        and ``b``). Otherwise :func:`.on_command_error` and local error handlers
        are called with :exc:`.TooManyArguments`. Defaults to ``True``.
    """
    def __init__(self, name, callback, **kwargs):
        self.name = name
        if not isinstance(name, str):
            raise TypeError('Name of a command must be a string.')

        self.callback = callback
        self.enabled = kwargs.get('enabled', True)
        self.help = kwargs.get('help')
        self.brief = kwargs.get('brief')
        self.usage = kwargs.get('usage')
        self.rest_is_raw = kwargs.get('rest_is_raw', False)
        self.aliases = kwargs.get('aliases', [])
        self.description = inspect.cleandoc(kwargs.get('description', ''))
        self.hidden = kwargs.get('hidden', False)
        signature = inspect.signature(callback)
        self.params = signature.parameters.copy()
        self.checks = kwargs.get('checks', [])
        self.module = callback.__module__
        self.ignore_extra = kwargs.get('ignore_extra', True)
        self.instance = None
        self.parent = None
        self._buckets = CooldownMapping(kwargs.get('cooldown'))
        self._before_invoke = None
        self._after_invoke = None

    @asyncio.coroutine
    def dispatch_error(self, ctx, error):
        ctx.command_failed = True
        cog = self.instance
        try:
            coro = self.on_error
        except AttributeError:
            pass
        else:
            injected = wrap_callback(coro)
            if cog is not None:
                yield from injected(cog, ctx, error)
            else:
                yield from injected(ctx, error)

        try:
            local = getattr(cog, '_{0.__class__.__name__}__error'.format(cog))
        except AttributeError:
            pass
        else:
            wrapped = wrap_callback(local)
            yield from wrapped(ctx, error)
        finally:
            ctx.bot.dispatch('command_error', ctx, error)

    def __get__(self, instance, owner):
        if instance is not None:
            self.instance = instance
        return self

    @asyncio.coroutine
    def do_conversion(self, ctx, converter, argument):
        if converter is bool:
            return _convert_to_bool(argument)

        if converter.__module__.startswith('discord.') and not converter.__module__.endswith('converter'):
            converter = getattr(converters, converter.__name__ + 'Converter')

        if inspect.isclass(converter):
            if issubclass(converter, converters.Converter):
                instance = converter()
                ret = yield from instance.convert(ctx, argument)
                return ret
            else:
                method = getattr(converter, 'convert', None)
                if method is not None and inspect.ismethod(method):
                    ret = yield from method(ctx, argument)
                    return ret
        elif isinstance(converter, converters.Converter):
            ret = yield from converter.convert(ctx, argument)
            return ret

        return converter(argument)

    def _get_converter(self, param):
        converter = param.annotation
        if converter is param.empty:
            if param.default is not param.empty:
                converter = str if param.default is None else type(param.default)
            else:
                converter = str
        return converter

    @asyncio.coroutine
    def transform(self, ctx, param):
        required = param.default is param.empty
        converter = self._get_converter(param)
        consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
        view = ctx.view
        view.skip_ws()

        if view.eof:
            if param.kind == param.VAR_POSITIONAL:
                raise RuntimeError() # break the loop
            if required:
                raise MissingRequiredArgument(param)
            return param.default

        if consume_rest_is_special:
            argument = view.read_rest().strip()
        else:
            argument = quoted_word(view)

        try:
            return (yield from self.do_conversion(ctx, converter, argument))
        except CommandError as e:
            raise e
        except Exception as e:
            try:
                name = converter.__name__
            except AttributeError:
                name = converter.__class__.__name__

            raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from e

    @property
    def clean_params(self):
        """Retrieves the parameter OrderedDict without the context or self parameters.

        Useful for inspecting signature.
        """
        result = self.params.copy()
        if self.instance is not None:
            # first parameter is self
            result.popitem(last=False)

        try:
            # first/second parameter is context
            result.popitem(last=False)
        except Exception as e:
            raise ValueError('Missing context parameter') from None

        return result

    @property
    def full_parent_name(self):
        """Retrieves the fully qualified parent command name.

        This the base command name required to execute it. For example,
        in ``?one two three`` the parent name would be ``one two``.
        """
        entries = []
        command = self
        while command.parent is not None:
            command = command.parent
            entries.append(command.name)

        return ' '.join(reversed(entries))

    @property
    def root_parent(self):
        """Retrieves the root parent of this command.

        If the command has no parents then it returns ``None``.

        For example in commands ``?a b c test``, the root parent is
        ``a``.
        """
        entries = []
        command = self
        while command.parent is not None:
            command = command.parent
            entries.append(command)

        if len(entries) == 0:
            return None

        return entries[-1]

    @property
    def qualified_name(self):
        """Retrieves the fully qualified command name.

        This is the full parent name with the command name as well.
        For example, in ``?one two three`` the qualified name would be
        ``one two three``.
        """

        parent = self.full_parent_name
        if parent:
            return parent + ' ' + self.name
        else:
            return self.name

    def __str__(self):
        return self.qualified_name

    @asyncio.coroutine
    def _parse_arguments(self, ctx):
        ctx.args = [ctx] if self.instance is None else [self.instance, ctx]
        ctx.kwargs = {}
        args = ctx.args
        kwargs = ctx.kwargs

        view = ctx.view
        iterator = iter(self.params.items())

        if self.instance is not None:
            # we have 'self' as the first parameter so just advance
            # the iterator and resume parsing
            try:
                next(iterator)
            except StopIteration:
                fmt = 'Callback for {0.name} command is missing "self" parameter.'
                raise discord.ClientException(fmt.format(self))

        # next we have the 'ctx' as the next parameter
        try:
            next(iterator)
        except StopIteration:
            fmt = 'Callback for {0.name} command is missing "ctx" parameter.'
            raise discord.ClientException(fmt.format(self))

        for name, param in iterator:
            if param.kind == param.POSITIONAL_OR_KEYWORD:
                transformed = yield from self.transform(ctx, param)
                args.append(transformed)
            elif param.kind == param.KEYWORD_ONLY:
                # kwarg only param denotes "consume rest" semantics
                if self.rest_is_raw:
                    converter = self._get_converter(param)
                    argument = view.read_rest()
                    kwargs[name] = yield from self.do_conversion(ctx, converter, argument)
                else:
                    kwargs[name] = yield from self.transform(ctx, param)
                break
            elif param.kind == param.VAR_POSITIONAL:
                while not view.eof:
                    try:
                        transformed = yield from self.transform(ctx, param)
                        args.append(transformed)
                    except RuntimeError:
                        break

        if not self.ignore_extra:
            if not view.eof:
                raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)

    @asyncio.coroutine
    def _verify_checks(self, ctx):
        if not self.enabled:
            raise DisabledCommand('{0.name} command is disabled'.format(self))

        if not (yield from self.can_run(ctx)):
            raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self))

    @asyncio.coroutine
    def call_before_hooks(self, ctx):
        # now that we're done preparing we can call the pre-command hooks
        # first, call the command local hook:
        cog = self.instance
        if self._before_invoke is not None:
            if cog is None:
                yield from self._before_invoke(ctx)
            else:
                yield from self._before_invoke(cog, ctx)

        # call the cog local hook if applicable:
        try:
            hook = getattr(cog, '_{0.__class__.__name__}__before_invoke'.format(cog))
        except AttributeError:
            pass
        else:
            yield from hook(ctx)

        # call the bot global hook if necessary
        hook = ctx.bot._before_invoke
        if hook is not None:
            yield from hook(ctx)

    @asyncio.coroutine
    def call_after_hooks(self, ctx):
        cog = self.instance
        if self._after_invoke is not None:
            if cog is None:
                yield from self._after_invoke(ctx)
            else: