# TODO Windows Vista: need to call SetProcessDPIAware?  May affect GDI+ calls as well as font.

import math
import warnings

from sys import byteorder
from typing import Optional

import pyglet
from pyglet.font import base
from pyglet.font import win32query
import pyglet.image
from pyglet.libs.win32.constants import *
from pyglet.libs.win32.context_managers import device_context
from pyglet.libs.win32.types import *
from pyglet.libs.win32 import _gdi32 as gdi32, _user32 as user32
from pyglet.libs.win32 import _kernel32 as kernel32
from pyglet.util import asbytes

_debug_font = pyglet.options['debug_font']


def str_ucs2(text):
    if byteorder == 'big':
        text = text.encode('utf_16_be')
    else:
        text = text.encode('utf_16_le')   # explicit endian avoids BOM
    return create_string_buffer(text + '\0')


_debug_dir = 'debug_font'


def _debug_filename(base, extension):
    import os
    if not os.path.exists(_debug_dir):
        os.makedirs(_debug_dir)
    name = f'{os.path.join(_debug_dir, base)}-{{0}}.{{1}}'
    num = 1
    while os.path.exists(name.format(num, extension)):
        num += 1
    return name.format(num, extension)


def _debug_image(image, name):
    filename = _debug_filename(name, 'png')
    image.save(filename)
    _debug(f'Saved image {image} to {filename}')


_debug_logfile = None


def _debug(msg):
    global _debug_logfile
    if not _debug_logfile:
        _debug_logfile = open(_debug_filename('log', 'txt'), 'wt')
    _debug_logfile.write(msg + '\n')


class Win32GlyphRenderer(base.GlyphRenderer):

    def __init__(self, font):
        self._bitmap = None
        self._dc = None
        self._bitmap_rect = None
        super(Win32GlyphRenderer, self).__init__(font)
        self.font = font

        # Pessimistically round up width and height to 4 byte alignment
        width = font.max_glyph_width
        height = font.ascent - font.descent
        width = (width | 0x3) + 1
        height = (height | 0x3) + 1
        self._create_bitmap(width, height)

        gdi32.SelectObject(self._dc, self.font.hfont)

    def _create_bitmap(self, width, height) -> None:
        pass

    def render(self, text: str) -> pyglet.font.base.Glyph:
        raise NotImplementedError('abstract')


