"""Objects related to layout of rendered text, such as TextFitter."""

from __future__ import annotations

from typing import TYPE_CHECKING

from PIL import ImageFont

if TYPE_CHECKING:
    from pptx.util import Length


class TextFitter(tuple):
    """Value object that knows how to fit text into given rectangular extents."""

    def __new__(cls, line_source, extents, font_file):
        width, height = extents
        return tuple.__new__(cls, (line_source, width, height, font_file))

    @classmethod
    def best_fit_font_size(
        cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str
    ) -> int:
        """Return whole-number best fit point size less than or equal to `max_size`.

        The return value is the largest whole-number point size less than or equal to
        `max_size` that allows `text` to fit completely within `extents` when rendered
        using font defined in `font_file`.
        """
        line_source = _LineSource(text)
        text_fitter = cls(line_source, extents, font_file)
        return text_fitter._best_fit_font_size(max_size)

    def _best_fit_font_size(self, max_size):
        """
        Return the largest whole-number point size less than or equal to
        *max_size* that this fitter can fit.
        """
        predicate = self._fits_inside_predicate
        sizes = _BinarySearchTree.from_ordered_sequence(range(1, int(max_size) + 1))
        return sizes.find_max(predicate)

    def _break_line(self, line_source, point_size):
        """
        Return a (line, remainder) pair where *line* is the longest line in
        *line_source* that will fit in this fitter's width and *remainder* is
        a |_LineSource| object containing the text following the break point.
        """
        lines = _BinarySearchTree.from_ordered_sequence(line_source)
        predicate = self._fits_in_width_predicate(point_size)
        return lines.find_max(predicate)

    def _fits_in_width_predicate(self, point_size):
        """
        Return a function taking a text string value and returns |True| if
        that text fits in this fitter when rendered at *point_size*. Used as
        predicate for _break_line()
        """

        def predicate(line):
            """
            Return |True| if *line* fits in this fitter when rendered at
            *point_size*.
            """
            cx = _rendered_size(line.text, point_size, self._font_file)[0]
            return cx <= self._width

        return predicate

    @property
    def _fits_inside_predicate(self):
        """Return  function taking an integer point size argument.

        The function returns |True| if the text in this fitter can be wrapped to fit
        entirely within its extents when rendered at that point size.
        """

        def predicate(point_size):
            """Return |True| when text in `line_source` can be wrapped to fit.

            Fit means text can be broken into lines that fit entirely within `extents`
            when rendered at `point_size` using the font defined in `font_file`.
            """
            text_lines = self._wrap_lines(self._line_source, point_size)
            cy = _rendered_size("Ty", point_size, self._font_file)[1]
            return (cy * len(text_lines)) <= self._height

        return predicate

    @property
    def _font_file(self):
        return self[3]

    @property
    def _height(self):
        return self[2]

    @property
    def _line_source(self):
        return self[0]

    @property
    def _width(self):
        return self[1]

    def _wrap_lines(self, line_source, point_size):
        """
        Return a sequence of str values representing the text in
        *line_source* wrapped within this fitter when rendered at
        *point_size*.
        """
        text, remainder = self._break_line(line_source, point_size)
        lines = [text]
        if remainder:
            lines.extend(self._wrap_lines(remainder, point_size))
        return lines


