#include "monoformat_structured.hpp"
#include "monoformat_fontreader.hpp"
#include "monoformat_bithelpers.hpp"

#include <iostream>
#include <stdexcept>
#include <format>

namespace monoformat {

static std::string base64Encode(std::span<std::byte const> buffer);
static std::vector<std::byte> base64Decode(std::string_view data);

NLOHMANN_JSON_SERIALIZE_ENUM(SectionType, {
    {SectionType::AlwaysDrawn, "AlwaysDrawn"},
    {SectionType::TimeBasedDrawn, "TimeBasedDrawn"},
    {SectionType::MultiTimeBasedDrawn, "MultiTimeBasedDrawn"},
    {SectionType::ExpiryDate, "ExpiryDate"},
    {SectionType::CustomFont, "CustomFont"},
})

NLOHMANN_JSON_SERIALIZE_ENUM(ElementType, {
    {ElementType::Image, "Image"},
    {ElementType::Animation, "Animation"},
    {ElementType::HScrollImage, "HScrollImage"},
    {ElementType::VScrollImage, "VScrollImage"},
    {ElementType::Line, "Line"},
    {ElementType::Box, "Box"},
    {ElementType::ClippedText, "ClippedText"},
    {ElementType::HScrollText, "HScrollText"},
    {ElementType::CurrentTime, "CurrentTime"},
})

NLOHMANN_JSON_SERIALIZE_ENUM(LineStyle, {
    {LineStyle::Solid, "Solid"},
})

NLOHMANN_JSON_SERIALIZE_ENUM(FillPattern, {
    {FillPattern::Solid, "Solid"},
})

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ScrollFlags, endless, invertDirection, padBefore, padAfter)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LineFlags, dark)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(FillFlags, dark)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TextFlags, dark, autoWordWrap)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TimeDisplayFlags, use12h, showHours, showMinutes, showSeconds)

ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
{
    std::size_t const bufferSize = (m_width * m_height + 8 - 1) / 8;\
    m_buffer.resize(bufferSize);
}

std::uint16_t ImageElement::x() const noexcept {
    return m_x;
}

std::uint16_t ImageElement::y() const noexcept {
    return m_y;
}

std::uint16_t ImageElement::width() const noexcept {
    return m_width;
}

std::uint16_t ImageElement::height() const noexcept {
    return m_height;
}

std::span<std::byte> ImageElement::buffer() noexcept {
    return m_buffer;
}

std::span<std::byte const> ImageElement::buffer() const noexcept {
    return m_buffer;
}

MemoryOneBitBuffer ImageElement::image() noexcept {
    return MemoryOneBitBuffer{m_buffer, m_width, m_height};
}

ConstMemoryOneBitBuffer ImageElement::image() const noexcept {
    return ConstMemoryOneBitBuffer{m_buffer, m_width, m_height};
}

void ImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
    std::size_t n = std::min(m_buffer.size(), newBufferData.size());
    std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
    std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
}

ImageElement::~ImageElement() {
}

ElementType ImageElement::elementType() const {
    return ElementType::Image;
}

std::uint32_t ImageElement::minimumFormatVersion() const {
    return 1;
}

std::size_t ImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Image));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    pos = writeU16LE(target, pos, 0);
    pos = writeBuffer(target, pos, m_buffer);
    return alignNextWrite(target, pos, 4);
}

nlohmann::json ImageElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "Image";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    result["image"] = nlohmann::json::array();

    auto ownImage = image();
    for (std::uint16_t dy = 0; dy < m_height; ++dy) {
        for (std::uint16_t dx = 0; dx < m_width; ++dx) {
            result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
        }
    }

    return result;
}

void ImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = animationTick;
    std::ignore = currentTimestamp;
    std::ignore = customFonts;
    auto ownImage = image();
    for (std::uint16_t dy = 0; dy < m_height; ++dy) {
        for (std::uint16_t dx = 0; dx < m_width; ++dx) {
            imageBuffer->setPixel(m_x + dx, m_y + dy, ownImage.isPixelSet(dx, dy));
        }
    }
}

std::expected<std::unique_ptr<ImageElement>, ParseError> ImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::Image)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    auto reserved_ = readU16LE(buffer);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }
    std::size_t imageSize = ((*width) * (*height) + 8 - 1) / 8;
    auto imageData = readBuffer(buffer, imageSize);
    if (!imageData) {
        return std::unexpected(reserved_.error());
    }
    std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
    if (imageSize < rounded) {
        buffer = buffer.subspan(rounded - imageSize);
    }

    auto result = std::make_unique<ImageElement>(*x, *y, *width, *height);
    result->updateBuffer(*imageData);
    return result;
}

std::unique_ptr<ImageElement> ImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    std::vector<int> pixels = j.at("image");
    if (pixels.size() != width * height) {
        throw std::runtime_error(std::format("Error unserializing an Image element: the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, height, pixels.size(), width * height));
    }
    auto result = std::make_unique<ImageElement>(x, y, width, height);
    auto image = result->image();
    for (std::uint16_t dy = 0; dy < height; ++dy) {
        std::size_t base = static_cast<std::size_t>(dy) * width;
        for (std::uint16_t dx = 0; dx < width; ++dx) {
            image.setPixel(dx, dy, !!pixels[base + dx]);
        }
    }
    return result;
}

AnimationElement::AnimationElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t numberOfFrames, std::uint16_t updateInterval)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_numberOfFrames{numberOfFrames}
    , m_updateInterval{updateInterval}
{
    std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
    m_buffer.resize(imageBufferSize * numberOfFrames);
}

std::uint16_t AnimationElement::x() const noexcept {
    return m_x;
}

std::uint16_t AnimationElement::y() const noexcept {
    return m_y;
}

std::uint16_t AnimationElement::width() const noexcept {
    return m_width;
}

std::uint16_t AnimationElement::height() const noexcept {
    return m_height;
}

std::uint16_t AnimationElement::numberOfFrames() const noexcept {
    return m_numberOfFrames;
}

std::uint16_t AnimationElement::updateInterval() const noexcept {
    return m_updateInterval;
}

std::span<std::byte> AnimationElement::buffers() noexcept {
    return m_buffer;
}

std::span<std::byte const> AnimationElement::buffers() const noexcept {
    return m_buffer;
}

std::span<std::byte> AnimationElement::buffer(std::uint16_t frameIndex) noexcept {
    if (frameIndex >= m_numberOfFrames) {
        return {};
    }
    std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
    return std::span{m_buffer}.subspan(frameIndex * imageBufferSize, imageBufferSize);
}

std::span<std::byte const> AnimationElement::buffer(std::uint16_t frameIndex) const noexcept {
    if (frameIndex >= m_numberOfFrames) {
        return {};
    }
    std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
    return std::span{m_buffer}.subspan(frameIndex * imageBufferSize, imageBufferSize);
}

MemoryOneBitBuffer AnimationElement::image(std::uint16_t frameIndex) noexcept {
    return MemoryOneBitBuffer{buffer(frameIndex), m_width, m_height};
}

ConstMemoryOneBitBuffer AnimationElement::image(std::uint16_t frameIndex) const noexcept {
    return ConstMemoryOneBitBuffer{buffer(frameIndex), m_width, m_height};
}

void AnimationElement::updateBuffer(std::span<std::byte const> newBufferData) {
    std::size_t n = std::min(m_buffer.size(), newBufferData.size());
    std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
    std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
}

AnimationElement::~AnimationElement() {
}

ElementType AnimationElement::elementType() const {
    return ElementType::Animation;
}

std::uint32_t AnimationElement::minimumFormatVersion() const {
    return 1;
}

std::size_t AnimationElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Animation));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    pos = writeU16LE(target, pos, m_numberOfFrames);
    pos = writeU16LE(target, pos, m_updateInterval);
    pos = writeU16LE(target, pos, 0);
    pos = writeBuffer(target, pos, m_buffer);
    return alignNextWrite(target, pos, 4);
}

nlohmann::json AnimationElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "Animation";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    result["updateInterval"] = m_updateInterval;
    result["frames"] = nlohmann::json::array();

    for (std::uint16_t f = 0; f < m_numberOfFrames; ++f) {
        nlohmann::json frame = nlohmann::json::array();
        auto ownImage = image(f);
        for (std::uint16_t dy = 0; dy < m_height; ++dy) {
            for (std::uint16_t dx = 0; dx < m_width; ++dx) {
                frame.push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
            }
        }
        result["frames"].push_back(frame);
    }

    return result;
}

void AnimationElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = currentTimestamp;
    std::ignore = customFonts;

    if (m_numberOfFrames == 0) {
        return;
    }

    std::size_t const tick = animationTick / (static_cast<std::size_t>(m_updateInterval) + 1);
    std::size_t const frameIndex = tick % m_numberOfFrames;

    auto ownImage = image(frameIndex);
    for (std::uint16_t dy = 0; dy < m_height; ++dy) {
        for (std::uint16_t dx = 0; dx < m_width; ++dx) {
            imageBuffer->setPixel(m_x + dx, m_y + dy, ownImage.isPixelSet(dx, dy));
        }
    }
}

std::expected<std::unique_ptr<AnimationElement>, ParseError> AnimationElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::Animation)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    auto numberOfFrames = readU16LE(buffer);
    if (!numberOfFrames) {
        return std::unexpected(numberOfFrames.error());
    }
    auto updateInterval = readU16LE(buffer);
    if (!updateInterval) {
        return std::unexpected(updateInterval.error());
    }
    auto reserved_ = readU16LE(buffer);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }
    std::size_t imageSize = ((*width) * (*height) + 8 - 1) / 8;
    auto imageData = readBuffer(buffer, imageSize * (*numberOfFrames));
    if (!imageData) {
        return std::unexpected(reserved_.error());
    }
    std::size_t rounded = (imageSize * (*numberOfFrames) + 4 - 1) / 4 * 4;
    if (imageSize * (*numberOfFrames) < rounded) {
        buffer = buffer.subspan(rounded - imageSize * (*numberOfFrames));
    }

    auto result = std::make_unique<AnimationElement>(*x, *y, *width, *height, *numberOfFrames, *updateInterval);
    result->updateBuffer(*imageData);
    return result;
}

std::unique_ptr<AnimationElement> AnimationElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    std::uint16_t updateInterval = j.at("updateInterval");
    std::vector<std::vector<int>> frames = j.at("frames");

    if (frames.size() > std::numeric_limits<std::uint16_t>::max()) {
        throw std::runtime_error(std::format("Error unserializing an Animation element: the number of frames, {:d}, is too large", frames.size()));
    }
    for (std::size_t i = 0; i < frames.size(); ++i) {
        if (frames[i].size() != width * height) {
            throw std::runtime_error(std::format("Error unserializing an Animation element: in frame {:d}, the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", i, width, height, frames[i].size(), width * height));
        }
    }

    auto result = std::make_unique<AnimationElement>(x, y, width, height, static_cast<std::uint16_t>(frames.size()), updateInterval);
    for (std::size_t i = 0; i < frames.size(); ++i) {
        auto image = result->image(i);
        for (std::uint16_t dy = 0; dy < height; ++dy) {
            std::size_t base = static_cast<std::size_t>(dy) * width;
            for (std::uint16_t dx = 0; dx < width; ++dx) {
                image.setPixel(dx, dy, !!frames[i][base + dx]);
            }
        }
    }
    return result;
}

