/*
 * fontconfig.c
 * Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

#include "state.h"
#include "cleanup.h"
#include "lineops.h"
#include "fonts.h"
#include <fontconfig/fontconfig.h>
#include <dlfcn.h>
#include "emoji.h"
#include "freetype_render_ui_text.h"
#ifndef FC_COLOR
#define FC_COLOR "color"
#endif


static bool initialized = false;
static void* libfontconfig_handle = NULL;

#define FcInit dynamically_loaded_fc_symbol.Init
#define FcFini dynamically_loaded_fc_symbol.Fini
#define FcCharSetAddChar dynamically_loaded_fc_symbol.CharSetAddChar
#define FcPatternDestroy dynamically_loaded_fc_symbol.PatternDestroy
#define FcObjectSetDestroy dynamically_loaded_fc_symbol.ObjectSetDestroy
#define FcPatternAddDouble dynamically_loaded_fc_symbol.PatternAddDouble
#define FcPatternAddString dynamically_loaded_fc_symbol.PatternAddString
#define FcFontMatch dynamically_loaded_fc_symbol.FontMatch
#define FcCharSetCreate dynamically_loaded_fc_symbol.CharSetCreate
#define FcPatternGetString dynamically_loaded_fc_symbol.PatternGetString
#define FcFontSetDestroy dynamically_loaded_fc_symbol.FontSetDestroy
#define FcPatternGetInteger dynamically_loaded_fc_symbol.PatternGetInteger
#define FcPatternAddBool dynamically_loaded_fc_symbol.PatternAddBool
#define FcFontList dynamically_loaded_fc_symbol.FontList
#define FcObjectSetBuild dynamically_loaded_fc_symbol.ObjectSetBuild
#define FcCharSetDestroy dynamically_loaded_fc_symbol.CharSetDestroy
#define FcConfigSubstitute dynamically_loaded_fc_symbol.ConfigSubstitute
#define FcDefaultSubstitute dynamically_loaded_fc_symbol.DefaultSubstitute
#define FcPatternAddInteger dynamically_loaded_fc_symbol.PatternAddInteger
#define FcPatternCreate dynamically_loaded_fc_symbol.PatternCreate
#define FcPatternGetBool dynamically_loaded_fc_symbol.PatternGetBool
#define FcPatternAddCharSet dynamically_loaded_fc_symbol.PatternAddCharSet

static struct {
    FcBool(*Init)(void);
    void(*Fini)(void);
    FcBool (*CharSetAddChar) (FcCharSet *fcs, FcChar32 ucs4);
    void (*PatternDestroy) (FcPattern *p);
    void (*ObjectSetDestroy) (FcObjectSet *os);
    FcBool (*PatternAddDouble) (FcPattern *p, const char *object, double d);
    FcBool (*PatternAddString) (FcPattern *p, const char *object, const FcChar8 *s);
    FcPattern * (*FontMatch) (FcConfig	*config, FcPattern	*p, FcResult	*result);
    FcCharSet* (*CharSetCreate) (void);
    FcResult (*PatternGetString) (const FcPattern *p, const char *object, int n, FcChar8 ** s);
    void (*FontSetDestroy) (FcFontSet *s);
    FcResult (*PatternGetInteger) (const FcPattern *p, const char *object, int n, int *i);
    FcBool (*PatternAddBool) (FcPattern *p, const char *object, FcBool b);
    FcFontSet * (*FontList) (FcConfig	*config, FcPattern	*p, FcObjectSet *os);
    FcObjectSet * (*ObjectSetBuild) (const char *first, ...);
    void (*CharSetDestroy) (FcCharSet *fcs);
    FcBool (*ConfigSubstitute) (FcConfig	*config, FcPattern	*p, FcMatchKind	kind);
    void (*DefaultSubstitute) (FcPattern *pattern);
    FcBool (*PatternAddInteger) (FcPattern *p, const char *object, int i);
    FcPattern * (*PatternCreate) (void);
    FcResult (*PatternGetBool) (const FcPattern *p, const char *object, int n, FcBool *b);
    FcBool (*PatternAddCharSet) (FcPattern *p, const char *object, const FcCharSet *c);
} dynamically_loaded_fc_symbol = {0};
#define LOAD_FUNC(name) {\
    *(void **) (&dynamically_loaded_fc_symbol.name) = dlsym(libfontconfig_handle, "Fc" #name); \
    if (!dynamically_loaded_fc_symbol.name) { \
        const char* error = dlerror(); \
        fatal("Failed to load the function Fc" #name " with error: %s", error ? error : ""); \
    } \
}


static void
load_fontconfig_lib(void) {
        const char* libnames[] = {
#if defined(_KITTY_FONTCONFIG_LIBRARY)
            _KITTY_FONTCONFIG_LIBRARY,
#else
            "libfontconfig.so",
            // some installs are missing the .so symlink, so try the full name
            "libfontconfig.so.1",
#endif
            NULL
        };
        for (int i = 0; libnames[i]; i++) {
            libfontconfig_handle = dlopen(libnames[i], RTLD_LAZY);
            if (libfontconfig_handle) break;
        }
        if (libfontconfig_handle == NULL) { fatal("Failed to find and load fontconfig"); }
        dlerror();    /* Clear any existing error */
        LOAD_FUNC(Init);
        LOAD_FUNC(Fini);
        LOAD_FUNC(CharSetAddChar);
        LOAD_FUNC(PatternDestroy);
        LOAD_FUNC(ObjectSetDestroy);
        LOAD_FUNC(PatternAddDouble);
        LOAD_FUNC(PatternAddString);
        LOAD_FUNC(FontMatch);
        LOAD_FUNC(CharSetCreate);
        LOAD_FUNC(PatternGetString);
        LOAD_FUNC(FontSetDestroy);
        LOAD_FUNC(PatternGetInteger);
        LOAD_FUNC(PatternAddBool);
        LOAD_FUNC(FontList);
        LOAD_FUNC(ObjectSetBuild);
        LOAD_FUNC(CharSetDestroy);
        LOAD_FUNC(ConfigSubstitute);
        LOAD_FUNC(DefaultSubstitute);
        LOAD_FUNC(PatternAddInteger);
        LOAD_FUNC(PatternCreate);
        LOAD_FUNC(PatternGetBool);
        LOAD_FUNC(PatternAddCharSet);
}
#undef LOAD_FUNC

