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

/*
 * Output to TiMidity++ MIDI server support
 *                            by Dmitry Marakasov <amdmi3@amdmi3.ru>
 * based on:
 * - Raw output support (seq.cpp) by Michael Pearce
 * - Pseudo /dev/sequencer of TiMidity (timidity-io.c)
 *                                 by Masanao Izumo <mo@goice.co.jp>
 * - sys/soundcard.h by Hannu Savolainen (got from my FreeBSD
 *   distribution, for which it was modified by Luigi Rizzo)
 *
 */

// Disable symbol overrides so that we can use system headers.
#define FORBIDDEN_SYMBOL_ALLOW_ALL

#include "common/scummsys.h"

#if defined(USE_TIMIDITY)

#include "common/endian.h"
#include "common/error.h"
#include "common/str.h"
#include "common/textconsole.h"
#include "audio/musicplugin.h"
#include "audio/mpu401.h"

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netdb.h>  /* for getaddrinfo */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include <stdlib.h>
#include <errno.h>

// BeOS BONE uses snooze (x/1000) in place of usleep(x)
#ifdef __BEOS__
#define usleep(v) snooze(v/1000)
#endif

#define SEQ_MIDIPUTC 5

#define TIMIDITY_LOW_DELAY

#ifdef TIMIDITY_LOW_DELAY
#define BUF_LOW_SYNC	0.1
#define BUF_HIGH_SYNC	0.15
#else
#define BUF_LOW_SYNC	0.4
#define BUF_HIGH_SYNC	0.8
#endif

/* default host & port */
#define DEFAULT_TIMIDITY_HOST "127.0.0.1"
#define DEFAULT_TIMIDITY_PORT "7777"

class MidiDriver_TIMIDITY : public MidiDriver_MPU401 {
public:
	MidiDriver_TIMIDITY();

	int open() override;
	bool isOpen() const override { return _isOpen; }
	void close() override;
	void send(uint32 b) override;
	void sysEx(const byte *msg, uint16 length) override;

private:
	/* creates a tcp connection to TiMidity server, returns filedesc (like open()) */
	int connect_to_server(const char* hostname, const char* tcp_port);

	/* send command to the server; printf-like; returns reply string */
	char *timidity_ctl_command(MSVC_PRINTF const char *fmt, ...) GCC_PRINTF(2, 3);

	/* timidity data socket-related stuff */
	void timidity_meta_seq(int p1, int p2, int p3);
	int timidity_sync(int centsec);
	int timidity_eot();

	/* write() analogue for any midi data */
	void timidity_write_data(const void *buf, size_t nbytes);

	/* get single line of server reply on control connection */
	int fdgets(char *buff, size_t buff_size);

	/* teardown connection to server */
	void teardown();

	/* close (if needed) and nullify both control and data filedescs */
	void close_all();

private:
	bool _isOpen;
	int _device_num;

	int _control_fd;
	int _data_fd;

	/* buffer for partial data read from _control_fd - from timidity-io.c, see fdgets() */
	char _controlbuffer[BUFSIZ];
	int _controlbuffer_count;	/* beginning of read pointer */
	int _controlbuffer_size;	/* end of read pointer */
};

MidiDriver_TIMIDITY::MidiDriver_TIMIDITY() {
	_isOpen = false;
	_device_num = 0;

	/* init fd's */
	_control_fd = _data_fd = -1;

	/* init buffer for control connection */
	_controlbuffer_count = _controlbuffer_size = 0;
}

