#include <Rcpp.h>
#include "microtex.h"
#include "graphic/graphic.h"
#include "graphic/graphic_recorder.h"
#include "atom/font_family_atom.h"
#include "macro/macro.h"
#include "atom/mark_atom.h"
#include "atom/image_atom.h"
#include "utils/bidi.h"
#include "utils/utf.h"
#include "unimath/font_src.h"
#include "unimath/uni_font.h"
#include "otf/otf.h"
#include "otf/glyph.h"

#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>

// CLM v6 synthesis from a font file, in the layout engine.
#include "otf/otf_math_reader.h"

using namespace microtex;

// --- Global R callback for text measurement ---
// When set, TextLayout_R::getBounds() calls this R function to get
// accurate font metrics from R's graphics system instead of heuristics.
// Use NULL (not R_NilValue) for static init to avoid DLL load ordering issues.
static SEXP g_text_measure_fn = nullptr;

// Per-measurer cache of (src, style) -> (width, ascent, height) ratios
// relative to font size. Cleared whenever the measurer is swapped so stale
// metrics from a different gp$fontfamily/fontface never leak across calls.
static std::unordered_map<std::string, std::array<float, 3>> g_text_measure_cache;

static bool has_text_measurer() {
    return g_text_measure_fn != nullptr && g_text_measure_fn != R_NilValue;
}

// [[Rcpp::export]]
void register_text_measurer(SEXP fn) {
    if (has_text_measurer()) {
        R_ReleaseObject(g_text_measure_fn);
    }
    g_text_measure_fn = fn;
    R_PreserveObject(g_text_measure_fn);
    g_text_measure_cache.clear();
}

// [[Rcpp::export]]
void clear_text_measurer() {
    if (has_text_measurer()) {
        R_ReleaseObject(g_text_measure_fn);
    }
    g_text_measure_fn = nullptr;
    g_text_measure_cache.clear();
}


// --- UTF-8 helpers for text metrics ---

// Decode one UTF-8 codepoint, advance ptr. Returns 0 on end/error.
static u32 next_codepoint(const char*& p, const char* end) {
    if (p >= end) return 0;
    u32 cp;
    unsigned char c = static_cast<unsigned char>(*p);
    if (c < 0x80) {
        cp = c; p += 1;
    } else if (c < 0xE0) {
        if (p + 1 >= end) { p = end; return 0; }
        cp = (c & 0x1F) << 6 | (p[1] & 0x3F); p += 2;
    } else if (c < 0xF0) {
        if (p + 2 >= end) { p = end; return 0; }
        cp = (c & 0x0F) << 12 | (p[1] & 0x3F) << 6 | (p[2] & 0x3F); p += 3;
    } else {
        if (p + 3 >= end) { p = end; return 0; }
        cp = (c & 0x07) << 18 | (p[1] & 0x3F) << 12 | (p[2] & 0x3F) << 6 | (p[3] & 0x3F);
        p += 4;
    }
    return cp;
}

// Heuristic: is this codepoint a fullwidth/CJK character?
static bool is_fullwidth(u32 cp) {
    return (cp >= 0x3000 && cp <= 0x303F)   // CJK punctuation
        || (cp >= 0x3040 && cp <= 0x309F)   // Hiragana
        || (cp >= 0x30A0 && cp <= 0x30FF)   // Katakana
        || (cp >= 0x4E00 && cp <= 0x9FFF)   // CJK Unified Ideographs
        || (cp >= 0xF900 && cp <= 0xFAFF)   // CJK Compatibility Ideographs
        || (cp >= 0xFF00 && cp <= 0xFFEF)   // Fullwidth forms
        || (cp >= 0x20000 && cp <= 0x2FA1F) // CJK Extension B+ and Compatibility Supplement
        || (cp >= 0xAC00 && cp <= 0xD7AF);  // Hangul Syllables
}

