// The Klammertext font store: INFRASTRUCTURE, not part of the
// Klammermachine (the Machine class never references it) and not part of
// any klammer set.  Klammer sets (the SKS's @document, a future music
// set) consume the store; the kdesc command lists, installs into, and
// samples it without loading any klammer set.  The store's search runs
// over the KLAMMERTEXT_FONTS directories and ends at the distribution's
// default font set in $KLAMMERTEXT_HOME/fnt.

#include "font_store.h"
#include "file.h"
#include "locator.h"
#include "util.h"
#include "error.h"
#include "log.h"
#include "show.h"

#include <algorithm>
#include <fstream>
#include <regex>
#include <set>
#include <sstream>
#include <filesystem>

namespace fs = std::filesystem;


// A filesystem extension lowercased for case-insensitive matching, so a
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
static std::string lower_extension(const fs::path& path)
{
    std::string ext = path.extension();
    for (char& c : ext) {
        c = std::tolower(static_cast<unsigned char>(c));
    }
    return ext;
}


std::string name_to_dirname(const std::string& name)
{
    std::string result {};
    for (char c : name) {
        if (c == ' ')
            result += '-';
        else
            result += std::tolower(c);
    }
    return result;
}


// The directories searched for installed fonts: the colon-separated
// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it
// is not set.  Directories are searched in listed order, and the default
// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font
// can deliberately shadow a default one.
strings_t installed_font_dirs()
{
    strings_t result {};
    std::string paths {};
    const char* env = std::getenv("KLAMMERTEXT_FONTS");
    if (env && *env) {
        paths = env;
    } else if (const char* home = std::getenv("HOME"); home && *home) {
        paths = std::string(home) + "/.klammertext/fonts";
    }
    std::stringstream ss(paths);
    std::string dir;
    while (std::getline(ss, dir, ':')) {
        if (!dir.empty()) {
            result.push_back(dir);
        }
    }
    return result;
}

std::string default_font_dir()
{
    const char* home = std::getenv(klammertext_home_var.c_str());
    if (home == nullptr || *home == '\0') {
        throw Argument_error(
            "The environment variable " + klammertext_home_var + " is not defined");
    }
    return std::string(home) + "/fnt";
}


static void classify_from_css(Resolved_font& font, std::string css_path)
{
    // Parse the @font-face blocks in the CSS to determine variant → filename mapping
    std::string css = string_from_file(css_path);
    std::regex face_rgx(
        R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
        std::regex::multiline);
    auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
    auto end = std::sregex_iterator();
    for (auto it = begin; it != end; ++it) {
        std::string style = (*it)[1];
        std::string weight = (*it)[2];
        std::string url_path = (*it)[3];
        // URL is like 'dir-name/Filename.ttf' — extract just the filename
        std::string filename = url_path.substr(url_path.rfind('/') + 1);
        bool is_bold = (weight == "700" || weight == "bold");
        bool is_italic = (style == "italic" || style == "oblique");
        if (is_bold && is_italic)
            font.bold_italic = filename;
        else if (is_bold)
            font.bold = filename;
        else if (is_italic)
            font.italic = filename;
        else
            font.regular = filename;
    }
}

// Resolve a font within one base directory: <base>/<dir-name>/ holding the
// .ttf files and <base>/<dir-name>.css declaring the @font-face variants.
static Resolved_font resolve_in_directory(
    const std::string& family_name, const std::string& dir_name, const std::string& base_dir)
{
    std::string font_dir = base_dir + "/" + dir_name;
    std::string css_file = font_dir + ".css";
    if (fs::exists(font_dir) && fs::exists(css_file)) {
        Resolved_font font {};
        font.family_name = family_name;
        font.dir_name = dir_name;
        font.font_dir = font_dir;
        font.css_file = css_file;
        classify_from_css(font, css_file);
        return font;
    }
    return {};
}

