#include "BWFFile.hpp"

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

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

#include "pimpl_impl.hpp"

#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 len = std::min (size_t (4), strlen (id));
          memcpy (id_.data (), id, len);
          memset (id_.data () + len, ' ', 4u - len);
        }
      else
        {
          memcpy (id_.data (), "JUNK", 4);
        }
    }

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

    char * operator & () {return reinterpret_cast<char *> (this);}
    char const * operator & () const {return &*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 ();
      std::strncpy (origination_date_,
                    now.date ().toString ("yyyy-MM-dd").toLocal8Bit ().constData (),
                    sizeof origination_date_);
      std::strncpy (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_[];
  };
}

class BWFFile::impl final
{
public:
  impl (QAudioFormat const& format)
    : header_dirty_ {true}
    , format_ {format}
    , header_length_ {-1}
    , data_size_ {-1}
  {
  }

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

  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}
  {
  }

  ~impl ()
  {
    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_;
};

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))
                    {
                      FormatChunk fmt;
                      if (file_.read (reinterpret_cast<char *> (&fmt), sizeof fmt) != sizeof fmt) return false;
                      auto audio_format = be ? qFromBigEndian<quint16> (fmt.audio_format) : qFromLittleEndian<quint16> (fmt.audio_format);
                      if (audio_format != 0 && audio_format != 1) return false; // not PCM nor undefined
                      format_.setByteOrder (be ? QAudioFormat::BigEndian : QAudioFormat::LittleEndian);
                      format_.setChannelCount (be ? qFromBigEndian<quint16> (fmt.num_channels) : qFromLittleEndian<quint16> (fmt.num_channels));
                      format_.setCodec ("audio/pcm");
                      format_.setSampleRate (be ? qFromBigEndian<quint32> (fmt.sample_rate) : qFromLittleEndian<quint32> (fmt.sample_rate));
                      int bits_per_sample {be ? qFromBigEndian<quint16> (fmt.bits_per_sample) : qFromLittleEndian<quint16> (fmt.bits_per_sample)};
                      format_.setSampleSize (bits_per_sample);
                      format_.setSampleType (8 == bits_per_sample ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt);
                    }
                  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 ("audio/pcm" != format.codec ()) return false;
  if (!(file_.openMode () & WriteOnly)) return false;
  if (!file_.seek (0)) return false;
  header_length_ = 0;
  bool be {QAudioFormat::BigEndian == format_.byteOrder ()};
  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.sampleSize ());
      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.sampleSize ());
      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 {QAudioFormat::BigEndian == format_.byteOrder ()};
  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 result {false};
  if (!(mode & WriteOnly))
    {
      result = m_->file_.open (mode & ~Text) && m_->read_header ();
    }
  else
    {
      if ((result = m_->file_.open (mode & ~Text)))
        {
          if (!(result = m_->read_header ()
                || m_->write_header (m_->format_)
                || m_->file_.resize (m_->header_length_)))
            {
              m_->file_.close ();
              return false;
            }
        }
    }
  if (result && (result = QIODevice::open (mode | Unbuffered)))
    {
      return m_->initialize (this, mode);
    }
  if (!result) close ();
  return result;
}

bool BWFFile::open(FILE * fh, OpenMode mode, FileHandleFlags flags)
{
  bool result {false};
  if (!(mode & ReadOnly)) return result;
  if (!(mode & WriteOnly))
    {
      result = m_->file_.open (fh, mode & ~Text, flags) && m_->read_header ();
    }
  else
    {
      if ((result = m_->file_.open (fh, mode & ~Text, flags)))
        {
          if (!(result = m_->read_header ()
                || m_->write_header (m_->format_)
                || m_->file_.resize (m_->header_length_)))
            {
              m_->file_.close ();
              return false;
            }
        }
    }
  if (result && (result = QIODevice::open (mode | Unbuffered)))
    {
      return m_->initialize (this, mode);
    }
  if (!result) close ();
  return result;
}

bool BWFFile::open (int fd, OpenMode mode, FileHandleFlags flags)
{
  bool result {false};
  if (!(mode & ReadOnly)) return result;
  if (!(mode & WriteOnly))
    {
      result = m_->file_.open (fd, mode & ~Text, flags) && m_->read_header ();
    }
  else
    {
      if ((result = m_->file_.open (fd, mode & ~Text, flags)))
        {
          if (!(result = m_->read_header ()
                || m_->write_header (m_->format_)
                || m_->file_.resize (m_->header_length_)))
            {
              m_->file_.close ();
              return false;
            }
        }
    }
  if (result && (result = QIODevice::open (mode | Unbuffered)))
    {
      return m_->initialize (this, mode);
    }
  if (!result) close ();
  return result;
}

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;
  std::strncpy (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;
  std::strncpy (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;
  std::strncpy (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"), Qt::UTC};
}

void BWFFile::bext_origination_date_time (QDateTime const& dt)
{
  m_->header_dirty_ = true;
  std::strncpy (m_->bext ()->origination_date_,
                dt.date ().toString ("yyyy-MM-dd").toLocal8Bit ().constData (),
                sizeof (BroadcastAudioExtension::origination_date_));
  std::strncpy (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 length = std::min (strlen (text.constData ()), size_t (text.size ()));
  m_->bext_.resize (sizeof (BroadcastAudioExtension) + length);
  std::strncpy (m_->bext ()->coding_history_, text.constData (), length);
}


bool BWFFile::reset ()
{
  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_;
          m_->write_header (m_->format_);
          auto new_pos = m_->header_length_;
          QByteArray buffer;
          while (size)
            {
              m_->file_.seek (old_pos);
              buffer = m_->file_.read (std::min (size, qint64 (32768)));
              m_->file_.seek (new_pos);
              m_->file_.write (buffer);
              new_pos += buffer.size ();
              old_pos += buffer.size ();
              size -= buffer.size ();
            }
        }
      m_->file_.resize (m_->header_length_ + m_->data_size_);
      m_->header_dirty_ = true;
    }
  return 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 ();
}

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

bool BWFFile::seek (qint64 pos)
{
  if (pos < 0) return false;
  QIODevice::seek (pos);
  return m_->file_.seek (pos + m_->header_length_);
}

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) {return m_->file_.setPermissions (permissions);}

auto BWFFile::error () const -> FileError {return m_->file_.error ();}

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

int BWFFile::handle () const {return m_->file_.handle ();}

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

bool BWFFile::unmap (uchar * address) {return m_->file_.unmap (address);}

void BWFFile::unsetError () {m_->file_.unsetError ();}
