// 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

#include "qqmllsutils_p.h"
#include "documentsymbolutils_p.h"
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlDom/private/qqmldomoutwriter_p.h>
#include <stack>

QT_BEGIN_NAMESPACE

namespace DocumentSymbolUtils {
using QLspSpecification::SymbolKind;
using namespace QQmlJS::Dom;

struct TypeSymbolRelation
{
    DomType domType;
    SymbolKind symbolKind;
};

constexpr static std::array<TypeSymbolRelation, 9> s_TypeSymbolRelations = { {
        { DomType::Binding, SymbolKind::Variable },
        { DomType::PropertyDefinition, SymbolKind::Property },
        // Although MethodInfo simply relates to Method, SymbolKind requires special handling:
        // When MethodInfo represents a Signal, its SymbolKind is set to Event.
        // This distinction is explicitly managed in the symbolKindOf() helper function.
        // see also QTBUG-128423
        { DomType::MethodInfo, SymbolKind::Method },
        { DomType::Id, SymbolKind::Key },
        { DomType::QmlObject, SymbolKind::Object },
        { DomType::EnumDecl, SymbolKind::Enum },
        { DomType::EnumItem, SymbolKind::EnumMember },
        { DomType::QmlComponent, SymbolKind::Module },
        { DomType::QmlFile, SymbolKind::File },
} };

[[nodiscard]] constexpr static inline SymbolKind symbolKindFor(const DomType &type)
{
    // constexpr std::find_if is only from c++20
    for (const auto &mapping : s_TypeSymbolRelations) {
        if (mapping.domType == type) {
            return mapping.symbolKind;
        }
    }
    return SymbolKind::Null;
}

constexpr static inline bool documentSymbolNotSupportedFor(const DomType &type)
{
    return symbolKindFor(type) == SymbolKind::Null;
}

static bool propertyBoundAtDefinitionLine(const DomItem &propertyDefinition)
{
    Q_ASSERT(propertyDefinition.internalKind() == DomType::PropertyDefinition);
    return FileLocations::treeOf(propertyDefinition)->info().regions[ColonTokenRegion].isValid();
}

static inline bool shouldFilterOut(const DomItem &item)
{
    const auto itemType = item.internalKind();
    if (documentSymbolNotSupportedFor(itemType)) {
        return true;
    }
    if (itemType == DomType::PropertyDefinition && propertyBoundAtDefinitionLine(item)) {
        // without this check there is a "duplication" of symbols.
        // one representing PropertyDefinition another one - Binding
        return true;
    }
    return false;
}

static std::optional<QByteArray> tryGetQmlObjectDetail(const DomItem &qmlObj)
{
    using namespace QQmlJS::Dom;
    Q_ASSERT(qmlObj.internalKind() == DomType::QmlObject);
    bool hasId = !qmlObj.idStr().isEmpty();
    if (hasId) {
        return qmlObj.idStr().toUtf8();
    }
    const bool isRootObject = qmlObj.component().field(Fields::objects).index(0) == qmlObj;
    if (isRootObject) {
        return "root";
    }
    return std::nullopt;
}

static std::optional<QByteArray> tryGetBindingDetail(const DomItem &bItem)
{
    const auto *bindingPtr = bItem.as<Binding>();
    Q_ASSERT(bindingPtr);
    switch (bindingPtr->valueKind()) {
    case BindingValueKind::ScriptExpression: {
        auto exprCode = bindingPtr->scriptExpressionValue()->code();
        if (exprCode.length() > 25) {
            return QStringView(exprCode).first(22).toUtf8().append("...");
        }
        if (exprCode.endsWith(QStringLiteral(";"))) {
            exprCode.chop(1);
        }
        return exprCode.toUtf8();
    }
    default:
        // Value is QmlObject or QList<QmlObject> => no detail
        return std::nullopt;
    }
}

static inline QByteArray getMethodDetail(const DomItem &mItem)
{
    const auto *methodInfoPtr = mItem.as<MethodInfo>();
    Q_ASSERT(methodInfoPtr);
    return methodInfoPtr->signature(mItem).toUtf8();
}

std::optional<QByteArray> tryGetDetailOf(const DomItem &item)
{
    switch (item.internalKind()) {
    case DomType::Id: {
        const auto name = item.name();
        return name.isEmpty() ? std::nullopt : std::make_optional(name.toUtf8());
    }
    case DomType::EnumItem:
        return QByteArray::number(item.as<EnumItem>()->value());
    case DomType::QmlObject:
        return tryGetQmlObjectDetail(item);
    case DomType::MethodInfo:
        return getMethodDetail(item);
    case DomType::Binding:
        return tryGetBindingDetail(item);
    default:
        return std::nullopt;
    }
}

// TODO move to qmllsUtils?
static inline bool isSubRange(const QLspSpecification::Range &potentialSubRange,
                              const QLspSpecification::Range &range)
{
    // Check if the start of a is greater than or equal to the start of b
    bool startContained = (potentialSubRange.start.line > range.start.line
                           || (potentialSubRange.start.line == range.start.line
                               && potentialSubRange.start.character >= range.start.character));

    // Check if the end of a is less than or equal to the end of b
    bool endContained = (potentialSubRange.end.line < range.end.line
                         || (potentialSubRange.end.line == range.end.line
                             && potentialSubRange.end.character <= range.end.character));

    return startContained && endContained;
}

using MutableRefToDocumentSymbol = QLspSpecification::DocumentSymbol &;
[[nodiscard]] static MutableRefToDocumentSymbol
findDirectParentFor(const QLspSpecification::DocumentSymbol &child,
                    MutableRefToDocumentSymbol currentParent)
{
    const auto containsChildRange =
            [&range = child.range](const QLspSpecification::DocumentSymbol &symbol) {
                return isSubRange(range, symbol.range);
            };
    // Parent's Range covers children's Ranges
    // all children, grand-children, grand-grand-children and so forth
    // are not supposed to have overlapping Ranges, hence it's just "gready" approach
    // 1. find a Symbol among children, containing a Range
    // 2. set it as currentCandidate
    // 3. repeat
    std::reference_wrapper<QLspSpecification::DocumentSymbol> currentCandidate(currentParent);
    while (containsChildRange(currentCandidate) && currentCandidate.get().children.has_value()) {
        auto newCandidate =
                std::find_if(currentCandidate.get().children->begin(),
                             currentCandidate.get().children->end(), containsChildRange);
        if (newCandidate == currentCandidate.get().children->end()) {
            break;
        }
        currentCandidate = std::ref(*newCandidate);
    }
    return currentCandidate;
}

using DocumentSymbolPredicate =
        qxp::function_ref<bool(const QLspSpecification::DocumentSymbol &) const>;
[[nodiscard]] static SymbolsList extractChildrenIf(const DocumentSymbolPredicate shouldBeReadopted,
                                                   MutableRefToDocumentSymbol currentParent)
{
    if (!currentParent.children.has_value()) {
        return {};
    }
    auto &parentsChildren = currentParent.children.value();
    SymbolsList extractedChildren;
    extractedChildren.reserve(parentsChildren.size());
    auto [_, toBeRemoved] = std::partition_copy(parentsChildren.cbegin(), parentsChildren.cend(),
                                                std::back_inserter(extractedChildren),
                                                parentsChildren.begin(), shouldBeReadopted);
    parentsChildren.erase(toBeRemoved, parentsChildren.end());
    return extractedChildren;
}

static inline void adopt(QLspSpecification::DocumentSymbol &&child,
                         MutableRefToDocumentSymbol parent)
{
    if (!parent.children.has_value()) {
        parent.children.emplace({ std::move(child) });
        return;
    }
    parent.children->emplace_back(std::move(child));
}

static void readoptChildrenIf(const DocumentSymbolPredicate unaryPred,
                              MutableRefToDocumentSymbol currentParent)
{
    auto childrenToBeReadopted = extractChildrenIf(unaryPred, currentParent);
    for (auto &&child : childrenToBeReadopted) {
        auto &newParentRef = findDirectParentFor(child, currentParent);
        adopt(std::move(child), newParentRef);
    }
}

// Readopts all Enum-s and Id-s
static void reorganizeQmlComponentSymbol(MutableRefToDocumentSymbol qmlCompSymbol)
{
    Q_ASSERT(qmlCompSymbol.kind == symbolKindFor(DomType::QmlComponent));
    if (!qmlCompSymbol.children.has_value()) {
        // nothing to reorganize
        return;
    }

    constexpr auto enumDeclSymbolKind = symbolKindFor(DomType::EnumDecl);
    const auto symbolIsEnumDecl = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
        return symbol.kind == enumDeclSymbolKind;
    };
    readoptChildrenIf(symbolIsEnumDecl, qmlCompSymbol);
}