HScrollImageElement::HScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentWidth, ScrollFlags flags, std::uint8_t scrollSpeed)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_contentWidth{contentWidth}
    , m_flags{flags}
    , m_scrollSpeed{scrollSpeed}
{
    std::size_t const imageBufferSize = (m_contentWidth * m_height + 8 - 1) / 8;
    m_buffer.resize(imageBufferSize);
}

std::uint16_t HScrollImageElement::x() const noexcept {
    return m_x;
}

std::uint16_t HScrollImageElement::y() const noexcept {
    return m_y;
}

std::uint16_t HScrollImageElement::width() const noexcept {
    return m_width;
}

std::uint16_t HScrollImageElement::height() const noexcept {
    return m_height;
}

std::uint16_t HScrollImageElement::contentWidth() const noexcept {
    return m_contentWidth;
}

ScrollFlags HScrollImageElement::flags() const noexcept {
    return m_flags;
}

std::uint8_t HScrollImageElement::scrollSpeed() const noexcept {
    return m_scrollSpeed;
}

std::span<std::byte> HScrollImageElement::buffer() noexcept {
    return std::span{m_buffer};
}

std::span<std::byte const> HScrollImageElement::buffer() const noexcept {
    return std::span{m_buffer};
}

MemoryOneBitBuffer HScrollImageElement::image() noexcept {
    return MemoryOneBitBuffer{m_buffer, m_contentWidth, m_height};
}

ConstMemoryOneBitBuffer HScrollImageElement::image() const noexcept {
    return ConstMemoryOneBitBuffer{m_buffer, m_contentWidth, m_height};
}

void HScrollImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
    std::size_t n = std::min(m_buffer.size(), newBufferData.size());
    std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
    std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
}

HScrollImageElement::~HScrollImageElement() {
}

ElementType HScrollImageElement::elementType() const {
    return ElementType::HScrollImage;
}

std::uint32_t HScrollImageElement::minimumFormatVersion() const {
    return 1;
}

std::size_t HScrollImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::HScrollImage));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    pos = writeU16LE(target, pos, m_contentWidth);
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
    pos = writeU8LE(target, pos, m_scrollSpeed);
    pos = writeU16LE(target, pos, 0);
    pos = writeBuffer(target, pos, m_buffer);
    return alignNextWrite(target, pos, 4);
}

nlohmann::json HScrollImageElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "HScrollImage";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    result["contentWidth"] = m_contentWidth;
    result["flags"] = m_flags;
    result["scrollSpeed"] = int(m_scrollSpeed);
    result["image"] = nlohmann::json::array();

    auto ownImage = image();
    for (std::uint16_t dy = 0; dy < m_height; ++dy) {
        for (std::uint16_t dx = 0; dx < m_contentWidth; ++dx) {
            result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
        }
    }

    return result;
}

void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = currentTimestamp;
    std::ignore = customFonts;

    auto ownImage = image();
    ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};

    if (m_contentWidth == 0) {
        return;
    }

    bool restarting = !m_flags.endless;
    bool invert = m_flags.invertDirection;
    bool padBefore = m_flags.padBefore;
    bool padAfter = m_flags.padAfter;

    if (!padBefore && !padAfter && m_contentWidth < m_width) {
        std::int16_t offset = invert ? (m_width - m_contentWidth) : 0;
        for (std::size_t dy = 0; dy < m_height; ++dy) {
            for (std::size_t dx = 0; dx < m_contentWidth; ++dx) {
                target.setPixel(dx + offset, dy, ownImage.isPixelSet(dx, dy));
            }
        }
        return;
    }

    std::int32_t totalSize = m_contentWidth;
    if (padBefore) {
        totalSize += m_width;
    }
    if (padAfter) {
        totalSize += m_width;
    }

    // FIXME: handle overflow here...
    std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;
    if (invert) {
        offset = totalSize - offset - 1;
    }
    if (restarting) {
        offset = std::min(offset, totalSize - m_width);
    }

    std::int32_t partOffset = 0;
    if (padBefore) {
        for (std::int32_t dy = 0; dy < m_height; ++dy) {
            for (std::int32_t dx = 0; dx < m_width; ++dx) {
                std::int32_t ddx = dx + partOffset;
                if ((ddx - offset) < 0) {
                    continue;
                }
                target.setPixel(ddx - offset, dy, false);
            }
        }
        partOffset += m_width;
    }
    for (std::int32_t dy = 0; dy < m_height; ++dy) {
        for (std::int32_t dx = 0; dx < m_contentWidth; ++dx) {
            std::int32_t ddx = dx + partOffset;
            if ((ddx - offset) < 0) {
                continue;
            }
            target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy));
        }
        partOffset += m_contentWidth;
    }
    if (padAfter) {
        for (std::int32_t dy = 0; dy < m_height; ++dy) {
            for (std::int32_t dx = 0; dx < m_width; ++dx) {
                std::int32_t ddx = dx + partOffset;
                if ((ddx - offset) < 0) {
                    continue;
                }
                target.setPixel(ddx - offset, dy, false);
            }
        }
        partOffset += m_width;
    }
    if (!restarting) {
        if (padBefore) {
            for (std::int32_t dy = 0; dy < m_height; ++dy) {
                for (std::int32_t dx = 0; dx < m_width; ++dx) {
                    std::int32_t ddx = dx + partOffset;
                    if ((ddx - offset) < 0) {
                        continue;
                    }
                    target.setPixel(ddx - offset, dy, false);
                }
            }
            partOffset += m_width;
        }
        for (std::int32_t dy = 0; dy < m_height; ++dy) {
            for (std::int32_t dx = 0; dx < m_contentWidth; ++dx) {
                std::int32_t ddx = dx + partOffset;
                if ((ddx - offset) < 0) {
                    continue;
                }
                target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy));
            }
        }
        partOffset += m_contentWidth;
    }
}

std::expected<std::unique_ptr<HScrollImageElement>, ParseError> HScrollImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::HScrollImage)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    auto contentWidth = readU16LE(buffer);
    if (!contentWidth) {
        return std::unexpected(contentWidth.error());
    }
    auto flags = readU8LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto scrollSpeed = readU8LE(buffer);
    if (!scrollSpeed) {
        return std::unexpected(scrollSpeed.error());
    }
    auto reserved_ = readU16LE(buffer);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }
    std::size_t imageSize = ((*contentWidth) * (*height) + 8 - 1) / 8;
    auto imageData = readBuffer(buffer, imageSize);
    if (!imageData) {
        return std::unexpected(reserved_.error());
    }
    std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
    if (imageSize < rounded) {
        buffer = buffer.subspan(rounded - imageSize);
    }

    auto result = std::make_unique<HScrollImageElement>(*x, *y, *width, *height, *contentWidth, ScrollFlags{*flags}, *scrollSpeed);
    result->updateBuffer(*imageData);
    return result;
}

std::unique_ptr<HScrollImageElement> HScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    std::uint16_t contentWidth = j.at("contentWidth");
    ScrollFlags flags = j.at("flags");
    int scrollSpeed = j.at("scrollSpeed");
    std::vector<int> pixels = j.at("image");
    if (scrollSpeed < 0 || scrollSpeed > 255) {
        throw std::runtime_error(std::format("Error unserializing an HScrollImage element: invalid scroll speed {:d} specified", scrollSpeed));
    }
    if (pixels.size() != contentWidth * height) {
        throw std::runtime_error(std::format("Error unserializing an HScrollImage element: the content width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", contentWidth, height, pixels.size(), contentWidth * height));
    }

    auto result = std::make_unique<HScrollImageElement>(x, y, width, height, contentWidth, flags, static_cast<std::uint8_t>(scrollSpeed));
    auto image = result->image();
    for (std::uint16_t dy = 0; dy < height; ++dy) {
        std::size_t base = static_cast<std::size_t>(dy) * contentWidth;
        for (std::uint16_t dx = 0; dx < contentWidth; ++dx) {
            image.setPixel(dx, dy, !!pixels[base + dx]);
        }
    }
    return result;
}

VScrollImageElement::VScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentHeight, ScrollFlags flags, std::uint8_t scrollSpeed)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_contentHeight{contentHeight}
    , m_flags{flags}
    , m_scrollSpeed{scrollSpeed}
{
    std::size_t const imageBufferSize = (m_width * m_contentHeight + 8 - 1) / 8;
    m_buffer.resize(imageBufferSize);
}

std::uint16_t VScrollImageElement::x() const noexcept {
    return m_x;
}

std::uint16_t VScrollImageElement::y() const noexcept {
    return m_y;
}

std::uint16_t VScrollImageElement::width() const noexcept {
    return m_width;
}

std::uint16_t VScrollImageElement::height() const noexcept {
    return m_height;
}

std::uint16_t VScrollImageElement::contentHeight() const noexcept {
    return m_contentHeight;
}

ScrollFlags VScrollImageElement::flags() const noexcept {
    return m_flags;
}

std::uint8_t VScrollImageElement::scrollSpeed() const noexcept {
    return m_scrollSpeed;
}

std::span<std::byte> VScrollImageElement::buffer() noexcept {
    return std::span{m_buffer};
}

std::span<std::byte const> VScrollImageElement::buffer() const noexcept {
    return std::span{m_buffer};
}

MemoryOneBitBuffer VScrollImageElement::image() noexcept {
    return MemoryOneBitBuffer{m_buffer, m_width, m_contentHeight};
}

ConstMemoryOneBitBuffer VScrollImageElement::image() const noexcept {
    return ConstMemoryOneBitBuffer{m_buffer, m_width, m_contentHeight};
}

void VScrollImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
    std::size_t n = std::min(m_buffer.size(), newBufferData.size());
    std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
    std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
}

VScrollImageElement::~VScrollImageElement() {
}

ElementType VScrollImageElement::elementType() const {
    return ElementType::VScrollImage;
}

std::uint32_t VScrollImageElement::minimumFormatVersion() const {
    return 1;
}

std::size_t VScrollImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::VScrollImage));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    pos = writeU16LE(target, pos, m_contentHeight);
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
    pos = writeU8LE(target, pos, m_scrollSpeed);
    pos = writeU16LE(target, pos, 0);
    pos = writeBuffer(target, pos, m_buffer);
    return alignNextWrite(target, pos, 4);
}

nlohmann::json VScrollImageElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "VScrollImage";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    result["contentHeight"] = m_contentHeight;
    result["flags"] = m_flags;
    result["scrollSpeed"] = int(m_scrollSpeed);
    result["image"] = nlohmann::json::array();

    auto ownImage = image();
    for (std::uint16_t dy = 0; dy < m_contentHeight; ++dy) {
        for (std::uint16_t dx = 0; dx < m_width; ++dx) {
            result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
        }
    }

    return result;
}

