/* -*- 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/.
 */

#include <WPXSvInputStream.hxx>

#include <com/sun/star/packages/zip/XZipFileAccess2.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>

#include <comphelper/processfactory.hxx>
#include <comphelper/seekableinput.hxx>
#include <o3tl/safeint.hxx>
#include <rtl/string.hxx>
#include <sal/log.hxx>

#include <unotools/streamwrap.hxx>
#include <unotools/ucbstreamhelper.hxx>

#include <climits>
#include <limits>
#include <memory>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

namespace writerperfect
{
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::io;

namespace container = css::container;
namespace packages = css::packages;

namespace
{
class PositionHolder
{
public:
    explicit PositionHolder(const Reference<XSeekable>& rxSeekable);
    ~PositionHolder();
    PositionHolder(const PositionHolder&) = delete;
    PositionHolder& operator=(const PositionHolder&) = delete;

private:
    const Reference<XSeekable> mxSeekable;
    const sal_uInt64 mnPosition;
};
} // anonymous namespace

PositionHolder::PositionHolder(const Reference<XSeekable>& rxSeekable)
    : mxSeekable(rxSeekable)
    , mnPosition(rxSeekable->getPosition())
{
}

PositionHolder::~PositionHolder()
{
    try
    {
        mxSeekable->seek(mnPosition);
    }
    catch (...)
    {
    }
}

static OUString lcl_normalizeSubStreamPath(const OUString& rPath)
{
    // accept paths which begin by '/'
    // TODO: maybe this should do a full normalization
    if (rPath.startsWith("/") && rPath.getLength() >= 2)
        return rPath.copy(1);
    return rPath;
}

static OUString concatPath(std::u16string_view lhs, const OUString& rhs)
{
    if (lhs.empty())
        return rhs;
    return OUString::Concat(lhs) + "/" + rhs;
}

namespace
{
struct OLEStreamData
{
    OLEStreamData(OString aName, OString rvngName)
        : name(std::move(aName))
        , RVNGname(std::move(rvngName))
    {
    }

    rtl::Reference<SotStorageStream> stream;

    /** Name of the stream.
      *
      * This is not @c OUString, because we need to be able to
      * produce const char* from it.
      */
    OString name;
    /** librevenge name of the stream.
      *
      * This is not @c OUString, because we need to be able to
      * produce const char* from it.
      */
    OString RVNGname;
};
} // anonymous namespace

typedef std::unordered_map<OUString, std::size_t> NameMap_t;
typedef std::unordered_map<OUString, rtl::Reference<SotStorage>> OLEStorageMap_t;

/** Representation of an OLE2 storage.
  *
  * This class tries to bring a bit of sanity to use of SotStorage with
  * respect to the needs of @c librevenge::RVNGInputStream API. It
  * holds all nested storages for the whole lifetime (more precisely,
  * since initialization, which is performed by calling @c
  * initialize()), thus ensuring that no created stream is destroyed
  * just because its parent storage went out of scope. It also holds a
  * bidirectional map of stream names to their indexes (index of a
  * stream is determined by deep-first traversal), which is also
  * populated during initialization (member variables @c maStreams and
  * @c maNameMap).
  *
  * Streams are created on demand (and saved, for the same reason as
  * storages).
  */
struct OLEStorageImpl
{
    OLEStorageImpl();

    void initialize(std::unique_ptr<SvStream> pStream);

    rtl::Reference<SotStorageStream> getStream(const OUString& rPath);
    rtl::Reference<SotStorageStream> const& getStream(std::size_t nId);

private:
    void traverse(const rtl::Reference<SotStorage>& rStorage, std::u16string_view rPath);

