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

// Disable symbol overrides for FILE and fseek as those are used in the
// mikmod headers.
#define FORBIDDEN_SYMBOL_EXCEPTION_FILE

// On Windows, unlink and setjmp/longjmp may also be triggered.
#if defined(WIN32)
#define FORBIDDEN_SYMBOL_EXCEPTION_chdir
#define FORBIDDEN_SYMBOL_EXCEPTION_getcwd
#define FORBIDDEN_SYMBOL_EXCEPTION_mkdir
#define FORBIDDEN_SYMBOL_EXCEPTION_unlink
#define FORBIDDEN_SYMBOL_EXCEPTION_setjmp
#define FORBIDDEN_SYMBOL_EXCEPTION_longjmp
#endif

#include "common/ptr.h"
#include "common/stream.h"
#include "common/textconsole.h"
#include "common/util.h"

#include "audio/audiostream.h"
#include "audio/decoders/raw.h"

#ifdef USE_OPENMPT

#include <libopenmpt/libopenmpt.h>

#elif defined(USE_MIKMOD)

#include <mikmod.h>

#endif

#ifdef USE_OPENMPT

static size_t memoryReaderRead(void *stream, void *dst, size_t bytes) {
	Common::SeekableReadStream *reader = (Common::SeekableReadStream *)stream;

	if (!reader) {
		return 0;
	}

	uint32 receivedBytes = reader->read(dst, bytes);

	if (receivedBytes < bytes) {
		return 0;
	}

	return receivedBytes;
}

static int memoryReaderSeek(void *stream, int64_t offset, int whence) {
	Common::SeekableReadStream *reader = (Common::SeekableReadStream *)stream;

	if (!reader) {
		return -1;
	}

	bool ret = reader->seek(offset, whence);

	if (ret)
		return 0;

	return -1;
}

static int64_t memoryReaderTell(void *stream) {
	Common::SeekableReadStream *reader = (Common::SeekableReadStream *)stream;

	if (reader) {
		return reader->pos();
	}

	return -1;
}

#elif defined(USE_MIKMOD)

typedef struct MikMemoryReader {
	MREADER core;
	Common::SeekableReadStream *stream;
} MikMemoryReader;

static BOOL memoryReaderEof(MREADER *reader);
static BOOL memoryReaderRead(MREADER *reader, void *ptr, size_t size);
static int  memoryReaderGet(MREADER *reader);
static int  memoryReaderSeek(MREADER *reader, long offset, int whence);
static long memoryReaderTell(MREADER *reader);

MREADER *createMikMemoryReader(Common::SeekableReadStream *stream) {
	MikMemoryReader *reader = (MikMemoryReader *)calloc(1, sizeof(MikMemoryReader));

	if (reader) {
		reader->core.Eof = &memoryReaderEof;
		reader->core.Read = &memoryReaderRead;
		reader->core.Get = &memoryReaderGet;
		reader->core.Seek = &memoryReaderSeek;
		reader->core.Tell = &memoryReaderTell;
		reader->stream = stream;
	}

	return (MREADER *)reader;
}

static BOOL memoryReaderEof(MREADER *reader) {
	MikMemoryReader *mr = (MikMemoryReader *)reader;

	if (!mr)
		return 1;

	if (mr->stream && mr->stream->eos() == true)
		return 1;

	return 0;
}

static BOOL memoryReaderRead(MREADER *reader, void *ptr, size_t size) {
	MikMemoryReader *mr;
	mr = (MikMemoryReader *)reader;

	if (!mr || !mr->stream)
		return 0;

	uint32 receivedBytes = mr->stream->read(ptr, size);

	if (receivedBytes < size)
		return 0; // not enough remaining bytes (or error)

	return 1;
}

static int memoryReaderGet(MREADER *reader) {
	MikMemoryReader *mr;
	mr = (MikMemoryReader *)reader;

	if (!mr->stream)
		return -1;

	return mr->stream->readByte();
}

static int memoryReaderSeek(MREADER *reader, long offset, int whence) {
	MikMemoryReader *mr;
	mr = (MikMemoryReader *)reader;

	if (!reader || !mr->stream)
		return -1;

	return mr->stream->seek(offset, whence);
}

static long memoryReaderTell(MREADER *reader) {
	if (reader)
		return ((MikMemoryReader *)reader)->stream->pos();

	return 0;
}
// End memory wrappper

#endif // USE_MIKMOD

#ifdef USE_OPENMPT

namespace Audio {
class UniversalTrackerMod : public RewindableAudioStream {
public:
	UniversalTrackerMod(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, int rate);
	~UniversalTrackerMod();

	// ImpulseTrackerMod functions
	bool isLoaded() {
		return _mpt_load_successful;
	}

	int readBuffer(int16 *buffer, const int numSamples) override {
		openmpt_module_read_interleaved_stereo(_mod, getRate(), numSamples / 2, buffer);

		return numSamples;
	}

	bool isStereo() const override {
		return true;
	}

	int getRate() const override {
		return _sampleRate;
	}

	bool endOfData() const override {
		return _stream->eos();
	}

	bool endOfStream() const override {
		return endOfData();
	}

