/*
 *
 *    soniK digital audio editor
 *    Copyright (C) 2003-2006  Robert Walker <rob@tenfoot.org.uk>
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "data.h"
#include "actionmanager.h"
#include "fileio.h"
#include "sonik_util.h"

#include "test.h"

#include <cppunit/extensions/HelperMacros.h>

using Sonik::Data;

namespace Sonik
{
  class DataTest : public CppUnit::TestFixture
  {
    CPPUNIT_TEST_SUITE( DataTest );
    CPPUNIT_TEST( testParameters );
    CPPUNIT_TEST( testSimple );
    CPPUNIT_TEST( testInsert );
    CPPUNIT_TEST( testCompact );
    CPPUNIT_TEST( testRemove );
    CPPUNIT_TEST( testFileIO );
    CPPUNIT_TEST_SUITE_END();

  public:
    DataTest()
      : buf_0_9(10), buf_10_19(10), buf_0_19(20), buf_get(20),
        data(am)
    {
    }

    void setUp();

    void testParameters();
    void testSimple();
    void testInsert();
    void testCompact();
    void testRemove();
    void testFileIO();

  private:
    SampleBuffer buf_0_9;
    SampleBuffer buf_10_19;
    SampleBuffer buf_0_19;
    SampleBuffer buf_get;

    ActionManager am;
    Data data;
  };
}

CPPUNIT_TEST_SUITE_REGISTRATION(Sonik::DataTest);


#define CHECK_CHUNK(_data, _chunkIndex, _start, _length)           \
  _checkChunk((_data), (_chunkIndex), (_start), (_length),  \
              "\n" + __POS + ":" #_data "(" #_chunkIndex "," #_start "," #_length ")")

void _checkChunk(const Data& _data, uint32_t _chunkIndex,
                 off_t _start, size_t _length,
                 const char *msg)
{
  off_t start; size_t length;
  _data.chunkInfo(_chunkIndex, start, length);
  CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, start, _start);
  CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, length, _length);
}

void setRandom(Sonik::SampleBuffer& b)
{
  for (Sonik::Sample *p = b.data(); p != b.end(); ++p)
  {
    *p = rand()/(RAND_MAX+1.0f);
  }
}

Sonik::Sample setSequence(Sonik::SampleBuffer& b,
                          Sonik::Sample v, Sonik::Sample s)
{
  for (Sonik::Sample *p = b.data(); p != b.end(); ++p)
  {
    *p = v;
    v += s;
  }

  return v;
}

class MemReader : public Sonik::FileIO::Reader
{
 public:
  MemReader()
    : Sonik::FileIO::Reader(QString::null, QString::null),
      mV(0.0f)
  {
    mLength     = 1024 * 1024;
    mChannels   = 1;
    mSampleRate = 44100;
    mBits       = 16;
  }

  virtual Sonik::IOResult open()  { return Sonik::kSuccess; }
  virtual void     close() { }
  virtual uint32_t read(Sonik::SampleBuffer& data)
  {
    mV = setSequence(data, mV, 0.001f);
    return data.size();
  }

  virtual bool     optionsAvailable() { return false; }
  virtual QWidget* makeOptionsPage(QWidget*, const char*)
  {
    return 0;
  }

  Sonik::Sample mV;
};

void Sonik::DataTest::setUp()
{
  setSequence(buf_0_9,    0.0f, 1.0f);
  setSequence(buf_10_19, 10.0f, 1.0f);
  setSequence(buf_0_19,   0.0f, 1.0f);
}

void Sonik::DataTest::testParameters()
{
  //
  // initial state
  //
  CPPUNIT_ASSERT_EQUAL(data.channels(), (uint8_t)1);
  CPPUNIT_ASSERT_EQUAL(data.length(), (size_t)0);
  CPPUNIT_ASSERT_EQUAL(data.sampleRate(), (uint32_t)48000);
  CPPUNIT_ASSERT_EQUAL(data.bits(), (uint8_t)16);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 0u);

  //
  // set parameters
  //
  data.reset(2, 100);
  CPPUNIT_ASSERT_EQUAL(data.channels(), (uint8_t)2);
  CPPUNIT_ASSERT_EQUAL(data.length(), (size_t)100);
  CPPUNIT_ASSERT_EQUAL(data.sampleRate(), (uint32_t)48000);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 1u);

  data.setBits(16);
  data.setSampleRate(44100);
  CPPUNIT_ASSERT_EQUAL(data.bits(), (uint8_t)16);
  CPPUNIT_ASSERT_EQUAL(data.sampleRate(), (uint32_t)44100);
}

void Sonik::DataTest::testSimple()
{
  const Sonik::Sample   s   = 0.12345;
  const Sonik::Sample8  s8  = Sonik::to<Sonik::Sample8>(s);
  const Sonik::Sample16 s16 = Sonik::to<Sonik::Sample16>(s);
  const Sonik::Sample24 s24 = Sonik::to<Sonik::Sample24>(s);
  const size_t kSz = 100;

  data.reset(2, kSz);
  CPPUNIT_ASSERT_EQUAL(data.length(), kSz);
  data.fillData(0, 0, kSz, s);

  // get as float
  Sonik::SampleBuffer bf(kSz);
  data.data(0, 0, kSz, bf);
  CPPUNIT_ASSERT_EQUAL(bf.size(), kSz);
  CHECK_BUFFER_EQ(bf, s);

  // get as 8 bit
  Sonik::Sample8Buffer b8(kSz);
  data.data(0, 0, kSz, b8);
  CPPUNIT_ASSERT_EQUAL(b8.size(), kSz);
  CHECK_BUFFER_EQ(b8, s8);

  // get as 16 bit
  Sonik::Sample16Buffer b16(kSz);
  data.data(0, 0, kSz, b16);
  CPPUNIT_ASSERT_EQUAL(b16.size(), kSz);
  CHECK_BUFFER_EQ(b16, s16);

  // get as 24 bit
  Sonik::Sample24Buffer b24(kSz);
  data.data(0, 0, kSz, b24);
  CPPUNIT_ASSERT_EQUAL(b24.size(), kSz);
  CHECK_BUFFER_EQ(b24, s24);

  // insert other value in middle
  data.insert(kSz/2, kSz/2);
  data.fillData(0, kSz/2, kSz/2, -s);

  data.data(0, 0, kSz/2, bf);
  CPPUNIT_ASSERT_EQUAL(bf.size(), kSz/2);
  CHECK_BUFFER_EQ(bf, s);

  data.data(0, kSz/2, kSz/2, bf);
  CPPUNIT_ASSERT_EQUAL(bf.size(), kSz/2);
  CHECK_BUFFER_EQ(bf, -s);

  data.data(0, kSz, kSz/2, bf);
  CPPUNIT_ASSERT_EQUAL(bf.size(), kSz/2);
  CHECK_BUFFER_EQ(bf, s);

  // remove added data
  data.remove(kSz/2, kSz/2);
  data.data(0, 0, kSz, bf);
  CPPUNIT_ASSERT_EQUAL(bf.size(), kSz);
  CHECK_BUFFER_EQ(bf, s);

  data.reset(1, 10);
  data.setData(0, 0, buf_0_9);

  data.data(0, 0, 10, bf);
  CHECK_BUFFER_EQ(bf, buf_0_9);

  data.remove(0, 1);
  // should be 1,2,3,4,5,...
  CPPUNIT_ASSERT_EQUAL(data.length(), 9u);
  data.data(0, 0, 9, bf);
  // TODO:
  CHECK_BUFFER_EQ_X(bf.data(), buf_0_9.data()+1, 9);

  data.remove(2, 2);
  // should be 1,2,5,6...
  CPPUNIT_ASSERT_EQUAL(data.length(), 7u);
  data.data(0, 0, 7, bf);
  CPPUNIT_ASSERT_EQUAL(bf[0], 1.0f);
  CPPUNIT_ASSERT_EQUAL(bf[1], 2.0f);
  CPPUNIT_ASSERT_EQUAL(bf[2], 5.0f);
  CPPUNIT_ASSERT_EQUAL(bf[3], 6.0f);
  CPPUNIT_ASSERT_EQUAL(bf[4], 7.0f);
}

void Sonik::DataTest::testInsert()
{
  data.reset(2, 100);

  size_t chunkSize = data.chunkSize();

  // append to end of last chunk
  data.setData(0, 90, buf_0_9);
  data.insert(100, 100);
  CPPUNIT_ASSERT_EQUAL(data.length(), 200u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 1u);
  CHECK_CHUNK(data, 0,   0, 200);
  data.data(0, 90, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // insert at start
  data.setData(0, 0, buf_0_9);
  data.insert(0, 100);
  CPPUNIT_ASSERT_EQUAL(data.length(), 300u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 2u);
  CHECK_CHUNK(data, 0,   0, 100);
  CHECK_CHUNK(data, 1, 100, 200);
  data.data(0, 100, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // append at end of first chunk
  data.setData(0, 90, buf_0_9);
  data.insert(100, 100);
  CPPUNIT_ASSERT_EQUAL(data.length(), 400u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 2u);
  CHECK_CHUNK(data, 0,   0, 200);
  CHECK_CHUNK(data, 1, 200, 200);
  data.data(0, 90, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // split first chunk
  data.setData(0, 90, buf_0_19);
  data.insert(100, 100);
  CPPUNIT_ASSERT_EQUAL(data.length(), 500u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 3u);
  CHECK_CHUNK(data, 0,   0, 200);
  CHECK_CHUNK(data, 1, 200, 100);
  CHECK_CHUNK(data, 2, 300, 200);
  data.data(0, 90, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);
  data.data(0, 200, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_10_19);

  // split second chunk
  data.setData(0, 240, buf_0_19);
  data.insert(250, 50);
  CPPUNIT_ASSERT_EQUAL(data.length(), 550u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 4u);
  CHECK_CHUNK(data, 0,   0, 200);
  CHECK_CHUNK(data, 1, 200, 100);
  CHECK_CHUNK(data, 2, 300,  50);
  CHECK_CHUNK(data, 3, 350, 200);
  data.data(0, 240, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);
  data.data(0, 300, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_10_19);

  // split last chunk
  data.setData(0, 360, buf_0_19);
  data.insert(370, 50);
  CPPUNIT_ASSERT_EQUAL(data.length(), 600u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 5u);
  CHECK_CHUNK(data, 0,   0, 200);
  CHECK_CHUNK(data, 1, 200, 100);
  CHECK_CHUNK(data, 2, 300,  50);
  CHECK_CHUNK(data, 3, 350,  70);
  CHECK_CHUNK(data, 4, 420, 180);
  data.data(0, 360, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);
  data.data(0, 420, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_10_19);

  // insert big chunk at start
  data.setData(0, 0, buf_0_9);
  data.insert(0, chunkSize * 3 + 100);
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 3 + 700);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 9u);
  CHECK_CHUNK(data, 0,                 0, chunkSize);
  CHECK_CHUNK(data, 1,   chunkSize      , chunkSize);
  CHECK_CHUNK(data, 2, 2*chunkSize      , chunkSize);
  CHECK_CHUNK(data, 3, 3*chunkSize      ,       100);
  CHECK_CHUNK(data, 4, 3*chunkSize + 100,       200);
  CHECK_CHUNK(data, 5, 3*chunkSize + 300,       100);
  CHECK_CHUNK(data, 6, 3*chunkSize + 400,        50);
  CHECK_CHUNK(data, 7, 3*chunkSize + 450,        70);
  CHECK_CHUNK(data, 8, 3*chunkSize + 520,       180);
  data.data(0, chunkSize * 3 + 100, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // insert big chunk at end
  off_t pos = data.length()-10;
  data.setData(0, pos, buf_0_9);
  data.insert(data.length(), chunkSize * 4 - 180);
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 7 + 520);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 12u);
  CHECK_CHUNK(data,  0,                 0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize      , chunkSize);
  CHECK_CHUNK(data,  2, 2*chunkSize      , chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize      ,       100);
  CHECK_CHUNK(data,  4, 3*chunkSize + 100,       200);
  CHECK_CHUNK(data,  5, 3*chunkSize + 300,       100);
  CHECK_CHUNK(data,  6, 3*chunkSize + 400,        50);
  CHECK_CHUNK(data,  7, 3*chunkSize + 450,        70);
  CHECK_CHUNK(data,  8, 3*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  9, 4*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data, 10, 5*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data, 11, 6*chunkSize + 520, chunkSize);
  data.data(0, pos, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // insert into empty
  Sonik::ActionManager am;
  Data data2(am);
  data2.reset(1, 0);
  CPPUNIT_ASSERT_EQUAL(data2.chunkCount(), 0u);
  data2.insert(0, 100);
  CPPUNIT_ASSERT_EQUAL(data2.chunkCount(), 1u);
  CHECK_CHUNK(data2, 0, 0, 100);
}

void Sonik::DataTest::testCompact()
{
  size_t chunkSize = data.chunkSize();

  // create data, inserting in random places
  data.reset(2, 100);
  data.insert(100, 100);
  data.insert(0, 100);
  data.insert(100, 100);
  data.insert(100, 100);
  data.insert(250, 50);
  data.insert(370, 50);
  data.insert(0, chunkSize * 3 + 100);
  data.insert(data.length(), chunkSize * 4 - 180);

  Sonik::SampleBuffer rand_set(data.length()), rand_get(data.length());
  setRandom(rand_set);
  data.setData(0, 0, rand_set);

  data.compact();
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 7 + 520);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                 0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize      , chunkSize);
  CHECK_CHUNK(data,  2, 2*chunkSize      , chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize      ,       520);
  CHECK_CHUNK(data,  4, 3*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  5, 4*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  6, 5*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  7, 6*chunkSize + 520, chunkSize);
  data.data(0, 0, data.length(), rand_get);
  CHECK_BUFFER_EQ(rand_get, rand_set);

  // repeat should have no effect
  data.compact();
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 7 + 520);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                 0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize      , chunkSize);
  CHECK_CHUNK(data,  2, 2*chunkSize      , chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize      ,       520);
  CHECK_CHUNK(data,  4, 3*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  5, 4*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  6, 5*chunkSize + 520, chunkSize);
  CHECK_CHUNK(data,  7, 6*chunkSize + 520, chunkSize);
  data.data(0, 0, data.length(), rand_get);
  CHECK_BUFFER_EQ(rand_get, rand_set);

  // compact at beginning
  data.insert(0,   1000);
  data.insert(500, 1000);
  rand_set.reset(data.length());
  rand_get.reset(data.length());
  setRandom(rand_set);
  data.setData(0, 0, rand_set);
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 7 + 2520);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 10u);
  CHECK_CHUNK(data,  0,                 0,      1500);
  CHECK_CHUNK(data,  1,              1500,       500);
  CHECK_CHUNK(data,  2,              2000, chunkSize);
  data.compact();
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 9u);
  CHECK_CHUNK(data,  0,                  0,      2000);
  CHECK_CHUNK(data,  1,               2000, chunkSize);
  CHECK_CHUNK(data,  2,   chunkSize + 2000, chunkSize);
  CHECK_CHUNK(data,  3, 2*chunkSize + 2000, chunkSize);
  CHECK_CHUNK(data,  4, 3*chunkSize + 2000,       520);
  CHECK_CHUNK(data,  5, 3*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  6, 4*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  7, 5*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  8, 6*chunkSize + 2520, chunkSize);
  data.data(0, 0, data.length(), rand_get);
  CHECK_BUFFER_EQ(rand_get, rand_set);

  // compact at end
  data.insert(data.length() - 1000, 1000);
  data.insert(data.length() -  500, 1000);
  rand_set.reset(data.length());
  rand_get.reset(data.length());
  setRandom(rand_set);
  data.setData(0, 0, rand_set);
  CPPUNIT_ASSERT_EQUAL(data.length(), chunkSize * 7 + 4520);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 11u);
  CHECK_CHUNK(data,  7, 5*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  8, 6*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  9, 7*chunkSize + 2520,      1500);
  CHECK_CHUNK(data, 10, 7*chunkSize + 4020,       500);
  data.compact();
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 10u);
  CHECK_CHUNK(data,  0,                  0,      2000);
  CHECK_CHUNK(data,  1,               2000, chunkSize);
  CHECK_CHUNK(data,  2,   chunkSize + 2000, chunkSize);
  CHECK_CHUNK(data,  3, 2*chunkSize + 2000, chunkSize);
  CHECK_CHUNK(data,  4, 3*chunkSize + 2000,       520);
  CHECK_CHUNK(data,  5, 3*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  6, 4*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  7, 5*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  8, 6*chunkSize + 2520, chunkSize);
  CHECK_CHUNK(data,  9, 7*chunkSize + 2520,      2000);
  data.data(0, 0, data.length(), rand_get);
  CHECK_BUFFER_EQ(rand_get, rand_set);

  // compact 1 chunk (NOP)
  Sonik::ActionManager am;
  Data data2(am);
  data2.reset(1, 0);
  data2.insert(0, 100);

  data2.compact();
  CPPUNIT_ASSERT_EQUAL(data2.chunkCount(), 1u);
  CHECK_CHUNK(data2, 0, 0, 100);

  // compact 2 chunks
  data2.insert(0, 100);
  CPPUNIT_ASSERT_EQUAL(data2.chunkCount(), 2u);
  CHECK_CHUNK(data2, 0,   0, 100);
  CHECK_CHUNK(data2, 1, 100, 100);
  data2.compact();
  CPPUNIT_ASSERT_EQUAL(data2.chunkCount(), 1u);
  CHECK_CHUNK(data2, 0, 0, 200);
}

void Sonik::DataTest::testRemove()
{
  size_t chunkSize = data.chunkSize();
  size_t expectedSize = chunkSize * 10;

  data.reset(2, expectedSize);
  data.setData(0, chunkSize - 1000, buf_0_9);

  // remove from start
  data.remove(0, chunkSize - 1000);
  expectedSize -= (chunkSize - 1000);
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 10u);
  CHECK_CHUNK(data,  0,                  0,      1000);
  CHECK_CHUNK(data,  1,               1000, chunkSize);
  CHECK_CHUNK(data,  2,   chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  3, 2*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  4, 3*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  5, 4*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  6, 5*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  7, 6*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  8, 7*chunkSize + 1000, chunkSize);
  CHECK_CHUNK(data,  9, 8*chunkSize + 1000, chunkSize);
  data.data(0, 0, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // remove all of first chunk
  data.setData(0, 1000, buf_0_9);
  data.remove(0, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 9u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize);
  CHECK_CHUNK(data,  2, 2*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  4, 4*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize       , chunkSize);
  CHECK_CHUNK(data,  8, 8*chunkSize       , chunkSize);
  data.data(0, 0, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // remove from start of chunk
  data.setData(0, chunkSize - 10, buf_0_9);
  data.setData(0, chunkSize + 1000, buf_10_19);
  data.remove(chunkSize, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 9u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 1000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  4, 4*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  8, 8*chunkSize - 1000, chunkSize);
  data.data(0, chunkSize-10, 20, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_19);

  // remove from end of data
  data.setData(0, expectedSize - 1000 - 10, buf_0_9);
  data.remove(expectedSize - 1000, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 9u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 1000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  4, 4*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  8, 8*chunkSize - 1000, chunkSize - 1000);
  data.data(0, expectedSize - 10, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // remove all of last chunk
  data.setData(0, expectedSize - (chunkSize - 1000) - 10, buf_0_9);
  data.remove(expectedSize - (chunkSize - 1000), chunkSize - 1000);
  expectedSize -= (chunkSize - 1000);
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 1000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  4, 4*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 1000, chunkSize);
  data.data(0, expectedSize - 10, 10, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_9);

  // remove from end of chunk
  data.setData(0, 4*chunkSize - 2000 - 10, buf_0_9);
  data.setData(0, 4*chunkSize - 1000, buf_10_19);
  data.remove(4 * chunkSize - 1000 - 1000, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 1000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 1000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 1000, chunkSize - 1000);
  CHECK_CHUNK(data,  4, 4*chunkSize - 2000, chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize - 2000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 2000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 2000, chunkSize);
  data.data(0, 4*chunkSize - 2000 - 10, 20, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_19);

  // remove from middle of chunk
  data.setData(0, chunkSize + 1000 - 10, buf_0_9);
  data.setData(0, chunkSize + 2000, buf_10_19);
  data.remove(chunkSize + 1000, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 2000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 2000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 2000, chunkSize - 1000);
  CHECK_CHUNK(data,  4, 4*chunkSize - 3000, chunkSize);
  CHECK_CHUNK(data,  5, 5*chunkSize - 3000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 3000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 3000, chunkSize);
  data.data(0, chunkSize + 1000 - 10, 20, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_19);

  // remove spanning 2 chunks
  data.setData(0, 5*chunkSize - 3000 - 500 - 10, buf_0_9);
  data.setData(0, 5*chunkSize - 3000 + 500, buf_10_19);
  data.remove(5*chunkSize - 3000 - 500, 1000);
  expectedSize -= 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 8u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 2000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 2000, chunkSize);
  CHECK_CHUNK(data,  3, 3*chunkSize - 2000, chunkSize - 1000);
  CHECK_CHUNK(data,  4, 4*chunkSize - 3000, chunkSize - 500);
  CHECK_CHUNK(data,  5, 5*chunkSize - 3500, chunkSize - 500);
  CHECK_CHUNK(data,  6, 6*chunkSize - 4000, chunkSize);
  CHECK_CHUNK(data,  7, 7*chunkSize - 4000, chunkSize);
  data.data(0, 5*chunkSize - 3500 - 10, 20, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_19);

  // remove spanning several chunks
  data.setData(0, 3*chunkSize - 3000 - 10, buf_0_9);
  data.setData(0, 4*chunkSize - 3000 + 1000, buf_10_19);
  data.remove(3*chunkSize - 3000, 1000 + (chunkSize - 1000) + 1000);
  expectedSize -= 1000 + (chunkSize - 1000) + 1000;
  CPPUNIT_ASSERT_EQUAL(data.length(), expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 7u);
  CHECK_CHUNK(data,  0,                  0, chunkSize);
  CHECK_CHUNK(data,  1,   chunkSize       , chunkSize - 2000);
  CHECK_CHUNK(data,  2, 2*chunkSize - 2000, chunkSize - 1000);
  CHECK_CHUNK(data,  3, 3*chunkSize - 3000, chunkSize - 1500);
  CHECK_CHUNK(data,  4, 4*chunkSize - 4500, chunkSize - 500);
  CHECK_CHUNK(data,  5, 5*chunkSize - 5000, chunkSize);
  CHECK_CHUNK(data,  6, 6*chunkSize - 5000, chunkSize);
  data.data(0, 3*chunkSize - 3000 - 10, 20, buf_get);
  CHECK_BUFFER_EQ(buf_get, buf_0_19);

  // remove all
  data.remove(0, expectedSize);
  CPPUNIT_ASSERT_EQUAL(data.length(), 0u);
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(), 0u);
}

void Sonik::DataTest::testFileIO()
{
  MemReader m;

  CPPUNIT_ASSERT_EQUAL(data.open(m), Sonik::kSuccess);
  CPPUNIT_ASSERT_EQUAL(data.channels(), m.channels());
  CPPUNIT_ASSERT_EQUAL(data.length(), m.length());
  CPPUNIT_ASSERT_EQUAL(data.sampleRate(), m.sampleRate());
  CPPUNIT_ASSERT_EQUAL(data.bits(), m.bits());
  CPPUNIT_ASSERT_EQUAL(data.chunkCount(),
                       (m.length() % data.chunkSize() > 0) + m.length() / data.chunkSize());

  const size_t kStep = 4096;
  Sonik::Sample v = 0.0f;
  for (off_t pos = 0; pos < (off_t)data.length(); )
  {
    size_t sz = QMIN(data.length() - pos, kStep);
    Sonik::SampleBuffer buf(sz), cmp(sz);
    v = setSequence(cmp, v, 0.001f);
    data.data(0, pos, sz, buf);
    CHECK_BUFFER_EQ(buf, cmp);
    pos += sz;
  }
}