/*! \internal
 *  This function reorganizes \c qmlFileSymbols (result of assembleSymbolsForQmlFile)
 *  in the following way:
 *  1. Moves Symbol-s representing Enum-s and inline QmlComponent-s
 * to their respective range-containing parents , a.k.a. direct structural parents.
 *  2. Reassignes head to the DocumentSymbol representing root QmlObject of the main
 * QmlComponent
 */
void reorganizeForOutlineView(SymbolsList &qmlFileSymbols)
{
    Q_ASSERT(qmlFileSymbols.at(0).kind == symbolKindFor(DomType::QmlFile)
             && qmlFileSymbols.at(0).children.has_value());

    auto &qmlFileSymbol = qmlFileSymbols[0];
    constexpr auto qmlCompSymbolKind = symbolKindFor(DomType::QmlComponent);
    for (auto &childSymbol : qmlFileSymbol.children.value()) {
        if (childSymbol.kind == qmlCompSymbolKind) {
            reorganizeQmlComponentSymbol(childSymbol);
        }
    }

    const auto symbolIsInlineComp = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
        return symbol.kind == qmlCompSymbolKind && symbol.name.contains(".");
    };
    readoptChildrenIf(symbolIsInlineComp, qmlFileSymbol);

    // move pointer from the documentSymbol representing QmlFile
    // to the documentSymbols representing children of main QmlComponent
    // a.k.a. ignore / not to show QmlFile and mainComponent symbols
    qmlFileSymbols = qmlFileSymbol.children->at(0).children.value();
}

