#undef ioctl
#undef open
#undef close
#include <stdio.h>
#include <sys/soundcard.h>
#include <sys/awe_voice.h>
#define	SEQUENCER_DEV	"/dev/sequencer"

#define BUF_LOW_AT	0.1
#define BUF_HIGH_AT	0.15

#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>

SEQ_USE_EXTBUF();

#define TIMIDITY_HOST "127.0.0.1"
#define TIMIDITY_CONTROL_PORT	7777

#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 centsec)
{
    char *res;
    int status;

    _CHN_COMMON(0, 0xff, 0x7f, 0x02, 0x00, centsec); /* 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);
}

void timidity_eot(void)
{
    _CHN_COMMON(0, 0xff, 0x2f, 0x00, 0x00, 0x0000); /* End of playing */
    timidity_sync(0);
}

int timidity_ioctl(int fildes, int request, void *arg)
{
    char *res;

    switch(request)
    {
      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:
	timidity_eot();
	_CHN_COMMON(0, 0xff, 0x7f, 0x01, 0x00, 0x0000); /* TiMidity reset */
	SEQ_DUMPBUF();
	return 0;

      case SNDCTL_SEQ_RESETSAMPLES:
	return 0;

      case SNDCTL_SEQ_SYNC:
	/* Not implemented */
	return 0;

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

void timidity_close(int fd)
{
    extern int seqfd;
    char *res;

    if(fd == seqfd)
    {
	timidity_eot();
	timidity_sync(0);
	do
	{
	    res = timidity_ctl_command("CLOSE");
	} while(atoi(res) && atoi(res) != 302);
    }

    close(fd);
}



/* 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 timidity_open(const char *path, int oflag, int mode)
{
    char *res;
    int fd, i, data_port;
    static int first = 1;

    if(strcmp(path, SEQUENCER_DEV) != 0)
	return open(path, oflag, mode);

    if(first)
    {
	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);
	first = 0;
    }

    res = timidity_ctl_command("SETBUF %f %f", BUF_LOW_AT, BUF_HIGH_AT);
    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, 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;
}
