#include "LoadHandler.h"

#include <algorithm>      // for copy
#include <array>          // for array
#include <cmath>          // for isnan
#include <cstddef>        // for byte
#include <cstdlib>        // for atoi, size_t
#include <cstring>        // for strcmp, strlen
#include <iterator>       // for back_inserter
#include <memory>         // for make_unique, make_shared...
#include <optional>       // for optional
#include <ranges>         // for take
#include <regex>          // for regex_search, match_results
#include <stdexcept>      // for runtime_error
#include <string>         // for string
#include <string_view>    // for string_view
#include <unordered_map>  // for unordered_map
#include <utility>        // for move, forward, exchange
#include <vector>         // for vector

#include <gio/gio.h>     // for g_file_get_path, g_fil...
#include <glib.h>        // for g_message...
#include <glibconfig.h>  // for gssize...
#include <zip.h>         // for zip_file_t, zip_fopen,...
#include <zipconf.h>     // for zip_int64_t, zip_uint64_t

#include "control/xojfile/XmlParser.h"  // for XmlParser
#include "model/BackgroundImage.h"      // for BackgroundImage
#include "model/Document.h"             // for Document
#include "model/Font.h"                 // for XojFont
#include "model/Image.h"                // for Image
#include "model/Layer.h"                // for Layer
#include "model/Link.h"                 // for Link
#include "model/PageType.h"             // for PageType, PageTypeFormat
#include "model/Point.h"                // for Point
#include "model/Stroke.h"               // for Stroke, StrokeCapStyle
#include "model/TexImage.h"             // for TexImage
#include "model/Text.h"                 // for Text
#include "model/XojPage.h"              // for XojPage
#include "util/Assert.h"                // for xoj_assert
#include "util/Color.h"                 // for Color
#include "util/GzInputStream.h"         // for GzInputStream
#include "util/LoopUtil.h"              // for for_first_then_each
#include "util/PathUtil.h"              // for PathStorageMode
#include "util/StringUtils.h"           // for char_cast
#include "util/ZipInputStream.h"        // for ZipInputStream
#include "util/i18n.h"                  // for _F, FS, _
#include "util/raii/CLibrariesSPtr.h"   // for adopt
#include "util/raii/GLibGuards.h"       // for GErrorGuard
#include "util/raii/GObjectSPtr.h"      // for GObjectSPtr

#include "filesystem.h"  // for path, is_regular_file


namespace {
constexpr size_t MAX_VERSION_LENGTH = 50;
constexpr size_t MAX_MIMETYPE_LENGTH = 25;

struct zip_file_deleter {
    void operator()(zip_file_t* ptr) noexcept { zip_fclose(ptr); }
};
}  // namespace

using zip_file_wrapper = std::unique_ptr<zip_file_t, zip_file_deleter>;

LoadHandler::LoadHandler(std::vector<std::string>* errorMessages):
        parsingComplete(false),
        errorMessages(errorMessages),
        attachedPdfMissing(false),
        pdfFilenameParsed(false),
        fileVersion(0),
        minimalFileVersion(0),
        isGzFile(false) {}

LoadHandler::~LoadHandler() = default;

auto LoadHandler::isAttachedPdfMissing() const -> bool { return this->attachedPdfMissing; }

auto LoadHandler::getMissingPdfFilename() const -> const fs::path& { return this->missingPdf; }

auto LoadHandler::getFileVersion() const -> int { return this->fileVersion; }

void LoadHandler::addDocument(std::u8string creator, int fileVersion) {
    this->creator = std::move(creator);
    if (this->isGzFile) {
        this->fileVersion = fileVersion;
    }

    this->doc = std::make_unique<Document>(&this->dHandler);
}

void LoadHandler::finalizeDocument() {
    xoj_assert(this->doc);

    this->doc->addPages(this->pages.begin(), this->pages.end());
    this->pages.clear();
    this->parsingComplete = true;
}

void LoadHandler::addPage(double width, double height) {
    xoj_assert(!this->page);

    this->page = std::make_shared<XojPage>(width, height, /*suppressLayerCreation*/ true);
    this->pages.emplace_back(this->page);
}

