// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.

#include <algorithm>
#include <cerrno> // for ENOENT
#include <cmath>
#include <ctime> // time()
#include <iterator>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <fmt/core.h>

#include "libtransmission/transmission.h"

#include "libtransmission/block-info.h" // tr_block_info
#include "libtransmission/crypto-utils.h"
#include "libtransmission/error.h"
#include "libtransmission/file.h"
#include "libtransmission/log.h"
#include "libtransmission/makemeta.h"
#include "libtransmission/quark.h" // TR_KEY_length, TR_KEY_a...
#include "libtransmission/session.h" // TR_NAME
#include "libtransmission/torrent-files.h"
#include "libtransmission/tr-assert.h"
#include "libtransmission/tr-strbuf.h" // tr_pathbuf
#include "libtransmission/utils.h" // for _()
#include "libtransmission/variant.h"
#include "libtransmission/version.h"

using namespace std::literals;

namespace
{

namespace find_files_helpers
{

struct TorrentFile
{
    TorrentFile(std::string_view subpath, size_t size)
        : subpath_{ subpath }
        , lowercase_{ tr_strlower(subpath) }
        , size_{ size }
    {
    }

    [[nodiscard]] auto operator<(TorrentFile const& that) const noexcept
    {
        return lowercase_ < that.lowercase_;
    }

    std::string subpath_;
    std::string lowercase_;
    uint64_t size_ = 0;
};

void walkTree(std::string_view const top, std::string_view const subpath, std::set<TorrentFile>& files)
{
    TR_ASSERT(!std::empty(top));
    TR_ASSERT(!std::empty(subpath));

    if (std::empty(top) || std::empty(subpath))
    {
        return;
    }

    auto path = tr_pathbuf{ top, '/', subpath };
    tr_sys_path_native_separators(std::data(path));
    auto error = tr_error{};
    auto const info = tr_sys_path_get_info(path, 0, &error);
    if (error)
    {
        tr_logAddWarn(fmt::format(
            fmt::runtime(_("Skipping '{path}': {error} ({error_code})")),
            fmt::arg("path", path),
            fmt::arg("error", error.message()),
            fmt::arg("error_code", error.code())));
    }
    if (!info)
    {
        return;
    }

    switch (info->type)
    {
    case TR_SYS_PATH_IS_DIRECTORY:
        for (auto const& name : tr_sys_dir_get_files(path))
        {
            if (!std::empty(subpath))
            {
                walkTree(top, tr_pathbuf{ subpath, '/', name }, files);
            }
            else
            {
                walkTree(top, name, files);
            }
        }
        break;

    case TR_SYS_PATH_IS_FILE:
        files.emplace(subpath, info->size);
        break;

    default:
        break;
    }
}

} // namespace find_files_helpers

tr_torrent_files findFiles(std::string_view const top, std::string_view const subpath)
{
    using namespace find_files_helpers;

    auto tmp = std::set<TorrentFile>{};
    walkTree(top, subpath, tmp);
    auto files = tr_torrent_files{};
    for (auto const& file : tmp)
    {
        files.add(file.subpath_, file.size_);
    }
    return files;
}

} // namespace

tr_metainfo_builder::tr_metainfo_builder(std::string_view single_file_or_parent_directory)
    : top_{ single_file_or_parent_directory }
{
    files_ = findFiles(tr_sys_path_dirname(top_), tr_sys_path_basename(top_));
    block_info_ = tr_block_info{ files_.total_size(), default_piece_size(files_.total_size()) };
}

bool tr_metainfo_builder::set_piece_size(uint32_t piece_size) noexcept
{
    if (!is_legal_piece_size(piece_size))
    {
        return false;
    }

    block_info_ = tr_block_info{ files_.total_size(), piece_size };
    return true;
}

bool tr_metainfo_builder::blocking_make_checksums(tr_error* error)
{
    checksum_piece_ = 0;
    cancel_ = false;

    if (total_size() == 0U)
    {
        if (error != nullptr)
        {
            error->set(ENOENT, "zero-length torrents are not allowed"sv);
        }

        return false;
    }

    auto hashes = std::vector<std::byte>(std::size(tr_sha1_digest_t{}) * piece_count());
    auto* walk = std::data(hashes);
    auto sha = tr_sha1{};

    auto file_index = tr_file_index_t{ 0U };
    auto piece_index = tr_piece_index_t{ 0U };
    auto total_remain = total_size();
    auto off = uint64_t{ 0U };

    auto buf = std::vector<char>(piece_size());

    auto const parent = tr_sys_path_dirname(top_);
    auto fd = tr_sys_file_open(
        tr_pathbuf{ parent, '/', path(file_index) },
        TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
        0,
        error);
    if (fd == TR_BAD_SYS_FILE)
    {
        return false;
    }

    while (!cancel_ && (total_remain > 0U))
    {
        checksum_piece_ = piece_index;

        TR_ASSERT(piece_index < piece_count());

        auto const piece_size = block_info_.piece_size(piece_index);
        buf.resize(piece_size);
        auto* bufptr = std::data(buf);

        auto left_in_piece = piece_size;
        while (left_in_piece > 0U)
        {
            auto const n_this_pass = std::min(file_size(file_index) - off, uint64_t{ left_in_piece });
            auto n_read = uint64_t{};

            (void)tr_sys_file_read(fd, bufptr, n_this_pass, &n_read, error);
            bufptr += n_read;
            off += n_read;
            left_in_piece -= n_read;

            if (off == file_size(file_index))
            {
                off = 0;
                tr_sys_file_close(fd);
                fd = TR_BAD_SYS_FILE;

                if (++file_index < file_count())
                {
                    fd = tr_sys_file_open(
                        tr_pathbuf{ parent, '/', path(file_index) },
                        TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
                        0,
                        error);
                    if (fd == TR_BAD_SYS_FILE)
                    {
                        return false;
                    }
                }
            }
        }

        TR_ASSERT(bufptr - std::data(buf) == (int)piece_size);
        TR_ASSERT(left_in_piece == 0);
        sha.add(std::data(buf), std::size(buf));
        auto const digest = sha.finish();
        walk = std::copy(std::begin(digest), std::end(digest), walk);
        sha.clear();

        total_remain -= piece_size;
        ++piece_index;
    }

    TR_ASSERT(cancel_ || size_t(walk - std::data(hashes)) == std::size(hashes));
    TR_ASSERT(cancel_ || total_remain == 0U);

    if (fd != TR_BAD_SYS_FILE)
    {
        tr_sys_file_close(fd);
    }

    if (cancel_)
    {
        if (error != nullptr)
        {
            error->set_from_errno(ECANCELED);
        }

        return false;
    }

    piece_hashes_ = std::move(hashes);
    return true;
}

