"""Helper classes for formatting data as tables"""

from collections import OrderedDict
from collections.abc import Mapping

from django.contrib.admin.utils import quote
from django.forms import MediaDefiningClass
from django.template.loader import get_template
from django.templatetags.l10n import unlocalize
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.text import capfirst
from django.utils.translation import gettext, gettext_lazy

from wagtail.admin.ui.components import Component
from wagtail.coreutils import multigetattr


class BaseColumn(metaclass=MediaDefiningClass):
    class Header:
        # Helper object used for rendering column headers in templates -
        # behaves as a component (i.e. it has a render_html method) but delegates rendering
        # to Column.render_header_html
        def __init__(self, column):
            self.column = column

        def render_html(self, parent_context):
            return self.column.render_header_html(parent_context)

    class Cell:
        # Helper object used for rendering table cells in templates -
        # behaves as a component (i.e. it has a render_html method) but delegates rendering
        # to Column.render_cell_html
        def __init__(self, column, instance):
            self.column = column
            self.instance = instance

        def render_html(self, parent_context):
            return self.column.render_cell_html(self.instance, parent_context)

    header_template_name = "wagtailadmin/tables/column_header.html"
    cell_template_name = None

    def __init__(
        self,
        name,
        label=None,
        accessor=None,
        classname=None,
        sort_key=None,
        width=None,
        ascending_title_text=None,
        descending_title_text=None,
    ):
        self.name = name
        self.accessor = accessor or name
        if label is None:
            self.label = capfirst(name.replace("_", " "))
        else:
            self.label = label
        self.classname = classname
        self.sort_key = sort_key
        self.header = Column.Header(self)
        self.width = width
        self.ascending_title_text = ascending_title_text
        self.descending_title_text = descending_title_text

    def get_header_context_data(self, parent_context):
        """
        Compiles the context dictionary to pass to the header template when rendering the column header
        """
        table = parent_context["table"]
        return {
            "column": self,
            "table": table,
            "is_orderable": bool(self.sort_key),
            "is_ascending": self.sort_key and table.ordering == self.sort_key,
            "is_descending": self.sort_key and table.ordering == ("-" + self.sort_key),
            "request": parent_context.get("request"),
            "ascending_title_text": self.ascending_title_text
            or table.get_ascending_title_text(self),
            "descending_title_text": self.descending_title_text
            or table.get_descending_title_text(self),
        }

    @cached_property
    def header_template(self):
        return get_template(self.header_template_name)

    @cached_property
    def cell_template(self):
        if self.cell_template_name is None:
            raise NotImplementedError(
                "cell_template_name must be specified on %r" % self
            )
        return get_template(self.cell_template_name)

    def render_header_html(self, parent_context):
        """
        Renders the column's header
        """
        context = self.get_header_context_data(parent_context)
        return self.header_template.render(context)

    def get_cell_context_data(self, instance, parent_context):
        """
        Compiles the context dictionary to pass to the cell template when rendering a table cell for
        the given instance
        """
        return {
            "instance": instance,
            "column": self,
            "row": parent_context["row"],
            "table": parent_context["table"],
            "request": parent_context.get("request"),
        }

    def render_cell_html(self, instance, parent_context):
        """
        Renders a table cell containing data for the given instance
        """
        context = self.get_cell_context_data(instance, parent_context)
        return self.cell_template.render(context)

    def get_cell(self, instance):
        """
        Return an object encapsulating this column and an item of data, which we can use for
        rendering a table cell into a template
        """
        return Column.Cell(self, instance)

    def __repr__(self):
        return "<{}.{}: {}>".format(
            self.__class__.__module__,
            self.__class__.__qualname__,
            self.name,
        )


class Column(BaseColumn):
    """A column that displays a single field of data from the model"""

    cell_template_name = "wagtailadmin/tables/cell.html"

    def get_value(self, instance):
        """
        Given an instance (i.e. any object containing data), extract the field of data to be
        displayed in a cell of this column
        """
        if callable(self.accessor):
            return self.accessor(instance)
        else:
            try:
                return multigetattr(instance, self.accessor)
            except AttributeError:
                return None

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        value = self.get_value(instance)
        if isinstance(value, int) and not isinstance(value, bool):
            # To prevent errors arising from USE_THOUSAND_SEPARATOR, we require all numbers output
            # on templates to be explicitly localized or unlocalized. For numeric table cells, we
            # unlocalize them by default; developers may subclass Column to obtain formatted numbers.
            value = unlocalize(value)
        context["value"] = value
        return context


