// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:trusted-sources

#include <qdochtmlparser_p.h>
#include <QtCore/qregularexpression.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

// An emprical value to avoid too much content
static constexpr qsizetype firstIndexOfParagraphTag = 400;

// A paragraph can start with <p><i>, or <p><tt>
// We need smallest value to use QString::indexOf
static constexpr auto lengthOfStartParagraphTag = qsizetype(std::char_traits<char>::length("<p>"));
static constexpr auto lengthOfEndParagraphTag = qsizetype(std::char_traits<char>::length("</p>"));

static QString getContentsByMarks(const QString &html, QString startMark, QString endMark)
{
    startMark.prepend("$$$"_L1);
    endMark.prepend("<!-- @@@"_L1);

    QString contents;
    qsizetype start = html.indexOf(startMark);
    if (start != -1) {
        start = html.indexOf("-->"_L1, start);
        if (start != -1) {
            qsizetype end = html.indexOf(endMark, start);
            if (end != -1) {
                start += qsizetype(std::char_traits<char>::length("-->"));
                contents = html.mid(start, end - start);
            }
        }
    }
    return contents;
}


static void stripAllHtml(QString *html)
{
    Q_ASSERT(html);
    html->remove(QRegularExpression("<.*?>"_L1));
}

/*! \internal
    \brief Process the string obtained from start mark to end mark.
    This is duplicated from QtC's Utils::HtmlExtractor, modified on top of it.
*/
static void processOutput(QString *html)
{
    Q_ASSERT(html);
    if (html->isEmpty())
        return;

    // Do not write the first paragraph in case it has extra tags below.
    // <p><i>This is only used on the Maemo platform.</i></p>
    // or: <p><tt>This is used on Windows only.</tt></p>
    // or: <p>[Conditional]</p>
    const auto skipFirstParagraphIfNeeded = [html](qsizetype &index){
        const bool shouldSkipFirstParagraph = html->indexOf(QLatin1String("<p><i>")) == index ||
                html->indexOf(QLatin1String("<p><tt>")) == index ||
                html->indexOf(QLatin1String("<p><b>")) == index ||
                html->indexOf(QLatin1String("<p>[Conditional]</p>")) == index;

        if (shouldSkipFirstParagraph)
            index = html->indexOf(QLatin1String("</p>"), index) + lengthOfEndParagraphTag;
    };

    // Try to get the entire first paragraph, but if one is not found or if its opening
    // tag is not in the very beginning (using an empirical value as the limit)
    // the html is cleared out to avoid too much content.
    qsizetype index = html->indexOf(QLatin1String("<p>"));
    if (index != -1 && index < firstIndexOfParagraphTag) {
        skipFirstParagraphIfNeeded(index);
        qsizetype endIndex = html->indexOf(QLatin1String("</p>"), index + lengthOfStartParagraphTag);
        if (endIndex != -1) {
            *html = html->mid(index, endIndex - index);
        } else {
            html->clear();
        }
    } else {
        html->clear();
    }
}

class ExtractQmlType : public HtmlExtractor
{
public:
    QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override;
};

class ExtractQmlProperty : public HtmlExtractor
{
public:
    QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override;
};

class ExtractQmlMethodOrSignal : public HtmlExtractor
{
public:
    QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override;
};

QString ExtractQmlType::extract(const QString &code, const QString &element, ExtractionMode mode)
{
    QString result;
    // Get brief description
    if (mode == ExtractionMode::Simplified) {
        result = getContentsByMarks(code, element + "-brief"_L1 , element);
        // Remove More...
        if (!result.isEmpty()) {
            const auto tailToRemove = "More..."_L1;
            const auto lastIndex = result.lastIndexOf(tailToRemove);
            if (lastIndex != -1)
                result.remove(lastIndex, tailToRemove.length());
        }
    } else {
        result = getContentsByMarks(code, element + "-description"_L1, element);
        // Remove header
        if (!result.isEmpty()) {
            const auto headerToRemove = "Detailed Description"_L1;
            const auto firstIndex = result.indexOf(headerToRemove);
            if (firstIndex != -1)
                result.remove(firstIndex, headerToRemove.length());
        }
    }

    stripAllHtml(&result);
    return result.trimmed();
}

QString ExtractQmlProperty::extract(const QString &code, const QString &keyword, ExtractionMode mode)
{
    QString result;
    // Qt 5.15 way of finding properties in doc
    QString startMark = QString::fromLatin1("<a name=\"%1-prop\">").arg(keyword);
    qsizetype startIndex = code.indexOf(startMark);
    if (startIndex == -1) {
        // if not found, try Qt6
        startMark = QString::fromLatin1(
                            "<td class=\"tblQmlPropNode\"><p>\n<span class=\"name\">%1</span>")
                            .arg(keyword);
        startIndex = code.indexOf(startMark);
    }

    if (startIndex != -1) {
        result = code.mid(startIndex + startMark.size());
        startIndex = result.indexOf(QLatin1String("<div class=\"qmldoc\"><p>"));
    } else {
        result = getContentsByMarks(code, keyword + "-prop"_L1, keyword );
        startIndex = result.indexOf(QLatin1String("<p>"));
    }

    if (startIndex == -1)
        return {};
    result = result.mid(startIndex);
    if (mode == ExtractionMode::Simplified)
        processOutput(&result);
    stripAllHtml(&result);
    return result.trimmed();
}

QString ExtractQmlMethodOrSignal::extract(const QString &code, const QString &keyword, ExtractionMode mode)
{
    // the case with <!-- $$$childAt[overload1]$$$childAtrealreal -->
    QString mark = QString::fromLatin1("$$$%1[overload1]$$$%1").arg(keyword);
    qsizetype startIndex = code.indexOf(mark);
    if (startIndex != -1) {
        startIndex = code.indexOf("-->"_L1, startIndex + mark.length());
        if (startIndex == -1)
            return {};
    } else {
        // it could be part of the method list
        mark = QString::fromLatin1("<span class=\"name\">%1</span>")
                .arg(keyword);
        startIndex = code.indexOf(mark);
        if (startIndex != -1)
            startIndex += mark.length();
        else
            return {};
    }

    startIndex = code.indexOf(QLatin1String("<div class=\"qmldoc\"><p>"), startIndex);
    if (startIndex == -1)
        return {};

    QString endMark = QString::fromLatin1("<!-- @@@");
    qsizetype endIndex = code.indexOf(endMark, startIndex);
    QString contents = code.mid(startIndex, endIndex);
    if (mode == ExtractionMode::Simplified)
        processOutput(&contents);
    stripAllHtml(&contents);
    return contents.trimmed();
}

ExtractDocumentation::ExtractDocumentation(QQmlJS::Dom::DomType domType)
{
    using namespace QQmlJS::Dom;
    switch (domType) {
    case DomType::QmlObject:
        m_extractor = std::make_unique<ExtractQmlType>();
        break;
    case DomType::Binding:
    case DomType::PropertyDefinition:
        m_extractor = std::make_unique<ExtractQmlProperty>();
        break;
    case DomType::MethodInfo:
        m_extractor = std::make_unique<ExtractQmlMethodOrSignal>();
        break;
    default:
        break;
    }
}

QString ExtractDocumentation::execute(const QString &code, const QString &keyword, HtmlExtractor::ExtractionMode mode)
{
    Q_ASSERT(m_extractor);
    return m_extractor->extract(code, keyword, mode);
}

QT_END_NAMESPACE