int MidiDriver_TIMIDITY::open() {
	char *res;
	char timidity_host[NI_MAXHOST];
	char timidity_port[6], data_port[6];
	int num;

	/* count ourselves open */
	if (_isOpen)
		return MERR_ALREADY_OPEN;
	_isOpen = true;

	/* get server hostname; if not specified in env, use default */
	if ((res = getenv("TIMIDITY_HOST")) == NULL)
		Common::strlcpy(timidity_host, DEFAULT_TIMIDITY_HOST, sizeof(timidity_host));
	else
		Common::strlcpy(timidity_host, res, sizeof(timidity_host));

	/* extract control port */
	if ((res = strrchr(timidity_host, ':')) != NULL) {
		*res++ = '\0';
		Common::strlcpy(timidity_port, res, sizeof(timidity_port));
	} else {
		Common::strlcpy(timidity_port, DEFAULT_TIMIDITY_PORT, sizeof(timidity_port));
	}

	/*
	 * create control connection to the server
	 */
	if ((_control_fd = connect_to_server(timidity_host, timidity_port)) < 0) {
		warning("TiMidity: can't open control connection (host=%s, port=%s)", timidity_host, timidity_port);
		return -1;
	}

	/* should read greeting issued by server upon connect:
	 * "220 TiMidity++ v2.13.2 ready)" */
	res = timidity_ctl_command(NULL);
	if (atoi(res) != 220) {
		warning("TiMidity: bad response from server (host=%s, port=%s): %s", timidity_host, timidity_port, res);
		close_all();
		return -1;
	}

	/*
	 * setup buf and prepare data connection
	 */
	/* should read: "200 OK" */
	res = timidity_ctl_command("SETBUF %f %f", BUF_LOW_SYNC, BUF_HIGH_SYNC);
	if (atoi(res) != 200)
		warning("TiMidity: bad reply for SETBUF command: %s", res);

	/* should read something like "200 63017 is ready acceptable",
	 * where 63017 is port for data connection */
#ifdef SCUMM_LITTLE_ENDIAN
	res = timidity_ctl_command("OPEN lsb");
#else
	res = timidity_ctl_command("OPEN msb");
#endif

	if (atoi(res) != 200) {
		warning("TiMidity: bad reply for OPEN command: %s", res);
		close_all();
		return -1;
	}

	/*
	 * open data connection
	 */
	num = atoi(res + 4);
	if (num > 65535) {
		warning("TiMidity: Invalid port %d given.\n", num);
		close_all();
		return -1;
	}
	snprintf(data_port, sizeof(data_port), "%d", num);
	if ((_data_fd = connect_to_server(timidity_host, data_port)) < 0) {
		warning("TiMidity: can't open data connection (host=%s, port=%s)", timidity_host, data_port);
		close_all();
		return -1;
	}

	/* should read message issued after connecting to data port:
	 * "200 Ready data connection" */
	res = timidity_ctl_command(NULL);
	if (atoi(res) != 200) {
		warning("Can't connect timidity: %s\t(host=%s, port=%s)", res, timidity_host, data_port);
		close_all();
		return -1;
	}

	/*
	 * From seq.cpp
	 */
	if (getenv("SCUMMVM_MIDIPORT"))
		_device_num = atoi(getenv("SCUMMVM_MIDIPORT"));

	return 0;
}

void MidiDriver_TIMIDITY::close() {
	teardown();

	MidiDriver_MPU401::close();
	_isOpen = false;
}

void MidiDriver_TIMIDITY::close_all() {
	if (_control_fd >= 0)
		::close(_control_fd);

	if (_data_fd >= 0)
		::close(_data_fd);

	_control_fd = _data_fd = -1;
}

void MidiDriver_TIMIDITY::teardown() {
	char *res;

	/* teardown connection to server (see timidity-io.c) if it
	 * is initialized */
	if (_data_fd >= 0 && _control_fd >= 0) {
		timidity_eot();
		timidity_sync(0);

		/* scroll through all "302 Data connection is (already) closed"
		 * messages till we reach something like "200 Bye" */
		do {
			res = timidity_ctl_command("QUIT");
		} while (*res && atoi(res) && atoi(res) != 302);
	}

	/* now close and nullify both filedescs */
	close_all();
}

