#include "customDocument.h"
#include <QDebug>
#include <QGuiApplication>//应用程序
#include <QTextCursor>//文本光标
#include <QMessageBox>//消息框

CustomDocument::CustomDocument(QWidget *parent) : QTextEdit(parent)
{
    installEventFilter(this); // 安装事件过滤器，以便可以处理事件
    viewport()->installEventFilter(this); // 安装视口的事件过滤器
    setMouseTracking(true); // 开启鼠标追踪，以便可以捕获鼠标移动事件
    setAttribute(Qt::WidgetAttribute::WA_Hover, true); // 设置悬停属性，以便可以处理鼠标悬停事件
}

/*!
 * \brief CustomDocument::setDocumentPadding
 * We use a custom document for MainWindow::m_textEdit
 * so we can set the document padding without the (upstream Qt) issue
 * where the vertical scrollbar gets padded with the text as well.
 * This way, only the text gets padded, and the vertical scroll bar stays where it is.
 * \param left
 * \param top
 * \param right
 * \param bottom
 */
void CustomDocument::setDocumentPadding(int left, int top, int right, int bottom)
{
    setViewportMargins(left, top, right, bottom);//设置视口的边距
}

void CustomDocument::resizeEvent(QResizeEvent *event)//处理窗口变化
{
    QTextEdit::resizeEvent(event);
    emit resized();
}

void CustomDocument::mouseMoveEvent(QMouseEvent *event)//处理鼠标移动事件
{
    QTextEdit::mouseMoveEvent(event);
    emit mouseMoved();
}

bool CustomDocument::eventFilter(QObject *obj, QEvent *event)
{
    // qDebug() << event->type();
    //分情况处理按键和鼠标状况：
    //先处理关注的事件，再处理不关注的事件
    //先处理不常见的事件——如按键。后处理常见的事件——如鼠标移动或松开
    if (event->type() == QEvent::HoverMove) {
        // if hovering and the control key is active, check whether the mouse is over a link
        if (QGuiApplication::keyboardModifiers() == Qt::ExtraButton24
            && getUrlUnderMouse().isValid()) {
            viewport()->setCursor(Qt::PointingHandCursor);
        } else {
            viewport()->setCursor(Qt::IBeamCursor);
        }
    } else if (event->type() == QEvent::KeyPress) {//检测只按键的事件并做出响应
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Control) {
            // check if mouse is over a link
            auto url = getUrlUnderMouse();
            viewport()->setCursor(url.isValid() ? Qt::PointingHandCursor : Qt::IBeamCursor);
        } else if (keyEvent->modifiers().testFlag(Qt::AltModifier)) {
            // alt + arrow up/down
            if (keyEvent->key() == Qt::Key_Up) {
                moveBlockUp();
                return true;
            } else if (keyEvent->key() == Qt::Key_Down) {
                moveBlockDown();
                return true;
            }
        }
    } else if (event->type() == QEvent::MouseButtonRelease) {//检测松鼠标的事件并做出响应

        auto *mouseEvent = static_cast<QMouseEvent *>(event);

        // track `Ctrl + Click` in the text edit
        if ((obj == viewport()) && (mouseEvent->button() == Qt::LeftButton)
            && (QGuiApplication::keyboardModifiers() == Qt::ExtraButton24)) {
            // open the link (if any) at the current position
            // in the noteTextEdit

            viewport()->setCursor(Qt::IBeamCursor);

            openLinkAtCursorPosition();

            return true;
        }
    } else if (event->type() == QEvent::KeyRelease) {//检测松按键的事件并做出响应
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        // reset cursor if control key was released
        if (keyEvent->key() == Qt::Key_Control) {
            viewport()->setCursor(Qt::IBeamCursor);
        }
    }

    return QTextEdit::eventFilter(obj, event);
}

/**
 * @brief Returns the markdown url at position
 * @param text
 * @param position
 * @return url string
 */
