/* -*- 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 <accelerators/storageholder.hxx>
#include <accelerators/acceleratorconfiguration.hxx>
#include <sal/log.hxx>

#include <com/sun/star/embed/ElementModes.hpp>

#include <com/sun/star/embed/XTransactedObject.hpp>

#include <rtl/ustrbuf.hxx>
#include <o3tl/string_view.hxx>

#include <algorithm>

constexpr OUString PATH_SEPARATOR = u"/"_ustr;
#define PATH_SEPARATOR_UNICODE      u'/'

namespace framework
{

StorageHolder::StorageHolder()
{
}

StorageHolder::~StorageHolder()
{
    // TODO implement me
    // dispose/clear etcpp.
}

void StorageHolder::forgetCachedStorages()
{
    std::unique_lock g(m_mutex);
    for (auto & lStorage : m_lStorages)
    {
        TStorageInfo& rInfo = lStorage.second;
        // TODO think about listener !
        rInfo.Storage.clear();
    }
    m_lStorages.clear();
}

void StorageHolder::setRootStorage(const css::uno::Reference< css::embed::XStorage >& xRoot)
{
    std::unique_lock g(m_mutex);
    m_xRoot = xRoot;
}

css::uno::Reference< css::embed::XStorage > StorageHolder::getRootStorage() const
{
    std::unique_lock g(m_mutex);
    return m_xRoot;
}

css::uno::Reference< css::embed::XStorage > StorageHolder::openPath(const OUString& sPath    ,
                                                                          sal_Int32        nOpenMode)
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(sPath);
    std::vector<OUString> lFolders    = StorageHolder::impl_st_parsePath(sNormedPath);

    // SAFE -> ----------------------------------
    std::unique_lock aReadLock(m_mutex);
    css::uno::Reference< css::embed::XStorage > xParent = m_xRoot;
    aReadLock.unlock();
    // <- SAFE ----------------------------------

    css::uno::Reference< css::embed::XStorage > xChild;
    OUString                             sRelPath;

    for (auto const& lFolder : lFolders)
    {
        OUString  sCheckPath (sRelPath + lFolder + PATH_SEPARATOR);

        // SAFE -> ------------------------------
        aReadLock.lock();

        // If we found an already open storage ... we must increase
        // its use count. Otherwise it will may be closed too early :-)
        TPath2StorageInfo::iterator pCheck = m_lStorages.find(sCheckPath);
        TStorageInfo*               pInfo  = nullptr;
        if (pCheck != m_lStorages.end())
        {
            pInfo = &(pCheck->second);
            ++(pInfo->UseCount);
            xChild = pInfo->Storage;

            aReadLock.unlock();
            // <- SAFE ------------------------------
        }
        else
        {
            aReadLock.unlock();
            // <- SAFE ------------------------------

            try
            {
                xChild = StorageHolder::openSubStorageWithFallback(xParent, lFolder, nOpenMode); // TODO think about delegating fallback decision to our own caller!
            }
            catch(const css::uno::RuntimeException&)
                { throw; }
            catch(const css::uno::Exception&)
                {
                    /* TODO URGENT!
                        in case we found some "already existing storages" on the path before and increased its UseCount ...
                        and now we will get an exception on creating a new sub storage ...
                        we must decrease all UseCounts, which was touched before. Otherwise these storages can't be closed!

                        Idea: Using of another structure member "PossibleUseCount" as vector of unique numbers.
                        Every thread use another unique number to identify all "owned candidates".
                        A flush method with the same unique number force increasing of the "UseCount" variable then
                        inside a synchronized block ...
                    */
                    throw;
                }

            std::unique_lock g(m_mutex);
            pInfo = &(m_lStorages[sCheckPath]);
            pInfo->Storage  = xChild;
            pInfo->UseCount = 1;
        }

        xParent   = xChild;
        sRelPath += lFolder + PATH_SEPARATOR;
    }

    // TODO think about return last storage as working storage ... but don't caching it inside this holder!
    // => otherwise the same storage is may be commit more than once.

    return xChild;
}

StorageHolder::TStorageList StorageHolder::getAllPathStorages(const OUString& sPath)
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(sPath);
    std::vector<OUString> lFolders    = StorageHolder::impl_st_parsePath(sNormedPath);

    StorageHolder::TStorageList  lStoragesOfPath;
    OUString              sRelPath;

    std::unique_lock g(m_mutex);

    for (auto const& lFolder : lFolders)
    {
        OUString  sCheckPath (sRelPath + lFolder + PATH_SEPARATOR);

        TPath2StorageInfo::iterator pCheck = m_lStorages.find(sCheckPath);
        if (pCheck == m_lStorages.end())
        {
            // at least one path element was not found
            // Seems that this path isn't open ...
            lStoragesOfPath.clear();
            return lStoragesOfPath;
        }

        TStorageInfo& rInfo = pCheck->second;
        lStoragesOfPath.push_back(rInfo.Storage);

        sRelPath += lFolder + PATH_SEPARATOR;
    }

    return lStoragesOfPath;
}

