/**
 * @file BWFFile.cpp
 * @brief Implementation of BWFFile class
 *
 * BWFFile is an enhanced, industry-standard version of a standard WAV file
 */
#include "BWFFile.h"
#include "JS8_Include/pimpl_impl.h"

#include <QAudioFormat>
#include <QDate>
#include <QDateTime>
#include <QString>
#include <QTime>
#include <QTimeZone>
#include <QUuid>
#include <qendian.h>

#include <algorithm>
#include <cstring>
#include <numeric>

#include "moc_BWFFile.cpp"

namespace {
// chunk descriptor
struct Desc {
    Desc() : size_{0} {}

    explicit Desc(char const *id, quint32 size = 0) : size_{size} { set(id); }

    void set(char const *id = nullptr) {
        if (id) {
            auto end = reinterpret_cast<char const *>(::memchr(id, '\0', 4u));
            auto len = end ? end - id : 4u;
            ::memcpy(id_.data(), id, len);
            if (len < 4u) {
                memset(id_.data() + len, ' ', 4u - len);
            }
        } else {
            ::memcpy(id_.data(), "JUNK", 4u);
        }
    }

    void set(char const *id, quint32 size) {
        set(id);
        size_ = size;
    }

    char *operator&() { return reinterpret_cast<char *>(this); }
    char const *operator&() const {
        return reinterpret_cast<char const *>(this);
    }

    std::array<char, 4> id_;
    quint32 size_;
};

// "fmt " chunk contents
struct FormatChunk {
    quint16 audio_format;
    quint16 num_channels;
    quint32 sample_rate;
    quint32 byte_rate;
    quint16 block_align;
    quint16 bits_per_sample;
};

// "bext" chunk contents
struct BroadcastAudioExtension {
    using Version = BWFFile::BextVersion;
    using UMID = BWFFile::UMID;

    BroadcastAudioExtension(Version version = Version::v_0)
        : version_{static_cast<quint16>(version)}, umid_{{}} {
        // set some sensible defaults for the "bext" fields
        auto now = QDateTime::currentDateTimeUtc();
        ::memcpy(origination_date_,
                 now.date().toString("yyyy-MM-dd").toLocal8Bit().constData(),
                 sizeof origination_date_);
        ::memcpy(origination_time_,
                 now.time().toString("hh-mm-ss").toLocal8Bit().constData(),
                 sizeof origination_time_);
        auto uuid = QUuid::createUuid().toRfc4122();
        std::copy(uuid.cbegin(), uuid.cend(), umid_.data() + 16);
    }

    char description_[256];
    char originator_[32];
    char originator_reference_[32];
    char origination_date_[10];
    char origination_time_[10];
    quint32 time_reference_low_;
    quint32 time_reference_high_;
    quint16 version_;
    UMID umid_;                       // V1 zero for V0
    quint16 loudness_value_;          // V2
    quint16 loudness_range_;          // V2
    quint16 max_true_peak_level_;     // V2
    quint16 max_momentary_loudness_;  // V2
    quint16 max_short_term_loudness_; // V2
    quint8 reserved_[180];
    char coding_history_[];
};
} // namespace

class BWFFile::impl final {
  public:
    using FileError = QFile::FileError;

    impl(QAudioFormat const &format)
        : header_dirty_{true}, format_{format}, header_length_{-1},
          data_size_{-1}, error_{FileError::NoError} {}

    impl(QAudioFormat const &format, QString const &name)
        : header_dirty_{true}, format_{format}, file_{name}, header_length_{-1},
          data_size_{-1}, error_{FileError::NoError} {}

    impl(QAudioFormat const &format, QString const &name,
         InfoDictionary const &dictionary)
        : header_dirty_{true}, format_{format}, file_{name},
          info_dictionary_{dictionary}, header_length_{-1}, data_size_{-1},
          error_{FileError::NoError} {}

    ~impl() {
        if (file_.isOpen())
            file_.close();
    }

    bool initialize(BWFFile *self, OpenMode mode);
    bool read_header();
    bool write_header(QAudioFormat);
    bool update_header();

    BroadcastAudioExtension const *bext() const {
        return bext_.isEmpty()
                   ? nullptr
                   : reinterpret_cast<BroadcastAudioExtension const *>(
                         bext_.constData());
    }