/*! \internal
 * Constructs a \c DocumentSymbol for an \c Item with the provided \c children.
 * Returns \c children if the current \c Item should not be represented via a \c DocumentSymbol.
 */
SymbolsList buildSymbolOrReturnChildren(const DomItem &item, SymbolsList &&children)
{
    if (shouldFilterOut(item)) {
        // nothing to build, just returning children
        return std::move(children);
    }

    const auto buildPartialSymbol = [](const DomItem &item) {
        QLspSpecification::DocumentSymbol symbol;
        symbol.kind = symbolKindOf(item);
        symbol.name = symbolNameOf(item);
        symbol.detail = tryGetDetailOf(item);
        std::tie(symbol.range, symbol.selectionRange) = symbolRangesOf(item);
        return symbol;
    };

    auto symbol = buildPartialSymbol(item);
    if (!children.empty()) {
        symbol.children.emplace(std::move(children));
    }
    /*
     To avoid pushing down Id items through the DocumentSymbol tree,
     as part of rearrangement step, it was decided to handle them here explicitly.
     That Id issue atm only affects objects
     If / when Id is moving from component level to Object level this should be reflected
     also in the visiting logic.
     TODO(QTBUG-128274)
     */
    if (const auto objPtr = item.as<QmlObject>()) {
        if (const auto idItem = item.component().field(Fields::ids).key(objPtr->idStr()).index(0)) {
            auto idSymbol = buildPartialSymbol(idItem);
            adopt(std::move(idSymbol), symbol);
        }
    }
    return SymbolsList{ std::move(symbol) };
}