//注释表明函数需要用到text和position变量，并且需要返回一个string类型的url
//但是在实际操作过程中，作者用QString代替了String类型
QString CustomDocument::getMarkdownUrlAtPosition(const QString &text, int position)
{
    QString url;

    // get a map of parsed markdown urls with their link texts as key
    const QMap<QString, QString> urlMap = parseMarkdownUrlsFromText(text);
    QMap<QString, QString>::const_iterator i = urlMap.constBegin();
    for (; i != urlMap.constEnd(); ++i) {
        const QString &linkText = i.key();
        const QString &urlString = i.value();

        const int foundPositionStart = text.indexOf(linkText);

        if (foundPositionStart >= 0) {//谨慎处理url的长度引起的溢出问题，是健壮性的体现
            // calculate end position of found linkText
            const int foundPositionEnd = foundPositionStart + linkText.size();

            // check if position is in found string range
            if ((position >= foundPositionStart) && (position < foundPositionEnd)) {
                url = urlString;
                break;
            }
        }
    }

    return url;
}

/**
 * @brief Returns the URL under the current mouse cursor
 *
 * @return QUrl
 */
//注释表明函数有返回值，且返回值是游标下面的url
QUrl CustomDocument::getUrlUnderMouse()
{
    // place a temp cursor at the mouse position
    //获取当前鼠标光标在全局坐标系中的位置
    //并将其映射到视图端口的局部坐标系中
    // 存储在 pos变量中。
    auto pos = viewport()->mapFromGlobal(QCursor::pos());
    //使用 cursorForPosition 方法创建一个 QTextCursor 对象，该对象位于 pos 指定的位置
    QTextCursor cursor = cursorForPosition(pos);
    //获取当前 QTextCursor 的位置，即光标在文档中的索引位置
    const int cursorPosition = cursor.position();

    // select the text of the current block
    //将 QTextCursor 移动到当前文本块的开始位置
    cursor.movePosition(QTextCursor::StartOfBlock);
    //计算光标在当前文本块中的索引位置，即光标位置与文本块开始位置的差值
    const int indexInBlock = cursorPosition - cursor.position();
    //将 QTextCursor 移动到当前文本块的结束位置，并保持起始位置不变
    //即选择文本块中的所有文本
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    //先放到块的首段，再放到块的尾端，即选中文本块中所有文本，并且可以计算出当前游标的位置，即链接的位置

    // get the correct link from the selected text, or an empty URL if none found
    //调用 getMarkdownUrlAtPosition 函数，传入选中的文本和光标在文本块中的索引位置，获取对应的 Markdown URL，并将其转换为 QUrl 对象返回。如果未找到 URL，则返回空的 QUrl 对象
    return QUrl(getMarkdownUrlAtPosition(cursor.selectedText(), indexInBlock));
}

/**
 * @brief Opens the link (if any) at the current cursor position
 */