    BroadcastAudioExtension *bext() {
        if (bext_.isEmpty()) // create a "bext" chunk in place
        {
            bext_.data();
            bext_.fill('\0', sizeof(BroadcastAudioExtension));
            new (bext_.data()) BroadcastAudioExtension{};
        }
        return reinterpret_cast<BroadcastAudioExtension *>(bext_.data());
    }

    bool header_dirty_;
    QAudioFormat format_;
    QFile file_;
    QByteArray bext_;
    InfoDictionary info_dictionary_;
    qint64 header_length_;
    qint64 data_size_;
    quint16 bits_per_sample_{0};
    quint16 block_align_{0};
    FileError error_;
};

bool BWFFile::impl::initialize(BWFFile *self, OpenMode mode) {
    bool result{false};
    if (mode & Append) {
        result = file_.seek(file_.size());
        if (result)
            result = self->seek(file_.size() - header_length_);
    } else {
        result = self->seek(0);
    }
    return result;
}

bool BWFFile::impl::read_header() {
    header_length_ = -1;
    data_size_ = -1;
    if (!(file_.openMode() & ReadOnly))
        return false;
    if (!file_.seek(0))
        return false;
    Desc outer_desc;
    quint32 outer_offset = file_.pos();
    quint32 outer_size{0};
    bool be{false};
    while (outer_offset <
           sizeof outer_desc + outer_desc.size_ - 1) // allow for uncounted pad
    {
        if (file_.read(&outer_desc, sizeof outer_desc) != sizeof outer_desc)
            return false;
        be = !memcmp(&outer_desc.id_, "RIFX", 4);
        outer_size = be ? qFromBigEndian<quint32>(outer_desc.size_)
                        : qFromLittleEndian<quint32>(outer_desc.size_);
        if (!memcmp(&outer_desc.id_, "RIFF", 4) || be) {
            // RIFF or RIFX
            char riff_item[4];
            if (file_.read(riff_item, sizeof riff_item) != sizeof riff_item)
                return false;
            if (!memcmp(riff_item, "WAVE", 4)) {
                // WAVE
                Desc wave_desc;
                quint32 wave_offset = file_.pos();
                quint32 wave_size{0};
                while (wave_offset <
                       outer_offset + sizeof outer_desc + outer_size - 1) {
                    if (file_.read(&wave_desc, sizeof wave_desc) !=
                        sizeof wave_desc)
                        return false;
                    wave_size =
                        be ? qFromBigEndian<quint32>(wave_desc.size_)
                           : qFromLittleEndian<quint32>(wave_desc.size_);
                    if (!memcmp(&wave_desc.id_, "bext", 4)) {
                        bext_ = file_.read(wave_size);
                    }
                    if (!memcmp(&wave_desc.id_, "fmt ", 4)) {
                        if (wave_size < sizeof(FormatChunk)) {
                            return false;
                        }

                        FormatChunk fmt;
                        if (file_.read(reinterpret_cast<char*>(&fmt), sizeof fmt) != sizeof fmt) {
                            return false;
                        }

                        const auto audio_format = be ? qFromBigEndian<quint16>(fmt.audio_format)
                                                     : qFromLittleEndian<quint16>(fmt.audio_format);
                        if (audio_format != 0 && audio_format != 1 && audio_format != 3) {
                            return false; // not PCM nor undefined
                        }

                        const auto ch   = be ? qFromBigEndian<quint16>(fmt.num_channels)
                                             : qFromLittleEndian<quint16>(fmt.num_channels);
                        const auto rate = be ? qFromBigEndian<quint32>(fmt.sample_rate)
                                             : qFromLittleEndian<quint32>(fmt.sample_rate);
                        const auto bits = be ? qFromBigEndian<quint16>(fmt.bits_per_sample)
                                             : qFromLittleEndian<quint16>(fmt.bits_per_sample);
                        const auto ba   = be ? qFromBigEndian<quint16>(fmt.block_align)
                                             : qFromLittleEndian<quint16>(fmt.block_align);

                        if (audio_format == 3 && bits != 32) {
                            return false;
                        }

                        bits_per_sample_ = bits;
                        block_align_ = ba;

                        format_.setChannelCount(ch);
                        format_.setSampleRate(rate);

                        switch (bits) {
                            case 8:  format_.setSampleFormat(QAudioFormat::UInt8); break;
                            case 16: format_.setSampleFormat(QAudioFormat::Int16); break;
                            case 24: format_.setSampleFormat(QAudioFormat::Int32); break; // NOTE: we'll convert data to int32
                            case 32:
                                if (audio_format == 3) {
                                    format_.setSampleFormat(QAudioFormat::Float);   // 32-bit float WAV
                                } else {
                                    format_.setSampleFormat(QAudioFormat::Int32);   // 32-bit int PCM
                                }
                                break;
                        default:
                            return false; // no 24-bit packed etc
                        }

                        if (bits != 24) {
                            if (ba != format_.bytesPerFrame())
                                return false;
                        } else {
                            // 24-bit packed: header must say 3 bytes/sample
                            if (ba != ch * 3)
                                return false;
                        }
                    } else if (!memcmp(&wave_desc.id_, "data", 4)) {
                        data_size_ = wave_size;
                        header_length_ = file_.pos();
                    } else if (!memcmp(&wave_desc.id_, "LIST", 4)) {
                        char list_type[4];
                        if (file_.read(list_type, sizeof list_type) !=
                            sizeof list_type)
                            return false;
                        if (!memcmp(list_type, "INFO", 4)) {
                            Desc info_desc;
                            quint32 info_offset = file_.pos();
                            quint32 info_size{0};
                            while (info_offset < wave_offset +
                                                     sizeof wave_desc +
                                                     wave_size - 1) {
                                if (file_.read(&info_desc, sizeof info_desc) !=
                                    sizeof info_desc)
                                    return false;
                                info_size = be ? qFromBigEndian<quint32>(
                                                     info_desc.size_)
                                               : qFromLittleEndian<quint32>(
                                                     info_desc.size_);
                                info_dictionary_[info_desc.id_] =
                                    file_.read(info_size);
                                if (!file_.seek(info_offset + sizeof info_desc +
                                                (info_size + 1) / 2 * 2))
                                    return false;
                                ;
                                info_offset = file_.pos();
                            }
                        }
                    }
                    if (!file_.seek(wave_offset + sizeof wave_desc +
                                    (wave_size + 1) / 2 * 2))
                        return false;
                    wave_offset = file_.pos();
                }
            }
        }
        if (!file_.seek(outer_offset + sizeof outer_desc +
                        (outer_size + 1) / 2 * 2))
            return false;
        outer_offset = file_.pos();
    }
    return data_size_ >= 0 && file_.seek(header_length_);
}