void LoadHandler::finalizePage() {
    xoj_assert(this->page);

    // Handle unnecessary layer insertion in case of existing layers in file
    if (this->page->getLayerCount() == 0) {
        this->page->addLayer(new Layer{});
    }
    // this->pages already holds a reference to the page
    this->page.reset();
}

void LoadHandler::addAudioAttachment(const fs::path& filename) {
    // Verify if attachment exists and is valid
    zip_stat_t attachmentFileStat;
    const int statStatus = zip_stat(this->zipFp.get(), char_cast(filename.u8string().c_str()), 0, &attachmentFileStat);
    if (statStatus != 0) {
        logError(FS(_F("Could not open attachment: {1}. Error message: {2}") % filename.u8string() %
                    zip_error_strerror(zip_get_error(this->zipFp.get()))));
        return;
    }

    zip_int64_t length = 0;
    if (attachmentFileStat.valid & ZIP_STAT_SIZE) {
        length = static_cast<zip_int64_t>(attachmentFileStat.size);
    } else {
        logError(FS(_F("Could not open attachment: {1}. Error message: No valid file size provided") %
                    filename.u8string()));
        return;
    }

    auto attachmentFile = zip_file_wrapper{zip_fopen(this->zipFp.get(), char_cast(filename.u8string().c_str()), 0)};

    if (!attachmentFile) {
        logError(FS(_F("Could not open attachment: {1}. Error message: {2}") % filename.u8string() %
                    zip_error_strerror(zip_get_error(this->zipFp.get()))));
        return;
    }

    // Extract audio to temporary file
    GFileIOStream* fileStream = nullptr;
    const xoj::util::GObjectSPtr<GFile> tmpFile(g_file_new_tmp("xournal_audio_XXXXXX.tmp", &fileStream, nullptr),
                                                xoj::util::adopt);
    if (!tmpFile) {
        logError(_("Unable to create temporary file for audio attachment"));
        return;
    }

    GOutputStream* outputStream = g_io_stream_get_output_stream(G_IO_STREAM(fileStream));

    auto data = std::make_unique<std::array<std::byte, 1024>>();
    zip_int64_t readBytes = 0;
    while (readBytes < length) {
        const zip_int64_t read = zip_fread(attachmentFile.get(), data->data(), data->size());
        if (read == -1) {
            logError(
                    FS(_F("Could not open attachment: {1}. Error message: Could not read file") % filename.u8string()));
            return;
        }

        const gboolean writeSuccessful = g_output_stream_write_all(outputStream, data->data(), static_cast<gsize>(read),
                                                                   nullptr, nullptr, nullptr);
        if (!writeSuccessful) {
            logError(FS(_F("Could not open attachment: {1}. Error message: Could not write temporary file") %
                        filename.u8string()));
            return;
        }

        readBytes += read;
    }

    // Map the filename to the extracted temporary file path
    const char* tmpPath = g_file_peek_path(tmpFile.get());
    this->audioFiles[filename] = tmpPath;
}

void LoadHandler::setBgName(const std::string& name) {
    xoj_assert(!name.empty());
    this->page->setBackgroundName(name);
}

void LoadHandler::setBgSolid(const PageType& bg, Color color) {
    this->page->setBackgroundType(bg);
    this->page->setBackgroundColor(color);
}

void LoadHandler::setBgPixmap(bool attach, const fs::path& filename) {
    BackgroundImage img;

    if (this->isGzFile || !attach) {
        const fs::path fileToLoad = getAbsoluteFilepath(filename, attach);

        xoj::util::GErrorGuard error{};
        img.loadFile(fileToLoad, xoj::util::out_ptr(error));

        if (error) {
            logError(FS(_F("Could not read image: {1}. Error message: {2}") % fileToLoad.u8string() % error->message));
        }
    } else {
        // The image is stored in an attachment inside the zip archive
        const auto readResult = readZipAttachment(filename);  // Do not remove the const qualifier - see below
        if (!readResult) {
            return;
        }
        const std::string& imgData = *readResult;  // Do not remove the const qualifier - see below

        /**
         * To avoid an unnecessary copy, the data is still managed by the std::unique_ptr<std::string> instance. The
         * input stream assumes the data will not be modified: do not remove the const qualifier on readResult or
         * imgData
         */
        const xoj::util::GObjectSPtr<GInputStream> inputStream{
                g_memory_input_stream_new_from_data(imgData.data(), static_cast<gssize>(imgData.size()), nullptr),
                xoj::util::adopt};

        xoj::util::GErrorGuard error{};
        img.loadFile(inputStream.get(), filename, xoj::util::out_ptr(error));

        g_input_stream_close(inputStream.get(), nullptr, nullptr);

        if (error) {
            logError(FS(_F("Could not read image: {1}. Error message: {2}") % filename.u8string() % error->message));
        }
    }
    img.setAttach(attach);
    this->page->setBackgroundImage(img);
    this->page->setBackgroundType(PageType{PageTypeFormat::Image});
}

