/* -*- 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 <sal/config.h>
#include <sal/log.hxx>

#include <deque>
#include <string_view>

#include <com/sun/star/container/XNameAccess.hpp>
#include <com/sun/star/io/XInputStream.hpp>
#include <com/sun/star/packages/zip/ZipFileAccess.hpp>
#include <com/sun/star/ucb/SimpleFileAccess.hpp>
#include <com/sun/star/uno/Exception.hpp>
#include <com/sun/star/uno/RuntimeException.hpp>
#include <com/sun/star/uno/Sequence.hxx>
#include <comphelper/processfactory.hxx>
#include <cppuhelper/implbase.hxx>
#include <osl/file.hxx>
#include <osl/diagnose.h>
#include <osl/process.h>
#include <rtl/bootstrap.hxx>
#include <rtl/uri.hxx>
#include <rtl/strbuf.hxx>

#include <comphelper/diagnose_ex.hxx>
#include <tools/stream.hxx>
#include <tools/urlobj.hxx>
#include <implimagetree.hxx>

#include <utility>
#include <vcl/bitmapex.hxx>
#include <vcl/dibtools.hxx>
#include <vcl/settings.hxx>
#include <vcl/svapp.hxx>
#include <vcl/BitmapTools.hxx>
#include <IconThemeScanner.hxx>
#include <vcl/filter/PngImageReader.hxx>
#include <vcl/outdev.hxx>
#include <vcl/filter/PngImageWriter.hxx>
#include <o3tl/string_view.hxx>
#include <bitmap/BitmapLightenFilter.hxx>

using namespace css;

bool ImageRequestParameters::convertToDarkTheme()
{
    static bool bIconsForDarkTheme = !!getenv("VCL_ICONS_FOR_DARK_THEME");

    bool bConvertToDarkTheme = false;
    if (!(meFlags & ImageLoadFlags::IgnoreDarkTheme))
        bConvertToDarkTheme = bIconsForDarkTheme;

    return bConvertToDarkTheme;
}

sal_Int32 ImageRequestParameters::scalePercentage()
{
    sal_Int32 aScalePercentage = 100;
    OutputDevice* pDevice = Application::GetDefaultDevice();
    if (!(meFlags & ImageLoadFlags::IgnoreScalingFactor) && pDevice != nullptr)
        aScalePercentage = pDevice->GetDPIScalePercentage();
    else if (mnScalePercentage > 0)
        aScalePercentage = mnScalePercentage;
    return aScalePercentage;
}

namespace
{

OUString convertLcTo32Path(std::u16string_view rPath)
{
    OUString aResult;
    size_t nSlashPos = rPath.rfind('/');
    if (nSlashPos != std::u16string_view::npos)
    {
        sal_Int32 nCopyFrom = nSlashPos + 1;
        std::u16string_view sFile = rPath.substr(nCopyFrom);
        std::u16string_view sDir = rPath.substr(0, nSlashPos);
        if (!sFile.empty() && o3tl::starts_with(sFile, u"lc_"))
        {
            aResult = OUString::Concat(sDir) + "/32/" + sFile.substr(3);
        }
    }
    return aResult;
}

OUString createPath(std::u16string_view name, sal_Int32 pos, std::u16string_view locale)
{
    return OUString::Concat(name.substr(0, pos + 1)) + locale + name.substr(pos);
}

OUString getIconCacheUrl(std::u16string_view sVariant, ImageRequestParameters const & rParameters)
{
    // the macro expansion can be expensive in bulk, so cache that
    static OUString CACHE_DIR = []()
    {
        OUString sDir = "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/";
        rtl::Bootstrap::expandMacros(sDir);
        return sDir;
    }();
    return CACHE_DIR + rParameters.msStyle + "/" + sVariant + "/" + rParameters.msName;
}

OUString createIconCacheUrl(
    std::u16string_view sVariant, ImageRequestParameters const & rParameters)
{
    OUString sUrl(getIconCacheUrl(sVariant, rParameters));
    OUString sDir = sUrl.copy(0, sUrl.lastIndexOf('/'));
    osl::Directory::createPath(sDir);
    return sUrl;
}

bool urlExists(OUString const & sUrl)
{
    osl::File aFile(sUrl);
    osl::FileBase::RC eRC = aFile.open(osl_File_OpenFlag_Read);
    return osl::FileBase::E_None == eRC;
}

OUString getNameNoExtension(std::u16string_view sName)
{
    size_t nDotPosition = sName.rfind('.');
    return OUString(sName.substr(0, nDotPosition));
}

std::shared_ptr<SvMemoryStream> wrapStream(uno::Reference<io::XInputStream> const & rInputStream)
{
    // This could use SvInputStream instead if that did not have a broken
    // SeekPos implementation for an XInputStream that is not also XSeekable
    // (cf. "@@@" at tags/DEV300_m37/svtools/source/misc1/strmadpt.cxx@264807
    // l. 593):
    OSL_ASSERT(rInputStream.is());
    std::shared_ptr<SvMemoryStream> aMemoryStream(std::make_shared<SvMemoryStream>());
    for (;;)
    {
        const sal_Int32 nSize(2048);
        uno::Sequence<sal_Int8> aData(nSize);
        sal_Int32 nRead = rInputStream->readBytes(aData, nSize);
        aMemoryStream->WriteBytes(aData.getConstArray(), nRead);
        if (nRead < nSize)
            break;
    }
    aMemoryStream->Seek(0);
    rInputStream->closeInput();
    return aMemoryStream;
}

void loadImageFromStream(std::shared_ptr<SvStream> const & xStream, OUString const & rPath, ImageRequestParameters& rParameters)
{
    bool bConvertToDarkTheme = rParameters.convertToDarkTheme();
    sal_Int32 aScalePercentage = rParameters.scalePercentage();

    if (rPath.endsWith(".png"))
    {
        vcl::PngImageReader aPNGReader(*xStream);
        aPNGReader.read(rParameters.mrBitmap);
    }
    else if (rPath.endsWith(".svg"))
    {
        rParameters.mbWriteImageToCache = true; // We always want to cache a SVG image
        vcl::bitmap::loadFromSvg(*xStream, rPath, rParameters.mrBitmap, aScalePercentage / 100.0);

        if (bConvertToDarkTheme)
            BitmapFilter::Filter(rParameters.mrBitmap, BitmapLightenFilter());

        return;
    }
    else
    {
        ReadDIBBitmapEx(rParameters.mrBitmap, *xStream);
    }

    if (bConvertToDarkTheme)
    {
        rParameters.mbWriteImageToCache = true; // Cache the dark variant
        BitmapFilter::Filter(rParameters.mrBitmap, BitmapLightenFilter());
    }

    if (aScalePercentage > 100)
    {
        rParameters.mbWriteImageToCache = true; // Cache the scaled variant
        double aScaleFactor(aScalePercentage / 100.0);
        // when scaling use the full 24bit RGB values
        rParameters.mrBitmap.Convert(BmpConversion::N24Bit);
        rParameters.mrBitmap.Scale(aScaleFactor, aScaleFactor, BmpScaleFlag::Fast);
    }
}

} // end anonymous namespace

ImplImageTree::ImplImageTree()
{
}

ImplImageTree::~ImplImageTree()
{
}

std::vector<OUString> ImplImageTree::getPaths(OUString const & name, LanguageTag const & rLanguageTag)
{
    std::vector<OUString> sPaths;

    sal_Int32 pos = name.lastIndexOf('/');
    if (pos != -1)
    {
        for (const OUString& rFallback : rLanguageTag.getFallbackStrings(true))
        {
            OUString aFallbackName = getNameNoExtension(getRealImageName(createPath(name, pos, rFallback)));
            sPaths.emplace_back(aFallbackName + ".png");
            sPaths.emplace_back(aFallbackName + ".svg");
        }
    }

    OUString aRealName = getNameNoExtension(getRealImageName(name));
    sPaths.emplace_back(aRealName + ".png");
    sPaths.emplace_back(aRealName + ".svg");

    return sPaths;
}

OUString ImplImageTree::getImageUrl(OUString const & rName, OUString const & rStyle, OUString const & rLang)
{
    OUString aStyle(rStyle);

    while (!aStyle.isEmpty())
    {
        try
        {
            setStyle(aStyle);

            if (checkPathAccess())
            {
                IconSet& rIconSet = getCurrentIconSet();
                const uno::Reference<container::XNameAccess> & rNameAccess = rIconSet.maNameAccess;

                LanguageTag aLanguageTag(rLang);

                for (const OUString& rPath: getPaths(rName, aLanguageTag))
                {
                    if (rNameAccess->hasByName(rPath))
                    {
                        return "vnd.sun.star.zip://"
                            + rtl::Uri::encode(rIconSet.maURL, rtl_UriCharClassRegName,
                                               rtl_UriEncodeIgnoreEscapes, RTL_TEXTENCODING_UTF8)
                            + "/" + rPath;
                    }
                }
            }
        }
        catch (const uno::Exception &)
        {
            TOOLS_INFO_EXCEPTION("vcl", "");
        }

        aStyle = fallbackStyle(aStyle);
    }
    return OUString();
}

uno::Reference<io::XInputStream> ImplImageTree::getImageXInputStream(OUString const & rName, OUString const & rStyle, OUString const & rLang)
{
    OUString aStyle(rStyle);

    while (!aStyle.isEmpty())
    {
        try
        {
            setStyle(aStyle);

            if (checkPathAccess())
            {
                IconSet& rIconSet = getCurrentIconSet();
                const uno::Reference<container::XNameAccess>& rNameAccess = rIconSet.maNameAccess;

                LanguageTag aLanguageTag(rLang);

                for (const OUString& rPath: getPaths(rName, aLanguageTag))
                {
                    if (rNameAccess->hasByName(rPath))
                    {
                        uno::Reference<io::XInputStream> aStream;
                        bool ok = rNameAccess->getByName(rPath) >>= aStream;
                        assert(ok);
                        (void)ok; // prevent unused warning in release build
                        return aStream;
                    }
                }
            }
        }
        catch (const uno::Exception &)
        {
            TOOLS_INFO_EXCEPTION("vcl", "");
        }

        aStyle = fallbackStyle(aStyle);
    }
    return nullptr;
}

std::shared_ptr<SvMemoryStream> ImplImageTree::getImageStream(OUString const & rName, OUString const & rStyle, OUString const & rLang)
{
    uno::Reference<io::XInputStream> xStream = getImageXInputStream(rName, rStyle, rLang);
    if (xStream)
        return wrapStream(xStream);
    return std::shared_ptr<SvMemoryStream>();
}

OUString ImplImageTree::fallbackStyle(std::u16string_view rsStyle)
{
    OUString sResult;

    if (rsStyle == u"colibre" || rsStyle == u"helpimg")
        sResult = "";
    else if (rsStyle == u"sifr" || rsStyle == u"breeze_dark")
        sResult = "breeze";
    else if (rsStyle == u"sifr_dark" )
        sResult = "breeze_dark";
    else
        sResult = "colibre";

    return sResult;
}

bool ImplImageTree::loadImage(OUString const & rName, OUString const & rStyle, BitmapEx & rBitmap, bool localized,
                              const ImageLoadFlags eFlags, sal_Int32 nScalePercentage)
{
    OUString aCurrentStyle(rStyle);
    while (!aCurrentStyle.isEmpty())
    {
        try
        {
            ImageRequestParameters aParameters(rName, aCurrentStyle, rBitmap, localized, eFlags, nScalePercentage);
            if (doLoadImage(aParameters))
                return true;
        }
        catch (uno::RuntimeException &)
        {}

        aCurrentStyle = fallbackStyle(aCurrentStyle);
    }
    return false;
}

namespace
{

OUString createVariant(ImageRequestParameters& rParameters)
{
    bool bConvertToDarkTheme = rParameters.convertToDarkTheme();
    sal_Int32 aScalePercentage = rParameters.scalePercentage();

    OUString aVariant = OUString::number(aScalePercentage);

    if (bConvertToDarkTheme)
        aVariant += "-dark";

    return aVariant;
}

bool loadDiskCachedVersion(std::u16string_view sVariant, ImageRequestParameters& rParameters)
{
    OUString sUrl(getIconCacheUrl(sVariant, rParameters));
    if (!urlExists(sUrl))
        return false;
    SvFileStream aFileStream(sUrl, StreamMode::READ);
    vcl::PngImageReader aPNGReader(aFileStream);
    aPNGReader.read(rParameters.mrBitmap);
    return true;
}

void cacheBitmapToDisk(std::u16string_view sVariant, ImageRequestParameters const & rParameters)
{
    OUString sUrl(createIconCacheUrl(sVariant, rParameters));
    try
    {
        SvFileStream aStream(sUrl, StreamMode::WRITE);
        vcl::PngImageWriter aWriter(aStream);
        aWriter.write(rParameters.mrBitmap);
        aStream.Close();
    }
    catch (...)
    {}
}

} // end anonymous namespace

bool ImplImageTree::doLoadImage(ImageRequestParameters& rParameters)
{
    setStyle(rParameters.msStyle);

    if (iconCacheLookup(rParameters))
        return true;

    OUString aVariant = createVariant(rParameters);
    if (loadDiskCachedVersion(aVariant, rParameters))
        return true;

    if (!rParameters.mrBitmap.IsEmpty())
        rParameters.mrBitmap.SetEmpty();

    LanguageTag aLanguageTag = Application::GetSettings().GetUILanguageTag();

    std::vector<OUString> aPaths = getPaths(rParameters.msName, aLanguageTag);

    bool bFound = false;

    try
    {
        bFound = findImage(aPaths, rParameters);
    }
    catch (uno::RuntimeException&)
    {
        throw;
    }
    catch (const uno::Exception&)
    {
        TOOLS_INFO_EXCEPTION("vcl", "ImplImageTree::doLoadImage");
    }

    if (bFound)
    {
        if (rParameters.mbWriteImageToCache)
        {
            cacheBitmapToDisk(aVariant, rParameters);
        }
        getIconCache(rParameters)[rParameters.msName] = std::make_pair(rParameters.mbLocalized, rParameters.mrBitmap);
    }

    return bFound;
}

void ImplImageTree::shutdown()
{
    maCurrentStyle.clear();
    maIconSets.clear();
}

void ImplImageTree::setStyle(OUString const & style)
{
    assert(!style.isEmpty());
    if (style != maCurrentStyle)
    {
        maCurrentStyle = style;
        createStyle();
    }
}

/**
 * The vcldemo app doesn't set up all the config stuff that the main app does, so we need another
 * way of finding the cursor images.
 */
