# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2023 pyglet contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
"""
Wrapper around the Linux FontConfig library. Used to find available fonts.
"""

from collections import OrderedDict
from ctypes import *

import pyglet.lib
from pyglet.util import asbytes, asstr
from pyglet.font.base import FontException


# fontconfig library definitions

(FcResultMatch,
 FcResultNoMatch,
 FcResultTypeMismatch,
 FcResultNoId,
 FcResultOutOfMemory) = range(5)
FcResult = c_int

FC_FAMILY = asbytes('family')
FC_SIZE = asbytes('size')
FC_SLANT = asbytes('slant')
FC_WEIGHT = asbytes('weight')
FC_FT_FACE = asbytes('ftface')
FC_FILE = asbytes('file')

FC_WEIGHT_REGULAR = 80
FC_WEIGHT_BOLD = 200

FC_SLANT_ROMAN = 0
FC_SLANT_ITALIC = 100

(FcTypeVoid,
 FcTypeInteger,
 FcTypeDouble,
 FcTypeString,
 FcTypeBool,
 FcTypeMatrix,
 FcTypeCharSet,
 FcTypeFTFace,
 FcTypeLangSet) = range(9)
FcType = c_int

(FcMatchPattern,
 FcMatchFont) = range(2)
FcMatchKind = c_int


class _FcValueUnion(Union):
    _fields_ = [
        ('s', c_char_p),
        ('i', c_int),
        ('b', c_int),
        ('d', c_double),
        ('m', c_void_p),
        ('c', c_void_p),
        ('f', c_void_p),
        ('p', c_void_p),
        ('l', c_void_p),
    ]


class FcValue(Structure):
    _fields_ = [
        ('type', FcType),
        ('u', _FcValueUnion)
    ]

# End of library definitions


class FontConfig:
    def __init__(self):
        self._fontconfig = self._load_fontconfig_library()
        self._search_cache = OrderedDict()
        self._cache_size = 20

    def dispose(self):
        while len(self._search_cache) > 0:
            self._search_cache.popitem().dispose()

        self._fontconfig.FcFini()
        self._fontconfig = None

    def create_search_pattern(self):
        return FontConfigSearchPattern(self._fontconfig)

    def find_font(self, name, size=12, bold=False, italic=False):
        result = self._get_from_search_cache(name, size, bold, italic)
        if result:
            return result

        search_pattern = self.create_search_pattern()
        search_pattern.name = name
        search_pattern.size = size
        search_pattern.bold = bold
        search_pattern.italic = italic

        result = search_pattern.match()
        self._add_to_search_cache(search_pattern, result)
        search_pattern.dispose()
        return result

    def have_font(self, name):
        result = self.find_font(name)
        if result:
            # Check the name matches, fontconfig can return a default
            if name and result.name and result.name.lower() != name.lower():
                return False
            return True
        else:
            return False

    def char_index(self, ft_face, character):
        return self._fontconfig.FcFreeTypeCharIndex(ft_face, ord(character))

    def _add_to_search_cache(self, search_pattern, result_pattern):
        self._search_cache[(search_pattern.name,
                            search_pattern.size,
                            search_pattern.bold,
                            search_pattern.italic)] = result_pattern
        if len(self._search_cache) > self._cache_size:
            self._search_cache.popitem(last=False)[1].dispose()

    def _get_from_search_cache(self,  name, size, bold, italic):
        result = self._search_cache.get((name, size, bold, italic), None)

        if result and result.is_valid:
            return result
        else:
            return None

    @staticmethod
    def _load_fontconfig_library():
        fontconfig = pyglet.lib.load_library('fontconfig')
        fontconfig.FcInit()

        fontconfig.FcPatternBuild.restype = c_void_p
        fontconfig.FcPatternCreate.restype = c_void_p
        fontconfig.FcFontMatch.restype = c_void_p
        fontconfig.FcFreeTypeCharIndex.restype = c_uint

        fontconfig.FcPatternAddDouble.argtypes = [c_void_p, c_char_p, c_double]
        fontconfig.FcPatternAddInteger.argtypes = [c_void_p, c_char_p, c_int]
        fontconfig.FcPatternAddString.argtypes = [c_void_p, c_char_p, c_char_p]
        fontconfig.FcConfigSubstitute.argtypes = [c_void_p, c_void_p, c_int]
        fontconfig.FcDefaultSubstitute.argtypes = [c_void_p]
        fontconfig.FcFontMatch.argtypes = [c_void_p, c_void_p, c_void_p]
        fontconfig.FcPatternDestroy.argtypes = [c_void_p]

        fontconfig.FcPatternGetFTFace.argtypes = [c_void_p, c_char_p, c_int, c_void_p]
        fontconfig.FcPatternGet.argtypes = [c_void_p, c_char_p, c_int, c_void_p]

        return fontconfig