class ButtonsColumnMixin:
    """A mixin for columns that contain buttons"""

    buttons = []

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["buttons"] = sorted(self.get_buttons(instance, parent_context))
        return context

    def get_buttons(self, instance, parent_context):
        return self.buttons


class TitleColumn(Column):
    """A column where data is styled as a title and wrapped in a link or <label>"""

    cell_template_name = "wagtailadmin/tables/title_cell.html"

    def __init__(
        self,
        name,
        url_name=None,
        get_url=None,
        get_title_id=None,
        label_prefix=None,
        get_label_id=None,
        link_classname=None,
        link_attrs=None,
        id_accessor="pk",
        **kwargs,
    ):
        super().__init__(name, **kwargs)
        self.url_name = url_name
        self._get_url_func = get_url
        self._get_title_id_func = get_title_id
        self.label_prefix = label_prefix
        self._get_label_id_func = get_label_id
        self.link_attrs = link_attrs or {}
        self.link_classname = link_classname
        self.id_accessor = id_accessor

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["link_attrs"] = self.get_link_attrs(instance, parent_context)
        context["link_attrs"]["href"] = context["link_url"] = self.get_link_url(
            instance, parent_context
        )
        if self.link_classname is not None:
            context["link_attrs"]["class"] = self.link_classname
        context["title_id"] = self.get_title_id(instance, parent_context)
        context["label_id"] = self.get_label_id(instance, parent_context)
        return context

    def get_link_attrs(self, instance, parent_context):
        return self.link_attrs.copy()

    def get_link_url(self, instance, parent_context):
        if self._get_url_func:
            return self._get_url_func(instance)
        elif self.url_name:
            id = multigetattr(instance, self.id_accessor)
            return reverse(self.url_name, args=(quote(id),))

    def get_title_id(self, instance, parent_context):
        if self._get_title_id_func:
            return self._get_title_id_func(instance)

    def get_label_id(self, instance, parent_context):
        if self._get_label_id_func:
            return self._get_label_id_func(instance)
        elif self.label_prefix:
            id = multigetattr(instance, self.id_accessor)
            return f"{self.label_prefix}-{id}"


class StatusFlagColumn(Column):
    """Represents a boolean value as a status tag (or lack thereof, if the corresponding label is None)"""

    cell_template_name = "wagtailadmin/tables/status_flag_cell.html"

    def __init__(self, name, true_label=None, false_label=None, **kwargs):
        super().__init__(name, **kwargs)
        self.true_label = true_label
        self.false_label = false_label


class StatusTagColumn(Column):
    """Represents a status tag"""

    cell_template_name = "wagtailadmin/tables/status_tag_cell.html"

    def __init__(self, name, primary=None, **kwargs):
        super().__init__(name, **kwargs)
        self.primary = primary

    def get_primary(self, instance):
        if callable(self.primary):
            return self.primary(instance)
        return self.primary

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["primary"] = self.get_primary(instance)
        return context


class BooleanColumn(Column):
    """Represents a True/False/None value as a tick/cross/question icon"""

    cell_template_name = "wagtailadmin/tables/boolean_cell.html"

    def get_value(self, instance):
        value = super().get_value(instance)
        if value is None:
            return None
        return bool(value)


class LiveStatusTagColumn(StatusTagColumn):
    """Represents a live/draft status tag"""

    def __init__(self, **kwargs):
        super().__init__(
            "status_string",
            label=kwargs.pop("label", gettext("Status")),
            sort_key=kwargs.pop("sort_key", "live"),
            primary=lambda instance: instance.live,
            **kwargs,
        )


class DateColumn(Column):
    """Outputs a date in human-readable format"""

    cell_template_name = "wagtailadmin/tables/date_cell.html"


class UpdatedAtColumn(DateColumn):
    """Outputs the _updated_at date annotation in human-readable format"""

    def __init__(self, **kwargs):
        super().__init__(
            "_updated_at",
            label=kwargs.pop("label", gettext("Updated")),
            sort_key=kwargs.pop("sort_key", "_updated_at"),
            **kwargs,
        )


class UserColumn(Column):
    """Outputs the username and avatar for a user"""

    cell_template_name = "wagtailadmin/tables/user_cell.html"

    def __init__(self, name, blank_display_name="", **kwargs):
        super().__init__(name, **kwargs)
        self.blank_display_name = blank_display_name

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)

        user = context["value"]
        if user:
            try:
                full_name = user.get_full_name().strip()
            except AttributeError:
                full_name = ""
            context["display_name"] = full_name or user.get_username()
        else:
            context["display_name"] = self.blank_display_name
        return context


