#include <Geode/binding/ProfilePage.hpp>
#include <Geode/binding/CCContentLayer.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/MDTextArea.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/string.hpp>
#include <md4c.h>

USE_GEODE_NAMESPACE();

static constexpr float g_fontScale = .5f;
static constexpr float g_paragraphPadding = 7.f;
static constexpr float g_indent = 7.f;
static constexpr float g_codeBlockIndent = 8.f;
static constexpr ccColor3B g_linkColor = cc3x(0x7ff4f4);

TextRenderer::Font g_mdFont = [](int style) -> TextRenderer::Label {
    if ((style & TextStyleBold) && (style & TextStyleItalic)) {
        return CCLabelBMFont::create("", "mdFontBI.fnt"_spr);
    }
    if ((style & TextStyleBold)) {
        return CCLabelBMFont::create("", "mdFontB.fnt"_spr);
    }
    if ((style & TextStyleItalic)) {
        return CCLabelBMFont::create("", "mdFontI.fnt"_spr);
    }
    return CCLabelBMFont::create("", "mdFont.fnt"_spr);
};

TextRenderer::Font g_mdMonoFont = [](int style) -> TextRenderer::Label {
    return CCLabelBMFont::create("", "mdFontMono.fnt"_spr);
};

class MDContentLayer : public CCContentLayer {
protected:
    CCMenu* m_content;

public:
    static MDContentLayer* create(CCMenu* content, float width, float height) {
        auto ret = new MDContentLayer();
        if (ret && ret->initWithColor({ 0, 255, 0, 0 }, width, height)) {
            ret->m_content = content;
            ret->autorelease();
            return ret;
        }
        CC_SAFE_DELETE(ret);
        return nullptr;
    }

    void setPosition(CCPoint const& pos) override {
        // cringe CCContentLayer expect its children to
        // all be TableViewCells
        CCLayerColor::setPosition(pos);

        // so that's why based MDContentLayer expects itself
        // to have a CCMenu :-)
        if (m_content) {
            for (auto child : CCArrayExt<CCNode>(m_content->getChildren())) {
                auto y = this->getPositionY() + child->getPositionY();
                child->setVisible(
                    !((m_content->getContentSize().height < y) ||
                      (y < -child->getContentSize().height))
                );
            }
        }
    }
};

Result<ccColor3B> colorForIdentifier(std::string const& tag) {
    if (utils::string::contains(tag, ' ')) {
        auto hexStr = utils::string::split(utils::string::normalize(tag), " ").at(1);
        try {
            auto hex = std::stoi(hexStr, nullptr, 16);
            return Ok(cc3x(hex));
        }
        catch (...) {
            return Err("Invalid hex");
        }
    }
    else {
        auto colorText = tag.substr(1);
        if (!colorText.size()) {
            return Err("No color specified");
        }
        else if (colorText.size() > 1) {
            return Err("Color tag " + tag + " unexpectedly long, either do <cx> or <c hex>");
        }
        else {
            switch (colorText.front()) {
                case 'b': return Ok(cc3x(0x4a52e1)); break;
                case 'g': return Ok(cc3x(0x40e348)); break;
                case 'l': return Ok(cc3x(0x60abef)); break;
                case 'j': return Ok(cc3x(0x32c8ff)); break;
                case 'y': return Ok(cc3x(0xffff00)); break;
                case 'o': return Ok(cc3x(0xffa54b)); break;
                case 'r': return Ok(cc3x(0xff5a5a)); break;
                case 'p': return Ok(cc3x(0xff00ff)); break;
                default: return Err("Unknown color " + colorText);
            }
        }
    }
    return Err("Unknown error");
}

bool MDTextArea::init(std::string const& str, CCSize const& size) {
    if (!CCLayer::init()) return false;

    m_text = str;
    m_size = size;
    this->setContentSize(size);
    m_renderer = TextRenderer::create();
    CC_SAFE_RETAIN(m_renderer);

    m_bgSprite = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
    m_bgSprite->setScale(.5f);
    m_bgSprite->setColor({ 0, 0, 0 });
    m_bgSprite->setOpacity(75);
    m_bgSprite->setContentSize(size * 2 + CCSize { 25.f, 25.f });
    m_bgSprite->setPosition(size / 2);
    this->addChild(m_bgSprite);

    m_scrollLayer = ScrollLayer::create({ 0, 0, m_size.width, m_size.height }, true);

    m_content = CCMenu::create();
    m_content->setZOrder(2);
    m_scrollLayer->m_contentLayer->addChild(m_content);

    CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
    m_scrollLayer->registerWithTouchDispatcher();

    this->addChild(m_scrollLayer);

    this->updateLabel();

    return true;
}

