#include "./mediafileinfo.h"
#include "./abstracttrack.h"
#include "./backuphelper.h"
#include "./diagnostics.h"
#include "./exceptions.h"
#include "./locale.h"
#include "./progressfeedback.h"
#include "./signature.h"
#include "./tag.h"

#include "./id3/id3v1tag.h"
#include "./id3/id3v2tag.h"

#include "./wav/waveaudiostream.h"

#include "./mpegaudio/mpegaudioframestream.h"

#include "./adts/adtsstream.h"

#include "./ivf/ivfstream.h"

#include "./mp4/mp4atom.h"
#include "./mp4/mp4container.h"
#include "./mp4/mp4ids.h"
#include "./mp4/mp4tag.h"
#include "./mp4/mp4track.h"

#include "./matroska/ebmlelement.h"
#include "./matroska/matroskacontainer.h"
#include "./matroska/matroskatag.h"
#include "./matroska/matroskatrack.h"

#include "./ogg/oggcontainer.h"

#include "./flac/flacmetadata.h"
#include "./flac/flacstream.h"

#include <c++utilities/chrono/timespan.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/path.h>

#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <functional>
#include <iomanip>
#include <ios>
#include <memory>
#include <system_error>

using namespace std;
using namespace std::placeholders;
using namespace CppUtilities;

/*!
 * \namespace IoUtilities
 * \brief Contains utility classes helping to read and write streams.
 */

namespace TagParser {

/// \brief The MediaFileInfoPrivate struct contains private fields of the MediaFileInfo class.
struct MediaFileInfoPrivate {};

/*!
 * \class TagParser::MediaFileInfo
 * \brief The MediaFileInfo class allows to read and write tag information providing
 *        a container/tag format independent interface.
 *
 * It also provides some technical information such as contained streams.
 *
 * For examples see "cli/mainfeatures.cpp" of the tageditor repository.
 */

/*!
 * \brief Constructs a new MediaFileInfo for the specified file.
 *
 * \param path Specifies the absolute or relative path of the file.
 */
MediaFileInfo::MediaFileInfo(std::string &&path)
    : BasicFileInfo(std::move(path))
    , m_containerParsingStatus(ParsingStatus::NotParsedYet)
    , m_containerFormat(ContainerFormat::Unknown)
    , m_containerOffset(0)
    , m_paddingSize(0)
    , m_effectiveSize(0)
    , m_fileStructureFlags(MediaFileStructureFlags::None)
    , m_tracksParsingStatus(ParsingStatus::NotParsedYet)
    , m_tagsParsingStatus(ParsingStatus::NotParsedYet)
    , m_chaptersParsingStatus(ParsingStatus::NotParsedYet)
    , m_attachmentsParsingStatus(ParsingStatus::NotParsedYet)
    , m_minPadding(0)
    , m_maxPadding(0)
    , m_preferredPadding(0)
    , m_tagPosition(ElementPosition::BeforeData)
    , m_indexPosition(ElementPosition::BeforeData)
    , m_fileHandlingFlags(MediaFileHandlingFlags::ForceRewrite | MediaFileHandlingFlags::ForceTagPosition | MediaFileHandlingFlags::ForceIndexPosition
          | MediaFileHandlingFlags::NormalizeKnownTagFieldIds | MediaFileHandlingFlags::PreserveRawTimingValues)
    , m_maxFullParseSize(0x3200000)
{
}

/*!
 * \brief Constructs a new MediaFileInfo.
 */
MediaFileInfo::MediaFileInfo()
    : MediaFileInfo(std::string())
{
}

/*!
 * \brief Constructs a new MediaFileInfo.
 */
MediaFileInfo::MediaFileInfo(std::string_view path)
    : MediaFileInfo(std::string(path))
{
}

/*!
 * \brief Destroys the MediaFileInfo.
 */
MediaFileInfo::~MediaFileInfo()
{
}

/*!
 * \brief Parses the container format of the current file.
 *
 * This method parses the container of the current file format if it has not been
 * parsed yet.
 *
 * After calling this method the methods containerFormat(), containerFormatName(),
 * containerFormatAbbreviation(), containerFormatSubversion(), containerMimeType(),
 * container(), mp4Container() and matroskaContainer() will return the parsed
 * information.
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \sa isContainerParsed(), parseTracks(), parseTag(), parseChapters(), parseEverything()
 */
void MediaFileInfo::parseContainerFormat(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    CPP_UTILITIES_UNUSED(progress)

    // skip if container format already parsed
    if (containerParsingStatus() != ParsingStatus::NotParsedYet) {
        return;
    }

    static const string context("parsing file header");
    open(); // ensure the file is open
    m_containerFormat = ContainerFormat::Unknown;

    // file size
    m_paddingSize = 0;
    m_containerOffset = 0;
    std::size_t bytesSkippedBeforeContainer = 0;
    std::streamoff id3v2Size = 0;

    // read signatrue
    char buff[16];
    const char *const buffEnd = buff + sizeof(buff), *buffOffset;
startParsingSignature:
    if (progress.isAborted()) {
        diag.emplace_back(DiagLevel::Information, "Parsing the container format has been aborted.", context);
        return;
    }
    if (size() - containerOffset() >= 16) {
        stream().seekg(m_containerOffset, ios_base::beg);
        stream().read(buff, sizeof(buff));

        // skip zero/junk bytes
        // notes:
        // - Only skipping 4 or more consecutive zero bytes at this point because some signatures start with up to 4 zero bytes.
        // - It seems that most players/tools¹ skip junk bytes, at least when reading MP3 files. Hence the tagparser library is following
        //   the same approach. (¹e.g. ffmpeg: "[mp3 @ 0x559e1f4cbd80] Skipping 1670 bytes of junk at 1165.")
        std::size_t bytesSkipped = 0;
        for (buffOffset = buff; buffOffset != buffEnd && !(*buffOffset); ++buffOffset, ++bytesSkipped)
            ;
        if (bytesSkipped >= 4) {
        skipJunkBytes:
            m_containerOffset += static_cast<std::streamoff>(bytesSkipped);
            m_paddingSize += bytesSkipped;

            // give up after 0x800 bytes
            if ((bytesSkippedBeforeContainer += bytesSkipped) >= 0x800u) {
                m_containerParsingStatus = ParsingStatus::NotSupported;
                m_containerFormat = ContainerFormat::Unknown;
                m_containerOffset = id3v2Size;
                return;
            }

            // try again
            goto startParsingSignature;
        }

        // parse signature
        switch ((m_containerFormat = parseSignature(buff, sizeof(buff)))) {
        case ContainerFormat::Id3v2Tag:
            // save position of ID3v2 tag
            m_actualId3v2TagOffsets.push_back(m_containerOffset);
            if (m_actualId3v2TagOffsets.size() == 2) {
                diag.emplace_back(DiagLevel::Warning, "There is more than just one ID3v2 header at the beginning of the file.", context);
            }

            // read ID3v2 header
            stream().seekg(m_containerOffset + 5, ios_base::beg);
            stream().read(buff, 5);

            // set the container offset to skip ID3v2 header
            m_containerOffset += toNormalInt(BE::toInt<std::uint32_t>(buff + 1)) + 10;
            if ((*buff) & 0x10) {
                // footer present
                m_containerOffset += 10;
            }
            id3v2Size = m_containerOffset;

            // continue reading signature
            goto startParsingSignature;

        case ContainerFormat::Mp4:
        case ContainerFormat::QuickTime: {
            // MP4/QuickTime is handled using Mp4Container instance
            m_container = make_unique<Mp4Container>(*this, m_containerOffset);
            try {
                static_cast<Mp4Container *>(m_container.get())->validateElementStructure(diag, progress, &m_paddingSize);
            } catch (const OperationAbortedException &) {
                diag.emplace_back(DiagLevel::Information, "Validating the MP4 element structure has been aborted.", context);
            } catch (const Failure &) {
                m_containerParsingStatus = ParsingStatus::CriticalFailure;
            }
            break;
        }
        case ContainerFormat::Ebml: {
            // EBML/Matroska is handled using MatroskaContainer instance
            auto container = make_unique<MatroskaContainer>(*this, m_containerOffset);
            try {
                container->parseHeader(diag, progress);
                if (container->documentType() == "matroska") {
                    m_containerFormat = ContainerFormat::Matroska;
                } else if (container->documentType() == "webm") {
                    m_containerFormat = ContainerFormat::Webm;
                }
                if (isForcingFullParse()) {
                    // validating the element structure of Matroska files takes too long when
                    // parsing big files so do this only when explicitly desired
                    container->validateElementStructure(diag, progress, &m_paddingSize);
                    container->validateIndex(diag, progress);
                }
            } catch (const OperationAbortedException &) {
                diag.emplace_back(DiagLevel::Information, "Validating the Matroska element structure has been aborted.", context);
            } catch (const Failure &) {
                m_containerParsingStatus = ParsingStatus::CriticalFailure;
            }
            m_container = std::move(container);
            break;
        }
        case ContainerFormat::Ogg:
            // Ogg is handled by OggContainer instance
            m_container = make_unique<OggContainer>(*this, m_containerOffset);
            static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(isForcingFullParse());
            break;
        case ContainerFormat::Unknown:
        case ContainerFormat::ApeTag:
            // skip APE tag if the specified size makes sense at all
            if (m_containerFormat == ContainerFormat::ApeTag) {
                if (const auto apeEnd = m_containerOffset + 32 + LE::toUInt32(buff + 12); apeEnd <= static_cast<std::streamoff>(size())) {
                    // take record of APE tag
                    diag.emplace_back(DiagLevel::Critical,
                        argsToString("Found an APE tag at the beginning of the file at offset ", m_containerOffset,
                            ". This tag format is not supported and the tag will therefore be ignored. It will NOT be preserved when saving as "
                            "placing an APE tag at the beginning of a file is strongly unrecommended."),
                        context);
                    // continue reading signature
                    m_containerOffset = apeEnd;
                    goto startParsingSignature;
                }
                m_containerFormat = ContainerFormat::Unknown;
            }

            // check for magic numbers at odd offsets
            // -> check for tar (magic number at offset 0x101)
            if (size() > 0x107) {
                stream().seekg(0x101);
                stream().read(buff, 6);
                if (buff[0] == 0x75 && buff[1] == 0x73 && buff[2] == 0x74 && buff[3] == 0x61 && buff[4] == 0x72 && buff[5] == 0x00) {
                    m_containerFormat = ContainerFormat::Tar;
                    break;
                }
            }
            // skip previously determined zero-bytes or try our luck on the next byte
            if (!bytesSkipped) {
                ++bytesSkipped;
            }
            goto skipJunkBytes;
        default:;
        }
    }

    if (bytesSkippedBeforeContainer) {
        diag.emplace_back(DiagLevel::Warning, argsToString(bytesSkippedBeforeContainer, " bytes of junk skipped"), context);
    }

    // set parsing status
    if (m_containerParsingStatus == ParsingStatus::NotParsedYet) {
        if (m_containerFormat == ContainerFormat::Unknown) {
            m_containerParsingStatus = ParsingStatus::NotSupported;
        } else {
            m_containerParsingStatus = ParsingStatus::Ok;
        }
    }
}

/*!
 * \brief Parses the tracks of the current file.
 *
 * This method parses the tracks of the current file if not been parsed yet.
 * After calling this method the methods trackCount(), tracks(), and
 * hasTracksOfType() will return the parsed information.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \remarks parseContainerFormat() must be called before.
 * \sa areTracksParsed(), parseContainerFormat(), parseTags(), parseChapters(), parseEverything()
 */
void MediaFileInfo::parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    // skip if tracks already parsed
    if (tracksParsingStatus() != ParsingStatus::NotParsedYet) {
        return;
    }
    static const string context("parsing tracks");

