#@PydevCodeAnalysisIgnore
# Copyright 2004-2023 Tom Rothamel <pytom@bishoujo.us>
#
# 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.

from __future__ import print_function

from sdl2 cimport *
from pygame_sdl2 cimport *
import_pygame_sdl2()

from freetype cimport *
from ttgsubtable cimport *
from renpy.text.textsupport cimport Glyph, SPLIT_INSTEAD
import traceback
import sys

import renpy.config

cdef bint _use_ucs2 = (sys.maxunicode == 0xffff)

cdef extern from "ftsupport.h":
    char *freetype_error_to_string(int error)

# The freetype library object we use.
cdef FT_Library library

# Represents a cached glyph.
cdef struct glyph_cache:

    # The character we're caching. -1 if we're not representing a valid
    # character.
    int index

    int width
    float advance

    FT_Bitmap bitmap
    int bitmap_left
    int bitmap_top


class FreetypeError(Exception):
    def __init__(self, code):
        Exception.__init__(self, "%d: %s" % (code, freetype_error_to_string(code)))

def init():

    cdef int error

    error = FT_Init_FreeType(&library)
    if error:
        raise FreetypeError(error)

cdef bint is_vs(unsigned int char):
    if 0xfe00 <= char <= 0xfe0f: # VS1-16
        return True

    elif 0xe0100 <= char <= 0xe01ef: # VS17-256
        return True

    elif 0x180b <= char <= 0x180d: # FVS1-3
        return True

    return False

cdef int is_ucs2_surrogate(unsigned int char):
    if _use_ucs2:
        if 0xd800 <= char <= 0xdbff:
            # High
            return 2
        elif 0xdc00 <= char <= 0xdfff:
            # Low
            return 1
        else:
            # Not a surrogate
            return 0
    else:
        # skip when not using UCS-2
        return 0

cdef bint is_zerowidth(unsigned int char):
    if char == 0x200b: # Zero-width space.
        return True

    if char == 0x200c: # Zero-width non-joiner.
        return True

    if char == 0x200d: # Zero-width joiner.
        return True

    if char == 0x2060: # Word joiner.
        return True

    if char == 0xfeff: # Zero width non-breaking space.
        return True

    if is_vs(char): # Variation sequences
        return True

    if is_ucs2_surrogate(char) == 1: # Low surrogate pair (width is calculated when a high surrogate is read)
        return True

    return False

cdef unsigned long io_func(FT_Stream stream, unsigned long offset, unsigned char *buffer, unsigned long count) noexcept:
    """
    Seeks to offset, and then reads count bytes from the stream into buffer.
    """

    cdef FTFace face
    cdef char *cbuf
    cdef unsigned long i

    face = <FTFace> stream.descriptor.pointer
    f = face.f

    if face.offset != offset:

        try:
            f.seek(offset)
            face.offset = offset
        except Exception:
            traceback.print_exc()
            return -1

    if count != 0:
        try:
            buf = f.read(count)
            cbuf = buf
            count = len(buf)

            for i from 0 <= i < count:
                buffer[i] = cbuf[i]
        except Exception:
            traceback.print_exc()
            return -1

    face.offset += count

    return count

cdef void close_func(FT_Stream stream) noexcept:
    """
    Close the stream.

    Currently does nothing, closing is taken care of at the Font
    object level.
    """

    return


cdef class FTFace:
    """
    Represents a freetype face.
    """

    cdef:
        FT_StreamRec stream
        FT_Open_Args open_args
        FT_Face face

        float size

        # The file the font is read from.
        object f

        # The offset in that file.
        unsigned long offset

        public object fn

    def __init__(self, f, index, fn):

        cdef int error
        cdef unsigned long size

        # The filename.
        self.fn = fn

        # The file that the font is opened from.
        self.f = f

        f.seek(0, 2)
        size = f.tell()
        f.seek(0, 0)

        # The offset within the stream we're currently at.
        self.offset = 0

        self.open_args.flags = FT_OPEN_STREAM
        self.open_args.stream = &self.stream

        self.stream.size = size
        self.stream.pos = 0
        self.stream.descriptor.pointer = <void *> self
        self.stream.read = io_func
        self.stream.close = close_func

        error = FT_Open_Face(library, &self.open_args, index, &self.face)
        if error:
            raise FreetypeError(error)

        error = FT_Select_Charmap(self.face, FT_ENCODING_UNICODE)
        if error:
            raise FreetypeError(error)

        # The size the face is at.
        self.size = -1