// Every font available for the three @document role parameters: each
// <name>.css with a matching <name>/ directory, across the installed
// directories and the default font set.  The reported name is the family name
// from the CSS (what the writer types), falling back to the directory name.
strings_t available_font_families()
{
    strings_t result {};
    static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
    auto scan = [&](const std::string& base) {
        if (!fs::exists(base)) {
            return;
        }
        for (auto& entry : fs::directory_iterator(base)) {
            if (entry.path().extension() == ".css" &&
                fs::is_directory(base + "/" + entry.path().stem().string())) {
                std::string css = string_from_file(entry.path());
                std::smatch match {};
                if (std::regex_search(css, match, family_rgx)) {
                    result.push_back(match[1]);
                } else {
                    result.push_back(entry.path().stem());
                }
            }
        }
    };
    for (const std::string& dir : installed_font_dirs()) {
        scan(dir);
    }
    scan(default_font_dir());
    std::sort(result.begin(), result.end());
    result.erase(std::unique(result.begin(), result.end()), result.end());
    return result;
}


static void extract_font_metrics(Resolved_font& font)
{
    if (font.regular.empty() || font.font_dir.empty())
        return;
    std::string ttf_path = font.font_dir + "/" + font.regular;
    if (!fs::exists(ttf_path))
        return;
    // Extract both x-height and cap-height ratios from OS/2 table
    std::string script =
        "python3 -c \""
        "import struct; "
        "f = open('" + ttf_path + "', 'rb'); "
        "_, n = struct.unpack('>IH', f.read(6)); "
        "f.read(6); "
        "t = {};\n"
        "for _ in range(n):\n"
        "    tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
        "    _, o, l = struct.unpack('>III', f.read(12)); "
        "    t[tag] = o\n"
        "f.seek(t['head'] + 18); "
        "upm = struct.unpack('>H', f.read(2))[0]; "
        "f.seek(t['OS/2']); "
        "ver = struct.unpack('>H', f.read(2))[0]; "
        "f.seek(t['OS/2'] + 86); "
        "xh, ch = struct.unpack('>hh', f.read(4)); "
        "print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
        "f.close()\"";
    std::string result = trim(exec(script.c_str()));
    if (!result.empty()) {
        try {
            auto pos = result.find(' ');
            if (pos != std::string::npos) {
                font.xheight_ratio = std::stof(result.substr(0, pos));
                font.capheight_ratio = std::stof(result.substr(pos + 1));
            }
        } catch (...) {}
    }
}

Resolved_font resolve_font(const std::string& family_name)
{
    if (family_name.empty())
        return {};

    std::string dir_name = name_to_dirname(family_name);

    // Installed directories in listed order, then the default font set, so an
    // installed font can shadow a default one.
    strings_t bases = installed_font_dirs();
    bases.push_back(default_font_dir());
    for (const std::string& base : bases) {
        Resolved_font font = resolve_in_directory(family_name, dir_name, base);
        if (!font.family_name.empty()) {
            extract_font_metrics(font);
            // A DERIVED value: an installed font may shadow a default one, and
            // which directory won is not readable from ":serif_font Whatever".
            // The commonest font question -- "why does it not look like the
            // one I installed?" -- is answered by this line.
            (void)K::log(1, "Font \"" + family_name + "\": " + base + "/" + dir_name);
            return font;
        }
    }

    std::stringstream ss {};
    ss << "The font \"" << family_name << "\" is not installed.\n\n"
       << "Available fonts:\n";
    for (const std::string& name : available_font_families()) {
        ss << "  " << name << "\n";
    }
    ss << "\nFonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
       << "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
       << "and then in the default font set.  To install a font,\n"
       << "place its files as <fonts-dir>/" << dir_name << "/*.ttf with a\n"
       << "<fonts-dir>/" << dir_name << ".css declaring its @font-face variants.";
    throw Argument_error(ss.str());
}


// Font file classification: read the family name, weight, and style from
// the font's internal tables (sfnt 'name', 'OS/2', 'fvar') rather than
// from filenames, which vary by source (Google zips, foundries, ...).

namespace {

uint16_t be16(const std::string& d, size_t off)
{
    return (uint8_t(d[off]) << 8) | uint8_t(d[off + 1]);
}

uint32_t be32(const std::string& d, size_t off)
{
    return (uint32_t(be16(d, off)) << 16) | be16(d, off + 2);
}

std::string read_binary_file(const std::string& path)
{
    std::ifstream in(path, std::ios::binary);
    std::stringstream ss {};
    ss << in.rdbuf();
    return ss.str();
}

// Decode a name-table string: UTF-16BE for Windows records (keep the BMP
// low bytes; family names are almost always Latin), bytes as-is otherwise.
std::string decode_name(const std::string& raw, bool utf16be)
{
    std::string result {};
    if (utf16be) {
        for (size_t i = 0; i + 1 < raw.size(); i += 2) {
            if (raw[i] == 0) {
                result += raw[i + 1];
            }
        }
    } else {
        result = raw;
    }
    return result;
}

} // namespace

