/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <basidesh.hxx>
#include <objectbrowser.hxx>
#include <objectbrowsersearch.hxx>
#include <idedataprovider.hxx>
#include "idetimer.hxx"

#include <bitmaps.hlst>
#include <iderid.hxx>
#include <strings.hrc>

#include <basctl/sbxitem.hxx>
#include <comphelper/processfactory.hxx>
#include <o3tl/string_view.hxx>
#include <rtl/string.hxx>
#include <rtl/ustrbuf.hxx>
#include <sal/log.hxx>
#include <sfx2/bindings.hxx>
#include <sfx2/dispatch.hxx>
#include <sfx2/event.hxx>
#include <svl/itemset.hxx>
#include <sfx2/sfxsids.hrc>
#include <sfx2/viewfrm.hxx>
#include <vcl/taskpanelist.hxx>
#include <vcl/weld.hxx>

#include <com/sun/star/system/XSystemShellExecute.hpp>
#include <com/sun/star/system/SystemShellExecuteFlags.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>
#include <com/sun/star/lang/XMultiComponentFactory.hpp>

namespace basctl
{
namespace
{
// Helper to get an icon resource ID for a given symbol type.
OUString GetIconForSymbol(IdeSymbolKind eKind)
{
    switch (eKind)
    {
        case IdeSymbolKind::ROOT_APPLICATION_LIBS:
            return RID_BMP_INSTALLATION;
        case IdeSymbolKind::ROOT_DOCUMENT_LIBS:
            return RID_BMP_DOCUMENT;
        case IdeSymbolKind::LIBRARY:
            return RID_BMP_MODLIB;
        case IdeSymbolKind::MODULE:
            return RID_BMP_MODULE;
        case IdeSymbolKind::FUNCTION:
        case IdeSymbolKind::SUB:
            return RID_BMP_MACRO;
        case IdeSymbolKind::ROOT_UNO_APIS:
            return u"cmd/sc_configuredialog.png"_ustr;
        case IdeSymbolKind::CLASS_MODULE:
            return u"cmd/sc_insertobject.png"_ustr;
        case IdeSymbolKind::UNO_NAMESPACE:
            return u"cmd/sc_navigator.png"_ustr;
        case IdeSymbolKind::UNO_INTERFACE:
            return u"cmd/sc_insertplugin.png"_ustr;
        case IdeSymbolKind::UNO_SERVICE:
            return u"cmd/sc_insertobjectstarmath.png"_ustr;
        case IdeSymbolKind::UNO_STRUCT:
            return u"cmd/sc_insertframe.png"_ustr;
        case IdeSymbolKind::UNO_ENUM:
            return u"cmd/sc_numberformatmenu.png"_ustr;
        case IdeSymbolKind::UNO_PROPERTY:
            return u"cmd/sc_controlproperties.png"_ustr;
        case IdeSymbolKind::UNO_METHOD:
            return u"cmd/sc_insertformula.png"_ustr;
        case IdeSymbolKind::ENUM_MEMBER:
            return u"cmd/sc_bullet.png"_ustr;
        case IdeSymbolKind::PLACEHOLDER:
            return u"cmd/sc_more.png"_ustr;
        default:
            return u"cmd/sc_insertobject.png"_ustr;
    }
}

// Helper to determine if a symbol is expandable in the tree view.
bool IsExpandable(const IdeSymbolInfo& rSymbol)
{
    switch (rSymbol.eKind)
    {
        case IdeSymbolKind::ROOT_UNO_APIS:
        case IdeSymbolKind::ROOT_APPLICATION_LIBS:
        case IdeSymbolKind::ROOT_DOCUMENT_LIBS:
        case IdeSymbolKind::LIBRARY:
        case IdeSymbolKind::MODULE:
        case IdeSymbolKind::CLASS_MODULE:
        case IdeSymbolKind::UNO_NAMESPACE:
            return true;
        default:
            // This case is for future use when members are nested.
            return !rSymbol.mapMembers.empty();
    }
}

std::shared_ptr<const IdeSymbolInfo>
GetSymbolForIter(const weld::TreeIter& rIter, weld::TreeView& rTree,
                 const std::map<OUString, std::shared_ptr<IdeSymbolInfo>>& rIndex)
{
    const OUString sId = rTree.get_id(rIter);
    if (sId.isEmpty())
    {
        return nullptr;
    }

    auto it = rIndex.find(sId);

    return (it != rIndex.end()) ? it->second : nullptr;
}

bool ShouldShowMembers(const IdeSymbolInfo& rSymbol)
{
    switch (rSymbol.eKind)
    {
        case IdeSymbolKind::UNO_CONSTANTS:
        case IdeSymbolKind::UNO_ENUM:
        case IdeSymbolKind::UNO_EXCEPTION:
        case IdeSymbolKind::UNO_INTERFACE:
        case IdeSymbolKind::UNO_SERVICE:
        case IdeSymbolKind::UNO_STRUCT:
        case IdeSymbolKind::UDT:
        case IdeSymbolKind::MODULE:
        case IdeSymbolKind::CLASS_MODULE:
            return true;
        default:
            return false;
    }
}
OUString GetGroupNameForKind(IdeSymbolKind eKind)
{
    switch (eKind)
    {
        case IdeSymbolKind::UNO_PROPERTY:
        case IdeSymbolKind::PROPERTY_GET:
        case IdeSymbolKind::PROPERTY_LET:
        case IdeSymbolKind::PROPERTY_SET:
            return IDEResId(RID_STR_OB_GROUP_PROPERTIES);
        case IdeSymbolKind::UNO_METHOD:
            return IDEResId(RID_STR_OB_GROUP_METHODS);
        case IdeSymbolKind::UNO_FIELD:
            return IDEResId(RID_STR_OB_GROUP_FIELDS);
        case IdeSymbolKind::ENUM_MEMBER:
            return IDEResId(RID_STR_OB_GROUP_MEMBERS);
        case IdeSymbolKind::SUB:
            return IDEResId(RID_STR_OB_GROUP_PROCEDURES);
        case IdeSymbolKind::FUNCTION:
            return IDEResId(RID_STR_OB_GROUP_FUNCTIONS);
        default:
            return IDEResId(RID_STR_OB_GROUP_OTHER);
    }
}

OUString GetSymbolTypeDescription(basctl::IdeSymbolKind eKind)
{
    using basctl::IdeSymbolKind;
    switch (eKind)
    {
        case IdeSymbolKind::ROOT_UNO_APIS:
            return IDEResId(RID_STR_OB_TYPE_UNO_APIS_ROOT);
        case IdeSymbolKind::ROOT_APPLICATION_LIBS:
            return IDEResId(RID_STR_OB_TYPE_APP_LIBS);
        case IdeSymbolKind::ROOT_DOCUMENT_LIBS:
            return IDEResId(RID_STR_OB_TYPE_DOC_LIBS);
        case IdeSymbolKind::LIBRARY:
            return IDEResId(RID_STR_OB_TYPE_LIBRARY);
        case IdeSymbolKind::MODULE:
            return IDEResId(RID_STR_OB_TYPE_MODULE);
        case IdeSymbolKind::CLASS_MODULE:
            return IDEResId(RID_STR_OB_TYPE_CLASS_MODULE);
        case IdeSymbolKind::UNO_NAMESPACE:
            return IDEResId(RID_STR_OB_TYPE_NAMESPACE);
        case IdeSymbolKind::UNO_INTERFACE:
            return IDEResId(RID_STR_OB_TYPE_INTERFACE);
        case IdeSymbolKind::UNO_SERVICE:
            return IDEResId(RID_STR_OB_TYPE_SERVICE);
        case IdeSymbolKind::UNO_STRUCT:
            return IDEResId(RID_STR_OB_TYPE_STRUCT);
        case IdeSymbolKind::UNO_ENUM:
            return IDEResId(RID_STR_OB_TYPE_ENUM);
        case IdeSymbolKind::UNO_CONSTANTS:
            return IDEResId(RID_STR_OB_TYPE_CONSTANTS);
        case IdeSymbolKind::UNO_EXCEPTION:
            return IDEResId(RID_STR_OB_TYPE_EXCEPTION);
        case IdeSymbolKind::UNO_TYPEDEF:
            return IDEResId(RID_STR_OB_TYPE_TYPEDEF);
        case IdeSymbolKind::UNO_METHOD:
            return IDEResId(RID_STR_OB_TYPE_METHOD);
        case IdeSymbolKind::UNO_PROPERTY:
            return IDEResId(RID_STR_OB_TYPE_PROPERTY);
        case IdeSymbolKind::UNO_FIELD:
            return IDEResId(RID_STR_OB_TYPE_FIELD);
        case IdeSymbolKind::SUB:
            return IDEResId(RID_STR_OB_TYPE_SUB);
        case IdeSymbolKind::FUNCTION:
            return IDEResId(RID_STR_OB_TYPE_FUNCTION);
        case IdeSymbolKind::ENUM_MEMBER:
            return IDEResId(RID_STR_OB_TYPE_ENUM_MEMBER);
        default:
            return IDEResId(RID_STR_OB_TYPE_ITEM);
    }
}

void FormatMethodSignature(rtl::OUStringBuffer& rBuffer, const basctl::IdeSymbolInfo& rSymbol)
{
    rBuffer.append(rSymbol.sName);
    rBuffer.append(u"(");
    if (!rSymbol.aParameters.empty())
    {
        for (size_t i = 0; i < rSymbol.aParameters.size(); ++i)
        {
            const auto& param = rSymbol.aParameters[i];
            if (param.bIsOptional)
            {
                rBuffer.append(u"Optional ");
            }
            if (!param.bIsByVal)
            {
                rBuffer.append(u"ByRef ");
            }
            if (param.bIsOut && !param.bIsIn)
            {
                rBuffer.append(u"[out] ");
            }
            else if (param.bIsOut && param.bIsIn)
            {
                rBuffer.append(u"[inout] ");
            }

            rBuffer.append(param.sName + u" As " + param.sTypeName);
            if (i < rSymbol.aParameters.size() - 1)
            {
                rBuffer.append(u", ");
            }
        }
    }
    rBuffer.append(u")");
    if (!rSymbol.sReturnTypeName.isEmpty() && rSymbol.sReturnTypeName != "Void")
    {
        rBuffer.append(u" As " + rSymbol.sReturnTypeName);
    }
}

void FormatPropertySignature(rtl::OUStringBuffer& rBuffer, const basctl::IdeSymbolInfo& rSymbol)
{
    rBuffer.append(rSymbol.sName);
    OUString sType = !rSymbol.sTypeName.isEmpty() ? rSymbol.sTypeName : rSymbol.sReturnTypeName;
    if (!sType.isEmpty())
    {
        rBuffer.append(u" As " + sType);
    }
}

OUString AccessModifierToString(IdeAccessModifier eAccess)
{
    switch (eAccess)
    {
        case IdeAccessModifier::PUBLIC:
            return IDEResId(RID_STR_OB_ACCESS_PUBLIC);
        case IdeAccessModifier::PRIVATE:
            return IDEResId(RID_STR_OB_ACCESS_PRIVATE);
        default:
            return OUString();
    }
}

OUString FormatSymbolSignature(const IdeSymbolInfo& rSymbol)
{
    rtl::OUStringBuffer sDescription;

    sDescription.append(u"━━━ " + GetSymbolTypeDescription(rSymbol.eKind) + u" ━━━\n\n📌 ");

    switch (rSymbol.eKind)
    {
        case IdeSymbolKind::UNO_METHOD:
        case IdeSymbolKind::SUB:
        case IdeSymbolKind::FUNCTION:
            FormatMethodSignature(sDescription, rSymbol);
            break;
        case IdeSymbolKind::UNO_PROPERTY:
        case IdeSymbolKind::UNO_FIELD:
            FormatPropertySignature(sDescription, rSymbol);
            break;
        default:
            sDescription.append(rSymbol.sName);
            break;
    }
    sDescription.append(u"\n\n");

    if (rSymbol.eAccessModifier != IdeAccessModifier::NOT_APPLICABLE)
    {
        sDescription.append(u"🔒 " + IDEResId(RID_STR_OB_ACCESS)
                            + AccessModifierToString(rSymbol.eAccessModifier) + u"\n");
    }

    if (!rSymbol.sReturnTypeName.isEmpty() && rSymbol.sReturnTypeName != "Void")
    {
        sDescription.append(u"↩️ " + IDEResId(RID_STR_OB_RETURNS) + rSymbol.sReturnTypeName
                            + u"\n");
    }

    if (!rSymbol.sTypeName.isEmpty() && rSymbol.sReturnTypeName.isEmpty())
    {
        sDescription.append(u"📦 " + IDEResId(RID_STR_OB_TYPE) + rSymbol.sTypeName + u"\n");
    }

    if (!rSymbol.aParameters.empty())
    {
        sDescription.append(u"\n📋 " + IDEResId(RID_STR_OB_PARAMETERS));
        for (const auto& param : rSymbol.aParameters)
        {
            sDescription.append(u"  • " + param.sName + u" : " + param.sTypeName);
            rtl::OUStringBuffer sModifiers;
            if (param.bIsOptional)
            {
                sModifiers.append(u"Optional");
            }
            if (param.bIsOut && !param.bIsIn)
            {
                sModifiers.append(sModifiers.getLength() ? u", [out]" : u"[out]");
            }
            else if (param.bIsOut && param.bIsIn)
            {
                sModifiers.append(sModifiers.getLength() ? u", [inout]" : u"[inout]");
            }
            if (!param.bIsByVal)
            {
                sModifiers.append(sModifiers.getLength() ? u", ByRef" : u"ByRef");
            }
            if (sModifiers.getLength() > 0)
            {
                sDescription.append(u" [" + sModifiers.makeStringAndClear() + u"]");
            }
            if (param.osDefaultValueExpression.has_value())
            {
                sDescription.append(u" = " + param.osDefaultValueExpression.value());
            }
            sDescription.append(u"\n");
        }
    }

    sDescription.append(
        u"\n━━━ " + OUString::Concat(o3tl::trim(IDEResId(RID_STR_OB_LOCATION_HEADER))) + u" ━━━\n");

    if (!rSymbol.sParentName.isEmpty())
    {
        sDescription.append(u"📂 " + IDEResId(RID_STR_OB_MEMBER_OF) + rSymbol.sParentName + u"\n");
    }
    if (!rSymbol.sOriginLibrary.isEmpty())
    {
        sDescription.append(u"📚 " + IDEResId(RID_STR_OB_LIBRARY) + rSymbol.sOriginLibrary + u"\n");
    }
    if (!rSymbol.sOriginLocation.isEmpty())
    {
        sDescription.append(u"📄 " + IDEResId(RID_STR_OB_DOCUMENT) + rSymbol.sOriginLocation
                            + u"\n");
    }
    if (rSymbol.nSourceLine > 0)
    {
        sDescription.append(u"📍 " + IDEResId(RID_STR_OB_LINE)
                            + OUString::number(rSymbol.nSourceLine) + u"\n");
    }
    if (!rSymbol.sQualifiedName.isEmpty() && rSymbol.sQualifiedName != rSymbol.sName)
    {
        sDescription.append(u"\n━━━ "
                            + OUString::Concat(o3tl::trim(IDEResId(RID_STR_OB_FULLNAME_HEADER)))
                            + u" ━━━\n");
        sDescription.append(rSymbol.sQualifiedName + u"\n");
    }

    return sDescription.makeStringAndClear();
}

OUString FormatContainerSignature(const IdeSymbolInfo& rSymbol,
                                  IdeDataProviderInterface* pDataProvider)
{
    if (!pDataProvider)
    {
        return OUString();
    }

    rtl::OUStringBuffer sInfo;
    sInfo.append(u"━━━ " + GetSymbolTypeDescription(rSymbol.eKind) + u" ━━━\n\n📌 " + rSymbol.sName
                 + u"\n\n");

    if (ShouldShowMembers(rSymbol))
    {
        GroupedSymbolInfoList aMembers = pDataProvider->GetMembers(rSymbol);
        size_t nTotalMembers = 0;
        for (const auto& pair : aMembers)
            nTotalMembers += pair.second.size();

        sInfo.append(IDEResId(RID_STR_OB_CONTENTS_HEADER) + IDEResId(RID_STR_OB_TOTAL_MEMBERS)
                     + OUString::number(static_cast<sal_Int64>(nTotalMembers)) + u"\n\n");

        for (const auto& pair : aMembers)
        {
            sInfo.append(u"  • " + GetGroupNameForKind(pair.first) + u": "
                         + OUString::number(static_cast<sal_Int64>(pair.second.size())) + u"\n");
        }
    }
    else if (IsExpandable(rSymbol))
    {
        auto aChildren = pDataProvider->GetChildNodes(rSymbol);
        sal_Int64 nChildren = static_cast<sal_Int64>(aChildren.size());
        sInfo.append(
            IDEResId(RID_STR_OB_CONTENTS_HEADER)
            + IDEResId(RID_STR_OB_CONTAINS_ITEMS).replaceFirst(u"%1", OUString::number(nChildren)));
    }

    if (!rSymbol.sQualifiedName.isEmpty() && rSymbol.sQualifiedName != rSymbol.sName)
    {
        sInfo.append(IDEResId(RID_STR_OB_FULLNAME_HEADER) + rSymbol.sQualifiedName + u"\n");
    }

    if (!rSymbol.sOriginLibrary.isEmpty() || !rSymbol.sOriginLocation.isEmpty())
    {
        sInfo.append(IDEResId(RID_STR_OB_LOCATION_HEADER));
        if (!rSymbol.sOriginLibrary.isEmpty())
        {
            sInfo.append(IDEResId(RID_STR_OB_LIBRARY) + rSymbol.sOriginLibrary + u"\n");
        }
        if (!rSymbol.sOriginLocation.isEmpty())
        {
            sInfo.append(IDEResId(RID_STR_OB_DOCUMENT) + rSymbol.sOriginLocation + u"\n");
        }
    }

    return sInfo.makeStringAndClear();
}

// Helper to add a symbol entry to a tree view and its corresponding data stores.
void AddEntry(weld::TreeView& rTargetTree, std::vector<std::shared_ptr<IdeSymbolInfo>>& rStore,
              std::map<OUString, std::shared_ptr<IdeSymbolInfo>>& rIndex,
              const weld::TreeIter* pParent, const std::shared_ptr<IdeSymbolInfo>& pSymbol,
              bool bChildrenOnDemand, weld::TreeIter* pRetIter = nullptr)
{
    if (!pSymbol)
        return;

    if (pSymbol->sName.isEmpty())
    {
        SAL_WARN("basctl", "AddEntry - Symbol with empty name. ID: " << pSymbol->sIdentifier);
        return;
    }

    OUString sId = pSymbol->sIdentifier;
    if (pSymbol->eKind == IdeSymbolKind::PLACEHOLDER)
    {
        sId = u"placeholder_for:"_ustr + (pParent ? rTargetTree.get_id(*pParent) : u"root"_ustr);
        pSymbol->sIdentifier = sId;
    }

    if (sId.isEmpty())
    {
        SAL_WARN("basctl", "AddEntry - Symbol with empty ID. Name: " << pSymbol->sName);
        return;
    }

    rStore.push_back(pSymbol);
    rIndex[sId] = pSymbol;

    std::unique_ptr<weld::TreeIter> xLocalIter;
    if (!pRetIter)
    {
        xLocalIter = rTargetTree.make_iterator();
        pRetIter = xLocalIter.get();
    }

    rTargetTree.insert(pParent, -1, &pSymbol->sName, &sId, nullptr, nullptr, bChildrenOnDemand,
                       pRetIter);
    OUString sIconName = GetIconForSymbol(pSymbol->eKind);
    rTargetTree.set_image(*pRetIter, sIconName, -1);
}

OUString BuildDoxygenUrl(const IdeSymbolInfo& rSymbol)
{
    if (rSymbol.sQualifiedName.isEmpty())
        return OUString();

    if (rSymbol.sQualifiedName.startsWith("ooo.vba."))
    {
        SAL_INFO("basctl", "BuildDoxygenUrl: Skipping VBA type '" << rSymbol.sQualifiedName << "'");
        return OUString();
    }
    // Doxygen mangles names by replacing '.' with "_1_1"
    OUString sMangledName;
    OUString sTypePrefix;
    OUString sAnchor;

    switch (rSymbol.eKind)
    {
        // Case 1: Types that have their OWN dedicated .html page
        case IdeSymbolKind::UNO_STRUCT:
            sTypePrefix = u"struct"_ustr;
            sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
            break;
        case IdeSymbolKind::UNO_INTERFACE:
            sTypePrefix = u"interface"_ustr;
            sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
            break;
        case IdeSymbolKind::UNO_SERVICE:
            sTypePrefix = u"service"_ustr;
            sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
            break;
        case IdeSymbolKind::UNO_EXCEPTION:
            sTypePrefix = u"exception"_ustr;
            sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
            break;
        case IdeSymbolKind::UNO_NAMESPACE:
            sTypePrefix = u"namespace"_ustr;
            sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
            break;

        // Case 2: Types that are documented as ANCHORS on their parent namespace page
        case IdeSymbolKind::UNO_ENUM:
        case IdeSymbolKind::UNO_CONSTANTS:
        case IdeSymbolKind::UNO_TYPEDEF:
        {
            if (rSymbol.sParentName.isEmpty())
            {
                return OUString();
            }
            sTypePrefix = u"namespace"_ustr;
            sMangledName = rSymbol.sParentName.replaceAll(".", "_1_1");
            sAnchor = u"#"_ustr + rSymbol.sName;
            break;
        }
        // This symbol kind does not have a Doxygen page
        default:
            return OUString();
    }

    if (sMangledName.isEmpty() || sTypePrefix.isEmpty())
    {
        return OUString();
    }

    return u"https://api.libreoffice.org/docs/idl/ref/"_ustr + sTypePrefix + sMangledName
           + u".html"_ustr + sAnchor;
}

void ShowDocsError(vcl::Window* pParent, const OUString& sPrimaryText,
                   const OUString& sSecondaryText = OUString())
{
    if (!pParent)
        return;

    std::unique_ptr<weld::MessageDialog> xBox(Application::CreateMessageDialog(
        pParent->GetFrameWeld(), VclMessageType::Warning, VclButtonsType::Ok, sPrimaryText));
    xBox->set_title(IDEResId(RID_STR_OB_DOCS_ERROR_TITLE));

    if (!sSecondaryText.isEmpty())
    {
        xBox->set_secondary_text(sSecondaryText);
    }
    xBox->run();
}

void OpenDoxygenDocumentation(vcl::Window* pParent, const IdeSymbolInfo& rSymbol)
{
    OUString sUrl = BuildDoxygenUrl(rSymbol);
    SAL_INFO("basctl", "OpenDoxygenDocumentation: Built URL='" << sUrl << "' for symbol '"
                                                               << rSymbol.sName << "'");
    if (sUrl.isEmpty())
    {
        ShowDocsError(pParent, IDEResId(RID_STR_OB_NO_DOCUMENTATION));
        return;
    }

    try
    {
        css::uno::Reference<css::uno::XComponentContext> xContext
            = comphelper::getProcessComponentContext();
        css::uno::Reference<css::lang::XMultiComponentFactory> xServiceManager
            = xContext->getServiceManager();

        css::uno::Reference<css::system::XSystemShellExecute> xSystemShell(
            xServiceManager->createInstanceWithContext(
                u"com.sun.star.system.SystemShellExecute"_ustr, xContext),
            css::uno::UNO_QUERY_THROW);

        xSystemShell->execute(sUrl, OUString(), css::system::SystemShellExecuteFlags::URIS_ONLY);
    }
    catch (const css::uno::Exception& e)
    {
        SAL_WARN("basctl", "Failed to open Doxygen documentation: " << e.Message);

        OUString sSecondaryMsg = u"URL: "_ustr + sUrl + u"\nError: "_ustr + e.Message;
        ShowDocsError(pParent, IDEResId(RID_STR_OB_BROWSER_LAUNCH_FAILED), sSecondaryMsg);
    }
}

} // anonymous namespace

ObjectBrowser::ObjectBrowser(Shell& rShell, vcl::Window* pParent)
    : basctl::DockingWindow(pParent, u"modules/BasicIDE/ui/objectbrowser.ui"_ustr,
                            u"ObjectBrowser"_ustr)
    , m_pShell(&rShell)
    , m_pDataProvider(std::make_unique<IdeDataProvider>())
    , m_bDisposed(false)
    , m_eInitState(ObjectBrowserInitState::NotInitialized)
    , m_bDataMayBeStale(true)
    , m_pDocNotifier(std::make_unique<DocumentEventNotifier>(*this))
{
    SetText(IDEResId(RID_STR_OBJECT_BROWSER));
    SetBackground(GetSettings().GetStyleSettings().GetWindowColor());
    EnableInput(true, true);
}

ObjectBrowser::~ObjectBrowser() { disposeOnce(); }

void ObjectBrowser::Initialize()
{
    if (m_eInitState != ObjectBrowserInitState::NotInitialized)
        return;

    m_eInitState = ObjectBrowserInitState::Initializing;

    // Handles to all our widgets
    m_xScopeSelector = m_xBuilder->weld_combo_box(u"ScopeSelector"_ustr);
    m_pFilterBox = m_xBuilder->weld_entry(u"FilterBox"_ustr);
    m_xLeftTreeView = m_xBuilder->weld_tree_view(u"LeftTreeView"_ustr);
    m_xRightMembersView = m_xBuilder->weld_tree_view(u"RightMembersView"_ustr);
    m_xDetailPane = m_xBuilder->weld_text_view(u"DetailPane"_ustr);
    m_xStatusLabel = m_xBuilder->weld_label(u"StatusLabel"_ustr);
    m_xRightPaneHeaderLabel = m_xBuilder->weld_label(u"RightPaneHeaderLabel"_ustr);
    m_xBackButton = m_xBuilder->weld_button(u"BackButton"_ustr);
    m_xForwardButton = m_xBuilder->weld_button(u"ForwardButton"_ustr);
    m_xClearSearchButton = m_xBuilder->weld_button(u"ClearSearchButton"_ustr);

    m_pSearchHandler = std::make_unique<ObjectBrowserSearch>(*this);
    m_pSearchHandler->Initialize();

    if (m_xScopeSelector)
    {
        m_xScopeSelector->append(u"ALL_LIBRARIES"_ustr, IDEResId(RID_STR_OB_SCOPE_ALL));
        m_xScopeSelector->append(u"CURRENT_DOCUMENT"_ustr, IDEResId(RID_STR_OB_SCOPE_CURRENT));
        m_xScopeSelector->set_active(0);
        m_xScopeSelector->connect_changed(LINK(this, ObjectBrowser, OnScopeChanged));
    }

    if (m_xLeftTreeView)
    {
        m_xLeftTreeView->connect_selection_changed(LINK(this, ObjectBrowser, OnLeftTreeSelect));
        m_xLeftTreeView->connect_expanding(LINK(this, ObjectBrowser, OnNodeExpand));
    }
    if (m_xRightMembersView)
    {
        m_xRightMembersView->connect_selection_changed(
            LINK(this, ObjectBrowser, OnRightTreeSelect));
        m_xRightMembersView->connect_expanding(LINK(this, ObjectBrowser, OnRightNodeExpand));
        m_xRightMembersView->connect_row_activated(
            LINK(this, ObjectBrowser, OnRightTreeDoubleClick));
    }

    if (m_xDetailPane)
    {
        vcl::Font aFont = m_xDetailPane->get_font();
        aFont.SetFamilyName(u"Monospace"_ustr);
        aFont.SetPitch(PITCH_FIXED);
        m_xDetailPane->set_font(aFont);
    }

    if (m_xBackButton)
        m_xBackButton->set_sensitive(false);
    if (m_xForwardButton)
        m_xForwardButton->set_sensitive(false);

    if (GetParent() && GetParent()->GetSystemWindow())
    {
        GetParent()->GetSystemWindow()->GetTaskPaneList()->AddWindow(this);
    }

    // Start listening for application-wide events like document activation
    StartListening(*SfxGetpApp());
    SfxViewFrame* pViewFrame = SfxViewFrame::Current();
    if (pViewFrame)
    {
        if (SfxObjectShell* pObjShell = pViewFrame->GetObjectShell())
        {
            m_sLastActiveDocumentIdentifier = pObjShell->GetTitle();
            SAL_INFO("basctl", "ObjectBrowser::Initialize: Active document -> '"
                                   << m_sLastActiveDocumentIdentifier << "'");
        }
    }

    m_eInitState = ObjectBrowserInitState::Initialized;
}

void ObjectBrowser::dispose()
{
    if (m_bDisposed)
    {
        return;
    }
    m_bDisposed = true;

    m_eInitState = ObjectBrowserInitState::Disposed;

    if (GetParent() && GetParent()->GetSystemWindow())
    {
        if (auto* pTaskPaneList = GetParent()->GetSystemWindow()->GetTaskPaneList())
            pTaskPaneList->RemoveWindow(this);
    }

    // Stop listening to SFX events
    EndListening(*SfxGetpApp());

    // Disconnect all signals
    if (m_xScopeSelector)
        m_xScopeSelector->connect_changed(Link<weld::ComboBox&, void>());
    if (m_xLeftTreeView)
    {
        m_xLeftTreeView->connect_selection_changed(Link<weld::TreeView&, void>());
        m_xLeftTreeView->connect_expanding(Link<const weld::TreeIter&, bool>());
    }
    if (m_xRightMembersView)
    {
        m_xRightMembersView->connect_selection_changed(Link<weld::TreeView&, void>());
    }

    m_pDocNotifier->dispose();
    m_pDocNotifier.reset();
    m_pSearchHandler.reset();
    m_pDataProvider.reset();

    // Destroy widgets
    m_xScopeSelector.reset();
    m_pFilterBox.reset();
    m_xLeftTreeView.reset();
    m_xRightMembersView.reset();
    m_xDetailPane.reset();
    m_xStatusLabel.reset();
    m_xRightPaneHeaderLabel.reset();
    m_xBackButton.reset();
    m_xForwardButton.reset();
    m_xClearSearchButton.reset();

    DockingWindow::dispose();
}

bool ObjectBrowser::Close()
{
    Show(false);
    m_pShell->GetViewFrame().GetBindings().Invalidate(SID_BASICIDE_OBJECT_BROWSER);
    return false;
}

void ObjectBrowser::Show(bool bVisible)
{
    DockingWindow::Show(bVisible);
    if (!bVisible)
    {
        return;
    }

    if (m_eInitState == ObjectBrowserInitState::NotInitialized)
    {
        Initialize();
    }

    if (m_pDataProvider)
    {
        if (!m_pDataProvider->IsInitialized())
        {
            ShowLoadingState();
            IdeTimer aTotalInitTimer("ObjectBrowser::FullInitialization");
            weld::WaitObject aWait(GetFrameWeld());
            m_pDataProvider->Initialize();
            m_bDataMayBeStale = true;

            RefreshUI();
            m_bDataMayBeStale = false;

            if (!m_bFirstLoadComplete)
            {
                m_bFirstLoadComplete = true;
                double fElapsedSeconds = aTotalInitTimer.getElapsedTimeMs() / 1000.0;
                OUString sStatus = IDEResId(RID_STR_OB_READY_LOADED)
                                       .replaceFirst(u"%1", OUString::number(fElapsedSeconds));
                m_xStatusLabel->set_label(sStatus);
            }
        }
        else if (m_bDataMayBeStale)
        {
            RefreshUI();
            m_bDataMayBeStale = false;
        }
    }
}

void ObjectBrowser::RefreshUI(bool /*bForceKeepUno*/)
{
    IdeTimer aTimer(u"ObjectBrowser::RefreshUI"_ustr);

    if (m_bPerformingAction)
    {
        SAL_INFO("basctl", "ObjectBrowser::RefreshUI: Blocked because an action is in progress.");
        return;
    }

    if (!m_pDataProvider || !m_pDataProvider->IsInitialized())
    {
        ShowLoadingState();
        return;
    }

    m_xLeftTreeView->freeze();
    m_xRightMembersView->freeze();
    ClearLeftTreeView();
    ClearRightTreeView();

    static_cast<IdeDataProvider*>(m_pDataProvider.get())->RefreshDocumentNodes();

    if (m_xRightPaneHeaderLabel)
    {
        m_xRightPaneHeaderLabel->set_label(IDEResId(RID_STR_OB_GROUP_MEMBERS));
    }

    UpdateDetailsPane(nullptr, false);

    // Get the filtered list of nodes based on the current scope
    SymbolInfoList aTopLevelNodes = m_pDataProvider->GetTopLevelNodes();
    for (const auto& pSymbol : aTopLevelNodes)
    {
        if (pSymbol)
        {
            AddEntry(*m_xLeftTreeView, m_aLeftTreeSymbolStore, m_aLeftTreeSymbolIndex, nullptr,
                     pSymbol, IsExpandable(*pSymbol));
        }
    }

    m_xLeftTreeView->thaw();
    m_xRightMembersView->thaw();

    if (!m_bFirstLoadComplete && m_xStatusLabel)
        m_xStatusLabel->set_label(IDEResId(RID_STR_OB_READY));
}

void ObjectBrowser::Notify(SfxBroadcaster& /*rBC*/, const SfxHint& rHint)
{
    if (m_bDisposed)
    {
        return;
    }

    const SfxEventHint* pEventHint = dynamic_cast<const SfxEventHint*>(&rHint);
    if (!pEventHint || pEventHint->GetEventId() != SfxEventHintId::ActivateDoc)
    {
        return;
    }

    rtl::Reference<SfxObjectShell> pObjShell = pEventHint->GetObjShell();
    if (!pObjShell)
    {
        return;
    }

    OUString sServiceName = pObjShell->GetFactory().GetDocumentServiceName();

    // Filter out BasicIDE and HTML/Web documents (Doxygen docs)
    if (sServiceName.endsWith(u"BasicIDE") || sServiceName.endsWith(u"WebDocument"))
    {
        SAL_INFO("basctl", "ObjectBrowser::Notify: Ignoring BasicIDE & WebDocument activation");
        return;
    }

    OUString sNewDocTitle = pObjShell->GetTitle();
    if (sNewDocTitle != m_sLastActiveDocumentIdentifier)
    {
        m_sLastActiveDocumentIdentifier = sNewDocTitle;
        SAL_INFO("basctl", "ObjectBrowser::Notify: Document activated -> '" << sNewDocTitle << "'");

        // If the user is in "Current Document" mode, a focus change
        // should trigger an immediate refresh to reflect the new context.
        if (IsVisible() && m_xScopeSelector
            && m_xScopeSelector->get_active_id() == "CURRENT_DOCUMENT")
        {
            SAL_INFO("basctl",
                     "ObjectBrowser::Notify: In CURRENT_DOCUMENT scope, updating scope to '"
                         << sNewDocTitle << "' and refreshing UI.");

            // Update scope to newly activated document
            m_pDataProvider->SetScope(IdeBrowserScope::CURRENT_DOCUMENT, sNewDocTitle);
            ScheduleRefresh();
        }
    }
}

void ObjectBrowser::ShowLoadingState()
{
    if (m_xLeftTreeView)
    {
        m_xLeftTreeView->freeze();
        ClearLeftTreeView();
        auto pLoadingNode = std::make_shared<IdeSymbolInfo>(u"[Initializing...]",
                                                            IdeSymbolKind::PLACEHOLDER, u"");
        AddEntry(*m_xLeftTreeView, m_aLeftTreeSymbolStore, m_aLeftTreeSymbolIndex, nullptr,
                 pLoadingNode, false);
        m_xLeftTreeView->thaw();
    }
    if (m_xStatusLabel)
    {
        m_xStatusLabel->set_label(u"Initializing Object Browser..."_ustr);
    }
}

void ObjectBrowser::ClearLeftTreeView()
{
    if (m_xLeftTreeView)
        m_xLeftTreeView->clear();
    m_aLeftTreeSymbolStore.clear();
    m_aLeftTreeSymbolIndex.clear();
}

void ObjectBrowser::ClearRightTreeView()
{
    if (m_xRightMembersView)
        m_xRightMembersView->clear();
    m_aRightTreeSymbolStore.clear();
    m_aRightTreeSymbolIndex.clear();
}

void ObjectBrowser::ScheduleRefresh()
{
    if (m_bPerformingAction)
    {
        SAL_INFO("basctl",
                 "ScheduleRefresh: Blocked refresh request because an action is in progress.");
        return;
    }

    m_bDataMayBeStale = true;

    if (IsVisible())
    {
        RefreshUI();
    }
}

void ObjectBrowser::UpdateDetailsPane(const IdeSymbolInfo* pSymbol, bool bIsContainer)
{
    if (!m_xDetailPane)
    {
        return;
    }

    if (pSymbol && pSymbol->eKind != IdeSymbolKind::PLACEHOLDER && pSymbol->bSelectable)
    {
        if (bIsContainer)
        {
            m_xDetailPane->set_text(FormatContainerSignature(*pSymbol, m_pDataProvider.get()));
        }
        else
        {
            m_xDetailPane->set_text(FormatSymbolSignature(*pSymbol));
        }
    }
    else
    {
        m_xDetailPane->set_text(u""_ustr);
    }
}

void ObjectBrowser::UpdateStatusBar(const IdeSymbolInfo* pLeftSymbol,
                                    const IdeSymbolInfo* pRightSymbol)
{
    if (!m_xStatusLabel)
    {
        return;
    }
    OUString sStatusText;

    if (pRightSymbol)
    {
        if (pRightSymbol->eKind == IdeSymbolKind::PLACEHOLDER)
        {
            sStatusText = pRightSymbol->sName;
        }
        else
        {
            sStatusText = GetSymbolTypeDescription(pRightSymbol->eKind);
        }
    }
    else if (pLeftSymbol)
    {
        if (ShouldShowMembers(*pLeftSymbol))
        {
            GroupedSymbolInfoList aMembers = m_pDataProvider->GetMembers(*pLeftSymbol);
            size_t nTotalMembers = 0;
            for (const auto& pair : aMembers)
                nTotalMembers += pair.second.size();
            sStatusText
                = IDEResId(RID_STR_OB_MEMBERS_COUNT)
                      .replaceFirst(u"%1", OUString::number(static_cast<sal_Int64>(nTotalMembers)));
        }
        else if (IsExpandable(*pLeftSymbol))
        {
            auto aChildren = m_pDataProvider->GetChildNodes(*pLeftSymbol);
            sStatusText
                = IDEResId(RID_STR_OB_ITEMS_COUNT)
                      .replaceFirst(u"%1",
                                    OUString::number(static_cast<sal_Int64>(aChildren.size())));
        }
        else
        {
            sStatusText = GetSymbolTypeDescription(pLeftSymbol->eKind);
        }
    }
    else if (m_bFirstLoadComplete)
    {
        sStatusText = m_xStatusLabel->get_label();
    }
    else
    {
        sStatusText = IDEResId(RID_STR_OB_READY);
    }
    m_xStatusLabel->set_label(sStatusText);
}

void ObjectBrowser::NavigateToMacroSource(const IdeSymbolInfo& rSymbol)
{
    SAL_INFO("basctl", "NavigateToMacroSource: Entry - Library='"
                           << rSymbol.sOriginLibrary << "', Module='" << rSymbol.sOriginModule
                           << "', Method='" << rSymbol.sName << "'");

    if (!m_pShell || rSymbol.sOriginLibrary.isEmpty() || rSymbol.sOriginModule.isEmpty())
    {
        SAL_WARN("basctl", "NavigateToMacroSource: Invalid parameters.");
        return;
    }

    ScriptDocument aDoc
        = rSymbol.sOriginLocation.isEmpty()
              ? ScriptDocument::getApplicationScriptDocument()
              : ScriptDocument::getDocumentWithURLOrCaption(rSymbol.sOriginLocation);

    if (aDoc.isAlive())
    {
        SAL_INFO("basctl",
                 "NavigateToMacroSource: Document is alive, dispatching SID_BASICIDE_SHOWSBX.");

        // Pass the method name so it navigates to the exact line
        SbxItem aSbxItem(SID_BASICIDE_ARG_SBX, std::move(aDoc), rSymbol.sOriginLibrary,
                         rSymbol.sOriginModule, rSymbol.sName, basctl::SBX_TYPE_METHOD);

        SfxViewFrame& rViewFrame = m_pShell->GetViewFrame();
        if (SfxDispatcher* pDispatcher = rViewFrame.GetDispatcher())
        {
            std::ignore = pDispatcher->ExecuteList(SID_BASICIDE_SHOWSBX, SfxCallMode::SYNCHRON,
                                                   { &aSbxItem });
            SAL_INFO("basctl", "NavigateToMacroSource: Dispatched successfully.");
        }
        else
        {
            SAL_WARN("basctl", "NavigateToMacroSource: Could not get dispatcher.");
        }
    }
    else
    {
        SAL_WARN("basctl", "NavigateToMacroSource: ScriptDocument is not alive.");
    }
}

void ObjectBrowser::PopulateMembersPane(const IdeSymbolInfo& rSymbol)
{
    if (!m_xRightMembersView)
    {
        return;
    }

    m_xRightMembersView->freeze();
    ClearRightTreeView();

    if (m_xRightPaneHeaderLabel)
    {
        m_xRightPaneHeaderLabel->set_label(u"Members of: "_ustr + rSymbol.sName);
    }
    GroupedSymbolInfoList aGroupedMembers = m_pDataProvider->GetMembers(rSymbol);
    std::vector<std::unique_ptr<weld::TreeIter>> aGroupItersToExpand;

    for (const auto& rPair : aGroupedMembers)
    {
        if (rPair.second.empty())
        {
            continue;
        }
        OUString sGroupName = GetGroupNameForKind(rPair.first);
        auto pGroupNode
            = std::make_shared<IdeSymbolInfo>(sGroupName, IdeSymbolKind::PLACEHOLDER, u"");
        pGroupNode->bSelectable = false;
        auto xGroupIter = m_xRightMembersView->make_iterator();
        AddEntry(*m_xRightMembersView, m_aRightTreeSymbolStore, m_aRightTreeSymbolIndex, nullptr,
                 pGroupNode, true, xGroupIter.get());

        for (const auto& pMemberInfo : rPair.second)
        {
            bool bHasNestedMembers = !pMemberInfo->mapMembers.empty();

            AddEntry(*m_xRightMembersView, m_aRightTreeSymbolStore, m_aRightTreeSymbolIndex,
                     xGroupIter.get(), pMemberInfo, bHasNestedMembers);
        }
        aGroupItersToExpand.push_back(std::move(xGroupIter));
    }

    m_xRightMembersView->thaw();

    for (const auto& xGroupIter : aGroupItersToExpand)
    {
        m_xRightMembersView->expand_row(*xGroupIter);
    }
}

// Document Event Handlers
void ObjectBrowser::onDocumentCreated(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentOpened(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentSave(const ScriptDocument&) { /* STUB */}
void ObjectBrowser::onDocumentSaveDone(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentSaveAs(const ScriptDocument&) { /* STUB */}
void ObjectBrowser::onDocumentSaveAsDone(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentClosed(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentTitleChanged(const ScriptDocument&) { ScheduleRefresh(); }
void ObjectBrowser::onDocumentModeChanged(const ScriptDocument&) { /* STUB */}

IMPL_LINK(ObjectBrowser, OnLeftTreeSelect, weld::TreeView&, rTree, void)
{
    if (m_bDisposed)
    {
        return;
    }

    auto xSelectedIter = rTree.make_iterator();
    if (!rTree.get_selected(xSelectedIter.get()))
    {
        UpdateStatusBar(nullptr, nullptr);
        UpdateDetailsPane(nullptr, false);
        return;
    }

    auto pSymbol = GetSymbolForIter(*xSelectedIter, rTree, m_aLeftTreeSymbolIndex);
    if (!pSymbol)
    {
        return;
    }

    // A selection in the left pane is always a container
    UpdateDetailsPane(pSymbol.get(), true);
    UpdateStatusBar(pSymbol.get(), nullptr);

    ClearRightTreeView();

    if (ShouldShowMembers(*pSymbol))
    {
        PopulateMembersPane(*pSymbol);
    }
}

IMPL_LINK(ObjectBrowser, OnRightTreeSelect, weld::TreeView&, rTree, void)
{
    if (m_bDisposed)
    {
        return;
    }

    auto xLeftIter = m_xLeftTreeView->make_iterator();
    std::shared_ptr<const IdeSymbolInfo> pLeftSymbol
        = m_xLeftTreeView->get_selected(xLeftIter.get())
              ? GetSymbolForIter(*xLeftIter, *m_xLeftTreeView, m_aLeftTreeSymbolIndex)
              : nullptr;

    auto xRightIter = rTree.make_iterator();
    if (!rTree.get_selected(xRightIter.get()))
    {
        // Revert to showing container info if right-pane selection is cleared
        UpdateStatusBar(pLeftSymbol.get(), nullptr);
        UpdateDetailsPane(pLeftSymbol.get(), true);
        return;
    }

    auto pRightSymbol = GetSymbolForIter(*xRightIter, rTree, m_aRightTreeSymbolIndex);
    if (!pRightSymbol)
    {
        return;
    }

    // We create a temporary copy to correctly populate the parent name for display
    IdeSymbolInfo tempSymbol = *pRightSymbol;
    if (pLeftSymbol)
    {
        tempSymbol.sParentName = pLeftSymbol->sName;
    }

    UpdateDetailsPane(&tempSymbol, false);
    UpdateStatusBar(pLeftSymbol.get(), pRightSymbol.get());
}

IMPL_LINK(ObjectBrowser, OnRightNodeExpand, const weld::TreeIter&, rParentIter, bool)
{
    if (m_bDisposed)
    {
        return false;
    }

    if (m_xRightMembersView->iter_has_child(rParentIter))
    {
        return true;
    }

    auto pParentSymbol
        = GetSymbolForIter(rParentIter, *m_xRightMembersView, m_aRightTreeSymbolIndex);
    if (!pParentSymbol)
    {
        return false;
    }

    for (const auto& pair : pParentSymbol->mapMembers)
    {
        const auto& pChildSymbol = pair.second;
        if (pChildSymbol)
        {
            // Check if the child is also expandable
            bool bChildHasChildren = !pChildSymbol->mapMembers.empty();
            AddEntry(*m_xRightMembersView, m_aRightTreeSymbolStore, m_aRightTreeSymbolIndex,
                     &rParentIter, pChildSymbol, bChildHasChildren);
        }
    }

    return true;
}

IMPL_LINK(ObjectBrowser, OnRightTreeDoubleClick, weld::TreeView&, rTree, bool)
{
    SAL_INFO("basctl", "OnRightTreeDoubleClick: Handler entered.");

    if (m_bDisposed)
    {
        SAL_WARN("basctl", "OnRightTreeDoubleClick: Browser is disposed, aborting.");
        return false;
    }

    auto xSelectedIter = rTree.make_iterator();
    if (!rTree.get_selected(xSelectedIter.get()))
    {
        SAL_INFO("basctl", "OnRightTreeDoubleClick: No item selected.");
        return false;
    }

    auto pSymbol = GetSymbolForIter(*xSelectedIter, rTree, m_aRightTreeSymbolIndex);
    if (!pSymbol || pSymbol->eKind == IdeSymbolKind::PLACEHOLDER)
    {
        SAL_INFO("basctl",
                 "OnRightTreeDoubleClick: Selected item is a placeholder or has no symbol.");
        return false;
    }

    m_bPerformingAction = true;
    SAL_INFO("basctl", "OnRightTreeDoubleClick: Processing symbol '"
                           << pSymbol->sName << "' of kind " << static_cast<int>(pSymbol->eKind));

    // Symbol is a BASIC Macro
    if (pSymbol->eKind == IdeSymbolKind::SUB || pSymbol->eKind == IdeSymbolKind::FUNCTION)
    {
        SAL_INFO("basctl", "OnRightTreeDoubleClick: Action is NavigateToMacroSource.");
        NavigateToMacroSource(*pSymbol);
        m_bPerformingAction = false;
        return true;
    }

    // The symbol itself is documentable
    OUString sSymbolUrl = BuildDoxygenUrl(*pSymbol);
    if (!sSymbolUrl.isEmpty())
    {
        SAL_INFO("basctl",
                 "OnRightTreeDoubleClick: Symbol is a documentable container. Opening its docs.");
        OpenDoxygenDocumentation(this, *pSymbol);
        m_bPerformingAction = false;
        return true;
    }

    // Find documentable parent in RIGHT tree first
    auto xParentIter = rTree.make_iterator(xSelectedIter.get());
    while (rTree.iter_parent(*xParentIter))
    {
        auto pParentSymbol = GetSymbolForIter(*xParentIter, rTree, m_aRightTreeSymbolIndex);
        if (pParentSymbol && pParentSymbol->eKind != IdeSymbolKind::PLACEHOLDER)
        {
            // If this parent has a qualified name it's documentable
            if (!pParentSymbol->sQualifiedName.isEmpty())
            {
                SAL_INFO("basctl",
                         "OnRightTreeDoubleClick: Action is OpenDoxygenDocumentation for parent '"
                             << pParentSymbol->sName << "' (from right pane).");
                OpenDoxygenDocumentation(this, *pParentSymbol);
                m_bPerformingAction = false;
                return true;
            }
            // Otherwise, keep walking up the tree
        }
    }

    // Find documentable parent in LEFT tree
    auto xLeftTreeParentIter = m_xLeftTreeView->make_iterator();
    if (m_xLeftTreeView->get_selected(xLeftTreeParentIter.get()))
    {
        auto pParentSymbol
            = GetSymbolForIter(*xLeftTreeParentIter, *m_xLeftTreeView, m_aLeftTreeSymbolIndex);
        if (pParentSymbol)
        {
            // Try to build URL for parent
            OUString sParentUrl = BuildDoxygenUrl(*pParentSymbol);

            if (sParentUrl.isEmpty())
            {
                auto xAncestorIter = m_xLeftTreeView->make_iterator(xLeftTreeParentIter.get());
                while (m_xLeftTreeView->iter_parent(*xAncestorIter))
                {
                    auto pAncestorSymbol = GetSymbolForIter(*xAncestorIter, *m_xLeftTreeView,
                                                            m_aLeftTreeSymbolIndex);
                    if (pAncestorSymbol)
                    {
                        if (pAncestorSymbol->sQualifiedName.startsWith("ooo.vba"))
                        {
                            SAL_INFO("basctl", "OnRightTreeDoubleClick: Ancestor is VBA namespace, "
                                               "skipping documentation.");
                            break;
                        }
                        OUString sAncestorUrl = BuildDoxygenUrl(*pAncestorSymbol);
                        if (!sAncestorUrl.isEmpty())
                        {
                            SAL_INFO(
                                "basctl",
                                "OnRightTreeDoubleClick: Parent has no docs, opening ancestor '"
                                    << pAncestorSymbol->sName << "'");
                            OpenDoxygenDocumentation(this, *pAncestorSymbol);
                            m_bPerformingAction = false;
                            return true;
                        }
                    }
                }
                SAL_INFO("basctl", "OnRightTreeDoubleClick: No documentable ancestor found for '"
                                       << pParentSymbol->sName << "'");
            }
            else
            {
                SAL_INFO("basctl",
                         "OnRightTreeDoubleClick: Action is OpenDoxygenDocumentation for parent '"
                             << pParentSymbol->sName << "' (from left pane).");
                OpenDoxygenDocumentation(this, *pParentSymbol);
                m_bPerformingAction = false;
                return true;
            }
        }
    }

    m_bPerformingAction = false;
    SAL_WARN("basctl", "OnRightTreeDoubleClick: No action taken for symbol.");
    ShowDocsError(this, IDEResId(RID_STR_OB_NO_DOCUMENTATION));

    return false;
}

IMPL_LINK(ObjectBrowser, OnNodeExpand, const weld::TreeIter&, rParentIter, bool)
{
    if (m_bDisposed || !m_pDataProvider)
    {
        return false;
    }

    auto pParentSymbol = GetSymbolForIter(rParentIter, *m_xLeftTreeView, m_aLeftTreeSymbolIndex);
    if (!pParentSymbol)
    {
        return false;
    }

    if (m_xLeftTreeView->iter_has_child(rParentIter))
    {
        return true;
    }

    const auto aAllChildren = m_pDataProvider->GetChildNodes(*pParentSymbol);
    if (aAllChildren.empty())
    {
        return true;
    }

    for (const auto& pChildInfo : aAllChildren)
    {
        if (pChildInfo)
        {
            AddEntry(*m_xLeftTreeView, m_aLeftTreeSymbolStore, m_aLeftTreeSymbolIndex, &rParentIter,
                     pChildInfo, IsExpandable(*pChildInfo));
        }
    }

    return true;
}

IMPL_LINK(ObjectBrowser, OnScopeChanged, weld::ComboBox&, rComboBox, void)
{
    if (m_bDisposed || !m_pDataProvider)
        return;

    OUString sSelectedId = rComboBox.get_active_id();
    IdeBrowserScope eScope = (sSelectedId == "ALL_LIBRARIES") ? IdeBrowserScope::ALL_LIBRARIES
                                                              : IdeBrowserScope::CURRENT_DOCUMENT;

    m_pDataProvider->SetScope(eScope, m_sLastActiveDocumentIdentifier);
    RefreshUI();
}

} // namespace basctl

/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