    rtl::Reference<SotStorageStream> createStream(const OUString& rPath);

public:
    rtl::Reference<SotStorage> mxRootStorage; //< root storage of the OLE2
    OLEStorageMap_t maStorageMap; //< map of all sub storages by name
    ::std::vector<OLEStreamData> maStreams; //< list of streams and their names
    NameMap_t maNameMap; //< map of stream names to indexes (into @c maStreams)
    bool mbInitialized;
};

OLEStorageImpl::OLEStorageImpl()
    : mbInitialized(false)
{
}

void OLEStorageImpl::initialize(std::unique_ptr<SvStream> pStream)
{
    if (!pStream)
        return;

    mxRootStorage = new SotStorage(pStream.release(), true);

    traverse(mxRootStorage, u"");

    mbInitialized = true;
}

rtl::Reference<SotStorageStream> OLEStorageImpl::getStream(const OUString& rPath)
{
    const OUString aPath(lcl_normalizeSubStreamPath(rPath));
    NameMap_t::iterator aIt = maNameMap.find(aPath);

    // For the while don't return stream in this situation.
    // Later, given how libcdr's zip stream implementation behaves,
    // return the first stream in the storage if there is one.
    if (maNameMap.end() == aIt)
        return rtl::Reference<SotStorageStream>();

    if (!maStreams[aIt->second].stream.is())
        maStreams[aIt->second].stream
            = createStream(OStringToOUString(maStreams[aIt->second].name, RTL_TEXTENCODING_UTF8));

    return maStreams[aIt->second].stream;
}

rtl::Reference<SotStorageStream> const& OLEStorageImpl::getStream(const std::size_t nId)
{
    if (!maStreams[nId].stream.is())
        maStreams[nId].stream
            = createStream(OStringToOUString(maStreams[nId].name, RTL_TEXTENCODING_UTF8));

    return maStreams[nId].stream;
}

void OLEStorageImpl::traverse(const rtl::Reference<SotStorage>& rStorage, std::u16string_view rPath)
{
    SvStorageInfoList infos;

    rStorage->FillInfoList(&infos);

    for (const auto& info : infos)
    {
        if (info.IsStream())
        {
            OUString baseName = info.GetName(), rvngName = baseName;
            // librevenge::RVNGOLEStream ignores the first character when is a control code, so ...
            if (!rvngName.isEmpty() && rvngName.toChar() < 32)
                rvngName = rvngName.copy(1);
            maStreams.emplace_back(
                OUStringToOString(concatPath(rPath, baseName), RTL_TEXTENCODING_UTF8),
                OUStringToOString(concatPath(rPath, rvngName), RTL_TEXTENCODING_UTF8));
            maNameMap[concatPath(rPath, rvngName)] = maStreams.size() - 1;
        }
        else if (info.IsStorage())
        {
            const OUString aPath = concatPath(rPath, info.GetName());
            rtl::Reference<SotStorage> aStorage
                = rStorage->OpenSotStorage(info.GetName(), StreamMode::STD_READ);
            maStorageMap[aPath] = aStorage;

            // deep-first traversal
            traverse(aStorage, aPath);
        }
        else
        {
            SAL_WARN("writerperfect",
                     "OLEStorageImpl::traverse: invalid storage entry, neither stream nor file");
        }
    }
}

rtl::Reference<SotStorageStream> OLEStorageImpl::createStream(const OUString& rPath)
{
    const sal_Int32 nDelim = rPath.lastIndexOf(u'/');

    if (-1 == nDelim)
        return mxRootStorage->OpenSotStream(rPath, StreamMode::STD_READ);

    const OUString aDir = rPath.copy(0, nDelim);
    const OUString aName = rPath.copy(nDelim + 1);

    const OLEStorageMap_t::const_iterator aIt = maStorageMap.find(aDir);

    if (maStorageMap.end() == aIt)
        return nullptr;

    return aIt->second->OpenSotStream(aName, StreamMode::STD_READ);
}

namespace
{
struct ZipStreamData
{
    explicit ZipStreamData(OString aName);

    css::uno::Reference<css::io::XInputStream> xStream;

    /** Name of the stream.
      *
      * This is not @c OUString, because we need to be able to
      * produce const char* from it.
      */
    OString aName;
};
} // anonymous namespace

/** Representation of a Zip storage.
  *
  * This is quite similar to OLEStorageImpl, except that we do not need
  * to keep all storages (folders) open.
  */
struct ZipStorageImpl
{
    explicit ZipStorageImpl(const css::uno::Reference<css::container::XNameAccess>& rxContainer);

