#include "./vorbiscomment.h"
#include "./vorbiscommentids.h"

#include "../ogg/oggiterator.h"

#include "../diagnostics.h"
#include "../exceptions.h"

#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/binaryreader.h>
#include <c++utilities/io/binarywriter.h>
#include <c++utilities/io/copy.h>

#include <map>
#include <memory>

using namespace std;
using namespace CppUtilities;

namespace TagParser {

/*!
 * \class TagParser::VorbisComment
 * \brief Implementation of TagParser::Tag for Vorbis comments.
 */

const TagValue &VorbisComment::value(KnownField field) const
{
    switch (field) {
    case KnownField::Vendor:
        return vendor();
    default:
        return FieldMapBasedTag<VorbisComment>::value(field);
    }
}

bool VorbisComment::setValue(KnownField field, const TagValue &value)
{
    switch (field) {
    case KnownField::Vendor:
        setVendor(value);
        return true;
    default:
        return FieldMapBasedTag<VorbisComment>::setValue(field, value);
    }
}

VorbisComment::IdentifierType VorbisComment::internallyGetFieldId(KnownField field) const
{
    using namespace VorbisCommentIds;
    switch (field) {
    case KnownField::Album:
        return std::string(album());
    case KnownField::Artist:
        return std::string(artist());
    case KnownField::Comment:
        return std::string(comment());
    case KnownField::Cover:
        return std::string(cover());
    case KnownField::RecordDate:
        return std::string(date());
    case KnownField::Title:
        return std::string(title());
    case KnownField::Genre:
        return std::string(genre());
    case KnownField::TrackPosition:
        return std::string(trackNumber());
    case KnownField::DiskPosition:
        return std::string(diskNumber());
    case KnownField::PartNumber:
        return std::string(partNumber());
    case KnownField::Composer:
        return std::string(composer());
    case KnownField::Encoder:
        return std::string(encoder());
    case KnownField::EncodedBy:
        return std::string(encodedBy());
    case KnownField::EncoderSettings:
        return std::string(encoderSettings());
    case KnownField::Description:
        return std::string(description());
    case KnownField::Grouping:
        return std::string(grouping());
    case KnownField::RecordLabel:
        return std::string(label());
    case KnownField::Performers:
        return std::string(performer());
    case KnownField::Language:
        return std::string(language());
    case KnownField::Lyricist:
        return std::string(lyricist());
    case KnownField::Lyrics:
        return std::string(lyrics());
    case KnownField::AlbumArtist:
        return std::string(albumArtist());
    case KnownField::Conductor:
        return std::string(conductor());
    case KnownField::Copyright:
        return std::string(copyright());
    case KnownField::License:
        return std::string(license());
    case KnownField::Director:
        return std::string(director());
    case KnownField::ISRC:
        return std::string(isrc());
    case KnownField::Rating:
        return std::string(rating());
    case KnownField::Bpm:
        return std::string(bpm());
    case KnownField::Publisher:
        return std::string(publisher());
    case KnownField::PublisherWebpage:
        return std::string(publisherWebpage());
    case KnownField::PerformerWebpage:
        return std::string(website());
    case KnownField::Arranger:
        return std::string(arranger());
    default:
        return std::string();
    }
}

KnownField VorbisComment::internallyGetKnownField(const IdentifierType &id) const
{
    using namespace VorbisCommentIds;
    // clang-format off
    static const std::map<std::string_view, KnownField, CaseInsensitiveStringComparer> fieldMap({
         { album(), KnownField::Album },
         { artist(), KnownField::Artist },
         { comment(), KnownField::Comment },
         { cover(), KnownField::Cover },
         { date(), KnownField::RecordDate },
         { year(), KnownField::RecordDate },
         { title(), KnownField::Title },
         { genre(), KnownField::Genre },
         { trackNumber(), KnownField::TrackPosition },
         { diskNumber(), KnownField::DiskPosition },
         { partNumber(), KnownField::PartNumber },
         { composer(), KnownField::Composer },
         { encoder(), KnownField::Encoder },
         { encodedBy(), KnownField::EncodedBy },
         { encoderSettings(), KnownField::EncoderSettings },
         { description(), KnownField::Description },
         { grouping(), KnownField::Grouping },
         { label(), KnownField::RecordLabel },
         { performer(), KnownField::Performers },
         { lyricist(), KnownField::Lyricist },
         { lyrics(), KnownField::Lyrics },
         { albumArtist(), KnownField::AlbumArtist },
         { conductor(), KnownField::Conductor },
         { copyright(), KnownField::Copyright },
         { license(), KnownField::License },
         { director(), KnownField::Director },
         { isrc(), KnownField::ISRC },
         { rating(), KnownField::Rating },
         { bpm(), KnownField::Bpm },
         { publisher(), KnownField::Publisher },
         { publisherWebpage(), KnownField::PublisherWebpage },
         { website(), KnownField::PerformerWebpage },
         { arranger(), KnownField::Arranger },
    });
    // clang-format on
    const auto knownField(fieldMap.find(id));
    return knownField != fieldMap.cend() ? knownField->second : KnownField::Invalid;
}

/// \cond
void VorbisComment::extendPositionInSetField(std::string_view field, std::string_view totalField, const std::string &diagContext, Diagnostics &diag)
{
    auto totalValues = std::vector<std::int32_t>();
    auto fieldsIter = fields().equal_range(std::string(totalField));
    auto fieldsDist = std::distance(fieldsIter.first, fieldsIter.second);
    if (!fieldsDist) {
        return;
    }
    totalValues.reserve(static_cast<std::size_t>(fieldsDist));
    for (; fieldsIter.first != fieldsIter.second;) {
        try {
            totalValues.emplace_back(fieldsIter.first->second.value().toInteger());
            fields().erase(fieldsIter.first++);
        } catch (const ConversionException &e) {
            diag.emplace_back(DiagLevel::Warning, argsToString("Unable to parse \"", totalField, "\" as integer: ", e.what()), diagContext);
            totalValues.emplace_back(0);
            ++fieldsIter.first;
        }
    }

    auto totalIter = totalValues.begin(), totalEnd = totalValues.end();
    for (fieldsIter = fields().equal_range(std::string(field)); fieldsIter.first != fieldsIter.second && totalIter != totalEnd;
        ++fieldsIter.first, ++totalIter) {
        auto &v = fieldsIter.first->second.value();
        try {
            auto p = v.toPositionInSet();
            if (p.total() && p.total() != *totalIter) {
                diag.emplace_back(DiagLevel::Warning,
                    argsToString("The \"", totalField, "\" field value (", *totalIter, ") does not match \"", field, "\" field value (", p.total(),
                        "). Discarding the former in favor of the latter."),
                    diagContext);
            } else {
                p.setTotal(*totalIter);
                v.assignPosition(p);
            }
        } catch (const ConversionException &e) {
            diag.emplace_back(DiagLevel::Warning,
                argsToString("Unable to parse \"", field, "\" as position in set for incorporating \"", totalField, "\": ", e.what()), diagContext);
        }
    }
    if (totalIter != totalEnd) {
        diag.emplace_back(
            DiagLevel::Warning, argsToString("Vorbis Comment contains more \"", totalField, "\" fields than \"", field, "\" fields."), diagContext);
    }
    for (; totalIter != totalEnd; ++totalIter) {
        fields().insert(std::make_pair(field, VorbisCommentField(std::string(field), TagValue(PositionInSet(0, *totalIter)))));
    }
}
/// \endcond

/*!
 * \brief Converts TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields instead.
 */
void VorbisComment::convertTotalFields(const std::string &diagContext, Diagnostics &diag)
{
    extendPositionInSetField(VorbisCommentIds::trackNumber(), VorbisCommentIds::trackTotal(), diagContext, diag);
    extendPositionInSetField(VorbisCommentIds::diskNumber(), VorbisCommentIds::diskTotal(), diagContext, diag);
    extendPositionInSetField(VorbisCommentIds::partNumber(), VorbisCommentIds::partTotal(), diagContext, diag);
}

/*!
 * \brief Internal implementation for parsing.
 */
template <class StreamType>
void VorbisComment::internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, std::uint64_t &padding, Diagnostics &diag)
{
    // prepare parsing
    static const string context("parsing Vorbis comment");
    const auto startOffset = static_cast<std::uint64_t>(stream.tellg());
    try {
        // read signature: 0x3 + "vorbis"
        char sig[8];
        bool skipSignature = flags & VorbisCommentFlags::NoSignature;
        if (!skipSignature) {
            CHECK_MAX_SIZE(7)
            stream.read(sig, 7);
            skipSignature = (BE::toInt<std::uint64_t>(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
        }
        if (skipSignature) {
            // read vendor (length prefixed string)
            {
                CHECK_MAX_SIZE(4)
                stream.read(sig, 4);
                const auto vendorSize = LE::toUInt32(sig);
                if (vendorSize <= maxSize) {
                    auto buff = make_unique<char[]>(vendorSize);
                    stream.read(buff.get(), vendorSize);
                    m_vendor.assignData(std::move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8);
                    // TODO: Is the vendor string actually UTF-8 (like the field values)?
                } else {
                    diag.emplace_back(DiagLevel::Critical, "Vendor information is truncated.", context);
                    throw TruncatedDataException();
                }
                maxSize -= vendorSize;
            }
            // read field count
            CHECK_MAX_SIZE(4)
            stream.read(sig, 4);
            std::uint32_t fieldCount = LE::toUInt32(sig);
            for (std::uint32_t i = 0; i < fieldCount; ++i) {
                // read fields
                VorbisCommentField field;
                try {
                    field.parse(stream, maxSize, diag);
                    fields().emplace(field.id(), std::move(field));
                } catch (const TruncatedDataException &) {
                    throw;
                } catch (const Failure &) {
                    // nothing to do here since notifications will be added anyways
                }
            }
            if (!(flags & VorbisCommentFlags::NoFramingByte)) {
                stream.ignore(); // skip framing byte
            }
            m_size = static_cast<std::uint64_t>(stream.tellg()) - startOffset;
            // turn "YEAR" into "DATE" (unless "DATE" exists)
            // note: "DATE" is an official field and "YEAR" only an unofficial one but present in some files. In consistency with
            //       MediaInfo and VLC player it is treated like "DATE" here.
            static const auto dateFieldId = std::string(VorbisCommentIds::date()), yearFieldId = std::string(VorbisCommentIds::year());
            if (fields().find(dateFieldId) == fields().end()) {
                const auto [first, end] = fields().equal_range(yearFieldId);
                for (auto i = first; i != end; ++i) {
                    fields().emplace(dateFieldId, std::move(i->second));
                }
                fields().erase(first, end);
            }
        } else {
            diag.emplace_back(DiagLevel::Critical, "Signature is invalid.", context);
            throw InvalidDataException();
        }
    } catch (const TruncatedDataException &) {
        m_size = static_cast<std::uint64_t>(stream.tellg()) - startOffset;
        diag.emplace_back(DiagLevel::Critical, "Vorbis comment is truncated.", context);
        throw;
    }

    // warn if there are bytes left in the last segment of the Ogg packet containing the comment
    if constexpr (std::is_same_v<std::decay_t<StreamType>, OggIterator>) {
        auto bytesRemaining = std::uint64_t();
        if (stream) {
            bytesRemaining = stream.remainingBytesInCurrentSegment();
            if (stream.currentPage().isLastSegmentUnconcluded()) {
                stream.nextSegment();
                if (stream) {
                    bytesRemaining += stream.remainingBytesInCurrentSegment();
                }
            }
        }
        if (bytesRemaining) {
            diag.emplace_back(DiagLevel::Information, argsToString(bytesRemaining, " bytes left in last segment."), context);
            padding += bytesRemaining;
        }
    }

    if (flags & VorbisCommentFlags::ConvertTotalFields) {
        convertTotalFields(context, diag);
    }
}

/*!
 * \brief Parses tag information using the specified Ogg \a iterator.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \throws Throws TagParser::Failure or a derived exception when a parsing
 *         error occurs.
 */
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, Diagnostics &diag)
{
    auto padding = std::uint64_t();
    internalParse(iterator, iterator.streamSize(), flags, padding, diag);
}

/*!
 * \brief Parses tag information using the specified Ogg \a iterator.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \throws Throws TagParser::Failure or a derived exception when a parsing
 *         error occurs.
 */
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, std::uint64_t &padding, Diagnostics &diag)
{
    internalParse(iterator, iterator.streamSize(), flags, padding, diag);
}