MDTextArea::~MDTextArea() {
    CC_SAFE_RELEASE(m_renderer);
}

class BreakLine : public CCNode {
protected:
    void draw() override {
        // some nodes sometimes set the blend func to
        // something else without resetting it back
        ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        ccDrawSolidRect({ 0, 0 }, this->getContentSize(), { 1.f, 1.f, 1.f, .2f });
        CCNode::draw();
    }

public:
    static BreakLine* create(float width) {
        auto ret = new BreakLine;
        if (ret && ret->init()) {
            ret->autorelease();
            ret->setContentSize({ width, 1.f });
            return ret;
        }
        CC_SAFE_DELETE(ret);
        return nullptr;
    }
};

void MDTextArea::onLink(CCObject* pSender) {
    auto href = as<CCString*>(as<CCNode*>(pSender)->getUserObject());
    auto layer = FLAlertLayer::create(
        this, "Hold Up!",
        "Links are spooky! Are you sure you want to go to <cy>" + std::string(href->getCString()) +
            "</c>?",
        "Cancel", "Yes", 360.f
    );
    layer->setUserObject(href);
    layer->show();
}

void MDTextArea::onGDProfile(CCObject* pSender) {
    auto href = as<CCString*>(as<CCNode*>(pSender)->getUserObject());
    auto profile = std::string(href->getCString());
    profile = profile.substr(profile.find(":") + 1);
    try {
        ProfilePage::create(std::stoi(profile), false)->show();
    }
    catch (...) {
        FLAlertLayer::create(
            "Error",
            "Invalid profile ID: <cr>" + profile +
                "</c>. This is "
                "probably the modder's fault, report the bug to them.",
            "OK"
        )
            ->show();
    }
}

void MDTextArea::FLAlert_Clicked(FLAlertLayer* layer, bool btn) {
    if (btn) {
        web::openLinkInBrowser(as<CCString*>(layer->getUserObject())->getCString());
    }
}

struct MDParser {
    static std::string s_lastLink;
    static std::string s_lastImage;
    static bool s_isOrderedList;
    static bool s_isCodeBlock;
    static float s_codeStart;
    static size_t s_orderedListNum;
    static std::vector<TextRenderer::Label> s_codeSpans;