    try {
        // parse tracks via container object
        if (m_container) {
            m_container->parseTracks(diag, progress);
            m_tracksParsingStatus = ParsingStatus::Ok;
            return;
        }

        // parse tracks via track object for "single-track"-formats
        switch (m_containerFormat) {
        case ContainerFormat::Adts:
            m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset);
            break;
        case ContainerFormat::Flac:
            m_singleTrack = make_unique<FlacStream>(*this, m_containerOffset);
            break;
        case ContainerFormat::Ivf:
            m_singleTrack = make_unique<IvfStream>(stream(), m_containerOffset);
            break;
        case ContainerFormat::MpegAudioFrames:
            m_singleTrack = make_unique<MpegAudioFrameStream>(stream(), m_containerOffset);
            break;
        case ContainerFormat::RiffWave:
            m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset);
            break;
        default:
            throw NotImplementedException();
        }
        if (m_containerFormat != ContainerFormat::Flac) {
            // ensure the effective size has been determined
            // note: This is not required for FLAC and should also be avoided as parseTags() will invoke
            //       parseTracks() when dealing with FLAC files.
            parseTags(diag, progress);
            m_singleTrack->setSize(m_effectiveSize);
        }
        m_singleTrack->parseHeader(diag, progress);

        // take padding for some "single-track" formats into account
        switch (m_containerFormat) {
        case ContainerFormat::Flac:
            m_paddingSize += static_cast<FlacStream *>(m_singleTrack.get())->paddingSize();
            break;
        default:;
        }

        m_tracksParsingStatus = ParsingStatus::Ok;

    } catch (const NotImplementedException &) {
        diag.emplace_back(DiagLevel::Information, "Parsing tracks is not implemented for the container format of the file.", context);
        m_tracksParsingStatus = ParsingStatus::NotSupported;
    } catch (const OperationAbortedException &) {
        diag.emplace_back(DiagLevel::Information, "Parsing tracks has been aborted.", context);
    } catch (const Failure &) {
        diag.emplace_back(DiagLevel::Critical, "Unable to parse tracks.", context);
        m_tracksParsingStatus = ParsingStatus::CriticalFailure;
    }
}

/*!
 * \brief Parses the tag(s) of the current file.
 *
 * This method parses the tag(s) of the current file if not been parsed yet.
 * After calling this method the methods id3v1Tag(), id3v2Tags(),
 * mp4Tag() and allTags() will return the parsed information.
 *
 * Previously assigned but not applied tag information will be discarted.
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \remarks parseContainerFormat() must be called before.
 * \sa isTagParsed(), parseContainerFormat(), parseTracks(), parseChapters(), parseEverything()
 */