class GDIGlyphRenderer(Win32GlyphRenderer):

    def __del__(self):
        try:
            if self._dc:
                gdi32.DeleteDC(self._dc)
            if self._bitmap:
                gdi32.DeleteObject(self._bitmap)
        except:
            pass

    def render(self, text: str) -> pyglet.font.base.Glyph:
        # Attempt to get ABC widths (only for TrueType)
        abc = ABC()
        if gdi32.GetCharABCWidthsW(
            self._dc,
            ord(text), ord(text), byref(abc)
        ):
            width = abc.abcB
            lsb = abc.abcA
            advance = abc.abcA + abc.abcB + abc.abcC

        else:
            width_buf = c_int()
            gdi32.GetCharWidth32W(
                self._dc, ord(text), ord(text), byref(width_buf))

            width = width_buf.value
            lsb = 0
            advance = width

        # Can't get glyph-specific dimensions, use whole line-height.
        height = self._bitmap_height
        image = self._get_image(text, width, height, lsb)

        glyph = self.font.create_glyph(image)
        glyph.set_bearings(-self.font.descent, lsb, advance)

        if _debug_font:
            _debug(f'{self}.render({text})')
            _debug(f'abc.abcA = {abc.abcA}')
            _debug(f'abc.abcB = {abc.abcB}')
            _debug(f'abc.abcC = {abc.abcC}')
            _debug(f'width = {width}')
            _debug(f'height = {height}')
            _debug(f'lsb = {lsb}')
            _debug(f'advance = {advance}')
            _debug_image(image, f'glyph_{text}')
            _debug_image(self.font.textures[0], f'tex_{text}')

        return glyph

    def _get_image(self, text: str, width: int, height: int, lsb: int) -> pyglet.image.ImageData:
        # There's no such thing as a greyscale bitmap format in GDI.  We can
        # create an 8-bit palette bitmap with 256 shades of grey, but
        # unfortunately antialiasing will not work on such a bitmap.  So, we
        # use a 32-bit bitmap and use the red channel as OpenGL's alpha.

        gdi32.SelectObject(self._dc, self._bitmap)
        gdi32.SelectObject(self._dc, self.font.hfont)
        gdi32.SetBkColor(self._dc, 0x0)
        gdi32.SetTextColor(self._dc, 0x00ffffff)
        gdi32.SetBkMode(self._dc, OPAQUE)

        # Draw to DC
        user32.FillRect(self._dc, byref(self._bitmap_rect), self._black)
        gdi32.ExtTextOutA(
            self._dc, -lsb, 0, 0, None, text,
            len(text), None)

        gdi32.GdiFlush()

        # Create glyph object and copy bitmap data to texture
        image = pyglet.image.ImageData(
            width, height,
            'AXXX', self._bitmap_data, self._bitmap_rect.right * 4)

        return image

    def _create_bitmap(self, width: int, height: int) -> None:
        self._black = gdi32.GetStockObject(BLACK_BRUSH)
        self._white = gdi32.GetStockObject(WHITE_BRUSH)

        if self._dc:
            gdi32.ReleaseDC(self._dc)
        if self._bitmap:
            gdi32.DeleteObject(self._bitmap)

        pitch = width * 4
        data = POINTER(c_byte * (height * pitch))()
        info = BITMAPINFO()
        info.bmiHeader.biSize = sizeof(info.bmiHeader)
        info.bmiHeader.biWidth = width
        info.bmiHeader.biHeight = height
        info.bmiHeader.biPlanes = 1
        info.bmiHeader.biBitCount = 32
        info.bmiHeader.biCompression = BI_RGB

        self._dc = gdi32.CreateCompatibleDC(None)
        self._bitmap = gdi32.CreateDIBSection(
            None,
            byref(info), DIB_RGB_COLORS, byref(data), None,
            0)
        # Spookiness: the above line causes a "not enough storage" error,
        # even though that error cannot be generated according to docs,
        # and everything works fine anyway.  Call SetLastError to clear it.
        kernel32.SetLastError(0)

        self._bitmap_data = data.contents
        self._bitmap_rect = RECT()
        self._bitmap_rect.left = 0
        self._bitmap_rect.right = width
        self._bitmap_rect.top = 0
        self._bitmap_rect.bottom = height
        self._bitmap_height = height

        if _debug_font:
            _debug(f'{self}._create_dc({width}, {height})')
            _debug(f'_dc = {self._dc}')
            _debug(f'_bitmap = {self._bitmap}')
            _debug(f'pitch = {pitch}')
            _debug(f'info.bmiHeader.biSize = {info.bmiHeader.biSize}')