void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = currentTimestamp;
    std::ignore = customFonts;

    auto ownImage = image();
    ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};

    if (m_contentHeight == 0) {
        return;
    }

    bool restarting = !m_flags.endless;
    bool invert = m_flags.invertDirection;
    bool padBefore = m_flags.padBefore;
    bool padAfter = m_flags.padAfter;

    if (!padBefore && !padAfter && m_contentHeight < m_height) {
        std::int16_t offset = invert ? (m_height - m_contentHeight) : 0;
        for (std::size_t dy = 0; dy < m_contentHeight; ++dy) {
            for (std::size_t dx = 0; dx < m_width; ++dx) {
                target.setPixel(dx, dy + offset, ownImage.isPixelSet(dx, dy));
            }
        }
        return;
    }

    std::int32_t totalSize = m_contentHeight;
    if (padBefore) {
        totalSize += m_height;
    }
    if (padAfter) {
        totalSize += m_height;
    }

    // FIXME: handle overflow here...
    std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;
    if (invert) {
        offset = totalSize - offset - 1;
    }
    if (restarting) {
        offset = std::min(offset, totalSize - m_height);
    }

    std::int32_t partOffset = 0;
    if (padBefore) {
        for (std::int32_t dy = 0; dy < m_height; ++dy) {
            std::int32_t ddy = dy + partOffset;
            if ((ddy - offset) < 0) {
                continue;
            }
            for (std::int32_t dx = 0; dx < m_width; ++dx) {
                target.setPixel(dx, ddy - offset, false);
            }
        }
        partOffset += m_height;
    }
    for (std::int32_t dy = 0; dy < m_contentHeight; ++dy) {
        std::int32_t ddy = dy + partOffset;
        if ((ddy - offset) < 0) {
            continue;
        }
        for (std::int32_t dx = 0; dx < m_width; ++dx) {
            target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy));
        }
        partOffset += m_contentHeight;
    }
    if (padAfter) {
        for (std::int32_t dy = 0; dy < m_height; ++dy) {
            std::int32_t ddy = dy + partOffset;
            if ((ddy - offset) < 0) {
                continue;
            }
            for (std::int32_t dx = 0; dx < m_width; ++dx) {
                target.setPixel(dx, ddy - offset, false);
            }
        }
        partOffset += m_height;
    }
    if (!restarting) {
        if (padBefore) {
            for (std::int32_t dy = 0; dy < m_height; ++dy) {
                std::int32_t ddy = dy + partOffset;
                if ((ddy - offset) < 0) {
                    continue;
                }
                for (std::int32_t dx = 0; dx < m_width; ++dx) {
                    target.setPixel(dx, ddy - offset, false);
                }
            }
            partOffset += m_height;
        }
        for (std::int32_t dy = 0; dy < m_contentHeight; ++dy) {
            std::int32_t ddy = dy + partOffset;
            if ((ddy - offset) < 0) {
                continue;
            }
            for (std::int32_t dx = 0; dx < m_width; ++dx) {
                target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy));
            }
        }
        partOffset += m_contentHeight;
    }
}

std::expected<std::unique_ptr<VScrollImageElement>, ParseError> VScrollImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::VScrollImage)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    auto contentHeight = readU16LE(buffer);
    if (!contentHeight) {
        return std::unexpected(contentHeight.error());
    }
    auto flags = readU8LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto scrollSpeed = readU8LE(buffer);
    if (!scrollSpeed) {
        return std::unexpected(scrollSpeed.error());
    }
    auto reserved_ = readU16LE(buffer);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }
    std::size_t imageSize = ((*width) * (*contentHeight) + 8 - 1) / 8;
    auto imageData = readBuffer(buffer, imageSize);
    if (!imageData) {
        return std::unexpected(reserved_.error());
    }
    std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
    if (imageSize < rounded) {
        buffer = buffer.subspan(rounded - imageSize);
    }

    auto result = std::make_unique<VScrollImageElement>(*x, *y, *width, *height, *contentHeight, ScrollFlags{*flags}, *scrollSpeed);
    result->updateBuffer(*imageData);
    return result;
}

std::unique_ptr<VScrollImageElement> VScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    std::uint16_t contentHeight = j.at("contentHeight");
    ScrollFlags flags = j.at("flags");
    int scrollSpeed = j.at("scrollSpeed");
    std::vector<int> pixels = j.at("image");
    if (scrollSpeed < 0 || scrollSpeed > 255) {
        throw std::runtime_error(std::format("Error unserializing an VScrollImage element: invalid scroll speed {:d} specified", scrollSpeed));
    }
    if (pixels.size() != width * contentHeight) {
        throw std::runtime_error(std::format("Error unserializing an VScrollImage element: the width, {:d}, and content height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, contentHeight, pixels.size(), width * contentHeight));
    }

    auto result = std::make_unique<VScrollImageElement>(x, y, width, height, contentHeight, flags, static_cast<std::uint8_t>(scrollSpeed));
    auto image = result->image();
    for (std::uint16_t dy = 0; dy < contentHeight; ++dy) {
        std::size_t base = static_cast<std::size_t>(dy) * width;
        for (std::uint16_t dx = 0; dx < width; ++dx) {
            image.setPixel(dx, dy, !!pixels[base + dx]);
        }
    }
    return result;
}

LineElement::LineElement(std::uint16_t originX, std::uint16_t originY, std::uint16_t targetX, std::uint16_t targetY, LineStyle lineStyle, LineFlags flags)
    : m_originX{originX}
    , m_originY{originY}
    , m_targetX{targetX}
    , m_targetY{targetY}
    , m_lineStyle{lineStyle}
    , m_flags{flags}
{
}

std::uint16_t LineElement::originX() const noexcept {
    return m_originX;
}

std::uint16_t LineElement::originY() const noexcept {
    return m_originY;
}

std::uint16_t LineElement::targetX() const noexcept {
    return m_targetX;
}

std::uint16_t LineElement::targetY() const noexcept {
    return m_targetY;
}

LineStyle LineElement::lineStyle() const noexcept {
    return m_lineStyle;
}

LineFlags LineElement::flags() const noexcept {
    return m_flags;
}

LineElement::~LineElement() {
}

ElementType LineElement::elementType() const {
    return ElementType::Line;
}

std::uint32_t LineElement::minimumFormatVersion() const {
    if (static_cast<std::uint8_t>(m_flags) != 0) {
        return 2;
    } else {
        return 1;
    }
}

std::size_t LineElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Line));
    pos = writeU16LE(target, pos, m_originX);
    pos = writeU16LE(target, pos, m_originY);
    pos = writeU16LE(target, pos, m_targetX);
    pos = writeU16LE(target, pos, m_targetY);
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_lineStyle));
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
    return alignNextWrite(target, pos, 4);
}

nlohmann::json LineElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "Line";
    result["originX"] = m_originX;
    result["originY"] = m_originY;
    result["targetX"] = m_targetX;
    result["targetY"] = m_targetY;
    result["lineStyle"] = m_lineStyle;
    result["flags"] = m_flags;
    return result;
}

void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = animationTick;
    std::ignore = currentTimestamp;
    std::ignore = customFonts;

    std::int32_t dx = static_cast<std::int32_t>(m_targetX) - static_cast<std::int32_t>(m_originX);
    std::int32_t dy = static_cast<std::int32_t>(m_targetY) - static_cast<std::int32_t>(m_originY);
    bool value = true;

    std::uint16_t originX = m_originX;
    std::uint16_t originY = m_originY;
    std::uint16_t targetX = m_targetX;
    std::uint16_t targetY = m_targetY;


    if (dx == 0) {
        if (dy < 0) {
            std::swap(originY, targetY);
            dy = -dy;
        }
        for (std::int32_t i = 0; i <= dy; ++i) {
            imageBuffer->setPixel(originX, originY + i, value);
        }
    } else if (dy == 0) {
        if (dx < 0) {
            std::swap(originX, targetX);
            dx = -dx;
        }
        for (std::int32_t i = 0; i <= dx; ++i) {
            imageBuffer->setPixel(originX + i, originY, value);
        }
    } else {
        for (std::int32_t i = 0; i <= dx; ++i) {
            std::int32_t x = originX + i;
            std::int32_t y = i * dy / dx + originY;
            imageBuffer->setPixel(static_cast<std::uint16_t>(x), static_cast<std::uint16_t>(y), value);
        }
    }
}

std::expected<std::unique_ptr<LineElement>, ParseError> LineElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::Line)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto originX = readU16LE(buffer);
    if (!originX) {
        return std::unexpected(originX.error());
    }
    auto originY = readU16LE(buffer);
    if (!originY) {
        return std::unexpected(originY.error());
    }
    auto targetX = readU16LE(buffer);
    if (!targetX) {
        return std::unexpected(targetX.error());
    }
    auto targetY = readU16LE(buffer);
    if (!targetY) {
        return std::unexpected(targetY.error());
    }
    auto lineStyle = readU8LE(buffer);
    if (!lineStyle) {
        return std::unexpected(lineStyle.error());
    }
    if (*lineStyle != static_cast<std::uint8_t>(LineStyle::Solid)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto flags = readU8LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto result = std::make_unique<LineElement>(*originX, *originY, *targetX, *targetY, static_cast<LineStyle>(*lineStyle), LineFlags{*flags});
    return result;
}

std::unique_ptr<LineElement> LineElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t originX = j.at("originX");
    std::uint16_t originY = j.at("originY");
    std::uint16_t targetX = j.at("targetX");
    std::uint16_t targetY = j.at("targetY");
    LineStyle lineStyle = j.at("lineStyle");
    LineFlags flags = j.at("flags");

    auto result = std::make_unique<LineElement>(originX, originY, targetX, targetY, lineStyle, flags);
    return result;
}

BoxElement::BoxElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, FillPattern fillPattern, FillFlags flags)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_fillPattern{fillPattern}
    , m_flags{flags}
{
}

std::uint16_t BoxElement::x() const noexcept {
    return m_x;
}

std::uint16_t BoxElement::y() const noexcept {
    return m_y;
}

std::uint16_t BoxElement::width() const noexcept {
    return m_width;
}

std::uint16_t BoxElement::height() const noexcept {
    return m_height;
}

FillPattern BoxElement::fillPattern() const noexcept {
    return m_fillPattern;
}

FillFlags BoxElement::flags() const noexcept {
    return m_flags;
}

BoxElement::~BoxElement() {
}

ElementType BoxElement::elementType() const {
    return ElementType::Box;
}

std::uint32_t BoxElement::minimumFormatVersion() const {
    return 2;
}

std::size_t BoxElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Box));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_fillPattern));
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
    return alignNextWrite(target, pos, 4);
}

nlohmann::json BoxElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "Box";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    result["fillPattern"] = m_fillPattern;
    result["flags"] = m_flags;
    return result;
}

void BoxElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = animationTick;
    std::ignore = currentTimestamp;
    std::ignore = customFonts;

    bool color = !m_flags.dark;
    for (std::uint16_t dy = 0; dy < m_height; ++dy) {
        for (std::uint16_t dx = 0; dx < m_width; ++dx) {
            imageBuffer->setPixel(m_x + dx, m_y + dy, color);
        }
    }
}

std::expected<std::unique_ptr<BoxElement>, ParseError> BoxElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::Image)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    auto fillPattern = readU8LE(buffer);
    if (!fillPattern) {
        return std::unexpected(fillPattern.error());
    }
    auto flags = readU8LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }

    auto result = std::make_unique<BoxElement>(*x, *y, *width, *height, static_cast<FillPattern>(*fillPattern), FillFlags{*flags});
    return result;
}

std::unique_ptr<BoxElement> BoxElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    FillPattern fillPattern = j.at("fillPattern");
    FillFlags flags = j.at("flags");
    auto result = std::make_unique<BoxElement>(x, y, width, height, fillPattern, flags);
    return result;
}

ClippedTextElement::ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::string text)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_textFlags{textFlags}
    , m_fontIndex{fontIndex}
    , m_text{std::move(text)}
{
}

std::uint16_t ClippedTextElement::x() const noexcept {
    return m_x;
}

std::uint16_t ClippedTextElement::y() const noexcept {
    return m_y;
}

std::uint16_t ClippedTextElement::width() const noexcept {
    return m_width;
}

std::uint16_t ClippedTextElement::height() const noexcept {
    return m_height;
}

TextFlags ClippedTextElement::textFlags() const noexcept {
    return m_textFlags;
}

std::uint16_t ClippedTextElement::fontIndex() const noexcept {
    return m_fontIndex;
}

std::string ClippedTextElement::text() const {
    return m_text;
}

ClippedTextElement::~ClippedTextElement() {
}

ElementType ClippedTextElement::elementType() const {
    return ElementType::ClippedText;
}

std::uint32_t ClippedTextElement::minimumFormatVersion() const {
    if (static_cast<std::uint8_t>(m_textFlags) != 0) {
        return 2;
    } else {
        return 1;
    }
}

std::size_t ClippedTextElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::ClippedText));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    if (formatVersion >= 2) {
        pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
        pos = writeU8LE(target, pos, 0);
    }
    pos = writeU16LE(target, pos, m_fontIndex);
    pos = writeU16LE(target, pos, m_text.size());
    pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text}));
    return alignNextWrite(target, pos, 4);
}

nlohmann::json ClippedTextElement::serializeJSON(std::uint32_t formatVersion) const {
    nlohmann::json result;
    result["type"] = "ClippedText";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    if (formatVersion >= 2) {
        result["textFlags"] = m_textFlags;
    }
    result["fontIndex"] = m_fontIndex;
    result["text"] = m_text;
    return result;
}

void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = animationTick;
    std::ignore = currentTimestamp;

    std::span<std::byte const> fontData;
    if (m_fontIndex & 0x8000u) {
        std::size_t fontIndex = m_fontIndex & ~0x8000u;
        if (fontIndex >= customFonts.size()) {
            return;
        }
        fontData = customFonts[fontIndex]->fontData();
    } else {
        fontData = findEmbeddedFont(m_fontIndex);
    }
    if (fontData.size() < 23) {
        return;
    }

    ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};

    std::optional<bool> bgColor;
    bgColor = m_textFlags.dark;
    bool fgColor = !m_textFlags.dark;

    auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);
    if (m_textFlags.autoWordWrap) {
        std::string_view text = m_text;
        auto dimensions = renderer.getRenderedDimensions(text, Point{0, 0});
        if (!dimensions) {
            return;
        }
        std::int32_t y = 0;
        while (dimensions->boundingBox && dimensions->boundingBox->size.width > m_width) {
            std::size_t pos = text.find('\n');
            if (pos == std::string_view::npos) {
                pos = text.size();
            }
            std::string_view currentLine = text.substr(0, pos);
            if (pos < text.size()) {
                text = text.substr(pos + 1);
            } else {
                text = text.substr(pos);
            }
            while (currentLine.size() > 0) {
                dimensions = renderer.getRenderedDimensions(currentLine, Point{0, 0});
                if (!dimensions) {
                    return;
                }
                std::size_t pos = currentLine.size();
                while (pos > 0 && dimensions->boundingBox && dimensions->boundingBox->size.width > m_width) {
                    std::size_t prevPos = pos;
                    pos = currentLine.rfind(' ', prevPos - 1);
                    if (pos == std::string_view::npos) {
                        pos = prevPos;
                        break;
                    }
                    dimensions = renderer.getRenderedDimensions(currentLine.substr(0, pos), Point{0, 0});
                    if (!dimensions) {
                        return;
                    }
                }
                renderer.render(currentLine.substr(0, pos), Point{0, y + static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
                y += renderer.lineHeight();
                if (pos < currentLine.size()) {
                    currentLine = currentLine.substr(pos + 1);
                } else {
                    currentLine = currentLine.substr(pos);
                }
            }
            dimensions = renderer.getRenderedDimensions(text, Point{0, 0});
            if (!dimensions) {
                return;
            }
        }
        if (text.size() > 0) {
            renderer.render(text, Point{0, y + static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
            y += renderer.lineHeight();
        }
    } else {
        renderer.render(m_text, Point{0, static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
    }
}

std::expected<std::unique_ptr<ClippedTextElement>, ParseError> ClippedTextElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::ClippedText)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    std::expected<std::uint8_t, ParseError> textFlags = 0;
    if (formatVersion >= 2) {
        textFlags = readU8LE(buffer);
        if (!textFlags) {
            return std::unexpected(textFlags.error());
        }
        auto reserved1 = readU8LE(buffer);
        if (!reserved1) {
            return std::unexpected(reserved1.error());
        }
    }
    auto fontIndex = readU16LE(buffer);
    if (!fontIndex) {
        return std::unexpected(fontIndex.error());
    }
    auto textLength = readU16LE(buffer);
    if (!textLength) {
        return std::unexpected(textLength.error());
    }
    std::size_t alignedRoundedUp = (*textLength + 2 + 4 - 1) / 4 * 4 - 2;
    if (formatVersion >= 2) {
        alignedRoundedUp = (*textLength + 4 - 1) / 4 * 4;
    }
    auto textData = readBuffer(buffer, alignedRoundedUp);
    if (!textData) {
        return std::unexpected(textData.error());
    }
    std::string_view text{reinterpret_cast<char const*>(textData->data()), *textLength};
    auto result = std::make_unique<ClippedTextElement>(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, std::string{text});
    return result;
}

std::unique_ptr<ClippedTextElement> ClippedTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    TextFlags textFlags;
    if (formatVersion >= 2) {
        textFlags = j.at("textFlags");
    }
    std::uint16_t fontIndex = j.at("fontIndex");
    std::string text = j.at("text");

    auto result = std::make_unique<ClippedTextElement>(x, y, width, height, textFlags, fontIndex, std::move(text));
    return result;
}

HScrollTextElement::HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_textFlags{textFlags}
    , m_flags{flags}
    , m_scrollSpeed{scrollSpeed}
    , m_fontIndex{fontIndex}
    , m_text{std::move(text)}
{
}

std::uint16_t HScrollTextElement::x() const noexcept {
    return m_x;
}

std::uint16_t HScrollTextElement::y() const noexcept {
    return m_y;
}

std::uint16_t HScrollTextElement::width() const noexcept {
    return m_width;
}

std::uint16_t HScrollTextElement::height() const noexcept {
    return m_height;
}

TextFlags HScrollTextElement::textFlags() const noexcept {
    return m_textFlags;
}

ScrollFlags HScrollTextElement::flags() const noexcept {
    return m_flags;
}

std::uint8_t HScrollTextElement::scrollSpeed() const noexcept {
    return m_scrollSpeed;
}

std::uint16_t HScrollTextElement::fontIndex() const noexcept {
    return m_fontIndex;
}

std::string HScrollTextElement::text() const {
    return m_text;
}

HScrollTextElement::~HScrollTextElement() {
}

ElementType HScrollTextElement::elementType() const {
    return ElementType::HScrollText;
}

std::uint32_t HScrollTextElement::minimumFormatVersion() const {
    if (static_cast<std::uint8_t>(m_textFlags) != 0) {
        return 2;
    } else {
        return 1;
    }
}

std::size_t HScrollTextElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::HScrollText));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    if (formatVersion >= 2) {
        pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
        pos = writeU8LE(target, pos, 0);
    }
    pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
    pos = writeU8LE(target, pos, m_scrollSpeed);
    if (formatVersion >= 2) {
        pos = writeU16LE(target, pos, 0);
    }
    pos = writeU16LE(target, pos, m_fontIndex);
    pos = writeU16LE(target, pos, m_text.size());
    pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text}));
    return alignNextWrite(target, pos, 4);
}

nlohmann::json HScrollTextElement::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "HScrollText";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    if (formatVersion >= 2) {
        result["textFlags"] = m_textFlags;
    }
    result["flags"] = m_flags;
    result["scrollSpeed"] = int(m_scrollSpeed);
    result["fontIndex"] = m_fontIndex;
    result["text"] = m_text;
    return result;
}

