"""
Parameter usage / help text formatters

:author: Doug Skrypa
"""
# pylint: disable=W0613

from __future__ import annotations

from itertools import chain
from textwrap import indent
from typing import TYPE_CHECKING, Type, Tuple

from ..context import ctx
from ..exceptions import NoActiveContext
from ..parameters.base import ParamBase, BasePositional, BaseOption
from ..parameters.choice_map import ChoiceMap
from ..parameters import ParamGroup, PassThru, ParamOrGroup
from .restructured_text import RstTable
from .utils import HelpEntryFormatter, _should_add_default

if TYPE_CHECKING:
    from ..utils import Bool


class ParamHelpFormatter:
    _param_cls_fmt_cls_map = {}

    def __init_subclass__(cls, param_cls: Type[ParamBase] = None):  # noqa
        if param_cls is not None:
            cls._param_cls_fmt_cls_map[param_cls] = cls

    @classmethod
    def for_param_cls(cls, param_cls: Type[ParamBase]) -> Type[ParamHelpFormatter]:
        try:
            return cls._param_cls_fmt_cls_map[param_cls]
        except KeyError:
            pass

        for p_cls, f_cls in reversed(tuple(cls._param_cls_fmt_cls_map.items())):  # tuple() only for 3.7 compatibility
            if issubclass(param_cls, p_cls):
                return f_cls

        return ParamHelpFormatter

    def __new__(cls, param: ParamOrGroup):
        fmt_cls = cls.for_param_cls(param.__class__) if cls is ParamHelpFormatter else cls
        return super().__new__(fmt_cls)

    def __init__(self, param: ParamOrGroup):
        self.param = param

    def format_metavar(self, choice_delim: str = ',') -> str:
        param = self.param
        if param.choices:
            return '{{{}}}'.format(choice_delim.join(map(str, param.choices)))
        elif param.metavar:
            return param.metavar
        t = param.type
        if t is not None:
            try:
                return t.format_metavar(choice_delim)
            except Exception:  # noqa  # pylint: disable=W0703
                pass
        try:
            use_type_metavar = ctx.config.use_type_metavar
        except NoActiveContext:
            use_type_metavar = False
        if use_type_metavar and t is not None:
            try:
                name = t.__name__
            except AttributeError:
                pass
            else:
                if name != '<lambda>':
                    return name.upper()

        return param.name.upper()

    def format_basic_usage(self) -> str:
        raise NotImplementedError

    def format_usage(self, include_meta: Bool = False, full: Bool = False, delim: str = ', ') -> str:
        return self.format_metavar()

    def format_description(self, rst: Bool = False) -> str:
        param = self.param
        description = param.help or ''
        if _should_add_default(param.default, description, param.show_default):
            pad = ' ' if description else ''
            quote = '``' if rst else ''
            description += f'{pad}(default: {quote}{param.default!r}{quote})'

        return description

    def format_help(self, width: int = 30, prefix: str = '', tw_offset: int = 0) -> str:
        usage = self.format_usage(include_meta=True, full=True)
        description = self.format_description()
        entry_width = max(20, width - tw_offset)
        text = HelpEntryFormatter(usage, description, entry_width, tw_offset=tw_offset)()
        return indent(text, prefix) if prefix else text

    def rst_row(self) -> Tuple[str, str]:
        usage = self.format_usage(include_meta=True, full=True)
        return f'``{usage}``', self.format_description(rst=True)


class PositionalHelpFormatter(ParamHelpFormatter, param_cls=BasePositional):
    def format_basic_usage(self) -> str:
        return self.format_usage()

    def format_usage(self, include_meta: Bool = False, full: Bool = False, delim: str = ', ') -> str:
        metavar = self.format_metavar()
        return metavar if not full or self.param.nargs == 1 else f'{metavar} [{metavar} ...]'


class OptionHelpFormatter(ParamHelpFormatter, param_cls=BaseOption):
    def format_basic_usage(self) -> str:
        usage = self.format_usage(True)
        return usage if self.param.required else f'[{usage}]'

    def format_usage(self, include_meta: Bool = False, full: Bool = False, delim: str = ', ') -> str:
        param = self.param
        if include_meta:
            metavar = self.format_metavar().replace('{', '{{').replace('}', '}}')
            fmt = '{}' if param.nargs == 0 else f'{{}} [{metavar}]' if 0 in param.nargs else f'{{}} {metavar}'
            if full:
                return delim.join(fmt.format(opt) for opt in chain(param.long_opts, param.short_opts))
            else:
                return fmt.format(param.long_opts[0])
        else:
            if full:
                return delim.join(chain(param.long_opts, param.short_opts))
            else:
                return param.long_opts[0]