class FontConfigPattern:
    def __init__(self, fontconfig, pattern=None):
        self._fontconfig = fontconfig
        self._pattern = pattern

    @property
    def is_valid(self):
        return self._fontconfig and self._pattern

    def _create(self):
        assert not self._pattern
        assert self._fontconfig
        self._pattern = self._fontconfig.FcPatternCreate()

    def _destroy(self):
        assert self._pattern
        assert self._fontconfig
        self._fontconfig.FcPatternDestroy(self._pattern)
        self._pattern = None

    @staticmethod
    def _bold_to_weight(bold):
        return FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR

    @staticmethod
    def _italic_to_slant(italic):
        return FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN

    def _set_string(self, name, value):
        assert self._pattern
        assert name
        assert self._fontconfig

        if not value:
            return

        value = value.encode('utf8')

        self._fontconfig.FcPatternAddString(self._pattern, name, asbytes(value))

    def _set_double(self, name, value):
        assert self._pattern
        assert name
        assert self._fontconfig

        if not value:
            return

        self._fontconfig.FcPatternAddDouble(self._pattern, name, c_double(value))

    def _set_integer(self, name, value):
        assert self._pattern
        assert name
        assert self._fontconfig

        if not value:
            return

        self._fontconfig.FcPatternAddInteger(self._pattern, name, c_int(value))

    def _get_value(self, name):
        assert self._pattern
        assert name
        assert self._fontconfig

        value = FcValue()
        result = self._fontconfig.FcPatternGet(self._pattern, name, 0, byref(value))
        if _handle_fcresult(result):
            return value
        else:
            return None

    def _get_string(self, name):
        value = self._get_value(name)

        if value and value.type == FcTypeString:
            return asstr(value.u.s)
        else:
            return None

    def _get_face(self, name):
        value = self._get_value(name)

        if value and value.type == FcTypeFTFace:
            return value.u.f
        else:
            return None

    def _get_integer(self, name):
        value = self._get_value(name)

        if value and value.type == FcTypeInteger:
            return value.u.i
        else:
            return None

    def _get_double(self, name):
        value = self._get_value(name)

        if value and value.type == FcTypeDouble:
            return value.u.d
        else:
            return None


class FontConfigSearchPattern(FontConfigPattern):
    def __init__(self, fontconfig):
        super(FontConfigSearchPattern, self).__init__(fontconfig)

        self.name = None
        self.bold = False
        self.italic = False
        self.size = None

    def match(self):
        self._prepare_search_pattern()
        result_pattern = self._get_match()

        if result_pattern:
            return FontConfigSearchResult(self._fontconfig, result_pattern)
        else:
            return None

    def _prepare_search_pattern(self):
        self._create()
        self._set_string(FC_FAMILY, self.name)
        self._set_double(FC_SIZE, self.size)
        self._set_integer(FC_WEIGHT, self._bold_to_weight(self.bold))
        self._set_integer(FC_SLANT, self._italic_to_slant(self.italic))

        self._substitute_defaults()

    def _substitute_defaults(self):
        assert self._pattern
        assert self._fontconfig

        self._fontconfig.FcConfigSubstitute(None, self._pattern, FcMatchPattern)
        self._fontconfig.FcDefaultSubstitute(self._pattern)

    def _get_match(self):
        assert self._pattern
        assert self._fontconfig

        match_result = FcResult()
        match_pattern = self._fontconfig.FcFontMatch(0, self._pattern, byref(match_result))

        if _handle_fcresult(match_result.value):
            return match_pattern
        else:
            return None

    def dispose(self):
        self._destroy()


class FontConfigSearchResult(FontConfigPattern):
    def __init__(self, fontconfig, result_pattern):
        super(FontConfigSearchResult, self).__init__(fontconfig, result_pattern)

    @property
    def name(self):
        return self._get_string(FC_FAMILY)

    @property
    def size(self):
        return self._get_double(FC_SIZE)

    @property
    def bold(self):
        return self._get_integer(FC_WEIGHT) == FC_WEIGHT_BOLD

    @property
    def italic(self):
        return self._get_integer(FC_SLANT) == FC_SLANT_ITALIC

    @property
    def face(self):
        return self._get_face(FC_FT_FACE)

    @property
    def file(self):
        return self._get_string(FC_FILE)

    def dispose(self):
        self._destroy()


def _handle_fcresult(result):
    if result == FcResultMatch:
        return True
    elif result in (FcResultNoMatch, FcResultTypeMismatch, FcResultNoId):
        return False
    elif result == FcResultOutOfMemory:
        raise FontException('FontConfig ran out of memory.')


_fontconfig_instance = None


def get_fontconfig():
    global _fontconfig_instance
    if not _fontconfig_instance:
        _fontconfig_instance = FontConfig()
    return _fontconfig_instance
