/* Pseudo /dev/sequencer of TiMidity
 * This code for TiMidity++ v2.0.0.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>		/* for gethostbyname */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include <sys/soundcard.h>
#include <sys/awe_voice.h>
#include <sys/syscall.h>

SEQ_USE_EXTBUF();

#define TIMIDITY_HOST "127.0.0.1"
#define TIMIDITY_CONTROL_PORT	7777

#define SEQUENCER_DEV "/dev/sequencer"

#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff	/* -1 return */
#endif /* INADDR_NONE */


struct fd_read_buffer
{
    char buff[BUFSIZ];
    /* count: beginning of read pointer
     * size:  end of read pointer
     * fd:    file descripter for input
     */
    int count, size, fd;
};
static struct fd_read_buffer control_fd;
static int fdgets(char *buff, size_t buff_size, struct fd_read_buffer *p);
static char *timidity_ctl_command(const char* fmt, ...);

static struct synth_info ext_synth_info[] =
{
    {
	"TiMidity server",	/* name */
	0,			/* device */
	SYNTH_TYPE_MIDI,	/* synth_type */
	SAMPLE_TYPE_BASIC,	/* sunth_subtype */
	0,	/* perc_mode */
	36,	/* nr_voices */
	0,	/* nr_drums */
	128,	/* instr_bank_size */
	0	/* capabilities */
    },
    {
	"TiMidity server",	/* name */
	1,			/* device */
	SYNTH_TYPE_SAMPLE,	/* synth_type */
	SAMPLE_TYPE_AWE32,	/* sunth_subtype */
	0,	/* perc_mode */
	36,	/* nr_voices */
	0,	/* nr_drums */
	128,	/* instr_bank_size */
	0	/* capabilities */
    },
};

void timidity_sync(int tick)
{
    char *res;
    int status;
    unsigned long sleep_usec;

    _CHN_COMMON(0, 0xff, 0x7f, 0x02, 0x00, tick); /* Wait playout */
    SEQ_DUMPBUF();

    /* Wait "301 Sync OK" */
    do
    {
	res = timidity_ctl_command(NULL);
	status = atoi(res);
	if(status != 301)
	    fprintf(stderr, "ERROR: SYNC: %s", res);
    } while(status && status != 301);
    if(status == 301)
    {
	sleep_usec = (unsigned long)(atof(res + 4) * 1000000);
	if(sleep_usec > 0)
	{
	    usleep(sleep_usec);
	}
    }
}

static double get_time(void)
{
    struct timeval tv;
    struct timezone dmy;

#ifdef GETTIMEOFDAY_ONE_ARGUMENT
    gettimeofday(&tv);
#else
    gettimeofday(&tv, &dmy);
#endif

    return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0 ;
}

int ioctl(int fildes, int request, ...)
{
    char *res;
    void *arg;
    va_list ap;

    va_start(ap, request);
    arg = va_arg(ap, void *);
    va_end(ap);

    switch(request)
    {
      case SNDCTL_SEQ_CTRLRATE:
	*(int *)arg = 100;
	return 0;

      case SNDCTL_SEQ_NRSYNTHS:
	*(int *)arg = sizeof(ext_synth_info)/sizeof(ext_synth_info[0]);
	return 0;

      case SNDCTL_SYNTH_INFO:
	memcpy(arg, &ext_synth_info[ ((struct synth_info *)arg)->device ],
	       sizeof(struct synth_info));
	return 0;

      case SNDCTL_SEQ_NRMIDIS:
	*(int *)arg = 1;
	return 0;

      case SNDCTL_SEQ_RESET:
	_CHN_COMMON(0, 0xff, 0x2f, 0x00, 0x00, 0x0000); /* End of playing */
	_CHN_COMMON(0, 0xff, 0x7f, 0x01, 0x00, 0x0000); /* TiMidity reset */
	SEQ_DUMPBUF();
	timidity_sync(0);
	return 0;

      case SNDCTL_SEQ_RESETSAMPLES:
	return 0;

      case SNDCTL_SEQ_SYNC: {
	    static double last_time;
	    double t, sec;
	    t = get_time();
	    if(t - last_time < 0.1)
		return 0;
	    last_time = t;

	    timidity_sync(60);
	}
	return 0;

      case SNDCTL_FM_4OP_ENABLE:
      case SNDCTL_SYNTH_MEMAVL:
	return -1;
    }
    return syscall(SYS_ioctl, fildes, request, arg);
}


/* Return internet IP address in network byte order */
static unsigned int host_ip_address(const char* address)
{
    unsigned int addr;
    struct hostent *hp;

    if((addr = inet_addr(address)) != INADDR_NONE)
	return addr;

    if((hp = gethostbyname(address)) == NULL)
    {
	fprintf(stderr, "Unknown host name: %s\n", address);
	exit(1);
    }
    memcpy(&addr, hp->h_addr, hp->h_length);
    return addr;
}