void StorageHolder::commitPath(const OUString& sPath)
{
    StorageHolder::TStorageList lStorages = getAllPathStorages(sPath);

    css::uno::Reference< css::embed::XTransactedObject > xCommit;
    StorageHolder::TStorageList::reverse_iterator pIt;
    for (  pIt  = lStorages.rbegin(); // order of commit is important ... otherwise changes are not recognized!
           pIt != lStorages.rend();
         ++pIt                      )
    {
        xCommit.set(*pIt, css::uno::UNO_QUERY);
        if (!xCommit.is())
            continue;
        xCommit->commit();
    }

    // SAFE -> ------------------------------
    {
        std::unique_lock aReadLock(m_mutex);
        xCommit.set(m_xRoot, css::uno::UNO_QUERY);
    }
    // <- SAFE ------------------------------

    if (xCommit.is())
        xCommit->commit();
}

void StorageHolder::closePath(const OUString& rPath)
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(rPath);
    std::vector<OUString> lFolders    = StorageHolder::impl_st_parsePath(sNormedPath);

    /* convert list of paths in the following way:
        [0] = "path_1" => "path_1
        [1] = "path_2" => "path_1/path_2"
        [2] = "path_3" => "path_1/path_2/path_3"
    */
    OUString        sParentPath;
    for (auto & lFolder : lFolders)
    {
        OUString sCurrentRelPath(sParentPath + lFolder + PATH_SEPARATOR);
        lFolder = sCurrentRelPath;
        sParentPath = sCurrentRelPath;
    }

    std::unique_lock g(m_mutex);

    std::vector<OUString>::reverse_iterator pIt2;
    for (  pIt2  = lFolders.rbegin();
           pIt2 != lFolders.rend();
         ++pIt2                     )
    {
        const OUString&             sPath = *pIt2;
        TPath2StorageInfo::iterator pPath = m_lStorages.find(sPath);
        if (pPath == m_lStorages.end())
            continue; // ???

        TStorageInfo& rInfo = pPath->second;
        --rInfo.UseCount;
        if (rInfo.UseCount < 1)
        {
            rInfo.Storage.clear();
            m_lStorages.erase(pPath);
        }
    }
}

void StorageHolder::notifyPath(const OUString& sPath)
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(sPath);

    std::unique_lock g(m_mutex);

    TPath2StorageInfo::iterator pIt1 = m_lStorages.find(sNormedPath);
    if (pIt1 == m_lStorages.end())
        return;

    TStorageInfo& rInfo = pIt1->second;
    for (auto const& listener : rInfo.Listener)
    {
        if (listener)
            listener->changesOccurred();
    }
}

void StorageHolder::addStorageListener(      XMLBasedAcceleratorConfiguration* pListener,
                                       const OUString&  sPath    )
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(sPath);

    std::unique_lock g(m_mutex);

    TPath2StorageInfo::iterator pIt1 = m_lStorages.find(sNormedPath);
    if (pIt1 == m_lStorages.end())
        return;

    TStorageInfo& rInfo = pIt1->second;
    TStorageListenerList::iterator pIt2 = ::std::find(rInfo.Listener.begin(), rInfo.Listener.end(), pListener);
    if (pIt2 == rInfo.Listener.end())
        rInfo.Listener.push_back(pListener);
}

void StorageHolder::removeStorageListener(      XMLBasedAcceleratorConfiguration* pListener,
                                          const OUString&  sPath    )
{
    OUString sNormedPath = StorageHolder::impl_st_normPath(sPath);

    std::unique_lock g(m_mutex);

    TPath2StorageInfo::iterator pIt1 = m_lStorages.find(sNormedPath);
    if (pIt1 == m_lStorages.end())
        return;

    TStorageInfo& rInfo = pIt1->second;
    TStorageListenerList::iterator pIt2 = ::std::find(rInfo.Listener.begin(), rInfo.Listener.end(), pListener);
    if (pIt2 != rInfo.Listener.end())
        rInfo.Listener.erase(pIt2);
}

OUString StorageHolder::getPathOfStorage(const css::uno::Reference< css::embed::XStorage >& xStorage)
{
    std::unique_lock g(m_mutex);

    for (auto const& lStorage : m_lStorages)
    {
        const TStorageInfo& rInfo = lStorage.second;
        if (rInfo.Storage == xStorage)
            return lStorage.first;
    }

    return OUString();
}