std::pair<QLspSpecification::Range, QLspSpecification::Range> symbolRangesOf(const DomItem &item)
{
    const auto &fLoc = FileLocations::treeOf(item)->info();
    const auto fullRangeSourceloc = fLoc.fullRegion;
    const auto selectionRangeSourceLoc = fLoc.regions[IdentifierRegion].isValid()
            ? fLoc.regions[IdentifierRegion]
            : fullRangeSourceloc;

    auto fItem = item.containingFile();
    Q_ASSERT(fItem);
    const QString &code = fItem.ownerAs<QmlFile>()->code();
    return { QQmlLSUtils::qmlLocationToLspLocation(
                     QQmlLSUtils::Location::from({}, fullRangeSourceloc, code)),
             QQmlLSUtils::qmlLocationToLspLocation(
                     QQmlLSUtils::Location::from({}, selectionRangeSourceLoc, code)) };
}

QByteArray symbolNameOf(const DomItem &item)
{
    if (item.internalKind() == DomType::Id) {
        return "id";
    }
    return (item.name().isEmpty() ? item.internalKindStr() : item.name()).toUtf8();
}

QLspSpecification::SymbolKind symbolKindOf(const DomItem &item)
{
    if (item.internalKind() == DomType::MethodInfo) {
        const auto *methodInfoPtr = item.as<MethodInfo>();
        Q_ASSERT(methodInfoPtr);
        return methodInfoPtr->methodType == MethodInfo::MethodType::Signal
                ? SymbolKind::Event
                : symbolKindFor(DomType::MethodInfo);
    }
    return symbolKindFor(item.internalKind());
}

/*! \internal
 * Design decisions behind this class are the following:
 * 1. It is an implementation detail of the free \c assembleSymbolsForQmlFile function
 * 2. It can only be initialized and used once per \c Item.
 * This is enforced by its \c refToRootItem reference member.
 * 3. It is tested via the public \c assembleSymbolsForQmlFile function.
 */
class DocumentSymbolVisitor
{
public:
    DocumentSymbolVisitor(const DomItem &item, const AssemblingFunction af)
        : m_assemble(af), m_refToRootItem(item) {};

    static const FieldFilter &fieldsFilter();

    [[nodiscard]] SymbolsList assembleSymbols();

private:
    [[nodiscard]] SymbolsList popAndAssembleSymbolsFor(const DomItem &item);

    void appendToTop(const SymbolsList &symbols);

private:
    const AssemblingFunction m_assemble;
    const DomItem &m_refToRootItem;
    std::stack<SymbolsList> m_stackOfChildrenSymbols;
};

