/************************************************************************
 *
 * Copyright (C) 2021-2022 IRCAD France
 *
 * This file is part of Sight.
 *
 * Sight is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Sight is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Sight. If not, see <https://www.gnu.org/licenses/>.
 *
 ***********************************************************************/

#include "ArchiveReader.hpp"

#include "exception/Read.hpp"

#include "minizip/mz.h"
#include "minizip/mz_os.h"
#include "minizip/mz_strm.h"
#include "minizip/mz_strm_os.h"
#include "minizip/mz_zip.h"
#include "minizip/mz_zip_rw.h"

#include <core/exceptionmacros.hpp>

#include <boost/iostreams/stream.hpp>

#include <fstream>
#include <iostream>

namespace sight::io::zip
{

namespace
{

class RawArchiveReader final : public ArchiveReader
{
public:

    SIGHT_DECLARE_CLASS(RawArchiveReader, ArchiveReader);

    /// Delete default constructors and assignment operators, as we don't want to allow resources duplication
    RawArchiveReader()                                   = delete;
    RawArchiveReader(const RawArchiveReader&)            = delete;
    RawArchiveReader(RawArchiveReader&&)                 = delete;
    RawArchiveReader& operator=(const RawArchiveReader&) = delete;
    RawArchiveReader& operator=(RawArchiveReader&&)      = delete;

    /// Constructor. It open the archive and create all resources needed to access it.
    /// @param archivePath path of the archive file. The file will be kept opened as long as the instance lives.
    RawArchiveReader(const std::filesystem::path& root) :
        ArchiveReader(root),
        m_root(root)
    {
    }

    ~RawArchiveReader() override = default;

    //------------------------------------------------------------------------------

    std::unique_ptr<std::istream> openFile(
        const std::filesystem::path& filePath,
        [[maybe_unused]] const core::crypto::secure_string& password = ""
    ) override
    {
        return std::make_unique<std::ifstream>(m_root / filePath.relative_path(), std::ios::in | std::ios::binary);
    }

    //------------------------------------------------------------------------------

    bool isRaw() const override
    {
        return true;
    }

private:

    /// Path of the root directory
    const std::filesystem::path m_root;
};

class ZipHandle final
{
public:

    /// Delete default constructors and assignment operators, as we don't want to allow resources duplication
    ZipHandle()                            = delete;
    ZipHandle(const ZipHandle&)            = delete;
    ZipHandle(ZipHandle&&)                 = delete;
    ZipHandle& operator=(const ZipHandle&) = delete;
    ZipHandle& operator=(ZipHandle&&)      = delete;

    inline ZipHandle(const std::filesystem::path& archive_path) :
        m_archive_path(archive_path.string())
    {
        // Create zip reader instance
        mz_zip_reader_create(&m_zip_reader);

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot create zip reader instance",
                MZ_MEM_ERROR
            ),
            m_zip_reader == nullptr
        );

        const auto result = mz_zip_reader_open_file(m_zip_reader, m_archive_path.c_str());

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot open archive '" + m_archive_path + "'. Error code: " + std::to_string(result),
                result
            ),
            result != MZ_OK
        );
    }

    inline ~ZipHandle()
    {
        // Close zip handle
        const auto result = mz_zip_reader_close(m_zip_reader);

        // Cleanup
        mz_zip_reader_delete(&m_zip_reader);

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot close writer for archive '"
                + m_archive_path
                + "'. Error code: "
                + std::to_string(result),
                result
            ),
            result != MZ_OK
        );
    }

    // Path to the archive converted to string because on Windows std::filesystem::path.c_str() returns a wchar*
    const std::string m_archive_path;

    // Zip writer handle
    void* m_zip_reader {nullptr};
};

class ZipFileHandle final
{
public:

    /// Delete default constructors and assignment operators, as we don't want to allow resources duplication
    ZipFileHandle()                                = delete;
    ZipFileHandle(const ZipFileHandle&)            = delete;
    ZipFileHandle(ZipFileHandle&&)                 = delete;
    ZipFileHandle& operator=(const ZipFileHandle&) = delete;
    ZipFileHandle& operator=(ZipFileHandle&&)      = delete;