std::string tr_metainfo_builder::benc(tr_error* error) const
{
    TR_ASSERT_MSG(!std::empty(piece_hashes_), "did you forget to call makeChecksums() first?");

    auto const anonymize = this->anonymize();
    auto const& comment = this->comment();
    auto const& source = this->source();
    auto const& webseeds = this->webseeds();

    if (total_size() == 0)
    {
        if (error != nullptr)
        {
            error->set_from_errno(ENOENT);
        }

        return {};
    }

    auto top = tr_variant::Map{ 8U };

    // add the announce URLs
    announce_list().add_to_map(top);

    // add the webseeds
    if (auto const n_webseeds = std::size(webseeds); n_webseeds > 0U)
    {
        auto webseeds_vec = tr_variant::Vector{};
        webseeds_vec.reserve(n_webseeds);
        std::copy_n(std::cbegin(webseeds), n_webseeds, std::back_inserter(webseeds_vec));
        top.try_emplace(TR_KEY_url_list, std::move(webseeds_vec));
    }

    // add the comment
    if (!std::empty(comment))
    {
        top.try_emplace(TR_KEY_comment, comment);
    }

    // maybe add some optional metainfo
    if (!anonymize)
    {
        top.try_emplace(TR_KEY_created_by, TR_NAME "/" LONG_VERSION_STRING);
        top.try_emplace(TR_KEY_creation_date, time(nullptr));
    }

    top.try_emplace(TR_KEY_encoding, "UTF-8"sv);

    auto info_dict = tr_variant::Map{ 8U };
    auto const base = tr_sys_path_basename(top_);

    // "There is also a key `length` or a key `files`, but not both or neither.
    // If length is present then the download represents a single file,
    // otherwise it represents a set of files which go in a directory structure."
    if (file_count() == 1U && !tr_strv_contains(path(0), '/'))
    {
        info_dict.try_emplace(TR_KEY_length, file_size(0));
    }
    else
    {
        auto const n_files = file_count();
        auto file_vec = tr_variant::Vector{};
        file_vec.reserve(n_files);

        for (tr_file_index_t i = 0U; i < n_files; ++i)
        {
            auto file_map = tr_variant::Map{ 2U };
            file_map.try_emplace(TR_KEY_length, file_size(i));

            auto subpath = std::string_view{ path(i) };
            if (!std::empty(base))
            {
                subpath.remove_prefix(std::size(base) + std::size("/"sv));
            }

            auto path_vec = tr_variant::Vector{};
            auto token = std::string_view{};
            while (tr_strv_sep(&subpath, &token, '/'))
            {
                path_vec.emplace_back(token);
            }
            file_map.try_emplace(TR_KEY_path, std::move(path_vec));

            file_vec.emplace_back(std::move(file_map));
        }

        info_dict.try_emplace(TR_KEY_files, std::move(file_vec));
    }

    if (!std::empty(base))
    {
        info_dict.try_emplace(TR_KEY_name, base);
    }

    info_dict.try_emplace(TR_KEY_piece_length, piece_size());
    info_dict.try_emplace(TR_KEY_pieces, tr_variant::make_raw(std::data(piece_hashes_), std::size(piece_hashes_)));

    if (is_private_)
    {
        info_dict.try_emplace(TR_KEY_private, 1);
    }

    if (!std::empty(source))
    {
        info_dict.try_emplace(TR_KEY_source, source_);
    }

    top.try_emplace(TR_KEY_info, std::move(info_dict));
    return tr_variant_serde::benc().to_string(tr_variant{ std::move(top) });
}

bool tr_metainfo_builder::save(std::string_view filename, tr_error* error) const
{
    return tr_file_save(filename, benc(error), error);
}

uint32_t tr_metainfo_builder::default_piece_size(uint64_t total_size) noexcept
{
    // Ideally, we want approximately 2^10 = 1024 pieces, give or take a few hundred pieces.
    // So we subtract 10 from the log2 of total size.
    // The ideal number of pieces is up for debate.
    auto exp = (total_size > 0U ? std::log2(total_size) : 0) - 10;

    // We want a piece size between 16KiB (2^14 bytes) and 16MiB (2^24 bytes) for maximum compatibility
    exp = std::clamp(exp, 14., 24.);

    return uint32_t{ 1U } << std::lround(exp);
}
