// SPDX-License-Identifier: MIT
// Font render.
// Copyright (C) 2022 Artem Senichev <artemsen@gmail.com>

#include "font.hpp"

#include "defaults.hpp"
#include "log.hpp"

#include <fontconfig/fontconfig.h>

#include <memory>

namespace {

constexpr const char FALLBACK_CHR = '?'; // character used for absent glyphs

constexpr size_t POINT_FACTOR = 64;  // default points per pixel (26.6 format)
constexpr size_t MAX_TEXT_LEN = 120; // max length of text line (characters)

/** Font config wrapper.*/
class FontConfig {
public:
    using FcConfigPtr = std::unique_ptr<FcConfig, decltype(&FcConfigDestroy)>;
    using FcPatternPtr =
        std::unique_ptr<FcPattern, decltype(&FcPatternDestroy)>;

    FontConfig()
    {
        if (!FcInit()) {
            Log::error("Unable to initialize FontConfig");
        }
    }

    ~FontConfig() { FcFini(); }

    /**
     * Get path to the font file by its name.
     * @param name font name
     * @return path to the file or empty string if font not found
     */
    static std::filesystem::path get_font_file(const char* name)
    {
        const FcConfigPtr fc =
            FcConfigPtr(FcInitLoadConfigAndFonts(), &FcConfigDestroy);
        if (!fc) {
            return {};
        }

        const FcPatternPtr fc_name =
            FcPatternPtr(FcNameParse(reinterpret_cast<const FcChar8*>(name)),
                         FcPatternDestroy);
        if (!fc_name) {
            return {};
        }
        FcConfigSubstitute(fc.get(), fc_name.get(), FcMatchPattern);
        FcDefaultSubstitute(fc_name.get());

        FcResult result;
        const FcPatternPtr fc_font = FcPatternPtr(
            FcFontMatch(fc.get(), fc_name.get(), &result), FcPatternDestroy);
        if (fc_font) {
            FcChar8* path = nullptr;
            if (FcPatternGetString(fc_font.get(), FC_FILE, 0, &path) ==
                FcResultMatch) {
                return reinterpret_cast<const char*>(path);
            }
        }

        return {};
    }
};

/** FreeType lib wrapper.*/
struct FreeTypeLib {
    FreeTypeLib() noexcept
    {
        FT_Error rc;
        rc = FT_Init_FreeType(&lib);
        if (rc != 0) {
            Log::error("Unable to initialize FreeType: {}",
                       FreeTypeLib::error_text(rc));
        }
    }

    ~FreeTypeLib()
    {
        if (lib) {
            FT_Done_FreeType(lib);
        }
    }

    /**
     * Get error message for specified code.
     * @param rc error code
     * @return error message
     */
    static const char* error_text(const FT_Error rc)
    {
        const char* text = FT_Error_String(rc);
        return text ? text : "unknown error";
    }

    operator FT_Library() { return lib; }

private:
    FT_Library lib = nullptr; ///< Font lib instance
};

FreeTypeLib ft_lib;

} // anonymous namespace

Font::Font()
    : ft_face(nullptr)
    , size(Defaults::text::size)
    , scale(1.0)
{
}

Font::~Font()
{
    if (ft_face) {
        FT_Done_Face(ft_face);
    }
}

bool Font::load(const std::string& name)
{
    if (!ft_lib) {
        return false;
    }

    // get font file via FontConfig
    const FontConfig fcinit;
    const std::filesystem::path path = FontConfig::get_font_file(name.c_str());
    if (path.empty()) {
        Log::error("Unable to find font {}", name);
        return false;
    }

    return load(path);
}

bool Font::load(const std::filesystem::path& path)
{
    if (!ft_lib) {
        return false;
    }

    FT_Face face;
    const FT_Error rc = FT_New_Face(ft_lib, path.c_str(), 0, &face);
    if (rc != 0) {
        Log::error("Unable to load font from {}: {}", path.string(),
                   FreeTypeLib::error_text(rc));
        return false;
    }
    set_face(face);

    return true;
}