void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    // skip if tags already parsed
    if (tagsParsingStatus() != ParsingStatus::NotParsedYet) {
        return;
    }
    static const string context("parsing tag");

    // check for ID3v1 tag
    auto effectiveSize = static_cast<std::streamoff>(size());
    if (effectiveSize >= 128) {
        m_id3v1Tag = make_unique<Id3v1Tag>();
        try {
            stream().seekg(effectiveSize - 128, std::ios_base::beg);
            m_id3v1Tag->parse(stream(), diag);
            m_fileStructureFlags += MediaFileStructureFlags::ActualExistingId3v1Tag;
            effectiveSize -= 128;
        } catch (const NoDataFoundException &) {
            m_id3v1Tag.reset();
        } catch (const OperationAbortedException &) {
            diag.emplace_back(DiagLevel::Information, "Parsing ID3v1 tag has been aborted.", context);
            return;
        } catch (const Failure &) {
            m_tagsParsingStatus = ParsingStatus::CriticalFailure;
            diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v1 tag.", context);
        }
    }

    // check for APE tag at the end of the file (APE tags a the beginning are already covered when parsing the container format)
    if (constexpr auto apeHeaderSize = 32; effectiveSize >= apeHeaderSize) {
        const auto footerOffset = effectiveSize - apeHeaderSize;
        char buffer[apeHeaderSize];
        stream().seekg(footerOffset, std::ios_base::beg);
        stream().read(buffer, sizeof(buffer));
        if (BE::toInt<std::uint64_t>(buffer) == 0x4150455441474558ul /* APETAGEX */) {
            // take record of APE tag
            const auto tagSize = static_cast<std::streamoff>(LE::toInt<std::uint32_t>(buffer + 12));
            const auto flags = LE::toInt<std::uint32_t>(buffer + 20);
            // subtract tag size (footer size and contents) from effective size
            if (tagSize <= effectiveSize) {
                effectiveSize -= tagSize;
            }
            // subtract header size (not included in tag size) from effective size if flags indicate presence of header
            if ((flags & 0x80000000u) && (apeHeaderSize <= effectiveSize)) {
                effectiveSize -= apeHeaderSize;
            }
            diag.emplace_back(DiagLevel::Warning,
                argsToString("Found an APE tag at the end of the file at offset ", (footerOffset - tagSize),
                    ". This tag format is not supported and the tag will therefore be ignored. It will be preserved when saving as-is."),
                context);
        }
    }

    // check for ID3v2 tags: the offsets of the ID3v2 tags have already been parsed when parsing the container format
    m_id3v2Tags.clear();
    for (const auto offset : m_actualId3v2TagOffsets) {
        auto id3v2Tag = make_unique<Id3v2Tag>();
        stream().seekg(offset, ios_base::beg);
        try {
            id3v2Tag->parse(stream(), size() - static_cast<std::uint64_t>(offset), diag);
            m_paddingSize += id3v2Tag->paddingSize();
        } catch (const NoDataFoundException &) {
            continue;
        } catch (const OperationAbortedException &) {
            diag.emplace_back(DiagLevel::Information, "Parsing ID3v2 tags has been aborted.", context);
            return;
        } catch (const Failure &) {
            m_tagsParsingStatus = ParsingStatus::CriticalFailure;
            diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v2 tag.", context);
        }
        m_id3v2Tags.emplace_back(id3v2Tag.release());
    }

    // compute effective size
    m_effectiveSize = static_cast<std::uint64_t>(effectiveSize - m_containerOffset);

    // check for tags in tracks (FLAC only) or via container object
    try {
        if (m_containerFormat == ContainerFormat::Flac) {
            parseTracks(diag, progress);
            if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
                m_tagsParsingStatus = m_tracksParsingStatus;
            }
            return;
        } else if (m_container) {
            m_container->parseTags(diag, progress);
        } else if (m_containerFormat != ContainerFormat::MpegAudioFrames) {
            throw NotImplementedException();
        }

        // set status, but do not override error/unsupported status form ID3 tags here
        if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
            m_tagsParsingStatus = ParsingStatus::Ok;
        }

    } catch (const NotImplementedException &) {
        // set status to not supported, but do not override parsing status from ID3 tags here
        if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
            m_tagsParsingStatus = ParsingStatus::NotSupported;
        }
        diag.emplace_back(DiagLevel::Information, "Parsing tags is not implemented for the container format of the file.", context);
    } catch (const OperationAbortedException &) {
        diag.emplace_back(DiagLevel::Information, "Parsing tags from container/streams has been aborted.", context);
        return;
    } catch (const Failure &) {
        m_tagsParsingStatus = ParsingStatus::CriticalFailure;
        diag.emplace_back(DiagLevel::Critical, "Unable to parse tag.", context);
    }
}

/*!
 * \brief Parses the chapters of the current file.
 *
 * This method parses the chapters of the current file if not been parsed yet.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \remarks parseContainerFormat() must be called before.
 * \sa areChaptersParsed(), parseContainerFormat(), parseTracks(), parseTags(), parseEverything()
 */
void MediaFileInfo::parseChapters(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    // skip if chapters already parsed
    if (chaptersParsingStatus() != ParsingStatus::NotParsedYet) {
        return;
    }
    static const string context("parsing chapters");

    try {
        // parse chapters via container object
        if (!m_container) {
            throw NotImplementedException();
        }
        m_container->parseChapters(diag, progress);
        m_chaptersParsingStatus = ParsingStatus::Ok;
    } catch (const NotImplementedException &) {
        m_chaptersParsingStatus = ParsingStatus::NotSupported;
        diag.emplace_back(DiagLevel::Information, "Parsing chapters is not implemented for the container format of the file.", context);
    } catch (const Failure &) {
        m_chaptersParsingStatus = ParsingStatus::CriticalFailure;
        diag.emplace_back(DiagLevel::Critical, "Unable to parse chapters.", context);
    }
}

/*!
 * \brief Parses the attachments of the current file.
 *
 * This method parses the attachments of the current file if not been parsed yet.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \remarks parseContainerFormat() must be called before.
 * \sa areChaptersParsed(), parseContainerFormat(), parseTracks(), parseTags(), parseEverything()
 */
void MediaFileInfo::parseAttachments(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    // skip if attachments already parsed
    if (attachmentsParsingStatus() != ParsingStatus::NotParsedYet) {
        return;
    }
    static const string context("parsing attachments");

    try {
        // parse attachments via container object
        if (!m_container) {
            throw NotImplementedException();
        }
        m_container->parseAttachments(diag, progress);
        m_attachmentsParsingStatus = ParsingStatus::Ok;
    } catch (const NotImplementedException &) {
        m_attachmentsParsingStatus = ParsingStatus::NotSupported;
        diag.emplace_back(DiagLevel::Information, "Parsing attachments is not implemented for the container format of the file.", context);
    } catch (const Failure &) {
        m_attachmentsParsingStatus = ParsingStatus::CriticalFailure;
        diag.emplace_back(DiagLevel::Critical, "Unable to parse attachments.", context);
    }
}

/*!
 * \brief Parses the container format, the tracks and the tag information of the current file.
 *
 * See the individual methods to for more details and exceptions which might be thrown.
 * \sa parseContainerFormat(), parseTracks(), parseTags()
 */
void MediaFileInfo::parseEverything(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    parseContainerFormat(diag, progress);
    if (progress.isAborted()) {
        return;
    }
    parseTracks(diag, progress);
    if (progress.isAborted()) {
        return;
    }
    parseTags(diag, progress);
    if (progress.isAborted()) {
        return;
    }
    parseChapters(diag, progress);
    if (progress.isAborted()) {
        return;
    }
    parseAttachments(diag, progress);
}

/*!
 * \brief Ensures appropriate tags are created according the given \a settings.
 * \return Returns whether appropriate tags could be created for the file.
 * \remarks
 *  - Tags must have been parsed before invoking this method (otherwise it will just return false).
 *  - The ID3 related arguments are only practiced when the file format is MP3 or when the file format is unknown and \a treatUnknownFilesAsMp3Files is true.
 *  - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed if \a id3v1Usage is set to TagUsage::Never.
 *  - The method might do nothing if present tag(s) already match the given specifications.
 *  - This is only a convenience method. The task could be done by manually using the methods createId3v1Tag(), createId3v2Tag(), removeId3v1Tag() ... as well.
 *  - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (TagSettings::id3v2usage is set to TagUsage::Never) and an ID3v1 tag will be created instead not all fields can be transferred.
 */