bool BWFFile::impl::write_header(QAudioFormat format) {
    data_size_ = -1;
    if (!(file_.openMode() & WriteOnly))
        return false;
    if (!file_.seek(0))
        return false;
    header_length_ = 0;
    //  bool be {QSysInfo::BigEndian == format_.byteOrder ()};
    bool be = false; // Should be OK if we only write RIFF files.
    Desc desc{be ? "RIFX" : "RIFF"};
    if (file_.write(&desc, sizeof desc) != sizeof desc)
        return false;
    header_dirty_ = true;
    if (file_.write("WAVE", 4) != 4)
        return false;

    FormatChunk fmt;
    if (be) {
        fmt.audio_format = qToBigEndian<quint16>(1); // PCM
        fmt.num_channels = qToBigEndian<quint16>(format.channelCount());
        fmt.sample_rate = qToBigEndian<quint32>(format.sampleRate());
        fmt.byte_rate = qToBigEndian<quint32>(format.bytesForDuration(1000000));
        fmt.block_align = qToBigEndian<quint16>(format.bytesPerFrame());
        fmt.bits_per_sample =
            qToBigEndian<quint16>(format.bytesPerSample() * 8);
        desc.set("fmt", qToBigEndian<quint32>(sizeof fmt));
    } else {
        fmt.audio_format = qToLittleEndian<quint16>(1); // PCM
        fmt.num_channels = qToLittleEndian<quint16>(format.channelCount());
        fmt.sample_rate = qToLittleEndian<quint32>(format.sampleRate());
        fmt.byte_rate =
            qToLittleEndian<quint32>(format.bytesForDuration(1000000));
        fmt.block_align = qToLittleEndian<quint16>(format.bytesPerFrame());
        fmt.bits_per_sample =
            qToLittleEndian<quint16>(format.bytesPerSample()) * 8;
        desc.set("fmt", qToLittleEndian<quint32>(sizeof fmt));
    }
    if (file_.write(&desc, sizeof desc) != sizeof desc)
        return false;
    if (file_.write(reinterpret_cast<char const *>(&fmt), sizeof fmt) !=
        sizeof fmt)
        return false;

    desc.set("data");
    if (file_.write(&desc, sizeof desc) != sizeof desc)
        return false;
    header_length_ = file_.pos();
    return true;
}