    static int parseText(MD_TEXTTYPE type, MD_CHAR const* rawText, MD_SIZE size, void* mdtextarea) {
        auto textarea = static_cast<MDTextArea*>(mdtextarea);
        auto renderer = textarea->m_renderer;
        auto text = std::string(rawText, size);
        switch (type) {
            case MD_TEXTTYPE::MD_TEXT_CODE:
                {
                    auto rendered = renderer->renderString(text);
                    if (!s_isCodeBlock) {
                        // code span BGs need to be rendered after all
                        // rendering is done since the position of the
                        // rendered labels may change after alignments
                        // are adjusted
                        ranges::push(s_codeSpans, rendered);
                    }
                }
                break;

            case MD_TEXTTYPE::MD_TEXT_BR:
                {
                    renderer->breakLine();
                }
                break;

            case MD_TEXTTYPE::MD_TEXT_SOFTBR:
                {
                    renderer->breakLine();
                }
                break;

            case MD_TEXTTYPE::MD_TEXT_NORMAL:
                {
                    if (s_lastLink.size()) {
                        renderer->pushColor(g_linkColor);
                        renderer->pushDecoFlags(TextDecorationUnderline);
                        auto rendered = renderer->renderStringInteractive(
                            text, textarea,
                            utils::string::startsWith(s_lastLink, "user:")
                                ? menu_selector(MDTextArea::onGDProfile)
                                : menu_selector(MDTextArea::onLink)
                        );
                        for (auto const& label : rendered) {
                            label.m_node->setUserObject(CCString::create(s_lastLink));
                        }
                        renderer->popDecoFlags();
                        renderer->popColor();
                    }
                    else if (s_lastImage.size()) {
                        bool isFrame = false;
                        if (utils::string::startsWith(s_lastImage, "frame:")) {
                            s_lastImage = s_lastImage.substr(s_lastImage.find(":") + 1);
                            isFrame = true;
                        }
                        CCSprite* spr = nullptr;
                        if (isFrame) {
                            spr = CCSprite::createWithSpriteFrameName(s_lastImage.c_str());
                        }
                        else {
                            spr = CCSprite::create(s_lastImage.c_str());
                        }
                        if (spr) {
                            renderer->renderNode(spr);
                        }
                        else {
                            renderer->renderString(text);
                        }
                        s_lastImage = "";
                    }
                    else {
                        renderer->renderString(text);
                    }
                }
                break;

            case MD_TEXTTYPE::MD_TEXT_HTML:
                {
                    if (text.size() > 2) {
                        auto tag = utils::string::trim(text.substr(1, text.size() - 2));
                        auto isClosing = tag.front() == '/';
                        if (isClosing) tag = tag.substr(1);
                        if (tag.front() != 'c') {
                            log::warn("Unknown tag {}", text);
                            renderer->renderString(text);
                        }
                        else {
                            if (isClosing) {
                                renderer->popColor();
                            }
                            else {
                                auto color = colorForIdentifier(tag);
                                if (color) {
                                    renderer->pushColor(color.unwrap());
                                }
                                else {
                                    log::warn("Error parsing color: {}", color.unwrapErr());
                                }
                            }
                        }
                    }
                    else {
                        log::warn("Too short tag {}", text);
                        renderer->renderString(text);
                    }
                }
                break;

            default:
                {
                    log::warn("Unhandled text type {}", type);
                }
                break;
        }
        return 0;
    }

