/*
    SPDX-License-Identifier: GPL-2.0-or-later
    SPDX-FileCopyrightText: 2024 Louis Schul <schul9louis@gmail.com>
*/

#include "posCacheUtils.hpp"

// KleverNotes includes.
#include "logic/parser/plugins/emoji/emojiPlugin.hpp"

// C++ include.
#include <memory>

// Qt include.
#include <QDebug>

static const int USERDEFINEDINT = static_cast<int>(MD::ItemType::UserDefined);

/**
 * @brief Create and add DelimsInfo inside `openCloseDelims`, from delims found inside the item.
 *
 * @param item The items being treated.
 * @param waitingOpeningDelims List of opening delims already found, waiting to be paired.
 * @param openCloseDelims List where the new DelimsInfo will be added.
 * @param headingLevel The heading level of the newly created DelimsInfo.
 */
void makePairs(MD::ItemWithOpts *item, QList<MD::WithPosition> &waitingOpeningDelims, QList<posCacheUtils::DelimsInfo> &openCloseDelims, const int headingLevel)
{
    for (const MD::WithPosition &openStyle : item->openStyles()) { // Need the cast into MD::WithPosition
        waitingOpeningDelims.append(openStyle);
    }

    for (const auto &closeStyle : item->closeStyles()) {
        const posCacheUtils::DelimsInfo pairInfo = {headingLevel, closeStyle.style(), waitingOpeningDelims.takeLast(), closeStyle};
        openCloseDelims.append(pairInfo);
    }
}

/**
 * @brief Wrapper function for `makePairs` ignoring specific (useless) Items.
 *
 * @param item The items being treated.
 * @param waitingOpeningDelims List of opening delims already found, waiting to be paired.
 * @param openCloseDelims List where the new DelimsInfo will be added.
 * @param headingLevel The heading level of the newly created DelimsInfo.
 */
void getOpenCloseDelims(MD::Item *item,
                        QList<MD::WithPosition> &waitingOpeningDelims,
                        QList<posCacheUtils::DelimsInfo> &openCloseDelims,
                        const int headingLevel)
{
    switch (item->type()) {
    case MD::ItemType::Text:
    case MD::ItemType::Link:
    case MD::ItemType::Image: {
        const auto itemWithOpts = static_cast<MD::ItemWithOpts *>(item);
        return makePairs(itemWithOpts, waitingOpeningDelims, openCloseDelims, headingLevel);
    }
    case MD::ItemType::Code: {
        const auto codeItem = static_cast<MD::Code *>(item);
        const posCacheUtils::DelimsInfo outerDelims = {headingLevel, 0, codeItem->startDelim(), codeItem->endDelim()};
        openCloseDelims.append(outerDelims);

        return makePairs(codeItem, waitingOpeningDelims, openCloseDelims, headingLevel);
    }
    case MD::ItemType::RawHtml:
    case MD::ItemType::LineBreak: {
        // "useless" in this case, but no need for an error message,
        // we already it is not supported, not a bug
        break;
    }
    default:
        // Find a better way to do this with futur user defined
        const int itemType = static_cast<int>(item->type());

        if (itemType == USERDEFINEDINT + 1) {
            const auto emojiItem = static_cast<EmojiPlugin::EmojiItem *>(item);
            const posCacheUtils::DelimsInfo outerDelims = {headingLevel, 0, emojiItem->startDelim(), emojiItem->endDelim()};
            openCloseDelims.append(outerDelims);

            return makePairs(emojiItem, waitingOpeningDelims, openCloseDelims, headingLevel);
        } else {
            qWarning() << "Item not handled" << static_cast<int>(item->type());
            return;
        }
    }
}

/**
 * @brief Get the inner (child) items for specific item type.
 *
 * @param item The item for which we want the inner items.
 * @return The list of inner items. An empty list if none where found.
 */