class BulkActionsCheckboxColumn(BaseColumn):
    """
    A checkbox column for the bulk actions feature.

    When using this column, there should be another column (e.g. a TitleColumn)
    that has an element with the id "{obj_type}_{instance.pk}_title" that contains
    the title of the object (and nothing else) for screen reader purposes.
    """

    header_template_name = "wagtailadmin/bulk_actions/select_all_checkbox_cell.html"
    cell_template_name = "wagtailadmin/bulk_actions/listing_checkbox_cell.html"

    def __init__(self, *args, obj_type, **kwargs):
        super().__init__(*args, **kwargs)
        self.obj_type = obj_type

    def get_aria_describedby(self, instance):
        return f"{self.obj_type}_{quote(instance.pk)}_title"

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context.update(
            {
                "obj_type": self.obj_type,
                "aria_describedby": self.get_aria_describedby(instance),
            }
        )
        return context


class ReferencesColumn(Column):
    cell_template_name = "wagtailadmin/tables/references_cell.html"

    def __init__(
        self,
        name,
        label=None,
        accessor=None,
        classname=None,
        sort_key=None,
        width=None,
        get_url=None,
        describe_on_delete=False,
    ):
        super().__init__(name, label, accessor, classname, sort_key, width)
        self._get_url_func = get_url
        self.describe_on_delete = describe_on_delete

    def get_edit_url(self, instance):
        if self._get_url_func:
            return self._get_url_func(instance)

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["edit_url"] = self.get_edit_url(instance)
        context["describe_on_delete"] = self.describe_on_delete
        return context


class DownloadColumn(Column):
    cell_template_name = "wagtailadmin/tables/download_cell.html"

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["download_url"] = instance.url
        return context


class RelatedObjectsColumn(Column):
    """Outputs a list of objects related to the object through a one-to-many relationship"""

    cell_template_name = "wagtailadmin/tables/related_objects_cell.html"

    def get_value(self, instance):
        return getattr(instance, self.accessor).all()


class Table(Component):
    template_name = "wagtailadmin/tables/table.html"
    classname = "listing"
    header_row_classname = ""
    ascending_title_text_format = gettext_lazy(
        "Sort by '%(label)s' in ascending order."
    )
    descending_title_text_format = gettext_lazy(
        "Sort by '%(label)s' in descending order."
    )

    def __init__(
        self,
        columns,
        data,
        template_name=None,
        base_url=None,
        ordering=None,
        classname=None,
        attrs=None,
        caption=None,
    ):
        self.columns = OrderedDict([(column.name, column) for column in columns])
        self.caption = caption
        self.data = data
        if template_name:
            self.template_name = template_name
        self.base_url = base_url
        self.ordering = ordering
        if classname is not None:
            self.classname = classname
        self.base_attrs = attrs or {}

    def get_caption(self):
        return self.caption

    def get_context_data(self, parent_context):
        context = super().get_context_data(parent_context)
        context["table"] = self
        context["request"] = parent_context.get("request")
        return context

    @property
    def media(self):
        media = super().media
        for col in self.columns.values():
            media += col.media
        return media

    @property
    def rows(self):
        for index, instance in enumerate(self.data):
            yield Table.Row(self, instance, index)

    @property
    def row_count(self):
        return len(self.data)

    @property
    def attrs(self):
        attrs = self.base_attrs.copy()
        attrs["class"] = self.classname
        return attrs

    def get_row_classname(self, instance):
        return ""

    def get_row_attrs(self, instance):
        attrs = {}
        classname = self.get_row_classname(instance)
        if classname:
            attrs["class"] = classname
        return attrs

    def has_column_widths(self):
        return any(column.width for column in self.columns.values())

    def get_ascending_title_text(self, column):
        if self.ascending_title_text_format:
            return self.ascending_title_text_format % {"label": column.label}

    def get_descending_title_text(self, column):
        if self.descending_title_text_format:
            return self.descending_title_text_format % {"label": column.label}

    class Row(Mapping):
        # behaves as an OrderedDict whose items are the rendered results of
        # the corresponding column's format_cell method applied to the instance
        def __init__(self, table, instance, index):
            self.table = table
            self.columns = table.columns
            self.instance = instance
            self.index = index

        def __len__(self):
            return len(self.columns)

        def __getitem__(self, key):
            return self.columns[key].get_cell(self.instance)

        def __iter__(self):
            yield from self.columns

        def __repr__(self):
            return repr([col.get_cell(self.instance) for col in self.columns.values()])

        @cached_property
        def classname(self):
            return self.table.get_row_classname(self.instance)

        @cached_property
        def attrs(self):
            return self.table.get_row_attrs(self.instance)