	// RewindableAudioStream API
	bool rewind() override {
		openmpt_module_set_position_seconds(_mod, 0.0);
		return true;
	}

private:
	DisposeAfterUse::Flag _dispose;
	bool _mpt_load_successful = false;
	Common::SeekableReadStream *_stream;
	openmpt_module *_mod = nullptr;
	int _sampleRate;
};

UniversalTrackerMod::UniversalTrackerMod(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, int rate) {
	if (!stream) {
		warning("UniversalTrackerMod::UniversalTrackerMod(): Input file/stream is invalid.");
		return;
	}

	int mod_err;
	const char *mod_err_str = NULL;

	_stream = stream;
	_dispose = disposeAfterUse;
	_sampleRate = rate;

	openmpt_stream_callbacks stream_callbacks;
	stream_callbacks.read = &memoryReaderRead;
	stream_callbacks.seek = &memoryReaderSeek;
	stream_callbacks.tell = &memoryReaderTell;

	_mod = openmpt_module_create2(stream_callbacks, _stream, NULL, NULL, NULL, NULL, &mod_err, &mod_err_str, NULL);

	if (!_mod) {
		mod_err_str = openmpt_error_string(mod_err);
		warning("UniversalTrackerMod::UniversalTrackerMod(): Parsing mod error: %s ", mod_err_str);
		openmpt_free_string(mod_err_str);
		return;
	}

	_mpt_load_successful = true;
}

UniversalTrackerMod::~UniversalTrackerMod() {
	if (_mod)
		openmpt_module_destroy(_mod);

	if (_dispose == DisposeAfterUse::Flag::YES)
		delete _stream;
}

} // End of namespace Audio

#endif // #ifdef USE_OPENMPT

#ifdef USE_MIKMOD

namespace Audio {
class UniversalTrackerMod : public RewindableAudioStream {
public:
	UniversalTrackerMod(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, int rate);
	~UniversalTrackerMod();

	// ImpulseTrackerMod functions
	bool isLoaded() {
		return _mikmod_load_successful;
	}

	// AudioStream API
	int readBuffer(int16 *buffer, const int numSamples) override {
		// Multiplied by 2 as VC_WriteBytes function expects 8 byte integer arrays, whereas buffer needs 16 ones.
		VC_WriteBytes((SBYTE *)buffer, numSamples * 2);

		return numSamples;
	}

	bool isStereo() const override {
		return true;
	}
	int getRate() const override {
		return _sampleRate;
	}
	bool endOfData() const override {
		return !Player_Active();
	}
	bool endOfStream() const override {
		return endOfData();
	}

	// RewindableAudioStream API
	bool rewind() override {
		Player_SetPosition(0);
		return true;
	}

private:
	DisposeAfterUse::Flag _dispose;
	bool _mikmod_load_successful = false;
	Common::SeekableReadStream *_stream;
	MREADER *_reader = nullptr;
	MODULE *_mod = nullptr;
	int _sampleRate;
};

UniversalTrackerMod::UniversalTrackerMod(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, int rate) {
	if (!stream) {
		warning("UniversalTrackerMod::UniversalTrackerMod(): Input file/stream is invalid.");
		return;
	}

	_sampleRate = rate;

	MikMod_InitThreads();
	MikMod_RegisterDriver(&drv_nos);

	// Set flags
	md_mode |= DMODE_SOFT_MUSIC | DMODE_NOISEREDUCTION;
	md_mixfreq = getRate();

	if (MikMod_Init("")) {
		warning("UniversalTrackerMod::UniversalTrackerMod(): Could not initialize sound, reason: %s",
		        MikMod_strerror(MikMod_errno));
		return;
	}

	// Loading only impulse tracker loader!
	MikMod_RegisterLoader(&load_it);

	_stream = stream;
	_dispose = disposeAfterUse;

	// Load mod using custom loader class!
	_reader = createMikMemoryReader(_stream);
	_mod = Player_LoadGeneric(_reader, 64, 0);
	if (!_mod) {
		warning("UniversalTrackerMod::UniversalTrackerMod(): Parsing mod error: %s", MikMod_strerror(MikMod_errno));
		return;
	}

	// Start mikmod playing, ie fill VC_Driver buffer with data
	Player_Start(_mod);
	_mikmod_load_successful = true;
}

UniversalTrackerMod::~UniversalTrackerMod() {
	Player_Stop();

	if (_mod)
		Player_Free(_mod);

	if (_reader)
		free(_reader);

	if (_dispose == DisposeAfterUse::Flag::YES)
		delete _stream;

	MikMod_Exit();
}


} // End of namespace Audio

#endif // #ifdef USE_MIKMOD

namespace Audio {

RewindableAudioStream *makeUniversalTrackerStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, int rate) {

#if !defined(USE_OPENMPT) && !defined(USE_MIKMOD)
	warning("Modplayer Support not compiled in");
	return nullptr;
#else

	UniversalTrackerMod *impulseTrackerMod = new UniversalTrackerMod(stream, disposeAfterUse, rate);

	if (!impulseTrackerMod->isLoaded()) {
		delete impulseTrackerMod;
		return nullptr;
	}

	return impulseTrackerMod;

#endif
}

}