bool BWFFile::impl::update_header() {
    if (header_length_ < 0 || !(file_.openMode() & WriteOnly))
        return false;
    auto position = file_.pos();
    //  bool be {QSysInfo::BigEndian == format_.byteOrder ()};
    bool be = false;
    Desc desc;
    auto size = data_size_ < 0 ? file_.size() - header_length_ : data_size_;
    if (!file_.seek(header_length_ - sizeof desc))
        return false;
    desc.set("data",
             be ? qToBigEndian<quint32>(size) : qToLittleEndian<quint32>(size));
    if (file_.write(&desc, sizeof desc) != sizeof desc)
        return false;

    if (!bext_.isEmpty()) {
        if (!file_.seek(file_.size()))
            return false;
        auto size = bext_.size();
        desc.set("bext", be ? qToBigEndian<quint32>(size)
                            : qToLittleEndian<quint32>(size));
        if ((file_.size() % 2) && file_.write("\0", 1) != 1)
            return false;
        if (file_.write(&desc, sizeof desc) != sizeof desc)
            return false;
        auto *data = reinterpret_cast<BroadcastAudioExtension *>(bext_.data());
        if (be) {
            data->time_reference_low_ =
                qToBigEndian<quint32>(data->time_reference_low_);
            data->time_reference_high_ =
                qToBigEndian<quint32>(data->time_reference_high_);
            switch (static_cast<BextVersion>(data->version_)) {
            case BextVersion::v_0:
                data->version_ = qToBigEndian<quint32>(data->version_);
                // fall through
            default:
                data->loudness_value_ =
                    qToBigEndian<quint16>(data->loudness_value_);
                data->loudness_range_ =
                    qToBigEndian<quint16>(data->loudness_range_);
                data->max_true_peak_level_ =
                    qToBigEndian<quint16>(data->max_true_peak_level_);
                data->max_momentary_loudness_ =
                    qToBigEndian<quint16>(data->max_momentary_loudness_);
                data->max_short_term_loudness_ =
                    qToBigEndian<quint16>(data->max_short_term_loudness_);
            }
        } else {
            data->time_reference_low_ =
                qToLittleEndian<quint32>(data->time_reference_low_);
            data->time_reference_high_ =
                qToLittleEndian<quint32>(data->time_reference_high_);
            switch (static_cast<BextVersion>(data->version_)) {
            case BextVersion::v_0:
                data->version_ = qToLittleEndian<quint32>(data->version_);
                // fall through
            default:
                data->loudness_value_ =
                    qToLittleEndian<quint16>(data->loudness_value_);
                data->loudness_range_ =
                    qToLittleEndian<quint16>(data->loudness_range_);
                data->max_true_peak_level_ =
                    qToLittleEndian<quint16>(data->max_true_peak_level_);
                data->max_momentary_loudness_ =
                    qToLittleEndian<quint16>(data->max_momentary_loudness_);
                data->max_short_term_loudness_ =
                    qToLittleEndian<quint16>(data->max_short_term_loudness_);
            }
        }
        if (file_.write(bext_) != size)
            return false;
    }

    if (info_dictionary_.size()) {
        if (!file_.seek(file_.size()))
            return false;
        desc.set("LIST");
        if ((file_.size() % 2) && file_.write("\0", 1) != 1)
            return false;
        if (file_.write(&desc, sizeof desc) != sizeof desc)
            return false;
        auto list_start = file_.pos();
        if (file_.write("INFO", 4) != 4)
            return false;
        for (auto iter = info_dictionary_.constBegin();
             iter != info_dictionary_.constEnd(); ++iter) {
            auto value = iter.value();
            auto len = value.size() + 1; // include terminating null char
            desc.set(iter.key().data(), be ? qToBigEndian<quint32>(len)
                                           : qToLittleEndian<quint32>(len));
            if ((file_.size() % 2) && file_.write("\0", 1) != 1)
                return false;
            if (file_.write(&desc, sizeof desc) != sizeof desc)
                return false;
            if (file_.write(value.constData(), len) != len)
                return false;
        }
        auto size = file_.pos() - list_start;
        if (!file_.seek(list_start - sizeof desc))
            return false;
        desc.set("LIST", be ? qToBigEndian<quint32>(size)
                            : qToLittleEndian<quint32>(size));
        if (file_.write(&desc, sizeof desc) != sizeof desc)
            return false;
    }

    size = file_.size() - sizeof desc;
    if ((file_.size() % 2) && file_.seek(file_.size()) &&
        file_.write("\0", 1) != 1)
        return false;
    if (!file_.seek(0))
        return false;
    desc.set(be ? "RIFX" : "RIFF",
             be ? qToBigEndian<quint32>(size) : qToLittleEndian<quint32>(size));
    if (file_.write(&desc, sizeof desc) != sizeof desc)
        return false;
    return file_.seek(position);
}