static bool isVclDemo()
{
    static const bool bVclDemoOverride = std::getenv("LIBO_VCL_DEMO") != nullptr;
    return bVclDemoOverride;
}

void ImplImageTree::createStyle()
{
    if (maIconSets.find(maCurrentStyle) != maIconSets.end())
        return;

    OUString sThemeUrl;

    if (isVclDemo())
    {
        if (maCurrentStyle == "default")
            sThemeUrl = "file://" SRC_ROOT "/icon-themes/colibre-svg";
        else
            sThemeUrl = "file://" SRC_ROOT "/icon-themes/" + maCurrentStyle;
    }
    else if (maCurrentStyle != "default")
    {
        OUString paths = vcl::IconThemeScanner::GetStandardIconThemePath();
        std::deque<OUString> aPaths;
        sal_Int32 nIndex = 0;
        do
        {
            aPaths.push_front(paths.getToken(0, ';', nIndex));
        }
        while (nIndex >= 0);

        for (const auto& path : aPaths)
        {
            INetURLObject aUrl(path);
            OSL_ASSERT(!aUrl.HasError());

            bool ok = aUrl.Append(Concat2View("images_" + maCurrentStyle), INetURLObject::EncodeMechanism::All);
            OSL_ASSERT(ok);
            sThemeUrl = aUrl.GetMainURL(INetURLObject::DecodeMechanism::NONE) + ".zip";
            if (urlExists(sThemeUrl))
                break;
            sThemeUrl.clear();
        }

        if (sThemeUrl.isEmpty())
            return;
    }
    else
    {
        sThemeUrl += "images";
        if (!urlExists(sThemeUrl))
            return;
    }

    maIconSets[maCurrentStyle] = IconSet(sThemeUrl);

    loadImageLinks();
}