bool MediaFileInfo::createAppropriateTags(const TagCreationSettings &settings)
{
    // check if tags have been parsed yet (tags must have been parsed yet to create appropriate tags)
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet) {
        return false;
    }

    // check if tags need to be created/adjusted/removed
    const auto requiredTargets(settings.requiredTargets);
    const auto flags(settings.flags);
    const auto targetsRequired = !requiredTargets.empty() && (requiredTargets.size() != 1 || !requiredTargets.front().isEmpty());
    auto targetsSupported = false;
    if (areTagsSupported() && m_container) {
        // container object takes care of tag management
        if (targetsRequired) {
            // check whether container supports targets
            if (m_container->tagCount()) {
                // all tags in the container should support targets if the first one supports targets
                targetsSupported = m_container->tag(0)->supportsTarget();
            } else {
                // try to create a new tag and check whether targets are supported
                auto *const tag = m_container->createTag();
                if (tag && (targetsSupported = tag->supportsTarget())) {
                    tag->setTarget(requiredTargets.front());
                }
            }
            if (targetsSupported) {
                for (const auto &target : requiredTargets) {
                    m_container->createTag(target);
                }
            }
        } else {
            // no targets are required -> just ensure that at least one tag is present
            m_container->createTag();
        }
        return true;
    }

    // no container object present
    switch (m_containerFormat) {
    case ContainerFormat::Flac:
        static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
        break;
    default:
        // create ID3 tag(s)
        if (!hasAnyTag() && !(flags & TagCreationFlags::TreatUnknownFilesAsMp3Files)) {
            switch (containerFormat()) {
            case ContainerFormat::Adts:
            case ContainerFormat::Aiff:
            case ContainerFormat::MpegAudioFrames:
            case ContainerFormat::WavPack:
                break;
            default:
                return false;
            }
        }
        // create ID3 tags according to id3v2usage and id3v2usage
        // always create ID3v1 tag -> ensure there is one
        if (settings.id3v1usage == TagUsage::Always && !id3v1Tag()) {
            auto *const id3v1Tag = createId3v1Tag();
            if (flags & TagCreationFlags::Id3InitOnCreate) {
                for (const auto &id3v2Tag : id3v2Tags()) {
                    // overwrite existing values to ensure default ID3v1 genre "Blues" is updated as well
                    id3v1Tag->insertValues(*id3v2Tag, true);
                    // ID3v1 does not support all text encodings which might be used in ID3v2
                    id3v1Tag->ensureTextValuesAreProperlyEncoded();
                }
            }
        }
        if (settings.id3v2usage == TagUsage::Always && !hasId3v2Tag()) {
            // always create ID3v2 tag -> ensure there is one and set version
            auto *const id3v2Tag = createId3v2Tag();
            id3v2Tag->setVersion(settings.id3v2MajorVersion, 0);
            if ((flags & TagCreationFlags::Id3InitOnCreate) && id3v1Tag()) {
                id3v2Tag->insertValues(*id3v1Tag(), true);
            }
        }
    }

    if (flags & TagCreationFlags::MergeMultipleSuccessiveId3v2Tags) {
        mergeId3v2Tags();
    }
    // remove ID3 tags according to settings
    if (settings.id3v1usage == TagUsage::Never && hasId3v1Tag()) {
        // transfer tags to ID3v2 tag before removing
        if ((flags & TagCreationFlags::Id3TransferValuesOnRemoval) && hasId3v2Tag()) {
            id3v2Tags().front()->insertValues(*id3v1Tag(), false);
        }
        removeId3v1Tag();
    }
    if (settings.id3v2usage == TagUsage::Never) {
        if ((flags & TagCreationFlags::Id3TransferValuesOnRemoval) && hasId3v1Tag()) {
            // transfer tags to ID3v1 tag before removing
            for (const auto &tag : id3v2Tags()) {
                id3v1Tag()->insertValues(*tag, false);
            }
        }
        removeAllId3v2Tags();
    } else if (!(flags & TagCreationFlags::KeepExistingId3v2Version)) {
        // set version of ID3v2 tag according user preferences
        for (const auto &tag : id3v2Tags()) {
            tag->setVersion(settings.id3v2MajorVersion, 0);
        }
    }
    return true;
}

/*!
 * \brief Applies assigned/changed tag information to the current file.
 *
 * This method applies previously assigned tag information to the current file.
 *
 * Depending on the changes to be applied the file will be rewritten.
 *
 * When the file needs to be rewritten it will be renamed. A new file with the old name
 * will be created to replace the old file.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \throws Throws TagParser::Failure or a derived exception when a making error occurs.
 *
 * \remarks Tags and tracks need to be parsed without errors before this method can be called.
 *          All previous parsing results are cleared (using clearParsingResults()). Hence
 *          the file must be reparsed. All related objects (tags, tracks, ...) might get invalidated.
 *          This includes notifications of these objects as well.
 *
 * \sa clearParsingResults()
 */
void MediaFileInfo::applyChanges(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    static const string context("making file");
    diag.emplace_back(DiagLevel::Information, "Changes are about to be applied.", context);
    bool previousParsingSuccessful = true;
    switch (tagsParsingStatus()) {
    case ParsingStatus::Ok:
    case ParsingStatus::NotSupported:
        break;
    default:
        previousParsingSuccessful = false;
        diag.emplace_back(DiagLevel::Critical, "Tags have to be parsed without critical errors before changes can be applied.", context);
    }
    switch (tracksParsingStatus()) {
    case ParsingStatus::Ok:
    case ParsingStatus::NotSupported:
        break;
    default:
        previousParsingSuccessful = false;
        diag.emplace_back(DiagLevel::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context);
    }
    if (!previousParsingSuccessful) {
        throw InvalidDataException();
    }
    if (m_container) { // container object takes care
        // ID3 tags can not be applied in this case -> add warnings if ID3 tags have been assigned
        if (hasId3v1Tag()) {
            diag.emplace_back(DiagLevel::Warning, "Assigned ID3v1 tag can't be attached and will be ignored.", context);
        }
        if (hasId3v2Tag()) {
            diag.emplace_back(DiagLevel::Warning, "Assigned ID3v2 tag can't be attached and will be ignored.", context);
        }
        m_tracksParsingStatus = ParsingStatus::NotParsedYet;
        m_tagsParsingStatus = ParsingStatus::NotParsedYet;
        try {
            m_container->makeFile(diag, progress);
        } catch (...) {
            // since the file might be messed up, invalidate the parsing results
            clearParsingResults();
            throw;
        }
    } else { // implementation if no container object is present
        // assume the file is a MP3 file
        try {
            makeMp3File(diag, progress);
        } catch (...) {
            // since the file might be messed up, invalidate the parsing results
            clearParsingResults();
            throw;
        }
    }
    clearParsingResults();
}

/*!
 * \brief Returns the abbreviation of the container format.
 *
 * This abbreviation might be used as file extension.
 *
 * parseContainerFormat() needs to be called before. Otherwise
 * always an empty string will be returned.
 *
 * \sa containerFormat()
 * \sa containerFormatName()
 * \sa parseContainerFormat()
 */
string_view MediaFileInfo::containerFormatAbbreviation() const
{
    MediaType mediaType = MediaType::Unknown;
    unsigned int version = 0;
    switch (m_containerFormat) {
    case ContainerFormat::Ogg: {
        // check for video track or whether only Opus or Speex tracks are present
        const auto &tracks = static_cast<OggContainer *>(m_container.get())->tracks();
        if (tracks.empty()) {
            break;
        }
        bool onlyOpus = true, onlySpeex = true;
        for (const auto &track : static_cast<OggContainer *>(m_container.get())->tracks()) {
            if (track->mediaType() == MediaType::Video) {
                mediaType = MediaType::Video;
            }
            if (track->format().general != GeneralMediaFormat::Opus) {
                onlyOpus = false;
            }
            if (track->format().general != GeneralMediaFormat::Speex) {
                onlySpeex = false;
            }
        }
        if (onlyOpus) {
            version = static_cast<unsigned int>(GeneralMediaFormat::Opus);
        } else if (onlySpeex) {
            version = static_cast<unsigned int>(GeneralMediaFormat::Speex);
        }
        break;
    }
    case ContainerFormat::Matroska:
    case ContainerFormat::Mp4:
        mediaType = hasTracksOfType(MediaType::Video) ? MediaType::Video : MediaType::Audio;
        break;
    case ContainerFormat::MpegAudioFrames:
        if (m_singleTrack) {
            version = m_singleTrack->format().sub;
        }
        break;
    default:;
    }
    return TagParser::containerFormatAbbreviation(m_containerFormat, mediaType, version);
}

/*!
 * \brief Returns the MIME-type of the container format.
 *
 * parseContainerFormat() needs to be called before. Otherwise
 * always an empty string will be returned.
 *
 * \sa containerFormat()
 * \sa containerFormatName()
 * \sa parseContainerFormat()
 */
string_view MediaFileInfo::mimeType() const
{
    MediaType mediaType;
    switch (m_containerFormat) {
    case ContainerFormat::Mp4:
    case ContainerFormat::Ogg:
    case ContainerFormat::Matroska:
        mediaType = hasTracksOfType(MediaType::Video) ? MediaType::Video : MediaType::Audio;
        break;
    default:
        mediaType = MediaType::Unknown;
    }
    return TagParser::containerMimeType(m_containerFormat, mediaType);
}

/*!
 * \brief Returns the tracks for the current file.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns an empty vector.
 *
 * \remarks The MediaFileInfo keeps the ownership over the returned
 *          pointers. The returned Tracks will be destroyed when the
 *          MediaFileInfo is invalidated.
 *
 * \sa parseTracks()
 */