void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = currentTimestamp;

    std::span<std::byte const> fontData;
    if (m_fontIndex & 0x8000u) {
        std::size_t fontIndex = m_fontIndex & ~0x8000u;
        if (fontIndex >= customFonts.size()) {
            return;
        }
        fontData = customFonts[fontIndex]->fontData();
    } else {
        fontData = findEmbeddedFont(m_fontIndex);
    }
    if (fontData.size() < 23) {
        return;
    }

    auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);
    auto dimensions = renderer.getRenderedDimensions(m_text, {0, static_cast<std::int32_t>(renderer.lineHeight() - 2)});
    if (!dimensions || !dimensions->boundingBox) {
        return;
    }

    std::uint16_t contentWidth = dimensions->boundingBox->size.width;

    ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};

    if (contentWidth == 0) {
        return;
    }

    std::optional<bool> bgColor;
    bgColor = m_textFlags.dark;
    bool fgColor = !m_textFlags.dark;

    bool restarting = !m_flags.endless;
    bool invert = m_flags.invertDirection;
    bool padBefore = m_flags.padBefore;
    bool padAfter = m_flags.padAfter;

    if (!padBefore && !padAfter && contentWidth < m_width) {
        std::uint16_t offset = invert ? (m_width - contentWidth) : 0;
        renderer.render(m_text, Point{offset, static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
        return;
    }

    std::int32_t totalSize = contentWidth;
    if (padBefore) {
        totalSize += m_width;
    }
    if (padAfter) {
        totalSize += m_width;
    }

    // FIXME: handle overflow here...
    std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;

    if (invert) {
        offset = totalSize - offset - 1;
    }
    if (restarting) {
        offset = std::min(offset, totalSize - m_width);
    }

    std::int32_t partOffset = 0;
    if (padBefore) {
        if (bgColor) {
            for (std::int32_t dy = 0; dy < m_height; ++dy) {
                for (std::int32_t dx = 0; dx < m_width; ++dx) {
                    std::int32_t ddx = dx + partOffset;
                    if ((ddx - offset) < 0) {
                        continue;
                    }
                    target.setPixel(ddx - offset, dy, *bgColor);
                }
            }
        }
        partOffset += m_width;
    }
    renderer.render(m_text, Point{partOffset - offset, static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
    partOffset += contentWidth;
    if (padAfter) {
        if (bgColor) {
            for (std::int32_t dy = 0; dy < m_height; ++dy) {
                for (std::int32_t dx = 0; dx < m_width; ++dx) {
                    std::int32_t ddx = dx + partOffset;
                    if ((ddx - offset) < 0) {
                        continue;
                    }
                    target.setPixel(ddx - offset, dy, *bgColor);
                }
            }
        }
        partOffset += m_width;
    }
    if (!restarting) {
        if (padBefore) {
            if (bgColor) {
                for (std::int32_t dy = 0; dy < m_height; ++dy) {
                    for (std::int32_t dx = 0; dx < m_width; ++dx) {
                        std::int32_t ddx = dx + partOffset;
                        if ((ddx - offset) < 0) {
                            continue;
                        }
                        target.setPixel(ddx - offset, dy, *bgColor);
                    }
                }
            }
            partOffset += m_width;
        }
        renderer.render(m_text, Point{partOffset - offset, static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
    }
}

std::expected<std::unique_ptr<HScrollTextElement>, ParseError> HScrollTextElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::HScrollText)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    std::expected<std::uint8_t, ParseError> textFlags = 0;
    if (formatVersion >= 2) {
        textFlags = readU8LE(buffer);
        if (!textFlags) {
            return std::unexpected(textFlags.error());
        }
        auto reserved1 = readU8LE(buffer);
        if (!reserved1) {
            return std::unexpected(reserved1.error());
        }
    }
    auto flags = readU8LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto scrollSpeed = readU8LE(buffer);
    if (!scrollSpeed) {
        return std::unexpected(scrollSpeed.error());
    }
    if (formatVersion >= 2) {
        auto reserved2 = readU16LE(buffer);
        if (!reserved2) {
            return std::unexpected(reserved2.error());
        }
    }
    auto fontIndex = readU16LE(buffer);
    if (!fontIndex) {
        return std::unexpected(fontIndex.error());
    }
    auto textLength = readU16LE(buffer);
    if (!textLength) {
        return std::unexpected(textLength.error());
    }
    std::size_t alignedRoundedUp = (*textLength + 4 - 1) / 4 * 4;
    auto textData = readBuffer(buffer, alignedRoundedUp);
    if (!textData) {
        return std::unexpected(textData.error());
    }
    std::string_view text{reinterpret_cast<char const*>(textData->data()), *textLength};
    auto result = std::make_unique<HScrollTextElement>(*x, *y, *width, *height, TextFlags{*textFlags}, ScrollFlags{*flags}, *scrollSpeed, *fontIndex, std::string{text});
    return result;
}

std::unique_ptr<HScrollTextElement> HScrollTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    TextFlags textFlags;
    if (formatVersion >= 2) {
        textFlags = j.at("textFlags");
    }
    ScrollFlags flags = j.at("flags");
    int scrollSpeed = j.at("scrollSpeed");
    std::uint16_t fontIndex = j.at("fontIndex");
    std::string text = j.at("text");

    if (scrollSpeed < 0 || scrollSpeed > 255) {
        throw std::runtime_error(std::format("Error unserializing an HScrollText element: invalid scroll speed {:d} specified", scrollSpeed));
    }

    auto result = std::make_unique<HScrollTextElement>(x, y, width, height, textFlags, flags, static_cast<std::uint8_t>(scrollSpeed), fontIndex, std::move(text));
    return result;

}

CurrentTimeElement::CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::uint16_t utcOffset, TimeDisplayFlags flags)
    : m_x{x}
    , m_y{y}
    , m_width{width}
    , m_height{height}
    , m_textFlags{textFlags}
    , m_fontIndex{fontIndex}
    , m_utcOffset{utcOffset}
    , m_flags{flags}
{
}

std::uint16_t CurrentTimeElement::x() const noexcept {
    return m_x;
}

std::uint16_t CurrentTimeElement::y() const noexcept {
    return m_y;
}

std::uint16_t CurrentTimeElement::width() const noexcept {
    return m_width;
}

std::uint16_t CurrentTimeElement::height() const noexcept {
    return m_height;
}

TextFlags CurrentTimeElement::textFlags() const noexcept {
    return m_textFlags;
}

std::uint16_t CurrentTimeElement::fontIndex() const noexcept {
    return m_fontIndex;
}

std::uint16_t CurrentTimeElement::utcOffset() const noexcept {
    return m_utcOffset;
}

TimeDisplayFlags CurrentTimeElement::flags() const noexcept {
    return m_flags;
}

CurrentTimeElement::~CurrentTimeElement() {
}

ElementType CurrentTimeElement::elementType() const {
    return ElementType::CurrentTime;
}

std::uint32_t CurrentTimeElement::minimumFormatVersion() const {
    if (static_cast<std::uint8_t>(m_textFlags) != 0) {
        return 2;
    } else {
        return 1;
    }
}

std::size_t CurrentTimeElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::CurrentTime));
    pos = writeU16LE(target, pos, m_x);
    pos = writeU16LE(target, pos, m_y);
    pos = writeU16LE(target, pos, m_width);
    pos = writeU16LE(target, pos, m_height);
    if (formatVersion >= 2) {
        pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
        pos = writeU8LE(target, pos, 0);
    }
    pos = writeU16LE(target, pos, m_fontIndex);
    pos = writeU16LE(target, pos, m_utcOffset);
    pos = writeU16LE(target, pos, static_cast<std::uint16_t>(m_flags));
    if (formatVersion >= 2) {
        pos = writeU16LE(target, pos, 0);
    }
    return alignNextWrite(target, pos, 4);
}

nlohmann::json CurrentTimeElement::serializeJSON(std::uint32_t formatVersion) const {
    nlohmann::json result;
    result["type"] = "CurrentTime";
    result["x"] = m_x;
    result["y"] = m_y;
    result["width"] = m_width;
    result["height"] = m_height;
    if (formatVersion >= 2) {
        result["textFlags"] = m_textFlags;
    }
    result["fontIndex"] = m_fontIndex;
    result["utcOffset"] = m_utcOffset;
    result["flags"] = m_flags;
    return result;
}

void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
    std::ignore = animationTick;

    std::span<std::byte const> fontData;
    if (m_fontIndex & 0x8000u) {
        std::size_t fontIndex = m_fontIndex & ~0x8000u;
        if (fontIndex >= customFonts.size()) {
            return;
        }
        fontData = customFonts[fontIndex]->fontData();
    } else {
        fontData = findEmbeddedFont(m_fontIndex);
    }
    if (fontData.size() < 23) {
        return;
    }

    bool use12h = m_flags.use12h;
    bool showHours = m_flags.showHours;
    bool showMinutes = m_flags.showMinutes;
    bool showSeconds = m_flags.showSeconds;
    if (showHours && showSeconds) {
        showMinutes = true;
    }

    std::optional<bool> bgColor;
    bgColor = m_textFlags.dark;
    bool fgColor = !m_textFlags.dark;

    ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};

    auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);

    std::uint32_t seconds = (currentTimestamp + static_cast<std::int64_t>(m_utcOffset) * 60) % 86400;
    std::uint32_t hours = seconds / 3600;
    std::uint32_t minutes = (seconds - hours * 3600) / 60;
    seconds = seconds % 60;
    if (use12h) {
        hours = hours % 12;
        if (hours == 0) {
            hours = 12;
        }
    }
    std::string text;
    if (showHours && showMinutes && showSeconds) {
        text = std::format("{:02d}:{:02d}:{:02d}", hours, minutes, seconds);
    } else if (showHours && showMinutes) {
        text = std::format("{:02d}:{:02d}", hours, minutes);
    } else if (showHours) {
        text = std::format("{:02d}", hours);
    } else if (showMinutes && showSeconds) {
        text = std::format("{:02d}:{:02d}", minutes, seconds);
    } else if (showSeconds) {
        text = std::format("{:02d}", seconds);
    } else {
        return;
    }

    renderer.render(text, Point{0, static_cast<std::int32_t>(renderer.lineHeight() - 2)}, target, fgColor, bgColor);
}

std::expected<std::unique_ptr<CurrentTimeElement>, ParseError> CurrentTimeElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    auto type = readU16LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint16_t>(ElementType::CurrentTime)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto x = readU16LE(buffer);
    if (!x) {
        return std::unexpected(x.error());
    }
    auto y = readU16LE(buffer);
    if (!y) {
        return std::unexpected(y.error());
    }
    auto width = readU16LE(buffer);
    if (!width) {
        return std::unexpected(width.error());
    }
    auto height = readU16LE(buffer);
    if (!height) {
        return std::unexpected(height.error());
    }
    std::expected<std::uint8_t, ParseError> textFlags = 0;
    if (formatVersion >= 2) {
        textFlags = readU8LE(buffer);
        if (!textFlags) {
            return std::unexpected(textFlags.error());
        }
        auto reserved1 = readU8LE(buffer);
        if (!reserved1) {
            return std::unexpected(reserved1.error());
        }
    }
    auto fontIndex = readU16LE(buffer);
    if (!fontIndex) {
        return std::unexpected(fontIndex.error());
    }
    auto utcOffset = readU16LE(buffer);
    if (!utcOffset) {
        return std::unexpected(utcOffset.error());
    }
    auto flags = readU16LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    if (formatVersion >= 2) {
        auto reserved2 = readU16LE(buffer);
        if (!reserved2) {
            return std::unexpected(reserved2.error());
        }
    }
    auto result = std::make_unique<CurrentTimeElement>(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, *utcOffset, TimeDisplayFlags{*flags});
    return result;
}

std::unique_ptr<CurrentTimeElement> CurrentTimeElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::uint16_t x = j.at("x");
    std::uint16_t y = j.at("y");
    std::uint16_t width = j.at("width");
    std::uint16_t height = j.at("height");
    TextFlags textFlags;
    if (formatVersion >= 2) {
        textFlags = j.at("textFlags");
    }
    std::uint16_t fontIndex = j.at("fontIndex");
    std::uint16_t utcOffset = j.at("utcOffset");
    TimeDisplayFlags flags = j.at("flags");

    auto result = std::make_unique<CurrentTimeElement>(x, y, width, height, textFlags, fontIndex, utcOffset, flags);
    return result;
}

AlwaysDrawnSection::~AlwaysDrawnSection() {
}

bool AlwaysDrawnSection::doesDrawOnFront() const noexcept {
    return m_drawOnFront;
}

bool AlwaysDrawnSection::doesDrawOnBack() const noexcept {
    return m_drawOnBack;
}

bool AlwaysDrawnSection::doesClearBeforeDrawing() const noexcept {
    return m_clearBeforeDrawing;
}

void AlwaysDrawnSection::setDrawOnFront(bool value) noexcept {
    m_drawOnFront = value;
}