const FieldFilter &DocumentSymbolVisitor::fieldsFilter()
{
    // TODO(QTBUG-128118) add only fields to be visited and not the ones
    // to be removed.
    static const FieldFilter ff{
        {}, // to add
        {
                // to remove
                { QString(), Fields::code.toString() },
                { QString(), Fields::postCode.toString() },
                { QString(), Fields::preCode.toString() },
                { QString(), Fields::importScope.toString() },
                { QString(), Fields::fileLocationsTree.toString() },
                { QString(), Fields::astComments.toString() },
                { QString(), Fields::comments.toString() },
                { QString(), Fields::exports.toString() },
                { QString(), Fields::propertyInfos.toString() },
                { QLatin1String("FileLocationsNode"), Fields::parent.toString() },
                //^^^ FieldFilter::default
                { QString(), Fields::errors.toString() },
                { QString(), Fields::imports.toString() },
                { QString(), Fields::prototypes.toString() },
                { QString(), Fields::annotations.toString() },
                { QString(), Fields::attachedType.toString() },
                { QString(), Fields::canonicalFilePath.toString() },
                { QString(), Fields::isValid.toString() },
                { QString(), Fields::isSingleton.toString() },
                { QString(), Fields::isCreatable.toString() },
                { QString(), Fields::isComposite.toString() },
                { QString(), Fields::attachedTypeName.toString() },
                { QString(), Fields::pragmas.toString() },
                { QString(), Fields::defaultPropertyName.toString() },
                { QString(), Fields::name.toString() },
                { QString(), Fields::nameIdentifiers.toString() },
                { QString(), Fields::prototypes.toString() },
                { QString(), Fields::nextScope.toString() },
                { QString(), Fields::parameters.toString() },
                { QString(), Fields::methodType.toString() },
                { QString(), Fields::type.toString() },
                { QString(), Fields::isConstructor.toString() },
                { QString(), Fields::returnType.toString() },
                { QString(), Fields::body.toString() },
                { QString(), Fields::access.toString() },
                { QString(), Fields::typeName.toString() },
                { QString(), Fields::isReadonly.toString() },
                { QString(), Fields::isList.toString() },
                { QString(), Fields::bindingIdentifiers.toString() },
                { QString(), Fields::bindingType.toString() },
                { QString(), Fields::isSignalHandler.toString() },
                // prop def?
                { QString(), Fields::isPointer.toString() },
                { QString(), Fields::isFinal.toString() },
                { QString(), Fields::isAlias.toString() },
                { QString(), Fields::isDefaultMember.toString() },
                { QString(), Fields::isRequired.toString() },
                { QString(), Fields::read.toString() },
                { QString(), Fields::write.toString() },
                { QString(), Fields::bindable.toString() },
                { QString(), Fields::notify.toString() },
                { QString(), Fields::type.toString() },
                // scriptExpr
                { QString(), Fields::scriptElement.toString() },
                { QString(), Fields::localOffset.toString() },
                { QString(), Fields::astRelocatableDump.toString() },
                { QString(), Fields::expressionType.toString() },
                // components
                { QString(), Fields::subComponents.toString() },
                // BEWARE
                // Ids and IdStr are filtered out during the visit, because
                // documentSymbol-s for them will be explicitly handled as part of the
                // creation of symbol for QmlObject
                { QString(), Fields::ids.toString() },
                { QString(), Fields::idStr.toString() },

                // id
                { QString(), Fields::referredObject.toString() },
                // enum item
                { QLatin1String("EnumItem"), Fields::value.toString() },
        }
    };
    return ff;
}

SymbolsList DocumentSymbolVisitor::assembleSymbols()
{
    using namespace QQmlJS::Dom;
    auto openingVisitor = [this](const Path &, const DomItem &, bool) -> bool {
        m_stackOfChildrenSymbols.emplace();
        return true;
    };
    auto closingVisitor = [this](const Path &, const DomItem &item, bool) -> bool {
        // it's closing Visitor, openingVisitor must have pushed something
        Q_ASSERT(!m_stackOfChildrenSymbols.empty());
        if (m_stackOfChildrenSymbols.size() == 1) {
            // reached children of root, nothing to do
            return false;
        }
        auto symbols = popAndAssembleSymbolsFor(item);
        appendToTop(symbols);
        return true;
    };
    m_refToRootItem.visitTree(Path(), emptyChildrenVisitor, VisitOption::Default, openingVisitor,
                              closingVisitor, fieldsFilter());
    return popAndAssembleSymbolsFor(m_refToRootItem);
}

SymbolsList DocumentSymbolVisitor::popAndAssembleSymbolsFor(const DomItem &item)
{
    Q_ASSERT(!m_stackOfChildrenSymbols.empty());
    auto atEnd = qScopeGuard([this]() { m_stackOfChildrenSymbols.pop(); });
    return m_assemble(item, std::move(m_stackOfChildrenSymbols.top()));
}

void DocumentSymbolVisitor::appendToTop(const SymbolsList &symbols)
{
    Q_ASSERT(!m_stackOfChildrenSymbols.empty());
    m_stackOfChildrenSymbols.top().append(symbols);
}

SymbolsList assembleSymbolsForQmlFile(const DomItem &item, const AssemblingFunction af)
{
    Q_ASSERT(item.internalKind() == DomType::QmlFile);
    DocumentSymbolVisitor visitor(item, af);
    return visitor.assembleSymbols();
}
} // namespace DocumentSymbolUtils

QT_END_NAMESPACE
