#include <memory>

#include "tmachine.h"
#include "tsio_wav.h"
#include "tsystem.h"
#include "tfilepath_io.h"
#include "tsioutils.h"

using namespace std;

#if !defined(TNZ_LITTLE_ENDIAN)
TNZ_LITTLE_ENDIAN undefined !!
#endif

//==============================================================================

// TWAVChunk: classe base per i vari chunk WAV

class TWAVChunk {
public:
  static TINT32 HDR_LENGTH;

  string m_name;
  TINT32 m_length;  // lunghezza del chunk in byte

  TWAVChunk(string name, TINT32 length) : m_name(name), m_length(length) {}

  virtual ~TWAVChunk() {}

  virtual bool read(Tifstream &is) {
    skip(is);
    return true;
  }

  void skip(Tifstream &is) { is.seekg(m_length, ios::cur); }

  static bool readHeader(Tifstream &is, string &name, TINT32 &length) {
    char cName[5];
    TINT32 len = 0;
    memset(cName, 0, sizeof(cName));

    is.read(cName, 4);
    if (is.fail()) return false;
    cName[4] = '\0';

    is.read((char *)&len, sizeof(len));
    if (is.fail()) return false;

    // il formato WAV memorizza i dati come little-endian
    // se la piattaforma non e' little-endian bisogna scambiare i byte
    if (!TNZ_LITTLE_ENDIAN) len = swapTINT32(len);

    name   = string(cName);
    length = len;
    return true;
  }
};

TINT32 TWAVChunk::HDR_LENGTH = 8;

//====================================================================

//  FMT Chunk: Chunk contenente le informazioni sulla traccia

class TFMTChunk final : public TWAVChunk {
public:
  static TINT32 LENGTH;

  USHORT m_encodingType;  // PCM, ...
  USHORT m_chans;         // numero di canali
  TUINT32 m_sampleRate;
  TUINT32 m_avgBytesPerSecond;
  USHORT m_bytesPerSample;
  USHORT m_bitPerSample;

  TFMTChunk(TINT32 length) : TWAVChunk("fmt ", length) {}

  bool read(Tifstream &is) override {
    is.read((char *)&m_encodingType, sizeof(m_encodingType));
    is.read((char *)&m_chans, sizeof(m_chans));
    is.read((char *)&m_sampleRate, sizeof(m_sampleRate));
    is.read((char *)&m_avgBytesPerSecond, sizeof(m_avgBytesPerSecond));
    is.read((char *)&m_bytesPerSample, sizeof(m_bytesPerSample));
    is.read((char *)&m_bitPerSample, sizeof(m_bitPerSample));

    if (!TNZ_LITTLE_ENDIAN) {
      m_encodingType      = swapUshort(m_encodingType);
      m_chans             = swapUshort(m_chans);
      m_sampleRate        = swapTINT32(m_sampleRate);
      m_avgBytesPerSecond = swapTINT32(m_avgBytesPerSecond);
      m_bytesPerSample    = swapUshort(m_bytesPerSample);
      m_bitPerSample      = swapUshort(m_bitPerSample);
    }

    assert(m_length >= 16);
    if (m_length > 16) is.seekg((long)is.tellg() + m_length - 16);
    return true;
  }

  bool write(ofstream &os) {
    TUINT32 length         = m_length;
    USHORT type            = m_encodingType;
    USHORT chans           = m_chans;
    TUINT32 sampleRate     = m_sampleRate;
    TUINT32 bytesPerSecond = m_avgBytesPerSecond;
    USHORT bytesPerSample  = m_bytesPerSample;
    USHORT bitPerSample    = m_bitPerSample;

    if (!TNZ_LITTLE_ENDIAN) {
      length         = swapTINT32(length);
      type           = swapUshort(type);
      chans          = swapUshort(chans);
      sampleRate     = swapTINT32(sampleRate);
      bytesPerSecond = swapTINT32(bytesPerSecond);
      bytesPerSample = swapUshort(bytesPerSample);
      bitPerSample   = swapUshort(bitPerSample);
    }

    os.write((char *)"fmt ", 4);
    os.write((char *)&length, sizeof(length));
    os.write((char *)&type, sizeof(type));
    os.write((char *)&chans, sizeof(chans));
    os.write((char *)&sampleRate, sizeof(sampleRate));
    os.write((char *)&bytesPerSecond, sizeof(bytesPerSecond));
    os.write((char *)&bytesPerSample, sizeof(bytesPerSample));
    os.write((char *)&bitPerSample, sizeof(bitPerSample));

    return true;
  }
};

