"""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.contrib.humanize.templatetags.humanize import intcomma
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 get_locales_display_names, multigetattr
from wagtail.models import Locale


class BaseColumn(metaclass=MediaDefiningClass):
    """
    A column that encapsulates the header and data cells for rendering.


    :param name: The internal name of the column, used to identify it within the table.
    :param label: The human-readable label for the column header.
        If not provided, a label will be generated from the name.
    :param accessor: A dotted path to an attribute accessed from the current
        row's object, or a callable that takes the current row's object, that
        returns the value for this column. This is unused by default, but may be
        used by subclasses such as :class:`Column`.
    :param classname: Optional CSS classes to apply to all cells in this column.
    :param sort_key: An optional key for a query parameter that identifies the
        field by which the table can be sorted when this column's header is
        clicked. If not provided, the column will not be sortable.
    :param width: An optional width for the column.
    :param ascending_title_text: An optional text for the column header's
        ``title`` attribute when the column is sorted in ascending order. If not
        provided, a default string will be used that incorporates the column label.
    :param descending_title_text: An optional text for the column header's
        ``title`` attribute when the column is sorted in descending order. If not
        provided, a default string will be used that incorporates the column label.
    """

    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"
    """The name of the template used to render the column header."""
    cell_template_name = None
    """The name of the template used to render a cell of this column."""

    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):
        """
        Returns 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"
    empty_value_display = ""
    """The text to display in cells where the value is empty."""

    def get_value(self, instance):
        """
        Given an instance (i.e. any object containing data), extracts the field
        of data to be displayed in a cell of this column. By default, this uses
        the ``accessor`` to retrieve the value.
        """
        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)
        context["raw_value"] = 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)

        if not str(value).strip():
            context["value"] = self.empty_value_display
        else:
            context["value"] = value
        return context


class NumberColumn(Column):
    """A specialized column that displays numbers with locale-aware formatting."""

    def get_cell_context_data(self, instance, parent_context):
        context = super().get_cell_context_data(instance, parent_context)
        context["value"] = intcomma(context["raw_value"])
        return context


class ButtonsColumnMixin:
    """
    A mixin for columns that contain buttons. The column's template should
    iterate over the ``buttons`` variable in the context to render them.
    """

    buttons = []
    """A list of button components."""

    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):
        """
        Returns a list of button components to be supplied as ``buttons`` in the
        template context.
        """
        return self.buttons


class TitleColumn(Column):
    """
    A column that displays a title wrapped in a link (``<a>``) or ``<label>``.
    The cell's template has the ability to render action buttons.

    :param url_name: An optional URL pattern name to use for constructing the
        link URL for each cell in this column.
    :param get_url: An optional callable that takes the current row's object and
        returns the URL to link to for that row. If both this and ``url_name``
        are provided, this takes precedence.
    :param get_title_id: An optional callable that takes the current row's
        object and returns a string to use as the id attribute for an element
        that contains the object's title, such as ``"page_{obj.pk}_title"``.
        This can be used by other columns (such as
        :class:`BulkActionsCheckboxColumn`) for screen reader purposes.
    :param label_prefix: An optional string prefix to use for constructing the
        ``for`` attribute when the title is rendered as a ``<label>``. The id of
        the corresponding input element must be in the format of
        ``"{label_prefix}-{id}"``.
    :param get_label_id: An optional callable that takes the current row's
        object and returns a string to use as the ``for`` attribute when the
        title is rendered as a ``<label>``. If both this and ``label_prefix``
        are provided, this takes precedence.
    :param link_classname: An optional string of CSS classes to apply to the
        ``<a>`` element when the title is wrapped in a link.
    :param link_attrs: An optional dictionary of additional HTML attributes to
        apply to the ``<a>`` element when the title is wrapped in a link.
    :param id_accessor: An optional dotted path to an attribute accessed from
        the current row's object, that returns the value to use as the id when
        constructing URLs or label attributes. Defaults to ``"pk"``.
    """

    cell_template_name = "wagtailadmin/tables/title_cell.html"
    empty_value_display = gettext_lazy("(blank)")

    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):
    """
    A column that displays a boolean value as a status tag (or lack thereof,
    if the corresponding label is ``None``).

    :param true_label: The string to display when the value is ``True``.
    :param false_label: The string to display when the value is ``False``.
    """

    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):
    """
    A column that displays a status tag.

    :param primary: An optional boolean or callable that takes the current row's
        object and returns a boolean, indicating whether the status tag should
        be styled as "primary".
    """

    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):
    """
    A column that displays 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):
    """A column that displays 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 LocaleColumn(Column):
    """A column that displays a :class:`~wagtail.models.Locale` label."""

    cell_template_name = "wagtailadmin/tables/locale_cell.html"

    def __init__(self, **kwargs):
        super().__init__(
            "locale_id",
            label=kwargs.pop("label", gettext("Locale")),
            sort_key=kwargs.pop("sort_key", "locale"),
            classname=kwargs.pop("classname", "w-text-16"),
            **kwargs,
        )

    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):
            value = get_locales_display_names().get(value)
        elif isinstance(value, Locale):
            value = value.get_display_name()
        context["value"] = value
        return context


class DateColumn(Column):
    """A column that displays a date in human-readable format."""

    cell_template_name = "wagtailadmin/tables/date_cell.html"


class UpdatedAtColumn(DateColumn):
    """
    A column that displays 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):
    """
    A column that displays the username and avatar for a user.

    :param blank_display_name: The text to display when the user's name is empty.
    """

    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 (such as a :class:`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.

    :param obj_type: A string representing the type of object being listed, used
        to construct the ``aria-describedby`` attribute for the checkboxes.
    """

    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 UsageCountColumn(Column):
    """
    A column that displays the number of times an object has been referenced.
    """

    cell_template_name = "wagtailadmin/tables/usage_count_column_cell.html"


class ReferencesColumn(Column):
    """
    A column that displays references that were extracted from the current row's
    object.

    :param get_url: An optional callable that takes the current row's object and
        returns the URL to link to. If not provided, the references will be
        displayed without a link.
    :param describe_on_delete: An optional boolean that determines whether to
        display a description of the reference's ``on_delete`` behavior. Useful
        when confirming deletion of an object that has references to it, so the
        user can understand the consequences of deleting the object.
    """

    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):
    """
    A column that displays the object's ``url`` property as a link to download
    a file.
    """

    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):
    """
    A column that displays 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):
    """
    A component that renders a table.

    :param columns: An iterable of :class:`Column` objects representing the
        columns of the table.
    :param data: An iterable of objects representing the rows of the table. Each
        object will be passed to the columns to extract the data for each cell.
    :param template_name: An optional name of the template used to render the
        table. If not provided, a default template will be used.
    :param base_url: An optional base URL to use for constructing sorting links
        in the column headers.
    :param ordering: An optional string representing the current ordering of the
        table, used to determine the state of the sorting links in the column
        headers. This should match the sort_key of one of the columns,
        optionally prefixed with "-" to indicate descending order.
    :param classname: Optional CSS classes to apply to the ``<table>`` element.
    :param attrs: An optional dictionary of additional HTML attributes to apply
        to the ``<table>`` element.
    :param caption: An optional caption for the table.
    """

    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)