vector<AbstractTrack *> MediaFileInfo::tracks() const
{
    vector<AbstractTrack *> res;
    size_t trackCount = 0;
    size_t containerTrackCount = 0;
    if (m_singleTrack) {
        trackCount = 1;
    }
    if (m_container) {
        trackCount += (containerTrackCount = m_container->trackCount());
    }
    res.reserve(trackCount);

    if (m_singleTrack) {
        res.push_back(m_singleTrack.get());
    }
    for (size_t i = 0; i != containerTrackCount; ++i) {
        res.push_back(m_container->track(i));
    }
    return res;
}

/*!
 * \brief Returns an indication whether the current file has tracks of the specified \a type.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns false.
 *
 * \sa parseTracks()
 */
bool MediaFileInfo::hasTracksOfType(MediaType type) const
{
    if (tracksParsingStatus() == ParsingStatus::NotParsedYet) {
        return false;
    }
    if (m_singleTrack && m_singleTrack->mediaType() == type) {
        return true;
    } else if (m_container) {
        for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) {
            if (m_container->track(i)->mediaType() == type) {
                return true;
            }
        }
    }
    return false;
}

/*!
 * \brief Returns the overall duration of the file if known; otherwise
 *        returns a TimeSpan with zero ticks.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns false.
 *
 * \sa parseTracks()
 */
CppUtilities::TimeSpan MediaFileInfo::duration() const
{
    if (m_container) {
        return m_container->duration();
    } else if (m_singleTrack) {
        return m_singleTrack->duration();
    }
    return TimeSpan();
}

/*!
 * \brief Returns the overall average bitrate in kbit/s of the file if known; otherwise
 *        returns 0.0.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns false.
 *
 * \sa parseTracks()
 */
double MediaFileInfo::overallAverageBitrate() const
{
    const auto duration = this->duration();
    if (duration.isNull()) {
        return 0.0;
    }
    return 0.0078125 * static_cast<double>(size()) / duration.totalSeconds();
}

/*!
 * \brief Determines the available languages for specified media type (by default MediaType::Audio).
 *
 * If \a type is MediaType::Unknown, all media types are considered.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns an empty set.
 *
 * \sa parseTracks()
 */
unordered_set<string> MediaFileInfo::availableLanguages(MediaType type) const
{
    unordered_set<string> res;
    if (m_container) {
        for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) {
            const AbstractTrack *const track = m_container->track(i);
            if (type != MediaType::Unknown && track->mediaType() != type) {
                continue;
            }
            if (const auto &language = track->locale().someAbbreviatedName(); !language.empty()) {
                res.emplace(language);
            }
        }
    } else if (m_singleTrack && (type == MediaType::Unknown || m_singleTrack->mediaType() == type)) {
        if (const auto &language = m_singleTrack->locale().someAbbreviatedName(); !language.empty()) {
            res.emplace(language);
        }
    }
    return res;
}

/*!
 * \brief Generates a short technical summary about the file's tracks.
 *
 * parseTracks() needs to be called before. Otherwise this
 * method always returns an empty string.
 *
 * Example (exact format might change in the future!):
 * "H.264-720p / HE-AAC-6ch-eng / HE-AAC-2ch-ger / SRT-eng / SRT-ger"
 *
 * \sa parseTracks()
 */
string MediaFileInfo::technicalSummary() const
{
    if (m_container) {
        const size_t trackCount = m_container->trackCount();
        vector<string> parts;
        parts.reserve(trackCount);
        for (size_t i = 0; i != trackCount; ++i) {
            const string description(m_container->track(i)->description());
            if (!description.empty()) {
                parts.emplace_back(std::move(description));
            }
        }
        return joinStrings(parts, " / ");
    } else if (m_singleTrack) {
        return m_singleTrack->description();
    }
    return string();
}

/*!
 * \brief Removes a possibly assigned ID3v1 tag from the current file.
 *
 * To apply the removal and other changings call the applyChanges() method.
 *
 * \returns Returns whether there was an ID3v1 tag assigned which could be removed.
 * \remarks Invalidates the removed tag object (eg. returned via tags() or id3v1Tag()).
 * \sa applyChanges()
 */
bool MediaFileInfo::removeId3v1Tag()
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet) {
        return false;
    }
    if (m_id3v1Tag) {
        m_id3v1Tag.reset();
        return true;
    }
    return false;
}

/*!
 * \brief Creates an ID3v1 tag for the current file.
 *
 * This method does nothing the tags of the current file haven't been parsed using
 * the parseTags() method.
 *
 * If the file has already an ID3v1 tag no new tag will be created.
 *
 * To apply the created tag and other changings call the applyChanges() method.
 *
 * \returns Returns the ID3v1 tag of the current file or nullptr if the tag haven't been
 *          parsed yet.
 *
 * \sa applyChanges()
 */
Id3v1Tag *MediaFileInfo::createId3v1Tag()
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet) {
        return nullptr;
    }
    if (!m_id3v1Tag) {
        m_id3v1Tag = make_unique<Id3v1Tag>();
    }
    return m_id3v1Tag.get();
}

/*!
 * \brief Removes an assigned ID3v2 tag from the current file.
 *
 * To apply the removal and other changings call the applyChanges() method.
 *
 * \param tag Specifies the ID3v2 tag to be removed.
 * \returns Returns whether there the an ID3v2 tag could be removed.
 * \remarks Invalidates all removed tag objects (eg. returned via tags() or id3v2Tags()).
 * \sa applyChanges()
 */
bool MediaFileInfo::removeId3v2Tag(Id3v2Tag *tag)
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet) {
        return false;
    }
    for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) {
        if (i->get() == tag) {
            m_id3v2Tags.erase(i);
            return true;
        }
    }
    return false;
}

/*!
 * \brief Removes all assigned ID3v2 tags from the current file.
 *
 * To apply the removal and other changings call the applyChanges() method
 * \returns Returns whether there where ID3v2 tags assigned which could be removed.
 * \remarks Invalidates all removed tag objects (eg. returned via tags() or id3v2Tags()).
 * \sa applyChanges()
 */
bool MediaFileInfo::removeAllId3v2Tags()
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet || m_id3v2Tags.empty()) {
        return false;
    }
    m_id3v2Tags.clear();
    return true;
}

/*!
 * \brief Creates an ID3v2 tag for the current file.
 *
 * This method does nothing the tags of the current file haven't been parsed using
 * the parseTags() method.
 *
 * If the file has already an ID3v2 tag no new tag will be created.
 *
 * To apply the created tag and other changings call the applyChanges() method.
 *
 * \returns Returns the first ID3v2 tag of the current file.
 * \remarks The MediaFileInfo keeps the ownership over the created tag. It will be
 *          destroyed when the MediaFileInfo is invalidated.
 * \sa applyChanges()
 */
Id3v2Tag *MediaFileInfo::createId3v2Tag()
{
    if (m_id3v2Tags.empty()) {
        m_id3v2Tags.emplace_back(make_unique<Id3v2Tag>());
    }
    return m_id3v2Tags.front().get();
}

/*!
 * \brief Removes a possibly assigned \a tag from the current file.
 *
 * To apply the removal and other changings call the applyChanges() method.
 *
 * \param tag Specifies the tag to be removed. The tag will not only be detached from the
 *            file, it will be destroyed as well. Might be nullptr for convenience (eg.
 *            you might want to call file.removeTag(file.mp4Tag()) to ensure no MP4 tag
 *            is present without checking before).
 * \remarks Invalidates the removed tag object if it has been removed.
 *
 * \sa applyChanges()
 */
bool MediaFileInfo::removeTag(Tag *tag)
{
    if (!tag) {
        return false;
    }

    // remove tag via container
    if (m_container) {
        return m_container->removeTag(tag);
    }

    // remove tag via track for "single-track" formats
    if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) {
        auto *const flacStream(static_cast<FlacStream *>(m_singleTrack.get()));
        if (flacStream->vorbisComment() == tag) {
            return flacStream->removeVorbisComment();
        }
    }

    // remove ID3 tags
    if (m_id3v1Tag.get() == tag) {
        m_id3v1Tag.reset();
        return true;
    }
    for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) {
        if (i->get() == tag) {
            m_id3v2Tags.erase(i);
            return true;
        }
    }
    return false;
}

/*!
 * \brief Removes all assigned tags from the file.
 * \remarks Invalidates all removed tag objects (eg. returned via tags()).
 *
 * To apply the removal and other changings call the applyChanges() method.
 */