static void
ensure_initialized(void) {
    if (!initialized) {
        load_fontconfig_lib();
        if (!FcInit()) fatal("Failed to initialize fontconfig library");
        initialized = true;
    }
}

static void
finalize(void) {
    if (initialized) {
        FcFini();
        dlclose(libfontconfig_handle);
        libfontconfig_handle = NULL;
        initialized = false;
    }
}

static PyObject*
pybool(FcBool x) { PyObject *ans = x ? Py_True: Py_False; Py_INCREF(ans); return ans; }

static PyObject*
pyspacing(int val) {
#define S(x) case FC_##x: return PyUnicode_FromString(#x)
    switch(val) { S(PROPORTIONAL); S(DUAL); S(MONO); S(CHARCELL); default: return PyUnicode_FromString("UNKNOWN"); }
#undef S
}


static PyObject*
pattern_as_dict(FcPattern *pat) {
    PyObject *ans = PyDict_New(), *p = NULL, *list = NULL;
    if (ans == NULL) return NULL;

#define PS(x) PyUnicode_Decode((const char*)x, strlen((const char*)x), "UTF-8", "replace")

#define G(type, get, which, conv, name) { \
    type out; \
    if (get(pat, which, 0, &out) == FcResultMatch) { \
        p = conv(out); if (p == NULL) goto exit; \
        if (PyDict_SetItemString(ans, #name, p) != 0) goto exit; \
        Py_CLEAR(p); \
    }}

#define L(type, get, which, conv, name) { \
    type out; int n = 0; \
    list = PyList_New(0); \
    if (!list) goto exit; \
    while (get(pat, which, n++, &out) == FcResultMatch) { \
        p = conv(out); if (p == NULL) goto exit; \
        if (PyList_Append(list, p) != 0) goto exit; \
        Py_CLEAR(p); \
    } \
    if (PyDict_SetItemString(ans, #name, list) != 0) goto exit; \
    Py_CLEAR(list); \
}
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key)
#define LS(which, key) L(FcChar8*, FcPatternGetString, which, PS, key)
#define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key)
#define B(which, key) G(int, FcPatternGetBool, which, pybool, key)
#define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key)
    S(FC_FILE, path);
    S(FC_FAMILY, family);
    S(FC_STYLE, style);
    S(FC_FULLNAME, full_name);
    S(FC_POSTSCRIPT_NAME, postscript_name);
    LS(FC_FONT_FEATURES, fontfeatures);
    I(FC_WEIGHT, weight);
    I(FC_WIDTH, width)
    I(FC_SLANT, slant);
    I(FC_HINT_STYLE, hint_style);
    I(FC_INDEX, index);
    I(FC_RGBA, subpixel);
    I(FC_LCD_FILTER, lcdfilter);
    B(FC_HINTING, hinting);
    B(FC_SCALABLE, scalable);
    B(FC_OUTLINE, outline);
    B(FC_COLOR, color);
    E(FC_SPACING, spacing, pyspacing);
exit:
    if (PyErr_Occurred()) Py_CLEAR(ans);
    Py_CLEAR(p);
    Py_CLEAR(list);

    return ans;
#undef PS
#undef S
#undef I
#undef B
#undef E
#undef G
#undef L
#undef LS
}

static PyObject*
font_set(FcFontSet *fs) {
    PyObject *ans = PyTuple_New(fs->nfont);
    if (ans == NULL) return NULL;
    for (int i = 0; i < fs->nfont; i++) {
        PyObject *d = pattern_as_dict(fs->fonts[i]);
        if (d == NULL) { Py_CLEAR(ans); break; }
        PyTuple_SET_ITEM(ans, i, d);
    }
    return ans;
}

#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_ValueError, "Failed to add %s to fontconfig pattern", desc, NULL); goto end; }

static PyObject*
fc_list(PyObject UNUSED *self, PyObject *args) {
    ensure_initialized();
    int allow_bitmapped_fonts = 0, spacing = -1;
    PyObject *ans = NULL;
    FcObjectSet *os = NULL;
    FcPattern *pat = NULL;
    FcFontSet *fs = NULL;
    if (!PyArg_ParseTuple(args, "|ip", &spacing, &allow_bitmapped_fonts)) return NULL;
    pat = FcPatternCreate();
    if (pat == NULL) return PyErr_NoMemory();
    if (!allow_bitmapped_fonts) {
        AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
        AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
    }
    if (spacing > -1) AP(FcPatternAddInteger, FC_SPACING, spacing, "spacing");
    os = FcObjectSetBuild(FC_FILE, FC_POSTSCRIPT_NAME, FC_FAMILY, FC_STYLE, FC_FULLNAME, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_HINT_STYLE, FC_INDEX, FC_HINTING, FC_SCALABLE, FC_OUTLINE, FC_COLOR, FC_SPACING, NULL);
    if (!os) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig object set"); goto end; }
    fs = FcFontList(NULL, pat, os);
    if (!fs) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig font set"); goto end; }
    ans = font_set(fs);
end:
    if (pat != NULL) FcPatternDestroy(pat);
    if (os != NULL) FcObjectSetDestroy(os);
    if (fs != NULL) FcFontSetDestroy(fs);
    return ans;
}

static PyObject*
_fc_match(FcPattern *pat) {
    FcPattern *match = NULL;
    PyObject *ans = NULL;
    FcResult result;
    FcConfigSubstitute(NULL, pat, FcMatchPattern);
    FcDefaultSubstitute(pat);
    /* printf("fc_match = %s\n", FcNameUnparse(pat)); */
    match = FcFontMatch(NULL, pat, &result);
    if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
    ans = pattern_as_dict(match);
end:
    if (match) FcPatternDestroy(match);
    return ans;
}

static char_type char_buf[1024];

static void
add_charset(FcPattern *pat, size_t num) {
    FcCharSet *charset = NULL;
    if (num) {
        charset = FcCharSetCreate();
        if (charset == NULL) { PyErr_NoMemory(); goto end; }
        for (size_t i = 0; i < num; i++) {
            if (!FcCharSetAddChar(charset, char_buf[i])) {
                PyErr_SetString(PyExc_RuntimeError, "Failed to add character to fontconfig charset");
                goto end;
            }
        }
        AP(FcPatternAddCharSet, FC_CHARSET, charset, "charset");
    }
end:
    if (charset != NULL) FcCharSetDestroy(charset);
}

static bool
_native_fc_match(FcPattern *pat, FontConfigFace *ans) {
    bool ok = false;
    FcPattern *match = NULL;
    FcResult result;
    FcConfigSubstitute(NULL, pat, FcMatchPattern);
    FcDefaultSubstitute(pat);
    /* printf("fc_match = %s\n", FcNameUnparse(pat)); */
    match = FcFontMatch(NULL, pat, &result);
    if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
    FcChar8 *out;
#define g(func, prop, output) if (func(match, prop, 0, &output) != FcResultMatch) { PyErr_SetString(PyExc_ValueError, "No " #prop " found in fontconfig match result"); goto end; }
    g(FcPatternGetString, FC_FILE, out);
    g(FcPatternGetInteger, FC_INDEX, ans->index);
    g(FcPatternGetInteger, FC_HINT_STYLE, ans->hintstyle);
    g(FcPatternGetBool, FC_HINTING, ans->hinting);
#undef g
    ans->path = strdup((char*)out);
    if (!ans->path) { PyErr_NoMemory(); goto end; }
    ok = true;
end:
    if (match != NULL) FcPatternDestroy(match);
    return ok;
}


bool
information_for_font_family(const char *family, bool bold, bool italic, FontConfigFace *ans) {
    ensure_initialized();
    memset(ans, 0, sizeof(FontConfigFace));
    FcPattern *pat = FcPatternCreate();
    bool ok = false;
    if (pat == NULL) { PyErr_NoMemory(); return ok; }
    if (family && strlen(family) > 0) AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
    if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
    if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
    ok = _native_fc_match(pat, ans);
end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ok;
}


static PyObject*
fc_match(PyObject UNUSED *self, PyObject *args) {
    ensure_initialized();
    char *family = NULL;
    int bold = 0, italic = 0, allow_bitmapped_fonts = 0, spacing = FC_MONO;
    double size_in_pts = 0, dpi = 0;
    FcPattern *pat = NULL;
    PyObject *ans = NULL;

    if (!PyArg_ParseTuple(args, "|zppipdd", &family, &bold, &italic, &spacing, &allow_bitmapped_fonts, &size_in_pts, &dpi)) return NULL;
    pat = FcPatternCreate();
    if (pat == NULL) return PyErr_NoMemory();

    if (family && strlen(family) > 0) AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
    if (spacing >= FC_DUAL) {
        // pass the family,monospace as the family parameter to fc-match,
        // which will fallback to using monospace if the family does not match.
        AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)"monospace", "family");
        AP(FcPatternAddInteger, FC_SPACING, spacing, "spacing");
    }
    if (!allow_bitmapped_fonts) {
        AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
        AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
    }
    if (size_in_pts > 0) { AP(FcPatternAddDouble, FC_SIZE, size_in_pts, "size"); }
    if (dpi > 0) { AP(FcPatternAddDouble, FC_DPI, dpi, "dpi"); }
    if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
    if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
    ans = _fc_match(pat);