namespace microtex {

// Cache of fontFile → FontContext id. Built lazily on demand — FontContext
// assigns sequential ids on addFont(), so a linear scan of getFont(i) finds
// a match; we memoize so subsequent glyphs hit the cache directly.
static std::unordered_map<std::string, i32> g_font_id_cache;

static sptr<const OtfFont> lookup_otf_font_by_file(const std::string& fontFile) {
    if (fontFile.empty()) return nullptr;
    auto it = g_font_id_cache.find(fontFile);
    if (it != g_font_id_cache.end()) {
        return FontContext::getFont(it->second);
    }
    for (i32 id = 0; ; ++id) {
        auto f = FontContext::getFont(id);
        if (!f) break;
        if (f->fontFile == fontFile) {
            g_font_id_cache[fontFile] = id;
            return f;
        }
    }
    return nullptr;
}

// Shared with graphic_recorder.cpp: glyph advance width (in world units at
// fontSize) used to compensate for horizontal flips on drawGlyph.
float measure_glyph_advance(const std::string& fontFile, u16 glyphId, float fontSize) {
    auto f = lookup_otf_font_by_file(fontFile);
    if (!f) return 0.f;
    const auto& o = f->otf();
    const Glyph* g = o.glyph(glyphId);
    if (!g) return 0.f;
    u16 em = o.em();
    if (em == 0) return 0.f;
    return g->metrics().width() * fontSize / static_cast<float>(em);
}

// Cache key for one measured run. The style goes in as both its bytes:
// the high one carries the font family index, so keying on the low byte
// alone would measure two different families as whichever was asked for
// first. The prefix is fixed width, so no (style, src) pair can alias.
std::string text_measure_key(const std::string& text, int fontStyle) {
    std::string key;
    key.reserve(text.size() + 4);
    key.push_back(static_cast<char>(fontStyle & 0xFF));
    key.push_back(static_cast<char>((fontStyle >> 8) & 0xFF));
    key.push_back('\x01');
    key.append(text);
    return key;
}

// Shared with graphic_recorder.cpp: width lookup used to compensate for
// horizontal flips (e.g. \reflectbox) at drawTextRun time.
float measure_cached_text_width(const std::string& text, int fontStyle, float fontSize) {
    if (has_text_measurer()) {
        const std::string key = text_measure_key(text, fontStyle);
        auto it = g_text_measure_cache.find(key);
        if (it != g_text_measure_cache.end()) {
            return it->second[0] * fontSize;
        }
    }
    float total = 0.f;
    const char* p = text.c_str();
    const char* end = p + text.size();
    while (p < end) {
        u32 cp = next_codepoint(p, end);
        if (cp == 0) break;
        total += is_fullwidth(cp) ? fontSize : fontSize * 0.55f;
    }
    return total;
}

}  // namespace microtex

// --- TextLayout implementation that emits TEXT records ---

class TextLayout_R : public TextLayout {
public:
    TextLayout_R(const std::string& src, FontStyle style, float size)
        : _src(src), _style(style), _size(size) {}

    void getBounds(Rect& bounds) override {
        // Try R callback for accurate font metrics
        if (has_text_measurer()) {
            std::string key = text_measure_key(_src, static_cast<int>(_style));
            auto it = g_text_measure_cache.find(key);
            if (it != g_text_measure_cache.end()) {
                bounds.x = 0;
                bounds.w = it->second[0] * _size;
                bounds.y = -it->second[1] * _size;
                bounds.h = it->second[2] * _size;
                return;
            }

            try {
                Rcpp::Function fn(g_text_measure_fn);
                // The family goes over as a name, not the packed index:
                // the registry is ours, and R only wants something it can
                // hand to gpar(fontfamily=).
                Rcpp::NumericVector result = fn(
                    _src, static_cast<int>(_style),
                    font_family_of_style(static_cast<int>(_style)));
                if (result.size() >= 3) {
                    // result = c(width_ratio, ascent_ratio, height_ratio)
                    // Ratios are relative to font size, multiply by _size
                    float wr = static_cast<float>(result[0]);
                    float ar = static_cast<float>(result[1]);
                    float hr = static_cast<float>(result[2]);
                    g_text_measure_cache.emplace(std::move(key),
                                                 std::array<float, 3>{wr, ar, hr});
                    bounds.x = 0;
                    bounds.w = wr * _size;
                    bounds.y = -ar * _size;
                    bounds.h = hr * _size;
                    return;
                }
            } catch (...) {
                // Fall through to heuristic on any error
            }
        }

        // Heuristic fallback: estimate width by iterating Unicode codepoints.
        // CJK/fullwidth chars get ~1.0 em, others ~0.55 em.
        float totalWidth = 0.f;
        const char* p = _src.c_str();
        const char* end = p + _src.size();
        while (p < end) {
            u32 cp = next_codepoint(p, end);
            if (cp == 0) break;
            totalWidth += is_fullwidth(cp) ? _size * 1.0f : _size * 0.55f;
        }
        bounds.x = 0;
        bounds.y = -_size * 0.8f;   // ascent
        bounds.w = totalWidth;
        bounds.h = _size;
    }

    void draw(Graphics2D& g2, float x, float y) override {
        auto* recorder = dynamic_cast<Graphics2D_Recorder*>(&g2);
        if (recorder) {
            recorder->drawTextRun(_src, x, y, static_cast<int>(_style), _size);
        }
    }

private:
    std::string _src;
    FontStyle _style;
    float _size;
};

// --- Platform factory ---

class PlatformFactory_R : public PlatformFactory {
public:
    sptr<Font> createFont(const std::string& file) override {
        return sptrOf<RecordedFont>(file);
    }

    sptr<TextLayout> createTextLayout(const std::string& src, FontStyle style, float size) override {
        return sptrOf<TextLayout_R>(src, style, size);
    }
};

// --- State ---

static bool s_initialized = false;

// [[Rcpp::export]]
void microtex_init(std::string clm_path, std::string otf_path) {
    if (s_initialized) return;

    PlatformFactory::registerFactory("r", std::make_unique<PlatformFactory_R>());
    PlatformFactory::activate("r");

    FontSrcFile fontSrc(clm_path, otf_path);
    try {
        MicroTeX::init(fontSrc);
    } catch (const std::exception& e) {
        Rcpp::stop(std::string("MicroTeX::init failed: ") + e.what());
    }

    // Default to path rendering (universal compatibility)
    if (MicroTeX::hasGlyphPathRender()) {
        MicroTeX::setRenderGlyphUsePath(true);
    }

    register_mark_macro();
    register_font_family_macro();
    register_image_macros();
    s_initialized = true;
}

