# The MIT License (MIT)
#
# Copyright (c) 2020 Kevin Matocha
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`bitmap_label`
================================================================================

Text graphics handling for CircuitPython, including text boxes


* Author(s): Kevin Matocha

Implementation Notes
--------------------

**Hardware:**

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""

import displayio

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"


class Label(displayio.Group):
    """A label displaying a string of text that is stored in a bitmap.
       Note: This ``bitmap_label.py`` library utilizes a bitmap to display the text.
       This method is memory-conserving relative to ``label.py``.
       For the bitmap_label library, the font, text, and line_spacing must be set at
       instancing and are immutable.  The ``max_glyphs`` parameter is ignored and is present
       only for direct compatability with label.py.
       For use cases where text changes are required after the initial instancing, please
       use the `label.py` library.
       For further reduction in memory usage, set save_text to False (text string will not
       be stored).

       The origin point set by ``x`` and ``y``
       properties will be the left edge of the bounding box, and in the center of a M
       glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
       it will try to have it be center-left as close as possible.

       :param Font font: A font class that has ``get_bounding_box`` and ``get_glyph``.
         Must include a capital M for measuring character size.
       :param str text: Text to display
       :param int max_glyphs: Unnecessary parameter (provided only for direct compability
       with label.py)
       :param int color: Color of all text in RGB hex
       :param int background_color: Color of the background, use `None` for transparent
       :param double line_spacing: Line spacing of text to display
       :param boolean background_tight: Set `True` only if you want background box to tightly
       surround text
       :param int padding_top: Additional pixels added to background bounding box at top
       :param int padding_bottom: Additional pixels added to background bounding box at bottom
       :param int padding_left: Additional pixels added to background bounding box at left
       :param int padding_right: Additional pixels added to background bounding box at right
       :param (double,double) anchor_point: Point that anchored_position moves relative to.
        Tuple with decimal percentage of width and height.
        (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
       :param (int,int) anchored_position: Position relative to the anchor_point. Tuple
       containing x,y pixel coordinates.
       :param int scale: Integer value of the pixel scaling
       :param bool save_text: Set True to save the text string as a constant in the
        label structure.  Set False to reduce memory use.
       """

    # pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals, too-many-arguments
    # pylint: disable=too-many-branches, no-self-use
    # Note: max_glyphs parameter is unnecessary, this is used for direct
    # compatibility with label.py

    def __init__(
        self,
        font,
        x=0,
        y=0,
        text="",
        max_glyphs=None,  # This input parameter is ignored, only present for compatibility
        # with label.py
        color=0xFFFFFF,
        background_color=None,
        line_spacing=1.25,
        background_tight=False,
        padding_top=0,
        padding_bottom=0,
        padding_left=0,
        padding_right=0,
        anchor_point=None,
        anchored_position=None,
        save_text=True,  # can reduce memory use if save_text = False
        **kwargs
    ):

        if text == "":
            raise RuntimeError(
                "Please provide text string, or use label.py for mutable text"
            )

        self._font = font

        # Scale will be passed to Group using kwargs.
        if "scale" in kwargs.keys():
            self._scale = kwargs["scale"]
        else:
            self._scale = 1

        self._line_spacing = line_spacing
        self._save_text = save_text

        if self._save_text:  # text string will be saved
            self._text = text
        else:
            self._text = None  # save a None value since text string is not saved

        # limit padding to >= 0
        padding_top = max(0, padding_top)
        padding_bottom = max(0, padding_bottom)
        padding_left = max(0, padding_left)
        padding_right = max(0, padding_right)

        # Calculate the text bounding box

        # Calculate tight box to provide bounding box dimensions to match label for
        # anchor_position calculations
        (
            tight_box_x,
            tight_box_y,
            tight_x_offset,
            tight_y_offset,
        ) = self._text_bounding_box(
            text, font, self._line_spacing, background_tight=True,
        )

        if background_tight:
            box_x = tight_box_x
            box_y = tight_box_y
            y_offset = tight_y_offset
            x_offset = tight_x_offset

        else:
            (box_x, box_y, x_offset, y_offset) = self._text_bounding_box(
                text, font, self._line_spacing, background_tight=background_tight,
            )
        # Calculate the background size including padding
        box_x = box_x + padding_left + padding_right
        box_y = box_y + padding_top + padding_bottom

        # Create the two-color palette
        self.palette = displayio.Palette(2)

        self.background_color = background_color
        self.color = color

        # Create the bitmap and TileGrid
        self.bitmap = displayio.Bitmap(box_x, box_y, len(self.palette))

        # Place the text into the Bitmap
        self._place_text(
            self.bitmap,
            text,
            font,
            self._line_spacing,
            padding_left - x_offset,
            padding_top + y_offset,
        )

        label_position_yoffset = int(  # To calibrate with label.py positioning
            (font.get_glyph(ord("M")).height) / 2
        )

        self.tilegrid = displayio.TileGrid(
            self.bitmap,
            pixel_shader=self.palette,
            width=1,
            height=1,
            tile_width=box_x,
            tile_height=box_y,
            default_tile=0,
            x=-padding_left + x_offset,
            y=label_position_yoffset - y_offset - padding_top,
        )

        # instance the Group
        # this Group will contain just one TileGrid with one contained bitmap
        super().__init__(
            max_size=1, x=x, y=y, **kwargs
        )  # this will include any arguments, including scale
        self.append(self.tilegrid)  # add the bitmap's tilegrid to the group

        # Update bounding_box values.  Note: To be consistent with label.py,
        # this is the bounding box for the text only, not including the background.

        self._bounding_box = (
            self.tilegrid.x,
            self.tilegrid.y,
            tight_box_x,
            tight_box_y,
        )

        self._anchored_position = anchored_position
        self.anchor_point = anchor_point
        self.anchored_position = (
            self._anchored_position
        )  # sets anchored_position with setter after bitmap is created

    @staticmethod
    def _line_spacing_ypixels(font, line_spacing):
        # Note: Scale is not implemented at this time, any scaling is pushed up to the Group level
        return_value = int(line_spacing * font.get_bounding_box()[1])
        return return_value

    def _text_bounding_box(self, text, font, line_spacing, background_tight=False):

        # This empirical approach checks several glyphs for maximum ascender and descender height
        # (consistent with label.py)
        glyphs = "M j'"  # choose glyphs with highest ascender and lowest
        # descender, will depend upon font used

        try:
            self._font.load_glyphs(text + glyphs)
        except AttributeError:
            # ignore if font does not have load_glyphs
            pass

        ascender_max = descender_max = 0
        for char in glyphs:
            this_glyph = font.get_glyph(ord(char))
            if this_glyph:
                ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
                descender_max = max(descender_max, -this_glyph.dy)

        lines = 1

        xposition = (
            x_start
        ) = yposition = y_start = 0  # starting x and y position (left margin)

        left = None
        right = x_start
        top = bottom = y_start

        y_offset_tight = int((font.get_glyph(ord("M")).height) / 2)
        # this needs to be reviewed (also in label.py), since it doesn't respond
        # properly to the number of newlines.

        newline = False

        for char in text:

            if char == "\n":  # newline
                newline = True

            else:

                my_glyph = font.get_glyph(ord(char))

                if my_glyph is None:  # Error checking: no glyph found
                    print("Glyph not found: {}".format(repr(char)))
                else:
                    if newline:
                        newline = False
                        xposition = x_start  # reset to left column
                        yposition = yposition + self._line_spacing_ypixels(
                            font, line_spacing
                        )  # Add a newline
                        lines += 1
                    if xposition == x_start:
                        if left is None:
                            left = my_glyph.dx
                        else:
                            left = min(left, my_glyph.dx)
                    xright = xposition + my_glyph.width + my_glyph.dx
                    xposition += my_glyph.shift_x

                    right = max(right, xposition, xright)

                    if yposition == y_start:  # first line, find the Ascender height
                        top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight)
                    bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight)

        if left is None:
            left = 0

        final_box_width = right - left
        if background_tight:
            final_box_height = bottom - top
            final_y_offset = -top + y_offset_tight

        else:
            final_box_height = (lines - 1) * self._line_spacing_ypixels(
                font, line_spacing
            ) + (ascender_max + descender_max)
            final_y_offset = ascender_max

        return (final_box_width, final_box_height, left, final_y_offset)

    # pylint: disable=too-many-nested-blocks
    def _place_text(
        self,
        bitmap,
        text,
        font,
        line_spacing,
        xposition,
        yposition,
        text_palette_index=1,
        background_palette_index=0,
        print_only_pixels=True,  # print_only_pixels = True: only update the bitmap where the glyph
        # pixel color is > 0.  This is especially useful for script fonts where glyph
        # bounding boxes overlap
        # Set `print_only_pixels=False` to write all pixels
    ):
        # placeText - Writes text into a bitmap at the specified location.
        #
        # Verify paletteIndex is working properly with * operator, especially
        # if accommodating multicolored fonts
        #
        # Note: Scale is not implemented at this time, is pushed up to Group level

        bitmap_width = bitmap.width
        bitmap_height = bitmap.height

        x_start = xposition  # starting x position (left margin)
        y_start = yposition

        left = None
        right = x_start
        top = bottom = y_start

        for char in text:

            if char == "\n":  # newline
                xposition = x_start  # reset to left column
                yposition = yposition + self._line_spacing_ypixels(
                    font, line_spacing
                )  # Add a newline

            else:

                my_glyph = font.get_glyph(ord(char))

                if my_glyph is None:  # Error checking: no glyph found
                    print("Glyph not found: {}".format(repr(char)))
                else:
                    if xposition == x_start:
                        if left is None:
                            left = my_glyph.dx
                        else:
                            left = min(left, my_glyph.dx)

                    right = max(
                        right,
                        xposition + my_glyph.shift_x,
                        xposition + my_glyph.width + my_glyph.dx,
                    )
                    if yposition == y_start:  # first line, find the Ascender height
                        top = min(top, -my_glyph.height - my_glyph.dy)
                    bottom = max(bottom, yposition - my_glyph.dy)

                    glyph_offset_x = (
                        my_glyph.tile_index * my_glyph.width
                    )  # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
                    # for BDF loaded fonts, this should equal 0

                    for y in range(my_glyph.height):
                        for x in range(my_glyph.width):
                            x_placement = x + xposition + my_glyph.dx
                            y_placement = y + yposition - my_glyph.height - my_glyph.dy

                            if (bitmap_width > x_placement >= 0) and (
                                bitmap_height > y_placement >= 0
                            ):

                                # Allows for remapping the bitmap indexes using paletteIndex
                                # for background and text.
                                palette_indexes = (
                                    background_palette_index,
                                    text_palette_index,
                                )

                                this_pixel_color = palette_indexes[
                                    my_glyph.bitmap[
                                        y * my_glyph.bitmap.width + x + glyph_offset_x
                                    ]
                                ]

                                if not print_only_pixels or this_pixel_color > 0:
                                    # write all characters if printOnlyPixels = False,
                                    # or if thisPixelColor is > 0
                                    bitmap[
                                        y_placement * bitmap_width + x_placement
                                    ] = this_pixel_color
                            elif y_placement > bitmap_height:
                                break

                    xposition = xposition + my_glyph.shift_x

        return (left, top, right - left, bottom - top)  # bounding_box

    @property
    def bounding_box(self):
        """An (x, y, w, h) tuple that completely covers all glyphs. The
        first two numbers are offset from the x, y origin of this group"""
        return self._bounding_box

    @property
    def line_spacing(self):
        """The amount of space between lines of text, in multiples of the font's
        bounding-box height. (E.g. 1.0 is the bounding-box height)"""
        return self._line_spacing

    @line_spacing.setter
    def line_spacing(self, new_line_spacing):
        raise RuntimeError(
            "line_spacing is immutable for bitmap_label.py; use label.py for mutable line_spacing"
        )

    @property
    def color(self):
        """Color of the text as an RGB hex number."""
        return self._color

    @color.setter
    def color(self, new_color):
        self._color = new_color
        if new_color is not None:
            self.palette[1] = new_color
            self.palette.make_opaque(1)
        else:
            self.palette[1] = 0
            self.palette.make_transparent(1)

    @property
    def background_color(self):
        """Color of the background as an RGB hex number."""
        return self._background_color

    @background_color.setter
    def background_color(self, new_color):
        self._background_color = new_color
        if new_color is not None:
            self.palette[0] = new_color
            self.palette.make_opaque(0)
        else:
            self.palette[0] = 0
            self.palette.make_transparent(0)

    @property
    def text(self):
        """Text to displayed."""
        return self._text

    @text.setter
    def text(self, new_text):
        raise RuntimeError(
            "text is immutable for bitmap_label.py; use label.py library for mutable text"
        )

    @property
    def font(self):
        """Font to use for text display."""
        return self.font

    @font.setter
    def font(self, new_font):
        raise RuntimeError(
            "font is immutable for bitmap_label.py; use label.py library for mutable font"
        )

    @property
    def anchor_point(self):
        """Point that anchored_position moves relative to.
           Tuple with decimal percentage of width and height.
           (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)"""
        return self._anchor_point

    @anchor_point.setter
    def anchor_point(self, new_anchor_point):
        self._anchor_point = new_anchor_point
        self.anchored_position = (
            self._anchored_position
        )  # update the anchored_position using setter

    @property
    def anchored_position(self):
        """Position relative to the anchor_point. Tuple containing x,y
           pixel coordinates."""
        return self._anchored_position

    @anchored_position.setter
    def anchored_position(self, new_position):
        self._anchored_position = new_position

        # Set anchored_position
        if (self._anchor_point is not None) and (self._anchored_position is not None):
            self.x = int(
                new_position[0]
                - (self._bounding_box[0] * self._scale)
                - round(self._anchor_point[0] * (self._bounding_box[2] * self._scale))
            )
            self.y = int(
                new_position[1]
                - (self._bounding_box[1] * self._scale)
                - round(self._anchor_point[1] * self._bounding_box[3] * self._scale)
            )
