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

#include "scumm/imuse_digi/dimuse_engine.h"

namespace Scumm {

int IMuseDigital::streamerInit() {
	for (int l = 0; l < DIMUSE_MAX_STREAMS; l++) {
		_streams[l].soundId = 0;
	}
	_lastStreamLoaded = nullptr;
	return 0;
}

IMuseDigiStream *IMuseDigital::streamerAllocateSound(int soundId, int bufId, int32 maxRead) {
	IMuseDigiSndBuffer *bufInfoPtr = _filesHandler->getBufInfo(bufId);
	if (!bufInfoPtr) {
		debug(5, "IMuseDigital::streamerAlloc(): ERROR: couldn't get buffer info");
		return nullptr;
	}

	if ((bufInfoPtr->bufSize / 4) <= maxRead) {
		debug(5, "IMuseDigital::streamerAlloc(): ERROR: maxRead too big for buffer");
		return nullptr;
	}

	for (int l = 0; l < DIMUSE_MAX_STREAMS; l++) {
		if (_streams[l].soundId && _streams[l].bufId == bufId) {
			debug(5, "IMuseDigital::streamerAlloc(): ERROR: stream bufId %d already in use", bufId);
			return nullptr;
		}
	}

	for (int l = 0; l < DIMUSE_MAX_STREAMS; l++) {
		if (!_streams[l].soundId) {
			_streams[l].endOffset = _filesHandler->seek(soundId, 0, SEEK_END, bufId);
			_streams[l].curOffset = 0;
			_streams[l].soundId = soundId;
			_streams[l].bufId = bufId;
			_streams[l].buf = bufInfoPtr->buffer;
			_streams[l].bufFreeSize = bufInfoPtr->bufSize - maxRead - (_isEarlyDiMUSE ? 0 : 4);
			_streams[l].loadSize = bufInfoPtr->loadSize;
			_streams[l].criticalSize = bufInfoPtr->criticalSize;
			_streams[l].maxRead = maxRead;
			_streams[l].loadIndex = 0;
			_streams[l].readIndex = 0;
			_streams[l].paused = 0;
			_streams[l].vocLoopFlag = 0;
			_streams[l].vocLoopTriggerOffset = 0;
			return &_streams[l];
		}
	}
	debug(5, "IMuseDigital::streamerAlloc(): ERROR: no spare streams");
	return nullptr;
}

int IMuseDigital::streamerClearSoundInStream(IMuseDigiStream *streamPtr) {
	streamPtr->soundId = 0;
	if (_lastStreamLoaded == streamPtr) {
		_lastStreamLoaded = 0;
	}
	return 0;
}

int IMuseDigital::streamerProcessStreams() {
	if (!_isEarlyDiMUSE)
		dispatchPredictFirstStream();

	IMuseDigiStream *stream1 = nullptr;
	IMuseDigiStream *stream2 = nullptr;

	for (int l = 0; l < DIMUSE_MAX_STREAMS; l++) {
		if ((_streams[l].soundId) && (!_streams[l].paused)) {
			if (stream2) {
				if (stream1) {
					debug(5, "IMuseDigital::streamerProcessStreams(): WARNING: three streams in use");
				} else {
					stream1 = &_streams[l];
				}
			} else {
				stream2 = &_streams[l];
			}
		}
	}

	if (!stream1) {
		if (stream2)
			streamerFetchData(stream2);
		return 0;
	}

	if (!stream2) {
		if (stream1)
			streamerFetchData(stream1);
		return 0;
	}

	// Check if we've reached or surpassed criticalSize
	bool critical1 = (streamerGetFreeBufferAmount(stream1) >= stream1->criticalSize);
	bool critical2 = (streamerGetFreeBufferAmount(stream2) >= stream2->criticalSize);

	if (!critical1) {
		if (!critical2) {
			if (stream1 == _lastStreamLoaded) {
				streamerFetchData(stream1);
				streamerFetchData(stream2);
				return 0;
			} else {
				streamerFetchData(stream2);
				streamerFetchData(stream1);
				return 0;
			}
		} else {
			streamerFetchData(stream1);
			return 0;
		}
	}

	if (!critical2) {
		streamerFetchData(stream2);
		return 0;
	} else {
		if (stream1 == _lastStreamLoaded) {
			streamerFetchData(stream1);
		} else {
			streamerFetchData(stream2);
		}
		return 0;
	}
}

uint8 *IMuseDigital::streamerGetStreamBuffer(IMuseDigiStream *streamPtr, int size) {
	if (size > streamerGetFreeBufferAmount(streamPtr) || streamPtr->maxRead < size)
		return 0;

	if (size > streamPtr->bufFreeSize - streamPtr->readIndex) {
		memcpy(&streamPtr->buf[streamPtr->bufFreeSize],
			streamPtr->buf,
			size - streamPtr->bufFreeSize + streamPtr->readIndex + (_isEarlyDiMUSE ? 0 : 4));
	}

	int32 readIndex_tmp = streamPtr->readIndex;
	uint8 *ptr = &streamPtr->buf[readIndex_tmp];
	streamPtr->readIndex = readIndex_tmp + size;
	if (streamPtr->bufFreeSize <= readIndex_tmp + size)
		streamPtr->readIndex = readIndex_tmp + size - streamPtr->bufFreeSize;

	return ptr;
}

uint8 *IMuseDigital::streamerGetStreamBufferAtOffset(IMuseDigiStream *streamPtr, int32 offset, int size) {
	if (offset + size > streamerGetFreeBufferAmount(streamPtr) || streamPtr->maxRead < size)
		return 0;

	int32 offsetReadIndex = offset + streamPtr->readIndex;
	if (offsetReadIndex >= streamPtr->bufFreeSize) {
		offsetReadIndex -= streamPtr->bufFreeSize;
	}

	if (size > streamPtr->bufFreeSize - offsetReadIndex) {
		memcpy(&streamPtr->buf[streamPtr->bufFreeSize], streamPtr->buf, size + offsetReadIndex - streamPtr->bufFreeSize);
	}

	return &streamPtr->buf[offsetReadIndex];
}

int IMuseDigital::streamerSetReadIndex(IMuseDigiStream *streamPtr, int offset) {
	_streamerBailFlag = 1;
	if (offset > streamerGetFreeBufferAmount(streamPtr))
		return -1;

	streamPtr->readIndex += offset;
	if (streamPtr->readIndex >= streamPtr->bufFreeSize) {
		streamPtr->readIndex -= streamPtr->bufFreeSize;
	}
	return 0;
}

int IMuseDigital::streamerSetLoadIndex(IMuseDigiStream *streamPtr, int offset) {
	_streamerBailFlag = 1;
	if (offset > streamerGetFreeBufferAmount(streamPtr))
		return -1;

	streamPtr->loadIndex = streamPtr->readIndex + offset;
	if (streamPtr->loadIndex >= streamPtr->bufFreeSize) {
		streamPtr->loadIndex -= streamPtr->bufFreeSize;
	}
	return 0;
}

int IMuseDigital::streamerGetFreeBufferAmount(IMuseDigiStream *streamPtr) {
	int32 freeBufferSize = streamPtr->loadIndex - streamPtr->readIndex;
	if (freeBufferSize < 0)
		freeBufferSize += streamPtr->bufFreeSize;
	return freeBufferSize;
}

int IMuseDigital::streamerSetSoundToStreamFromOffset(IMuseDigiStream *streamPtr, int soundId, int32 offset) {
	_streamerBailFlag = 1;
	streamPtr->soundId = soundId;
	streamPtr->curOffset = offset;
	streamPtr->endOffset = _isEarlyDiMUSE ? _filesHandler->seek(streamPtr->soundId, 0, SEEK_END, streamPtr->bufId) : 0;
	streamPtr->paused = 0;
	if (_lastStreamLoaded == streamPtr) {
		_lastStreamLoaded = nullptr;
	}
	return 0;
}

void IMuseDigital::streamerQueryStream(IMuseDigiStream *streamPtr, int32 &bufSize, int32 &criticalSize, int32 &freeSpace, int &paused) {
	if (!isFTSoundEngine())
		dispatchPredictFirstStream();

	bufSize = streamPtr->bufFreeSize;
	criticalSize = (isFTSoundEngine() && streamPtr->paused) ? 0 : streamPtr->criticalSize;
	freeSpace = streamerGetFreeBufferAmount(streamPtr);
	paused = streamPtr->paused;
}

int IMuseDigital::streamerFeedStream(IMuseDigiStream *streamPtr, uint8 *srcBuf, int32 sizeToFeed, int paused) {
	int32 size = streamPtr->readIndex - streamPtr->loadIndex;
	if (size <= 0)
		size += streamPtr->bufFreeSize;
	if (sizeToFeed > (size - 4)) {
		// Don't worry, judging from the intro of The Dig, this is totally expected
		// to happen, and when it does, the output matches the EXE behavior
		debug(5, "IMuseDigital::streamerFeedStream(): WARNING: buffer overflow");
		_streamerBailFlag = 1;

		int32 newTentativeSize = sizeToFeed - (size - 4) - (sizeToFeed - (size - 4)) % 12 + 12;
		size = streamPtr->loadIndex - streamPtr->readIndex;
		if (size < 0)
			size += streamPtr->bufFreeSize;
		if (size >= newTentativeSize) {
			streamPtr->readIndex = newTentativeSize + streamPtr->readIndex;
			if (streamPtr->bufFreeSize <= streamPtr->readIndex)
				streamPtr->readIndex -= streamPtr->bufFreeSize;
		}
	}

	if (sizeToFeed > 0) {
		do {
			size = streamPtr->bufFreeSize - streamPtr->loadIndex;
			if (size >= sizeToFeed) {
				size = sizeToFeed;
			}
			sizeToFeed -= size;
			memcpy(&streamPtr->buf[streamPtr->loadIndex], srcBuf, size);
			srcBuf += size;

			int val = size + streamPtr->loadIndex;
			streamPtr->curOffset += size;
			streamPtr->loadIndex += size;
			if (val >= streamPtr->bufFreeSize) {
				streamPtr->loadIndex = val - streamPtr->bufFreeSize;
			}
		} while (sizeToFeed > 0);
	}

	streamPtr->paused = paused;
	return 0;
}

int IMuseDigital::streamerFetchData(IMuseDigiStream *streamPtr) {
	if (!_isEarlyDiMUSE && streamPtr->endOffset == 0) {
		streamPtr->endOffset = _filesHandler->seek(streamPtr->soundId, 0, SEEK_END, streamPtr->bufId);
	}

	int32 size = streamPtr->readIndex - streamPtr->loadIndex;
	if (size <= 0)
		size += streamPtr->bufFreeSize;

	int32 loadSize = streamPtr->loadSize;
	int32 remainingSize = streamPtr->endOffset - streamPtr->curOffset;

	if (loadSize >= size - (_isEarlyDiMUSE ? 1 : 4)) {
		loadSize = size - (_isEarlyDiMUSE ? 1 : 4);
	}

	if (loadSize >= remainingSize) {
		loadSize = remainingSize;
	}

	if (remainingSize <= 0) {
		streamPtr->paused = 1;

		if (!_isEarlyDiMUSE) {
			// Pad the buffer
			streamPtr->buf[streamPtr->loadIndex] = 127;
			streamPtr->loadIndex++;
			streamPtr->buf[streamPtr->loadIndex] = 127;
			streamPtr->loadIndex++;
			streamPtr->buf[streamPtr->loadIndex] = 127;
			streamPtr->loadIndex++;
			streamPtr->buf[streamPtr->loadIndex] = 127;
			streamPtr->loadIndex++;
		}
	}

	int32 actualAmount;
	int32 requestedAmount;

	while (1) {
		if (!_isEarlyDiMUSE && loadSize <= 0)
			return 0;

		requestedAmount = streamPtr->bufFreeSize - streamPtr->loadIndex;
		if (requestedAmount >= loadSize) {
			requestedAmount = loadSize;
		}

		if (_filesHandler->seek(streamPtr->soundId, streamPtr->curOffset, SEEK_SET, streamPtr->bufId) != streamPtr->curOffset) {
			debug(5, "IMuseDigital::streamerFetchData(): ERROR: invalid seek in streamer (%d), pausing stream...", streamPtr->curOffset);
			streamPtr->paused = 1;
			return 0;
		}

		_streamerBailFlag = 0;

		_mutex->lock();
		actualAmount = _filesHandler->read(streamPtr->soundId, &streamPtr->buf[streamPtr->loadIndex], requestedAmount, streamPtr->bufId);
		_mutex->unlock();

		// FT has no bailFlag
		if (!_isEarlyDiMUSE && _streamerBailFlag)
			return 0;

		loadSize -= actualAmount;
		streamPtr->curOffset += actualAmount;
		_lastStreamLoaded = streamPtr;

		int32 newLoadIndex = actualAmount + streamPtr->loadIndex;
		streamPtr->loadIndex = newLoadIndex;
		if (newLoadIndex >= streamPtr->bufFreeSize) {
			streamPtr->loadIndex = newLoadIndex - streamPtr->bufFreeSize;
		}

		if (_isEarlyDiMUSE && streamPtr->vocLoopFlag) {
			if (streamPtr->vocLoopTriggerOffset <= streamPtr->curOffset) {
				dispatchVOCLoopCallback(streamPtr->soundId);
				streamPtr->vocLoopFlag = 0;
			}
		}

		if (actualAmount != requestedAmount)
			break;

		// FT checks for negative or zero loadSize here
		if (_isEarlyDiMUSE && loadSize <= 0)
			return 0;
	}

	debug(5, "IMuseDigital::streamerFetchData(): ERROR: unable to load the correct amount of data (req=%d, act=%d)", requestedAmount, actualAmount);
	_lastStreamLoaded = nullptr;
	return 0;
}

void IMuseDigital::streamerSetLoopFlag(IMuseDigiStream *streamPtr, int offset) {
	streamPtr->vocLoopFlag = 1;
	streamPtr->vocLoopTriggerOffset = offset;
}

void IMuseDigital::streamerRemoveLoopFlag(IMuseDigiStream *streamPtr) {
	streamPtr->vocLoopFlag = 0;
}

} // End of namespace Scumm