bool CustomDocument::openLinkAtCursorPosition()
{
    // find out which url in the selected text was clicked
    //先转化成QString变量
    QUrl const url = getUrlUnderMouse();
    QString const urlString = url.toString();
    //检查是否为本地文件类型
    const bool isFileUrl = urlString.startsWith(QLatin1String("file://"));
    //检查是否为遗留的附件URL?
    const bool isLegacyAttachmentUrl = urlString.startsWith(QLatin1String("file://attachments"));
    //检查是否为本地文件夹类型
    const bool isLocalFilePath = urlString.startsWith(QLatin1String("/"));
    //定义一个标志变量，指示是否将本地文件路径转换为 URL。这里硬编码为 true
    const bool convertLocalFilepathsToURLs = true;
    //如果 URL 是有效的，并且通过 isValidUrl 函数验证，或者是文件 URL、本地文件路径或遗留附件 URL，则进入后续处理逻辑
    //其中url.isValid()是基本验证，是 QUrl 类的成员函数
    //用于检查 URL 是否符合基本的 URL 格式规范
    //它会验证 URL 是否包含有效的 scheme、host 和 path 等部分，但不会对 URL 的具体内容进行深入的验证
    //而isValidUrl是自定义的函数，可以对url内容进行核验
    //例如，isValidUrl 可能会检查 URL 是否指向一个有效的资源
    //或者是否符合特定的 URL 模式（如特定的域名或路径格式）
    //它还可以结合应用程序的上下文来判断 URL 是否有效，比如检查数据库中是否存在对应的记录等
    // 但是在下面的实现中，很显然作者并没有实现这样的功能，只是检查了基本格式，可能需要完善
    //两个检验程序依次递增，符合提高效率的要求
    //如不符合基本URL的格式规范，就不用调用自定义的函数进行搜索和审查，降低了对算力的要求。
    if ((url.isValid() && isValidUrl(urlString)) || isFileUrl || isLocalFilePath
        || isLegacyAttachmentUrl) {
        //如果 URL 的 scheme（协议）包含在 _ignoredClickUrlSchemata 集合中，则忽略该 URL 并返回 false
        //可能是自定义内容的一部分，可以实现链接的自定义实现
        //提高了用户的自定义能力，值得借鉴
        if (_ignoredClickUrlSchemata.contains(url.scheme())) {
            qDebug() << __func__ << "ignored URL scheme:" << urlString;
            return false;
        }

        // ignore non-existent files
        //检查文件URL
        if (isFileUrl) {
            //从第8个字符开始提取
            QString trimmed = urlString.mid(7);
            if (!QFile::exists(trimmed)) {
                qDebug() << __func__ << ": File does not exist:" << urlString;
                // show a message box
                //使用 QMessageBox::warning 显示一个警告信息框，告知用户文件不存在
                //信息框的标题是 "File not found"
                //内容是 "The file <strong>%1</strong> does not exist."
                //其中 %1 被替换为实际的文件路径 trimmed，使用<strong> 标签以突出显示
                //提示我们可以在输出的deBug中使用html等标签语言，增加信息的分辨度，以更快的进行筛选错误
                QMessageBox::warning(
                        nullptr, tr("File not found"),
                        tr("The file <strong>%1</strong> does not exist.").arg(trimmed));
                return false;
            }
        }

        if (isLocalFilePath && !QFile::exists(urlString)) {
            qDebug() << __func__ << ": File does not exist:" << urlString;
            // show a message box
            QMessageBox::warning(nullptr, tr("File not found"),
                                 tr("The file <strong>%1</strong> does not exist.").arg(urlString));
            return false;
        }
        //验证完毕后再统一集中打开链接，有利于对打开链接的种类进行集中调控
        //在后期的纠错过程中，将打开部分的函数放在一起更有利于代码的纠错和DeBug
        if (isLocalFilePath && convertLocalFilepathsToURLs) {
            openUrl(QString("file://") + urlString);
        } else {
            openUrl(urlString);
        }

        return true;
    }
    //查找到不是URL就返回false，其余均为true，以保证程序的顺利执行
    return false;
}

/**
 * Checks if urlString is a valid url
 *
 * @param urlString
 * @return
 */
//在注释中我们可以了解到函数的作用时间差是否为可达的路径
//函数需要一个urString，返回某个值
//实际来看并没有对函数的有效性进行充分验证，函数是失败的
bool CustomDocument::isValidUrl(const QString &urlString)
{
    //关于正则表达式
    //使用 QRegularExpression 类来创建一个正则表达式对象，并使用该对象的 match 方法来匹配 urlString
    //正则表达式中，^表示匹配的开始
    //\w+表示有一个或多个单词字符（字母、数字或下划线）
    // 这里应该表示的是http或https或file等之类的，我认为应该写的更详细一些，因为常用的协议就那么些
    // 两个反斜杠
    //.+表示匹配一个或多个任意字符
    // 这里表示的应该是主机及内部的文件路径，没有固定格式
    //总的来说，这里的匹配在我看来很糟糕，没有起到多少自定义的作用
    const QRegularExpressionMatch match = QRegularExpression(R"(^\w+:\/\/.+)").match(urlString);
    return match.hasMatch();
}

/**
 * Handles clicked urls
 *
 * examples:
 * - <https://www.qownnotes.org> opens the webpage
 * - <file:///path/to/my/file/QOwnNotes.pdf> opens the file
 *   "/path/to/my/file/QOwnNotes.pdf" if the operating system supports that
 *  handler
 */