void MediaFileInfo::removeAllTags()
{
    if (m_container) {
        m_container->removeAllTags();
    }
    if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) {
        static_cast<FlacStream *>(m_singleTrack.get())->removeVorbisComment();
    }
    m_id3v1Tag.reset();
    m_id3v2Tags.clear();
}

/*!
 * \brief Returns an indication whether this library supports parsing the chapters of the current file.
 */
bool MediaFileInfo::areChaptersSupported() const
{
    if (m_container && m_container->chapterCount()) {
        return true;
    }
    switch (m_containerFormat) {
    case ContainerFormat::Matroska:
    case ContainerFormat::Webm:
        return true;
    default:
        return false;
    }
}

/*!
 * \brief Returns an indication whether this library supports attachment format of the current file.
 */
bool MediaFileInfo::areAttachmentsSupported() const
{
    if (m_container && m_container->attachmentCount()) {
        return true;
    }
    switch (m_containerFormat) {
    case ContainerFormat::Matroska:
    case ContainerFormat::Webm:
        return true;
    default:
        return false;
    }
}

/*!
 * \brief Returns an indication whether this library supports parsing the tracks information of the current file.
 */
bool MediaFileInfo::areTracksSupported() const
{
    if (trackCount()) {
        return true;
    }
    switch (m_containerFormat) {
    case ContainerFormat::Mp4:
    case ContainerFormat::MpegAudioFrames:
    case ContainerFormat::RiffWave:
    case ContainerFormat::Ogg:
    case ContainerFormat::Matroska:
    case ContainerFormat::Webm:
        return true;
    default:
        return false;
    }
}

/*!
 * \brief Returns an indication whether this library supports the tag format of the current file.
 */
bool MediaFileInfo::areTagsSupported() const
{
    switch (m_containerFormat) {
    case ContainerFormat::Adts:
    case ContainerFormat::Flac:
    case ContainerFormat::Matroska:
    case ContainerFormat::MpegAudioFrames:
    case ContainerFormat::Mp4:
    case ContainerFormat::Ogg:
    case ContainerFormat::WavPack:
    case ContainerFormat::Webm:
        // these container formats are supported
        return true;
    default:
        // the container format is unsupported
        // -> an ID3 tag might be already present, in this case the tags are considered supported
        return !m_container && (hasId3v1Tag() || hasId3v2Tag());
    }
}

/*!
 * \brief Returns a pointer to the assigned MP4 tag or nullptr if none is assigned.
 * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the
 *          MediaFileInfo is invalidated.
 */
Mp4Tag *MediaFileInfo::mp4Tag() const
{
    // simply return the first tag here since MP4 files never contain multiple tags
    return (m_containerFormat == ContainerFormat::Mp4 || m_containerFormat == ContainerFormat::QuickTime) && m_container
            && m_container->tagCount() > 0
        ? static_cast<Mp4Container *>(m_container.get())->tags().front().get()
        : nullptr;
}

/*!
 * \brief Returns pointers to the assigned Matroska tags.
 * \remarks The MediaFileInfo keeps the ownership over the returned
 *          pointers. The returned Matroska tags will be destroyed when the
 *          MediaFileInfo is invalidated.
 */
const vector<unique_ptr<MatroskaTag>> &MediaFileInfo::matroskaTags() const
{
    // matroska files might contain multiple tags (targeting different scopes)
    if (m_containerFormat == ContainerFormat::Matroska && m_container) {
        return static_cast<MatroskaContainer *>(m_container.get())->tags();
    } else {
        static const std::vector<std::unique_ptr<MatroskaTag>> empty;
        return empty;
    }
}

/*!
 * \brief Returns a pointer to the first assigned Vorbis comment or nullptr if none is assigned.
 * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the
 *          MediaFileInfo is invalidated.
 */
VorbisComment *MediaFileInfo::vorbisComment() const
{
    return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount()
        ? static_cast<OggContainer *>(m_container.get())->tags().front().get()
        : (m_containerFormat == ContainerFormat::Flac && m_singleTrack ? static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment() : nullptr);
}

/*!
 * \brief Returns all chapters assigned to the current file.
 * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the
 *          MediaFileInfo is invalidated.
 */
vector<AbstractChapter *> MediaFileInfo::chapters() const
{
    vector<AbstractChapter *> res;
    if (m_container) {
        const size_t count = m_container->chapterCount();
        res.reserve(count);
        for (size_t i = 0; i != count; ++i) {
            res.push_back(m_container->chapter(i));
        }
    }
    return res;
}

/*!
 * \brief Returns all attachments assigned to the current file.
 * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the
 *          MediaFileInfo is invalidated.
 */
vector<AbstractAttachment *> MediaFileInfo::attachments() const
{
    vector<AbstractAttachment *> res;
    if (m_container) {
        const size_t count = m_container->attachmentCount();
        res.reserve(count);
        for (size_t i = 0; i != count; ++i) {
            res.push_back(m_container->attachment(i));
        }
    }
    return res;
}

/*!
 * \brief Clears all parsing results and assigned/created/changed information such as
 *        detected container format, tracks, tags, ...
 *
 * This allows a rescan of the file using parsing methods like parseContainerFormat().
 * Otherwise, these methods do nothing if the information to be parsed has already been
 * gathered.
 *
 * \remarks Any pointers previously returned by tags(), tracks(), ... object should be
 *          considered invalidated.
 */
void MediaFileInfo::clearParsingResults()
{
    m_containerParsingStatus = ParsingStatus::NotParsedYet;
    m_containerFormat = ContainerFormat::Unknown;
    m_containerOffset = 0;
    m_paddingSize = 0;
    m_tracksParsingStatus = ParsingStatus::NotParsedYet;
    m_tagsParsingStatus = ParsingStatus::NotParsedYet;
    m_chaptersParsingStatus = ParsingStatus::NotParsedYet;
    m_attachmentsParsingStatus = ParsingStatus::NotParsedYet;
    m_id3v1Tag.reset();
    m_id3v2Tags.clear();
    m_actualId3v2TagOffsets.clear();
    m_fileStructureFlags = MediaFileStructureFlags::None;
    m_container.reset();
    m_singleTrack.reset();
}

/*!
 * \brief Writes the specified number of zeroes to \a outputStream.
 */
void MediaFileInfo::writePadding(std::ostream &outputStream, std::uint64_t size)
{
    static constexpr auto bufferSize = static_cast<std::uint64_t>(1024 * 8);
    for (constexpr char buffer[bufferSize] = {}; size;) {
        const auto count = std::min(bufferSize, size);
        outputStream.write(buffer, static_cast<std::streamsize>(count));
        size -= count;
    }
}

/*!
 * \brief Merges the assigned ID3v2 tags into a single ID3v2 tag.
 *
 * Some files I've got contain multiple successive ID3v2 tags. If the tags of
 * such an file is parsed by this class, these tags will be kept separate.
 * This method merges all assigned ID3v2 tags. All fields from the additional
 * ID3v2 tags will be inserted to the first tag. All assigned ID3v2 tag instances
 * except thefirst will be destroyed.
 *
 * A possibly assigned ID3v1 tag remains unaffected.
 *
 * This method does nothing the tags of the current file haven't been parsed using
 * the parseTags() method.
 *
 * \sa id3v2Tags()
 */
void MediaFileInfo::mergeId3v2Tags()
{
    auto begin = m_id3v2Tags.begin(), end = m_id3v2Tags.end();
    if (begin == end) {
        return;
    }
    Id3v2Tag &first = **begin;
    auto isecond = begin + 1;
    if (isecond == end) {
        return;
    }
    for (auto i = isecond; i != end; ++i) {
        first.insertFields(**i, false);
    }
    m_id3v2Tags.erase(isecond, end);
}

/*!
 * \brief Converts an existing ID3v1 tag into an ID3v2 tag.
 *
 * Effectively merges all ID3 tags into a single ID3v2 tag.
 *
 * \remarks Does nothing if
 * - there is no ID3v1 tag assigned.
 * - the file format isn't known to support ID3 tags (unless there is an ID3 tag present).
 * - the tags of the current file haven't been parsed using the parseTags() method.
 */
