/*
 * Copyright © 2009 Keith Packard <keithp@keithp.com>
 *
 * 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.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */

#include "aoview.h"
#include <termios.h>

#define AOVIEW_SERIAL_IN_BUF	64
#define AOVIEW_SERIAL_OUT_BUF	64

struct aoview_buf {
	char		*buf;
	int		off;
	int		count;
	int		size;
};

static int
aoview_buf_write(struct aoview_buf *buf, char *data, int len)
{
	if (buf->count + len > buf->size) {
		int	new_size = buf->size * 2;
		if (new_size == 0)
			new_size = 1024;
		if (buf->buf)
			buf->buf = realloc (buf->buf, new_size);
		else
			buf->buf = malloc (new_size);
		buf->size = new_size;
	}
	memcpy(buf->buf + buf->count, data, len);
	buf->count += len;
	return len;
}

static int
aoview_buf_read(struct aoview_buf *buf, char *data, int len)
{
	if (len > buf->count - buf->off)
		len = buf->count - buf->off;
	memcpy (data, buf->buf + buf->off, len);
	buf->off += len;
	if (buf->off == buf->count)
		buf->off = buf->count = 0;
	return len;
}

static int
aoview_buf_getc(struct aoview_buf *buf)
{
	char	b;
	int	r;

	r = aoview_buf_read(buf, &b, 1);
	if (r == 1)
		return (int) b;
	return -1;
}

static void
aoview_buf_flush(struct aoview_buf *buf, int fd)
{
	int	ret;

	if (buf->count > buf->off) {
		ret = write(fd, buf->buf + buf->off, buf->count - buf->off);
		if (ret > 0) {
			buf->off += ret;
			if (buf->off == buf->count)
				buf->off = buf->count = 0;
		}
	}
}

static void
aoview_buf_fill(struct aoview_buf *buf, int fd)
{
	int ret;

	while (buf->count >= buf->size) {
		int new_size = buf->size * 2;
		buf->buf = realloc (buf->buf, new_size);
		buf->size = new_size;
	}

	ret = read(fd, buf->buf + buf->count, buf->size - buf->count);
	if (ret > 0)
		buf->count += ret;
}

static void
aoview_buf_init(struct aoview_buf *buf)
{
	buf->buf = malloc (buf->size = 1024);
	buf->count = 0;
}

static void
aoview_buf_fini(struct aoview_buf *buf)
{
	free(buf->buf);
}

struct aoview_serial {
	GSource			source;
	int			fd;
	struct termios		save_termios;
	struct aoview_buf	in_buf;
	struct aoview_buf	out_buf;
	GPollFD			poll_fd;
};


void
aoview_serial_printf(struct aoview_serial *serial, char *format, ...)
{
	char	buf[1024];
	va_list	ap;
	int	ret;

	/* sprintf to a local buffer */
	va_start(ap, format);
	ret = vsnprintf(buf, sizeof(buf), format, ap);
	va_end(ap);
	if (ret > sizeof(buf)) {
		fprintf(stderr, "printf overflow for format %s\n",
			format);
	}

	/* flush local buffer to the wire */
	aoview_buf_write(&serial->out_buf, buf, ret);
	aoview_buf_flush(&serial->out_buf, serial->fd);
}

int
aoview_serial_read(struct aoview_serial *serial, char *buf, int len)
{
	return aoview_buf_read(&serial->in_buf, buf, len);
}

int
aoview_serial_getc(struct aoview_serial *serial)
{
	return aoview_buf_getc(&serial->in_buf);
}

static gboolean
serial_prepare(GSource *source, gint *timeout)
{
	struct aoview_serial *serial = (struct aoview_serial *) source;
	*timeout = -1;

	if (serial->out_buf.count)
		serial->poll_fd.events |= G_IO_OUT;
	else
		serial->poll_fd.events &= ~G_IO_OUT;
	return FALSE;
}

static gboolean
serial_check(GSource *source)
{
	struct aoview_serial *serial = (struct aoview_serial *) source;
	gint revents = serial->poll_fd.revents;

	if (revents & G_IO_NVAL)
		return FALSE;
	if (revents & G_IO_IN)
		return TRUE;
	if (revents & G_IO_OUT)
		return TRUE;
	return FALSE;
}

static gboolean
serial_dispatch(GSource *source,
		GSourceFunc callback,
		gpointer user_data)
{
	struct aoview_serial *serial = (struct aoview_serial *) source;
	aoview_serial_callback func = (aoview_serial_callback) callback;
	gint revents = serial->poll_fd.revents;

	if (revents & G_IO_IN)
		aoview_buf_fill(&serial->in_buf, serial->fd);

	if (revents & G_IO_OUT)
		aoview_buf_flush(&serial->out_buf, serial->fd);

	if (func)
		(*func)(user_data, serial, revents);
	return TRUE;
}

static void
serial_finalize(GSource *source)
{
	struct aoview_serial *serial = (struct aoview_serial *) source;

	aoview_buf_fini(&serial->in_buf);
	aoview_buf_fini(&serial->out_buf);
	tcsetattr(serial->fd, TCSAFLUSH, &serial->save_termios);
	close (serial->fd);
}

static GSourceFuncs serial_funcs = {
	serial_prepare,
	serial_check,
	serial_dispatch,
	serial_finalize
};

struct aoview_serial *
aoview_serial_open(const char *tty)
{
	struct aoview_serial	*serial;
	struct termios	termios;

	serial = (struct aoview_serial *) g_source_new(&serial_funcs, sizeof (struct aoview_serial));
	aoview_buf_init(&serial->in_buf);
	aoview_buf_init(&serial->out_buf);
	serial->fd = open (tty, O_RDWR | O_NONBLOCK);
	if (serial->fd < 0) {
		g_source_destroy(&serial->source);
		return NULL;
	}
	tcgetattr(serial->fd, &termios);
	serial->save_termios = termios;
	cfmakeraw(&termios);
	tcsetattr(serial->fd, TCSAFLUSH, &termios);

	aoview_serial_printf(serial, "E 0\n");
	tcdrain(serial->fd);
	usleep(15*1000);
	tcflush(serial->fd, TCIFLUSH);
	serial->poll_fd.fd = serial->fd;
	serial->poll_fd.events = G_IO_IN | G_IO_OUT | G_IO_HUP | G_IO_ERR;
	g_source_attach(&serial->source, NULL);
	g_source_add_poll(&serial->source,&serial->poll_fd);
	aoview_serial_set_callback(serial, NULL);
	return serial;
}

void
aoview_serial_close(struct aoview_serial *serial)
{
	g_source_remove_poll(&serial->source, &serial->poll_fd);
	close(serial->fd);
	g_source_destroy(&serial->source);
}

void
aoview_serial_set_callback(struct aoview_serial *serial,
			   aoview_serial_callback func)
{
	g_source_set_callback(&serial->source, (GSourceFunc) func, serial, NULL);
}