void LoadHandler::setBgPixmapCloned(size_t pageNr) {
    if (pageNr >= this->doc->getPageCount()) {
        logError(FS(_F("Cloned pixmap page background on page {1} references inexistent page {2}") %
                    this->doc->getPageCount() % pageNr));
        return;
    }

    auto p = this->doc->getPage(pageNr);
    if (p) {
        this->page->setBackgroundImage(p->getBackgroundImage());
    }
    this->page->setBackgroundType(PageType{PageTypeFormat::Image});
}

void LoadHandler::setBgPdf(size_t pageno) { this->page->setBackgroundPdfPageNr(pageno); }

void LoadHandler::loadBgPdf(bool attach, const fs::path& filename) {
    if (this->isGzFile || !attach) {
        // Resolve correct file path
        const fs::path pdfFilepath = getAbsoluteFilepath(filename, attach);

        // pdfFilepath should point to the background PDF file
        if (fs::is_regular_file(pdfFilepath)) {
            this->doc->readPdf(pdfFilepath, false, attach);

            if (!this->doc->getLastErrorMsg().empty()) {
                logError(FS(_F("Error reading PDF: {1}") % this->doc->getLastErrorMsg()));
            }
        } else {
            // Even if loading the PDF failed, tell the document what should
            // have been loaded so the background is not silently removed on save
            this->doc->setPdfAttributes(pdfFilepath, attach);
            if (attach) {
                this->attachedPdfMissing = true;
            } else {
                this->missingPdf = pdfFilepath;
            }
        }
    } else {
        // filename should point to an attachment inside the zip archive
        auto pdfBytes = readZipAttachment(filename);
        if (pdfBytes) {
            this->doc->readPdf(filename, false, attach, std::move(pdfBytes));

            if (!this->doc->getLastErrorMsg().empty()) {
                logError(FS(_F("Error reading PDF: {1}") % this->doc->getLastErrorMsg()));
            }
        } else {
            this->doc->setPdfAttributes(filename, true);
            this->attachedPdfMissing = true;
        }
    }
    this->pdfFilenameParsed = true;
}

void LoadHandler::addLayer(const std::optional<std::string_view>& name) {
    xoj_assert(!this->layer);
    this->layer = std::make_unique<Layer>();

    if (name) {
        this->layer->setName(std::string{*name});
    }
}

void LoadHandler::finalizeLayer() {
    xoj_assert(this->layer);
    this->page->addLayer(this->layer.release());
}

void LoadHandler::addStroke(StrokeTool tool, Color color, double width, int fill, StrokeCapStyle capStyle,
                            const LineStyle& lineStyle, fs::path filename, size_t timestamp) {
    xoj_assert(!this->stroke);
    this->stroke = std::make_unique<Stroke>();

    this->stroke->setToolType(tool);
    this->stroke->setColor(color);
    this->stroke->setWidth(width);
    this->stroke->setFill(fill);
    this->stroke->setStrokeCapStyle(capStyle);
    this->stroke->setLineStyle(lineStyle);

    setAudioAttributes(*this->stroke, std::move(filename), timestamp);
}

void LoadHandler::setStrokePoints(std::vector<Point> pointVector, bool hasPressure) {
    // Check if stroke still exists or has already been assigned points.
    // In corrupt files, this function may be called more than once, and the
    // stroke may have been deleted in a previous call.
    if (!this->stroke || !this->stroke->getPointVector().empty()) {
        return;
    }

    if (hasPressure && !std::ranges::all_of(
                               pointVector | std::views::take(pointVector.size() - 1),
                               [](double pressure) { return pressure > 0; }, &Point::z)) {
        // Warning: this may delete this->stroke if no positive pressure values are provided
        // Do not dereference this->stroke after that without checking first
        fixNullPressureValues(std::move(pointVector));
    } else {
        this->stroke->setPointVector(std::move(pointVector));
    }
}