//并没有对函数调用做出一定的检查，直接调用函数库打开链接
void CustomDocument::openUrl(const QString &urlString)
{
    qDebug() << "CustomDocument " << __func__ << " - 'urlString': " << urlString;

    QDesktopServices::openUrl(QUrl(urlString));
}

/**
 * @brief Returns a map of parsed markdown urls with their link texts as key
 *
 * @param text
 * @return parsed urls
 */
//主要作用是对输入的文本内的链接进行提取
QMap<QString, QString> CustomDocument::parseMarkdownUrlsFromText(const QString &text)
{
    QMap<QString, QString> urlMap;
    QRegularExpression regex;
    QRegularExpressionMatchIterator iterator;

    // match urls like this: <http://mylink>
    //    re = QRegularExpression("(<(.+?:\\/\\/.+?)>)");
    //匹配链接类型为<http://mylink>
    regex = QRegularExpression(QStringLiteral("(<(.+?)>)"));
    iterator = regex.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }
    //匹配链接类型为[链接文本](链接)
    regex = QRegularExpression(R"((\[.*?\]\((.+?)\)))");
    iterator = regex.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: http://mylink
    //匹配裸链接
    regex = QRegularExpression(R"(\b\w+?:\/\/[^\s]+[^\s>\)])");
    iterator = regex.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = url;
    }

    // match urls like this: www.github.com
    //匹配以www开头的链接
    regex = QRegularExpression(R"(\bwww\.[^\s]+\.[^\s]+\b)");
    iterator = regex.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = QStringLiteral("http://") + url;
    }

    // match reference urls like this: [this url][1] with this later:
    // [1]: http://domain
    //匹配引用定义的链接
    regex = QRegularExpression(R"((\[.*?\]\[(.+?)\]))");
    iterator = regex.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString referenceId = match.captured(2);

        QRegularExpression refRegExp(QStringLiteral("\\[") + QRegularExpression::escape(referenceId)
                                     + QStringLiteral("\\]: (.+)"));
        QRegularExpressionMatch urlMatch = refRegExp.match(toPlainText());

        if (urlMatch.hasMatch()) {
            QString url = urlMatch.captured(1);
            urlMap[linkText] = url;
        }
    }

    return urlMap;
}

//向上移动一个块
//具体操作是，先删除本块内容，再移动到上一块的开始处，插入本块和换行，最后移动光标到指定位置
void CustomDocument::moveBlockUp()
{
    QTextCursor cursor = textCursor();

    if (cursor.blockNumber() > 0) {
        QString currentBlock = cursor.block().text();
        const int currentHorizontalPosition = cursor.positionInBlock();

        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        // also remove empty line
        cursor.deletePreviousChar();

        // cursor is now at end of prev line, move to start
        if (!cursor.movePosition(QTextCursor::StartOfBlock)) {
            // block above is empty, this is fine
        }
        // insert the removed block again
        cursor.insertText(currentBlock);
        cursor.insertBlock();

        // move cursor to previous block
        if (!cursor.movePosition(QTextCursor::PreviousBlock)) {
            qDebug() << "Could not move to previous block";
        }
        const int startPosition = cursor.position();
        cursor.setPosition(startPosition + currentHorizontalPosition);

        setTextCursor(cursor);
    }
}

//向下移动一个块，同理
void CustomDocument::moveBlockDown()
{
    QTextCursor cursor = textCursor();

    QString currentBlock = cursor.block().text();
    const int currentHorizontalPosition = cursor.positionInBlock();

    cursor.movePosition(QTextCursor::StartOfBlock);
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    cursor.removeSelectedText();
    // also remove empty line
    cursor.deleteChar();

    if (!cursor.movePosition(QTextCursor::EndOfBlock)) {
        // block below is empty, this is fine
    }
    // insert the removed block again
    cursor.insertBlock();
    cursor.insertText(currentBlock);

    // move cursor to next block
    if (!cursor.movePosition(QTextCursor::StartOfBlock)) {
        qDebug() << "Could not move to start of next block";
    }
    const int startPosition = cursor.position();
    cursor.setPosition(startPosition + currentHorizontalPosition);

    setTextCursor(cursor);
}