/// Find an icon cache for the right scale factor
ImplImageTree::IconCache &ImplImageTree::getIconCache(const ImageRequestParameters& rParameters)
{
    IconSet &rSet = getCurrentIconSet();
    auto it = rSet.maScaledIconCaches.find(rParameters.mnScalePercentage);
    if ( it != rSet.maScaledIconCaches.end() )
        return it->second;
    rSet.maScaledIconCaches.emplace(rParameters.mnScalePercentage, IconCache());
    return rSet.maScaledIconCaches[rParameters.mnScalePercentage];
}

bool ImplImageTree::iconCacheLookup(ImageRequestParameters& rParameters)
{
    IconCache& rIconCache = getIconCache(rParameters);

    IconCache::iterator i(rIconCache.find(getRealImageName(rParameters.msName)));
    if (i != rIconCache.end() && i->second.first == rParameters.mbLocalized)
    {
        rParameters.mrBitmap = i->second.second;
        return true;
    }

    return false;
}

bool ImplImageTree::findImage(std::vector<OUString> const & rPaths, ImageRequestParameters& rParameters)
{
    if (!checkPathAccess())
        return false;

    uno::Reference<container::XNameAccess> const & rNameAccess = getCurrentIconSet().maNameAccess;

    for (OUString const & rPath : rPaths)
    {
        if (rNameAccess->hasByName(rPath))
        {
            uno::Reference<io::XInputStream> aStream;
            bool ok = rNameAccess->getByName(rPath) >>= aStream;
            assert(ok);
            (void)ok; // prevent unused warning in release build

            loadImageFromStream(wrapStream(aStream), rPath, rParameters);

            return true;
        }
    }
    return false;
}