void LoadHandler::finalizeStroke() {
    // fixNullPressureValues() may have deleted the stroke. Check if it still
    // exists before accessing it.
    if (this->stroke) {
        if (this->stroke->getPointCount() < 2) {
            // Strokes with less than two points can't be drawn
            g_warning("LoadHandler: Ignoring stroke with less than two points");
            this->stroke.reset();
            return;
        }

        this->layer->addElement(std::move(this->stroke));
    }
}

void LoadHandler::addText(std::string font, double size, double x, double y, Color color, std::optional<double> wrap,
                          fs::path filename, size_t timestamp) {
    xoj_assert(!this->text);
    this->text = std::make_unique<Text>();

    XojFont& f = this->text->getFont();
    f.setName(std::move(font));
    f.setSize(size);
    this->text->setX(x);
    this->text->setY(y);
    this->text->setColor(color);
    this->text->setWrap(wrap.value_or(Text::NO_WRAP));

    setAudioAttributes(*this->text, std::move(filename), timestamp);
}

void LoadHandler::setTextContents(std::string contents) {
    xoj_assert(this->text);

    this->text->setText(std::move(contents));
}

void LoadHandler::finalizeText() {
    xoj_assert(this->text);

    this->layer->addElement(std::move(this->text));
}

void LoadHandler::addImage(double left, double top, double right, double bottom) {
    xoj_assert(!this->image);
    this->image = std::make_unique<Image>();

    this->image->setX(left);
    this->image->setY(top);
    this->image->setWidth(right - left);
    this->image->setHeight(bottom - top);
}

void LoadHandler::setImageData(std::string data) {
    xoj_assert(this->image);

    if (this->image->hasData() && !data.empty()) {
        g_warning("LoadHandler: Image data section found, but the image already has data");
    }

    this->image->setImage(std::move(data));
}

void LoadHandler::setImageAttachment(const fs::path& filename) {
    xoj_assert(this->image);

    if (this->image->hasData()) {
        g_warning("LoadHandler: Image attachment found, but the image already has data");
    }

    auto imageData = readZipAttachment(filename);
    if (imageData) {
        this->image->setImage(std::move(*imageData));
    }
}

void LoadHandler::finalizeImage() {
    xoj_assert(this->image);

    if (!this->image->hasData()) {
        g_warning("LoadHandler: Ignoring image with no data");
        this->image.reset();
        return;
    }

    this->layer->addElement(std::move(this->image));
}

void LoadHandler::addTexImage(double left, double top, double right, double bottom, std::string text) {
    xoj_assert(!this->teximage);
    this->teximage = std::make_unique<TexImage>();

    this->teximage->setX(left);
    this->teximage->setY(top);
    this->teximage->setWidth(right - left);
    this->teximage->setHeight(bottom - top);

    this->teximage->setText(std::move(text));
}

void LoadHandler::setTexImageData(std::string data) {
    xoj_assert(this->teximage);

    this->teximage->loadData(std::move(data));
}

void LoadHandler::setTexImageAttachment(const fs::path& filename) {
    xoj_assert(this->teximage);

    auto imageData = readZipAttachment(filename);
    if (imageData) {
        this->teximage->loadData(std::move(*imageData));
    }
}

void LoadHandler::finalizeTexImage() {
    xoj_assert(this->teximage);

    this->layer->addElement(std::move(this->teximage));
}

void LoadHandler::addLink(TextAlignment align, std::string font, double size, double x, double y, Color color,
                          std::string url) {
    this->link = std::make_unique<Link>();

    this->link->setAlignment(align);

    this->link->setUrl(std::string(url.c_str()));

    this->link->setX(x);
    this->link->setY(y);

    XojFont& f = this->link->getFont();
    f.setName(std::move(font));
    f.setSize(size);
    this->link->setColor(color);
}