Font_file classify_font_file(const std::string& path)
{
    Font_file file {};
    file.path = path;
    // Normalize to lowercase so the installed filename, its @font-face url,
    // and the truetype/opentype format detection are uniform regardless of
    // how the source file's extension was capitalized.
    file.extension = lower_extension(path);

    std::string d = read_binary_file(path);
    if (d.size() < 12) {
        file.note = "not a font file (too short)";
        return file;
    }
    uint32_t tag = be32(d, 0);
    if (tag == 0x74746366) {  // 'ttcf'
        file.note = "font collections (.ttc) are not supported; "
                    "use the individual font files";
        return file;
    }
    if (tag != 0x00010000 && tag != 0x4F54544F) {  // sfnt or 'OTTO'
        file.note = "not a TrueType or OpenType font";
        return file;
    }

    uint16_t num_tables = be16(d, 4);
    std::map<std::string, std::pair<uint32_t, uint32_t>> tables {};  // tag -> offset,length
    for (uint16_t i = 0; i < num_tables; i++) {
        size_t rec = 12 + i * 16;
        if (rec + 16 > d.size()) {
            break;
        }
        tables[d.substr(rec, 4)] = { be32(d, rec + 8), be32(d, rec + 12) };
    }
    file.variable = tables.contains("fvar");

    // Family name from the 'name' table: typographic family (16) wins
    // over family (1); Windows records (platform 3) win over Macintosh.
    if (auto it = tables.find("name"); it != tables.end()) {
        size_t base = it->second.first;
        uint16_t count = be16(d, base + 2);
        uint16_t string_offset = be16(d, base + 4);
        int best_rank = -1;
        for (uint16_t i = 0; i < count; i++) {
            size_t rec = base + 6 + i * 12;
            if (rec + 12 > d.size()) {
                break;
            }
            uint16_t platform = be16(d, rec);
            uint16_t name_id = be16(d, rec + 6);
            uint16_t length = be16(d, rec + 8);
            uint16_t offset = be16(d, rec + 10);
            if (name_id != 1 && name_id != 16) {
                continue;
            }
            int rank = (name_id == 16 ? 2 : 0) + (platform == 3 ? 1 : 0);
            size_t at = base + string_offset + offset;
            if (rank > best_rank && at + length <= d.size()) {
                file.family = decode_name(d.substr(at, length), platform == 3);
                best_rank = rank;
            }
        }
    }
    if (file.family.empty()) {
        file.note = "no family name found in the font's name table";
        return file;
    }

    bool italic = false;
    if (auto it = tables.find("OS/2"); it != tables.end()) {
        size_t base = it->second.first;
        file.weight = be16(d, base + 4);
        italic = be16(d, base + 62) & 0x0001;  // fsSelection italic bit
    } else if (auto ht = tables.find("head"); ht != tables.end()) {
        uint16_t mac_style = be16(d, ht->second.first + 44);
        file.weight = (mac_style & 0x0001) ? 700 : 400;
        italic = mac_style & 0x0002;
    }

    if (file.variable) {
        // A variable font covers the weight axis; use it as the regular
        // (or italic) face and let renderers derive weights.
        file.variant = italic ? "Italic" : "Regular";
    } else if (file.weight >= 380 && file.weight <= 450) {
        file.variant = italic ? "Italic" : "Regular";
    } else if (file.weight >= 650 && file.weight <= 760) {
        file.variant = italic ? "BoldItalic" : "Bold";
    } else {
        std::stringstream note {};
        note << "weight " << file.weight
             << " not installed (only regular 400 and bold 700 are used)";
        file.note = note.str();
    }
    return file;
}

std::vector<Font_file> classify_font_files(const std::string& directory)
{
    std::vector<Font_file> result {};
    if (!fs::exists(directory)) {
        throw Argument_error(
            "The font directory \"" + directory + "\" does not exist");
    }
    for (auto& entry : fs::recursive_directory_iterator(directory)) {
        std::string ext = lower_extension(entry.path());
        if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
            result.push_back(classify_font_file(entry.path()));
        }
    }
    return result;
}