end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ans;
}

static PyObject*
fc_match_postscript_name(PyObject UNUSED *self, PyObject *args) {
    ensure_initialized();
    const char *postscript_name = NULL;
    FcPattern *pat = NULL;
    PyObject *ans = NULL;

    if (!PyArg_ParseTuple(args, "s", &postscript_name)) return NULL;
    if (!postscript_name || !postscript_name[0]) { PyErr_SetString(PyExc_KeyError, "postscript_name must not be empty"); return NULL; }

    pat = FcPatternCreate();
    if (pat == NULL) return PyErr_NoMemory();

    AP(FcPatternAddString, FC_POSTSCRIPT_NAME, (const FcChar8*)postscript_name, "postscript_name");

    ans = _fc_match(pat);

end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ans;
}

PyObject*
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) {
    ensure_initialized();
    PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL;
    PyObject *idx = PyDict_GetItemString(base_descriptor, "index");
    if (p == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no path"); return NULL; }
    if (idx == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no index"); return NULL; }
    FcPattern *pat = FcPatternCreate();
    if (pat == NULL) return PyErr_NoMemory();
    long face_idx = MAX(0, PyLong_AsLong(idx));
    AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path");
    AP(FcPatternAddInteger, FC_INDEX, face_idx, "index");
    AP(FcPatternAddDouble, FC_SIZE, fg->font_sz_in_pts, "size");
    AP(FcPatternAddDouble, FC_DPI, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0, "dpi");
    ans = _fc_match(pat);
    if (face_idx > 0) {
        // For some reason FcFontMatch sets the index to zero, so manually restore it.
        PyDict_SetItemString(ans, "index", idx);
    }
end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ans;
}

bool
fallback_font(char_type ch, const char *family, bool bold, bool italic, bool prefer_color, FontConfigFace *ans) {
    ensure_initialized();
    memset(ans, 0, sizeof(FontConfigFace));
    bool ok = false;
    FcPattern *pat = FcPatternCreate();
    if (pat == NULL) { PyErr_NoMemory(); return ok; }
    if (family) AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
    if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
    if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
    if (prefer_color) { AP(FcPatternAddBool, FC_COLOR, true, "color"); }
    char_buf[0] = ch;
    add_charset(pat, 1);
    ok = _native_fc_match(pat, ans);
end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ok;
}

PyObject*
create_fallback_face(PyObject UNUSED *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
    ensure_initialized();
    PyObject *ans = NULL;
    FcPattern *pat = FcPatternCreate();
    if (pat == NULL) return PyErr_NoMemory();
    AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)(emoji_presentation ? "emoji" : "monospace"), "family");
    if (!emoji_presentation && bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
    if (!emoji_presentation && italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
    if (emoji_presentation) { AP(FcPatternAddBool, FC_COLOR, true, "color"); }
    size_t num = cell_as_unicode_for_fallback(cell, char_buf);
    add_charset(pat, num);
    PyObject *d = _fc_match(pat);
    if (d) { ans = face_from_descriptor(d, fg); Py_CLEAR(d); }
end:
    if (pat != NULL) FcPatternDestroy(pat);
    return ans;
}

#undef AP
static PyMethodDef module_methods[] = {
    METHODB(fc_list, METH_VARARGS),
    METHODB(fc_match, METH_VARARGS),
    METHODB(fc_match_postscript_name, METH_VARARGS),
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

bool
init_fontconfig_library(PyObject *module) {
    register_at_exit_cleanup_func(FONTCONFIG_CLEANUP_FUNC, finalize);
    if (PyModule_AddFunctions(module, module_methods) != 0) return false;
    PyModule_AddIntMacro(module, FC_WEIGHT_REGULAR);
    PyModule_AddIntMacro(module, FC_WEIGHT_MEDIUM);
    PyModule_AddIntMacro(module, FC_WEIGHT_SEMIBOLD);
    PyModule_AddIntMacro(module, FC_WEIGHT_BOLD);
    PyModule_AddIntMacro(module, FC_SLANT_ITALIC);
    PyModule_AddIntMacro(module, FC_SLANT_ROMAN);
    PyModule_AddIntMacro(module, FC_PROPORTIONAL);
    PyModule_AddIntMacro(module, FC_DUAL);
    PyModule_AddIntMacro(module, FC_MONO);
    PyModule_AddIntMacro(module, FC_CHARCELL);
    PyModule_AddIntMacro(module, FC_WIDTH_NORMAL);

    return true;
}