void ImplImageTree::loadImageLinks()
{
    static const OUStringLiteral aLinkFilename(u"links.txt");

    if (!checkPathAccess())
        return;

    const uno::Reference<container::XNameAccess> &rNameAccess = getCurrentIconSet().maNameAccess;

    if (rNameAccess->hasByName(aLinkFilename))
    {
        uno::Reference<io::XInputStream> xStream;
        bool ok = rNameAccess->getByName(aLinkFilename) >>= xStream;
        assert(ok);
        (void)ok; // prevent unused warning in release build

        parseLinkFile(wrapStream(xStream));
        return;
    }
}

void ImplImageTree::parseLinkFile(std::shared_ptr<SvStream> const & xStream)
{
    OStringBuffer aLine;
    OUString aLink, aOriginal;
    int nLineNo = 0;
    while (xStream->ReadLine(aLine))
    {
        ++nLineNo;
        if (aLine.isEmpty())
            continue;

        sal_Int32 nIndex = 0;
        aLink = OStringToOUString(o3tl::getToken(aLine, 0, ' ', nIndex), RTL_TEXTENCODING_UTF8);
        aOriginal = OStringToOUString(o3tl::getToken(aLine, 0, ' ', nIndex), RTL_TEXTENCODING_UTF8);

        // skip comments, or incomplete entries
        if (aLink.isEmpty() || aLink[0] == '#' || aOriginal.isEmpty())
        {
            if (aLink.isEmpty() || aOriginal.isEmpty())
                SAL_WARN("vcl", "ImplImageTree::parseLinkFile: icon links.txt parse error, incomplete link at line " << nLineNo);
            continue;
        }

        getCurrentIconSet().maLinkHash[aLink] = aOriginal;

        OUString aOriginal32 = convertLcTo32Path(aOriginal);
        OUString aLink32 = convertLcTo32Path(aLink);

        if (!aOriginal32.isEmpty() && !aLink32.isEmpty())
            getCurrentIconSet().maLinkHash[aLink32] = aOriginal32;
    }
}