void LoadHandler::setLinkContent(std::string contents) {
    xoj_assert(this->link);

    this->link->setText(std::move(contents));
}

void LoadHandler::finalizeLink() {
    xoj_assert(this->link);

    this->layer->addElement(std::move(this->link));
}


void LoadHandler::logError(const std::string& error) {
    g_warning("LoadHandler: %s", error.c_str());
    if (this->errorMessages) {
        this->errorMessages->emplace_back(error);
    }
}


auto LoadHandler::openFile(fs::path const& filepath) -> std::unique_ptr<xoj::util::InputStream> {
    this->xournalFilepath = filepath;
    int zipError = 0;
    this->zipFp = zip_wrapper{zip_open(char_cast(filepath.u8string().c_str()), ZIP_RDONLY, &zipError)};

    // Check if file is a gzip
    if (!this->zipFp && zipError == ZIP_ER_NOZIP) {
        this->isGzFile = true;
        return std::make_unique<xoj::util::GzInputStream>(filepath);
    }

    // Check if file is a zip
    if (this->zipFp && !this->isGzFile) {
        // Check the mimetype
        auto mimetypeFp = zip_file_wrapper{zip_fopen(this->zipFp.get(), "mimetype", 0)};
        if (!mimetypeFp) {
            throw std::runtime_error{
                    FS(_F("The file \"{1}\" is no valid .xopp file (Mimetype missing). Error message: {2}") %
                       filepath.u8string() % zip_error_strerror(zip_get_error(this->zipFp.get())))};
        }
        std::array<char, MAX_MIMETYPE_LENGTH + 1> mimetype = {};
        // read the mimetype and a few more bytes to make sure we do not only read a subset
        zip_fread(mimetypeFp.get(), mimetype.data(), MAX_MIMETYPE_LENGTH);
        if (!strcmp(mimetype.data(), "application/xournal++")) {
            throw std::runtime_error{FS(_F("The file \"{1}\" is no valid .xopp file (Mimetype wrong): \"{2}\"") %
                                        filepath.u8string() % mimetype.data())};
        }

        // Get the file version
        auto versionFp = zip_file_wrapper{zip_fopen(this->zipFp.get(), "META-INF/version", 0)};
        if (!versionFp) {
            throw std::runtime_error{
                    FS(_F("The file \"{1}\" is no valid .xopp file (Version missing). Error message: {2}") %
                       filepath.u8string() % zip_error_strerror(zip_get_error(zipFp.get())))};
        }
        std::array<char, MAX_VERSION_LENGTH + 1> versionString = {};
        const auto length = std::max(zip_fread(versionFp.get(), versionString.data(), MAX_VERSION_LENGTH),
                                     static_cast<zip_int64_t>(0));
        const std::regex versionRegex{"current=(\\d+?)(?:\n|\r\n)min=(\\d+?)"};
        std::match_results<char*> match;
        if (std::regex_search(versionString.data(), versionString.data() + length, match, versionRegex)) {
            this->fileVersion = std::stoi(match.str(1));
            this->minimalFileVersion = std::stoi(match.str(2));
        } else {
            throw std::runtime_error{
                    FS(_F("The file \"{1}\" is not a valid .xopp file (Version string corrupted): \"{2}\"") %
                       filepath.u8string() % std::string(versionString.begin(), versionString.end()))};
        }

        // open the main content file
        return std::make_unique<xoj::util::ZipInputStream>(this->zipFp.get(), "content.xml");
    }

    // Fail if neither method could open the file
    throw std::runtime_error{FS(_F("Could not open file: \"{1}\"") % filepath.u8string())};
}

void LoadHandler::closeFile() noexcept { this->zipFp.reset(); }

XOJ_GIO_GUARD_GENERATOR_TYPE(GMarkupParseContext, g_markup_parse_context_free);