// The css is always generated, never copied, so its urls are relative and
// the installed pair stays relocatable.
static std::string font_face_css(
    const std::string& family, const std::string& dir_name,
    const std::map<std::string, const Font_file*>& slots)
{
    std::stringstream css {};
    auto emit = [&](const std::string& variant,
                    const std::string& style, const std::string& weight) {
        auto it = slots.find(variant);
        if (it == slots.end()) {
            return;
        }
        std::string format =
            it->second->extension == ".otf" ? "opentype" : "truetype";
        css << "\n@font-face {\n"
            << "  font-family: '" << family << "';\n"
            << "  font-style: " << style << ";\n"
            << "  font-weight: " << weight << ";\n"
            << "  src: url('" << dir_name << "/" << variant
            << it->second->extension << "') format('" << format << "');\n"
            << "}\n";
    };
    emit("Regular", "normal", "400");
    emit("Bold", "normal", "700");
    emit("Italic", "italic", "400");
    emit("BoldItalic", "italic", "700");
    return css.str();
}

std::string install_fonts(const std::string& source_dir, std::string dest_dir)
{
    if (dest_dir.empty()) {
        strings_t dirs = installed_font_dirs();
        if (dirs.empty()) {
            throw Argument_error(
                "No installation directory: KLAMMERTEXT_FONTS is empty and "
                "HOME is not set");
        }
        dest_dir = dirs[0];
    }
    std::vector<Font_file> files = classify_font_files(source_dir);
    if (files.empty()) {
        throw Argument_error(
            "No font files (.ttf or .otf) found under \"" + source_dir + "\"");
    }

    // Choose one file per (family, variant) slot; a static face wins over
    // a variable font's derived face.
    std::map<std::string, std::map<std::string, const Font_file*>> families {};
    std::stringstream report {};
    for (const Font_file& file : files) {
        if (file.variant.empty()) {
            report << "  skipped " << fs::path(file.path).filename().string()
                   << ": " << file.note << "\n";
            continue;
        }
        auto& slots = families[file.family];
        auto it = slots.find(file.variant);
        if (it == slots.end() || (it->second->variable && !file.variable)) {
            slots[file.variant] = &file;
        }
    }

    for (auto& [family, slots] : families) {
        std::string dir_name = name_to_dirname(family);
        std::string family_dir = dest_dir + "/" + dir_name;
        fs::create_directories(family_dir);
        strings_t variants {};
        for (auto& [variant, file] : slots) {
            copy_file_stream(file->path,
                             family_dir + "/" + variant + file->extension);
            variants.push_back(variant + (file->variable ? " (variable)" : ""));
        }
        string_to_file(dest_dir + "/" + dir_name + ".css",
                       font_face_css(family, dir_name, slots));
        report << "  installed \"" << family << "\" (" << join(variants, ", ")
               << ") in " << family_dir << "\n";
    }
    return report.str();
}

std::string describe_fonts()
{
    std::stringstream ss {};
    std::set<std::string> seen {};
    strings_t bases = installed_font_dirs();
    bases.push_back(default_font_dir());
    for (size_t i = 0; i < bases.size(); i++) {
        const std::string& base = bases[i];
        bool is_default = (i == bases.size() - 1);
        ss << base << (is_default ? " (default font set)" : "") << ":\n";
        if (!fs::exists(base)) {
            ss << "  [directory does not exist]\n";
            continue;
        }
        strings_t names {};
        for (auto& entry : fs::directory_iterator(base)) {
            if (entry.path().extension() == ".css" &&
                fs::is_directory(base + "/" + entry.path().stem().string())) {
                names.push_back(entry.path().stem());
            }
        }
        std::sort(names.begin(), names.end());
        if (names.empty()) {
            ss << "  [no fonts]\n";
        }
        for (const std::string& dir_name : names) {
            Resolved_font font = resolve_in_directory("?", dir_name, base);
            std::string css = string_from_file(font.css_file);
            std::smatch match {};
            std::string family = dir_name;
            if (std::regex_search(css, match,
                                  std::regex(R"(font-family:\s*'([^']+)')"))) {
                family = match[1];
            }
            strings_t variants {};
            if (!font.regular.empty()) variants.push_back("Regular");
            if (!font.bold.empty()) variants.push_back("Bold");
            if (!font.italic.empty()) variants.push_back("Italic");
            if (!font.bold_italic.empty()) variants.push_back("BoldItalic");
            ss << "  " << family << " (" << join(variants, ", ") << ")";
            if (seen.contains(dir_name)) {
                ss << "  [shadowed by an earlier directory]";
            }
            seen.insert(dir_name);
            ss << "\n";
        }
    }
    return ss.str();
}