void AlwaysDrawnSection::setDrawOnBack(bool value) noexcept {
    m_drawOnBack = value;
}

void AlwaysDrawnSection::setClearBeforeDrawing(bool value) noexcept {
    m_clearBeforeDrawing = value;
}

std::size_t AlwaysDrawnSection::elementCount() const noexcept {
    return m_elements.size();
}

Element* AlwaysDrawnSection::elementAt(std::size_t index) const {
    if (index < m_elements.size()) {
        return m_elements[index].get();
    } else {
        return nullptr;
    }
}

void AlwaysDrawnSection::appendElement(std::unique_ptr<Element> element) {
    m_elements.push_back(std::move(element));
}

void AlwaysDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
    if (index < m_elements.size()) {
        m_elements.insert(m_elements.begin() + index, std::move(element));
    } else {
        m_elements.push_back(std::move(element));
    }
}

std::unique_ptr<Element> AlwaysDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements[index] = std::move(element);
    }
    return result;
}

std::unique_ptr<Element> AlwaysDrawnSection::eraseElement(std::size_t index) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements.erase(m_elements.begin() + index);
    }
    return result;
}

SectionType AlwaysDrawnSection::sectionType() const {
    return SectionType::AlwaysDrawn;
}

std::uint32_t AlwaysDrawnSection::minimumFormatVersion() const {
    std::uint32_t result = 1;
    for (auto const& element : m_elements) {
        result = std::max(result, element->minimumFormatVersion());
    }
    return result;
}

std::size_t AlwaysDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::AlwaysDrawn));
    // Will be replaced later
    pos = writeU24LE(target, pos, 0);
    std::uint16_t flags = 0;
    maybeSetBit(flags, 0, m_drawOnFront);
    maybeSetBit(flags, 1, m_drawOnBack);
    maybeSetBit(flags, 2, m_clearBeforeDrawing);
    pos = writeU16LE(target, pos, flags);
    pos = writeU16LE(target, pos, m_elements.size());
    for (auto const& element : m_elements) {
        if (pos < target.size()) {
            pos += element->serializeTo(target.subspan(pos), formatVersion);
        } else {
            pos += element->serializeTo({}, formatVersion);
        }
    }
    pos = alignNextWrite(target, pos, 4);
    std::ignore = writeU24LE(target, 1, pos);
    return pos;
}

nlohmann::json AlwaysDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "AlwaysDrawn";
    result["flags"]["drawOnFront"] = m_drawOnFront;
    result["flags"]["drawOnBack"] = m_drawOnBack;
    result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
    result["elements"] = nlohmann::json::array();
    for (auto const& element : m_elements) {
        result["elements"].push_back(element->serializeJSON(formatVersion));
    }
    return result;
}

std::expected<std::unique_ptr<AlwaysDrawnSection>, ParseError> AlwaysDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    auto type = readU8LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint8_t>(SectionType::AlwaysDrawn)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto size = readU24LE(buffer);
    if (!size) {
        return std::unexpected(size.error());
    }
    if (*size < 8) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto flags = readU16LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto elementCount = readU16LE(buffer);
    if (!elementCount) {
        return std::unexpected(elementCount.error());
    }
    auto sectionData = buffer.subspan(0, *size - 8);
    buffer = buffer.subspan(*size - 8);
    auto section = std::make_unique<AlwaysDrawnSection>();
    section->setDrawOnFront(isBitSet(*flags, 0));
    section->setDrawOnBack(isBitSet(*flags, 1));
    section->setClearBeforeDrawing(isBitSet(*flags, 2));
    section->m_elements.reserve(*elementCount);
    for (std::size_t i = 0; i < *elementCount; ++i) {
        auto type = peekU16LE(sectionData);
        if (!type) {
            return std::unexpected(type.error());
        }
        std::expected<std::unique_ptr<Element>, ParseError> element;
        switch (static_cast<ElementType>(*type)) {
        case ElementType::Image:        element = ImageElement::parse(sectionData, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parse(sectionData, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::Line:         element = LineElement::parse(sectionData, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parse(sectionData, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parse(sectionData, formatVersion);  break;
        default:
            return std::unexpected(ParseError::InvalidValue);
        }
        if (!element) {
            return std::unexpected(element.error());
        }
        section->m_elements.push_back(std::move(*element));
    }
    return section;
}

std::unique_ptr<AlwaysDrawnSection> AlwaysDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    bool drawOnFront = j.at("flags").at("drawOnFront");
    bool drawOnBack = j.at("flags").at("drawOnBack");
    bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");

    auto section = std::make_unique<AlwaysDrawnSection>();
    section->setDrawOnFront(drawOnFront);
    section->setDrawOnBack(drawOnBack);
    section->setClearBeforeDrawing(clearBeforeDrawing);

    std::size_t const elementCount = j.at("elements").size();
    for (std::size_t i = 0; i < elementCount; ++i) {
        nlohmann::json const& jElement = j.at("elements").at(i);
        ElementType type = jElement.at("type");
        std::unique_ptr<Element> element;
        switch (type) {
        case ElementType::Image:        element = ImageElement::parseJSON(jElement, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parseJSON(jElement, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::Line:         element = LineElement::parseJSON(jElement, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parseJSON(jElement, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parseJSON(jElement, formatVersion);  break;
        default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
                                                      jElement.at("type").get<std::string>()));
        }
        section->m_elements.push_back(std::move(element));
    }
    return section;
}

TimeBasedDrawnSection::~TimeBasedDrawnSection() {
}

bool TimeBasedDrawnSection::doesDrawOnFront() const noexcept {
    return m_drawOnFront;
}

bool TimeBasedDrawnSection::doesDrawOnBack() const noexcept {
    return m_drawOnBack;
}

bool TimeBasedDrawnSection::doesClearBeforeDrawing() const noexcept {
    return m_clearBeforeDrawing;
}

void TimeBasedDrawnSection::setDrawOnFront(bool value) noexcept {
    m_drawOnFront = value;
}

void TimeBasedDrawnSection::setDrawOnBack(bool value) noexcept {
    m_drawOnBack = value;
}

void TimeBasedDrawnSection::setClearBeforeDrawing(bool value) noexcept {
    m_clearBeforeDrawing = value;
}

std::size_t TimeBasedDrawnSection::elementCount() const noexcept {
    return m_elements.size();
}

Element* TimeBasedDrawnSection::elementAt(std::size_t index) const {
    if (index < m_elements.size()) {
        return m_elements[index].get();
    } else {
        return nullptr;
    }
}

void TimeBasedDrawnSection::appendElement(std::unique_ptr<Element> element) {
    m_elements.push_back(std::move(element));
}

void TimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
    if (index < m_elements.size()) {
        m_elements.insert(m_elements.begin() + index, std::move(element));
    } else {
        m_elements.push_back(std::move(element));
    }
}

std::unique_ptr<Element> TimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements[index] = std::move(element);
    }
    return result;
}

std::unique_ptr<Element> TimeBasedDrawnSection::eraseElement(std::size_t index) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements.erase(m_elements.begin() + index);
    }
    return result;
}

std::int64_t TimeBasedDrawnSection::startTimestamp() const noexcept {
    return m_startTimestamp;
}

std::int64_t TimeBasedDrawnSection::endTimestamp() const noexcept {
    return m_endTimestamp;
}

void TimeBasedDrawnSection::setStartTimestamp(std::int64_t value) {
    m_startTimestamp = value;
}

void TimeBasedDrawnSection::setEndTimestamp(std::int64_t value) {
    m_endTimestamp = value;
}

SectionType TimeBasedDrawnSection::sectionType() const {
    return SectionType::TimeBasedDrawn;
}

std::uint32_t TimeBasedDrawnSection::minimumFormatVersion() const {
    std::uint32_t result = 1;
    for (auto const& element : m_elements) {
        result = std::max(result, element->minimumFormatVersion());
    }
    return result;
}

std::size_t TimeBasedDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::TimeBasedDrawn));
    // Will be replaced later
    pos = writeU24LE(target, pos, 0);
    std::uint16_t flags = 0;
    maybeSetBit(flags, 0, m_drawOnFront);
    maybeSetBit(flags, 1, m_drawOnBack);
    maybeSetBit(flags, 2, m_clearBeforeDrawing);
    pos = writeU16LE(target, pos, flags);
    pos = writeU16LE(target, pos, m_elements.size());
    pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_startTimestamp));
    pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_endTimestamp));
    for (auto const& element : m_elements) {
        if (pos < target.size()) {
            pos += element->serializeTo(target.subspan(pos), formatVersion);
        } else {
            pos += element->serializeTo({}, formatVersion);
        }
    }
    pos = alignNextWrite(target, pos, 4);
    std::ignore = writeU24LE(target, 1, pos);
    return pos;
}

nlohmann::json TimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
    nlohmann::json result;
    result["type"] = "TimeBasedDrawn";
    result["flags"]["drawOnFront"] = m_drawOnFront;
    result["flags"]["drawOnBack"] = m_drawOnBack;
    result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
    result["startTimestamp"] = m_startTimestamp;
    result["endTimestamp"] = m_endTimestamp;
    result["elements"] = nlohmann::json::array();
    for (auto const& element : m_elements) {
        result["elements"].push_back(element->serializeJSON(formatVersion));
    }
    return result;
}