int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, const char* tcp_port) {
	int fd;
	struct addrinfo  hints;
	struct addrinfo *result, *rp;

	/* get all address(es) matching host and port */
	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_family   = AF_UNSPEC; /* Allow IPv4 or IPv6 */
	if (getaddrinfo(hostname, tcp_port, &hints, &result) != 0) {
		warning("TiMidity: getaddrinfo: %s\n", strerror(errno));
		return -1;
	}

	/* Try all address structures we have got previously */
	for (rp = result; rp != NULL; rp = rp->ai_next) {
		if ((fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
			continue;
		if (connect(fd, rp->ai_addr, rp->ai_addrlen) != -1)
			break;
		::close(fd);
	}

	freeaddrinfo(result);

	if (rp == NULL) {
		warning("TiMidity: Could not connect\n");
		return -1;
	}

	return fd;
}

char *MidiDriver_TIMIDITY::timidity_ctl_command(const char *fmt, ...) {
	/* XXX: I don't like this static buffer!!! */
	static char buff[BUFSIZ];
	va_list ap;

	if (fmt != NULL) {
		/* if argumends are present, write them to control connection */
		va_start(ap, fmt);
		int len = vsnprintf(buff, BUFSIZ-1, fmt, ap); /* leave one byte for \n */
		va_end(ap);

		/* add newline if needed */
		if (len > 0 && buff[len - 1] != '\n')
			buff[len++] = '\n';

		/* write command to control socket */
		if (write(_control_fd, buff, len) == -1) {
			warning("TiMidity: CONTROL WRITE FAILED (%s)", strerror(errno));
			// TODO: Disable output?
			//close_all();
		}
	}

	while (1) {
		/* read reply */
		if (fdgets(buff, sizeof(buff)) <= 0) {
			strcpy(buff, "Read error\n");
			break;
		}

		/* report errors from server */
		int status = atoi(buff);
		if (400 <= status && status <= 499) { /* Error of data stream */
			warning("TiMidity: error from server: %s", buff);
			continue;
		}
		break;
	}

	return buff;
}

void MidiDriver_TIMIDITY::timidity_meta_seq(int p1, int p2, int p3) {
	/* see _CHN_COMMON from soundcard.h; this is simplified
	 * to just send seq to the server without any buffers,
	 * delays and extra functions/macros */
	unsigned char seqbuf[8];

	seqbuf[0] = 0x92;
	seqbuf[1] = 0;
	seqbuf[2] = 0xff;
	seqbuf[3] = 0x7f;
	seqbuf[4] = p1;
	seqbuf[5] = p2;
	WRITE_UINT16(&seqbuf[6], p3);

	timidity_write_data(seqbuf, sizeof(seqbuf));
}

int MidiDriver_TIMIDITY::timidity_sync(int centsec) {
	char *res;
	int status;
	unsigned long sleep_usec;

	timidity_meta_seq(0x02, 0x00, centsec); /* Wait playout */

	/* Wait "301 Sync OK" */
	do {
		res = timidity_ctl_command(NULL);
		status = atoi(res);

		if (status != 301)
			warning("TiMidity: error: SYNC: %s", res);

	} while (status && status != 301);

	if (status != 301)
		return -1; /* error */

	sleep_usec = (unsigned long)(atof(res + 4) * 1000000);

	if (sleep_usec > 0)
		usleep(sleep_usec);

	return 0;
}

int MidiDriver_TIMIDITY::timidity_eot(void) {
	timidity_meta_seq(0x00, 0x00, 0); /* End of playing */
	return timidity_sync(0);
}

void MidiDriver_TIMIDITY::timidity_write_data(const void *buf, size_t nbytes) {
	/* nowhere to write... */
	if (_data_fd < 0)
		return;

	/* write, and disable everything if write failed */
	/* TODO: add reconnect? */
	if (write(_data_fd, buf, nbytes) == -1) {
		warning("TiMidity: DATA WRITE FAILED (%s), DISABLING MUSIC OUTPUT", strerror(errno));
		close_all();
	}
}

int MidiDriver_TIMIDITY::fdgets(char *buff, size_t buff_size) {
	int n, count, size;
	char *buff_endp = buff + buff_size - 1, *pbuff, *beg;

	count = _controlbuffer_count;
	size = _controlbuffer_size;
	pbuff = _controlbuffer;
	beg = buff;
	do {
		if (count == size) {
			if ((n = read(_control_fd, pbuff, BUFSIZ)) <= 0) {
				*buff = '\0';
				if (n == 0) {
					_controlbuffer_count = _controlbuffer_size = 0;
					return buff - beg;
				}
				return -1; /* < 0 error */
			}
			count = _controlbuffer_count = 0;
			size = _controlbuffer_size = n;
		}
		*buff++ = pbuff[count++];
	} while (*(buff - 1) != '\n' && buff != buff_endp);

	*buff = '\0';
	_controlbuffer_count = count;

	return buff - beg;
}

void MidiDriver_TIMIDITY::send(uint32 b) {
	unsigned char buf[256];
	int position = 0;

	midiDriverCommonSend(b);

	switch (b & 0xF0) {
	case 0x80:
	case 0x90:
	case 0xA0:
	case 0xB0:
	case 0xE0:
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char)b;
		buf[position++] = _device_num;
		buf[position++] = 0;
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char)((b >> 8) & 0x7F);
		buf[position++] = _device_num;
		buf[position++] = 0;
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char)((b >> 16) & 0x7F);
		buf[position++] = _device_num;
		buf[position++] = 0;
		break;
	case 0xC0:
	case 0xD0:
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char)b;
		buf[position++] = _device_num;
		buf[position++] = 0;
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char)((b >> 8) & 0x7F);
		buf[position++] = _device_num;
		buf[position++] = 0;
		break;
	default:
		warning("MidiDriver_TIMIDITY::send: unknown : %08x", (int)b);
		break;
	}

	timidity_write_data(buf, position);
}