//
// BWFFile implementation
//
BWFFile::BWFFile(QAudioFormat const &format, QObject *parent)
    : QIODevice{parent}, m_{format} {}

BWFFile::BWFFile(QAudioFormat const &format, QString const &name,
                 QObject *parent)
    : QIODevice{parent}, m_{format, name} {}

BWFFile::BWFFile(QAudioFormat const &format, QString const &name,
                 InfoDictionary const &dictionary, QObject *parent)
    : QIODevice{parent}, m_{format, name, dictionary} {}

BWFFile::~BWFFile() {
    if (isOpen())
        close();
}

bool BWFFile::open(OpenMode mode) {
    bool success{false};
    if (!(mode & WriteOnly)) {
        if (!(success = m_->file_.open(mode & ~Text))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success = m_->read_header())) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to read file header");
                    m_->error_ = FileError::ReadError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            }
        }
    } else {
        if (!(success = m_->file_.open(mode & ~Text))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success =
                      (m_->read_header() || m_->write_header(m_->format_)))) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to update file header");
                    m_->error_ = FileError::WriteError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            } else if (!(success = m_->file_.resize(m_->header_length_))) {
                setErrorString(m_->file_.errorString());
            }
        }
    }
    success && (success = QIODevice::open(mode | Unbuffered));
    if (success && !(success = m_->initialize(this, mode))) {
        if (FileError::NoError == m_->file_.error()) {
            setErrorString("Unable to initialize file header");
            m_->error_ = FileError::WriteError;
        } else {
            setErrorString(m_->file_.errorString());
        }
    }
    if (!success)
        close();
    return success;
}

bool BWFFile::open(FILE *fh, OpenMode mode, FileHandleFlags flags) {
    bool success{false};
    if (!(mode & ReadOnly)) {
        setErrorString("Read or read/write access must be specified");
        m_->error_ = FileError::OpenError;
        return success;
    }
    if (!(mode & WriteOnly)) {
        if (!(success = m_->file_.open(fh, mode & ~Text, flags))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success = m_->read_header())) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to read file header");
                    m_->error_ = FileError::ReadError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            }
        }
    } else {
        if (!(success = m_->file_.open(fh, mode & ~Text, flags))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success =
                      (m_->read_header() || m_->write_header(m_->format_)))) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to update file header");
                    m_->error_ = FileError::WriteError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            } else if (!(success = m_->file_.resize(m_->header_length_))) {
                setErrorString(m_->file_.errorString());
            }
        }
    }
    success && (success = QIODevice::open(mode | Unbuffered));
    if (success && !(success = m_->initialize(this, mode))) {
        if (FileError::NoError == m_->file_.error()) {
            setErrorString("Unable to initialize file header");
            m_->error_ = FileError::WriteError;
        } else {
            setErrorString(m_->file_.errorString());
        }
    }
    if (!success)
        close();
    return success;
}