/*!
 * \brief Parses tag information using the specified Ogg \a iterator.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \throws Throws TagParser::Failure or a derived exception when a parsing
 *         error occurs.
 */
void VorbisComment::parse(istream &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag)
{
    auto padding = std::uint64_t();
    internalParse(stream, maxSize, flags, padding, diag);
}

/// \cond
static std::uint32_t makeField(VorbisCommentField &field, BinaryWriter &writer, VorbisCommentFlags flags, Diagnostics &diag)
{
    if (field.value().isEmpty()) {
        return 0;
    }
    try {
        if (field.make(writer, flags, diag)) {
            return 1;
        }
    } catch (const Failure &) {
    }
    return 0;
}
/// \endcond

/*!
 * \brief Writes tag information to the specified \a stream.
 *
 * \throws Throws std::ios_base::failure when an IO error occurs.
 * \throws Throws TagParser::Failure or a derived exception when a making
 *                error occurs.
 */
void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags, Diagnostics &diag)
{
    // prepare making
    static const string context("making Vorbis comment");
    string vendor;
    try {
        m_vendor.toString(vendor);
    } catch (const ConversionException &) {
        diag.emplace_back(DiagLevel::Warning, "Can not convert the assigned vendor to string.", context);
    }
    BinaryWriter writer(&stream);
    if (!(flags & VorbisCommentFlags::NoSignature)) {
        // write signature
        static const char sig[7] = { 0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 };
        stream.write(sig, sizeof(sig));
    }
    // write vendor
    writer.writeUInt32LE(static_cast<std::uint32_t>(vendor.size()));
    writer.writeString(vendor);
    // write field count later
    const auto fieldCountOffset = stream.tellp();
    writer.writeUInt32LE(0);
    // write fields
    std::uint32_t fieldsWritten = 0;
    static const auto coverId = std::string(VorbisCommentIds::cover());
    for (auto &i : fields()) {
        if (i.first != coverId) { // write cover at the end
            fieldsWritten += makeField(i.second, writer, flags, diag);
        }
    }
    if (const auto cover = fields().find(coverId); cover != fields().end()) {
        fieldsWritten += makeField(cover->second, writer, flags, diag);
    }
    // write field count
    const auto framingByteOffset = stream.tellp();
    stream.seekp(fieldCountOffset);
    writer.writeUInt32LE(fieldsWritten);
    stream.seekp(framingByteOffset);
    // write framing byte
    if (!(flags & VorbisCommentFlags::NoFramingByte)) {
        stream.put(0x01);
    }
}

} // namespace TagParser