// [[Rcpp::export]]
void microtex_init_from_otf(std::string otf_path, int index = 0) {
    if (s_initialized) return;

    PlatformFactory::registerFactory("r", std::make_unique<PlatformFactory_R>());
    PlatformFactory::activate("r");

    std::vector<std::uint8_t> clm;
    try {
        clm = microtex::otfToClmBytes(otf_path, index);
    } catch (const std::exception& e) {
        Rcpp::stop(std::string("Failed to read font '") + otf_path + "': " + e.what());
    }

    FontSrcData fontSrc(clm.size(), clm.data(), otf_path);
    try {
        MicroTeX::init(fontSrc);
    } catch (const std::exception& e) {
        Rcpp::stop(std::string("MicroTeX::init failed: ") + e.what());
    }

    if (MicroTeX::hasGlyphPathRender()) {
        MicroTeX::setRenderGlyphUsePath(true);
    }

    register_mark_macro();
    register_font_family_macro();
    register_image_macros();
    s_initialized = true;
}

// [[Rcpp::export]]
void microtex_add_font(std::string clm_path, std::string otf_path) {
    if (!s_initialized) {
        Rcpp::stop("MicroTeX is not initialized.");
    }

    FontSrcFile fontSrc(clm_path, otf_path);
    try {
        auto meta = MicroTeX::addFont(fontSrc);
        if (!meta.isValid()) {
            Rcpp::warning("Failed to load font from: " + clm_path);
        }
    } catch (const std::exception& e) {
        Rcpp::warning(std::string("Font load failed: ") + e.what());
    }
}

// [[Rcpp::export]]
std::vector<std::string> microtex_math_font_names() {
    if (!s_initialized) return {};
    return MicroTeX::mathFontNames();
}

// [[Rcpp::export]]
bool microtex_set_default_math_font(std::string name) {
    if (!s_initialized) return false;
    return MicroTeX::setDefaultMathFont(name);
}

// --- Hyphenation patterns ---
//
// Whether the package was built against FriBidi. Without it, text that
// wraps keeps its logical (left-to-right) word order; everything else is
// unaffected. Exposed so tests can skip the wrapped right-to-left case.
// [[Rcpp::export]]
bool microtex_bidi_available() {
    return microtex::bidi_available();
}

// Run by R when the shared object is unloaded. It only fires because
// .onUnload() calls library.dynam.unload() -- without that R keeps the
// DLL mapped and this never runs at all.
//
// Releasing the preserved text-measurer callback is the whole job. It
// deliberately does NOT call MicroTeX::release(): the macro registries
// are torn down by the static object at the end of macro_def.cpp, which
// covers both exits. If dlclose really unmaps us, that destructor runs
// here and the statics -- registries and the s_registered guards in
// font_family_atom.cpp / mark_atom.cpp / image_atom.cpp alike -- are
// gone together, so a later reload re-registers from scratch. If it does
// not unmap (glibc keeps libraries exporting STB_GNU_UNIQUE symbols, and
// C++ templates emit those), nothing is destroyed and nothing is
// cleared, so the guards still match a populated table. Clearing the
// registries from here would break exactly that second case: the guards
// would still read "registered" over an empty table and \gmfontfamily,
// \textrm, \mark and \includegraphics would vanish after a reload.
extern "C" void R_unload_gridmicrotex(DllInfo*) {
    clear_text_measurer();
}

// [[Rcpp::export]]
void microtex_release() {
    if (!s_initialized) return;
    // Deliberately NOT MicroTeX::release(). That is final teardown, and
    // the registries it empties are only ever repopulated from inside
    // MicroTeX::init(). An in-process release is not paired with a
    // re-init -- the next parse goes straight through -- so clearing them
    // here would leave the parser without \frac, \mark, \gmfontfamily and
    // the rest until the shared object itself was reloaded. Drop only
    // what is genuinely per-session; R_unload_gridmicrotex above does the
    // real teardown, where losing the registries costs nothing.
    NewCommandMacro::clearUserMacros();
    microtex::g_font_id_cache.clear();
    // The built-in registry survives, so our macros are still there and
    // their registration guards must stay set.
    s_initialized = false;
}

// [[Rcpp::export]]
bool microtex_is_inited() {
    return s_initialized && MicroTeX::isInited();
}

// [[Rcpp::export]]
std::string microtex_version() {
    return MicroTeX::version();
}

// [[Rcpp::export]]
bool microtex_set_default_main_font(std::string family) {
    if (!s_initialized) return false;
    return MicroTeX::setDefaultMainFont(family);
}

// [[Rcpp::export]]
std::vector<std::string> microtex_main_font_families() {
    if (!s_initialized) return {};
    return MicroTeX::mainFontFamilies();
}