std::string write_font_samples(const std::string& output_dir, const std::string& source_dir)
{
    fs::create_directories(output_dir);
    strings_t families {};
    if (source_dir.empty()) {
        for (const std::string& family : available_font_families()) {
            install_resolved_font(resolve_font(family), output_dir);
            families.push_back(family);
        }
    } else {
        // Uninstalled preview: install the classified fonts directly into
        // the sample page's own fonts directory.
        install_fonts(source_dir, output_dir + "/fonts");
        for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) {
            if (entry.path().extension() == ".css") {
                std::string css = string_from_file(entry.path());
                std::smatch match {};
                if (std::regex_search(css, match,
                                      std::regex(R"(font-family:\s*'([^']+)')"))) {
                    families.push_back(match[1]);
                }
            }
        }
        std::sort(families.begin(), families.end());
    }

    std::stringstream html {};
    html << "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
         << "<meta charset=\"UTF-8\">\n<title>Klammertext font samples</title>\n";
    for (const std::string& family : families) {
        html << "<link href=\"fonts/" << name_to_dirname(family)
             << ".css\" rel=\"stylesheet\">\n";
    }
    html << "<style>\n"
         << "body { margin: 2rem auto; max-width: 46rem; font-family: sans-serif; }\n"
         << "h2 { border-bottom: 1px solid #999; margin-top: 2.5rem; }\n"
         << ".sample { font-size: 1.3rem; margin: .3rem 0; }\n"
         << ".alphabet { font-size: 1.0rem; color: #333; margin: .3rem 0; }\n"
         << "</style>\n</head>\n<body>\n<h1>Klammertext font samples</h1>\n";
    for (const std::string& family : families) {
        html << "<h2>" << family << "</h2>\n"
             << "<div style=\"font-family: '" << family << "'\">\n"
             << "<p class=\"sample\">The quick brown fox jumps over the lazy dog.</p>\n"
             << "<p class=\"sample\" style=\"font-style: italic\">"
             << "The quick brown fox jumps over the lazy dog.</p>\n"
             << "<p class=\"sample\" style=\"font-weight: bold\">"
             << "The quick brown fox jumps over the lazy dog.</p>\n"
             << "<p class=\"sample\" style=\"font-weight: bold; font-style: italic\">"
             << "The quick brown fox jumps over the lazy dog.</p>\n"
             << "<p class=\"alphabet\">ABCDEFGHIJKLMNOPQRSTUVWXYZ "
             << "abcdefghijklmnopqrstuvwxyz 0123456789 "
             << "&auml;&ouml;&uuml;&szlig; &ldquo;quoted&rdquo; 3.14159</p>\n"
             << "</div>\n";
    }
    html << "</body>\n</html>\n";
    std::string index = output_dir + "/index.html";
    string_to_file(index, html.str());
    return index;
}

// Font assets are copied with copy_file_stream() (mac/file.h) rather than
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
// — see the note on copy_file_stream() in file.cpp for the full rationale.


void install_resolved_font(const Resolved_font& font, std::string output_dir)
{
    if (font.family_name.empty())
        return;

    std::string output_font_dir = output_dir + "/fonts";
    if (!fs::exists(output_font_dir))
        fs::create_directory(output_font_dir);

    // Copy .css file and font directory to output
    std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
    std::string dest_dir = output_font_dir + "/" + font.dir_name;

    copy_file_stream(font.css_file, dest_css);

    if (!fs::exists(dest_dir)) {
        fs::create_directory(dest_dir);
        for (auto& entry : fs::directory_iterator(font.font_dir)) {
            copy_file_stream(entry.path(),
                             dest_dir + "/" + entry.path().filename().string());
        }
    }
}