    /** Initialize for access.
      *
      * This creates a bidirectional map of stream names to their
      * indexes (index of a stream is determined by deep-first
      * traversal).
      */
    void initialize();

    Reference<css::io::XInputStream> getStream(const OUString& rPath);
    Reference<css::io::XInputStream> const& getStream(std::size_t nId);

private:
    void traverse(const Reference<container::XNameAccess>& rxEnum);

    Reference<XInputStream> createStream(const OUString& rPath);

public:
    Reference<container::XNameAccess> mxContainer; //< root of the Zip
    ::std::vector<ZipStreamData> maStreams; //< list of streams and their names
    NameMap_t maNameMap; //< map of stream names to indexes (into @c maStreams)
    bool mbInitialized;
};

ZipStreamData::ZipStreamData(OString _aName)
    : aName(std::move(_aName))
{
}

ZipStorageImpl::ZipStorageImpl(const Reference<container::XNameAccess>& rxContainer)
    : mxContainer(rxContainer)
    , mbInitialized(false)
{
    assert(mxContainer.is());
}

void ZipStorageImpl::initialize()
{
    traverse(mxContainer);

    mbInitialized = true;
}

Reference<XInputStream> ZipStorageImpl::getStream(const OUString& rPath)
{
    const OUString aPath(lcl_normalizeSubStreamPath(rPath));
    NameMap_t::iterator aIt = maNameMap.find(aPath);

    // For the while don't return stream in this situation.
    // Later, given how libcdr's zip stream implementation behaves,
    // return the first stream in the storage if there is one.
    if (maNameMap.end() == aIt)
        return Reference<XInputStream>();

    if (!maStreams[aIt->second].xStream.is())
        maStreams[aIt->second].xStream = createStream(aPath);

    return maStreams[aIt->second].xStream;
}

Reference<XInputStream> const& ZipStorageImpl::getStream(const std::size_t nId)
{
    if (!maStreams[nId].xStream.is())
        maStreams[nId].xStream
            = createStream(OStringToOUString(maStreams[nId].aName, RTL_TEXTENCODING_UTF8));

    return maStreams[nId].xStream;
}

void ZipStorageImpl::traverse(const Reference<container::XNameAccess>& rxContainer)
{
    const Sequence<OUString> lNames = rxContainer->getElementNames();

    maStreams.reserve(lNames.getLength());

    for (const auto& rName : lNames)
    {
        if (!rName.endsWith("/")) // skip dirs
        {
            maStreams.emplace_back(OUStringToOString(rName, RTL_TEXTENCODING_UTF8));
            maNameMap[rName] = maStreams.size() - 1;
        }
    }
}

Reference<XInputStream> ZipStorageImpl::createStream(const OUString& rPath)
{
    Reference<XInputStream> xStream;

    try
    {
        const Reference<XInputStream> xInputStream(mxContainer->getByName(rPath), UNO_QUERY);
        if (!xInputStream)
            return xStream;
        const Reference<XSeekable> xSeekable(xInputStream, UNO_QUERY);

        if (xSeekable.is())
            xStream = xInputStream;
        else
            xStream.set(new comphelper::OSeekableInputWrapper(
                xInputStream, comphelper::getProcessComponentContext()));
    }
    catch (const Exception&)
    {
        // nothing needed
    }

    return xStream;
}

WPXSvInputStream::WPXSvInputStream(Reference<XInputStream> const& xStream)
    : mxStream(xStream)
    , mxSeekable(xStream, UNO_QUERY)
    , maData(0)
    , mnLength(0)
    , mnReadBufferPos(0)
    , mbCheckedOLE(false)
    , mbCheckedZip(false)
{
    if (!xStream.is() || !mxStream.is())
        mnLength = 0;
    else
    {
        if (!mxSeekable.is())
            mnLength = 0;
        else
        {
            try
            {
                mnLength = mxSeekable->getLength();
                if (0 < mxSeekable->getPosition())
                    mxSeekable->seek(0);
            }
            catch (...)
            {
                SAL_WARN("writerperfect", "mnLength = mxSeekable->getLength() threw exception");
                mnLength = 0;
            }
        }
    }
}

const unsigned char* WPXSvInputStream::read(unsigned long numBytes, unsigned long& numBytesRead)
{
    numBytesRead = 0;

    if (numBytes == 0 || isEnd())
        return nullptr;

    numBytesRead = mxStream->readSomeBytes(maData, numBytes);
    if (numBytesRead == 0)
        return nullptr;

    return reinterpret_cast<const unsigned char*>(maData.getConstArray());
}

tools::Long WPXSvInputStream::tellImpl()
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return -1;
    else
    {
        const sal_Int64 tmpPosition = mxSeekable->getPosition();
        if (tmpPosition < 0)
            return -1;
#if SAL_TYPES_SIZEOFLONG == 4
        if (tmpPosition > LONG_MAX)
            return -1;
#endif
        return static_cast<tools::Long>(tmpPosition);
    }
}

