#include "FontMgrDefaultFactory.hh"
#include <iostream>
#include <jni.h>
#include "../interop.hh"
#include "interop.hh"
#include "FontRunIterator.hh"
#include "SkShaper.h"
#include "SkShaper_harfbuzz.h"
#include "SkUnicode.h"
#include "src/base/SkUTF.h"
#include "TextLineRunHandler.hh"
#include "unicode/ubidi.h"
#include "SkUnicode_icu.h"

#ifdef SK_SHAPER_CORETEXT_AVAILABLE
#include "SkShaper_coretext.h"
#endif

static void deleteShaper(SkShaper* instance) {
    // std::cout << "Deleting [SkShaper " << instance << "]" << std::endl;
    delete instance;
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt_Shaper_1nGetFinalizer(JNIEnv* env, jclass jclass) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&deleteShaper));
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nMakePrimitive
  (JNIEnv* env, jclass jclass) {
    return reinterpret_cast<jlong>(SkShapers::Primitive::PrimitiveText().release());
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nMakeShaperDrivenWrapper
  (JNIEnv* env, jclass jclass, jlong fontMgrPtr) {
    SkFontMgr* fontMgr = reinterpret_cast<SkFontMgr*>(static_cast<uintptr_t>(fontMgrPtr));
    return reinterpret_cast<jlong>(SkShaper::MakeShaperDrivenWrapper(sk_ref_sp(fontMgr)).release());
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nMakeShapeThenWrap
  (JNIEnv* env, jclass jclass, jlong fontMgrPtr) {
    SkFontMgr* fontMgr = reinterpret_cast<SkFontMgr*>(static_cast<uintptr_t>(fontMgrPtr));
    return reinterpret_cast<jlong>(SkShaper::MakeShapeThenWrap(sk_ref_sp(fontMgr)).release());
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nMakeShapeDontWrapOrReorder
  (JNIEnv* env, jclass jclass, jlong fontMgrPtr) {
    SkFontMgr* fontMgr = reinterpret_cast<SkFontMgr*>(static_cast<uintptr_t>(fontMgrPtr));
    // TODO: consider if we need/want to use ICU4X or Libgrapheme (skuincode/include has those implementations too)
    sk_sp<SkUnicode> unicode = SkUnicodes::ICU::Make();
    return reinterpret_cast<jlong>(SkShapers::HB::ShapeDontWrapOrReorder(unicode, sk_ref_sp(fontMgr)).release());
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nMakeCoreText
  (JNIEnv* env, jclass jclass) {
    #ifdef SK_SHAPER_CORETEXT_AVAILABLE
        return reinterpret_cast<jlong>(SkShapers::CT::CoreText().release());
    #else
        return 0;
    #endif
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt_Shaper_1nMake
  (JNIEnv* env, jclass jclass, jlong fontMgrPtr) {
    SkFontMgr* fontMgr = reinterpret_cast<SkFontMgr*>(static_cast<uintptr_t>(fontMgrPtr));
    return reinterpret_cast<jlong>(SkShaper::Make(sk_ref_sp(fontMgr)).release());
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nShapeBlob
  (JNIEnv* env, jclass jclass, jlong ptr, jlong textPtr, jlong fontPtr, jint optsFeaturesLen, jintArray optsFeatures, jint optsBooleanProps, jfloat width, jfloat offsetX, jfloat offsetY) {
    SkShaper* instance = reinterpret_cast<SkShaper*>(static_cast<uintptr_t>(ptr));
    SkString& text = *(reinterpret_cast<SkString*>(static_cast<uintptr_t>(textPtr)));
    std::shared_ptr<UBreakIterator> graphemeIter = skija::shaper::graphemeBreakIterator(text);
    if (!graphemeIter) return 0;
    SkFont* font = reinterpret_cast<SkFont*>(static_cast<uintptr_t>(fontPtr));

    std::vector<SkShaper::Feature> features = skija::shaper::ShapingOptions::getFeaturesFromIntsArray(env, optsFeatures, optsFeaturesLen);

    bool aproximatePunctuation = (optsBooleanProps & 0x01) != 0;
    bool aproximateSpaces = (optsBooleanProps & 0x02) != 0;
    bool isLeftToRight = (optsBooleanProps & 0x04) != 0;

    uint8_t defaultBiDiLevel = isLeftToRight ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL;
    std::unique_ptr<SkShaper::BiDiRunIterator> bidiRunIter(SkShaper::MakeBiDiRunIterator(text.c_str(), text.size(), defaultBiDiLevel));
    if (!bidiRunIter) return 0;

    std::unique_ptr<SkShaper::ScriptRunIterator> scriptRunIter(SkShaper::MakeHbIcuScriptRunIterator(text.c_str(), text.size()));
    if (!scriptRunIter) return 0;

    std::unique_ptr<SkShaper::LanguageRunIterator> languageRunIter(SkShaper::MakeStdLanguageRunIterator(text.c_str(), text.size()));
    if (!languageRunIter) return 0;

    FontRunIterator fontRunIter(
        text.c_str(),
        text.size(),
        *font,
        SkFontMgrSkikoDefault(),
        graphemeIter,
        aproximateSpaces,
        aproximatePunctuation
    );

    SkTextBlobBuilderRunHandler rh(text.c_str(), {offsetX, offsetY});
    instance->shape(text.c_str(), text.size(), fontRunIter, *bidiRunIter, *scriptRunIter, *languageRunIter, features.data(), features.size(), width, &rh);
    SkTextBlob* blob = rh.makeBlob().release();

    return reinterpret_cast<jlong>(blob);
}

extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_shaper_ShaperKt__1nShapeLine
  (JNIEnv* env, jclass jclass, jlong ptr, jlong textPtr, jlong fontPtr, jint optsFeaturesLen, jintArray optsFeatures, jint optsBooleanProps) {
    SkShaper* instance = reinterpret_cast<SkShaper*>(static_cast<uintptr_t>(ptr));

    SkString& text = *(reinterpret_cast<SkString*>(static_cast<uintptr_t>(textPtr)));
    SkFont* font = reinterpret_cast<SkFont*>(static_cast<uintptr_t>(fontPtr));

    if (text.size() == 0) {
        return reinterpret_cast<jlong>(new TextLine(*font));
    }

    std::shared_ptr<UBreakIterator> graphemeIter = skija::shaper::graphemeBreakIterator(text);
    if (!graphemeIter) return 0;

    std::vector<SkShaper::Feature> features = skija::shaper::ShapingOptions::getFeaturesFromIntsArray(env, optsFeatures, optsFeaturesLen);

    bool aproximatePunctuation = (optsBooleanProps & 0x01) != 0;
    bool aproximateSpaces = (optsBooleanProps & 0x02) != 0;
    bool isLeftToRight = (optsBooleanProps & 0x04) != 0;

    uint8_t defaultBiDiLevel = isLeftToRight ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL;
    std::unique_ptr<SkShaper::BiDiRunIterator> bidiRunIter(SkShaper::MakeBiDiRunIterator(text.c_str(), text.size(), defaultBiDiLevel));
    if (!bidiRunIter) return 0;

    std::unique_ptr<SkShaper::ScriptRunIterator> scriptRunIter(SkShaper::MakeHbIcuScriptRunIterator(text.c_str(), text.size()));
    if (!scriptRunIter) return 0;

    std::unique_ptr<SkShaper::LanguageRunIterator> languageRunIter(SkShaper::MakeStdLanguageRunIterator(text.c_str(), text.size()));
    if (!languageRunIter) return 0;

    FontRunIterator fontRunIter(
        text.c_str(),
        text.size(),
        *font,
        SkFontMgrSkikoDefault(),
        graphemeIter,
        aproximateSpaces,
        aproximatePunctuation);

    TextLineRunHandler rh(text, graphemeIter);
    instance->shape(text.c_str(), text.size(), fontRunIter, *bidiRunIter, *scriptRunIter, *languageRunIter, features.data(), features.size(), std::numeric_limits<float>::infinity(), &rh);

    return reinterpret_cast<jlong>(rh.makeLine().release());
}

template <typename RunIteratorSubclass>
class SkijaRunIterator: public RunIteratorSubclass {
public:
    SkijaRunIterator(JNIEnv* env, jobject obj, SkString& text):
      fEnv(env),
      fIteratorObj(obj),
      fIndicesConverter(text.c_str(), text.size()),
      fEndOfRun(0)
    {
        fHasNext = fEnv->CallBooleanMethod(fIteratorObj, java::util::Iterator::hasNext);
        java::lang::Throwable::exceptionThrown(fEnv);
    }

    void consume() override {
        SkASSERT(fHasNext);
        skija::AutoLocal<jobject> runObj(fEnv, fEnv->CallObjectMethod(fIteratorObj, java::util::Iterator::next));
        java::lang::Throwable::exceptionThrown(fEnv);
        jint endOfRun16 = onConsume(runObj.get());
        fEndOfRun = fIndicesConverter.from16To8(endOfRun16);
        fHasNext = fEnv->CallBooleanMethod(fIteratorObj, java::util::Iterator::hasNext);
        java::lang::Throwable::exceptionThrown(fEnv);
    }

    size_t endOfCurrentRun() const override {
        return fEndOfRun;
    }

    bool atEnd() const override {
        return !fHasNext;
    }

protected:
    JNIEnv* fEnv;
    jobject fIteratorObj;
    skija::UtfIndicesConverter fIndicesConverter;
    size_t fEndOfRun;
    bool fHasNext;

    virtual jint onConsume(jobject) = 0;
};

class SkijaFontRunIterator: public SkijaRunIterator<SkShaper::FontRunIterator> {
public:
    SkijaFontRunIterator(JNIEnv* env, jobject obj, SkString text):
      SkijaRunIterator<SkShaper::FontRunIterator>(env, obj, text)
    {}

    const SkFont& currentFont() const override {
        return *fFont;
    }

    jint onConsume(jobject runObj) override {
        jlong fontPtr = fEnv->CallLongMethod(runObj, skija::shaper::FontRun::_getFontPtr);
        java::lang::Throwable::exceptionThrown(fEnv);
        fFont = reinterpret_cast<SkFont*>(static_cast<uintptr_t>(fontPtr));
        return fEnv->GetIntField(runObj, skija::shaper::FontRun::_end);
    }

protected:
    SkFont* fFont;
};

class SkijaBidiRunIterator: public SkijaRunIterator<SkShaper::BiDiRunIterator> {
public:
    SkijaBidiRunIterator(JNIEnv* env, jobject obj, SkString& text):
      SkijaRunIterator<SkShaper::BiDiRunIterator>(env, obj, text)
    {}

    uint8_t currentLevel() const override {
        return fLevel;
    }

    jint onConsume(jobject runObj) override {
        fLevel = fEnv->GetIntField(runObj, skija::shaper::BidiRun::_level) & 0xFF;
        return fEnv->GetIntField(runObj, skija::shaper::BidiRun::_end);
    }

protected:
    uint8_t fLevel;
};

class SkijaScriptRunIterator: public SkijaRunIterator<SkShaper::ScriptRunIterator> {
public:
    SkijaScriptRunIterator(JNIEnv* env, jobject obj, SkString text):
      SkijaRunIterator<SkShaper::ScriptRunIterator>(env, obj, text)
    {}

    SkFourByteTag currentScript() const override {
        return fScript;
    }

    jint onConsume(jobject runObj) override {
        fScript = fEnv->GetIntField(runObj, skija::shaper::ScriptRun::_scriptTag);
        return fEnv->GetIntField(runObj, skija::shaper::ScriptRun::_end);
    }

protected:
    uint32_t fScript;
};

class SkijaLanguageRunIterator: public SkijaRunIterator<SkShaper::LanguageRunIterator> {
public:
    SkijaLanguageRunIterator(JNIEnv* env, jobject obj, SkString text):
      SkijaRunIterator<SkShaper::LanguageRunIterator>(env, obj, text)
    {}

    const char* currentLanguage() const override {
        return fLang.c_str();
    }

    jint onConsume(jobject runObj) override {
        jstring langObj = static_cast<jstring>(fEnv->GetObjectField(runObj, skija::shaper::LanguageRun::_language));
        fLang = skString(fEnv, langObj);
        return fEnv->GetIntField(runObj, skija::shaper::LanguageRun::_end);
    }

protected:
    SkString fLang;
};

class SkijaRunHandler: public SkShaper::RunHandler {
public:
    SkijaRunHandler(JNIEnv* env, jobject runHandler, const SkString& text):
        fEnv(env),
        fRunHandler(runHandler),
        fIndicesConverter(text.c_str(), text.size())
    {}

    void beginLine() {
        fEnv->CallVoidMethod(fRunHandler, skija::shaper::RunHandler::beginLine);
        java::lang::Throwable::exceptionThrown(fEnv);
    }

    void runInfo(const SkShaper::RunHandler::RunInfo& info) {
        skija::AutoLocal<jobject> runInfoObj(fEnv, skija::shaper::RunInfo::toJava(fEnv, info, fIndicesConverter));
        fEnv->CallVoidMethod(fRunHandler, skija::shaper::RunHandler::runInfo, runInfoObj.get());
        java::lang::Throwable::exceptionThrown(fEnv);
        fEnv->SetLongField(runInfoObj.get(), skija::shaper::RunInfo::_fontPtr, 0);
    }

    void commitRunInfo() {
        fEnv->CallVoidMethod(fRunHandler, skija::shaper::RunHandler::commitRunInfo);
        java::lang::Throwable::exceptionThrown(fEnv);
    }

    SkShaper::RunHandler::Buffer runBuffer(const SkShaper::RunHandler::RunInfo& info) {
        fGlyphs    = std::vector<jshort>(info.glyphCount);
        fPositions = std::vector<SkPoint>(info.glyphCount);
        fClusters  = std::vector<jint>(info.glyphCount);

        skija::AutoLocal<jobject> runInfoObj(fEnv, skija::shaper::RunInfo::toJava(fEnv, info, fIndicesConverter));
        skija::AutoLocal<jobject> point(fEnv, fEnv->CallObjectMethod(fRunHandler, skija::shaper::RunHandler::runOffset, runInfoObj.get()));
        java::lang::Throwable::exceptionThrown(fEnv);
        fEnv->SetLongField(runInfoObj.get(), skija::shaper::RunInfo::_fontPtr, 0);

        jfloat x = fEnv->GetFloatField(point.get(), skija::Point::x);
        jfloat y = fEnv->GetFloatField(point.get(), skija::Point::y);

        return SkShaper::RunHandler::Buffer{
            reinterpret_cast<SkGlyphID*>(fGlyphs.data()),
            fPositions.data(),
            nullptr,
            reinterpret_cast<uint32_t*>(fClusters.data()),
            {x, y}};
    }

    void commitRunBuffer(const SkShaper::RunHandler::RunInfo& info) {
        size_t begin = fIndicesConverter.from8To16(info.utf8Range.fBegin);
        for (int i = 0; i < fClusters.size(); ++i)
            fClusters[i] = fIndicesConverter.from8To16(fClusters[i]);
        skija::AutoLocal<jintArray> clusters(fEnv, javaIntArray(fEnv, fClusters));
        size_t end = fIndicesConverter.from8To16(info.utf8Range.fBegin + info.utf8Range.fSize);
        skija::AutoLocal<jobject> runInfoObj(fEnv, skija::shaper::RunInfo::toJava(fEnv, info, begin, end));
        skija::AutoLocal<jshortArray> glyphs(fEnv, javaShortArray(fEnv, fGlyphs));
        skija::AutoLocal<jobjectArray> positions(fEnv, skija::Point::fromSkPoints(fEnv, fPositions));
        fEnv->CallVoidMethod(fRunHandler, skija::shaper::RunHandler::commitRun, runInfoObj.get(), glyphs.get(), positions.get(), clusters.get());
        java::lang::Throwable::exceptionThrown(fEnv);
        fEnv->SetLongField(runInfoObj.get(), skija::shaper::RunInfo::_fontPtr, 0);
    }

    void commitLine() {
        fEnv->CallVoidMethod(fRunHandler, skija::shaper::RunHandler::commitLine);
        java::lang::Throwable::exceptionThrown(fEnv);
    }

private:
    JNIEnv* fEnv;
    jobject fRunHandler;
    skija::UtfIndicesConverter fIndicesConverter;
    std::vector<jshort> fGlyphs;
    std::vector<SkPoint> fPositions;
    std::vector<jint> fClusters;
};

extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_shaper_ShaperKt_Shaper_1nShape
  (JNIEnv* env, jclass jclass, jlong ptr, jlong textPtr, jobject fontRunIterObj, jobject bidiRunIterObj, jobject scriptRunIterObj, jobject languageRunIterObj, jint optsFeaturesLen, jintArray optsFeatures, jint optsBooleanProps, jfloat width, jobject runHandlerObj)
{
    SkShaper* instance = reinterpret_cast<SkShaper*>(static_cast<uintptr_t>(ptr));
    SkString* text = reinterpret_cast<SkString*>(static_cast<uintptr_t>(textPtr));

    auto nativeFontRunIter = (SkShaper::FontRunIterator*) skija::impl::Native::fromJava(env, fontRunIterObj, skija::shaper::FontMgrRunIterator::cls);
    std::unique_ptr<SkijaFontRunIterator> localFontRunIter;
    if (nativeFontRunIter == nullptr)
        localFontRunIter.reset(new SkijaFontRunIterator(env, fontRunIterObj, *text));

    auto nativeBidiRunIter = (SkShaper::BiDiRunIterator*) skija::impl::Native::fromJava(env, bidiRunIterObj, skija::shaper::IcuBidiRunIterator::cls);
    std::unique_ptr<SkijaBidiRunIterator> localBidiRunIter;
    if (nativeBidiRunIter == nullptr)
        localBidiRunIter.reset(new SkijaBidiRunIterator(env, bidiRunIterObj, *text));

    auto nativeScriptRunIter = (SkShaper::ScriptRunIterator*) skija::impl::Native::fromJava(env, scriptRunIterObj, skija::shaper::HbIcuScriptRunIterator::cls);
    std::unique_ptr<SkijaScriptRunIterator> localScriptRunIter;
    if (nativeScriptRunIter == nullptr)
        localScriptRunIter.reset(new SkijaScriptRunIterator(env, scriptRunIterObj, *text));

    auto languageRunIter = SkijaLanguageRunIterator(env, languageRunIterObj, *text);

    std::vector<SkShaper::Feature> features = skija::shaper::ShapingOptions::getFeaturesFromIntsArray(env, optsFeatures, optsFeaturesLen);

    auto nativeRunHandler = (SkShaper::RunHandler*) skija::impl::Native::fromJava(env, runHandlerObj, skija::shaper::TextBlobBuilderRunHandler::cls);
    std::unique_ptr<SkijaRunHandler> localRunHandler;
    if (nativeRunHandler == nullptr)
        localRunHandler.reset(new SkijaRunHandler(env, runHandlerObj, *text));

    instance->shape(text->c_str(), text->size(),
        nativeFontRunIter != nullptr ? *nativeFontRunIter : *localFontRunIter,
        nativeBidiRunIter != nullptr ? *nativeBidiRunIter : *localBidiRunIter,
        nativeScriptRunIter != nullptr ? *nativeScriptRunIter : *localScriptRunIter,
        languageRunIter,
        features.data(),
        features.size(),
        width,
        nativeRunHandler != nullptr ? nativeRunHandler : localRunHandler.get());
}