TINT32 TFMTChunk::LENGTH = TWAVChunk::HDR_LENGTH + 16;

//====================================================================

//  DATA Chunk: Chunk contenente i campioni

class TDATAChunk final : public TWAVChunk {
public:
  std::unique_ptr<UCHAR[]> m_samples;

  TDATAChunk(TINT32 length) : TWAVChunk("data", length) {}

  bool read(Tifstream &is) override {
    // alloca il buffer dei campioni
    m_samples.reset(new UCHAR[m_length]);
    if (!m_samples) return false;
    is.read((char *)m_samples.get(), m_length);
    return true;
  }

  bool write(ofstream &os) {
    TINT32 length = m_length;

    if (!TNZ_LITTLE_ENDIAN) {
      length = swapTINT32(length);
    }

    os.write((char *)"data", 4);
    os.write((char *)&length, sizeof(length));
    os.write((char *)m_samples.get(), m_length);
    return true;
  }
};

//==============================================================================

TSoundTrackReaderWav::TSoundTrackReaderWav(const TFilePath &fp)
    : TSoundTrackReader(fp) {}

//------------------------------------------------------------------------------

TSoundTrackP TSoundTrackReaderWav::load() {
  char chunkName[5];
  char RIFFType[5];
  TINT32 chunkLength;

  Tifstream is(m_path);

  if (!is) throw TException(m_path.getWideString() + L" : File doesn't exist");

  // legge il nome del chunk
  is.read((char *)&chunkName, sizeof(chunkName) - 1);
  chunkName[4] = '\0';

  // legge la lunghezza del chunk
  is.read((char *)&chunkLength, sizeof(chunkLength));

  if (!TNZ_LITTLE_ENDIAN) chunkLength = swapTINT32(chunkLength);

  // legge il RIFFType
  is.read((char *)&RIFFType, sizeof(RIFFType) - 1);
  RIFFType[4] = '\0';

  // per i .wav il RIFFType DEVE essere uguale a "WAVE"
  if ((string(RIFFType, 4) != "WAVE"))
    throw TException("The WAV file doesn't contain the WAVE form");

  TFMTChunk *fmtChunk   = 0;
  TDATAChunk *dataChunk = 0;

  while (!is.eof()) {
    string name   = "";
    TINT32 length = 0;

    bool ret = TWAVChunk::readHeader(is, name, length);

    if (!ret) break;

    // legge solo i chunk che ci interessano, ossia FMT e DATA

    if (name == "fmt ") {
      // legge i dati del chunk FMT
      fmtChunk = new TFMTChunk(length);
      fmtChunk->read(is);

      // considera il byte di pad alla fine del chunk nel caso
      // in cui la lunghezza di questi e' dispari
      if (length & 1) {
        is.seekg((long)is.tellg() + 1);
      }
    } else if (name == "data") {
      // legge i dati del chunk DATA
      dataChunk = new TDATAChunk(length);

      dataChunk->read(is);

      // considera il byte di pad alla fine del chunk nel caso
      // in cui la lunghezza di questi e' dispari
      if (length & 1) {
        is.seekg((long)is.tellg() + 1);
      }
    } else {
      // spostati nello stream di un numero di byte pari a length
      if (length & 1)
        is.seekg((long)is.tellg() + (long)length + 1);
      else
        is.seekg((long)is.tellg() + (long)length);
    }
  }

  TSoundTrackP track = 0;

  if (fmtChunk && dataChunk) {
    TINT32 sampleCount = dataChunk->m_length / fmtChunk->m_bytesPerSample;
    int sampleType = 0;

    if (fmtChunk->m_encodingType == 1)  // WAVE_FORMAT_PCM
      sampleType = (fmtChunk->m_bitPerSample == 8) ? TSound::UINT : TSound::INT;
    else if (fmtChunk->m_encodingType == 3)  // WAVE_FORMAT_IEEE_FLOAT
      sampleType = TSound::FLOAT;

    if (sampleType)  // valid sample type
      track = TSoundTrack::create((int)fmtChunk->m_sampleRate,
                                  fmtChunk->m_bitPerSample, fmtChunk->m_chans,
                                  sampleCount, sampleType);

    if (track) {
      if (!TNZ_LITTLE_ENDIAN) {
        switch (fmtChunk->m_bitPerSample) {
        case 16:
          swapAndCopy16Bits((short *)dataChunk->m_samples.get(),
                            (short *)track->getRawData(),
                            sampleCount * fmtChunk->m_chans);
          break;
        case 24:
          swapAndCopy24Bits((short *)dataChunk->m_samples.get(),
                            (short *)track->getRawData(),
                            sampleCount * fmtChunk->m_chans);
          break;
        case 32:
          swapAndCopy32Bits((TINT32 *)dataChunk->m_samples.get(),
                            (TINT32 *)track->getRawData(),
                            sampleCount * fmtChunk->m_chans);
          break;
        default:
          memcpy((void *)track->getRawData(),
                 (void *)(dataChunk->m_samples.get()),
                 sampleCount * fmtChunk->m_bytesPerSample);
          break;
        }
      } else {
        memcpy((void *)track->getRawData(),
               (void *)(dataChunk->m_samples.get()),
               sampleCount * fmtChunk->m_bytesPerSample);
      }
    }
  }

  if (fmtChunk) delete fmtChunk;
  if (dataChunk) delete dataChunk;

  return track;
}