bool BWFFile::open(int fd, OpenMode mode, FileHandleFlags flags) {
    bool success{false};
    if (!(mode & ReadOnly)) {
        setErrorString("Read or read/write access must be specified");
        m_->error_ = FileError::OpenError;
        return success;
    }
    if (!(mode & WriteOnly)) {
        if (!(success = m_->file_.open(fd, mode & ~Text, flags))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success = m_->read_header())) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to read file header");
                    m_->error_ = FileError::ReadError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            }
        }
    } else {
        if (!(success = m_->file_.open(fd, mode & ~Text, flags))) {
            setErrorString(m_->file_.errorString());
        } else {
            if (!(success =
                      (m_->read_header() || m_->write_header(m_->format_)))) {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to update file header");
                    m_->error_ = FileError::WriteError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
            } else if (!(success = m_->file_.resize(m_->header_length_))) {
                setErrorString(m_->file_.errorString());
            }
        }
    }
    success && (success = QIODevice::open(mode | Unbuffered));
    if (success && !(success = m_->initialize(this, mode))) {
        if (FileError::NoError == m_->file_.error()) {
            setErrorString("Unable to initialize file header");
            m_->error_ = FileError::WriteError;
        } else {
            setErrorString(m_->file_.errorString());
        }
    }
    if (!success)
        close();
    return success;
}

QAudioFormat const &BWFFile::format() const { return m_->format_; }

auto BWFFile::list_info() -> InfoDictionary & {
    m_->header_dirty_ = true;
    return m_->info_dictionary_;
}

// Broadcast Audio Extension fields
auto BWFFile::bext_version() const -> BextVersion {
    return static_cast<BextVersion>(m_->bext() ? 0 : m_->bext()->version_);
}

void BWFFile::bext_version(BextVersion version) {
    m_->header_dirty_ = true;
    m_->bext()->version_ = static_cast<quint16>(version);
}

QByteArray BWFFile::bext_description() const {
    if (!m_->bext())
        return {};
    return QByteArray::fromRawData(m_->bext()->description_,
                                   strlen(m_->bext()->description_));
}

void BWFFile::bext_description(QByteArray const &description) {
    m_->header_dirty_ = true;
    ::memcpy(m_->bext()->description_, description.constData(),
             sizeof BroadcastAudioExtension::description_);
}

QByteArray BWFFile::bext_originator() const {
    if (!m_->bext())
        return {};
    return QByteArray::fromRawData(m_->bext()->originator_,
                                   strlen(m_->bext()->originator_));
}

void BWFFile::bext_originator(QByteArray const &originator) {
    m_->header_dirty_ = true;
    ::memcpy(m_->bext()->originator_, originator.constData(),
             sizeof BroadcastAudioExtension::originator_);
}

QByteArray BWFFile::bext_originator_reference() const {
    if (!m_->bext())
        return {};
    return QByteArray::fromRawData(m_->bext()->originator_reference_,
                                   strlen(m_->bext()->originator_reference_));
}

void BWFFile::bext_originator_reference(QByteArray const &reference) {
    m_->header_dirty_ = true;
    ::memcpy(m_->bext()->originator_reference_, reference.constData(),
             sizeof BroadcastAudioExtension::originator_reference_);
}

QDateTime BWFFile::bext_origination_date_time() const {
    if (!m_->bext())
        return {};
    return {QDate::fromString(m_->bext()->origination_date_, "yyyy-MM-dd"),
            QTime::fromString(m_->bext()->origination_time_, "hh-mm-ss"),
            QTimeZone::utc()};
}

void BWFFile::bext_origination_date_time(QDateTime const &dt) {
    m_->header_dirty_ = true;
    ::memcpy(m_->bext()->origination_date_,
             dt.date().toString("yyyy-MM-dd").toLocal8Bit().constData(),
             sizeof BroadcastAudioExtension::origination_date_);
    ::memcpy(m_->bext()->origination_time_,
             dt.time().toString("hh-mm-ss").toLocal8Bit().constData(),
             sizeof BroadcastAudioExtension::origination_time_);
}

quint64 BWFFile::bext_time_reference() const {
    if (!m_->bext())
        return 0;
    return (quint64(m_->bext()->time_reference_high_) << 32) +
           m_->bext()->time_reference_low_;
}

void BWFFile::bext_time_reference(quint64 time_code) {
    m_->header_dirty_ = true;
    m_->bext()->time_reference_low_ = time_code & 0x00000000ffffffffll;
    m_->bext()->time_reference_high_ = time_code >> 32;
}

auto BWFFile::bext_umid() const -> UMID {
    UMID umid{{'\0'}};
    if (m_->bext()) {
        umid = m_->bext()->umid_;
    }
    return umid;
}

void BWFFile::bext_umid(UMID const &umid) {
    m_->header_dirty_ = true;
    m_->bext()->umid_ = umid;
}

