/* 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 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.
 *
 */

#ifndef DISABLE_SID

#include "engines/engine.h"
#include "scumm/players/player_sid.h"
#include "scumm/scumm.h"
#include "audio/mixer.h"

namespace Scumm {

/*
 * The player's update() routine is called once per (NTSC/PAL) frame as it is
 * called by the VIC Rasterline interrupt handler which is in turn called
 * approx. 50 (PAL) or 60 (NTSC) times per second.
 * The SCUMM V0/V1 music playback routines or sound data have not been adjusted
 * to PAL systems. As a consequence, music is played audibly (-16%) slower
 * on PAL systems.
 * In addition, the SID oscillator frequency depends on the video clock too.
 * As SCUMM games use an NTSC frequency table for both NTSC and PAL versions
 * all tone frequencies on PAL systems are slightly (-4%) lower than on NTSC ones.
 *
 * For more info on the SID chip see:
 * - http://www.dopeconnection.net/C64_SID.htm (German)
 * For more info on the VIC chip see:
 * - http://www.htu.tugraz.at/~herwig/c64/man-vic.php (German)
 * - http://www.c64-wiki.de/index.php/VIC (German)
 */

struct TimingProps {
	double clockFreq;
	int cyclesPerFrame;
};

static const TimingProps timingProps[2] = {
	{ 17734472.0 / 18, 312 * 63 }, // PAL:  312*63 cycles/frame @  985248 Hz (~50Hz)
	{ 14318180.0 / 14, 263 * 65 }  // NTSC: 263*65 cycles/frame @ 1022727 Hz (~60Hz)
};

static const uint8 BITMASK[7] = {
	0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40
};
static const uint8 BITMASK_INV[7] = {
	0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF
};

static const int SID_REG_OFFSET[7] = {
	0, 7, 14, 21, 2, 9, 16
};

// NTSC frequency table (also used for PAL versions).
// FREQ_TBL[i] = tone_freq[i] * 2^24 / clockFreq
static const uint16 FREQ_TBL[97] = {
	0x0000, 0x010C, 0x011C, 0x012D, 0x013E, 0x0151, 0x0166, 0x017B,
	0x0191, 0x01A9, 0x01C3, 0x01DD, 0x01FA, 0x0218, 0x0238, 0x025A,
	0x027D, 0x02A3, 0x02CC, 0x02F6, 0x0323, 0x0353, 0x0386, 0x03BB,
	0x03F4, 0x0430, 0x0470, 0x04B4, 0x04FB, 0x0547, 0x0598, 0x05ED,
	0x0647, 0x06A7, 0x070C, 0x0777, 0x07E9, 0x0861, 0x08E1, 0x0968,
	0x09F7, 0x0A8F, 0x0B30, 0x0BDA, 0x0C8F, 0x0D4E, 0x0E18, 0x0EEF,
	0x0FD2, 0x10C3, 0x11C3, 0x12D1, 0x13EF, 0x151F, 0x1660, 0x17B5,
	0x191E, 0x1A9C, 0x1C31, 0x1DDF, 0x1FA5, 0x2187, 0x2386, 0x25A2,
	0x27DF, 0x2A3E, 0x2CC1, 0x2F6B, 0x323C, 0x3539, 0x3863, 0x3BBE,
	0x3F4B, 0x430F, 0x470C, 0x4B45, 0x4FBF, 0x547D, 0x5983, 0x5ED6,
	0x6479, 0x6A73, 0x70C7, 0x777C, 0x7E97, 0x861E, 0x8E18, 0x968B,
	0x9F7E, 0xA8FA, 0xB306, 0xBDAC, 0xC8F3, 0xD4E6, 0xE18F, 0xEEF8,
	0xFD2E
};

static const int SONG_CHANNEL_OFFSET[3] = { 6, 8, 10 };
static const int RES_ID_CHANNEL[3] = { 3, 4, 5 };

#define LOBYTE_(a) ((a) & 0xFF)
#define HIBYTE_(a) (((a) >> 8) & 0xFF)

#define GETBIT(var, pos) ((var) & (1<<(pos)))

void Player_SID::handleMusicBuffer() { // $33cd
	int channel = 2;
	while (channel >= 0) {
		if ((statusBits1A & BITMASK[channel]) == 0 ||
		    (busyChannelBits & BITMASK[channel]) != 0) {
			--channel;
			continue;
		}

		if (setupSongFileData() == 1)
			return;

		uint8* l_chanFileDataPtr = chanFileData[channel];

		uint16 l_freq = 0;
		bool l_keepFreq = false;

		int y = 0;
		uint8 curByte = l_chanFileDataPtr[y++];

		// freq or 0/0xFF
		if (curByte == 0) {
			func_3674(channel);
			if (!isMusicPlaying)
				return;
			continue;
		} else if (curByte == 0xFF) {
			l_keepFreq = true;
		} else {
			l_freq = FREQ_TBL[curByte];
		}

		uint8 local1 = 0;
		curByte = l_chanFileDataPtr[y++];
		bool isLastCmdByte = (curByte & 0x80) != 0;
		uint16 curStepSum = stepTbl[curByte & 0x7f];

		for (int i = 0; !isLastCmdByte && (i < 2); ++i) {
			curByte = l_chanFileDataPtr[y++];
			isLastCmdByte = (curByte & 0x80) != 0;
			if (curByte & 0x40) {
				// note: bit used in zak theme (95) only (not used/handled in MM)
				_music_timer = curByte & 0x3f;
			} else {
				local1 = curByte & 0x3f;
			}
		}

		chanFileData[channel] += y;
		chanDataOffset[channel] += y;

		uint8 *l_chanBuf = getResource(RES_ID_CHANNEL[channel]);

		if (local1 != 0) {
			// TODO: signed or unsigned?
			uint16 offset = READ_LE_UINT16(&actSongFileData[local1*2 + 12]);
			l_chanFileDataPtr = actSongFileData + offset;

			// next five bytes: freqDelta, attack, sustain and phase bit
			for (int i = 0; i < 5; ++i) {
				l_chanBuf[15 + i] = l_chanFileDataPtr[i];
			}
			phaseBit[channel] = l_chanFileDataPtr[4];

			for (int i = 0; i < 17; ++i) {
				l_chanBuf[25 + i] = l_chanFileDataPtr[5 + i];
			}
		}

		if (l_keepFreq) {
			if (!releasePhase[channel]) {
				l_chanBuf[10] &= 0xfe; // release phase
			}
			releasePhase[channel] = true;
		} else {
			if (releasePhase[channel]) {
				l_chanBuf[19] = phaseBit[channel];
				l_chanBuf[10] |= 0x01; // attack phase
			}
			l_chanBuf[11] = LOBYTE_(l_freq);
			l_chanBuf[12] = HIBYTE_(l_freq);
			releasePhase[channel] = false;
		}

		// set counter value for frequency update (freqDeltaCounter)
		l_chanBuf[13] = LOBYTE_(curStepSum);
		l_chanBuf[14] = HIBYTE_(curStepSum);

		_soundQueue[channel] = RES_ID_CHANNEL[channel];
		processSongData(channel);
		_soundQueue[channel+4] = RES_ID_CHANNEL[channel];
		processSongData(channel+4);
		--channel;
	}
}

int Player_SID::setupSongFileData() { // $36cb
	// no song playing
	// TODO: remove (never NULL)
	if (_music == NULL) {
		for (int i = 2; i >= 0; --i) {
			if (songChannelBits & BITMASK[i]) {
				func_3674(i);
			}
		}
		return 1;
	}

	// no new song
	songFileOrChanBufData = _music;
	if (_music == actSongFileData) {
		return 0;
	}

	// new song selected
	actSongFileData = _music;
	for (int i = 0; i < 3; ++i) {
		chanFileData[i] = _music + chanDataOffset[i];
	}

	return -1;
}

//x:0..2
void Player_SID::func_3674(int channel) { // $3674
	statusBits1B &= BITMASK_INV[channel];
	if (statusBits1B == 0) {
		isMusicPlaying = false;
		unlockCodeLocation();
		safeUnlockResource(resID_song);
		for (int i = 0; i < 3; ++i) {
			safeUnlockResource(RES_ID_CHANNEL[i]);
		}
	}

	chanPrio[channel] = 2;

	statusBits1A &= BITMASK_INV[channel];
	phaseBit[channel] = 0;

	func_4F45(channel);
}

void Player_SID::resetPlayerState() { // $48f7
	for (int i = 6; i >= 0; --i)
		releaseChannel(i);

	isMusicPlaying = false;
	unlockCodeLocation(); // does nothing
	statusBits1B = 0;
	statusBits1A = 0;
	freeChannelCount = 3;
	swapPrepared = false;
	filterSwapped = false;
	pulseWidthSwapped = false;
	//var5163 = 0;
}

void Player_SID::resetSID() { // $48D8
	SIDReg24 = 0x0f;

	SID_Write( 4, 0);
	SID_Write(11, 0);
	SID_Write(18, 0);
	SID_Write(23, 0);
	SID_Write(21, 0);
	SID_Write(22, 0);
	SID_Write(24, SIDReg24);

	resetPlayerState();
}

void Player_SID::update() { // $481B
	if (initializing)
		return;

	if (_soundInQueue) {
		for (int i = 6; i >= 0; --i) {
			if (_soundQueue[i] != -1)
				processSongData(i);
		}
		_soundInQueue = false;
	}

	// no sound
	if (busyChannelBits == 0)
		return;

	for (int i = 6; i >= 0; --i) {
		if (busyChannelBits & BITMASK[i]) {
			updateFreq(i);
		}
	}

	// seems to be used for background (prio=1?) sounds.
	// If a bg sound cannot be played because all SID
	// voices are used by higher priority sounds, the
	// bg sound's state is updated here so it will be at
	// the correct state when a voice is available again.
	if (swapPrepared) {
		swapVars(0, 0);
		swapVarLoaded = true;
		updateFreq(0);
		swapVars(0, 0);
		if (pulseWidthSwapped) {
			swapVars(4, 1);
			updateFreq(4);
			swapVars(4, 1);
		}
		swapVarLoaded = false;
	}

	for (int i = 6; i >= 0; --i) {
		if (busyChannelBits & BITMASK[i])
			setSIDWaveCtrlReg(i);
	};

	if (isMusicPlaying) {
		handleMusicBuffer();
	}

	return;
}

// channel: 0..6
void Player_SID::processSongData(int channel) { // $4939
	// always: _soundQueue[channel] != -1
	// -> channelMap[channel] != -1
	channelMap[channel] = _soundQueue[channel];
	_soundQueue[channel] = -1;
	songPosUpdateCounter[channel] = 0;

	isVoiceChannel = (channel < 3);

	songFileOrChanBufOffset[channel] = vec6[channel];

	setupSongPtr(channel);

	//vec5[channel] = songFileOrChanBufData; // not used

	if (songFileOrChanBufData == NULL) { // chanBuf (4C1C)
		/*
		// TODO: do we need this?
		LOBYTE_(vec20[channel]) = 0;
		LOBYTE_(songPosPtr[channel]) = LOBYTE_(songFileOrChanBufOffset[channel]);
		*/
		releaseResourceUnk(channel);
		return;
	}

	vec20[channel] = songFileOrChanBufData; // chanBuf (4C1C)
	songPosPtr[channel] = songFileOrChanBufData + songFileOrChanBufOffset[channel]; // chanBuf (4C1C)
	uint8* ptr1 = songPosPtr[channel];

	int y = -1;
	if (channel < 4) {
		++y;
		if (channel == 3) {
			readSetSIDFilterAndProps(&y, ptr1);
		} else if (statusBits1A & BITMASK[channel]) {
			++y;
		} else { // channel = 0/1/2
			waveCtrlReg[channel] = ptr1[y];

			++y;
			if (ptr1[y] & 0x0f) {
				// filter on for voice channel
				SIDReg23 |= BITMASK[channel];
			} else {
				// filter off for voice channel
				SIDReg23 &= BITMASK_INV[channel];
			}
			SID_Write(23, SIDReg23);
		}
	}

	saveSongPos(y, channel);
	busyChannelBits |= BITMASK[channel];
	readSongChunk(channel);
}

void Player_SID::readSetSIDFilterAndProps(int *offset, uint8* dataPtr) {  // $49e7
	SIDReg23 |= dataPtr[*offset];
	SID_Write(23, SIDReg23);
	++*offset;
	SIDReg24 = dataPtr[*offset];
	SID_Write(24, SIDReg24);
}

void Player_SID::saveSongPos(int y, int channel) {
	++y;
	songPosPtr[channel] += y;
	songFileOrChanBufOffset[channel] += y;
}

// channel: 0..6
void Player_SID::updateFreq(int channel) {
	isVoiceChannel = (channel < 3);

	--freqDeltaCounter[channel];
	if (freqDeltaCounter[channel] < 0) {
		readSongChunk(channel);
	} else {
		freqReg[channel] += freqDelta[channel];
	}
	setSIDFreqAS(channel);
}

void Player_SID::resetFreqDelta(int channel) {
	freqDeltaCounter[channel] = 0;
	freqDelta[channel] = 0;
}

void Player_SID::readSongChunk(int channel) { // $4a6b
	while (true) {
		if (setupSongPtr(channel) == 1) {
			// do something with code resource
			releaseResourceUnk(1);
			return;
		}

		uint8* ptr1 = songPosPtr[channel];

		//curChannelActive = true;

		uint8 l_cmdByte = ptr1[0];
		if (l_cmdByte == 0) {
			//curChannelActive = false;
			songPosUpdateCounter[channel] = 0;

			var481A = -1;
			releaseChannel(channel);
			return;
		}

		//vec19[channel] = l_cmdByte;

		// attack (1) / release (0) phase
		if (isVoiceChannel) {
			if (GETBIT(l_cmdByte, 0))
				waveCtrlReg[channel] |= 0x01; // start attack phase
			else
				waveCtrlReg[channel] &= 0xfe; // start release phase
		}

		// channel finished bit
		if (GETBIT(l_cmdByte, 1)) {
			var481A = -1;
			releaseChannel(channel);
			return;
		}

		int y = 0;

		// frequency
		if (GETBIT(l_cmdByte, 2)) {
			y += 2;
			freqReg[channel] = READ_LE_UINT16(&ptr1[y-1]);
			if (!GETBIT(l_cmdByte, 6)) {
				y += 2;
				freqDeltaCounter[channel] = READ_LE_UINT16(&ptr1[y-1]);
				y += 2;
				freqDelta[channel] = READ_LE_UINT16(&ptr1[y-1]);
			} else {
				resetFreqDelta(channel);
			}
		} else {
			resetFreqDelta(channel);
		}

		// attack / release
		if (isVoiceChannel && GETBIT(l_cmdByte, 3)) {
			// start release phase
			waveCtrlReg[channel] &= 0xfe;
			setSIDWaveCtrlReg(channel);

			++y;
			attackReg[channel] = ptr1[y];
			++y;
			sustainReg[channel] = ptr1[y];

			// set attack (1) or release (0) phase
			waveCtrlReg[channel]  |= (l_cmdByte & 0x01);
		}

		if (GETBIT(l_cmdByte, 4)) {
			++y;
			uint8 curByte = ptr1[y];

			// pulse width
			if (isVoiceChannel && GETBIT(curByte, 0)) {
				int reg = SID_REG_OFFSET[channel+4];

				y += 2;
				SID_Write(reg, ptr1[y-1]);
				SID_Write(reg+1, ptr1[y]);
			}

			if (GETBIT(curByte, 1)) {
				++y;
				readSetSIDFilterAndProps(&y, ptr1);

				y += 2;
				SID_Write(21, ptr1[y-1]);
				SID_Write(22, ptr1[y]);
			}

			if (GETBIT(curByte, 2)) {
				resetFreqDelta(channel);

				y += 2;
				freqDeltaCounter[channel] = READ_LE_UINT16(&ptr1[y-1]);
			}
		}

		// set waveform (?)
		if (GETBIT(l_cmdByte, 5)) {
			++y;
			waveCtrlReg[channel] = (waveCtrlReg[channel] & 0x0f) | ptr1[y];
		}

		// song position
		if (GETBIT(l_cmdByte, 7)) {
			if (songPosUpdateCounter[channel] == 1) {
				y += 2;
				--songPosUpdateCounter[channel];
				saveSongPos(y, channel);
			} else {
				// looping / skipping / ...
				++y;
				songPosPtr[channel] -= ptr1[y];
				songFileOrChanBufOffset[channel] -= ptr1[y];

				++y;
				if (songPosUpdateCounter[channel] == 0) {
					songPosUpdateCounter[channel] = ptr1[y];
				} else {
					--songPosUpdateCounter[channel];
				}
			}
		} else {
			saveSongPos(y, channel);
			return;
		}
	}
}

/**
 * Sets frequency, attack and sustain register
 */
void Player_SID::setSIDFreqAS(int channel) { // $4be6
	if (swapVarLoaded)
		return;
	int reg = SID_REG_OFFSET[channel];
	SID_Write(reg,   LOBYTE_(freqReg[channel]));   // freq/pulseWidth voice 1/2/3
	SID_Write(reg+1, HIBYTE_(freqReg[channel]));
	if (channel < 3) {
		SID_Write(reg+5, attackReg[channel]); // attack
		SID_Write(reg+6, sustainReg[channel]); // sustain
	}
}

void Player_SID::setSIDWaveCtrlReg(int channel) { // $4C0D
	if (channel < 3) {
		int reg = SID_REG_OFFSET[channel];
		SID_Write(reg+4, waveCtrlReg[channel]);
	}
}

// channel: 0..6
int Player_SID::setupSongPtr(int channel) { // $4C1C
	//resID:5,4,3,songid
	int resID = channelMap[channel];

	// TODO: when does this happen, only if resID == 0?
	if (getResource(resID) == NULL) {
		releaseResourceUnk(resID);
		if (resID == bgSoundResID) {
			bgSoundResID = 0;
			bgSoundActive = false;
			swapPrepared = false;
			pulseWidthSwapped = false;
		}
		return 1;
	}

	songFileOrChanBufData = getResource(resID); // chanBuf (4C1C)
	if (songFileOrChanBufData == vec20[channel]) {
		return 0;
	} else {
		vec20[channel] = songFileOrChanBufData;
		songPosPtr[channel] = songFileOrChanBufData + songFileOrChanBufOffset[channel];
		return -1;
	}
}

// ignore: no effect
// chanResIndex: 3,4,5 or 58
void Player_SID::unlockResource(int chanResIndex) { // $4CDA
	if ((resStatus[chanResIndex] & 0x7F) != 0)
		--resStatus[chanResIndex];
}

void Player_SID::countFreeChannels() { // $4f26
	freeChannelCount = 0;
	for (int i = 0; i < 3; ++i) {
		if (GETBIT(usedChannelBits, i) == 0)
			++freeChannelCount;
	}
}

void Player_SID::func_4F45(int channel) { // $4F45
	if (swapVarLoaded) {
		if (channel == 0) {
			swapPrepared = false;
			resetSwapVars();
		}
		pulseWidthSwapped = false;
	} else {
		if (channel == 3) {
			filterUsed = false;
		}

		if (chanPrio[channel] == 1) {
			if (var481A == 1)
				prepareSwapVars(channel);
			else if (channel < 3)
				clearSIDWaveform(channel);
		} else if (channel < 3 && bgSoundActive && swapPrepared &&
		    !(filterSwapped && filterUsed))
		{
			busyChannelBits |= BITMASK[channel];
			useSwapVars(channel);
			waveCtrlReg[channel] |= 0x01;
			setSIDWaveCtrlReg(channel);

			safeUnlockResource(channelMap[channel]);
			return;
		}

		chanPrio[channel] = 0;
		usedChannelBits &= BITMASK_INV[channel];
		countFreeChannels();
	}

	int resIndex = channelMap[channel];
	channelMap[channel] = 0;
	safeUnlockResource(resIndex);
}

// chanResIndex: 3,4,5 or 58
void Player_SID::safeUnlockResource(int resIndex) { // $4FEA
	if (!isMusicPlaying) {
		unlockResource(resIndex);
	}
}

void Player_SID::releaseResource(int resIndex) { // $5031
	releaseResChannels(resIndex);
	if (resIndex == bgSoundResID && var481A == -1) {
		safeUnlockResource(resIndex);

		bgSoundResID = 0;
		bgSoundActive = false;
		swapPrepared = false;
		pulseWidthSwapped = false;

		resetSwapVars();
	}
}

void Player_SID::releaseResChannels(int resIndex) { // $5070
	for (int i = 3; i >= 0; --i) {
		if (resIndex == channelMap[i]) {
			releaseChannel(i);
		}
	}
}

void Player_SID::stopSound_intern(int soundResID) { // $5093
	for (int i = 0; i < 7; ++i) {
		if (soundResID == _soundQueue[i]) {
			_soundQueue[i] = -1;
		}
	}
	var481A = -1;
	releaseResource(soundResID);
}

void Player_SID::stopMusic_intern() { // $4CAA
	statusBits1B = 0;
	isMusicPlaying = false;

	if (resID_song != 0) {
		unlockResource(resID_song);
	}

	chanPrio[0] = 2;
	chanPrio[1] = 2;
	chanPrio[2] = 2;

	statusBits1A = 0;
	phaseBit[0] = 0;
	phaseBit[1] = 0;
	phaseBit[2] = 0;
}

void Player_SID::releaseResourceUnk(int resIndex) { // $50A4
	var481A = -1;
	releaseResource(resIndex);
}

// a: 0..6
void Player_SID::releaseChannel(int channel) {
	stopChannel(channel);
	if (channel >= 4) {
		return;
	}
	if (channel < 3) {
		SIDReg23Stuff = SIDReg23;
		clearSIDWaveform(channel);
	}
	func_4F45(channel);
	if (channel >= 3) {
		return;
	}
	if ((SIDReg23 != SIDReg23Stuff) &&
	    (SIDReg23 & 0x07) == 0)
	{
		if (filterUsed) {
			func_4F45(3);
			stopChannel(3);
		}
	}

	stopChannel(channel + 4);
}

void Player_SID::clearSIDWaveform(int channel) {
	if (!isMusicPlaying && var481A == -1) {
		waveCtrlReg[channel] &= 0x0e;
		setSIDWaveCtrlReg(channel);
	}
}

void Player_SID::stopChannel(int channel) {
	songPosUpdateCounter[channel] = 0;
	// clear "channel" bit
	busyChannelBits &= BITMASK_INV[channel];
	if (channel >= 4) {
		// pulsewidth = 0
		channelMap[channel] = 0;
	}
}

// channel: 0..6, swapIndex: 0..2
void Player_SID::swapVars(int channel, int swapIndex) { // $51a5
	if (channel < 3) {
		SWAP(attackReg[channel], swapAttack[swapIndex]);
		SWAP(sustainReg[channel], swapSustain[swapIndex]);
	}
	//SWAP(vec5[channel],  swapVec5[swapIndex]);  // not used
	//SWAP(vec19[channel], swapVec19[swapIndex]); // not used

	SWAP(chanPrio[channel], swapSongPrio[swapIndex]);
	SWAP(channelMap[channel], swapVec479C[swapIndex]);
	SWAP(songPosUpdateCounter[channel], swapSongPosUpdateCounter[swapIndex]);
	SWAP(waveCtrlReg[channel], swapWaveCtrlReg[swapIndex]);
	SWAP(songPosPtr[channel],  swapSongPosPtr[swapIndex]);
	SWAP(freqReg[channel],  swapFreqReg[swapIndex]);
	SWAP(freqDeltaCounter[channel], swapVec11[swapIndex]);
	SWAP(freqDelta[channel], swapVec10[swapIndex]);
	SWAP(vec20[channel], swapVec20[swapIndex]);
	SWAP(songFileOrChanBufOffset[channel],  swapVec8[swapIndex]);
}

void Player_SID::resetSwapVars() { // $52d0
	for (int i = 0; i < 2; ++i) {
		swapAttack[i] = 0;
		swapSustain[i] = 0;
	}
	for (int i = 0; i < 3; ++i) {
		swapVec5[i] = 0;
		swapSongPrio[i] = 0;
		swapVec479C[i] = 0;
		swapVec19[i] = 0;
		swapSongPosUpdateCounter[i] = 0;
		swapWaveCtrlReg[i] = 0;
		swapSongPosPtr[i] = 0;
		swapFreqReg[i] = 0;
		swapVec11[i] = 0;
		swapVec10[i] = 0;
		swapVec20[i] = 0;
		swapVec8[i] = 0;
	}
}

void Player_SID::prepareSwapVars(int channel) { // $52E5
	if (channel >= 4)
		return;

	if (channel < 3) {
		if (!keepSwapVars) {
			resetSwapVars();
		}
		swapVars(channel, 0);
		if (busyChannelBits & BITMASK[channel+4]) {
			swapVars(channel+4, 1);
			pulseWidthSwapped = true;
		}
	} else if (channel == 3) {
		SIDReg24_HiNibble = SIDReg24 & 0x70;
		resetSwapVars();
		keepSwapVars = true;
		swapVars(3, 2);
		filterSwapped = true;
	}
	swapPrepared = true;
}

void Player_SID::useSwapVars(int channel) { // $5342
	if (channel >= 3)
		return;

	swapVars(channel, 0);
	setSIDFreqAS(channel);
	if (pulseWidthSwapped) {
		swapVars(channel+4, 1);
		setSIDFreqAS(channel+4);
	}
	if (filterSwapped) {
		swapVars(3, 2);

		// resonating filter freq. or voice-to-filter mapping?
		SIDReg23 = (SIDReg23Stuff & 0xf0) | BITMASK[channel];
		SID_Write(23, SIDReg23);

		// filter props
		SIDReg24 = (SIDReg24 & 0x0f) | SIDReg24_HiNibble;
		SID_Write(24, SIDReg24);

		// filter freq.
		SID_Write(21, LOBYTE_(freqReg[3]));
		SID_Write(22, HIBYTE_(freqReg[3]));
	} else {
		SIDReg23 = SIDReg23Stuff & BITMASK_INV[channel];
		SID_Write(23, SIDReg23);
	}

	swapPrepared = false;
	pulseWidthSwapped = false;
	keepSwapVars = false;
	SIDReg24_HiNibble = 0;
	filterSwapped = false;
}

// ignore: no effect
// resIndex: 3,4,5 or 58
void Player_SID::lockResource(int resIndex) { // $4ff4
	if (!isMusicPlaying)
		++resStatus[resIndex];
}

void Player_SID::reserveChannel(int channel, uint8 prioValue, int chanResIndex) { // $4ffe
	if (channel == 3) {
		filterUsed = true;
	} else if (channel < 3) {
		usedChannelBits |= BITMASK[channel];
		countFreeChannels();
	}

	chanPrio[channel] = prioValue;
	lockResource(chanResIndex);
}

// ignore: no effect
void Player_SID::unlockCodeLocation() { // $513e
	resStatus[1] &= 0x80;
	resStatus[2] &= 0x80;
}

// ignore: no effect
void Player_SID::lockCodeLocation() { // $514f
	resStatus[1] |= 0x01;
	resStatus[2] |= 0x01;
}

void Player_SID::initMusic(int songResIndex) { // $7de6
	unlockResource(resID_song);

	resID_song = songResIndex;
	_music = getResource(resID_song);
	if (_music == NULL) {
		return;
	}

	// song base address
	uint8* songFileDataPtr = _music;
	actSongFileData = _music;

	initializing = true;
	_soundInQueue = false;
	isMusicPlaying = false;

	unlockCodeLocation();
	resetPlayerState();

	lockResource(resID_song);
	buildStepTbl(songFileDataPtr[5]);

	// fetch sound
	songChannelBits = songFileDataPtr[4];
	for (int i = 2; i >= 0; --i) {
		if ((songChannelBits & BITMASK[i]) != 0) {
			func_7eae(i, songFileDataPtr);
		}
	}

	isMusicPlaying = true;
	lockCodeLocation();

	SIDReg23 &= 0xf0;
	SID_Write(23, SIDReg23);

	handleMusicBuffer();

	initializing = false;
	_soundInQueue = true;
}

// params:
//   channel: channel 0..2
void Player_SID::func_7eae(int channel, uint8* songFileDataPtr) {
	int pos = SONG_CHANNEL_OFFSET[channel];
	chanDataOffset[channel] = READ_LE_UINT16(&songFileDataPtr[pos]);
	chanFileData[channel] = songFileDataPtr + chanDataOffset[channel];

	//vec5[channel+4] = vec5[channel] = CHANNEL_BUFFER_ADDR[RES_ID_CHANNEL[channel]]; // not used
	vec6[channel+4] = 0x0019;
	vec6[channel]   = 0x0008;

	func_819b(channel);

	waveCtrlReg[channel] = 0;
}

void Player_SID::func_819b(int channel) {
	reserveChannel(channel, 127, RES_ID_CHANNEL[channel]);

	statusBits1B |= BITMASK[channel];
	statusBits1A |= BITMASK[channel];
}

void Player_SID::buildStepTbl(int step) { // $82B4
	stepTbl[0] = 0;
	stepTbl[1] = step - 2;
	for (int i = 2; i < 33; ++i) {
		stepTbl[i] = stepTbl[i-1] + step;
	}
}

int Player_SID::reserveSoundFilter(uint8 value, uint8 chanResIndex) { // $4ED0
	int channel = 3;
	reserveChannel(channel, value, chanResIndex);
	return channel;
}

int Player_SID::reserveSoundVoice(uint8 value, uint8 chanResIndex) { // $4EB8
	for (int i = 2; i >= 0; --i) {
		if ((usedChannelBits & BITMASK[i]) == 0) {
			reserveChannel(i, value, chanResIndex);
			return i;
		}
	}
	return 0;
}

void Player_SID::findLessPrioChannels(uint8 soundPrio) { // $4ED8
	minChanPrio = 127;

	chansWithLowerPrioCount = 0;
	for (int i = 2; i >= 0; --i) {
		if (usedChannelBits & BITMASK[i]) {
			if (chanPrio[i] < soundPrio)
				++chansWithLowerPrioCount;
			if (chanPrio[i] < minChanPrio) {
				minChanPrio = chanPrio[i];
				minChanPrioIndex = i;
			}
		}
	}

	if (chansWithLowerPrioCount == 0)
		return;

	if (soundPrio >= chanPrio[3]) {
		actFilterHasLowerPrio = true;
	} else {
		/* TODO: is this really a no-op?
		if (minChanPrioIndex < chanPrio[3])
			minChanPrioIndex = minChanPrioIndex;
		*/

		actFilterHasLowerPrio = false;
	}
}

void Player_SID::releaseResourceBySound(int resID) { // $5088
	var481A = 1;
	releaseResource(resID);
}

void Player_SID::readVec6Data(int x, int *offset, uint8 *songFilePtr, int chanResID) { // $4E99
	//vec5[x] = songFilePtr;
	vec6[x] = songFilePtr[*offset];
	*offset += 2;
	_soundQueue[x] = chanResID;
}

int Player_SID::initSound(int soundResID) { // $4D0A
	initializing = true;

	if (isMusicPlaying && (statusBits1A & 0x07) == 0x07) {
		initializing = false;
		return -2;
	}

	uint8 *songFilePtr = getResource(soundResID);
	if (songFilePtr == NULL) {
		initializing = false;
		return 1;
	}

	uint8 soundPrio = songFilePtr[4];
	// for (mostly but not always looped) background sounds
	if (soundPrio == 1) {
		bgSoundResID = soundResID;
		bgSoundActive = true;
	}

	uint8 requestedChannels = 0;
	if ((songFilePtr[5] & 0x40) == 0) {
		++requestedChannels;
		if (songFilePtr[5] & 0x02)
			++requestedChannels;
		if (songFilePtr[5] & 0x08)
			++requestedChannels;
	}

	bool filterNeeded = (songFilePtr[5] & 0x20) != 0;
	bool filterBlocked = (filterUsed && filterNeeded);
	if (filterBlocked || (freeChannelCount < requestedChannels)) {
		findLessPrioChannels(soundPrio);

		if ((freeChannelCount + chansWithLowerPrioCount < requestedChannels) ||
		    (filterBlocked && !actFilterHasLowerPrio)) {
			initializing = false;
			return -1;
		}

		if (filterBlocked) {
			if (soundPrio < chanPrio[3]) {
				initializing = false;
				return -1;
			}

			uint8 l_resID = channelMap[3];
			releaseResourceBySound(l_resID);
		}

		while ((freeChannelCount < requestedChannels) || (filterNeeded && filterUsed)) {
			findLessPrioChannels(soundPrio);
			if (minChanPrio >= soundPrio) {
				initializing = false;
				return -1;
			}

			uint8 l_resID = channelMap[minChanPrioIndex];
			releaseResourceBySound(l_resID);
		}
	}

	int x;
	uint8 soundByte5 = songFilePtr[5];
	if (soundByte5 & 0x40)
		x = reserveSoundFilter(soundPrio, soundResID);
	else
		x = reserveSoundVoice(soundPrio, soundResID);

	uint8 var4CF3 = x;
	int y = 6;
	if (soundByte5 & 0x01) {
		x += 4;
		readVec6Data(x, &y, songFilePtr, soundResID);
	}
	if (soundByte5 & 0x02) {
		x = reserveSoundVoice(soundPrio, soundResID);
		readVec6Data(x, &y, songFilePtr, soundResID);
	}
	if (soundByte5 & 0x04) {
		x += 4;
		readVec6Data(x, &y, songFilePtr, soundResID);
	}
	if (soundByte5 & 0x08) {
		x = reserveSoundVoice(soundPrio, soundResID);
		readVec6Data(x, &y, songFilePtr, soundResID);
	}
	if (soundByte5 & 0x10) {
		x += 4;
		readVec6Data(x, &y, songFilePtr, soundResID);
	}
	if (soundByte5 & 0x20) {
		x = reserveSoundFilter(soundPrio, soundResID);
		readVec6Data(x, &y, songFilePtr, soundResID);
	}

	//vec5[var4CF3] = songFilePtr;
	vec6[var4CF3] = y;
	_soundQueue[var4CF3] = soundResID;

	initializing = false;
	_soundInQueue = true;

	return soundResID;
}

void Player_SID::unused1() { // $50AF
	var481A = -1;
	if (bgSoundResID != 0) {
		releaseResourceUnk(bgSoundResID);
	}
}

///////////////////////////
///////////////////////////

#define ZEROMEM(a) memset(a, 0, sizeof(a))

Player_SID::Player_SID(ScummEngine *scumm, Audio::Mixer *mixer) {
	/*
	 * clear memory
	 */

	resID_song = 0;
	statusBits1A = 0;
	statusBits1B = 0;
	busyChannelBits = 0;
	SIDReg23 = 0;
	SIDReg23Stuff = 0;
	SIDReg24 = 0;
	bgSoundResID = 0;
	freeChannelCount = 0;
	usedChannelBits = 0;
	var481A = 0;
	songChannelBits = 0;
	//var5163 = 0;
	SIDReg24_HiNibble = 0;
	chansWithLowerPrioCount = 0;
	minChanPrio = 0;
	minChanPrioIndex = 0;

	_music = NULL;
	songFileOrChanBufData = NULL;
	actSongFileData = NULL;

	initializing = false;
	_soundInQueue = false;
	isVoiceChannel = false;
	isMusicPlaying = false;
	swapVarLoaded = false;
	bgSoundActive = false;
	filterUsed = false;
	pulseWidthSwapped = false;
	swapPrepared = false;
	filterSwapped = false;
	keepSwapVars = false;
	actFilterHasLowerPrio = false;

	ZEROMEM(chanFileData);
	ZEROMEM(chanDataOffset);
	ZEROMEM(songPosPtr);
	ZEROMEM(freqReg);
	ZEROMEM(vec6);
	ZEROMEM(songFileOrChanBufOffset);
	ZEROMEM(freqDelta);
	ZEROMEM(freqDeltaCounter);
	ZEROMEM(swapSongPosPtr);
	ZEROMEM(swapVec5);
	ZEROMEM(swapVec8);
	ZEROMEM(swapVec10);
	ZEROMEM(swapFreqReg);
	ZEROMEM(swapVec11);
	ZEROMEM(vec20);
	ZEROMEM(swapVec20);
	ZEROMEM(resStatus);
	ZEROMEM(attackReg);
	ZEROMEM(sustainReg);
	ZEROMEM(phaseBit);
	ZEROMEM(releasePhase);
	ZEROMEM(_soundQueue);
	ZEROMEM(channelMap);
	ZEROMEM(songPosUpdateCounter);
	ZEROMEM(chanPrio);
	ZEROMEM(waveCtrlReg);
	ZEROMEM(swapAttack);
	ZEROMEM(swapSustain);
	ZEROMEM(swapSongPrio);
	ZEROMEM(swapVec479C);
	ZEROMEM(swapVec19);
	ZEROMEM(swapSongPosUpdateCounter);
	ZEROMEM(swapWaveCtrlReg);
	ZEROMEM(stepTbl);

	/*
	 * initialize data
	 */

	const uint8 chanBuffer_const[3][45] = {
		{
			0x00,0x00,0x00,0x00,0x7f,0x01,0x19,0x00,
			0x00,0x00,0x2d,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0xf0,0x40,0x10,0x04,0x00,0x00,
			0x00,0x04,0x27,0x03,0xff,0xff,0x01,0x00,
			0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0x00,0x00,0x00
		},
		{
			0x00,0x00,0x00,0x00,0x7f,0x01,0x19,0x00,
			0x00,0x00,0x2d,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0xf0,0x20,0x10,0x04,0x00,0x00,
			0x00,0x04,0x27,0x03,0xff,0xff,0x02,0x00,
			0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0x00,0x00,0x00
		},
		{
			0x00,0x00,0x00,0x00,0x7f,0x01,0x19,0x00,
			0x00,0x00,0x2d,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0xf0,0x20,0x10,0x04,0x00,0x00,
			0x00,0x04,0x27,0x03,0xff,0xff,0x02,0x00,
			0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
			0x00,0x00,0x00,0x00,0x00
		}
	};
	memcpy(chanBuffer, chanBuffer_const, sizeof(chanBuffer_const));

	for (int i = 0; i < 7; ++i) {
		_soundQueue[i] = -1;
	};

	_music_timer = 0;

	_mixer = mixer;
	_sampleRate = _mixer->getOutputRate();
	_vm = scumm;

	// sound speed is slightly different on NTSC and PAL machines
	// as the SID clock depends on the frame rate.
	// ScummVM does not distinguish between NTSC and PAL targets
	// so we use the NTSC timing here as the music was composed for
	// NTSC systems (music on PAL systems is slower).
	_videoSystem = NTSC;
	_cpuCyclesLeft = 0;

	initSID();
	resetSID();

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

Player_SID::~Player_SID() {
	_mixer->stopHandle(_soundHandle);
	delete _sid;
}

uint8 *Player_SID::getResource(int resID) {
	switch (resID) {
	case 0:
		return NULL;
	case 3:
	case 4:
	case 5:
		return 	chanBuffer[resID-3];
	default:
		return _vm->getResourceAddress(rtSound, resID);
	}
}

int Player_SID::readBuffer(int16 *buffer, const int numSamples) {
	int samplesLeft = numSamples;

	Common::StackLock lock(_mutex);

	while (samplesLeft > 0) {
		// update SID status after each frame
		if (_cpuCyclesLeft <= 0) {
			update();
			_cpuCyclesLeft = timingProps[_videoSystem].cyclesPerFrame;
		}
		// fetch samples
		int sampleCount = _sid->updateClock(_cpuCyclesLeft, (short *)buffer, samplesLeft);
		samplesLeft -= sampleCount;
		buffer += sampleCount;
	}

	return numSamples;
}

void Player_SID::SID_Write(int reg, uint8 data) {
	_sid->write(reg, data);
}

void Player_SID::initSID() {
	_sid = new Resid::SID();
	_sid->set_sampling_parameters(
		timingProps[_videoSystem].clockFreq,
		_sampleRate);
	_sid->enable_filter(true);

	_sid->reset();
	// Synchronize the waveform generators (must occur after reset)
	_sid->write( 4, 0x08);
	_sid->write(11, 0x08);
	_sid->write(18, 0x08);
	_sid->write( 4, 0x00);
	_sid->write(11, 0x00);
	_sid->write(18, 0x00);
}

void Player_SID::startSound(int nr) {
	byte *data = _vm->getResourceAddress(rtSound, nr);
	assert(data);

	// WORKAROUND:
	// sound[4] contains either a song prio or a music channel usage byte.
	// As music channel usage is always 0x07 for all music files and
	// prio 7 is never used in any sound file use this byte for auto-detection.
	bool isMusic = (data[4] == 0x07);

	Common::StackLock lock(_mutex);

	if (isMusic) {
		initMusic(nr);
	} else {
		stopSound_intern(nr);
		initSound(nr);
	}
}

void Player_SID::stopSound(int nr) {
	if (nr == -1)
		return;

	Common::StackLock lock(_mutex);
	stopSound_intern(nr);
}

void Player_SID::stopAllSounds() {
	Common::StackLock lock(_mutex);
	resetPlayerState();
}

int Player_SID::getSoundStatus(int nr) const {
	int result = 0;

	//Common::StackLock lock(_mutex);

	if (resID_song == nr && isMusicPlaying) {
		result = 1;
	}

	for (int i = 0; (i < 4) && (result == 0); ++i) {
		if (nr == _soundQueue[i] || nr == channelMap[i]) {
			result = 1;
		}
	}

	return result;
}

int Player_SID::getMusicTimer() {
	int result = _music_timer;
	_music_timer = 0;
	return result;
}

} // End of namespace Scumm

#endif