SharedItems getInnerItems(MD::Item *item)
{
    switch (item->type()) {
    case MD::ItemType::Heading: {
        const auto h = static_cast<MD::Heading *>(item);
        return h->text().get()->items();
    }
    case MD::ItemType::Paragraph: {
        return static_cast<MD::Paragraph *>(item)->items();
    }
    case MD::ItemType::Blockquote:
    case MD::ItemType::List:
    case MD::ItemType::ListItem: {
        // Those block are "useless" in this case, but no need for an error message,
        // we already know that they're not supported, not a bug
        break;
    }
    default:
        qWarning() << "getInnerItems: Unsupported block item" << static_cast<int>(item->type());
    }
    return {};
}

/**
 * @brief Create and add the header DelimsInfo into `delims`.
 * Handle the specific case of heading.
 *
 * @param delims A list of DelimsInfo where all the new DelimsInfo will be added.
 * @param item The item being treated for which we know it is a Heading.
 * @param headingLevel The heading level of the newly created DelimsInfo. This will be modified based on the level of the item (being a Heading).
 */
void addHeadingDelims(QList<posCacheUtils::DelimsInfo> &delims, MD::Item *item, int &headingLevel)
{
    const auto &h = static_cast<MD::Heading *>(item);
    headingLevel = h->level();

    for (const auto &delim : h->delims()) {
        int delimType = 0;
        switch (headingLevel) {
        case 1:
            delimType = posCacheUtils::BlockDelimTypes::Heading1;
            break;
        case 2:
            delimType = posCacheUtils::BlockDelimTypes::Heading2;
            break;
        case 3:
            delimType = posCacheUtils::BlockDelimTypes::Heading3;
            break;
        case 4:
            delimType = posCacheUtils::BlockDelimTypes::Heading4;
            break;
        case 5:
            delimType = posCacheUtils::BlockDelimTypes::Heading5;
            break;
        case 6:
            delimType = posCacheUtils::BlockDelimTypes::Heading6;
            break;
        }
        const posCacheUtils::DelimsInfo delimInfo = {headingLevel, delimType, delim};
        if (!delims.contains(delimInfo)) {
            delims.append(delimInfo);
        }
    }
}

/**
 * @brief Create and add all the DelimsInfo found inside the item to `delims`, if this item one is a block.
 *
 * @param delims A list of DelimsInfo where all the new DelimsInfo will be added.
 * @param item The item being treated.
 * @param pos The position of the cursor.
 * @param headingLevel The heading level of the newly created DelimsInfo.
 */
void addBlockItemDelims(QList<posCacheUtils::DelimsInfo> &delims, MD::Item *item, const MD::WithPosition &pos, int &headingLevel)
{
    posCacheUtils::DelimsInfo delimInfo;
    switch (item->type()) {
    case MD::ItemType::Heading: { // Happens when hovering a heading delim
        int headingLevel = 0;
        addHeadingDelims(delims, item, headingLevel);
        return;
    }
    case MD::ItemType::Code: {
        const auto codeItem = static_cast<MD::Code *>(item);
        if (codeItem->isFensedCode()) {
            delimInfo = {0, posCacheUtils::BlockDelimTypes::CodeBlock, codeItem->startDelim(), codeItem->endDelim()};
        }
        break;
    }
    case MD::ItemType::Blockquote: {
        const auto quoteItem = static_cast<MD::Blockquote *>(item);
        for (const auto &delim : quoteItem->delims()) {
            if (delim.startLine() == pos.startLine()) {
                delimInfo = {headingLevel, posCacheUtils::BlockDelimTypes::BlockQuote, delim};
                break;
            }
        }
        break;
    }
    case MD::ItemType::ListItem: {
        const auto listItem = static_cast<MD::ListItem *>(item);
        int delimType = listItem->listType() == MD::ListItem::ListType::Ordered ? posCacheUtils::BlockDelimTypes::OrderedList
                                                                                : posCacheUtils::BlockDelimTypes::UnorderedList;
        delimInfo = {headingLevel, delimType, listItem->delim()};
        break;
    }
    case MD::ItemType::List:
    case MD::ItemType::Paragraph:
    case MD::ItemType::HorizontalLine:
    case MD::ItemType::RawHtml: {
        // Those block are "useless" in this case, but no need for an error message,
        // we already know that they're not supported, not a bug
        break;
    }
    default:
        qWarning() << "addBlockItemDelims: Unsupported block item" << static_cast<int>(item->type());
    }

    if (delimInfo.opening.startLine() != -1 && !delims.contains(delimInfo)) {
        delims.append(delimInfo);
    }
}