class _BinarySearchTree(object):
    """
    A node in a binary search tree. Uniform for root, subtree root, and leaf
    nodes.
    """

    def __init__(self, value):
        self._value = value
        self._lesser = None
        self._greater = None

    def find_max(self, predicate, max_=None):
        """
        Return the largest item in or under this node that satisfies
        *predicate*.
        """
        if predicate(self.value):
            max_ = self.value
            next_node = self._greater
        else:
            next_node = self._lesser
        if next_node is None:
            return max_
        return next_node.find_max(predicate, max_)

    @classmethod
    def from_ordered_sequence(cls, iseq):
        """
        Return the root of a balanced binary search tree populated with the
        values in iterable *iseq*.
        """
        seq = list(iseq)
        # optimize for usually all fits by making longest first
        bst = cls(seq.pop())
        bst._insert_from_ordered_sequence(seq)
        return bst

    def insert(self, value):
        """
        Insert a new node containing *value* into this tree such that its
        structure as a binary search tree is preserved.
        """
        side = "_lesser" if value < self.value else "_greater"
        child = getattr(self, side)
        if child is None:
            setattr(self, side, _BinarySearchTree(value))
        else:
            child.insert(value)

    def tree(self, level=0, prefix=""):
        """
        A string representation of the tree rooted in this node, useful for
        debugging purposes.
        """
        text = "%s%s\n" % (prefix, self.value.text)
        prefix = "%s└── " % ("    " * level)
        if self._lesser:
            text += self._lesser.tree(level + 1, prefix)
        if self._greater:
            text += self._greater.tree(level + 1, prefix)
        return text

    @property
    def value(self):
        """
        The value object contained in this node.
        """
        return self._value

    @staticmethod
    def _bisect(seq):
        """
        Return a (medial_value, greater_values, lesser_values) 3-tuple
        obtained by bisecting sequence *seq*.
        """
        if len(seq) == 0:
            return [], None, []
        mid_idx = int(len(seq) / 2)
        mid = seq[mid_idx]
        greater = seq[mid_idx + 1 :]
        lesser = seq[:mid_idx]
        return mid, greater, lesser

    def _insert_from_ordered_sequence(self, seq):
        """
        Insert the new values contained in *seq* into this tree such that
        a balanced tree is produced.
        """
        if len(seq) == 0:
            return
        mid, greater, lesser = self._bisect(seq)
        self.insert(mid)
        self._insert_from_ordered_sequence(greater)
        self._insert_from_ordered_sequence(lesser)


class _LineSource(object):
    """
    Generates all the possible even-word line breaks in a string of text,
    each in the form of a (line, remainder) 2-tuple where *line* contains the
    text before the break and *remainder* the text after as a |_LineSource|
    object. Its boolean value is |True| when it contains text, |False| when
    its text is the empty string or whitespace only.
    """

    def __init__(self, text):
        self._text = text

    def __bool__(self):
        """
        Gives this object boolean behaviors (in Python 3). bool(line_source)
        is False if it contains the empty string or whitespace only.
        """
        return self._text.strip() != ""

    def __eq__(self, other):
        return self._text == other._text

    def __iter__(self):
        """
        Generate a (text, remainder) pair for each possible even-word line
        break in this line source, where *text* is a str value and remainder
        is a |_LineSource| value.
        """
        words = self._text.split()
        for idx in range(1, len(words) + 1):
            line_text = " ".join(words[:idx])
            remainder_text = " ".join(words[idx:])
            remainder = _LineSource(remainder_text)
            yield _Line(line_text, remainder)

    def __nonzero__(self):
        """
        Gives this object boolean behaviors (in Python 2). bool(line_source)
        is False if it contains the empty string or whitespace only.
        """
        return self._text.strip() != ""

    def __repr__(self):
        return "<_LineSource('%s')>" % self._text


class _Line(tuple):
    """
    A candidate line broken at an even word boundary from a string of text,
    and a |_LineSource| value containing the text that remains after the line
    is broken at this spot.
    """

    def __new__(cls, text, remainder):
        return tuple.__new__(cls, (text, remainder))

    def __gt__(self, other):
        return len(self.text) > len(other.text)

    def __lt__(self, other):
        return not self.__gt__(other)

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

    def __repr__(self):
        return "'%s' => '%s'" % (self.text, self.remainder)

    @property
    def remainder(self):
        return self[1]

    @property
    def text(self):
        return self[0]


class _Fonts(object):
    """
    A memoizing cache for ImageFont objects.
    """

    fonts = {}

    @classmethod
    def font(cls, font_path, point_size):
        if (font_path, point_size) not in cls.fonts:
            cls.fonts[(font_path, point_size)] = ImageFont.truetype(font_path, point_size)
        return cls.fonts[(font_path, point_size)]


def _rendered_size(text, point_size, font_file):
    """
    Return a (width, height) pair representing the size of *text* in English
    Metric Units (EMU) when rendered at *point_size* in the font defined in
    *font_file*.
    """
    emu_per_inch = 914400
    px_per_inch = 72.0

    font = _Fonts.font(font_file, point_size)
    try:
        px_width, px_height = font.getsize(text)
    except AttributeError:
        left, top, right, bottom = font.getbbox(text)
        px_width, px_height = right - left, bottom - top

    emu_width = int(px_width / px_per_inch * emu_per_inch)
    emu_height = int(px_height / px_per_inch * emu_per_inch)

    return emu_width, emu_height
