////////////////////////////////////////////////////////////////////////////////
// Sound.cpp
// Class to represent a sound
// Copyright Bjoern Ganster 2021
// Developed since 2021
////////////////////////////////////////////////////////////////////////////////

#ifdef MECHVM
#include "MechWarriorIIPRJ.h"
#endif

#include "Sound.h"

#include "algorithm"

#define FILE_NUMBER FILE_NO_SOUND_CPP

// Constructor
Sound::Sound()
: VirtualClass("Sound", Sound_ID)
{
	SDL_memset(&mAudioFormat, 0, sizeof(mAudioFormat));
}

// Destructor
Sound::~Sound()
{
}

// Decode Compact Disc Digital Audio
#ifdef MECHVM
void Sound::decodeCDDA(const MemoryBlock& data)
{
	SET_SCOPE;
	mData = data;
	mAudioFormat.freq = 44100;
	mAudioFormat.format = AUDIO_S16;
	mAudioFormat.channels = 2;
	RETURN;
}
#endif

// Decode MW2 SFL file
#ifdef MECHVM
void Sound::decodeSFL(const MemoryBlock& data)
{
	if (data.getSize() < 20) {
		return;
	}

	// Decode
	SET_SCOPE;
	DWORD blockCount = data.get<DWORD>(8);
	size_t maxBlockSize = data.get<WORD>(12);
	mData.resize(blockCount * maxBlockSize);
	
	sflx(data.getPtr<char>(), mData.getPtr<char>(0));

	// Set metadata
	SET_OP;
	mAudioFormat.freq = 11025;
	mAudioFormat.format = AUDIO_U8;
	mAudioFormat.channels = 1;
	mAudioFormat.samples = (uint16_t)std::min<size_t>(data.getSize(), 65535);
	mAudioFormat.size = data.getSize();
	RETURN;
}
#endif

struct WavHeader {
	char sig1[4];
	DWORD fileSize;
	char sig2[4];
	char sig3[4];// = "fmt "
	DWORD fmtLen;
	WORD formatTag; // PCM
	WORD channels;
	DWORD sampleRate;
	DWORD bytesPerSecond;
	WORD blockAlign;
	WORD bitsPerSample;
	char sig4[4];
	DWORD dataLen;
};

// Decode RIFF WAV file
bool Sound::decodeWAV(const MemoryBlock& data)
{
	SET_SCOPE;
	if (data.getSize() < sizeof(WavHeader)) {
		RETURN false;
	}

	SET_OP;
	const WavHeader* header = data.getPtr<WavHeader>(0);
	if (memcmp(header->sig1, "RIFF", 4) != 0
	||  header->fileSize < sizeof(WavHeader) // often inaccurate?
	||  header->fileSize > sizeof(WavHeader) + 2 * data.getSize()
	||  memcmp(header->sig2, "WAVE", 4) != 0
	||  memcmp(header->sig3, "fmt ", 4) != 0
	||  header->fmtLen != 16
	||  header->formatTag != 1
	||  header->channels == 0
	||  header->dataLen == 0
	||  header->bitsPerSample == 0) 
	{
		RETURN false;
	}

	// Copy data
	SET_OP;
	mData.resize(data.getSize() - sizeof(WavHeader));
	mData.write(0, data.getPtr<char>(sizeof(WavHeader)), data.getSize() - sizeof(WavHeader));

	// Copy metadata
	SET_OP;
	mAudioFormat.channels = (Uint8)header->channels;
	mAudioFormat.freq = header->sampleRate;
	if (header->bitsPerSample == 8) {
		mAudioFormat.format = AUDIO_U8; // 8-bit audio is stored unsigned
	} else if (header->bitsPerSample == 16) {
		mAudioFormat.format = AUDIO_S16; // 16-bit audio is stored signed
	}
	RETURN true;
}

