/* ScummVM - Scumm Interpreter
 * Copyright (C) 2001-2006 The ScummVM project
 *
 * 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.
 *
 * $Header$
 */

#include "common/stdafx.h"
#include "common/scummsys.h"

#ifdef USE_MT32EMU

#include "sound/softsynth/mt32/mt32emu.h"

#include "sound/softsynth/emumidi.h"
#include "sound/mpu401.h"

#include "common/util.h"
#include "common/file.h"
#include "common/config-manager.h"
#include "common/system.h"

#include "graphics/fontman.h"
#include "graphics/surface.h"

class MidiChannel_MT32 : public MidiChannel_MPU401 {
	void effectLevel(byte value) { }
	void chorusLevel(byte value) { }
};

class MidiDriver_MT32 : public MidiDriver_Emulated {
private:
	Audio::SoundHandle _handle;
	MidiChannel_MT32 _midiChannels[16];
	uint16 _channelMask;
	MT32Emu::Synth *_synth;

	int _outputRate;

protected:
	void generateSamples(int16 *buf, int len);

public:
	bool _initialising;

	MidiDriver_MT32(Audio::Mixer *mixer);
	virtual ~MidiDriver_MT32();

	int open();
	void close();
	void send(uint32 b);
	void setPitchBendRange (byte channel, uint range);
	void sysEx(byte *msg, uint16 length);

	uint32 property(int prop, uint32 param);
	MidiChannel *allocateChannel();
	MidiChannel *getPercussionChannel();

	// AudioStream API
	bool isStereo() const { return true; }
	int getRate() const { return _outputRate; }
};

class MT32File: public MT32Emu::File {
	Common::File file;
public:
	bool open(const char *filename, OpenMode mode) {
		Common::File::AccessMode accessMode = mode == OpenMode_read ? Common::File::kFileReadMode : Common::File::kFileWriteMode;
		return file.open(filename, accessMode);
	}
	void close() {
		return file.close();
	}
	size_t read(void *in, size_t size) {
		return file.read(in, size);
	}
	bool readLine(char *in, size_t size) {
		return file.readLine(in, size) != NULL;
	}
	bool readBit8u(MT32Emu::Bit8u *in) {
		byte b = file.readByte();
		if (file.eof())
			return false;
		*in = b;
		return true;
	}
	size_t write(const void *in, size_t size) {
		return file.write(in, size);
	}
	bool writeBit8u(MT32Emu::Bit8u out) {
		file.writeByte(out);
		return !file.ioFailed();
	}
	bool isEOF() {
		return file.eof();
	}
};

static int eatSystemEvents() {
	OSystem::Event event;
	while (g_system->pollEvent(event)) {
		switch (event.type) {
		case OSystem::EVENT_QUIT:
			return 1;
		default:
			break;
		}
	}
	return 0;
}

static void drawProgress(float progress) {
	const Graphics::Font &font(*FontMan.getFontByUsage(Graphics::FontManager::kOSDFont));
	Graphics::Surface surf;
	uint32 borderColor = 0x2;
	uint32 fillColor = 0x4;
	surf.w = g_system->getWidth() / 7 * 5;
	surf.h = font.getFontHeight();
	int x = g_system->getWidth() / 7;
	int y = g_system->getHeight() / 2 - surf.h / 2;
	surf.pitch = surf.w;
	surf.bytesPerPixel = 1;
	surf.pixels = calloc(surf.w, surf.h);
	Common::Rect r(surf.w, surf.h);
	surf.frameRect(r, borderColor);
	r.grow(-1);
	r.right = r.left + (uint16)(r.width() * progress);
	surf.fillRect(r, fillColor);
	g_system->copyRectToScreen((byte *)surf.pixels, surf.pitch, x, y, surf.w, surf.h);
	g_system->updateScreen();
	free(surf.pixels);
}

static void drawMessage(int offset, const Common::String &text) {
	const Graphics::Font &font(*FontMan.getFontByUsage(Graphics::FontManager::kOSDFont));
	Graphics::Surface surf;
	uint32 color = 0x2;
	surf.w = g_system->getWidth();
	surf.h = font.getFontHeight();
	surf.pitch = surf.w;
	surf.bytesPerPixel = 1;
	surf.pixels = calloc(surf.w, surf.h);
	font.drawString(&surf, text, 0, 0, surf.w, color, Graphics::kTextAlignCenter);
	int y = g_system->getHeight() / 2 - font.getFontHeight() / 2 + offset * (font.getFontHeight() + 1);
	g_system->copyRectToScreen((byte *)surf.pixels, surf.pitch, 0, y, surf.w, surf.h);
	g_system->updateScreen();
	free(surf.pixels);
}

static MT32Emu::File *MT32_OpenFile(void *userData, const char *filename, MT32Emu::File::OpenMode mode) {
	MT32File *file = new MT32File();
	if (!file->open(filename, mode)) {
		delete file;
		return NULL;
	}
	return file;
}