    static int enterBlock(MD_BLOCKTYPE type, void* detail, void* mdtextarea) {
        auto textarea = static_cast<MDTextArea*>(mdtextarea);
        auto renderer = textarea->m_renderer;
        switch (type) {
            case MD_BLOCKTYPE::MD_BLOCK_DOC:
                {
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_H:
                {
                    auto hdetail = static_cast<MD_BLOCK_H_DETAIL*>(detail);
                    renderer->pushStyleFlags(TextStyleBold);
                    switch (hdetail->level) {
                        case 1: renderer->pushScale(g_fontScale * 2.f); break;
                        case 2: renderer->pushScale(g_fontScale * 1.5f); break;
                        case 3: renderer->pushScale(g_fontScale * 1.17f); break;
                        case 4: renderer->pushScale(g_fontScale); break;
                        case 5: renderer->pushScale(g_fontScale * .83f); break;
                        default:
                        case 6: renderer->pushScale(g_fontScale * .67f); break;
                    }
                    // switch (hdetail->level) {
                    //     case 3: renderer->pushCaps(TextCapitalization::AllUpper); break;
                    // }
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_P:
                {
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_UL:
            case MD_BLOCKTYPE::MD_BLOCK_OL:
                {
                    renderer->pushIndent(g_indent);
                    s_isOrderedList = type == MD_BLOCKTYPE::MD_BLOCK_OL;
                    s_orderedListNum = 0;
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_HR:
                {
                    renderer->breakLine(g_paragraphPadding / 2);
                    renderer->renderNode(BreakLine::create(textarea->m_size.width));
                    renderer->breakLine(g_paragraphPadding);
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_LI:
                {
                    renderer->pushOpacity(renderer->getCurrentOpacity() / 2);
                    auto lidetail = static_cast<MD_BLOCK_LI_DETAIL*>(detail);
                    if (s_isOrderedList) {
                        s_orderedListNum++;
                        renderer->renderString(std::to_string(s_orderedListNum) + ". ");
                    }
                    else {
                        renderer->renderString("• ");
                    }
                    renderer->popOpacity();
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_CODE:
                {
                    s_isCodeBlock = true;
                    s_codeStart = renderer->getCursorPos().y;
                    renderer->pushFont(g_mdMonoFont);
                    renderer->pushIndent(g_codeBlockIndent);
                    renderer->pushWrapOffset(g_codeBlockIndent);
                }
                break;

            default:
                {
                    log::warn("Unhandled block enter type {}", type);
                }
                break;
        }
        return 0;
    }

    static int leaveBlock(MD_BLOCKTYPE type, void* detail, void* mdtextarea) {
        auto textarea = static_cast<MDTextArea*>(mdtextarea);
        auto renderer = textarea->m_renderer;
        switch (type) {
            case MD_BLOCKTYPE::MD_BLOCK_DOC:
                {
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_H:
                {
                    auto hdetail = static_cast<MD_BLOCK_H_DETAIL*>(detail);
                    renderer->breakLine();
                    if (hdetail->level == 1) {
                        renderer->breakLine(g_paragraphPadding / 2);
                        renderer->renderNode(BreakLine::create(textarea->m_size.width));
                    }
                    renderer->breakLine(g_paragraphPadding);
                    renderer->popScale();
                    renderer->popStyleFlags();
                    // switch (hdetail->level) {
                    //     case 3: renderer->popCaps(); break;
                    // }
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_P:
                {
                    renderer->breakLine();
                    renderer->breakLine(g_paragraphPadding);
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_OL:
            case MD_BLOCKTYPE::MD_BLOCK_UL:
                {
                    renderer->popIndent();
                    renderer->breakLine();
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_CODE:
                {
                    auto codeEnd = renderer->getCursorPos().y;

                    auto pad = g_codeBlockIndent / 1.5f;

                    CCSize size { textarea->m_size.width - renderer->getCurrentIndent() -
                                      renderer->getCurrentWrapOffset() + pad * 2,
                                  s_codeStart - codeEnd + pad * 2 };

                    auto bg =
                        CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
                    bg->setScale(.25f);
                    bg->setColor({ 0, 0, 0 });
                    bg->setOpacity(75);
                    bg->setContentSize(size * 4);
                    bg->setPosition(
                        size.width / 2 + renderer->getCurrentIndent() - pad,
                        // mmm i love magic numbers
                        // the -2.f is to offset the the box
                        // to fit the Ubuntu font very neatly.
                        // idk if it works the same for other
                        // fonts
                        s_codeStart - 2.f + pad - size.height / 2
                    );
                    bg->setAnchorPoint({ .5f, .5f });
                    bg->setZOrder(-1);
                    textarea->m_content->addChild(bg);

                    renderer->popWrapOffset();
                    renderer->popIndent();
                    renderer->popFont();

                    renderer->breakLine();
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_LI:
                {
                    renderer->breakLine();
                }
                break;

            case MD_BLOCKTYPE::MD_BLOCK_HR:
                {
                }
                break;

            default:
                {
                    log::warn("Unhandled block leave type {}", type);
                }
                break;
        }
        return 0;
    }

    static int enterSpan(MD_SPANTYPE type, void* detail, void* mdtextarea) {
        auto renderer = static_cast<MDTextArea*>(mdtextarea)->m_renderer;
        switch (type) {
            case MD_SPANTYPE::MD_SPAN_STRONG:
                {
                    renderer->pushStyleFlags(TextStyleBold);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_EM:
                {
                    renderer->pushStyleFlags(TextStyleItalic);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_DEL:
                {
                    renderer->pushDecoFlags(TextDecorationStrikethrough);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_U:
                {
                    renderer->pushDecoFlags(TextDecorationUnderline);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_IMG:
                {
                    auto adetail = static_cast<MD_SPAN_IMG_DETAIL*>(detail);
                    s_lastImage = std::string(adetail->src.text, adetail->src.size);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_A:
                {
                    auto adetail = static_cast<MD_SPAN_A_DETAIL*>(detail);
                    s_lastLink = std::string(adetail->href.text, adetail->href.size);
                }
                break;

            case MD_SPANTYPE::MD_SPAN_CODE:
                {
                    s_isCodeBlock = false;
                    renderer->pushFont(g_mdMonoFont);
                }
                break;

            default:
                {
                    log::warn("Unhandled span enter type {}", type);
                }
                break;
        }
        return 0;
    }

    static int leaveSpan(MD_SPANTYPE type, void* detail, void* mdtextarea) {
        auto renderer = static_cast<MDTextArea*>(mdtextarea)->m_renderer;
        switch (type) {
            case MD_SPANTYPE::MD_SPAN_STRONG:
                {
                    renderer->popStyleFlags();
                }
                break;

            case MD_SPANTYPE::MD_SPAN_EM:
                {
                    renderer->popStyleFlags();
                }
                break;

            case MD_SPANTYPE::MD_SPAN_DEL:
                {
                    renderer->popDecoFlags();
                }
                break;

            case MD_SPANTYPE::MD_SPAN_U:
                {
                    renderer->popDecoFlags();
                }
                break;

            case MD_SPANTYPE::MD_SPAN_A:
                {
                    s_lastLink = "";
                }
                break;

            case MD_SPANTYPE::MD_SPAN_IMG:
                {
                    s_lastImage = "";
                }
                break;

            case MD_SPANTYPE::MD_SPAN_CODE:
                {
                    renderer->popFont();
                }
                break;

            default:
                {
                    log::warn("Unhandled span leave type {}", type);
                }
                break;
        }
        return 0;
    }
};

std::string MDParser::s_lastLink = "";
std::string MDParser::s_lastImage = "";
bool MDParser::s_isOrderedList = false;
size_t MDParser::s_orderedListNum = 0;
bool MDParser::s_isCodeBlock = false;
float MDParser::s_codeStart = 0;
decltype(MDParser::s_codeSpans) MDParser::s_codeSpans = {};

void MDTextArea::updateLabel() {
    m_renderer->begin(m_content, CCPointZero, m_size);

    m_renderer->pushFont(g_mdFont);
    m_renderer->pushScale(.5f);
    m_renderer->pushVerticalAlign(TextAlignment::End);
    m_renderer->pushHorizontalAlign(TextAlignment::Begin);

    MD_PARSER parser;

    parser.abi_version = 0;
    parser.flags = MD_FLAG_UNDERLINE | MD_FLAG_STRIKETHROUGH | MD_FLAG_PERMISSIVEURLAUTOLINKS |
        MD_FLAG_PERMISSIVEWWWAUTOLINKS;

    parser.text = &MDParser::parseText;
    parser.enter_block = &MDParser::enterBlock;
    parser.leave_block = &MDParser::leaveBlock;
    parser.enter_span = &MDParser::enterSpan;
    parser.leave_span = &MDParser::leaveSpan;
    parser.debug_log = nullptr;
    parser.syntax = nullptr;

    MDParser::s_codeSpans = {};

    if (md_parse(m_text.c_str(), m_text.size(), &parser, this)) {
        m_renderer->renderString("Error parsing Markdown");
    }

    for (auto& render : MDParser::s_codeSpans) {
        auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
        bg->setScale(.125f);
        bg->setColor({ 0, 0, 0 });
        bg->setOpacity(75);
        bg->setContentSize(render.m_node->getScaledContentSize() * 8 + CCSize { 20.f, .0f });
        bg->setPosition(
            render.m_node->getPositionX() - 2.5f * (.5f - render.m_node->getAnchorPoint().x),
            render.m_node->getPositionY() - .5f
        );
        bg->setAnchorPoint(render.m_node->getAnchorPoint());
        bg->setZOrder(-1);
        m_content->addChild(bg);
        // i know what you're thinking.
        // my brother in christ, what the hell is this?
        // where did this magical + 1.5f come from?
        // the reason is that if you remove them, code
        // spans are slightly offset and it triggers my
        // OCD.
        render.m_node->setPositionY(render.m_node->getPositionY() + 1.5f);
    }

    m_renderer->end();

    m_scrollLayer->m_contentLayer->setContentSize(m_content->getContentSize());
    m_scrollLayer->moveToTop();
}

CCScrollLayerExt* MDTextArea::getScrollLayer() const {
    return m_scrollLayer;
}

void MDTextArea::setString(char const* text) {
    m_text = text;
    this->updateLabel();
}

char const* MDTextArea::getString() {
    return m_text.c_str();
}

MDTextArea* MDTextArea::create(std::string const& str, CCSize const& size) {
    auto ret = new MDTextArea;
    if (ret && ret->init(str, size)) {
        ret->autorelease();
        return ret;
    }
    CC_SAFE_DELETE(ret);
    return nullptr;
}