// Save to WAV
bool Sound::saveToWave(const PathChar* fn) const
{
	SET_SCOPE;
	WavHeader header;
	memcpy(header.sig1, "RIFF", 4);
	memcpy(header.sig2, "WAVE", 4);
	memcpy(header.sig3, "fmt ", 4);
	header.fmtLen = 16;
	header.formatTag = 1;
	header.channels = mAudioFormat.channels;
	header.sampleRate = mAudioFormat.freq;

	// todo: if the current data format differs, we may need to convert it ...
	SET_OP;
	switch (mAudioFormat.format) {
	case AUDIO_S8:
	case AUDIO_U8:
		header.bitsPerSample = 8;
		break;
	case AUDIO_S16MSB:
		header.bitsPerSample = 16;
		break;
	case AUDIO_F32LSB:
		// Data needs to be converted first
		RETURN false;
	}

	SET_OP;
	header.bytesPerSecond = header.bitsPerSample * mAudioFormat.channels * mAudioFormat.freq / 8;
	header.blockAlign = header.bitsPerSample * mAudioFormat.channels / 8;
	header.fileSize = mData.getSize() + sizeof(header)-8;
	header.dataLen = mData.getSize();
	memcpy(header.sig4, "data", 4);

	SET_OP;
	MemoryBlock wavMB(sizeof(header) + mData.getSize());
	wavMB.write(0, (char*)(&header), sizeof(header));
	wavMB.write(sizeof(header), mData.getPtr<char>(0), mData.getSize());
	RETURN wavMB.saveToFile(fn);
}

// Convert to new audio specification
void Sound::copyConvert(const Sound& sound, const SDL_AudioSpec& newSpec)
{
	SET_SCOPE;
	size_t audioInputBytesPerSample = getBytesPerSample(sound.mAudioFormat.format);
	size_t audioOutputBytesPerSample = getBytesPerSample(newSpec.format);
	if (audioInputBytesPerSample == 0 
	||  audioOutputBytesPerSample == 0
	||  sound.mAudioFormat.channels == 0)
	{
		RETURN;
	}

	SET_OP;
	int inputSamples = sound.mData.getSize() / (audioInputBytesPerSample * sound.mAudioFormat.channels);
	size_t audioBufSize = audioOutputBytesPerSample * inputSamples * newSpec.channels;

	SET_OP;
	mData.resize(audioBufSize);
	mData.fill(0, audioBufSize, 0);

	// Pass 1: convert format except frequency
	SET_OP;
	mAudioFormat = newSpec;
	mAudioFormat.freq = sound.mAudioFormat.freq;
	for (int i = 0; i < inputSamples; ++i) {
		for (int c = 0; c < newSpec.channels; ++c) {
			float sample = sound.getSampleF(i, c);
			setSampleF(i, c, sample);
		}
	}

	// Pass 2: convert frequency
	// todo: can this convert the other differences as well, since we are using 
	mAudioFormat.format = newSpec.format;
	mAudioFormat.channels = newSpec.channels;
	if (sound.mAudioFormat.freq != newSpec.freq) {
		SET_OP;
		adjustFrequency(newSpec.freq);
	}
	RETURN;
}

// Get sample as a float
float Sound::getSampleF(int sample, int channel) const
{
	SET_SCOPE;
	channel = channel % mAudioFormat.channels;
	float val = 0.0f;
	float conversionFactor_8_to_16 = 0.4f;

	switch (mAudioFormat.format) {
	case AUDIO_S8:
		SET_OP;
		// todo: this works, but sounds cracky
		val = mData.get<signed char>(sample*mAudioFormat.channels+channel);
		val = conversionFactor_8_to_16 * val / 128.0f;
		RETURN val;
	case AUDIO_U8:
		SET_OP;
		// todo: this works, but sounds cracky
		val = mData.get<unsigned char>(sample*mAudioFormat.channels+channel);
		val = conversionFactor_8_to_16 * (val-128) / 128.0f;
		RETURN val;
	case AUDIO_S16LSB:
		SET_OP;
		val = mData.get<int16_t>(2*(sample*mAudioFormat.channels+channel));
		val = (val / 32768.0f);
		RETURN val;
	case AUDIO_U16LSB:
		SET_OP;
		val = mData.get<WORD>(2*(sample*mAudioFormat.channels+channel));
		val = (val / 32768.0f) - 1.0f;
		RETURN val;
	case AUDIO_F32LSB:
		SET_OP;
		val = mData.get<float>(4*(sample*mAudioFormat.channels+channel));
		RETURN val;
	default:
		RETURN 0.0;
	}
}