void LoadHandler::parseXml(std::unique_ptr<xoj::util::InputStream> xmlContentStream) {
    xoj_assert(xmlContentStream);

    XmlParser parserInterface{*this};
    auto context = GMarkupParseContextGuard{g_markup_parse_context_new(
            &XmlParser::interface, static_cast<GMarkupParseFlags>(0), &parserInterface, nullptr)};

    const auto handleGError = [this](xoj::util::GErrorGuard error) -> void {
        if (error) {
            logError(FS(_F("XML Parser error: {1}") % error->message));
        }
    };

    std::array<char, 1024> buffer{};
    int len{};
    xoj::util::GErrorGuard error;
    while (true) {
        len = xmlContentStream->read(buffer.data(), buffer.size());
        if (len < 0) {
            throw std::runtime_error{_("Failed to read from compressed file")};
        } else if (len == 0) {
            // Reached EOF
            break;
        }

        auto valid = g_markup_parse_context_parse(context.get(), buffer.data(), len, xoj::util::out_ptr(error));
        handleGError(std::move(error));
        if (!valid) {
            throw std::runtime_error{_("Invalid XML data read")};
        }
    }

    // Sanity checks for document validity
    if (!g_markup_parse_context_end_parse(context.get(), xoj::util::out_ptr(error)) || !this->parsingComplete) {
        handleGError(std::move(error));

        // The end may be cut off. Attempt to recover contents by closing the remaining open nodes.
        parserInterface.closeOpenNodes();
        // If a document was created in addDocument(), finalizeDocument() must have been called now.
        xoj_assert((this->doc != nullptr) == this->parsingComplete);

        if (this->parsingComplete) {
            logError(_("Document is not complete.\n"
                       "We have tried recovering the data that could be read, but the last element may have been "
                       "discarded, and the end of the file is likely missing."));
        }
        // Else, then this->doc == nullptr and we will throw just below
    }
    if (!this->doc) {
        throw std::runtime_error{_("Document is corrupted (no document tag found in file)")};
    }
    if (this->doc->getPageCount() == 0) {
        throw std::runtime_error{_("Document is corrupted (no pages found in file)")};
    }
}


auto LoadHandler::loadDocument(fs::path const& filepath) -> std::unique_ptr<Document> {
    auto xmlContentStream = openFile(filepath);
    parseXml(std::move(xmlContentStream));
    closeFile();

    xoj_assert(this->doc);
    this->doc->setCreateBackupOnSave(true);

    if (this->fileVersion == 1 || (this->errorMessages && !this->errorMessages->empty())) {
        // Either, this file was created by Xournal, not Xournal++, or loading
        // the file failed to some extent (i.e. file is corrupt or uses unknown
        // features). In all cases, do not set the doc's filename, in order to
        // prompt a "Save as" dialog when the user saves the document. In this
        // way, we do not silently make the file will become incompatible with
        // Xournal, nor erase data that we couldn't parse.

        this->doc->setFilepath("");
    } else {
        this->doc->setFilepath(filepath);
    }

    return std::move(this->doc);
}


void LoadHandler::fixNullPressureValues(std::vector<Point> pts) {
    /*
     * Due to various bugs (see e.g. https://github.com/xournalpp/xournalpp/issues/3643), old files may contain strokes
     * with non-positive pressure values.
     *
     * Those strokes thus fail the somewhat reasonable assumption that pressure values should be positive.
     * The portions of the stroke with non-positive pressure values are in any case invisible.
     *
     * This function fixes corrupted strokes by
     *  1- removing every point with a non-positive pressure value.
     *  2- if required, splitting the affected stroke into several strokes.
     *  3- entirely deleting strokes without any valid pressure value
     */

    xoj_assert_message(pts.back().z == Point::NO_PRESSURE, "The last point of a stroke must have no pressure");
    auto nextPositive = std::ranges::find_if(pts, [](double v) { return v > 0; }, &Point::z);

    std::vector<std::vector<Point>> strokePortions;
    while (nextPositive != pts.end()) {
        auto nextNonPositive =
                std::find_if(nextPositive, pts.end(), [](const Point& p) { return p.z <= 0 || std::isnan(p.z); });

        // Since the last point has no pressure, nextNonPositive is always dereferencable
        xoj_assert(nextNonPositive != pts.end());
        std::vector<Point> ps{nextPositive, std::next(nextNonPositive)};
        ps.back().z = Point::NO_PRESSURE;
        strokePortions.emplace_back(std::move(ps));

        nextPositive = std::find_if(nextNonPositive, pts.end(), [](const Point& p) { return p.z > 0; });
    }

    if (strokePortions.empty()) {
        // There were no valid pressure values! Delete the stroke entirely
        g_warning("LoadHandler: Found a stroke with only non-positive pressure values! Removing this invisible stroke");
        this->stroke.reset();
        return;
    }

    g_warning("LoadHandler: Found a stroke with some non-positive pressure values. Removing the affected points");
    for_first_then_each(
            strokePortions, [&](std::vector<Point>& points) { this->stroke->setPointVector(std::move(points)); },
            [&](std::vector<Point>& points) {
                auto newStroke = std::make_unique<Stroke>();
                newStroke->applyStyleFrom(this->stroke.get());
                newStroke->setPointVector(std::move(points));
                this->layer->addElement(std::exchange(this->stroke, std::move(newStroke)));
            });
}