css::uno::Reference< css::embed::XStorage > StorageHolder::getParentStorage(const css::uno::Reference< css::embed::XStorage >& xChild)
{
    OUString sChildPath = getPathOfStorage(xChild);
    return getParentStorage(sChildPath);
}

css::uno::Reference< css::embed::XStorage > StorageHolder::getParentStorage(const OUString& sChildPath)
{
    // normed path = "a/b/c/" ... we search for "a/b/"
    OUString sNormedPath = StorageHolder::impl_st_normPath(sChildPath);
    std::vector<OUString> lFolders    = StorageHolder::impl_st_parsePath(sNormedPath);
    sal_Int32       c           = lFolders.size();

    // a) ""       => -       => no parent
    // b) "a/b/c/" => "a/b/"  => return storage "a/b/"
    // c) "a/"     => ""      => return root !

    // a)
    if (c < 1)
        return css::uno::Reference< css::embed::XStorage >();

    // SAFE -> ----------------------------------
    {
        std::unique_lock aReadLock(m_mutex);

        // b)
        if (c < 2)
            return m_xRoot;

        // c)
        OUStringBuffer sParentPath(64);
        sal_Int32       i = 0;
        for (i = 0; i < c - 1; ++i)
        {
            sParentPath.append(lFolders[i] + PATH_SEPARATOR);
        }

        auto pParent = m_lStorages.find(sParentPath.makeStringAndClear());
        if (pParent != m_lStorages.end())
            return pParent->second.Storage;
    }
    // <- SAFE ----------------------------------

    // ?
    SAL_INFO("fwk", "StorageHolder::getParentStorage(): Unexpected situation. Cached storage item seems to be wrong.");
    return css::uno::Reference< css::embed::XStorage >();
}

StorageHolder& StorageHolder::operator=(const StorageHolder& rCopy)
{
    std::unique_lock g(m_mutex);
    m_xRoot     = rCopy.m_xRoot;
    m_lStorages = rCopy.m_lStorages;
    return *this;
}

css::uno::Reference< css::embed::XStorage > StorageHolder::openSubStorageWithFallback(const css::uno::Reference< css::embed::XStorage >& xBaseStorage  ,
                                                                                      const OUString&                             sSubStorage   ,
                                                                                            sal_Int32                                    eOpenMode)
{
    // a) try it first with user specified open mode
    //    ignore errors ... but save it for later use!
    try
    {
        css::uno::Reference< css::embed::XStorage > xSubStorage = xBaseStorage->openStorageElement(sSubStorage, eOpenMode);
        if (xSubStorage.is())
            return xSubStorage;
    }
    catch(const css::uno::RuntimeException&)
    {
        throw;
    }
    catch(const css::uno::Exception&)
    {
        // b) readonly already tried? => forward last error!
        if ((eOpenMode & css::embed::ElementModes::WRITE) != css::embed::ElementModes::WRITE) // fallback possible ?
            throw;
    }

    // b) readonly already tried, throw error
    if ((eOpenMode & css::embed::ElementModes::WRITE) != css::embed::ElementModes::WRITE) // fallback possible ?
        throw css::uno::Exception();

    // c) try it readonly
    //    don't catch exception here! Outside code wish to know, if operation failed or not.
    //    Otherwise they work on NULL references ...
    sal_Int32 eNewMode = (eOpenMode & ~css::embed::ElementModes::WRITE);
    css::uno::Reference< css::embed::XStorage > xSubStorage = xBaseStorage->openStorageElement(sSubStorage, eNewMode);
    if (xSubStorage.is())
        return xSubStorage;

    // d) no chance!
    SAL_INFO("fwk", "openSubStorageWithFallback(): Unexpected situation! Got no exception for missing storage ...");
    return css::uno::Reference< css::embed::XStorage >();
}

OUString StorageHolder::impl_st_normPath(const OUString& sPath)
{
    // path must start without "/" but end with "/"!

    OUString sNormedPath = sPath;

    // "/bla" => "bla" && "/" => "" (!)
    (void)sNormedPath.startsWith(PATH_SEPARATOR, &sNormedPath);

    // "/" => "" || "" => "" ?
    if (sNormedPath.isEmpty())
        return OUString();

    // "bla" => "bla/"
    if (sNormedPath.lastIndexOf(PATH_SEPARATOR_UNICODE) != (sNormedPath.getLength()-1))
        sNormedPath += PATH_SEPARATOR;

    return sNormedPath;
}

std::vector<OUString> StorageHolder::impl_st_parsePath(std::u16string_view sPath)
{
    std::vector<OUString> lToken;
    sal_Int32    i  = 0;
    while (true)
    {
        OUString sToken( o3tl::getToken(sPath, 0, PATH_SEPARATOR_UNICODE, i) );
        if (i < 0)
            break;
        lToken.push_back(sToken);
    }
    return lToken;
}

} // namespace framework

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