/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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 <config_folders.h>

#include <dp_update.hxx>
#include <dp_version.hxx>
#include <dp_identifier.hxx>
#include <dp_descriptioninfoset.hxx>

#include <com/sun/star/ucb/CommandAbortedException.hpp>
#include <com/sun/star/ucb/CommandFailedException.hpp>
#include <osl/diagnose.h>
#include <rtl/bootstrap.hxx>
#include <sal/log.hxx>

using namespace ::com::sun::star;
using namespace ::com::sun::star::uno;


namespace dp_misc {
namespace {

int determineHighestVersion(
    OUString const & userVersion,
    OUString const & sharedVersion,
    OUString const & bundledVersion,
    std::u16string_view onlineVersion)
{
    int index = 0;
    OUString  greatest = userVersion;
    if (dp_misc::compareVersions(sharedVersion, greatest) == dp_misc::GREATER)
    {
        index = 1;
        greatest = sharedVersion;
    }
    if (dp_misc::compareVersions(bundledVersion, greatest) == dp_misc::GREATER)
    {
        index = 2;
        greatest = bundledVersion;
    }
    if (dp_misc::compareVersions(onlineVersion, greatest) == dp_misc::GREATER)
    {
        index = 3;
    }
    return index;
}

Sequence< Reference< xml::dom::XElement > >
getUpdateInformation( Reference<deployment::XUpdateInformationProvider > const & updateInformation,
                      Sequence< OUString > const & urls,
                      OUString const & identifier,
                      uno::Any & out_error)
{
    try {
        return updateInformation->getUpdateInformation(urls, identifier);
    } catch (const uno::RuntimeException &) {
        throw;
    } catch (const ucb::CommandFailedException & e) {
        out_error = e.Reason;
    } catch (const ucb::CommandAbortedException &) {
    } catch (const uno::Exception & e) {
        out_error <<= e;
    }
    return
        Sequence<Reference< xml::dom::XElement > >();
}

void getOwnUpdateInfos(
        Reference<uno::XComponentContext> const & xContext,
        Reference<deployment::XUpdateInformationProvider > const &  updateInformation,
        UpdateInfoMap& inout_map, std::vector<std::pair<Reference<deployment::XPackage>, uno::Any> > & out_errors,
        bool & out_allFound)
{
    bool bAllHaveOwnUpdateInformation = true;
    for (auto & inout : inout_map)
    {
        OSL_ASSERT(inout.second.extension.is());
        Sequence<OUString> urls(inout.second.extension->getUpdateInformationURLs());
        if (urls.hasElements())
        {
            const OUString search_id = dp_misc::getIdentifier(inout.second.extension);
            SAL_INFO( "extensions.update", "Searching update for " << search_id );
            uno::Any anyError;
            //It is unclear from the idl if there can be a null reference returned.
            //However all valid information should be the same
            const Sequence<Reference< xml::dom::XElement > >
                infos(getUpdateInformation(updateInformation, urls, search_id, anyError));
            if (anyError.hasValue())
                out_errors.emplace_back(inout.second.extension, anyError);

            for (const Reference< xml::dom::XElement >& element : infos)
            {
                dp_misc::DescriptionInfoset infoset(
                    xContext,
                    Reference< xml::dom::XNode >(element, UNO_QUERY_THROW));
                if (!infoset.hasDescription())
                    continue;
                std::optional< OUString > result_id(infoset.getIdentifier());
                if (!result_id)
                    continue;
                SAL_INFO( "extensions.update", "  found version "
                          << infoset.getVersion() << " for " << *result_id );
                if (*result_id != search_id)
                    continue;
                inout.second.version = infoset.getVersion();
                inout.second.info.set(element, UNO_QUERY_THROW);
                break;
            }
        }
        else
        {
            bAllHaveOwnUpdateInformation = false;
        }
    }
    out_allFound = bAllHaveOwnUpdateInformation;
}

void getDefaultUpdateInfos(
    Reference<uno::XComponentContext> const & xContext,
    Reference<deployment::XUpdateInformationProvider > const &  updateInformation,
    UpdateInfoMap& inout_map,
     std::vector<std::pair<Reference<deployment::XPackage>, uno::Any> > & out_errors)
{
    const OUString sDefaultURL(dp_misc::getExtensionDefaultUpdateURL());
    OSL_ASSERT(!sDefaultURL.isEmpty());

    Any anyError;
    const Sequence< Reference< xml::dom::XElement > >
        infos(
            getUpdateInformation(
                updateInformation,
                Sequence< OUString >(&sDefaultURL, 1), OUString(), anyError));
    if (anyError.hasValue())
        out_errors.emplace_back(Reference<deployment::XPackage>(), anyError);
    for (const Reference< xml::dom::XElement >& element : infos)
    {
        Reference< xml::dom::XNode > node(element, UNO_QUERY_THROW);
        dp_misc::DescriptionInfoset infoset(xContext, node);
        std::optional< OUString > id(infoset.getIdentifier());
        if (!id) {
            continue;
        }
        UpdateInfoMap::iterator j = inout_map.find(*id);
        if (j != inout_map.end())
        {
            //skip those extension which provide its own update urls
            if (j->second.extension->getUpdateInformationURLs().getLength())
                continue;
            OUString v(infoset.getVersion());
            //look for the highest version in the online repository
            if (dp_misc::compareVersions(v, j->second.version) ==
                dp_misc::GREATER)
            {
                j->second.version = v;
                j->second.info = std::move(node);
            }
        }
    }
}

bool containsBundledOnly(Sequence<Reference<deployment::XPackage> > const & sameIdExtensions)
{
    OSL_ASSERT(sameIdExtensions.getLength() == 3);
    return !sameIdExtensions[0].is() && !sameIdExtensions[1].is() && sameIdExtensions[2].is();
}

/** Returns true if the list of extensions are bundled extensions and there are no
    other extensions with the same identifier in the shared or user repository.
    If extensionList is NULL, then it is checked if there are only bundled extensions.
*/
bool onlyBundledExtensions(
    Reference<deployment::XExtensionManager> const & xExtMgr,
    std::vector< Reference<deployment::XPackage > > const * extensionList)
{
    OSL_ASSERT(xExtMgr.is());
    bool bOnlyBundled = true;
    if (extensionList)
    {
        for (auto const& elem : *extensionList)
        {
            Sequence<Reference<deployment::XPackage> > seqExt = xExtMgr->getExtensionsWithSameIdentifier(
                dp_misc::getIdentifier(elem), elem->getName(), Reference<ucb::XCommandEnvironment>());

            bOnlyBundled = containsBundledOnly(seqExt);
            if (!bOnlyBundled)
                break;
        }
    }
    else
    {
        const uno::Sequence< uno::Sequence< Reference<deployment::XPackage > > > seqAllExt =
            xExtMgr->getAllExtensions(Reference<task::XAbortChannel>(), Reference<ucb::XCommandEnvironment>());

        for (int pos(0), nLen(seqAllExt.getLength()); bOnlyBundled && pos != nLen; ++pos)
        {
            bOnlyBundled = containsBundledOnly(seqAllExt[pos]);
        }
    }
    return bOnlyBundled;
}

} // anon namespace


OUString getExtensionDefaultUpdateURL()
{
    OUString sUrl(
        u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version")
        ":Version:ExtensionUpdateURL}"_ustr);
    ::rtl::Bootstrap::expandMacros(sUrl);
    return sUrl;
}

/* returns the index of the greatest version, starting with 0

 */
UPDATE_SOURCE isUpdateUserExtension(
    bool bReadOnlyShared,
    OUString const & userVersion,
    OUString const & sharedVersion,
    OUString const & bundledVersion,
    std::u16string_view onlineVersion)
{
    UPDATE_SOURCE retVal = UPDATE_SOURCE_NONE;
    if (bReadOnlyShared)
    {
        if (!userVersion.isEmpty())
        {
            int index = determineHighestVersion(
                userVersion, sharedVersion, bundledVersion, onlineVersion);
            if (index == 1)
                retVal = UPDATE_SOURCE_SHARED;
            else if (index == 2)
                retVal = UPDATE_SOURCE_BUNDLED;
            else if (index == 3)
                retVal = UPDATE_SOURCE_ONLINE;
        }
        else if (!sharedVersion.isEmpty())
        {
            int index = determineHighestVersion(
                OUString(), sharedVersion, bundledVersion, onlineVersion);
            if (index == 2)
                retVal = UPDATE_SOURCE_BUNDLED;
            else if (index == 3)
                retVal = UPDATE_SOURCE_ONLINE;

        }
    }
    else
    {
        if (!userVersion.isEmpty())
        {
            int index = determineHighestVersion(
                userVersion, sharedVersion, bundledVersion, onlineVersion);
            if (index == 1)
                retVal = UPDATE_SOURCE_SHARED;
            else if (index == 2)
                retVal = UPDATE_SOURCE_BUNDLED;
            else if (index == 3)
                retVal = UPDATE_SOURCE_ONLINE;
        }
    }

    return retVal;
}

UPDATE_SOURCE isUpdateSharedExtension(
    bool bReadOnlyShared,
    OUString const & sharedVersion,
    OUString const & bundledVersion,
    std::u16string_view onlineVersion)
{
    if (bReadOnlyShared)
        return UPDATE_SOURCE_NONE;
    UPDATE_SOURCE retVal = UPDATE_SOURCE_NONE;

    if (!sharedVersion.isEmpty())
    {
        int index = determineHighestVersion(
            OUString(), sharedVersion, bundledVersion, onlineVersion);
        if (index == 2)
            retVal = UPDATE_SOURCE_BUNDLED;
        else if (index == 3)
            retVal = UPDATE_SOURCE_ONLINE;
    }
    return retVal;
}

Reference<deployment::XPackage>
getExtensionWithHighestVersion(
    Sequence<Reference<deployment::XPackage> > const & seqExt)
{
    if (!seqExt.hasElements())
        return Reference<deployment::XPackage>();

    Reference<deployment::XPackage> greatest;
    sal_Int32 len = seqExt.getLength();

    for (sal_Int32 i = 0; i < len; i++)
    {
        if (!greatest.is())
        {
            greatest = seqExt[i];
            continue;
        }
        Reference<deployment::XPackage> const & current = seqExt[i];
        //greatest has a value
        if (! current.is())
            continue;

        if (dp_misc::compareVersions(current->getVersion(), greatest->getVersion()) == dp_misc::GREATER)
            greatest = current;
    }
    return greatest;
}

UpdateInfo::UpdateInfo( Reference< deployment::XPackage> const & ext):
extension(ext)
{
}


UpdateInfoMap getOnlineUpdateInfos(
    Reference<uno::XComponentContext> const &xContext,
    Reference<deployment::XExtensionManager> const & xExtMgr,
    Reference<deployment::XUpdateInformationProvider > const & updateInformation,
    std::vector<Reference<deployment::XPackage > > const * extensionList,
    std::vector<std::pair< Reference<deployment::XPackage>, uno::Any> > & out_errors)
{
    OSL_ASSERT(xExtMgr.is());
    UpdateInfoMap infoMap;
    if (!xExtMgr.is() || onlyBundledExtensions(xExtMgr, extensionList))
        return infoMap;

    if (!extensionList)
    {
        const uno::Sequence< uno::Sequence< Reference<deployment::XPackage > > > seqAllExt =  xExtMgr->getAllExtensions(
            Reference<task::XAbortChannel>(), Reference<ucb::XCommandEnvironment>());

        //fill the UpdateInfoMap. key = extension identifier, value = UpdateInfo
        for (int pos = seqAllExt.getLength(); pos --; )
        {
            uno::Sequence<Reference<deployment::XPackage> > const &   seqExt = seqAllExt[pos];

            Reference<deployment::XPackage> extension = getExtensionWithHighestVersion(seqExt);
            OSL_ASSERT(extension.is());

            std::pair<UpdateInfoMap::iterator, bool> insertRet = infoMap.emplace(
                    dp_misc::getIdentifier(extension), UpdateInfo(extension));
            OSL_ASSERT(insertRet.second);
        }
    }
    else
    {
        for (auto const& elem : *extensionList)
        {
            OSL_ASSERT(elem.is());
            std::pair<UpdateInfoMap::iterator, bool> insertRet = infoMap.emplace(
                    dp_misc::getIdentifier(elem), UpdateInfo(elem));
            OSL_ASSERT(insertRet.second);
        }
    }

    //Now find the update information for the extensions which provide their own
    //URLs to update information.
    bool bAllInfosObtained = false;
    getOwnUpdateInfos(xContext, updateInformation, infoMap, out_errors, bAllInfosObtained);

    if (!bAllInfosObtained)
        getDefaultUpdateInfos(xContext, updateInformation, infoMap, out_errors);
    return infoMap;
}
OUString getHighestVersion(
    OUString const & sharedVersion,
    OUString const & bundledVersion,
    OUString const & onlineVersion)
{
    int index = determineHighestVersion(OUString(), sharedVersion, bundledVersion, onlineVersion);
    switch (index)
    {
    case 1: return sharedVersion;
    case 2: return bundledVersion;
    case 3: return onlineVersion;
    default: OSL_ASSERT(false);
    }

    return OUString();
}
} //namespace dp_misc

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