class Win32Font(base.Font):

    glyph_renderer_class = GDIGlyphRenderer

    def __init__(
            self,
            name: str, size: int,
            bold: bool = False, italic: bool = False, stretch: bool = False,
            dpi: Optional[float] = None
    ):
        super(Win32Font, self).__init__()

        self.logfont = self.get_logfont(name, size, bold, italic, dpi)
        self.hfont = gdi32.CreateFontIndirectW(byref(self.logfont))

        # Create a dummy DC for coordinate mapping
        with device_context(None) as dc:
            metrics = TEXTMETRIC()
            gdi32.SelectObject(dc, self.hfont)
            gdi32.GetTextMetricsA(dc, byref(metrics))
            self.ascent = metrics.tmAscent
            self.descent = -metrics.tmDescent
            self.max_glyph_width = metrics.tmMaxCharWidth

    def __del__(self):
        gdi32.DeleteObject(self.hfont)

    @staticmethod
    def get_logfont(name: str, size: float, bold: bool, italic: bool, dpi: Optional[float] = None) -> LOGFONTW:
        """Get a raw Win32 :py:class:`.LOGFONTW` struct for the given arguments.

        Args:
            name: The name of the font
            size: The font size in points
            bold: Whether to request the font as bold
            italic: Whether to request the font as italic
            dpi: The screen dpi

        Returns:
            LOGFONTW: a ctypes binding of a Win32 LOGFONTW struct
        """

        # Create a dummy DC for coordinate mapping
        with device_context(None) as dc:

            # Default to 96 DPI unless otherwise specified
            if dpi is None:
                dpi = 96
            log_pixels_y = dpi

            # Create LOGFONTW font description struct
            logfont = LOGFONTW()

            # Convert point size to actual device pixels
            logfont.lfHeight = int(-size * log_pixels_y // 72)

            # Configure the LOGFONTW's font properties
            if bold:
                logfont.lfWeight = FW_BOLD
            else:
                logfont.lfWeight = FW_NORMAL
            logfont.lfItalic = italic
            logfont.lfFaceName = name
            logfont.lfQuality = ANTIALIASED_QUALITY

        return logfont

    @classmethod
    def have_font(cls, name) -> bool:
        # [ ] add support for loading raster fonts
        return win32query.have_font(name)

    @classmethod
    def add_font_data(cls, data):
        numfonts = c_uint32()
        _handle = gdi32.AddFontMemResourceEx(data, len(data), 0, byref(numfonts))

        # None means a null handle was returned, ie something went wrong
        if _handle is None:
            raise ctypes.WinError()



# --- GDI+ font rendering ---
from pyglet.image.codecs.gdiplus import PixelFormat32bppARGB, gdiplus, Rect
from pyglet.image.codecs.gdiplus import ImageLockModeRead, BitmapData

DriverStringOptionsCmapLookup = 1
DriverStringOptionsRealizedAdvance = 4
TextRenderingHintAntiAlias = 4
TextRenderingHintAntiAliasGridFit = 3

StringFormatFlagsDirectionRightToLeft = 0x00000001
StringFormatFlagsDirectionVertical = 0x00000002
StringFormatFlagsNoFitBlackBox = 0x00000004
StringFormatFlagsDisplayFormatControl = 0x00000020
StringFormatFlagsNoFontFallback = 0x00000400
StringFormatFlagsMeasureTrailingSpaces = 0x00000800
StringFormatFlagsNoWrap = 0x00001000
StringFormatFlagsLineLimit = 0x00002000
StringFormatFlagsNoClip = 0x00004000


class Rectf(ctypes.Structure):
    _fields_ = [
        ('x', ctypes.c_float),
        ('y', ctypes.c_float),
        ('width', ctypes.c_float),
        ('height', ctypes.c_float),
    ]


class GDIPlusGlyphRenderer(Win32GlyphRenderer):
    def __del__(self):
        try:
            if self._matrix:
                res = gdiplus.GdipDeleteMatrix(self._matrix)
            if self._brush:
                res = gdiplus.GdipDeleteBrush(self._brush)
            if self._graphics:
                res = gdiplus.GdipDeleteGraphics(self._graphics)
            if self._bitmap:
                res = gdiplus.GdipDisposeImage(self._bitmap)
            if self._dc:
                res = user32.ReleaseDC(0, self._dc)
        except:
            pass

    def _create_bitmap(self, width, height):
        self._data = (BYTE * (4 * width * height))()
        self._bitmap = ctypes.c_void_p()
        self._format = PixelFormat32bppARGB
        gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
            self._format, self._data, ctypes.byref(self._bitmap))

        self._graphics = ctypes.c_void_p()
        gdiplus.GdipGetImageGraphicsContext(self._bitmap,
            ctypes.byref(self._graphics))
        gdiplus.GdipSetPageUnit(self._graphics, UnitPixel)

        self._dc = user32.GetDC(0)
        gdi32.SelectObject(self._dc, self.font.hfont)

        gdiplus.GdipSetTextRenderingHint(self._graphics,
            TextRenderingHintAntiAliasGridFit)

        self._brush = ctypes.c_void_p()
        gdiplus.GdipCreateSolidFill(0xffffffff, ctypes.byref(self._brush))

        self._matrix = ctypes.c_void_p()
        gdiplus.GdipCreateMatrix(ctypes.byref(self._matrix))

        self._flags = (DriverStringOptionsCmapLookup |
                       DriverStringOptionsRealizedAdvance)

        self._rect = Rect(0, 0, width, height)

        self._bitmap_height = height

    def render(self, text):
        ch = ctypes.create_unicode_buffer(text)
        len_ch = len(text)

        # Layout rectangle; not clipped against so not terribly important.
        width = 10000
        height = self._bitmap_height
        rect = Rectf(0, self._bitmap_height
                        - self.font.ascent + self.font.descent,
                     width, height)

        # Set up GenericTypographic with 1 character measure range
        generic = ctypes.c_void_p()
        gdiplus.GdipStringFormatGetGenericTypographic(ctypes.byref(generic))
        fmt = ctypes.c_void_p()
        gdiplus.GdipCloneStringFormat(generic, ctypes.byref(fmt))
        gdiplus.GdipDeleteStringFormat(generic)

        # --- Measure advance

        # XXX HACK HACK HACK
        # Windows GDI+ is a filthy broken toy.  No way to measure the bounding
        # box of a string, or to obtain LSB.  What a joke.
        #
        # For historical note, GDI cannot be used because it cannot composite
        # into a bitmap with alpha.
        #
        # It looks like MS have abandoned GDI and GDI+ and are finally
        # supporting accurate text measurement with alpha composition in .NET
        # 2.0 (WinForms) via the TextRenderer class; this has no C interface
        # though, so we're entirely screwed.
        #
        # So anyway, we first try to get the width with GdipMeasureString.
        # Then if it's a TrueType font, we use GetCharABCWidthsW to get the
        # correct LSB. If it's a negative LSB, we move the layoutRect `rect`
        # to the right so that the whole glyph is rendered on the surface.
        # For positive LSB, we let the renderer render the correct white
        # space and we don't pass the LSB info to the Glyph.set_bearings

        bbox = Rectf()
        flags = (StringFormatFlagsMeasureTrailingSpaces |
                 StringFormatFlagsNoClip |
                 StringFormatFlagsNoFitBlackBox)
        gdiplus.GdipSetStringFormatFlags(fmt, flags)
        gdiplus.GdipMeasureString(self._graphics,
                                  ch,
                                  len_ch,
                                  self.font._gdipfont,
                                  ctypes.byref(rect),
                                  fmt,
                                  ctypes.byref(bbox),
                                  None,
                                  None)

        # We only care about the advance from this whole thing.
        advance = int(math.ceil(bbox.width))

        # GDI functions only work for a single character so we transform
        # grapheme \r\n into \r
        if text == '\r\n':
            text = '\r'

        # XXX END HACK HACK HACK

        abc = ABC()
        width = 0
        lsb = 0
        ttf_font = True
        # Use GDI to get code points for the text passed. This is almost always 1.
        # For special unicode characters it may be comprised of 2+ codepoints. Get the width/lsb of each.
        # Function only works on TTF fonts.
        for codepoint in [ord(c) for c in text]:
            if gdi32.GetCharABCWidthsW(self._dc, codepoint, codepoint, byref(abc)):
                lsb += abc.abcA
                width += abc.abcB

                if lsb < 0:
                    # Negative LSB: we shift the layout rect to the right
                    # Otherwise we will cut the left part of the glyph
                    rect.x = -lsb
                    width -= lsb
                else:
                    width += lsb
            else:
                ttf_font = False
                break

        # Almost always a TTF font. Haven't seen a modern font that GetCharABCWidthsW fails on.
        # For safety, just use the advance as the width.
        if not ttf_font:
            width = advance

            # This hack bumps up the width if the font is italic;
            # this compensates for some common fonts.  It's also a stupid
            # waste of texture memory.
            if self.font.italic:
                width += width // 2
                # Do not enlarge more than the _rect width.
                width = min(width, self._rect.Width)

        # Draw character to bitmap
        gdiplus.GdipGraphicsClear(self._graphics, 0x00000000)
        gdiplus.GdipDrawString(self._graphics,
                               ch,
                               len_ch,
                               self.font._gdipfont,
                               ctypes.byref(rect),
                               fmt,
                               self._brush)

        gdiplus.GdipFlush(self._graphics, 1)
        gdiplus.GdipDeleteStringFormat(fmt)

        bitmap_data = BitmapData()
        gdiplus.GdipBitmapLockBits(
            self._bitmap,
            byref(self._rect), ImageLockModeRead, self._format,
            byref(bitmap_data))

        # Create buffer for RawImage
        buffer = create_string_buffer(
            bitmap_data.Stride * bitmap_data.Height)
        memmove(buffer, bitmap_data.Scan0, len(buffer))

        # Unlock data
        gdiplus.GdipBitmapUnlockBits(self._bitmap, byref(bitmap_data))

        image = pyglet.image.ImageData(
            width, height,
            'BGRA', buffer, -bitmap_data.Stride)

        glyph = self.font.create_glyph(image)
        # Only pass negative LSB info
        lsb = min(lsb, 0)
        glyph.set_bearings(-self.font.descent, lsb, advance)
        return glyph


