/* -*- 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <CommandCategoryListBox.hxx>

#include <com/sun/star/uno/XInterface.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/frame/XDispatchInformationProvider.hpp>
#include <com/sun/star/frame/theUICommandDescription.hpp>
#include <com/sun/star/ui/theUICategoryDescription.hpp>
#include <com/sun/star/script/browse/XBrowseNode.hpp>
#include <com/sun/star/script/browse/BrowseNodeTypes.hpp>
#include <com/sun/star/script/browse/theBrowseNodeFactory.hpp>
#include <com/sun/star/script/browse/BrowseNodeFactoryViewTypes.hpp>
#include <vcl/commandinfoprovider.hxx>
#include <vcl/settings.hxx>
#include <vcl/svapp.hxx>

// include search util
#include <com/sun/star/util/SearchFlags.hpp>
#include <com/sun/star/util/SearchAlgorithms2.hpp>
#include <comphelper/diagnose_ex.hxx>
#include <unotools/textsearch.hxx>

#include <dialmgr.hxx>
#include <strings.hrc>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequenceashashmap.hxx>
#include <comphelper/SetFlagContextHelper.hxx>
#include <comphelper/string.hxx>
#include <officecfg/Office/Common.hxx>
#include <i18nlangtag/languagetag.hxx>
#include <i18nutil/searchopt.hxx>
#include <sal/log.hxx>

#include <cfg.hxx> //for SaveInData

CommandCategoryListBox::CommandCategoryListBox(std::unique_ptr<weld::ComboBox> xControl)
    : pStylesInfo(nullptr)
    , m_xControl(std::move(xControl))
{
    //Initialize search util
    m_searchOptions.AlgorithmType2 = css::util::SearchAlgorithms2::ABSOLUTE;
    m_searchOptions.transliterateFlags |= TransliterationFlags::IGNORE_CASE;
    m_searchOptions.searchFlag |= (css::util::SearchFlags::REG_NOT_BEGINOFLINE
                                   | css::util::SearchFlags::REG_NOT_ENDOFLINE);
}

CommandCategoryListBox::~CommandCategoryListBox() { ClearAll(); }

void CommandCategoryListBox::ClearAll()
{
    // Clear objects from m_aGroupInfo vector to avoid memory leak
    for (const auto& It : m_aGroupInfo)
    {
        if (It->nKind == SfxCfgKind::GROUP_STYLES && It->pObject)
        {
            SfxStyleInfo_Impl* pStyle = static_cast<SfxStyleInfo_Impl*>(It->pObject);
            delete pStyle;
        }
        else if (It->nKind == SfxCfgKind::FUNCTION_SCRIPT && It->pObject)
        {
            OUString* pScriptURI = static_cast<OUString*>(It->pObject);
            delete pScriptURI;
        }
        else if (It->nKind == SfxCfgKind::GROUP_SCRIPTCONTAINER && It->pObject)
        {
            css::uno::XInterface* xi = static_cast<css::uno::XInterface*>(It->pObject);
            if (xi != nullptr)
            {
                xi->release();
            }
        }
    }

    m_aGroupInfo.clear();
    m_xControl->clear();
}

void CommandCategoryListBox::Init(const css::uno::Reference<css::uno::XComponentContext>& xContext,
                                  const css::uno::Reference<css::frame::XFrame>& xFrame,
                                  const OUString& sModuleLongName)
{
    // User will not see incomplete UI
    m_xControl->freeze();
    ClearAll();

    m_xContext = xContext;
    m_xFrame = xFrame;

    m_sModuleLongName = sModuleLongName;
    m_xGlobalCategoryInfo = css::ui::theUICategoryDescription::get(m_xContext);
    m_xModuleCategoryInfo.set(m_xGlobalCategoryInfo->getByName(m_sModuleLongName),
                              css::uno::UNO_QUERY_THROW);
    m_xUICmdDescription = css::frame::theUICommandDescription::get(m_xContext);

    // Support style commands
    css::uno::Reference<css::frame::XController> xController;
    css::uno::Reference<css::frame::XModel> xModel;
    if (xFrame.is())
        xController = xFrame->getController();
    if (xController.is())
        xModel = xController->getModel();

    m_aStylesInfo.init(sModuleLongName, xModel);
    SetStylesInfo(&m_aStylesInfo);

    try
    {
        css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
            m_xFrame, css::uno::UNO_QUERY_THROW);
        css::uno::Sequence<sal_Int16> lGroups = xProvider->getSupportedCommandGroups();

        sal_Int32 nGroupsLength = lGroups.getLength();

        if (nGroupsLength > 0)
        {
            // Add the category of "All commands"
            m_aGroupInfo.push_back(
                std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_ALLFUNCTIONS, 0));
            m_xControl->append(weld::toId(m_aGroupInfo.back().get()),
                               CuiResId(RID_CUISTR_ALLFUNCTIONS));
        }

        // Separate the "All commands"category from the actual categories
        m_xControl->append_separator(u""_ustr);

        typedef std::pair<OUString, sal_Int16> str_id;
        std::vector<str_id> aCategories;

        // Add the actual categories
        for (sal_Int32 i = 0; i < nGroupsLength; ++i)
        {
            sal_Int16 nGroupID = lGroups[i];
            OUString sGroupID = OUString::number(nGroupID);
            OUString sGroupName;

            try
            {
                m_xModuleCategoryInfo->getByName(sGroupID) >>= sGroupName;
                if (sGroupName.isEmpty())
                    continue;
            }
            catch (const css::container::NoSuchElementException&)
            {
                continue;
            }
            aCategories.emplace_back(std::make_pair(sGroupName, nGroupID));
        }

        auto const sort = comphelper::string::NaturalStringSorter(
            comphelper::getProcessComponentContext(),
            Application::GetSettings().GetUILanguageTag().getLocale());

        std::sort(aCategories.begin(), aCategories.end(),
                  [&sort](const str_id& a, const str_id& b) {
                      return sort.compare(a.first, b.first) < 0;
                  });

        // Add the actual categories
        for (const auto& a : aCategories)
        {
            const OUString& rGroupName = a.first;
            sal_Int16 nGroupID = a.second;
            m_aGroupInfo.push_back(
                std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_FUNCTION, nGroupID));
            m_xControl->append(weld::toId(m_aGroupInfo.back().get()), rGroupName);
        }

        // Separate regular commands from styles and macros
        m_xControl->append_separator(u""_ustr);

        // Add macros category
        m_aGroupInfo.push_back(
            std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_SCRIPTCONTAINER, 0, nullptr));
        m_xControl->append(weld::toId(m_aGroupInfo.back().get()), CuiResId(RID_CUISTR_MACROS));

        // Add styles category
        //TODO: last param should contain user data?
        m_aGroupInfo.push_back(
            std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_STYLES, 0, nullptr));
        m_xControl->append(weld::toId(m_aGroupInfo.back().get()),
                           CuiResId(RID_CUISTR_GROUP_STYLES));
    }
    catch (const css::uno::RuntimeException&)
    {
        throw;
    }
    catch (const css::uno::Exception&)
    {
    }

    // Reveal the updated UI to user
    m_xControl->thaw();
    m_xControl->set_active(0);
}

void CommandCategoryListBox::FillFunctionsList(
    const css::uno::Sequence<css::frame::DispatchInformation>& xCommands,
    CuiConfigFunctionListBox* pFunctionListBox, const OUString& filterTerm,
    SaveInData* pCurrentSaveInData)
{
    // Setup search filter parameters
    m_searchOptions.searchString = filterTerm;
    utl::TextSearch textSearch(m_searchOptions);
    const bool bInExperimentalMode = officecfg::Office::Common::Misc::ExperimentalMode::get();

    for (const auto& rInfo : xCommands)
    {
        auto aProperties
            = vcl::CommandInfoProvider::GetCommandProperties(rInfo.Command, m_sModuleLongName);

        OUString sUIName = getCommandName(rInfo.Command);
        OUString sLabel = vcl::CommandInfoProvider::GetLabelForCommand(aProperties);
        OUString sTooltipLabel
            = vcl::CommandInfoProvider::GetTooltipForCommand(rInfo.Command, aProperties, m_xFrame);
        OUString sPopupLabel = (vcl::CommandInfoProvider::GetPopupLabelForCommand(aProperties))
                                   .replaceFirst("~", "");
        bool bIsExperimental
            = vcl::CommandInfoProvider::IsExperimental(rInfo.Command, m_sModuleLongName);

        // Hide experimental commands when not in experimental mode
        bool bHideExperimental = bIsExperimental && !bInExperimentalMode;

        // Apply the search filter
        if (bHideExperimental
            || (!filterTerm.isEmpty() && !textSearch.searchForward(sUIName)
                && !textSearch.searchForward(sLabel) && !textSearch.searchForward(sTooltipLabel)
                && !textSearch.searchForward(sPopupLabel)))
        {
            continue;
        }

        css::uno::Reference<css::graphic::XGraphic> xImage;
        if (pCurrentSaveInData)
            xImage = pCurrentSaveInData->GetImage(rInfo.Command);

        m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::FUNCTION_SLOT, 0));
        SfxGroupInfo_Impl* pGrpInfo = m_aGroupInfo.back().get();
        pGrpInfo->sCommand = rInfo.Command;
        pGrpInfo->sLabel = sUIName;
        pGrpInfo->sTooltip = sTooltipLabel;
        pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName, xImage);
    }
}

OUString CommandCategoryListBox::getCommandName(const OUString& sCommand)
{
    OUString sUIName;
    try
    {
        css::uno::Reference<css::container::XNameAccess> xModuleConf;
        m_xUICmdDescription->getByName(m_sModuleLongName) >>= xModuleConf;
        if (xModuleConf.is())
        {
            ::comphelper::SequenceAsHashMap lProps(xModuleConf->getByName(sCommand));
            sUIName = lProps.getUnpackedValueOrDefault(u"Name"_ustr, OUString());
        }
    }
    catch (const css::uno::RuntimeException&)
    {
        throw;
    }
    catch (css::uno::Exception&)
    {
        sUIName.clear();
    }

    // fallback for missing UINames !?
    if (sUIName.isEmpty())
    {
        sUIName = sCommand;
    }

    return sUIName;
}

void CommandCategoryListBox::categorySelected(CuiConfigFunctionListBox* pFunctionListBox,
                                              const OUString& filterTerm,
                                              SaveInData* pCurrentSaveInData)
{
    SfxGroupInfo_Impl* pInfo = weld::fromId<SfxGroupInfo_Impl*>(m_xControl->get_active_id());
    std::vector<std::unique_ptr<weld::TreeIter>> aNodesToExpand;
    pFunctionListBox->freeze();
    pFunctionListBox->ClearAll();

    switch (pInfo->nKind)
    {
        case SfxCfgKind::GROUP_ALLFUNCTIONS:
        {
            css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
                m_xFrame, css::uno::UNO_QUERY);
            sal_Int32 nEntryCount = m_xControl->get_count();

            for (sal_Int32 nCurPos = 0; nCurPos < nEntryCount; ++nCurPos)
            {
                SfxGroupInfo_Impl* pCurrentInfo
                    = weld::fromId<SfxGroupInfo_Impl*>(m_xControl->get_id(nCurPos));

                if (!pCurrentInfo) //separator
                    continue;

                if (pCurrentInfo->nKind == SfxCfgKind::GROUP_FUNCTION)
                {
                    css::uno::Sequence<css::frame::DispatchInformation> lCommands;
                    try
                    {
                        lCommands = xProvider->getConfigurableDispatchInformation(
                            pCurrentInfo->nUniqueID);
                        FillFunctionsList(lCommands, pFunctionListBox, filterTerm,
                                          pCurrentSaveInData);
                    }
                    catch (css::container::NoSuchElementException&)
                    {
                    }
                }
            }

            break;
        }
        case SfxCfgKind::GROUP_FUNCTION:
        {
            sal_uInt16 nGroup = pInfo->nUniqueID;
            css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
                m_xFrame, css::uno::UNO_QUERY_THROW);
            css::uno::Sequence<css::frame::DispatchInformation> lCommands
                = xProvider->getConfigurableDispatchInformation(nGroup);
            FillFunctionsList(lCommands, pFunctionListBox, filterTerm, pCurrentSaveInData);
            break;
        }
        case SfxCfgKind::GROUP_SCRIPTCONTAINER: //Macros
        {
            SAL_INFO("cui.customize", "** ** About to initialise SF Scripts");
            // Add Scripting Framework entries
            css::uno::Reference<css::script::browse::XBrowseNode> rootNode;
            try
            {
                css::uno::Reference<css::script::browse::XBrowseNodeFactory> xFac
                    = css::script::browse::theBrowseNodeFactory::get(m_xContext);
                rootNode.set(xFac->createView(
                    css::script::browse::BrowseNodeFactoryViewTypes::MACROSELECTOR));
            }
            catch (css::uno::Exception const&)
            {
                TOOLS_WARN_EXCEPTION(
                    "cui.customize",
                    "Caught some exception whilst retrieving browse nodes from factory");
                // TODO exception handling
            }

            if (rootNode.is() && rootNode->hasChildNodes())
            {
                //We call acquire on the XBrowseNode so that it does not
                //get autodestructed and become invalid when accessed later.
                rootNode->acquire();

                m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
                    SfxCfgKind::GROUP_SCRIPTCONTAINER, 0, static_cast<void*>(rootNode.get())));

                // Add main macro groups
                const css::uno::Sequence<css::uno::Reference<css::script::browse::XBrowseNode>>
                    aChildNodes = rootNode->getChildNodes();
                for (auto const& childGroup : aChildNodes)
                {
                    childGroup->acquire();

                    if (childGroup->hasChildNodes())
                    {
                        OUString sUIName;
                        if (childGroup->getName() == "user")
                        {
                            sUIName = CuiResId(RID_CUISTR_MYMACROS);
                        }
                        else if (childGroup->getName() == "share")
                        {
                            sUIName = CuiResId(RID_CUISTR_PRODMACROS);
                        }
                        else
                        {
                            sUIName = childGroup->getName();
                        }

                        if (sUIName.isEmpty())
                        {
                            continue;
                        }

                        m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
                            SfxCfgKind::GROUP_SCRIPTCONTAINER, 0));
                        std::unique_ptr<weld::TreeIter> xMacroGroup(pFunctionListBox->tree_append(
                            weld::toId(m_aGroupInfo.back().get()), sUIName));

                        {
                            // tdf#128010: Do not nag user asking to enable JRE: if it's disabled,
                            // simply don't show relevant entries (user chose to not use JRE)
                            css::uno::ContextLayer layer(
                                comphelper::NoEnableJavaInteractionContext());
                            //Add the children and the grand children
                            addChildren(xMacroGroup.get(), childGroup, pFunctionListBox, filterTerm,
                                        pCurrentSaveInData, aNodesToExpand);
                        }

                        // Remove the main group if empty
                        if (!pFunctionListBox->iter_has_child(*xMacroGroup))
                        {
                            pFunctionListBox->remove(*xMacroGroup);
                        }
                        else if (!filterTerm.isEmpty())
                        {
                            aNodesToExpand.emplace_back(std::move(xMacroGroup));
                        }
                    }
                }
            }

            break;
        }
        case SfxCfgKind::GROUP_STYLES:
        {
            const std::vector<SfxStyleInfo_Impl> lStyleFamilies = pStylesInfo->getStyleFamilies();

            for (const auto& pIt : lStyleFamilies)
            {
                if (pIt.sLabel.isEmpty())
                {
                    continue;
                }

                m_aGroupInfo.push_back(
                    std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_STYLES, 0));
                // pIt.sLabel is Name of the style family
                std::unique_ptr<weld::TreeIter> xFuncEntry(pFunctionListBox->tree_append(
                    weld::toId(m_aGroupInfo.back().get()), pIt.sLabel));

                const std::vector<SfxStyleInfo_Impl> lStyles = pStylesInfo->getStyles(pIt.sFamily);

                // Setup search filter parameters
                m_searchOptions.searchString = filterTerm;
                utl::TextSearch textSearch(m_searchOptions);

                // Insert children (styles)
                for (const auto& pStyleIt : lStyles)
                {
                    OUString sUIName = pStyleIt.sLabel;
                    sal_Int32 aStartPos = 0;
                    sal_Int32 aEndPos = sUIName.getLength();

                    // Apply the search filter
                    if (!filterTerm.isEmpty()
                        && !textSearch.SearchForward(sUIName, &aStartPos, &aEndPos))
                    {
                        continue;
                    }

                    SfxStyleInfo_Impl* pStyle = new SfxStyleInfo_Impl(pStyleIt);

                    m_aGroupInfo.push_back(
                        std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::GROUP_STYLES, 0, pStyle));

                    m_aGroupInfo.back()->sCommand = pStyle->sCommand;
                    m_aGroupInfo.back()->sLabel = pStyle->sLabel;

                    pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName,
                                             xFuncEntry.get());
                }

                // Remove the style group from the list if no children
                if (!pFunctionListBox->iter_has_child(*xFuncEntry))
                {
                    pFunctionListBox->remove(*xFuncEntry);
                }
                else if (!filterTerm.isEmpty())
                {
                    aNodesToExpand.emplace_back(std::move(xFuncEntry));
                }
            }

            break;
        }
        default:
            // Do nothing, the list box will stay empty
            SAL_INFO("cui.customize",
                     "Ignoring unexpected SfxCfgKind: " << static_cast<int>(pInfo->nKind));
            break;
    }

    pFunctionListBox->thaw();

    if (pFunctionListBox->n_children())
        pFunctionListBox->select(0);

    //post freeze
    for (const auto& it : aNodesToExpand)
        pFunctionListBox->expand_row(*it);
}

void CommandCategoryListBox::SetStylesInfo(SfxStylesInfo_Impl* pStyles) { pStylesInfo = pStyles; }

void CommandCategoryListBox::addChildren(
    const weld::TreeIter* parentEntry,
    const css::uno::Reference<css::script::browse::XBrowseNode>& parentNode,
    CuiConfigFunctionListBox* pFunctionListBox, const OUString& filterTerm,
    SaveInData* pCurrentSaveInData, std::vector<std::unique_ptr<weld::TreeIter>>& rNodesToExpand)
{
    // Setup search filter parameters
    m_searchOptions.searchString = filterTerm;
    utl::TextSearch textSearch(m_searchOptions);

    const css::uno::Sequence<css::uno::Reference<css::script::browse::XBrowseNode>> aChildNodes
        = parentNode->getChildNodes();
    for (auto const& child : aChildNodes)
    {
        // Acquire to prevent auto-destruction
        child->acquire();

        if (child->hasChildNodes())
        {
            OUString sUIName = child->getName();

            m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
                SfxCfgKind::GROUP_SCRIPTCONTAINER, 0, static_cast<void*>(child.get())));
            std::unique_ptr<weld::TreeIter> xNewEntry(pFunctionListBox->tree_append(
                weld::toId(m_aGroupInfo.back().get()), sUIName, parentEntry));

            addChildren(xNewEntry.get(), child, pFunctionListBox, filterTerm, pCurrentSaveInData,
                        rNodesToExpand);

            // Remove the group if empty
            if (!pFunctionListBox->iter_has_child(*xNewEntry))
                pFunctionListBox->remove(*xNewEntry);
            else
                rNodesToExpand.emplace_back(std::move(xNewEntry));
        }
        else if (child->getType() == css::script::browse::BrowseNodeTypes::SCRIPT)
        {
            // Prepare for filtering
            OUString sUIName = child->getName();
            sal_Int32 aStartPos = 0;
            sal_Int32 aEndPos = sUIName.getLength();

            // Apply the search filter
            if (!filterTerm.isEmpty() && !textSearch.SearchForward(sUIName, &aStartPos, &aEndPos))
            {
                continue;
            }

            OUString uri, description;

            css::uno::Reference<css::beans::XPropertySet> xPropSet(child, css::uno::UNO_QUERY);

            if (!xPropSet.is())
            {
                continue;
            }

            css::uno::Any value = xPropSet->getPropertyValue(u"URI"_ustr);
            value >>= uri;

            try
            {
                value = xPropSet->getPropertyValue(u"Description"_ustr);
                value >>= description;
            }
            catch (css::uno::Exception&)
            {
                // do nothing, the description will be empty
            }

            if (description.isEmpty())
            {
                description = CuiResId(RID_CUISTR_NOMACRODESC);
            }

            OUString* pScriptURI = new OUString(uri);

            css::uno::Reference<css::graphic::XGraphic> xImage;
            if (pCurrentSaveInData)
                xImage = pCurrentSaveInData->GetImage(uri);

            m_aGroupInfo.push_back(
                std::make_unique<SfxGroupInfo_Impl>(SfxCfgKind::FUNCTION_SCRIPT, 0, pScriptURI));
            m_aGroupInfo.back()->sCommand = uri;
            m_aGroupInfo.back()->sLabel = sUIName;
            m_aGroupInfo.back()->sHelpText = description;
            pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName, xImage,
                                     parentEntry);
        }
    }
}

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