std::expected<std::unique_ptr<TimeBasedDrawnSection>, ParseError> TimeBasedDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU8LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint8_t>(SectionType::TimeBasedDrawn)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto size = readU24LE(buffer);
    if (!size) {
        return std::unexpected(size.error());
    }
    if (*size < 24) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto flags = readU16LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto elementCount = readU16LE(buffer);
    if (!elementCount) {
        return std::unexpected(elementCount.error());
    }
    auto startTimestamp = readU64LE(buffer);
    if (!startTimestamp) {
        return std::unexpected(startTimestamp.error());
    }
    auto endTimestamp = readU64LE(buffer);
    if (!endTimestamp) {
        return std::unexpected(endTimestamp.error());
    }
    auto sectionData = buffer.subspan(0, *size - 24);
    buffer = buffer.subspan(*size - 24);
    auto section = std::make_unique<TimeBasedDrawnSection>();
    section->setDrawOnFront(isBitSet(*flags, 0));
    section->setDrawOnBack(isBitSet(*flags, 1));
    section->setClearBeforeDrawing(isBitSet(*flags, 2));
    section->setStartTimestamp(std::bit_cast<std::int64_t>(*startTimestamp));
    section->setEndTimestamp(std::bit_cast<std::int64_t>(*endTimestamp));
    section->m_elements.reserve(*elementCount);
    for (std::size_t i = 0; i < *elementCount; ++i) {
        auto type = peekU16LE(sectionData);
        if (!type) {
            return std::unexpected(type.error());
        }
        std::expected<std::unique_ptr<Element>, ParseError> element;
        switch (static_cast<ElementType>(*type)) {
        case ElementType::Image:        element = ImageElement::parse(sectionData, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parse(sectionData, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::Line:         element = LineElement::parse(sectionData, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parse(sectionData, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parse(sectionData, formatVersion);  break;
        default:
            return std::unexpected(ParseError::InvalidValue);
        }
        if (!element) {
            return std::unexpected(element.error());
        }
        section->m_elements.push_back(std::move(*element));
    }
    return section;
}

std::unique_ptr<TimeBasedDrawnSection> TimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    bool drawOnFront = j.at("flags").at("drawOnFront");
    bool drawOnBack = j.at("flags").at("drawOnBack");
    bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");
    std::uint64_t startTimestamp = j.at("startTimestamp");
    std::uint64_t endTimestamp = j.at("endTimestamp");

    auto section = std::make_unique<TimeBasedDrawnSection>();
    section->setDrawOnFront(drawOnFront);
    section->setDrawOnBack(drawOnBack);
    section->setClearBeforeDrawing(clearBeforeDrawing);
    section->setStartTimestamp(startTimestamp);
    section->setEndTimestamp(endTimestamp);

    std::size_t const elementCount = j.at("elements").size();
    for (std::size_t i = 0; i < elementCount; ++i) {
        nlohmann::json const& jElement = j.at("elements").at(i);
        ElementType type = jElement.at("type");
        std::unique_ptr<Element> element;
        switch (type) {
        case ElementType::Image:        element = ImageElement::parseJSON(jElement, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parseJSON(jElement, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::Line:         element = LineElement::parseJSON(jElement, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parseJSON(jElement, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parseJSON(jElement, formatVersion);  break;
        default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
                                                      jElement.at("type").get<std::string>()));
        }
        section->m_elements.push_back(std::move(element));
    }
    return section;
}

MultiTimeBasedDrawnSection::~MultiTimeBasedDrawnSection() {
}

bool MultiTimeBasedDrawnSection::doesDrawOnFront() const noexcept {
    return m_drawOnFront;
}

bool MultiTimeBasedDrawnSection::doesDrawOnBack() const noexcept {
    return m_drawOnBack;
}

bool MultiTimeBasedDrawnSection::doesClearBeforeDrawing() const noexcept {
    return m_clearBeforeDrawing;
}

void MultiTimeBasedDrawnSection::setDrawOnFront(bool value) noexcept {
    m_drawOnFront = value;
}

void MultiTimeBasedDrawnSection::setDrawOnBack(bool value) noexcept {
    m_drawOnBack = value;
}

void MultiTimeBasedDrawnSection::setClearBeforeDrawing(bool value) noexcept {
    m_clearBeforeDrawing = value;
}

std::size_t MultiTimeBasedDrawnSection::elementCount() const noexcept {
    return m_elements.size();
}

Element* MultiTimeBasedDrawnSection::elementAt(std::size_t index) const {
    if (index < m_elements.size()) {
        return m_elements[index].get();
    } else {
        return nullptr;
    }
}

void MultiTimeBasedDrawnSection::appendElement(std::unique_ptr<Element> element) {
    m_elements.push_back(std::move(element));
}

void MultiTimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
    if (index < m_elements.size()) {
        m_elements.insert(m_elements.begin() + index, std::move(element));
    } else {
        m_elements.push_back(std::move(element));
    }
}

std::unique_ptr<Element> MultiTimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements[index] = std::move(element);
    }
    return result;
}

std::unique_ptr<Element> MultiTimeBasedDrawnSection::eraseElement(std::size_t index) {
    std::unique_ptr<Element> result;
    if (index < m_elements.size()) {
        result = std::move(m_elements[index]);
        m_elements.erase(m_elements.begin() + index);
    }
    return result;
}

std::vector<std::pair<std::int64_t, std::int64_t>> MultiTimeBasedDrawnSection::timeRanges() const {
    return m_timeRanges;
}

void MultiTimeBasedDrawnSection::setTimeRanges(std::vector<std::pair<std::int64_t, std::int64_t>> ranges) {
    m_timeRanges = std::move(ranges);
}

SectionType MultiTimeBasedDrawnSection::sectionType() const {
    return SectionType::MultiTimeBasedDrawn;
}

std::uint32_t MultiTimeBasedDrawnSection::minimumFormatVersion() const {
    std::uint32_t result = 2;
    for (auto const& element : m_elements) {
        result = std::max(result, element->minimumFormatVersion());
    }
    return result;
}

std::size_t MultiTimeBasedDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::size_t pos = 0;
    pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::MultiTimeBasedDrawn));
    // Will be replaced later
    pos = writeU24LE(target, pos, 0);
    std::uint16_t flags = 0;
    maybeSetBit(flags, 0, m_drawOnFront);
    maybeSetBit(flags, 1, m_drawOnBack);
    maybeSetBit(flags, 2, m_clearBeforeDrawing);
    pos = writeU16LE(target, pos, flags);
    pos = writeU16LE(target, pos, m_elements.size());
    pos = writeU16LE(target, pos, m_timeRanges.size());
    pos = writeU16LE(target, pos, 0);
    for (std::size_t i = 0; i < m_timeRanges.size(); ++i) {
        pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_timeRanges[i].first));
        pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_timeRanges[i].second));
    }
    for (auto const& element : m_elements) {
        if (pos < target.size()) {
            pos += element->serializeTo(target.subspan(pos), formatVersion);
        } else {
            pos += element->serializeTo({}, formatVersion);
        }
    }
    pos = alignNextWrite(target, pos, 4);
    std::ignore = writeU24LE(target, 1, pos);
    return pos;
}

nlohmann::json MultiTimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
    nlohmann::json result;
    result["type"] = "MultiTimeBasedDrawn";
    result["flags"]["drawOnFront"] = m_drawOnFront;
    result["flags"]["drawOnBack"] = m_drawOnBack;
    result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
    result["timeRanges"] = m_timeRanges;
    result["elements"] = nlohmann::json::array();
    for (auto const& element : m_elements) {
        result["elements"].push_back(element->serializeJSON(formatVersion));
    }
    return result;
}

std::expected<std::unique_ptr<MultiTimeBasedDrawnSection>, ParseError> MultiTimeBasedDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU8LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint8_t>(SectionType::TimeBasedDrawn)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto size = readU24LE(buffer);
    if (!size) {
        return std::unexpected(size.error());
    }
    if (*size < 24) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto flags = readU16LE(buffer);
    if (!flags) {
        return std::unexpected(flags.error());
    }
    auto elementCount = readU16LE(buffer);
    if (!elementCount) {
        return std::unexpected(elementCount.error());
    }
    auto rangeCount = readU16LE(buffer);
    if (!rangeCount) {
        return std::unexpected(rangeCount.error());
    }
    auto reserved1 = readU16LE(buffer);
    if (!reserved1) {
        return std::unexpected(reserved1.error());
    }
    std::vector<std::pair<std::int64_t, std::int64_t>> timeRanges;
    timeRanges.reserve(*rangeCount);
    for (std::size_t i = 0; i < *rangeCount; ++i) {
        auto startTimestamp = readU64LE(buffer);
        if (!startTimestamp) {
            return std::unexpected(startTimestamp.error());
        }
        auto endTimestamp = readU64LE(buffer);
        if (!endTimestamp) {
            return std::unexpected(endTimestamp.error());
        }
        timeRanges.push_back(std::make_pair(*startTimestamp, *endTimestamp));
    }
    auto sectionData = buffer.subspan(0, *size - 24);
    buffer = buffer.subspan(*size - 24);
    auto section = std::make_unique<MultiTimeBasedDrawnSection>();
    section->setDrawOnFront(isBitSet(*flags, 0));
    section->setDrawOnBack(isBitSet(*flags, 1));
    section->setClearBeforeDrawing(isBitSet(*flags, 2));
    section->setTimeRanges(std::move(timeRanges));
    section->m_elements.reserve(*elementCount);
    for (std::size_t i = 0; i < *elementCount; ++i) {
        auto type = peekU16LE(sectionData);
        if (!type) {
            return std::unexpected(type.error());
        }
        std::expected<std::unique_ptr<Element>, ParseError> element;
        switch (static_cast<ElementType>(*type)) {
        case ElementType::Image:        element = ImageElement::parse(sectionData, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parse(sectionData, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
        case ElementType::Line:         element = LineElement::parse(sectionData, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parse(sectionData, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parse(sectionData, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parse(sectionData, formatVersion);  break;
        default:
            return std::unexpected(ParseError::InvalidValue);
        }
        if (!element) {
            return std::unexpected(element.error());
        }
        section->m_elements.push_back(std::move(*element));
    }
    return section;
}

std::unique_ptr<MultiTimeBasedDrawnSection> MultiTimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    bool drawOnFront = j.at("flags").at("drawOnFront");
    bool drawOnBack = j.at("flags").at("drawOnBack");
    bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");
    std::vector<std::pair<std::int64_t, std::int64_t>> timeRanges = j.at("timeRanges");

    auto section = std::make_unique<MultiTimeBasedDrawnSection>();
    section->setDrawOnFront(drawOnFront);
    section->setDrawOnBack(drawOnBack);
    section->setClearBeforeDrawing(clearBeforeDrawing);
    section->setTimeRanges(std::move(timeRanges));

    std::size_t const elementCount = j.at("elements").size();
    for (std::size_t i = 0; i < elementCount; ++i) {
        nlohmann::json const& jElement = j.at("elements").at(i);
        ElementType type = jElement.at("type");
        std::unique_ptr<Element> element;
        switch (type) {
        case ElementType::Image:        element = ImageElement::parseJSON(jElement, formatVersion);        break;
        case ElementType::Animation:    element = AnimationElement::parseJSON(jElement, formatVersion);    break;
        case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
        case ElementType::Line:         element = LineElement::parseJSON(jElement, formatVersion);         break;
        case ElementType::Box:          element = BoxElement::parseJSON(jElement, formatVersion);          break;
        case ElementType::ClippedText:  element = ClippedTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::HScrollText:  element = HScrollTextElement::parseJSON(jElement, formatVersion);  break;
        case ElementType::CurrentTime:  element = CurrentTimeElement::parseJSON(jElement, formatVersion);  break;
        default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
                                                      jElement.at("type").get<std::string>()));
        }
        section->m_elements.push_back(std::move(element));
    }
    return section;
}

ExpiryDateSection::~ExpiryDateSection() {
}

std::int64_t ExpiryDateSection::expiryDate() const noexcept {
    return m_expiryDate;
}

void ExpiryDateSection::setExpiryDate(std::int64_t value) noexcept {
    m_expiryDate = value;
}

SectionType ExpiryDateSection::sectionType() const {
    return SectionType::CustomFont;
}

std::uint32_t ExpiryDateSection::minimumFormatVersion() const {
    return 2;
}

std::size_t ExpiryDateSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::ExpiryDate));
    // Will be replaced later
    pos = writeU24LE(target, pos, 0);
    pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_expiryDate));
    pos = alignNextWrite(target, pos, 4);
    std::ignore = writeU24LE(target, 1, pos);
    return pos;
}

nlohmann::json ExpiryDateSection::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "ExpiryDate";
    result["expiryDate"] = m_expiryDate;
    return result;
}