    inline ZipFileHandle(
        std::shared_ptr<ZipHandle> zip_handle,
        const std::filesystem::path& file_path,
        const core::crypto::secure_string& password = ""
    ) :
        m_file_path(file_path.string()),
        m_password(password),
        m_zip_handle(zip_handle)
    {
        // Set encryption
        mz_zip_reader_set_password(m_zip_handle->m_zip_reader, m_password.empty() ? nullptr : m_password.c_str());

        auto result = mz_zip_reader_locate_entry(m_zip_handle->m_zip_reader, m_file_path.c_str(), 0);

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot locate file '"
                + m_file_path
                + "' in archive '"
                + m_zip_handle->m_archive_path
                + "'. Error code: "
                + std::to_string(result),
                result
            ),
            result != MZ_OK
        );

        result = mz_zip_reader_entry_open(m_zip_handle->m_zip_reader);

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot open file '"
                + m_file_path
                + "' from archive '"
                + m_zip_handle->m_archive_path
                + "'. Error code: "
                + std::to_string(result),
                result
            ),
            result != MZ_OK
        );
    }

    inline ~ZipFileHandle()
    {
        const auto result = mz_zip_reader_entry_close(m_zip_handle->m_zip_reader);

        // Restore defaults
        mz_zip_reader_set_password(m_zip_handle->m_zip_reader, nullptr);

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot close file '"
                + m_file_path
                + "' from archive '"
                + m_zip_handle->m_archive_path
                + "'. Error code: "
                + std::to_string(result),
                result
            ),
            result != MZ_OK
        );
    }

private:

    friend class ZipSource;

    // Path to the file converted to string because on Windows std::filesystem::path.c_str() returns a wchar*
    const std::string m_file_path;

    // The password must be kept alive since minizip doesn't copy it
    const core::crypto::secure_string m_password;

    // Zip handles pack which contains the zip writer
    const std::shared_ptr<ZipHandle> m_zip_handle;
};

class ZipSource final
{
public:

    // Needed by Boost
    using char_type = char;
    using category  = boost::iostreams::source_tag;

    // BEWARE: Boost make shallow copies of the ZipSource...
    ZipSource(const std::shared_ptr<ZipFileHandle> zip_file_handle) :
        m_zip_file_handle(zip_file_handle)
    {
    }

    // Boost use this to read things
    std::streamsize read(char* buffer, std::streamsize size)
    {
        const auto read = mz_zip_reader_entry_read(
            m_zip_file_handle->m_zip_handle->m_zip_reader,
            buffer,
            std::int32_t(size)
        );

        SIGHT_THROW_EXCEPTION_IF(
            exception::Read(
                "Cannot read in file '"
                + m_zip_file_handle->m_file_path
                + "' in archive '"
                + m_zip_file_handle->m_zip_handle->m_archive_path
                + "'. Error code: "
                + std::to_string(read),
                read
            ),
            read < 0
        );

        return read;
    }

private:

    const std::shared_ptr<ZipFileHandle> m_zip_file_handle;
};

class ZipArchiveReader final : public ArchiveReader
{
public:

    SIGHT_DECLARE_CLASS(ZipArchiveReader, ArchiveReader);

    /// Delete default constructors and assignment operators, as we don't want to allow resources duplication
    ZipArchiveReader()                                   = delete;
    ZipArchiveReader(const ZipArchiveReader&)            = delete;
    ZipArchiveReader(ZipArchiveReader&&)                 = delete;
    ZipArchiveReader& operator=(const ZipArchiveReader&) = delete;
    ZipArchiveReader& operator=(ZipArchiveReader&&)      = delete;

    inline ZipArchiveReader(const std::filesystem::path& archive_path) :
        ArchiveReader(archive_path),
        m_zip_handle(std::make_shared<ZipHandle>(archive_path))
    {
    }

    ~ZipArchiveReader() override = default;

    //------------------------------------------------------------------------------

    inline std::unique_ptr<std::istream> openFile(
        const std::filesystem::path& file_path,
        const core::crypto::secure_string& password = ""
    ) override
    {
        const auto zip_file_handle = std::make_shared<ZipFileHandle>(
            m_zip_handle,
            file_path,
            password
        );

        return std::make_unique<boost::iostreams::stream<ZipSource> >(zip_file_handle);
    }

    //------------------------------------------------------------------------------

    inline bool isRaw() const override
    {
        return false;
    }

private:

    std::shared_ptr<ZipHandle> m_zip_handle;
};

} // anonymous namespace

ArchiveReader::ArchiveReader(const std::filesystem::path& archive_path) :
    Archive(archive_path)
{
}

//------------------------------------------------------------------------------

ArchiveReader::uptr ArchiveReader::get(
    const std::filesystem::path& archivePath,
    const ArchiveFormat format
)
{
    if(format == ArchiveFormat::FILESYSTEM)
    {
        return std::make_unique<RawArchiveReader>(archivePath);
    }
    else
    {
        return std::make_unique<ZipArchiveReader>(archivePath);
    }
}

} // namespace sight::io::zip