int WPXSvInputStream::seek(tools::Long offset)
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return -1;

    const sal_Int64 tmpPosition = mxSeekable->getPosition();
    if (tmpPosition < 0)
        return -1;

    try
    {
        mxSeekable->seek(offset);
    }
    catch (...)
    {
        SAL_WARN("writerperfect", "mxSeekable->seek(offset) threw exception");
        return -1;
    }

    return 0;
}

bool WPXSvInputStream::isStructured()
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return false;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    if (isOLE())
        return true;

    mxSeekable->seek(0);

    return isZip();
}

unsigned WPXSvInputStream::subStreamCount()
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return 0;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    if (isOLE())
    {
        ensureOLEIsInitialized();

        return mpOLEStorage->maStreams.size();
    }

    mxSeekable->seek(0);

    if (isZip())
    {
        ensureZipIsInitialized();

        return mpZipStorage->maStreams.size();
    }

    return 0;
}

const char* WPXSvInputStream::subStreamName(const unsigned id)
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return nullptr;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    if (isOLE())
    {
        ensureOLEIsInitialized();

        if (mpOLEStorage->maStreams.size() <= id)
            return nullptr;

        return mpOLEStorage->maStreams[id].RVNGname.getStr();
    }

    mxSeekable->seek(0);

    if (isZip())
    {
        ensureZipIsInitialized();

        if (mpZipStorage->maStreams.size() <= id)
            return nullptr;

        return mpZipStorage->maStreams[id].aName.getStr();
    }

    return nullptr;
}

bool WPXSvInputStream::existsSubStream(const char* const name)
{
    if (!name)
        return false;

    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return false;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    const OUString aName(OStringToOUString(std::string_view(name), RTL_TEXTENCODING_UTF8));

    if (isOLE())
    {
        ensureOLEIsInitialized();
        return mpOLEStorage->maNameMap.contains(aName);
    }

    mxSeekable->seek(0);

    if (isZip())
    {
        ensureZipIsInitialized();
        return mpZipStorage->maNameMap.contains(aName);
    }

    return false;
}

librevenge::RVNGInputStream* WPXSvInputStream::getSubStreamByName(const char* const name)
{
    if (!name)
        return nullptr;

    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return nullptr;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    const OUString aName(OStringToOUString(std::string_view(name), RTL_TEXTENCODING_UTF8));

    if (isOLE())
    {
        ensureOLEIsInitialized();
        return createWPXStream(mpOLEStorage->getStream(aName));
    }

    mxSeekable->seek(0);

    if (isZip())
    {
        ensureZipIsInitialized();

        try
        {
            return createWPXStream(mpZipStorage->getStream(aName));
        }
        catch (const Exception&)
        {
            // nothing needed
        }
    }

    return nullptr;
}

librevenge::RVNGInputStream* WPXSvInputStream::getSubStreamById(const unsigned id)
{
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return nullptr;

    PositionHolder pos(mxSeekable);
    mxSeekable->seek(0);

    if (isOLE())
    {
        ensureOLEIsInitialized();

        if (mpOLEStorage->maStreams.size() <= id)
            return nullptr;

        return createWPXStream(mpOLEStorage->getStream(id));
    }

    mxSeekable->seek(0);

    if (isZip())
    {
        ensureZipIsInitialized();

        if (mpZipStorage->maStreams.size() <= id)
            return nullptr;

        try
        {
            return createWPXStream(mpZipStorage->getStream(id));
        }
        catch (const Exception&)
        {
            // nothing needed
        }
    }
    return nullptr;
}