//==============================================================================

TSoundTrackWriterWav::TSoundTrackWriterWav(const TFilePath &fp)
    : TSoundTrackWriter(fp) {}

//------------------------------------------------------------------------------

bool TSoundTrackWriterWav::save(const TSoundTrackP &sndtrack) {
  if (!sndtrack)
    throw TException(L"Unable to save the soundtrack: " +
                     m_path.getWideString());

  if (sndtrack->getBitPerSample() == 8 && sndtrack->isSampleSigned())
    throw TException("The format (8 bit signed) is incompatible with WAV file");

  TINT32 soundDataLength =
      (TINT32)(sndtrack->getSampleCount() * (sndtrack->getBitPerSample() / 8) *
               sndtrack->getChannelCount() /*sndtrack->getSampleSize()*/);

  TINT32 RIFFChunkLength =
      TFMTChunk::LENGTH + TWAVChunk::HDR_LENGTH + soundDataLength;

  TFileStatus fs(m_path);
  if (fs.doesExist() && !fs.isWritable())
    throw TException(L"Unable to save the soundtrack: " +
                     m_path.getWideString() + L" is read-only");

  Tofstream os(m_path);

  TFMTChunk fmtChunk(16);

  fmtChunk.m_encodingType      = sndtrack->getSampleType() & TSound::WMASK;
  fmtChunk.m_chans             = sndtrack->getChannelCount();
  fmtChunk.m_sampleRate        = sndtrack->getSampleRate();
  fmtChunk.m_avgBytesPerSecond = (sndtrack->getBitPerSample() / 8) *
                                 fmtChunk.m_chans * sndtrack->getSampleRate();
  // sndtrack->getSampleSize()*sndtrack->getSampleRate();
  fmtChunk.m_bytesPerSample = (sndtrack->getBitPerSample() / 8) *
                              fmtChunk.m_chans;  // sndtrack->getSampleSize();
  fmtChunk.m_bitPerSample = sndtrack->getBitPerSample();

  TDATAChunk dataChunk(soundDataLength);

  std::unique_ptr<UCHAR[]> waveData(new UCHAR[soundDataLength]);

  if (!TNZ_LITTLE_ENDIAN) RIFFChunkLength = swapTINT32(RIFFChunkLength);

  if (!TNZ_LITTLE_ENDIAN) {
    switch (fmtChunk.m_bitPerSample) {
    case 16:
      swapAndCopy16Bits((short *)sndtrack->getRawData(),
                        (short *)waveData.get(),
                        sndtrack->getSampleCount() * fmtChunk.m_chans);
      break;
    case 24:
      swapAndCopy24Bits((void *)sndtrack->getRawData(), (void *)waveData.get(),
                        sndtrack->getSampleCount() * fmtChunk.m_chans);
      break;
    case 32:
      swapAndCopy32Bits((TINT32 *)sndtrack->getRawData(),
                        (TINT32 *)waveData.get(),
                        sndtrack->getSampleCount() * fmtChunk.m_chans);
      break;
    default:
      memcpy((void *)waveData.get(), (void *)sndtrack->getRawData(),
             soundDataLength);
      break;
    }
  } else {
    memcpy((void *)waveData.get(), (void *)sndtrack->getRawData(),
           soundDataLength);
  }

  dataChunk.m_samples = std::move(waveData);

  os.write("RIFF", 4);
  os.write((char *)&RIFFChunkLength, sizeof(TINT32));
  os.write("WAVE", 4);
  fmtChunk.write(os);
  dataChunk.write(os);

  return true;
}