inline bool isBetweenDelims(const MD::WithPosition value, const MD::WithPosition start, const MD::WithPosition end, const bool isCursor = false)
{
    // Note: since we're dealing with delims, it's fine to assume that, within a delim: startLine() == endLine()
    const bool betweenLine = start.startLine() <= value.startLine() && value.startLine() <= end.startLine();
    if (betweenLine) {
        // To be less strict with cursor
        const long long veryEnd = isCursor ? end.endColumn() + 1 : end.endColumn();

        const bool afterStart = start.startLine() < value.startLine() ? true : start.startColumn() <= value.startColumn();
        const bool beforeEnd = value.startLine() < end.startLine() ? true : value.endColumn() <= veryEnd;

        return afterStart && beforeEnd;
    }

    return false;
}

/**
 * @brief Create and add all the DelimsInfo found inside the item to `delims`.
 *
 * @param delims A list of DelimsInfo where all the new DelimsInfo will be added.
 * @param item The item being treated.
 * @param cursorPos The position of the cursor.
 * @param selectStartPos The position where the selection starts. If there's no selection, this is equal to `pos`.
 * @param selectEndPos The position where the selection ends. If there's no selection, this is equal to `pos`.
 * @param headingLevel The heading level of the newly created DelimsInfo.
 */
void addSurroundingDelimsPairs(QList<posCacheUtils::DelimsInfo> &delims,
                               MD::Item *item,
                               const MD::WithPosition &cursorPos,
                               const MD::WithPosition &selectStartPos,
                               const MD::WithPosition &selectEndPos,
                               const int headingLevel)
{
    auto items = getInnerItems(item);

    QList<MD::WithPosition> waitingOpeningDelims;
    QList<posCacheUtils::DelimsInfo> openCloseDelims;

    for (const auto &item : items) {
        getOpenCloseDelims(item.get(), waitingOpeningDelims, openCloseDelims, headingLevel);
    }

    if (!waitingOpeningDelims.isEmpty()) {
        qWarning() << "Error in processing open/close style for unhighlight, remaining open delims:";
        for (const auto &delim : waitingOpeningDelims) {
            qWarning() << delim.startColumn() << delim.startLine() << delim.endColumn() << delim.endLine();
        }
        return;
    }

    for (const auto &delimInfo : openCloseDelims) {
        const auto &openDelim = delimInfo.opening;
        const auto &closeDelim = delimInfo.closing;

        bool addPair = true;
        if (selectStartPos.startColumn() != -1 && selectEndPos.startColumn() != -1) {
            addPair = isBetweenDelims(cursorPos, openDelim, closeDelim, true) || isBetweenDelims(openDelim, selectStartPos, selectEndPos)
                || isBetweenDelims(closeDelim, selectStartPos, selectEndPos);
        }

        if (addPair) {
            if (!delims.contains(delimInfo)) {
                delims.append(delimInfo);
            }
        }
    }
}

namespace posCacheUtils
{
bool operator==(const DelimsInfo &d1, const DelimsInfo &d2)
{
    // We don't check for the heading level since the real deal here is the delims
    return (d1.opening == d2.opening && d1.closing == d2.closing);
}

void addDelimsFromItems(QList<posCacheUtils::DelimsInfo> &delims,
                        const Items &items,
                        const MD::WithPosition &pos,
                        const MD::WithPosition &selectStartPos,
                        const MD::WithPosition &selectEndPos)
{
    int headingLevel = 0;
    const int cacheLen = items.length();
    MD::Item *blockItem = 3 <= cacheLen ? items.at(cacheLen - 3) : items.first();

    addBlockItemDelims(delims, blockItem, pos, headingLevel);

    if (2 <= cacheLen) {
        const auto &secondToLast = items.at(cacheLen - 2);

        addSurroundingDelimsPairs(delims, secondToLast, pos, selectStartPos, selectEndPos, headingLevel);
    }
}
}