static void MT32_PrintDebug(void *userData, const char *fmt, va_list list) {
	char buf[512];
	if (((MidiDriver_MT32 *)userData)->_initialising) {
		vsnprintf(buf, 512, fmt, list);
		buf[70] = 0; // Truncate to a reasonable length
		drawMessage(1, buf);
	}
	//vdebug(0, fmt, list); // FIXME: Use a higher debug level
}

static int MT32_Report(void *userData, MT32Emu::ReportType type, const void *reportData) {
	switch(type) {
	case MT32Emu::ReportType_lcdMessage:
		g_system->displayMessageOnOSD((const char *)reportData);
		break;
	case MT32Emu::ReportType_errorControlROM:
		error("Failed to load MT32_CONTROL.ROM");
		break;
	case MT32Emu::ReportType_errorPCMROM:
		error("Failed to load MT32_PCM.ROM");
		break;
	case MT32Emu::ReportType_progressInit:
		if (((MidiDriver_MT32 *)userData)->_initialising) {
			drawProgress(*((const float *)reportData));
			return eatSystemEvents();
		}
		break;
	case MT32Emu::ReportType_availableSSE:
		debug(1, "MT32emu: SSE is avaliable");
		break;
	case MT32Emu::ReportType_usingSSE:
		debug(1, "MT32emu: using SSE");
		break;
	case MT32Emu::ReportType_available3DNow:
		debug(1, "MT32emu: 3DNow! is avaliable");
		break;
	case MT32Emu::ReportType_using3DNow:
		debug(1, "MT32emu: using 3DNow!");
		break;
	default:
		break;
	}
	return 0;
}

////////////////////////////////////////
//
// MidiDriver_MT32
//
////////////////////////////////////////

MidiDriver_MT32::MidiDriver_MT32(Audio::Mixer *mixer) : MidiDriver_Emulated(mixer) {
	_channelMask = 0xFFFF; // Permit all 16 channels by default
	uint i;
	for (i = 0; i < ARRAYSIZE(_midiChannels); ++i) {
		_midiChannels[i].init(this, i);
	}
	_synth = NULL;
	// A higher baseFreq reduces the length used in generateSamples(),
	// and means that the timer callback will be called more often.
	// That results in more accurate timing.
	_baseFreq = 10000;
	// Unfortunately bugs in the emulator cause inaccurate tuning
	// at rates other than 32KHz, thus we produce data at 32KHz and
	// rely on Mixer to convert.
	_outputRate = 32000; //_mixer->getOutputRate();
	_initialising = false;
}

MidiDriver_MT32::~MidiDriver_MT32() {
	if (_synth != NULL)
		delete _synth;
}

int MidiDriver_MT32::open() {
	MT32Emu::SynthProperties prop;

	if (_isOpen)
		return MERR_ALREADY_OPEN;

	MidiDriver_Emulated::open();

	memset(&prop, 0, sizeof(prop));
	prop.sampleRate = getRate();
	prop.useReverb = true;
	prop.useDefaultReverb = false;
	prop.reverbType = 0;
	prop.reverbTime = 5;
	prop.reverbLevel = 3;
	prop.userData = this;
	prop.printDebug = MT32_PrintDebug;
	prop.report = MT32_Report;
	prop.openFile = MT32_OpenFile;
	_synth = new MT32Emu::Synth();
	_initialising = true;
	const byte dummy_palette[] = {
		0, 0, 0, 0,
		0, 0, 171, 0,
		0, 171, 0, 0,
		0, 171, 171, 0,
		171, 0, 0, 0
	};

	g_system->setPalette(dummy_palette, 0, 5);
	drawMessage(-1, "Initialising MT-32 Emulator");
	if (!_synth->open(prop))
		return MERR_DEVICE_NOT_AVAILABLE;
	_initialising = false;
	g_system->clearScreen();
	g_system->updateScreen();
	_mixer->playInputStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1, 255, 0, false, true);
	return 0;
}

void MidiDriver_MT32::send(uint32 b) {
	_synth->playMsg(b);
}

void MidiDriver_MT32::setPitchBendRange(byte channel, uint range) {
	if (range > 24) {
		printf("setPitchBendRange() called with range > 24: %d", range);
	}
	byte benderRangeSysex[9];
	benderRangeSysex[0] = 0x41; // Roland
	benderRangeSysex[1] = channel;
	benderRangeSysex[2] = 0x16; // MT-32
	benderRangeSysex[3] = 0x12; // Write
	benderRangeSysex[4] = 0x00;
	benderRangeSysex[5] = 0x00;
	benderRangeSysex[6] = 0x04;
	benderRangeSysex[7] = (byte)range;
	benderRangeSysex[8] = MT32Emu::Synth::calcSysexChecksum(&benderRangeSysex[4], 4, 0);
	sysEx(benderRangeSysex, 9);
}

void MidiDriver_MT32::sysEx(byte *msg, uint16 length) {
	if (msg[0] == 0xf0) {
		_synth->playSysex(msg, length);
	} else {
		_synth->playSysexWithoutFraming(msg, length);
	}
}