librevenge::RVNGInputStream*
WPXSvInputStream::createWPXStream(const rtl::Reference<SotStorageStream>& rxStorage)
{
    if (rxStorage.is())
    {
        Reference<XInputStream> xContents(new utl::OSeekableInputStreamWrapper(rxStorage.get()));
        return new WPXSvInputStream(xContents);
    }
    return nullptr;
}

librevenge::RVNGInputStream*
WPXSvInputStream::createWPXStream(const Reference<XInputStream>& rxStream)
{
    if (rxStream.is())
        return new WPXSvInputStream(rxStream);
    else
        return nullptr;
}

bool WPXSvInputStream::isOLE()
{
    if (!mbCheckedOLE)
    {
        assert(0 == mxSeekable->getPosition());

        std::unique_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(mxStream));
        if (pStream && SotStorage::IsOLEStorage(pStream.get()))
            mpOLEStorage.reset(new OLEStorageImpl());

        mbCheckedOLE = true;
    }

    return bool(mpOLEStorage);
}

bool WPXSvInputStream::isZip()
{
    if (!mbCheckedZip)
    {
        assert(0 == mxSeekable->getPosition());

        try
        {
            const Reference<XComponentContext> xContext(comphelper::getProcessComponentContext(),
                                                        UNO_SET_THROW);
            const Reference<packages::zip::XZipFileAccess2> xZip(
                xContext->getServiceManager()->createInstanceWithArgumentsAndContext(
                    u"com.sun.star.packages.zip.ZipFileAccess"_ustr, { Any(mxStream) }, xContext),
                UNO_QUERY_THROW);
            mpZipStorage.reset(new ZipStorageImpl(xZip));
        }
        catch (const Exception&)
        {
            // ignore
        }

        mbCheckedZip = true;
    }

    return bool(mpZipStorage);
}

void WPXSvInputStream::ensureOLEIsInitialized()
{
    assert(mpOLEStorage);

    if (!mpOLEStorage->mbInitialized)
        mpOLEStorage->initialize(utl::UcbStreamHelper::CreateStream(mxStream));
}

void WPXSvInputStream::ensureZipIsInitialized()
{
    assert(mpZipStorage);

    if (!mpZipStorage->mbInitialized)
        mpZipStorage->initialize();
}

WPXSvInputStream::~WPXSvInputStream() {}

long WPXSvInputStream::tell()
{
    // coverity[tainted_data_return : FALSE] retVal is considered safe now
    tools::Long retVal = tellImpl();
    if (retVal < 0)
        return -1;
    return retVal + static_cast<tools::Long>(mnReadBufferPos);
}

int WPXSvInputStream::seek(long offset, librevenge::RVNG_SEEK_TYPE seekType)
{
    sal_Int64 tmpOffset = offset;
    if (seekType == librevenge::RVNG_SEEK_CUR)
        tmpOffset += tell();
    if (seekType == librevenge::RVNG_SEEK_END)
        tmpOffset += mnLength;

    int retVal = 0;
    if (tmpOffset < 0)
    {
        tmpOffset = 0;
        retVal = -1;
    }
    if (tmpOffset > mnLength)
    {
        tmpOffset = mnLength;
        retVal = -1;
    }

    if (tmpOffset < tellImpl() && o3tl::make_unsigned(tmpOffset) >= o3tl::make_unsigned(tellImpl()))
    {
        mnReadBufferPos = static_cast<unsigned long>(tmpOffset - tellImpl());
        return retVal;
    }

    if (seek(tmpOffset))
        return -1;
    return retVal;
}

bool WPXSvInputStream::isEnd()
{
    if (mnReadBufferPos != 0)
        return false;
    if ((mnLength == 0) || !mxStream.is() || !mxSeekable.is())
        return true;
    return (mxSeekable->getPosition() >= mnLength);
}
}

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