OUString const & ImplImageTree::getRealImageName(OUString const & rIconName)
{
    IconLinkHash & rLinkHash = maIconSets[maCurrentStyle].maLinkHash;

    OUString sNameWithNoExtension = getNameNoExtension(rIconName);

    // PNG is priority
    auto it = rLinkHash.find(sNameWithNoExtension + ".png");
    if (it != rLinkHash.end())
        return it->second;

    // also check SVG name
    it = rLinkHash.find(sNameWithNoExtension + ".svg");
    if (it != rLinkHash.end())
        return it->second;

    // neither was found so just return the original name
    return rIconName;
}

namespace {

class FolderFileAccess : public ::cppu::WeakImplHelper<css::container::XNameAccess>
{
public:
    uno::Reference< uno::XComponentContext > mxContext;
    OUString maURL;
    FolderFileAccess(uno::Reference< uno::XComponentContext > context, OUString url)
        : mxContext(std::move(context)), maURL(std::move(url)) {}
    // XElementAccess
    virtual css::uno::Type SAL_CALL getElementType() override { return cppu::UnoType<io::XInputStream>::get(); }
    virtual sal_Bool SAL_CALL hasElements() override { return true; }
    // XNameAccess
    virtual css::uno::Any SAL_CALL getByName( const OUString& aName ) override
    {
        uno::Reference< io::XInputStream > xInputStream = ucb::SimpleFileAccess::create(mxContext)->openFileRead( maURL + "/" + aName );
        return css::uno::Any(xInputStream);
    }
    virtual css::uno::Sequence< OUString > SAL_CALL getElementNames() override
    {
        return {};
    }
    virtual sal_Bool SAL_CALL hasByName( const OUString& aName ) override
    {
        osl::File aBaseFile(maURL + "/" + aName);
        return osl::File::E_None == aBaseFile.open(osl_File_OpenFlag_Read);
    }
};

}

bool ImplImageTree::checkPathAccess()
{
    IconSet& rIconSet = getCurrentIconSet();
    uno::Reference<container::XNameAccess> & rNameAccess = rIconSet.maNameAccess;
    if (rNameAccess.is())
        return true;

    try
    {
        if (isVclDemo())
            rNameAccess = new FolderFileAccess(comphelper::getProcessComponentContext(), rIconSet.maURL);
        else
            rNameAccess = packages::zip::ZipFileAccess::createWithURL(comphelper::getProcessComponentContext(), rIconSet.maURL);
    }
    catch (const uno::RuntimeException &)
    {
        throw;
    }
    catch (const uno::Exception &)
    {
        TOOLS_INFO_EXCEPTION("vcl", "ImplImageTree::zip file location " << rIconSet.maURL);
        return false;
    }
    return rNameAccess.is();
}

uno::Reference<container::XNameAccess> const & ImplImageTree::getNameAccess()
{
    (void)checkPathAccess();
    return getCurrentIconSet().maNameAccess;
}

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