FontStyleBold = 1
FontStyleItalic = 2
UnitPixel = 2
UnitPoint = 3


class GDIPlusFont(Win32Font):
    glyph_renderer_class = GDIPlusGlyphRenderer

    _private_fonts = None

    _default_name = 'Arial'

    def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None):
        if not name:
            name = self._default_name

        # assert type(bold) is bool, "Only a boolean value is supported for bold in the current font renderer."
        # assert type(italic) is bool, "Only a boolean value is supported for bold in the current font renderer."

        if stretch:
            warnings.warn("The current font render does not support stretching.")

        super().__init__(name, size, bold, italic, stretch, dpi)

        self._name = name

        family = ctypes.c_void_p()

        # GDI will add @ in front of a localized font for some Asian languages. However, GDI will also not find it
        # based on that name (???). Here we remove it before checking font collections.
        if name[0] == "@":
            name = name[1:]

        name = ctypes.c_wchar_p(name)

        # Look in private collection first:
        if self._private_fonts:
            gdiplus.GdipCreateFontFamilyFromName(name, self._private_fonts, ctypes.byref(family))

        # Then in system collection:
        if not family:
            if _debug_font:
                print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}")

            gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))

        # Nothing found, use default font.
        if not family:
            self._name = self._default_name
            gdiplus.GdipCreateFontFamilyFromName(ctypes.c_wchar_p(self._name), None, ctypes.byref(family))

        if dpi is None:
            unit = UnitPoint
            self.dpi = 96
        else:
            unit = UnitPixel
            size = (size * dpi) // 72
            self.dpi = dpi

        style = 0
        if bold:
            style |= FontStyleBold
        if italic:
            style |= FontStyleItalic
        self._gdipfont = ctypes.c_void_p()
        gdiplus.GdipCreateFont(family, ctypes.c_float(size), style, unit, ctypes.byref(self._gdipfont))
        gdiplus.GdipDeleteFontFamily(family)

    @property
    def name(self):
        return self._name

    def __del__(self):
        super(GDIPlusFont, self).__del__()
        gdiplus.GdipDeleteFont(self._gdipfont)

    @classmethod
    def add_font_data(cls, data):
        super(GDIPlusFont, cls).add_font_data(data)

        if not cls._private_fonts:
            cls._private_fonts = ctypes.c_void_p()
            gdiplus.GdipNewPrivateFontCollection(
                ctypes.byref(cls._private_fonts))
        gdiplus.GdipPrivateAddMemoryFont(cls._private_fonts, data, len(data))

    @classmethod
    def have_font(cls, name):
        family = ctypes.c_void_p()

        # Look in private collection first:
        num_count = ctypes.c_int()
        gdiplus.GdipGetFontCollectionFamilyCount(
            cls._private_fonts, ctypes.byref(num_count))
        gpfamilies = (ctypes.c_void_p * num_count.value)()
        numFound = ctypes.c_int()
        gdiplus.GdipGetFontCollectionFamilyList(
            cls._private_fonts, num_count, gpfamilies, ctypes.byref(numFound))

        font_name = ctypes.create_unicode_buffer(32)
        for gpfamily in gpfamilies:
            gdiplus.GdipGetFamilyName(gpfamily, font_name, '\0')
            if font_name.value == name:
                return True

        # Else call parent class for system fonts
        return super(GDIPlusFont, cls).have_font(name)