bool Font::load(const uint8_t* data, const size_t data_size)
{
    if (!ft_lib) {
        return false;
    }

    FT_Face face;
    const FT_Error rc = FT_New_Memory_Face(ft_lib, data, data_size, 0, &face);
    if (rc != 0) {
        Log::error("Unable to load font: {}", FreeTypeLib::error_text(rc));
        return false;
    }
    set_face(face);

    return true;
}

void Font::set_face(FT_Face face)
{
    if (ft_face) {
        FT_Done_Face(ft_face);
    }
    ft_face = face;

    set_size(size);
}

const char* Font::name() const
{
    return ft_face ? ft_face->family_name : nullptr;
}

void Font::set_size(const size_t size)
{
    this->size = size;
    if (ft_face) {
        FT_Set_Pixel_Sizes(ft_face, 0, size * scale);
    }
}

void Font::set_scale(const double scale)
{
    this->scale = scale;
    set_size(size);
}

std::wstring Font::to_wide(const std::string& text)
{
    size_t len = text.length();
    std::wstring wide(len + 1, 0);

    len = std::mbstowcs(wide.data(), text.c_str(), len * sizeof(wide[0]));
    if (len != std::wstring::npos) {
        wide.resize(len);
    } else {
        // something wrong with locale, try to convert ASCII
        wide.clear();
        for (const auto chr : text) {
            wide += chr < ' ' || chr > '~' ? FALLBACK_CHR : chr;
        }
        len = wide.length();
    }

    if (len > MAX_TEXT_LEN) {
        wide.resize(MAX_TEXT_LEN - 1);
        wide += L'…';
    }

    return wide;
}

Pixmap Font::render(const std::string& text)
{
    if (text.empty()) {
        return {};
    }
    if (!ft_face && !load(std::string(Defaults::text::font))) {
        return {};
    }

    const std::wstring wide = to_wide(text);

    // calculate total width in pixels
    size_t width = 0;
    for (const wchar_t ch : wide) {
        const FT_UInt index = FT_Get_Char_Index(ft_face, ch);
        if (index == 0 ||
            FT_Load_Glyph(ft_face, index, FT_LOAD_RENDER) != FT_Err_Ok) {
            FT_Load_Char(ft_face, FALLBACK_CHR, FT_LOAD_RENDER);
        }
        width += ft_face->glyph->advance.x / POINT_FACTOR;
    }

    // calculate text height in pixels
    const size_t height_base = ft_face->size->metrics.height / POINT_FACTOR;
    const size_t height = height_base + height_base / 3; // dirty hack

    // horizontal padding
    const size_t hpadding = height_base / 3;

    Pixmap pm;
    pm.create(Pixmap::GS, width + hpadding * 2, height);

    // draw glyphs
    size_t x = hpadding;
    for (const wchar_t ch : wide) {
        const FT_UInt index = FT_Get_Char_Index(ft_face, ch);
        if (index == 0 ||
            FT_Load_Glyph(ft_face, index, FT_LOAD_RENDER) != FT_Err_Ok) {
            FT_Load_Char(ft_face, FALLBACK_CHR, FT_LOAD_RENDER);
        }
        const FT_GlyphSlot& glyph = ft_face->glyph;
        const FT_Bitmap* bmp = &glyph->bitmap;
        const size_t x_start = x + glyph->bitmap_left;
        const size_t y_start = height_base - glyph->bitmap_top;
        for (size_t y = 0; y < bmp->rows; ++y) {
            if (y + y_start < pm.height() &&
                x_start + bmp->width < pm.width()) {
                uint8_t* dst =
                    reinterpret_cast<uint8_t*>(pm.ptr(x_start, y + y_start));
                std::memcpy(dst, &bmp->buffer[y * bmp->pitch], bmp->width);
            }
        }
        x += glyph->advance.x / POINT_FACTOR;
    }

    return pm;
}