class ChoiceMapHelpFormatter(PositionalHelpFormatter, param_cls=ChoiceMap):
    def format_metavar(self, choice_delim: str = ',') -> str:
        param = self.param
        if param.choices:
            return '{{{}}}'.format(choice_delim.join(map(str, filter(None, param.choices))))
        else:
            return param.metavar or param.name.upper()

    def format_usage(self, include_meta: Bool = False, full: Bool = False, delim: str = ', ') -> str:
        return self.format_metavar()

    def format_help(self, width: int = 30, prefix: str = '', tw_offset: int = 0) -> str:
        param: ChoiceMap = self.param
        usage = self.format_usage()
        entry_width = max(20, width - tw_offset)
        help_entry = HelpEntryFormatter(usage, param.description, entry_width, lpad=2, tw_offset=tw_offset)()

        parts = [f'{param.title or param._default_title}:', help_entry]
        for choice in param.choices.values():
            parts.append(choice.format_help(entry_width, lpad=4))

        parts.append('')
        text = '\n'.join(parts)
        return indent(text, prefix) if prefix else text

    def rst_table(self) -> RstTable:
        param = self.param
        table = RstTable(param.title or param._default_title, param.description)
        for choice in param.choices.values():
            usage = choice.format_usage()
            table.add_row(f'``{usage}``', choice.help)
        return table


class PassThruHelpFormatter(ParamHelpFormatter, param_cls=PassThru):
    def format_basic_usage(self) -> str:
        usage = self.format_usage()
        return f'-- {usage}' if self.param.required else f'[-- {usage}]'


class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup):  # noqa  # pylint: disable=W0223
    def format_usage(self, include_meta: Bool = False, full: Bool = False, delim: str = ', ') -> str:
        # TODO: | between mutually exclusive members, + between dependent?  Different chars?
        choices = ','.join(mem.formatter.format_usage(include_meta, full, delim) for mem in self.param.members)
        return f'{{{choices}}}'

    def format_description(self, rst: Bool = False) -> str:
        group = self.param
        if not group.description and not group._name:
            if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
                return 'Mutually {} options'.format('exclusive' if group.mutually_exclusive else 'dependent')
            else:
                return 'Optional arguments'
        else:
            description = group.description or f'{group.name} options'
            if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
                description += ' (mutually {})'.format('exclusive' if group.mutually_exclusive else 'dependent')
            return description

    def _get_spacer(self) -> str:
        group = self.param
        if group.mutually_exclusive:
            return '\u00A6 '  # BROKEN BAR
        elif group.mutually_dependent:
            return '\u2551 '  # BOX DRAWINGS DOUBLE VERTICAL
        else:
            return '\u2502 '  # BOX DRAWINGS LIGHT VERTICAL

    def format_help(self, width: int = 30, prefix: str = '', tw_offset: int = 0, clean: Bool = True) -> str:
        """
        Prepare the help text for this group.

        :param width: The width of the option/action/command column.
        :param prefix: Prefix to add to every line (primarily intended for use with nested groups)
        :param tw_offset: Terminal width offset for text width calculations
        :param clean: If this group only contains other groups or Action or SubCommand parameters, then omit the
          description.
        :return: The formatted help text.
        """
        description = self.format_description()
        parts = [f'{description}:']

        if ctx.config.show_group_tree:
            spacer = self._get_spacer()
            tw_offset += 2
        else:
            spacer = ''

        nested, params = 0, 0
        for member in self.param.members:
            if not member.show_in_help:
                continue

            if isinstance(member, (ChoiceMap, ParamGroup)):
                nested += 1
                parts.append(spacer)  # Add space for readability
            else:
                params += 1
            parts.append(member.formatter.format_help(width=width, prefix=spacer, tw_offset=tw_offset))

        if clean and nested and not params:
            parts = parts[2:]  # remove description and the first spacer

        if not parts[-1].endswith('\n'):  # ensure a new line separates sections, but avoid extra lines
            parts.append(spacer)

        text = '\n'.join(parts)
        return indent(text, prefix) if prefix else text

    def rst_table(self) -> RstTable:
        table = RstTable(self.format_description())
        # TODO: non-nested when config.show_group_tree is False; maybe separate options for rst vs help
        for member in self.param.members:
            if member.show_in_help:
                try:
                    sub_table: RstTable = member.formatter.rst_table()  # noqa
                except AttributeError:
                    table.add_row(*member.formatter.rst_row())
                else:
                    sub_table.show_title = False
                    table.add_row(sub_table.title, str(sub_table))

        return table