bool MediaFileInfo::id3v1ToId3v2()
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet || !areTagsSupported() || !hasId3v1Tag()) {
        return false;
    }
    return createAppropriateTags(TagCreationSettings{
        {}, TagCreationFlags::MergeMultipleSuccessiveId3v2Tags | TagCreationFlags::KeepExistingId3v2Version, TagUsage::Never, TagUsage::Always, 3 });
}

/*!
 * \brief Converts the existing ID3v2 tags into an ID3v1 tag.
 *
 * Effectively merges all ID3 tags into a single ID3v1 tag.
 *
 * \remarks Does nothing if
 * - there is not at least one ID3v2 tag assigned.
 * - the file format isn't known to support ID3 tags (unless there is an ID3 tag present).
 * - the tags of the current file haven't been parsed using the parseTags() method.
 */
bool MediaFileInfo::id3v2ToId3v1()
{
    if (tagsParsingStatus() == ParsingStatus::NotParsedYet || !areTagsSupported() || !hasId3v2Tag()) {
        return false;
    }
    return createAppropriateTags(TagCreationSettings{
        {}, TagCreationFlags::MergeMultipleSuccessiveId3v2Tags | TagCreationFlags::KeepExistingId3v2Version, TagUsage::Always, TagUsage::Never, 3 });
}

/*!
 * \brief Creates a Vorbis comment for the current file.
 *
 * This method does nothing if the tags/tracks of the current file haven't been parsed using
 * the parseTags() and parseTracks() methods.
 *
 * If the file has already a Vorbis comment no new tag will be created.
 *
 * To apply the created tag and other changings call the applyChanges() method.
 *
 * \returns Returns the Vorbis comment or nullptr if creation is not possible.
 *
 * \sa applyChanges()
 */
VorbisComment *MediaFileInfo::createVorbisComment()
{
    switch (m_containerFormat) {
    case ContainerFormat::Ogg:
        if (m_container) {
            return static_cast<OggContainer *>(m_container.get())->createTag(TagTarget());
        }
        break;
    case ContainerFormat::Flac:
        if (m_singleTrack) {
            return static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
        }
        break;
    default:;
    }
    return nullptr;
}

/*!
 * \brief Removes all assigned Vorbis comment from the current file.
 *
 * To apply the removal and other changings call the applyChanges() method.
 *
 * \returns Returns whether there was an Vorbis comment assigned which could be removed.
 *
 * \sa applyChanges()
 */
bool MediaFileInfo::removeVorbisComment()
{
    switch (m_containerFormat) {
    case ContainerFormat::Ogg:
        if (m_container) {
            bool hadTags = static_cast<OggContainer *>(m_container.get())->tagCount();
            static_cast<OggContainer *>(m_container.get())->removeAllTags();
            return hadTags;
        }
        break;
    case ContainerFormat::Flac:
        if (m_singleTrack) {
            return static_cast<FlacStream *>(m_singleTrack.get())->removeVorbisComment();
        }
        break;
    default:;
    }
    return false;
}

/*!
 * \brief Stores all tags assigned to the current file in the specified vector.
 * \remarks
 * - Previous elements of the vector will not be cleared.
 * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if
 *   those tags have not been stored to disk yet via applyChanges().
 * - The MediaFileInfo keeps the ownership over the tags which will be
 *   destroyed when the MediaFileInfo is invalidated.
 */
void MediaFileInfo::tags(std::vector<Tag *> &tags) const
{
    if (hasId3v1Tag()) {
        tags.push_back(m_id3v1Tag.get());
    }
    for (const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
        tags.push_back(tag.get());
    }
    if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) {
        if (auto *const vorbisComment = static_cast<const FlacStream *>(m_singleTrack.get())->vorbisComment()) {
            tags.push_back(vorbisComment);
        }
    }
    if (m_container) {
        for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
            tags.push_back(m_container->tag(i));
        }
    }
}

/*!
 * \brief Returns all tags assigned to the current file.
 * \remarks
 * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if
 *   those tags have not been stored to disk yet via applyChanges().
 * - The MediaFileInfo keeps the ownership over the tags which will be
 *   destroyed when the MediaFileInfo is invalidated.
 */
vector<Tag *> MediaFileInfo::tags() const
{
    auto res = vector<Tag *>();
    tags(res);
    return res;
}

/*!
 * \brief Returns an indication whether a tag of any format is assigned.
 * \remarks
 * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if
 *   those tags have not been stored to disk yet via applyChanges().
 */
bool MediaFileInfo::hasAnyTag() const
{
    return hasId3v1Tag() || hasId3v2Tag() || (m_container && m_container->tagCount())
        || (m_containerFormat == ContainerFormat::Flac && static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment());
}

/*!
 * \brief Returns all tags parsed from the current file.
 * \remarks
 * - Previous elements of the vector will not be cleared.
 * - Does **not** include tags which have been assigned, e.g. via createAppropriateTags() but
 *   have not been stored to disk yet via applyChanges().
 * - The MediaFileInfo keeps the ownership over the tags which will be
 *   destroyed when the MediaFileInfo is invalidated.
 */
void MediaFileInfo::parsedTags(std::vector<Tag *> &tags) const
{
    if (hasId3v1Tag() && m_id3v1Tag->size()) {
        tags.push_back(m_id3v1Tag.get());
    }
    for (const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
        if (tag->size()) {
            tags.push_back(tag.get());
        }
    }
    if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) {
        if (auto *const vorbisComment = static_cast<const FlacStream *>(m_singleTrack.get())->vorbisComment()) {
            if (vorbisComment->size()) {
                tags.push_back(vorbisComment);
            }
        }
    }
    if (m_container) {
        for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
            if (auto *const tag = m_container->tag(i); tag->size()) {
                tags.push_back(tag);
            }
        }
    }
}

/*!
 * \brief Returns all tags parsed from the current file.
 * \remarks
 * - Does **not** include tags which have been assigned, e.g. via createAppropriateTags() but
 *   have not been stored to disk yet via applyChanges().
 * - The MediaFileInfo keeps the ownership over the tags which will be
 *   destroyed when the MediaFileInfo is invalidated.
 */
std::vector<Tag *> MediaFileInfo::parsedTags() const
{
    auto res = vector<Tag *>();
    parsedTags(res);
    return res;
}

/*!
 * \brief Reimplemented from BasicFileInfo::invalidated().
 */
void MediaFileInfo::invalidated()
{
    BasicFileInfo::invalidated();
    clearParsingResults();
}

/*!
 * \brief Internally used to save chanings of MP3/FLAC files and any other files which might have ID3 tags.
 */