static int connect_to_server(const char* hostname,
			     unsigned short tcp_port)
{
    int fd, len;
    struct sockaddr_in in;
    unsigned int addr;
    struct hostent *hp;

    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
	perror("socket");
	exit(1);
    }

    memset(&in, 0, sizeof(in));
    in.sin_family = AF_INET;
    in.sin_port   = htons(tcp_port);
    addr = host_ip_address(hostname);
    memcpy(&in.sin_addr, &addr, 4);

    hp = gethostbyaddr((unsigned char *)&in.sin_addr, 4, AF_INET);
    if(hp != NULL)
	fprintf(stderr, "Try to connect to %s (addr=%s, port=%d)\n",
		hp->h_name,
		inet_ntoa(in.sin_addr),
		tcp_port);
    else
	fprintf(stderr, "Try to connect to %s (port=%d)\n",
		inet_ntoa(in.sin_addr), tcp_port);
    alarm(2 * 60);
    if(connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0)
    {
	perror("connect");
	return -1;
    }
    alarm(0);

    len = sizeof(in);
    if(getsockname(fd, (struct sockaddr *)&in, &len) < 0)
	perror("getsockname");
    return fd;
}

int open(const char *path, int oflag, ...)
{
    char *res;
    int fd, i, data_port;

    if(strcmp(path, SEQUENCER_DEV) != 0)
    {
  	int ret;
	va_list ap;
	mode_t mode;

	va_start(ap, oflag);
	mode = va_arg(ap, mode_t);
	ret = syscall(SYS_open, path, oflag, mode);
	va_end(ap);
	return ret;
    }

    memset(&control_fd, 0, sizeof(control_fd));
    control_fd.fd = connect_to_server(TIMIDITY_HOST, TIMIDITY_CONTROL_PORT);

    res = timidity_ctl_command(NULL);
    if(atoi(res) != 220)
    {
	fprintf(stderr, "Can't connect timidity: (host=%s, port=%d)\n",
		TIMIDITY_HOST, TIMIDITY_CONTROL_PORT);
	return -1;
    }
    fputs(res, stdout);

    res = timidity_ctl_command("SETBUF 0.4 0.6");
    if(atoi(res) != 200)
	fprintf(stderr, "Warning: %s", res);

    /* Voice optimaization for non-real time. */
    res = timidity_ctl_command("AUTOREDUCE on 400");
    if(atoi(res) != 200)
	fprintf(stderr, "Warning: %s", res);

    i = 1;
    if(*(char *)&i == 1)
	res = timidity_ctl_command("OPEN lsb");
    else
	res = timidity_ctl_command("OPEN msb");
    if(atoi(res) != 200)
    {
	fprintf(stderr, "Can't open timidity data connection: %s", res);
	close(control_fd.fd);
	return -1;
    }

    data_port = atoi(res + 4);
    fd = connect_to_server(TIMIDITY_HOST, data_port);
    res = timidity_ctl_command(NULL);
    if(atoi(res) != 200)
    {
	fprintf(stderr, "Can't connect timidity: %s\t(host=%s, port=%d)\n",
		res, TIMIDITY_HOST, data_port);
	close(control_fd.fd);
	return -1;
    }
    return fd;
}

static int fdgets(char *buff, size_t buff_size, struct fd_read_buffer *p)
{
    int n, len, count, size, fd;
    char *buff_endp = buff + buff_size - 1, *pbuff, *beg;

    fd = p->fd;
    if(buff_size == 0)
	return 0;
    else if(buff_size == 1) /* buff == buff_endp */
    {
	*buff = '\0';
	return 0;
    }

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

static char *timidity_ctl_command(const char* fmt, ...)
{
    static char buff[BUFSIZ];
    int status, len;
    va_list ap;

    if(fmt != NULL)
    {
	va_start(ap, fmt);
	vsprintf(buff, fmt, ap);
	va_end(ap);
	len = strlen(buff);
	if(len > 0 && buff[len - 1] != '\n')
	    buff[len++] = '\n';
	write(control_fd.fd, buff, len);
    }

    while(1)
    {
	if(fdgets(buff, sizeof(buff), &control_fd) <= 0)
	{
	    strcpy(buff, "Read error\n");
	    break;
	}
	status = atoi(buff);
	if(400 <= status && status <= 499) /* Error of data stream */
	{
	    fputs(buff, stderr);
	    continue;
	}
	break;
    }
    return buff;
}

int close(int fd)
{
    extern int seqfd;
    if(fd == seqfd)
    {
	_CHN_COMMON(0, 0xff, 0x2f, 0x00, 0x00, 0x0000); /* End of playing */
	timidity_sync(0);
    }
    return syscall(SYS_close, fd);
}

#if 0 /* Not used */
void timidity_gus_load(int pgm)
{
    int dr, bank, prog;
    char *res;

    if(pgm < 0)
	return;
    if(patchloaded[pgm])
	return;

    if(pgm >= 128)
	res = timidity_ctl_command("PATCH drumset 0 %d", pgm - 128);
    else
	res = timidity_ctl_command("PATCH bank 0 %d", pgm);
    if(atoi(res) != 200)
    {
	fputs(res, stderr);
	patchloaded[pgm] = -1;
    }
    else
	patchloaded[pgm] = 1;
}
#endif
