# -*- coding: utf-8 -*-

"""Pen to rasterize paths with FreeType."""

__all__ = ["FreeTypePen"]

import os
import ctypes
import platform
import subprocess
import collections
import math

import freetype
from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
from freetype.ft_types import FT_Pos
from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
from freetype.ft_enums import (
    FT_OUTLINE_NONE,
    FT_OUTLINE_EVEN_ODD_FILL,
    FT_PIXEL_MODE_GRAY,
    FT_CURVE_TAG_ON,
    FT_CURVE_TAG_CONIC,
    FT_CURVE_TAG_CUBIC,
)
from freetype.ft_errors import FT_Exception

from fontTools.pens.basePen import BasePen, PenError
from fontTools.misc.roundTools import otRound
from fontTools.misc.transform import Transform

Contour = collections.namedtuple("Contour", ("points", "tags"))


class FreeTypePen(BasePen):
    """Pen to rasterize paths with FreeType. Requires `freetype-py` module.

    Constructs ``FT_Outline`` from the paths, and renders it within a bitmap
    buffer.

    For ``array()`` and ``show()``, `numpy` and `matplotlib` must be installed.
    For ``image()``, `Pillow` is required. Each module is lazily loaded when the
    corresponding method is called.

    Args:
        glyphSet: a dictionary of drawable glyph objects keyed by name
            used to resolve component references in composite glyphs.

    :Examples:
        If `numpy` and `matplotlib` is available, the following code will
        show the glyph image of `fi` in a new window::

            from fontTools.ttLib import TTFont
            from fontTools.pens.freetypePen import FreeTypePen
            from fontTools.misc.transform import Offset
            pen = FreeTypePen(None)
            font = TTFont('SourceSansPro-Regular.otf')
            glyph = font.getGlyphSet()['fi']
            glyph.draw(pen)
            width, ascender, descender = glyph.width, font['OS/2'].usWinAscent, -font['OS/2'].usWinDescent
            height = ascender - descender
            pen.show(width=width, height=height, transform=Offset(0, -descender))

        Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen::

            import uharfbuzz as hb
            from fontTools.pens.freetypePen import FreeTypePen
            from fontTools.pens.transformPen import TransformPen
            from fontTools.misc.transform import Offset

            en1, en2, ar, ja = 'Typesetting', 'Jeff', 'صف الحروف', 'たいぷせっと'
            for text, font_path, direction, typo_ascender, typo_descender, vhea_ascender, vhea_descender, contain, features in (
                (en1, 'NotoSans-Regular.ttf',       'ltr', 2189, -600, None, None, False, {"kern": True, "liga": True}),
                (en2, 'NotoSans-Regular.ttf',       'ltr', 2189, -600, None, None, True,  {"kern": True, "liga": True}),
                (ar,  'NotoSansArabic-Regular.ttf', 'rtl', 1374, -738, None, None, False, {"kern": True, "liga": True}),
                (ja,  'NotoSansJP-Regular.otf',     'ltr', 880,  -120, 500,  -500, False, {"palt": True, "kern": True}),
                (ja,  'NotoSansJP-Regular.otf',     'ttb', 880,  -120, 500,  -500, False, {"vert": True, "vpal": True, "vkrn": True})
            ):
                blob = hb.Blob.from_file_path(font_path)
                face = hb.Face(blob)
                font = hb.Font(face)
                buf = hb.Buffer()
                buf.direction = direction
                buf.add_str(text)
                buf.guess_segment_properties()
                hb.shape(font, buf, features)

                x, y = 0, 0
                pen = FreeTypePen(None)
                for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
                    gid = info.codepoint
                    transformed = TransformPen(pen, Offset(x + pos.x_offset, y + pos.y_offset))
                    font.draw_glyph_with_pen(gid, transformed)
                    x += pos.x_advance
                    y += pos.y_advance

                offset, width, height = None, None, None
                if direction in ('ltr', 'rtl'):
                    offset = (0, -typo_descender)
                    width  = x
                    height = typo_ascender - typo_descender
                else:
                    offset = (-vhea_descender, -y)
                    width  = vhea_ascender - vhea_descender
                    height = -y
                pen.show(width=width, height=height, transform=Offset(*offset), contain=contain)

        For Jupyter Notebook, the rendered image will be displayed in a cell if
        you replace ``show()`` with ``image()`` in the examples.
    """

    def __init__(self, glyphSet):
        BasePen.__init__(self, glyphSet)
        self.contours = []

    def outline(self, transform=None, evenOdd=False):
        """Converts the current contours to ``FT_Outline``.

        Args:
            transform: An optional 6-tuple containing an affine transformation,
                or a ``Transform`` object from the ``fontTools.misc.transform``
                module.
            evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
        """
        transform = transform or Transform()
        if not hasattr(transform, "transformPoint"):
            transform = Transform(*transform)
        n_contours = len(self.contours)
        n_points = sum((len(contour.points) for contour in self.contours))
        points = []
        for contour in self.contours:
            for point in contour.points:
                point = transform.transformPoint(point)
                points.append(
                    FT_Vector(
                        FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64))
                    )
                )
        tags = []
        for contour in self.contours:
            for tag in contour.tags:
                tags.append(tag)
        contours = []
        contours_sum = 0
        for contour in self.contours:
            contours_sum += len(contour.points)
            contours.append(contours_sum - 1)
        flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
        return FT_Outline(
            (ctypes.c_short)(n_contours),
            (ctypes.c_short)(n_points),
            (FT_Vector * n_points)(*points),
            (ctypes.c_ubyte * n_points)(*tags),
            (ctypes.c_short * n_contours)(*contours),
            (ctypes.c_int)(flags),
        )

    def buffer(
        self, width=None, height=None, transform=None, contain=False, evenOdd=False
    ):
        """Renders the current contours within a bitmap buffer.

        Args:
            width: Image width of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            height: Image height of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            transform: An optional 6-tuple containing an affine transformation,
                or a ``Transform`` object from the ``fontTools.misc.transform``
                module. The bitmap size is not affected by this matrix.
            contain: If ``True``, the image size will be automatically expanded
                so that it fits to the bounding box of the paths. Useful for
                rendering glyphs with negative sidebearings without clipping.
            evenOdd: Pass ``True`` for even-odd fill instead of non-zero.

        Returns:
            A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes``
            object of the resulted bitmap and ``size`` is a 2-tuple of its
            dimension.

        :Notes:
            The image size should always be given explicitly if you need to get
            a proper glyph image. When ``width`` and ``height`` are omitted, it
            forcifully fits to the bounding box and the side bearings get
            cropped. If you pass ``0`` to both ``width`` and ``height`` and set
            ``contain`` to ``True``, it expands to the bounding box while
            maintaining the origin of the contours, meaning that LSB will be
            maintained but RSB won’t. The difference between the two becomes
            more obvious when rotate or skew transformation is applied.

        :Example:
            .. code-block::

                >> pen = FreeTypePen(None)
                >> glyph.draw(pen)
                >> buf, size = pen.buffer(width=500, height=1000)
                >> type(buf), len(buf), size
                (<class 'bytes'>, 500000, (500, 1000))

        """
        transform = transform or Transform()
        if not hasattr(transform, "transformPoint"):
            transform = Transform(*transform)
        contain_x, contain_y = contain or width is None, contain or height is None
        if contain_x or contain_y:
            dx, dy = transform.dx, transform.dy
            bbox = self.bbox
            p1, p2, p3, p4 = (
                transform.transformPoint((bbox[0], bbox[1])),
                transform.transformPoint((bbox[2], bbox[1])),
                transform.transformPoint((bbox[0], bbox[3])),
                transform.transformPoint((bbox[2], bbox[3])),
            )
            px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1])
            if contain_x:
                if width is None:
                    dx = dx - min(*px)
                    width = max(*px) - min(*px)
                else:
                    dx = dx - min(min(*px), 0.0)
                    width = max(width, max(*px) - min(min(*px), 0.0))
            if contain_y:
                if height is None:
                    dy = dy - min(*py)
                    height = max(*py) - min(*py)
                else:
                    dy = dy - min(min(*py), 0.0)
                    height = max(height, max(*py) - min(min(*py), 0.0))
            transform = Transform(*transform[:4], dx, dy)
        width, height = math.ceil(width), math.ceil(height)
        buf = ctypes.create_string_buffer(width * height)
        bitmap = FT_Bitmap(
            (ctypes.c_int)(height),
            (ctypes.c_int)(width),
            (ctypes.c_int)(width),
            (ctypes.POINTER(ctypes.c_ubyte))(buf),
            (ctypes.c_short)(256),
            (ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
            (ctypes.c_char)(0),
            (ctypes.c_void_p)(None),
        )
        outline = self.outline(transform=transform, evenOdd=evenOdd)
        err = FT_Outline_Get_Bitmap(
            freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)
        )
        if err != 0:
            raise FT_Exception(err)
        return buf.raw, (width, height)

    def array(
        self, width=None, height=None, transform=None, contain=False, evenOdd=False
    ):
        """Returns the rendered contours as a numpy array. Requires `numpy`.

        Args:
            width: Image width of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            height: Image height of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            transform: An optional 6-tuple containing an affine transformation,
                or a ``Transform`` object from the ``fontTools.misc.transform``
                module. The bitmap size is not affected by this matrix.
            contain: If ``True``, the image size will be automatically expanded
                so that it fits to the bounding box of the paths. Useful for
                rendering glyphs with negative sidebearings without clipping.
            evenOdd: Pass ``True`` for even-odd fill instead of non-zero.

        Returns:
            A ``numpy.ndarray`` object with a shape of ``(height, width)``.
            Each element takes a value in the range of ``[0.0, 1.0]``.

        :Notes:
            The image size should always be given explicitly if you need to get
            a proper glyph image. When ``width`` and ``height`` are omitted, it
            forcifully fits to the bounding box and the side bearings get
            cropped. If you pass ``0`` to both ``width`` and ``height`` and set
            ``contain`` to ``True``, it expands to the bounding box while
            maintaining the origin of the contours, meaning that LSB will be
            maintained but RSB won’t. The difference between the two becomes
            more obvious when rotate or skew transformation is applied.

        :Example:
            .. code-block::

                >> pen = FreeTypePen(None)
                >> glyph.draw(pen)
                >> arr = pen.array(width=500, height=1000)
                >> type(a), a.shape
                (<class 'numpy.ndarray'>, (1000, 500))
        """
        import numpy as np

        buf, size = self.buffer(
            width=width,
            height=height,
            transform=transform,
            contain=contain,
            evenOdd=evenOdd,
        )
        return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0

    def show(
        self, width=None, height=None, transform=None, contain=False, evenOdd=False
    ):
        """Plots the rendered contours with `pyplot`. Requires `numpy` and
        `matplotlib`.

        Args:
            width: Image width of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            height: Image height of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            transform: An optional 6-tuple containing an affine transformation,
                or a ``Transform`` object from the ``fontTools.misc.transform``
                module. The bitmap size is not affected by this matrix.
            contain: If ``True``, the image size will be automatically expanded
                so that it fits to the bounding box of the paths. Useful for
                rendering glyphs with negative sidebearings without clipping.
            evenOdd: Pass ``True`` for even-odd fill instead of non-zero.

        :Notes:
            The image size should always be given explicitly if you need to get
            a proper glyph image. When ``width`` and ``height`` are omitted, it
            forcifully fits to the bounding box and the side bearings get
            cropped. If you pass ``0`` to both ``width`` and ``height`` and set
            ``contain`` to ``True``, it expands to the bounding box while
            maintaining the origin of the contours, meaning that LSB will be
            maintained but RSB won’t. The difference between the two becomes
            more obvious when rotate or skew transformation is applied.

        :Example:
            .. code-block::

                >> pen = FreeTypePen(None)
                >> glyph.draw(pen)
                >> pen.show(width=500, height=1000)
        """
        from matplotlib import pyplot as plt

        a = self.array(
            width=width,
            height=height,
            transform=transform,
            contain=contain,
            evenOdd=evenOdd,
        )
        plt.imshow(a, cmap="gray_r", vmin=0, vmax=1)
        plt.show()

    def image(
        self, width=None, height=None, transform=None, contain=False, evenOdd=False
    ):
        """Returns the rendered contours as a PIL image. Requires `Pillow`.
        Can be used to display a glyph image in Jupyter Notebook.

        Args:
            width: Image width of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            height: Image height of the bitmap in pixels. If omitted, it
                automatically fits to the bounding box of the contours.
            transform: An optional 6-tuple containing an affine transformation,
                or a ``Transform`` object from the ``fontTools.misc.transform``
                module. The bitmap size is not affected by this matrix.
            contain: If ``True``, the image size will be automatically expanded
                so that it fits to the bounding box of the paths. Useful for
                rendering glyphs with negative sidebearings without clipping.
            evenOdd: Pass ``True`` for even-odd fill instead of non-zero.

        Returns:
            A ``PIL.image`` object. The image is filled in black with alpha
            channel obtained from the rendered bitmap.

        :Notes:
            The image size should always be given explicitly if you need to get
            a proper glyph image. When ``width`` and ``height`` are omitted, it
            forcifully fits to the bounding box and the side bearings get
            cropped. If you pass ``0`` to both ``width`` and ``height`` and set
            ``contain`` to ``True``, it expands to the bounding box while
            maintaining the origin of the contours, meaning that LSB will be
            maintained but RSB won’t. The difference between the two becomes
            more obvious when rotate or skew transformation is applied.

        :Example:
            .. code-block::

                >> pen = FreeTypePen(None)
                >> glyph.draw(pen)
                >> img = pen.image(width=500, height=1000)
                >> type(img), img.size
                (<class 'PIL.Image.Image'>, (500, 1000))
        """
        from PIL import Image

        buf, size = self.buffer(
            width=width,
            height=height,
            transform=transform,
            contain=contain,
            evenOdd=evenOdd,
        )
        img = Image.new("L", size, 0)
        img.putalpha(Image.frombuffer("L", size, buf))
        return img

    @property
    def bbox(self):
        """Computes the exact bounding box of an outline.

        Returns:
            A tuple of ``(xMin, yMin, xMax, yMax)``.
        """
        bbox = FT_BBox()
        outline = self.outline()
        FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox))
        return (bbox.xMin / 64.0, bbox.yMin / 64.0, bbox.xMax / 64.0, bbox.yMax / 64.0)

    @property
    def cbox(self):
        """Returns an outline's ‘control box’.

        Returns:
            A tuple of ``(xMin, yMin, xMax, yMax)``.
        """
        cbox = FT_BBox()
        outline = self.outline()
        FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox))
        return (cbox.xMin / 64.0, cbox.yMin / 64.0, cbox.xMax / 64.0, cbox.yMax / 64.0)

    def _moveTo(self, pt):
        contour = Contour([], [])
        self.contours.append(contour)
        contour.points.append(pt)
        contour.tags.append(FT_CURVE_TAG_ON)

    def _lineTo(self, pt):
        if not (self.contours and len(self.contours[-1].points) > 0):
            raise PenError("Contour missing required initial moveTo")
        contour = self.contours[-1]
        contour.points.append(pt)
        contour.tags.append(FT_CURVE_TAG_ON)

    def _curveToOne(self, p1, p2, p3):
        if not (self.contours and len(self.contours[-1].points) > 0):
            raise PenError("Contour missing required initial moveTo")
        t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON
        contour = self.contours[-1]
        for p, t in ((p1, t1), (p2, t2), (p3, t3)):
            contour.points.append(p)
            contour.tags.append(t)

    def _qCurveToOne(self, p1, p2):
        if not (self.contours and len(self.contours[-1].points) > 0):
            raise PenError("Contour missing required initial moveTo")
        t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON
        contour = self.contours[-1]
        for p, t in ((p1, t1), (p2, t2)):
            contour.points.append(p)
            contour.tags.append(t)