// Set sample to new float
void Sound::setSampleF(int sample, int channel, float newVal)
{
	SET_SCOPE;
	channel = channel % mAudioFormat.channels;
	switch (mAudioFormat.format) {
	case AUDIO_U8:
		SET_OP;
		mData.set<unsigned char>(sample*mAudioFormat.channels+channel, (unsigned char)((newVal + 1.0) * 127));
		break;
	case AUDIO_S16LSB:
		SET_OP;
		mData.set<int16_t>(2*(sample*mAudioFormat.channels+channel), (int16_t)(newVal * 32767));
		break;
	case AUDIO_U16LSB:
		SET_OP;
		mData.set<uint16_t>(2*(sample*mAudioFormat.channels+channel), (uint16_t)((newVal + 1.0) * 32767));
		break;
	case AUDIO_F32LSB:
		SET_OP;
		mData.set<float>(4*(sample*mAudioFormat.channels+channel), newVal);
		break;
	}

	RETURN;
}

// Get number of bytes used by a sample, per channel
int getBytesPerSample(int sdlFormat)
{
	switch(sdlFormat) {
	case AUDIO_S8:
	case AUDIO_U8:
		return 1;
	case AUDIO_U16LSB:
	case AUDIO_S16LSB:
	case AUDIO_U16MSB:
	case AUDIO_S16MSB:
		return 2;
	case AUDIO_S32LSB:
	case AUDIO_S32MSB:
	case AUDIO_F32LSB:
	case AUDIO_F32MSB:
	default:
		return 4;
	}
}

// Adjust audio frequency
void Sound::adjustFrequency(int newFreq)
{
	SET_SCOPE;
	Sound temp;
	int sampleSize = getBytesPerSample(mAudioFormat.format);
	int samples = mData.getSize() / (mAudioFormat.channels * sampleSize);
	int newSamples = ((int64_t)samples) * newFreq / mAudioFormat.freq;
	temp.mAudioFormat.channels = mAudioFormat.channels;
	temp.mAudioFormat.format = mAudioFormat.format;
	temp.mData.resize(mAudioFormat.channels * newSamples * sampleSize);

	for (int i = 0; i < newSamples; ++i) {
		for (int c = 0; c < mAudioFormat.channels; ++c) {
			float j = ((float)i) * mAudioFormat.freq / newFreq; // index into input
			int ji = (int)(j+0.5); // round to nearest
			int ji1 = (int)(j);
			int ji2 = ji1+1;
			float newVal;
			if (abs(j-ji) < 0.1 && ji2 <mAudioFormat.freq) {
				newVal = getSampleF(ji, c);
			} else {
				float v1 = getSampleF(ji1, c);
				float v2 = getSampleF(ji2, c);
				float fract = j-ji;
				newVal = (float)(fract * v1 + (1.0 - fract) * v2);
			}
			temp.setSampleF(i, c, newVal);
		}
	}

	mAudioFormat.freq = newFreq;
	mData = temp.mData;
	RETURN;
}

// Calculate duration of the current sound in milliseconds
DWORD Sound::getDurationMS() const
{
	SET_SCOPE;
	int sampleSize = getBytesPerSample(mAudioFormat.format);
	int samples = mData.getSize() / (mAudioFormat.channels * sampleSize);
	DWORD result = (DWORD)(1000.0 * samples / mAudioFormat.freq);
	RETURN result;
}