auto LoadHandler::readZipAttachment(fs::path const& filename) -> std::unique_ptr<std::string> {
    zip_stat_t attachmentFileStat;
    const int statStatus = zip_stat(this->zipFp.get(), char_cast(filename.u8string().c_str()), 0, &attachmentFileStat);
    if (statStatus != 0) {
        logError(FS(_F("Could not open attachment: {1}. Error message: {2}") % filename.u8string() %
                    zip_error_strerror(zip_get_error(this->zipFp.get()))));
        return {};
    }

    if (!(attachmentFileStat.valid & ZIP_STAT_SIZE)) {
        logError(FS(_F("Could not open attachment: {1}. Error message: No valid file size provided") %
                    filename.u8string()));
        return {};
    }
    const zip_uint64_t length = attachmentFileStat.size;

    auto attachmentFile = zip_file_wrapper{zip_fopen(this->zipFp.get(), char_cast(filename.u8string().c_str()), 0)};
    if (!attachmentFile) {
        logError(FS(_F("Could not open attachment: {1}. Error message: {2}") % filename.u8string() %
                    zip_error_strerror(zip_get_error(this->zipFp.get()))));
        return {};
    }

    auto data = std::make_unique<std::string>(length, 0);
    zip_uint64_t readBytes = 0;
    while (readBytes < length) {
        const zip_int64_t read = zip_fread(attachmentFile.get(), data->data() + readBytes, length - readBytes);
        if (read == -1) {
            logError(FS(_F("Could not open attachment: {1}. Error message: No valid file size provided") %
                        filename.u8string()));
            return {};
        }

        readBytes += static_cast<zip_uint64_t>(read);
    }

    return data;
}

void LoadHandler::setAudioAttributes(AudioElement& elem, fs::path filename, size_t timestamp) {
    if (!filename.empty()) {
        if (this->isGzFile) {
            elem.setAudioFilename(std::move(filename));
        } else {
            auto tempFile = getTempFileForPath(filename);
            if (!tempFile.empty()) {
                elem.setAudioFilename(std::move(tempFile));
            }
        }

        if (this->fileVersion < 4) {
            timestamp *= 1000;
        }
        elem.setTimestamp(timestamp);
    }
}

auto LoadHandler::getTempFileForPath(fs::path const& filename) -> fs::path {
    auto it = this->audioFiles.find(filename);

    if (it == this->audioFiles.end()) {
        logError(FS(_F("Requested temporary file was not found for attachment {1}") % filename.u8string()));
        return {};
    } else {
        return it->second;
    }
}

auto LoadHandler::getAbsoluteFilepath(const fs::path& filename, bool attach) const -> fs::path {
    fs::path absolutePath;

    if (attach) {
        // For a file xyz.xopp, the PDF background is saved in the same
        // directory as xyz.xopp.[filename]
        absolutePath = (fs::path{this->xournalFilepath} += ".") += filename;
    } else {
        // domain = "absolute" doesn't forcefully mean an absolute path
        if (filename.is_relative()) {
            // The path is relative to the .xopp file's location
            this->doc->setPathStorageMode(Util::PathStorageMode::AS_RELATIVE_PATH);
            absolutePath = fs::path{this->xournalFilepath}.remove_filename() / filename;
        } else {
            this->doc->setPathStorageMode(Util::PathStorageMode::AS_ABSOLUTE_PATH);
            absolutePath = filename;
        }
    }
    return absolutePath;
}