void MidiDriver_TIMIDITY::sysEx(const byte *msg, uint16 length) {
	fprintf(stderr, "Timidity::sysEx\n");
	unsigned char buf[266*4];
	int position = 0;
	const byte *chr = msg;

	assert(length + 2 <= 266);

	midiDriverCommonSysEx(msg, length);

	buf[position++] = SEQ_MIDIPUTC;
	buf[position++] = 0xF0;
	buf[position++] = _device_num;
	buf[position++] = 0;
	for (; length; --length, ++chr) {
		buf[position++] = SEQ_MIDIPUTC;
		buf[position++] = (unsigned char) *chr & 0x7F;
		buf[position++] = _device_num;
		buf[position++] = 0;
	}
	buf[position++] = SEQ_MIDIPUTC;
	buf[position++] = 0xF7;
	buf[position++] = _device_num;
	buf[position++] = 0;

	timidity_write_data(buf, position);
}


// Plugin interface

class TimidityMusicPlugin : public MusicPluginObject {
public:
	const char *getName() const {
		return "TiMidity";
	}

	const char *getId() const {
		return "timidity";
	}

	MusicDevices getDevices() const;
	Common::Error createInstance(MidiDriver **mididriver, MidiDriver::DeviceHandle = 0) const;
};

MusicDevices TimidityMusicPlugin::getDevices() const {
	MusicDevices devices;
	devices.push_back(MusicDevice(this, "", MT_GM));
	return devices;
}

Common::Error TimidityMusicPlugin::createInstance(MidiDriver **mididriver, MidiDriver::DeviceHandle) const {
	*mididriver = new MidiDriver_TIMIDITY();

	return Common::kNoError;
}

//#if PLUGIN_ENABLED_DYNAMIC(TIMIDITY)
	//REGISTER_PLUGIN_DYNAMIC(TIMIDITY, PLUGIN_TYPE_MUSIC, TimidityMusicPlugin);
//#else
	REGISTER_PLUGIN_STATIC(TIMIDITY, PLUGIN_TYPE_MUSIC, TimidityMusicPlugin);
//#endif

#endif // defined(USE_TIMIDITY)