quint16 BWFFile::bext_loudness_value() const {
    if (!m_->bext())
        return 0;
    return m_->bext()->loudness_value_;
}

void BWFFile::bext_loudness_value(quint16 value) {
    m_->header_dirty_ = true;
    m_->bext()->loudness_value_ = value;
}

quint16 BWFFile::bext_loudness_range() const {
    if (!m_->bext())
        return 0;
    return m_->bext()->loudness_range_;
}

void BWFFile::bext_loudness_range(quint16 range) {
    m_->header_dirty_ = true;
    m_->bext()->loudness_range_ = range;
}

quint16 BWFFile::bext_max_true_peak_level() const {
    if (!m_->bext())
        return 0;
    return m_->bext()->max_true_peak_level_;
}

void BWFFile::bext_max_true_peak_level(quint16 level) {
    m_->header_dirty_ = true;
    m_->bext()->max_true_peak_level_ = level;
}

quint16 BWFFile::bext_max_momentary_loudness() const {
    if (!m_->bext())
        return 0;
    return m_->bext()->max_momentary_loudness_;
}

void BWFFile::bext_max_momentary_loudness(quint16 loudness) {
    m_->header_dirty_ = true;
    m_->bext()->max_momentary_loudness_ = loudness;
}

quint16 BWFFile::bext_max_short_term_loudness() const {
    if (!m_->bext())
        return 0;
    return m_->bext()->max_short_term_loudness_;
}

void BWFFile::bext_max_short_term_loudness(quint16 loudness) {
    m_->header_dirty_ = true;
    m_->bext()->max_short_term_loudness_ = loudness;
}

QByteArray BWFFile::bext_coding_history() const {
    if (size_t(m_->bext_.size()) <= sizeof(BroadcastAudioExtension))
        return {};
    return QByteArray::fromRawData(m_->bext()->coding_history_,
                                   m_->bext_.size() -
                                       sizeof(BroadcastAudioExtension));
}

void BWFFile::bext_coding_history(QByteArray const &text) {
    m_->header_dirty_ = true;
    m_->bext(); // ensure we have a correctly
                // initialized m_->bext_
    auto end = static_cast<char const *>(
        ::memchr(text.constData(), '\0', size_t(text.size())));
    auto length = end ? end - text.constData() : size_t(text.size());
    m_->bext_.resize(sizeof(BroadcastAudioExtension) + length);
    std::strncpy(m_->bext()->coding_history_, text.constData(), length);
}

bool BWFFile::reset() {
    bool success{true};
    if (m_->file_.isOpen()) {
        m_->info_dictionary_.clear();
        m_->bext_.clear();
        auto size = m_->data_size_ < 0 ? m_->file_.size() - m_->header_length_
                                       : m_->data_size_;
        m_->data_size_ = size;
        if (m_->header_length_ >
            qint64(3 * sizeof(Desc) + 4 + sizeof(FormatChunk))) {
            // we need to move the data down
            auto old_pos = m_->header_length_;
            if (!(success = m_->write_header(m_->format_))) {
                auto new_pos = m_->header_length_;
                QByteArray buffer;
                while (size) {
                    success = m_->file_.seek(old_pos);
                    if (!success) {
                        setErrorString(m_->file_.errorString());
                    } else {
                        buffer = m_->file_.read(std::min(size, qint64(32768)));
                        success = m_->file_.seek(new_pos);
                        if (!success) {
                            setErrorString(m_->file_.errorString());
                        } else {
                            qint64 bytes = m_->file_.write(buffer);
                            if (bytes < 0) {
                                setErrorString(m_->file_.errorString());
                                success = false;
                            } else {
                                new_pos += buffer.size();
                                old_pos += buffer.size();
                                size -= buffer.size();
                            }
                        }
                    }
                }
            } else {
                if (FileError::NoError == m_->file_.error()) {
                    setErrorString("Unable to update file header");
                    m_->error_ = FileError::WriteError;
                } else {
                    setErrorString(m_->file_.errorString());
                }
                return success;
            }
        }
        if (success) {
            success = m_->file_.resize(m_->header_length_ + m_->data_size_);
            if (!success) {
                setErrorString(m_->file_.errorString());
            }
            m_->header_dirty_ = true;
        }
    } else {
        setErrorString("File not open");
        m_->error_ = FileError::WriteError;
        success = false;
    }
    return success && QIODevice::reset();
}