std::expected<std::unique_ptr<ExpiryDateSection>, ParseError> ExpiryDateSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU8LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint8_t>(SectionType::ExpiryDate)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto size = readU24LE(buffer);
    if (!size) {
        return std::unexpected(size.error());
    }
    if (*size < 8) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto expiryDate = readU64LE(buffer);
    if (!expiryDate) {
        return std::unexpected(expiryDate.error());
    }

    auto section = std::make_unique<ExpiryDateSection>();
    section->setExpiryDate(std::bit_cast<std::int64_t>(*expiryDate));
    return section;
}

std::unique_ptr<ExpiryDateSection> ExpiryDateSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    std::int64_t expiryDate = j.at("expiryDate");

    auto section = std::make_unique<ExpiryDateSection>();
    section->setExpiryDate(expiryDate);
    return section;
}

CustomFontSection::~CustomFontSection() {
}

std::span<std::byte const> CustomFontSection::fontData() const {
    return std::span{m_fontData};
}

void CustomFontSection::setFontData(std::span<std::byte const> fontData) {
    m_fontData.resize(fontData.size());
    std::copy(fontData.begin(), fontData.end(), m_fontData.begin());
}

SectionType CustomFontSection::sectionType() const {
    return SectionType::CustomFont;
}

std::uint32_t CustomFontSection::minimumFormatVersion() const {
    return 1;
}

std::size_t CustomFontSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    std::size_t pos = 0;
    pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::CustomFont));
    // Will be replaced later
    pos = writeU24LE(target, pos, 0);
    pos = writeU24LE(target, pos, m_fontData.size());
    pos = writeU8LE(target, pos, 0);
    pos = writeBuffer(target, pos, m_fontData);
    pos = alignNextWrite(target, pos, 4);
    std::ignore = writeU24LE(target, 1, pos);
    return pos;
}

nlohmann::json CustomFontSection::serializeJSON(std::uint32_t formatVersion) const {
    std::ignore = formatVersion;

    nlohmann::json result;
    result["type"] = "CustomFont";
    result["data"] = base64Encode(m_fontData);
    return result;
}

std::expected<std::unique_ptr<CustomFontSection>, ParseError> CustomFontSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto type = readU8LE(buffer);
    if (!type) {
        return std::unexpected(type.error());
    }
    if (*type != static_cast<std::uint8_t>(SectionType::CustomFont)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto size = readU24LE(buffer);
    if (!size) {
        return std::unexpected(size.error());
    }
    if (*size < 8) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto actualSize = readU24LE(buffer);
    if (!actualSize) {
        return std::unexpected(actualSize.error());
    }
    auto reserved_ = readU8LE(buffer);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }
    auto fontData = buffer.subspan(0, *size - 8);
    buffer = buffer.subspan(*size - 8);

    auto actualFontData = readBuffer(fontData, *actualSize);
    if (!actualFontData) {
        return std::unexpected(actualFontData.error());
    }

    auto section = std::make_unique<CustomFontSection>();
    section->setFontData(*actualFontData);
    return section;
}

std::unique_ptr<CustomFontSection> CustomFontSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
    std::ignore = formatVersion;

    auto fontData = base64Decode(j.at("data").get<std::string>());

    auto section = std::make_unique<CustomFontSection>();
    section->setFontData(fontData);
    return section;
}

std::size_t serializeFile(std::span<std::byte>& buffer, File const& file) {
    std::size_t pos = 0;
    std::uint32_t formatVersion = 1;
    for (auto const& section : file.sections) {
        formatVersion = std::max(formatVersion, section->minimumFormatVersion());
    }
    pos = writeU8LE(buffer, pos, 0xAF);
    pos = writeU8LE(buffer, pos, 0x7E);
    pos = writeU8LE(buffer, pos, 0x2B);
    pos = writeU8LE(buffer, pos, 0x63);
    pos = writeU32LE(buffer, pos, formatVersion);
    pos = writeU16LE(buffer, pos, file.sections.size());
    pos = writeU16LE(buffer, pos, 0);
    pos = alignNextWrite(buffer, pos, 4);
    for (auto const& section : file.sections) {
        if (pos < buffer.size()) {
            pos += section->serializeTo(buffer.subspan(pos), formatVersion);
        } else {
            pos += section->serializeTo({}, formatVersion);
        }
    }
    return pos;
}

nlohmann::json serializeFileJSON(File const& file) {
    std::uint32_t formatVersion = 1;
    for (auto const& section : file.sections) {
        formatVersion = std::max(formatVersion, section->minimumFormatVersion());
    }
    nlohmann::json result;
    result["version"] = formatVersion;
    result["sections"] = nlohmann::json::array();
    for (auto const& section : file.sections) {
        result["sections"].push_back(section->serializeJSON(formatVersion));
    }
    return result;
}

std::expected<File, ParseError> parseFile(std::span<std::byte const> data) {
    auto magic = readU32LE(data);
    if (!magic) {
        return std::unexpected(magic.error());
    }
    if (*magic != UINT32_C(0x632B7EAF)) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto version = readU32LE(data);
    if (!version) {
        return std::unexpected(version.error());
    }
    if (*version < 1 || *version > 2) {
        return std::unexpected(ParseError::InvalidValue);
    }
    auto sectionCount = readU16LE(data);
    if (!sectionCount) {
        return std::unexpected(sectionCount.error());
    }
    auto reserved_ = readU16LE(data);
    if (!reserved_) {
        return std::unexpected(reserved_.error());
    }

    File result;
    result.sections.reserve(*sectionCount);

    for (std::size_t i = 0; i < *sectionCount; ++i) {
        auto sectionType = peekU8LE(data);
        if (!sectionType) {
            return std::unexpected(sectionType.error());
        }

        std::expected<std::unique_ptr<Section>, ParseError> section;

        switch (static_cast<SectionType>(*sectionType)) {
        case SectionType::AlwaysDrawn:           section = AlwaysDrawnSection::parse(data, *version);         break;
        case SectionType::TimeBasedDrawn:        section = TimeBasedDrawnSection::parse(data, *version);      break;
        case SectionType::MultiTimeBasedDrawn:   section = MultiTimeBasedDrawnSection::parse(data, *version); break;
        case SectionType::ExpiryDate:            section = ExpiryDateSection::parse(data, *version);          break;
        case SectionType::CustomFont:            section = CustomFontSection::parse(data, *version);          break;
        default: return std::unexpected(ParseError::InvalidValue);
    }

        if (!section) {
            return std::unexpected(section.error());
        }

        result.sections.push_back(std::move(*section));
    }

    return result;
}

File parseFileJSON(nlohmann::json const& j) {
    File result;

    std::uint32_t version = j.at("version");
    if (version < 1 || version > 2) {
        throw std::runtime_error(std::format("Unsupported file format version {:d} encountered while parsing a file", version));
    }
    std::size_t const sectionCount = j.at("sections").size();
    for (std::size_t i = 0; i < sectionCount; ++i) {
        nlohmann::json const& jSection = j.at("sections").at(i);
        SectionType sectionType = jSection.at("type");
        std::unique_ptr<Section> section;
        switch (sectionType) {
        case SectionType::AlwaysDrawn:           section = AlwaysDrawnSection::parseJSON(jSection, version);         break;
        case SectionType::TimeBasedDrawn:        section = TimeBasedDrawnSection::parseJSON(jSection, version);      break;
        case SectionType::MultiTimeBasedDrawn:   section = MultiTimeBasedDrawnSection::parseJSON(jSection, version); break;
        case SectionType::ExpiryDate:            section = ExpiryDateSection::parseJSON(jSection, version);          break;
        case SectionType::CustomFont:            section = CustomFontSection::parseJSON(jSection, version);          break;
        default: throw std::runtime_error(std::format("Unsupported section type {:s} encountered while parsing a file",
                                                      jSection.at("type").get<std::string>()));
        }
        result.sections.push_back(std::move(section));
    }

    return result;
}

constexpr static char const* const Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
constexpr static std::int8_t const Base64ParseLookup[256] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};

std::string base64Encode(std::span<std::byte const> buffer) {
    std::string result;

    uint8_t const* data = reinterpret_cast<uint8_t const*>(buffer.data());
    std::size_t numberOfBlocks = (buffer.size() + 2) / 3;
    result.reserve(numberOfBlocks);

    for (std::size_t i = 0; i < numberOfBlocks; ++i) {
        std::size_t ii = i * 3;
        std::size_t ni = std::min(ii + 3, buffer.size()) - ii;

        int c0 = (((data[ii + 0] >> 2) & 0x3f) << 0);
        if (ni == 3) {
            int c1 = (((data[ii + 0] >> 0) & 0x03) << 4)
                   | (((data[ii + 1] >> 4) & 0x0f) << 0);
            int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2)
                   | (((data[ii + 2] >> 6) & 0x03) << 0);
            int c3 = (((data[ii + 2] >> 0) & 0x3f) << 0);
            result.push_back(Base64Alphabet[c0]);
            result.push_back(Base64Alphabet[c1]);
            result.push_back(Base64Alphabet[c2]);
            result.push_back(Base64Alphabet[c3]);
        } else if (ni == 2) {
            int c1 = (((data[ii + 0] >> 0) & 0x03) << 4)
                   | (((data[ii + 1] >> 4) & 0x0f) << 0);
            int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2);
            result.push_back(Base64Alphabet[c0]);
            result.push_back(Base64Alphabet[c1]);
            result.push_back(Base64Alphabet[c2]);
        } else if (ni == 1) {
            int c1 = (((data[ii + 0] >> 0) & 0x03) << 4);
            result.push_back(Base64Alphabet[c0]);
            result.push_back(Base64Alphabet[c1]);
        }
    }
    return result;
}

std::vector<std::byte> base64Decode(std::string_view data) {
    std::vector<std::byte> result;
    // This is just an initial estimate
    std::size_t numberOfBlocks = (data.size() + 3) / 4;
    result.reserve(numberOfBlocks * 3);

    int state = 0;
    int currentValue = 0;
    for (std::size_t i = 0; i < data.size(); ++i) {
        int value = Base64ParseLookup[static_cast<std::uint8_t>(data[i])];
        if (value == -2) {
            // '=': padding at the end
            break;
        } else if (value == -1) {
            // any invalid character (especially whitespace): ignore
            continue;
        }

        switch (state) {
        case 0:
        default:
            currentValue = value << 2;
            break;
        case 1:
            currentValue |= (value >> 4) & 0x03;
            result.push_back(static_cast<std::byte>(currentValue));
            currentValue = (value & 0x0f) << 4;
            break;
        case 2:
            currentValue |= (value >> 2) & 0x0f;
            result.push_back(static_cast<std::byte>(currentValue));
            currentValue = (value & 0x03) << 6;
            break;
        case 3:
            currentValue |= value & 0x3f;
            result.push_back(static_cast<std::byte>(currentValue));
            currentValue = 0;
            break;
        }
        state = (state + 1) % 4;
    }

    // Ignore any errors here
    return result;
}

} // namespace monoformat