cdef class FTFont:

    cdef:

        FTFace face_object
        FT_Face face
        TTGSUBTable gsubtable

        # A cache of various properties.
        float size
        float bold
        bint italic
        int outline
        bint antialias
        bint vertical

        # Information used to modify the font.

        # The offset and height of the underline.
        public int underline_offset
        public int underline_height

        # The stroker object.
        FT_Stroker stroker

        # The number of pixels the outlines are expanded by.
        public int expand

        # Basic Y-direction metrics for this font.
        public int ascent
        public int descent
        public int height
        public int lineskip

        glyph_cache cache[256]

        # Have we been setup at least once?
        bint has_setup

        # The hinting flag to use.
        int hinting

    def __cinit__(self):
        for i from 0 <= i < 256:
            self.cache[i].index = -1
            FT_Bitmap_New(&(self.cache[i].bitmap))

        init_gsubtable(&self.gsubtable)

    def __dealloc__(self):
        for i from 0 <= i < 256:
            FT_Bitmap_Done(library, &(self.cache[i].bitmap))

        if self.stroker != NULL:
            FT_Stroker_Done(self.stroker)

        free_gsubtable(&self.gsubtable)


    def __init__(self, face, float size, float bold, bint italic, int outline, bint antialias, bint vertical, hinting):

        if size < 1:
            size = 1

        if bold:
            antialias = True

        size = size * renpy.config.ftfont_scale.get(face.fn, 1.0) * renpy.game.preferences.font_size

        self.face_object = face
        self.face = self.face_object.face

        self.size = size
        self.bold = bold
        self.italic = italic
        self.outline = outline
        self.antialias = antialias
        self.vertical = vertical

        LoadGSUBTable(&self.gsubtable, self.face)

        if outline == 0:
            self.stroker = NULL;
            self.expand = 0

        else:
            FT_Stroker_New(library, &self.stroker)
            FT_Stroker_Set(self.stroker, outline * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0)
            self.expand = outline * 2

        self.has_setup = False

        if hinting == "bytecode":
            self.hinting = FT_LOAD_NO_AUTOHINT
        elif hinting == "none" or hinting is None:
            self.hinting = FT_LOAD_NO_HINTING
        else:
            self.hinting = FT_LOAD_FORCE_AUTOHINT

    cdef setup(self):
        """
        Changes the parameters of the face to match this font.
        """

        cdef int error
        cdef FT_Face face
        cdef FT_Fixed scale
        cdef float ascent_scale

        face = self.face

        if self.face_object.size != self.size:
            self.face_object.size = self.size

            error = FT_Set_Char_Size(face, 0, <int> (self.size * 64), 0, 0)
            if error:
                raise FreetypeError(error)

        if not self.has_setup:

            self.has_setup = True

            scale = face.size.metrics.y_scale

            vextent_scale = renpy.config.ftfont_vertical_extent_scale.get(self.face_object.fn, 1.0)

            self.ascent = FT_CEIL(int(face.size.metrics.ascender * vextent_scale))
            self.descent = FT_FLOOR(int(face.size.metrics.descender * vextent_scale))

            if self.descent > 0:
                self.descent = -self.descent

            self.ascent += self.expand
            self.descent -= self.expand

            self.height = self.ascent - self.descent

            # This is probably more correct, but isn't what 6.12.1 did.
            # self.lineskip = FT_CEIL(face.size.metrics.height) + self.expand

            # if self.height > self.lineskip:
            #     self.lineskip = self.height

            self.lineskip = <int> self.height * renpy.game.preferences.font_line_spacing

            if self.vertical:
                self.underline_offset = FT_FLOOR(FT_MulFix(face.ascender + face.descender - face.underline_position, scale))
            else:
                self.underline_offset = FT_FLOOR(FT_MulFix(face.underline_position, scale))
            self.underline_height = FT_FLOOR(FT_MulFix(face.underline_thickness, scale))

            if self.underline_height < 1:
                self.underline_height = 1

            self.underline_height += self.expand

        return

    cdef glyph_cache *get_glyph(self, int index):
        """
        Returns the glyph_cache object for a given glyph.
        """

        cdef FT_Face face
        cdef FT_Glyph g
        cdef FT_BitmapGlyph bg
        cdef FT_Matrix shear

        cdef int error
        cdef glyph_cache *rv
        cdef uint32_t vindex

        cdef int overhang
        cdef FT_Glyph_Metrics metrics

        cdef int x, y, glyph_rotate

        face = self.face

        if self.vertical:
            if GetVerticalGlyph(&self.gsubtable, index, &vindex) == 0:
                index = vindex
            if self.face.face_flags & 32 == 32: # FT_HAS_VERTICAL(face)
                glyph_rotate = 1
            else:
                # font doesn't have vertical metrics (simulate it)
                glyph_rotate = 2
        else:
            glyph_rotate = 0

        rv = &(self.cache[index & 255])
        if rv.index == index:
            return rv

        rv.index = index

        error = FT_Load_Glyph(face, index, self.hinting)
        if error:
            raise FreetypeError(error)

        error = FT_Get_Glyph(face.glyph, &g)

        if error:
            raise FreetypeError(error)

        if g.format != FT_GLYPH_FORMAT_BITMAP:

            if self.italic:
                shear.xx = 1 << 16
                shear.xy = (207 << 16) // 1000 # taken from SDL_ttf.
                shear.yx = 0
                shear.yy = 1 << 16

                FT_Outline_Transform(&(<FT_OutlineGlyph> g).outline, &shear)

            if glyph_rotate != 0:
                metrics = face.glyph.metrics
                # move the origin for vertical layout
                if glyph_rotate == 1:
                    FT_Outline_Translate(&(<FT_OutlineGlyph> g).outline, metrics.vertBearingX - metrics.horiBearingX, -metrics.vertBearingY - metrics.horiBearingY)
                else:
                    FT_Outline_Translate(&(<FT_OutlineGlyph> g).outline, -metrics.horiAdvance // 2, -face.bbox.yMax)
                shear.xx = 0
                shear.xy = -(1 << 16)
                shear.yx = 1 << 16
                shear.yy = 0
                FT_Outline_Transform(&(<FT_OutlineGlyph> g).outline, &shear)
                # set vertical baseline to a half of the height
                FT_Outline_Translate(&(<FT_OutlineGlyph> g).outline, 0, (face.bbox.yMax + face.bbox.yMin) // 2)

            if self.stroker != NULL:
                # FT_Glyph_StrokeBorder(&g, self.stroker, 0, 1)
                FT_Glyph_Stroke(&g, self.stroker, 1)

            if self.antialias:
                FT_Glyph_To_Bitmap(&g, FT_RENDER_MODE_NORMAL, NULL, 1)
            else:
                FT_Glyph_To_Bitmap(&g, FT_RENDER_MODE_MONO, NULL, 1)

        bg = <FT_BitmapGlyph> g

        if bg.bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
            FT_Bitmap_Convert(library, &(bg.bitmap), &(rv.bitmap), 4)

            # Freetype gives us a bitmap where values range from 0 to 1.
            for y from 0 <= y < rv.bitmap.rows:
                for x from 0 <= x < rv.bitmap.width:
                    if rv.bitmap.buffer[ y * rv.bitmap.pitch + x ]:
                        rv.bitmap.buffer[ y * rv.bitmap.pitch + x ] = 255

        else:
            FT_Bitmap_Copy(library, &(bg.bitmap), &(rv.bitmap))

        if self.bold:
            overhang = face.size.metrics.y_ppem // 10

            FT_Bitmap_Embolden(
                library,
                &(rv.bitmap),
                overhang << 6,
                0)

        else:
            overhang = 0


        # rv.width = FT_CEIL(face.glyph.metrics.width) + self.expand
        if glyph_rotate == 1:
            rv.advance = face.glyph.metrics.vertAdvance / 64.0 + self.expand + overhang
        elif glyph_rotate == 2:
            # rv.advance = (face.ascender - face.descender) / 64.0 + self.expand + overhang
            rv.advance = self.lineskip + overhang
        else:
            rv.advance = face.glyph.metrics.horiAdvance / 64.0 + self.expand + overhang

        rv.bitmap_left = bg.left + self.expand // 2
        rv.bitmap_top = bg.top - self.expand // 2

        rv.width = rv.bitmap.width + rv.bitmap_left

        FT_Done_Glyph(g)

        return rv


    def glyphs(self, unicode s):
        """
        Sizes s, returning a list of Glyph objects.
        """

        cdef FT_Face face
        cdef list rv
        cdef int len_s
        cdef FT_ULong c, next_c, vs
        cdef FT_UInt index, next_index
        cdef int error
        cdef Glyph gl
        cdef FT_Vector kerning
        cdef int kern
        cdef float advance
        cdef int i
        cdef int vs_offset
        cdef glyph_cache *cache

        cdef float min_advance, next_min_advance

        self.setup()

        len_s = len(s)

        face = self.face
        g = face.glyph

        rv = [ ]

        if len_s:

            next_min_advance = 0

            if len_s > 1 and is_ucs2_surrogate(s[0]) == 2 and is_ucs2_surrogate(s[1]) == 1:
                next_c = ((<FT_ULong> s[0]) & 0b1111111111) << 10
                next_c |= (<FT_ULong> s[1]) & 0b1111111111
                next_c += 0x10000
                vs_offset = 2
            else:
                next_c = s[0]
                vs_offset = 1

            if is_ucs2_surrogate(s[0]) != 1 and len_s > vs_offset and is_vs(s[vs_offset]):
                vs = s[vs_offset]
                next_index = FT_Face_GetCharVariantIndex(face, next_c, vs)

                # Fallback to 0 if variation doesn't exist
                if next_index == 0:
                    vs = 0
                    next_index = FT_Get_Char_Index(face, next_c)
            else:
                vs = 0
                next_index = FT_Get_Char_Index(face, next_c)

        for i from 0 <= i < len_s:

            c = next_c
            index = next_index
            min_advance = next_min_advance

            cache = self.get_glyph(index)

            gl = Glyph.__new__(Glyph)

            gl.character = c
            gl.variation = vs
            gl.ascent = self.ascent
            gl.width = cache.width
            gl.line_spacing = self.lineskip
            gl.draw = True

            if i < len_s - 1:

                if i < len_s - 2 and is_ucs2_surrogate(s[i + 1]) == 2 and is_ucs2_surrogate(s[i + 2]) == 1:
                    next_c = ((<FT_ULong> s[i + 1]) & 0b1111111111) << 10
                    next_c |= (<FT_ULong> s[i + 2]) & 0b1111111111
                    next_c += 0x10000
                    vs_offset = 3
                else:
                    next_c = s[i + 1]
                    vs_offset = 2

                if is_ucs2_surrogate(s[i + 1]) != 1 and i < len_s - vs_offset and is_vs(s[i + vs_offset]):
                    vs = s[i + vs_offset]
                    next_index = FT_Face_GetCharVariantIndex(face, next_c, vs)

                    # Fallback to 0 if variation doesn't exist
                    if next_index == 0:
                        vs = 0
                        next_index = FT_Get_Char_Index(face, next_c)
                else:
                    vs = 0
                    next_index = FT_Get_Char_Index(face, next_c)

                error = FT_Get_Kerning(face, index, next_index, FT_KERNING_DEFAULT, &kerning)
                if error:
                    raise FreetypeError(error)

                kern = FT_ROUND(kerning.x)

                if cache.advance + kern > min_advance:
                    gl.advance = cache.advance + kern
                else:
                    gl.advance = min_advance

                next_min_advance = cache.advance - gl.advance

            else:
                gl.advance = cache.advance

            if is_zerowidth(gl.character):
                gl.width = 0
                gl.advance = 0
                gl.draw = False

            rv.append(gl)

        return rv

    def bounds(self, glyphs, bounds):
        """
        Given a list of glyphs, get the intersection of bounds and the area where the glyphs
        will be drawn to. (Not including any offsets or expansions.)

        Returns an x, y, w, h tuple.
        """

        cdef Glyph glyph

        cdef FT_Face face
        cdef FT_GlyphSlot g
        cdef FT_UInt index

        cdef int bmx
        cdef int bmy

        cdef int x, y, w, h

        x, y, w, h = bounds

        face = self.face

        self.setup()

        for glyph in glyphs:

            if glyph.split == SPLIT_INSTEAD:
                continue

            if glyph.character == 0x200b:
                continue

            if glyph.variation == 0:
                index = FT_Get_Char_Index(face, glyph.character)
            else:
                index = FT_Face_GetCharVariantIndex(face, glyph.character, glyph.variation)

            cache = self.get_glyph(index)

            bmx = <int> (glyph.x + .5) + cache.bitmap_left
            bmy = glyph.y - cache.bitmap_top

            if bmx < x:
                x = bmx

            if bmy < y:
                y = bmy

            if bmx + <int> cache.bitmap.width > w:
                w = bmx + cache.bitmap.width

            if bmy + <int> cache.bitmap.rows > h:
                h = bmy + cache.bitmap.rows

        return x, y, w, h

    def draw(self, pysurf, float xo, int yo, color, list glyphs, int underline, bint strikethrough, black_color):
        """
        Draws a list of glyphs to surf, with the baseline starting at x, y.
        """

        cdef SDL_Surface *surf
        cdef unsigned int Sr, Sb, Sg, Sa
        cdef unsigned int Dr, Db, Dg, Da
        cdef unsigned int rshift, gshift, bshift, ashift
        cdef unsigned int fixed
        cdef unsigned int alpha
        cdef Glyph glyph

        cdef FT_Face face
        cdef FT_GlyphSlot g
        cdef FT_UInt index
        cdef int error
        cdef int bmx, bmy, px, py, pxstart
        cdef int ly, lh, rows, width
        cdef int underline_x, underline_end, expand
        cdef int x, y

        cdef unsigned char *pixels
        cdef unsigned char *line
        cdef unsigned char *gline
        cdef int pitch
        cdef glyph_cache *cache

        Sr, Sg, Sb, Sa = color

        if Sa == 0:
            return

        self.setup()

        surf = PySurface_AsSurface(pysurf)
        pixels = <unsigned char *> surf.pixels
        pitch = surf.pitch

        face = self.face
        g = face.glyph

        expand = self.expand

        for glyph in glyphs:

            if glyph.split == SPLIT_INSTEAD:
                continue

            x = <int> (glyph.x + xo)
            y = <int> (glyph.y + yo)

            underline_x = x - glyph.delta_x_offset
            underline_end = x + <int> glyph.advance + expand

            if glyph.variation == 0:
                index = FT_Get_Char_Index(face, glyph.character)
            else:
                index = FT_Face_GetCharVariantIndex(face, glyph.character, glyph.variation)

            cache = self.get_glyph(index)

            # with nogil used to be here, but it slowed things down.

            bmx = <int> (x + .5) + cache.bitmap_left
            bmy = y - cache.bitmap_top

            if bmx < 0:
                pxstart = -bmx
                bmx = 0
            else:
                pxstart = 0

            rows = min(cache.bitmap.rows, surf.h - bmy)
            width = min(cache.bitmap.width, surf.w - bmx)

            underline_end = min(underline_end, surf.w - 1)

            if glyph.draw:

                for py from 0 <= py < rows:

                    if bmy < 0:
                        bmy += 1
                        continue

                    line = pixels + bmy * pitch + bmx * 4
                    gline = cache.bitmap.buffer + py * cache.bitmap.pitch + pxstart

                    for px from 0 <= px < width:

                        alpha = gline[0]

                        # Modulate Sa by the glyph's alpha.

                        alpha = (alpha * Sa + Sa) >> 8

                        # Only draw if we increase the alpha - a cheap way to
                        # allow overlapping characters.
                        if line[3] < alpha:

                            line[0] = Sr
                            line[1] = Sg
                            line[2] = Sb
                            line[3] = alpha

                        gline += 1
                        line += 4

                    bmy += 1

            # Underlining.
            if underline:

                ly = y - self.underline_offset - 1
                lh = self.underline_height * underline

                for py from ly <= py < min(ly + lh, surf.h):
                    for px from underline_x <= px < underline_end:
                        line = pixels + py * pitch + px * 4

                        line[0] = Sr
                        line[1] = Sg
                        line[2] = Sb
                        line[3] = Sa

            # Strikethrough.
            if strikethrough:
                ly = y - self.ascent + self.height // 2
                lh = self.height // 10
                if lh < 1:
                    lh = 1

                for py from ly <= py < (ly + lh):
                    for px from underline_x <= px < underline_end:
                        line = pixels + py * pitch + px * 4

                        line[0] = Sr
                        line[1] = Sg
                        line[2] = Sb
                        line[3] = Sa
