#!/usr/bin/env python


"""Adapted Django Paginator

Paginator must be subclassed to work:
- get_count() should return total number of items
- query(bottom: int, top: int) should yield appropriate items

Usage is similar to Django's one otherwise."""


import math
from abc import abstractmethod


class InvalidPageError(Exception):
    pass


class PageNotAnIntegerError(InvalidPageError):
    pass


class EmptyPageError(InvalidPageError):
    pass


class Page:
    def __init__(self, object_list, number, paginator):
        self.object_list = object_list
        self.number = number
        self.paginator = paginator

    def __repr__(self):
        return f"<Page {self.number} of {self.paginator.num_pages}>"

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

    def __getitem__(self, index):
        if not isinstance(index, (int, slice)):
            raise TypeError(
                f"Page indices must be integers or slices, not {type(index).__name__}."
            )
        # The object_list is converted to a list so that if it was a QuerySet
        # it won't be a database hit per __getitem__.
        if not isinstance(self.object_list, list):
            self.object_list = list(self.object_list)
        return self.object_list[index]

    def has_next(self):
        return self.number < self.paginator.num_pages

    def has_previous(self):
        return self.number > 1

    def has_other_pages(self):
        return self.has_previous() or self.has_next()

    def next_page_number(self):
        return self.paginator.validate_number(self.number + 1)

    def previous_page_number(self):
        return self.paginator.validate_number(self.number - 1)

    def start_index(self):
        """
        Return the 1-based index of the first object on this page,
        relative to total objects in the paginator.
        """
        # Special case, return zero if no items.
        if self.paginator.count == 0:
            return 0
        return (self.paginator.per_page * (self.number - 1)) + 1

    def end_index(self):
        """
        Return the 1-based index of the last object on this page,
        relative to total objects found (hits).
        """
        # Special case for the last page because there can be orphans.
        if self.number == self.paginator.num_pages:
            return self.paginator.count
        return self.number * self.paginator.per_page


class Paginator:
    ELLIPSIS = "…"

    def __init__(self, per_page: int = 10, count: int | None = None):
        self.per_page = int(per_page)
        self.count = count if count is not None else self.get_count()
        self.num_pages = math.ceil(max(1, self.count) / self.per_page)
        self.page_range = range(1, self.num_pages + 1)

    @abstractmethod
    def get_count(self) -> int:
        pass

    def __iter__(self):
        for page_number in self.page_range:
            yield self.page(page_number)

    def validate_number(self, number):
        """Validate the given 1-based page number."""
        try:
            if isinstance(number, float) and not number.is_integer():
                raise ValueError
            number = int(number)
        except (TypeError, ValueError) as exc:
            raise PageNotAnIntegerError("That page number is not an integer") from exc
        if number < 1:
            raise EmptyPageError("That page number is less than 1")
        if number > self.num_pages:
            if number == 1:
                pass
            else:
                raise EmptyPageError("That page contains no results")
        return number

    def get_page(self, number):
        """
        Return a valid page, even if the page argument isn't a number or isn't
        in range.
        """
        try:
            number = self.validate_number(number)
        except PageNotAnIntegerError:
            number = 1
        except EmptyPageError:
            number = self.num_pages
        return self.page(number)

    def page(self, number):
        """Return a Page object for the given 1-based page number."""
        number = self.validate_number(number)
        bottom = (number - 1) * self.per_page
        top = bottom + self.per_page
        top = min(self.count, top)
        return self._get_page(self.query(bottom, top), number, self)

    def _get_page(self, *args, **kwargs):
        """
        Return an instance of a single page.
        This hook can be used by subclasses to use an alternative to the
        standard :cls:`Page` object.
        """
        return Page(*args, **kwargs)

    def get_elided_page_range(self, number=1, *, on_each_side=2, on_ends=2):
        """
        Return a 1-based range of pages with some values elided.
        If the page range is larger than a given size, the whole range is not
        provided and a compact form is returned instead, e.g. for a paginator
        with 50 pages, if page 43 were the current page, the output, with the
        default arguments, would be:
            1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
        """
        number = self.validate_number(number)

        if self.num_pages <= (on_each_side + on_ends) * 2:
            yield from self.page_range
            return

        if number > (1 + on_each_side + on_ends) + 1:
            yield from range(1, on_ends + 1)
            yield self.ELLIPSIS
            yield from range(number - on_each_side, number + 1)
        else:
            yield from range(1, number + 1)

        if number < (self.num_pages - on_each_side - on_ends) - 1:
            yield from range(number + 1, number + on_each_side + 1)
            yield self.ELLIPSIS
            yield from range(self.num_pages - on_ends + 1, self.num_pages + 1)
        else:
            yield from range(number + 1, self.num_pages + 1)

    def query(self, bottom: int, top: int):
        raise NotImplementedError()