void MidiDriver_MT32::close() {
	if (!_isOpen)
		return;
	_isOpen = false;

	// Detach the player callback handler
	setTimerCallback(NULL, NULL);
	// Detach the mixer callback handler
	_mixer->stopHandle(_handle);

	_synth->close();
	delete _synth;
	_synth = NULL;
}

void MidiDriver_MT32::generateSamples(int16 *data, int len) {
	_synth->render(data, len);
}

uint32 MidiDriver_MT32::property(int prop, uint32 param) {
	switch (prop) {
	case PROP_CHANNEL_MASK:
		_channelMask = param & 0xFFFF;
		return 1;
	}

	return 0;
}

MidiChannel *MidiDriver_MT32::allocateChannel() {
	MidiChannel_MT32 *chan;
	uint i;

	for (i = 0; i < ARRAYSIZE(_midiChannels); ++i) {
		if (i == 9 || !(_channelMask & (1 << i)))
			continue;
		chan = &_midiChannels[i];
		if (chan->allocate()) {
			return chan;
		}
	}
	return NULL;
}

MidiChannel *MidiDriver_MT32::getPercussionChannel() {
	return &_midiChannels[9];
}

// This code should be used when calling the timer callback from the mixer thread is undesirable.
// Note that it results in less accurate timing.
#if 0
class MidiEvent_MT32 {
public:
	MidiEvent_MT32 *_next;
	uint32 _msg; // 0xFFFFFFFF indicates a sysex message
	byte *_data;
	uint32 _len;

	MidiEvent_MT32(uint32 msg, byte *data, uint32 len) {
		_msg = msg;
		if (len > 0) {
			_data = new byte[len];
			memcpy(_data, data, len);
		}
		_len = len;
		_next = NULL;
	}

	MidiEvent_MT32() {
		if (_len > 0)
			delete _data;
	}
};

class MidiDriver_ThreadedMT32 : public MidiDriver_MT32 {
private:
	OSystem::Mutex _eventMutex;
	MidiEvent_MT32 *_events;
	Timer::TimerProc _timer_proc;

	void pushMidiEvent(MidiEvent_MT32 *event);
	MidiEvent_MT32 *popMidiEvent();

protected:
	void send(uint32 b);
	void sysEx(byte *msg, uint16 length);

public:
	MidiDriver_ThreadedMT32(Audio::Mixer *mixer);

	void onTimer();
	void close();
	void setTimerCallback(void *timer_param, Timer::TimerProc timer_proc);
};


MidiDriver_ThreadedMT32::MidiDriver_ThreadedMT32(Audio::Mixer *mixer) : MidiDriver_MT32(mixer) {
	_events = NULL;
	_timer_proc = NULL;
}

void MidiDriver_ThreadedMT32::close() {
	MidiDriver_MT32::close();
	while ((popMidiEvent() != NULL)) {
		// Just eat any leftover events
	}
}

void MidiDriver_ThreadedMT32::setTimerCallback(void *timer_param, Timer::TimerProc timer_proc) {
	if (!_timer_proc || !timer_proc) {
		if (_timer_proc)
			g_timer->removeTimerProc(_timer_proc);
		_timer_proc = timer_proc;
		if (timer_proc)
			g_timer->installTimerProc(timer_proc, getBaseTempo(), timer_param);
	}
}

void MidiDriver_ThreadedMT32::pushMidiEvent(MidiEvent_MT32 *event) {
	Common::StackLock lock(_eventMutex);
	if (_events == NULL) {
		_events = event;
	} else {
		MidiEvent_MT32 *last = _events;
		while (last->_next != NULL)
			last = last->_next;
		last->_next = event;
	}
}

MidiEvent_MT32 *MidiDriver_ThreadedMT32::popMidiEvent() {
	Common::StackLock lock(_eventMutex);
	MidiEvent_MT32 *event;
	event = _events;
	if (event != NULL)
		_events = event->_next;
	return event;
}

void MidiDriver_ThreadedMT32::send(uint32 b) {
	MidiEvent_MT32 *event = new MidiEvent_MT32(b, NULL, 0);
	pushMidiEvent(event);
}

void MidiDriver_ThreadedMT32::sysEx(byte *msg, uint16 length) {
	MidiEvent_MT32 *event = new MidiEvent_MT32(0xFFFFFFFF, msg, length);
	pushMidiEvent(event);
}

void MidiDriver_ThreadedMT32::onTimer() {
	MidiEvent_MT32 *event;
	while ((event = popMidiEvent()) != NULL) {
		if (event->_msg == 0xFFFFFFFF) {
			MidiDriver_MT32::sysEx(event->_data, event->_len);
		} else {
			MidiDriver_MT32::send(event->_msg);
		}
		delete event;
	}
}
#endif

////////////////////////////////////////
//
// MidiDriver_MT32 factory
//
////////////////////////////////////////

MidiDriver *MidiDriver_MT32_create(Audio::Mixer *mixer) {
	// HACK: It will stay here until engine plugin loader overhaul
	if (ConfMan.hasKey("extrapath"))
		Common::File::addDefaultDirectory(ConfMan.get("extrapath"));
	return new MidiDriver_MT32(mixer);
}

#endif