void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &progress)
{
    static const string context("making MP3/FLAC file");

    // don't rewrite the complete file if there are no ID3v2/FLAC tags present or to be written
    if (!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty()
        && m_containerFormat != ContainerFormat::Flac) {
        // alter ID3v1 tag
        if (!m_id3v1Tag) {
            // remove ID3v1 tag
            if (!(m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag)) {
                diag.emplace_back(DiagLevel::Information, "Nothing to be changed.", context);
                return;
            }
            progress.updateStep("Removing ID3v1 tag ...");
            stream().close();
            auto ec = std::error_code();
            std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), size() - 128, ec);
            if (!ec) {
                reportSizeChanged(size() - 128);
            } else {
                diag.emplace_back(DiagLevel::Critical, "Unable to truncate file to remove ID3v1 tag: " + ec.message(), context);
                throw std::ios_base::failure("Unable to truncate file to remove ID3v1 tag.");
            }
            return;
        } else {
            // add or update ID3v1 tag
            if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) {
                progress.updateStep("Updating existing ID3v1 tag ...");
                // ensure the file is still open / not readonly
                open();
                stream().seekp(-128, ios_base::end);
                try {
                    m_id3v1Tag->make(stream(), diag);
                } catch (const Failure &) {
                    diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
                }
            } else {
                progress.updateStep("Adding new ID3v1 tag ...");
                // ensure the file is still open / not readonly
                open();
                stream().seekp(0, ios_base::end);
                try {
                    m_id3v1Tag->make(stream(), diag);
                } catch (const Failure &) {
                    diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
                }
            }
            // prevent deferring final write operations (to catch and handle possible errors here)
            stream().flush();
        }
        return;
    }

    // ID3v2 needs to be modified
    FlacStream *const flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast<FlacStream *>(m_singleTrack.get()) : nullptr);
    progress.updateStep(flacStream ? "Updating FLAC tags ..." : "Updating ID3v2 tags ...");

    // prepare ID3v2 tags
    auto makers = std::vector<Id3v2TagMaker>();
    makers.reserve(m_id3v2Tags.size());
    auto tagsSize = std::uint64_t();
    for (auto &tag : m_id3v2Tags) {
        try {
            makers.emplace_back(tag->prepareMaking(diag));
            tagsSize += makers.back().requiredSize();
        } catch (const Failure &) {
        }
    }

    // determine stream offset and make track/format specific metadata
    auto streamOffset = std::uint32_t(); // where the actual stream starts
    auto flacMetaData = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
    flacMetaData.exceptions(std::ios_base::badbit | std::ios_base::failbit);
    auto startOfLastMetaDataBlock = std::streamoff();
    if (flacStream) {
        // if it is a raw FLAC stream, make FLAC metadata
        startOfLastMetaDataBlock = flacStream->makeHeader(flacMetaData, diag);
        tagsSize += static_cast<std::uint64_t>(flacMetaData.tellp());
        streamOffset = flacStream->streamOffset();
    } else {
        // make no further metadata, just use the container offset as stream offset
        streamOffset = static_cast<std::uint32_t>(m_containerOffset);
    }

    // check whether rewrite is required
    auto rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
    auto padding = std::size_t();
    if (!rewriteRequired) {
        // rewriting is not forced and new tag is not too big for available space
        // -> calculate new padding
        padding = streamOffset - tagsSize;
        // -> check whether the new padding matches specifications
        if (padding < minPadding() || padding > maxPadding()) {
            rewriteRequired = true;
        }
    }
    if (makers.empty() && !flacStream) {
        // an ID3v2 tag is not written and it is not a FLAC stream
        // -> can't include padding
        if (padding) {
            // but padding would be present -> need to rewrite
            padding = 0; // can't write the preferred padding despite rewriting
            rewriteRequired = true;
        }
    } else if (rewriteRequired) {
        // rewriting is forced or new ID3v2 tag is too big for available space
        // -> use preferred padding when rewriting anyways
        padding = preferredPadding();
    } else if (makers.empty() && flacStream && padding && padding < 4) {
        // no ID3v2 tag -> must include padding in FLAC stream
        // but padding of 1, 2, and 3 byte isn't possible -> need to rewrite
        padding = preferredPadding();
        rewriteRequired = true;
    }
    if (rewriteRequired && flacStream && makers.empty() && padding) {
        // the first 4 byte of FLAC padding actually don't count because these
        // can not be used for additional meta data
        padding += 4;
    }
    progress.updateStep(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ...");

    // setup stream(s) for writing
    // -> define variables needed to handle output stream and backup stream (required when rewriting the file)
    string originalPath = path(), backupPath;
    NativeFileStream &outputStream = stream();
    NativeFileStream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required

    if (rewriteRequired) {
        if (m_saveFilePath.empty()) {
            // move current file to temp dir and reopen it as backupStream, recreate original file
            try {
                BackupHelper::createBackupFileCanonical(backupDirectory(), originalPath, backupPath, outputStream, backupStream);
                // recreate original file, define buffer variables
                outputStream.open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
            } catch (const std::ios_base::failure &failure) {
                diag.emplace_back(
                    DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
                throw;
            }
        } else {
            // open the current file as backupStream and create a new outputStream at the specified "save file path"
            try {
                close();
                backupStream.exceptions(ios_base::badbit | ios_base::failbit);
                backupStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::binary);
                outputStream.open(BasicFileInfo::pathForOpen(m_saveFilePath).data(), ios_base::out | ios_base::binary | ios_base::trunc);
            } catch (const std::ios_base::failure &failure) {
                diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
                throw;
            }
        }

    } else { // !rewriteRequired
        // reopen original file to ensure it is opened for writing
        try {
            close();
            outputStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::out | ios_base::binary);
        } catch (const std::ios_base::failure &failure) {
            diag.emplace_back(DiagLevel::Critical, argsToString("Opening the file with write permissions failed: ", failure.what()), context);
            throw;
        }
    }
    // TODO: fix code duplication

    // start actual writing
    try {
        // ensure we can cast padding safely to uint32
        if (padding > numeric_limits<std::uint32_t>::max()) {
            padding = numeric_limits<std::uint32_t>::max();
            diag.emplace_back(
                DiagLevel::Critical, argsToString("Preferred padding is not supported. Setting preferred padding to ", padding, '.'), context);
        }

        if (!makers.empty()) {
            // write ID3v2 tags
            progress.updateStep("Writing ID3v2 tag ...");
            for (auto i = makers.begin(), end = makers.end() - 1; i != end; ++i) {
                i->make(outputStream, 0, diag);
            }
            // include padding into the last ID3v2 tag
            makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : static_cast<std::uint32_t>(padding), diag);
        }

        if (flacStream) {
            if (padding && startOfLastMetaDataBlock) {
                // if appending padding, ensure the last flag of the last "METADATA_BLOCK_HEADER" is not set
                flacMetaData.seekg(startOfLastMetaDataBlock);
                flacMetaData.seekp(startOfLastMetaDataBlock);
                flacMetaData.put(static_cast<std::uint8_t>(flacMetaData.peek()) & (0x80u - 1));
                flacMetaData.seekg(0);
            }

            // write FLAC metadata
            outputStream << flacMetaData.rdbuf();

            // write padding
            if (padding) {
                flacStream->makePadding(outputStream, static_cast<std::uint32_t>(padding), true, diag);
            }
        }

        if (makers.empty() && !flacStream) {
            writePadding(outputStream, padding);
        }

        // copy / skip actual stream data
        // -> determine media data size
        std::uint64_t mediaDataSize = size() - streamOffset;
        if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) {
            mediaDataSize -= 128;
        }

        if (rewriteRequired) {
            // copy data from original file
            switch (m_containerFormat) {
            case ContainerFormat::MpegAudioFrames:
                progress.updateStep("Writing MPEG audio frames ...");
                break;
            default:
                progress.updateStep("Writing data ...");
            }
            backupStream.seekg(static_cast<streamoff>(streamOffset));
            CopyHelper<0x4000> copyHelper;
            copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, std::bind(&AbortableProgressFeedback::isAborted, std::ref(progress)),
                std::bind(&AbortableProgressFeedback::updateStepPercentage, std::ref(progress), _1));
        } else {
            // just skip actual stream data
            outputStream.seekp(static_cast<std::streamoff>(mediaDataSize), ios_base::cur);
        }

        // write ID3v1 tag
        if (m_id3v1Tag) {
            progress.updateStep("Writing ID3v1 tag ...");
            try {
                m_id3v1Tag->make(stream(), diag);
            } catch (const Failure &) {
                diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
            }
        }

        // handle streams
        if (rewriteRequired) {
            // report new size
            reportSizeChanged(static_cast<std::uint64_t>(outputStream.tellp()));
            // "save as path" is now the regular path
            if (!saveFilePath().empty()) {
                reportPathChanged(saveFilePath());
                m_saveFilePath.clear();
            }
            // prevent deferring final write operations (to catch and handle possible errors here); stream is useless for further
            // usage anyways because it is write-only
            outputStream.close();
        } else {
            const auto newSize = static_cast<std::uint64_t>(outputStream.tellp());
            if (newSize < size()) {
                // file is smaller after the modification -> truncate
                // -> prevent deferring final write operations
                outputStream.close();
                // -> truncate file
                auto ec = std::error_code();
                std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), newSize, ec);
                if (!ec) {
                    reportSizeChanged(newSize);
                } else {
                    diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
                }
            } else {
                // file is longer after the modification
                // -> prevent deferring final write operations (to catch and handle possible errors here)
                outputStream.flush();
                // -> report new size
                reportSizeChanged(newSize);
            }
        }

    } catch (...) {
        BackupHelper::handleFailureAfterFileModifiedCanonical(*this, originalPath, backupPath, outputStream, backupStream, diag, context);
    }
}

} // namespace TagParser
