/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

/* Heavily based on code from NAGI
 *
 * COPYRIGHT AND PERMISSION NOTICE
 *
 * Copyright (c) 2001, 2001, 2002 Nick Sonneveld
 *
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, provided that the above
 * copyright notice(s) and this permission notice appear in all copies of
 * the Software and that both the above copyright notice(s) and this
 * permission notice appear in supporting documentation.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
 * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
 * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Except as contained in this notice, the name of a copyright holder
 * shall not be used in advertising or otherwise to promote the sale, use
 * or other dealings in this Software without prior written authorization
 *
 */

#include "audio/mixer.h"
#include "agi/agi.h"
#include "agi/sound.h"
#include "agi/sound_pcjr.h"
#include "common/config-manager.h"

namespace Agi {

// "fade out" or possibly "dissolve"
// v2.9xx
const int8 dissolveDataV2[] = {
	-2, -3, -2, -1,
	0x00, 0x00,
	0x01, 0x01, 0x01, 0x01,
	0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
	0x04, 0x04, 0x04, 0x04,
	0x05, 0x05, 0x05, 0x05,
	0x06, 0x06, 0x06, 0x06, 0x06,
	0x07, 0x07, 0x07, 0x07,
	0x08, 0x08, 0x08, 0x08,
	0x09, 0x09, 0x09, 0x09,
	0x0A, 0x0A, 0x0A, 0x0A,
	0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B,
	0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
	0x0D,
	-100
};

// v3
const int8 dissolveDataV3[] = {
	-2, -3, -2, -1,
	0x00, 0x00, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
	0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
	0x04, 0x04, 0x04, 0x04, 0x04,
	0x05, 0x05, 0x05, 0x05, 0x05,
	0x06, 0x06, 0x06, 0x06, 0x06,
	0x07, 0x07, 0x07, 0x07,
	0x08, 0x08, 0x08, 0x08,
	0x09, 0x09, 0x09, 0x09,
	0x0A, 0x0A, 0x0A, 0x0A,
	0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B,
	0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
	0x0D,
	-100
};

SoundGenPCJr::SoundGenPCJr(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer) {
	_chanAllocated = 10240; // preallocate something which will most likely fit
	_chanData = (int16 *)malloc(_chanAllocated << 1);

	// Pick dissolve method
	//
	// 0 = no dissolve.. just play for as long as it's meant to be played
	// this was used in older v2.4 and under games i THINK
	// 1 = not used
	// 2 = v2.9+ games used a shorter dissolve
	// 3 (default) = v3 games used this dissolve pattern.. slightly longer
	if (_vm->getVersion() >= 0x3000)
		_dissolveMethod = 3;
	else if (_vm->getVersion() >= 0x2900)
		_dissolveMethod = 2;
	else
		_dissolveMethod = 0;

	if (ConfMan.getBool("pcjr_16bitnoise"))
		_periodicNoiseMask = 0x10000; // non-standard, SEGA-like 16-bit periodic noise mask
	else
		_periodicNoiseMask = 0x08000; // default 15-bit periodic noise mask

	memset(_channel, 0, sizeof(_channel));
	memset(_tchannel, 0, sizeof(_tchannel));

	_v1data = nullptr;
	_v1size = 0;
	_v1duration = 0;

	_reg = 0;

	_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
}

SoundGenPCJr::~SoundGenPCJr() {
	free(_chanData);

	_mixer->stopHandle(*_soundHandle);
}

void SoundGenPCJr::play(int resnum) {
	Common::StackLock lock(_mutex);

	PCjrSound *pcjrSound = (PCjrSound *)_vm->_game.sounds[resnum];

	for (int i = 0; i < CHAN_MAX; i++) {
		_channel[i].data = pcjrSound->getVoicePointer(i % 4);
		_channel[i].duration = 0;
		_channel[i].avail = 0xffff;
		_channel[i].dissolveCount = 0xFFFF;
		_channel[i].attenuation = 0;
		_channel[i].attenuationCopy = 0;
		_channel[i].genType = kGenSilence;
		_channel[i].freqCount = 0;

		_tchannel[i].avail = 1;
		_tchannel[i].noteCount = 0;
		_tchannel[i].freqCount = 250;
		_tchannel[i].freqCountPrev = -1;
		_tchannel[i].atten = 0xF;   // silence
		_tchannel[i].genType = kGenTone;
		_tchannel[i].genTypePrev = -1;
		_tchannel[i].count = 0;
		_tchannel[i].scale = 0;
		_tchannel[i].sign = 0;
		_tchannel[i].noiseState = 0;
		_tchannel[i].feedback = 0;
	}

	_v1data = pcjrSound->getData() + 1;
	_v1size = pcjrSound->getLength() - 1;
	_v1duration = 0;

	_reg = 0;
}

void SoundGenPCJr::stop() {
	Common::StackLock lock(_mutex);

	for (int i = 0; i < CHAN_MAX; i++) {
		_channel[i].avail = 0;
		_tchannel[i].avail = 0;
	}
}

int SoundGenPCJr::volumeCalc(SndGenChan *chan) {
	int8 attenuation, dissolveValue;

	const int8 *dissolveData;

	switch (_dissolveMethod) {
	case 2:
		dissolveData = dissolveDataV2;
		break;
	case 3:
	default:
		dissolveData = dissolveDataV3;
		break;
	}

	assert(chan);

	attenuation = chan->attenuation;
	if (attenuation != 0x0F) {  // != silence
		if (chan->dissolveCount != 0xFFFF) {
			dissolveValue = dissolveData[chan->dissolveCount];
			if (dissolveValue == -100) {    // if at end of list
				chan->dissolveCount = 0xFFFF;
				chan->attenuation = chan->attenuationCopy;
				attenuation = chan->attenuation;
			} else {
				chan->dissolveCount++;

				attenuation += dissolveValue;
				if (attenuation < 0)
					attenuation = 0;
				if (attenuation > 0x0F)
					attenuation = 0x0F;

				chan->attenuationCopy = attenuation;

				attenuation &= 0x0F;
			}
		}
		//if (computer_type == 2) && (attenuation < 8)
		if (attenuation < 8)
			attenuation += 2;
	}

	return attenuation;
}

int SoundGenPCJr::getNextNote(int ch) {
	if (_vm->getVersion() > 0x2001)
		return getNextNote_v2(ch);
	else
		return getNextNote_v1(ch);

	return -1;
}

// read the next channel data.. fill it in *tone
// if tone isn't touched.. it should be inited so it just plays silence
// return 0 if it's passing more data
// return -1 if it's passing nothing (end of data)
int SoundGenPCJr::getNextNote_v2(int ch) {
	ToneChan *tpcm;
	SndGenChan *chan;
	const byte *data;

	assert(ch < CHAN_MAX);

	if (!_vm->getFlag(VM_FLAG_SOUND_ON))
		return -1;

	tpcm = &_tchannel[ch];
	chan = &_channel[ch];
	if (!chan->avail)
		return -1;

	while (chan->duration <= 0) {
		data = chan->data;

		// read the duration of the note
		chan->duration = READ_LE_UINT16(data);  // duration

		// if it's 0 then it's not going to be played
		// if it's 0xFFFF then the channel data has finished.
		if ((chan->duration == 0) || (chan->duration == 0xFFFF)) {
			tpcm->genTypePrev = -1;
			tpcm->freqCountPrev = -1;

			break;
		}

		tpcm->genTypePrev = tpcm->genType;
		tpcm->freqCountPrev = tpcm->freqCount;

		// only tone channels dissolve
		if ((ch != 3) && (_dissolveMethod != 0))    // != noise??
			chan->dissolveCount = 0;

		// attenuation (volume)
		writeData(data[4]);

		// frequency
		writeData(data[3]);
		writeData(data[2]);

		// data now points to the next data seg-a-ment
		chan->data += 5;
	}

	if (chan->duration == 0xFFFF) {
		// kill channel
		chan->avail = 0;
		chan->attenuation = 0x0F;   // silent
		chan->attenuationCopy = 0x0F;   // dunno really

		return -1;
	}

	chan->duration--;

	return 0;
}

int SoundGenPCJr::getNextNote_v1(int ch) {
	byte *data = _v1data;
	uint32 len = _v1size;

	if (!_vm->getFlag(VM_FLAG_SOUND_ON))
		return -1;

	if (len <= 0 || data == nullptr) {
		_channel[ch].avail = 0;
		_channel[ch].attenuation = 0x0F;
		_channel[ch].attenuationCopy = 0x0F;
		return -1;
	}

	// In the V1 player the default duration for a row is 3 ticks
	if (_v1duration > 0) {
		_v1duration--;
		return 0;
	}
	_v1duration = 3 * CHAN_MAX;

	// Otherwise fetch a row of data for all channels
	while (*data) {
		writeData(*data);
		data++;
		len--;
	}
	data++;
	len--;

	_v1data = data;
	_v1size = len;

	return 0;
}

void SoundGenPCJr::writeData(uint8 val) {
	debugC(5, kDebugLevelSound, "writeData(%.2X)", val);

	if ((val & 0x90) == 0x90) {
		_reg = (val >> 5) & 0x3;
		_channel[_reg].attenuation = val & 0xF;
	} else if ((val & 0xF0) == 0xE0) {
		_channel[3].genType = (val & 0x4) ? kGenWhite : kGenPeriod;
		int noiseFreq = val & 0x03;
		switch (noiseFreq) {
		case 0:
			_channel[3].freqCount = 32;
			break;
		case 1:
			_channel[3].freqCount = 64;
			break;
		case 2:
			_channel[3].freqCount = 128;
			break;
		case 3:
			_channel[3].freqCount = _channel[2].freqCount * 2;
			break;
		default:
			break;
		}
	} else if (val & 0x80) {
		_reg = (val >> 5) & 0x3;
		_channel[_reg].freqCount = val & 0xF;
		_channel[_reg].genType = kGenTone;
	} else {
		_channel[_reg].freqCount |= (val & 0x3F) << 4;
	}
}

// Formulas for noise generator
// bit0 = output

// noise feedback for white noise mode
#define FB_WNOISE 0x12000   // bit15.d(16bits) = bit0(out) ^ bit2
//#define FB_WNOISE 0x14000 // bit15.d(16bits) = bit0(out) ^ bit1
//#define FB_WNOISE 0x28000 // bit16.d(17bits) = bit0(out) ^ bit2 (same to AY-3-8910)
//#define FB_WNOISE 0x50000 // bit17.d(18bits) = bit0(out) ^ bit2

// noise feedback for periodic noise mode
// it is correct maybe (it was in the Megadrive sound manual)
//#define FB_PNOISE 0x10000 // 16bit rorate
//#define FB_PNOISE 0x08000

// noise generator start preset (for periodic noise)
#define NG_PRESET 0x0f35

//#define WAVE_HEIGHT (0x7FFF)

// Volume table.
//
// 2dB = 20*log(a/b)
// 10^(2/20)*b = a;
// value = 0x7fff;
// value /= 1.258925411794;
const int16 volTable[16] = {
	32767, 26027, 20674, 16422, 13044, 10361, 8230, 6537, 5193, 4125, 3276, 2602, 2067, 1642, 1304, 0
};

#define FREQ_DIV 111844
#define MULT FREQ_DIV

// fill buff
int SoundGenPCJr::chanGen(int chan, int16 *stream, int len) {
	ToneChan *tpcm;
	int fillSize;
	int retVal;

	tpcm = &_tchannel[chan];

	retVal = -1;

	while (len > 0) {
		if (tpcm->noteCount <= 0) {
			// get new tone data
			if ((tpcm->avail) && (getNextNote(chan) == 0)) {
				tpcm->atten = volumeCalc(&_channel[chan]);
				tpcm->freqCount = _channel[chan].freqCount;
				tpcm->genType = _channel[chan].genType;

				// setup counters 'n stuff
				// SAMPLE_RATE samples per sec.. tone changes 60 times per sec
				tpcm->noteCount = SAMPLE_RATE / 60;
				retVal = 0;
			} else {
				// if it doesn't return an
				tpcm->genType = kGenSilence;
				tpcm->noteCount = len;
				tpcm->avail = 0;
			}
		}

		// write nothing
		if ((tpcm->freqCount == 0) || (tpcm->atten == 0xf)) {
			tpcm->genType = kGenSilence;
		}

		// find which is smaller.. the buffer or the
		fillSize = (tpcm->noteCount <= len) ? tpcm->noteCount : len;

		switch (tpcm->genType) {
		case kGenTone:
			fillSize = fillSquare(tpcm, stream, fillSize);
			break;
		case kGenPeriod:
		case kGenWhite:
			fillSize = fillNoise(tpcm, stream, fillSize);
			break;
		case kGenSilence:
		default:
			// fill with whitespace
			memset(stream, 0, fillSize * sizeof(int16));
			break;
		}

		tpcm->noteCount -= fillSize;
		stream += fillSize;
		len -= fillSize;
	}

	return retVal;
}

int SoundGenPCJr::fillSquare(ToneChan *t, int16 *buf, int len) {
	int count;

	if (t->genType != t->genTypePrev) {
		// make sure the freqCount is checked
		t->freqCountPrev = -1;
		t->sign = 1;
		t->genTypePrev = t->genType;
	}

	if (t->freqCount != t->freqCountPrev) {
		//t->scale = (int)( (double)t->samp->freq*t->freqCount/FREQ_DIV * MULT + 0.5);
		t->scale = (SAMPLE_RATE / 2) * t->freqCount;
		//t->count = t->scale;
		t->freqCountPrev = t->freqCount;
	}

	count = len;

	int16 amp = (int16)(volTable[t->atten] * _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / Audio::Mixer::kMaxMixerVolume);
	while (count > 0) {
		*(buf++) = t->sign ? amp : -amp;
		count--;

		// get next sample
		t->count -= MULT;
		while (t->count <= 0) {
			t->sign ^= 1;
			t->count += t->scale;
		}
	}

	return len;
}

int SoundGenPCJr::fillNoise(ToneChan *t, int16 *buf, int len) {
	int count;

	if (t->genType != t->genTypePrev) {
		// make sure the freqCount is checked
		t->freqCountPrev = -1;
		t->genTypePrev = t->genType;
	}

	if (t->freqCount != t->freqCountPrev) {
		//t->scale = (int)( (double)t->samp->freq*t->freqCount/FREQ_DIV * MULT + 0.5);
		t->scale = (SAMPLE_RATE / 2) * t->freqCount;
		t->count = t->scale;
		t->freqCountPrev = t->freqCount;

		t->feedback = (t->genType == kGenWhite) ? FB_WNOISE : _periodicNoiseMask;
		// reset noise shifter
		t->noiseState = NG_PRESET;
		t->sign = t->noiseState & 1;
	}

	count = len;

	int16 amp = (int16)(volTable[t->atten] * _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / Audio::Mixer::kMaxMixerVolume);
	while (count > 0) {
		*(buf++) = t->sign ? amp : -amp;
		count--;

		// get next sample
		t->count -= MULT;
		while (t->count <= 0) {
			if (t->noiseState & 1)
				t->noiseState ^= t->feedback;

			t->noiseState >>= 1;
			t->sign = t->noiseState & 1;
			t->count += t->scale;
		}
	}

	return len;
}

int SoundGenPCJr::readBuffer(int16 *stream, const int len) {
	Common::StackLock lock(_mutex);

	if (_chanAllocated < len) {
		free(_chanData);
		_chanData = (int16 *)malloc(len << 1);
		_chanAllocated = len;
	}
	memset(stream, 0, len << 1);

	assert(stream);

	bool finished = true;

	for (int i = 0; i < CHAN_MAX; i++) {
		// get channel data(chan.userdata)
		if (chanGen(i, _chanData, len) == 0) {
			// divide by number of channels then add to stream
			int streamCount = len;
			int16 *sPtr = stream;
			int16 *cPtr = _chanData;

			while (streamCount--)
				*(sPtr++) += *(cPtr++) / CHAN_MAX;

			finished = false;
		}
	}

	if (finished)
		_vm->_sound->soundIsFinished();

	return len;
}

} // End of namespace Agi