qint64 BWFFile::size() const {
    return m_->data_size_ < 0 ? m_->file_.size() - m_->header_length_
                              : m_->data_size_;
}

bool BWFFile::isSequential() const { return m_->file_.isSequential(); }

quint16 BWFFile::bitsPerSample() const { return m_->bits_per_sample_; }
quint16 BWFFile::blockAlign() const { return m_->block_align_; }

void BWFFile::close() {
    if (isOpen())
        QIODevice::close();
    if (m_->header_dirty_ || m_->data_size_ < 0) {
        m_->update_header();
    }
    if (m_->file_.isOpen()) {
        m_->file_.close();
    }
}

bool BWFFile::seek(qint64 pos) {
    if (pos < 0) {
        setErrorString("Attempt to seek before beginning of data");
        m_->error_ = FileError::PositionError;
        return false;
    }
    bool success = QIODevice::seek(pos);
    if (success) {
        success = m_->file_.seek(pos + m_->header_length_);
        if (!success)
            setErrorString(m_->file_.errorString());
    }
    return success;
}

qint64 BWFFile::readData(char *data, qint64 max_size) {
    return m_->file_.read(data, max_size);
}

qint64 BWFFile::writeData(char const *data, qint64 max_size) {
    auto bytes = m_->file_.write(data, max_size);
    if (bytes > 0 && atEnd()) {
        m_->header_dirty_ = true;
        m_->data_size_ = -1;
    }
    return bytes;
}

// forward to QFile
bool BWFFile::copy(QString const &new_name) {
    close();
    return m_->file_.copy(new_name);
}

bool BWFFile::exists() const { return m_->file_.exists(); }

bool BWFFile::link(QString const &link_name) {
    return m_->file_.link(link_name);
}

bool BWFFile::remove() {
    close();
    return m_->file_.remove();
}

bool BWFFile::rename(QString const &new_name) {
    close();
    return m_->file_.rename(new_name);
}

void BWFFile::setFileName(QString const &name) { m_->file_.setFileName(name); }

QString BWFFile::symLinkTarget() const { return m_->file_.symLinkTarget(); }

QString BWFFile::fileName() const { return m_->file_.fileName(); }

auto BWFFile::permissions() const -> Permissions {
    return m_->file_.permissions();
}

bool BWFFile::resize(qint64 new_size) {
    auto size = m_->file_.size();
    if (pos() > new_size)
        seek(new_size);
    auto result = m_->file_.resize(m_->header_length_ + new_size);
    if (m_->data_size_ >= 0) {
        // set any fresh bytes to zero
        auto end_of_data = m_->header_length_ + m_->data_size_;
        auto length =
            std::min(size - end_of_data, m_->file_.size() - end_of_data);
        if (length > 0) {
            auto position = m_->file_.pos();
            m_->file_.seek(m_->header_length_ + m_->data_size_);
            m_->file_.write(QByteArray{int(length), '\0'});
            m_->file_.seek(position);
        }
        m_->data_size_ = -1;
    }
    m_->header_dirty_ = true;
    return result;
}

bool BWFFile::setPermissions(Permissions permissions) {
    bool success = m_->file_.setPermissions(permissions);
    if (!success)
        setErrorString(m_->file_.errorString());
    return success;
}

auto BWFFile::error() const -> FileError {
    return m_->error_ != FileError::NoError ? m_->error_ : m_->file_.error();
}

bool BWFFile::flush() {
    bool success = m_->file_.flush();
    if (!success)
        setErrorString(m_->file_.errorString());
    return success;
}

int BWFFile::handle() const {
    int h{m_->file_.handle()};
    if (h < 0)
        const_cast<BWFFile *>(this)->setErrorString(m_->file_.errorString());
    return h;
}

uchar *BWFFile::map(qint64 offset, qint64 size, MemoryMapFlags flags) {
    uchar *address = m_->file_.map(offset + m_->header_length_, size, flags);
    if (!address)
        setErrorString(m_->file_.errorString());
    return address;
}

bool BWFFile::unmap(uchar *address) {
    bool success = m_->file_.unmap(address);
    if (!success)
        setErrorString(m_->file_.errorString());
    return success;
}

void BWFFile::unsetError() {
    m_->error_ = FileError::NoError;
    m_->file_.unsetError();
}
