/* $Id: gsm.c,v 1.3 2010/04/24 15:26:11 khorben Exp $ */
/* Copyright (c) 2010 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Desktop Phone */
/* 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, version 3 of the License.
 *
 * 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/>. */



#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include "phone.h"
#include "gsm.h"


/* GSM */
/* private */
/* types */
struct _GSM
{
	char * device;
	unsigned int baudrate;
	unsigned int retry;
	GIOChannel * channel;
	char * rd_buf;
	size_t rd_buf_cnt;
	char * wr_buf;
	size_t wr_buf_cnt;

	/* internal */
	guint source;
	guint rd_io;
	guint wr_io;
};


/* prototypes */
static int _is_figure(int c);
static int _is_number(char const * number);

/* callbacks */
static gboolean _on_reset(gpointer data);

static gboolean _on_watch_read(GIOChannel * source, GIOCondition condition,
		gpointer data);
static gboolean _on_watch_write(GIOChannel * source, GIOCondition condition,
		gpointer data);


/* public */
/* functions */
/* gsm_new */
GSM * gsm_new(char const * device, unsigned int baudrate)
{
	GSM * gsm;

	if(device == NULL)
		return NULL;
	if((gsm = malloc(sizeof(*gsm))) == NULL)
		return NULL;
	gsm->device = strdup(device);
	gsm->baudrate = baudrate;
	gsm->retry = 1000;
	gsm->channel = NULL;
	gsm->rd_buf = NULL;
	gsm->rd_buf_cnt = 0;
	gsm->wr_buf = NULL;
	gsm->wr_buf_cnt = 0;
	gsm->source = 0;
	gsm->rd_io = 0;
	gsm->wr_io = 0;
	if(gsm->device == NULL)
	{
		gsm_delete(gsm);
		return NULL;
	}
	gsm_reset(gsm, 0);
	return gsm;
}


/* gsm_delete */
void gsm_delete(GSM * gsm)
{
	free(gsm->device);
	if(gsm->channel != NULL)
	{
		g_io_channel_shutdown(gsm->channel, TRUE, NULL);
		g_io_channel_unref(gsm->channel);
	}
	free(gsm->rd_buf);
	free(gsm->wr_buf);
	if(gsm->source != 0)
		g_source_remove(gsm->source);
	if(gsm->rd_io != 0)
		g_source_remove(gsm->rd_io);
	if(gsm->wr_io != 0)
		g_source_remove(gsm->wr_io);
	free(gsm);
}


/* accessors */
/* gsm_get_retry */
unsigned int gsm_get_retry(GSM * gsm)
{
	return gsm->retry;
}


/* gsm_set_retry */
void gsm_set_retry(GSM * gsm, unsigned int retry)
{
	gsm->retry = retry;
}


/* useful */
/* gsm_call */
int gsm_call(GSM * gsm, char const * number)
{
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, number);
#endif
	/* FIXME check current status before calling */
	return gsm_modem_call(gsm, number);
}


/* gsm_hangup */
int gsm_hangup(GSM * gsm)
{
	/* FIXME check current status before hanging up */
	return gsm_modem_hangup(gsm);
}


/* gsm_modem_call */
int gsm_modem_call(GSM * gsm, char const * number)
{
	int ret;
	char const cmd[] = "ATD";
	size_t len;
	char * buf;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, number);
#endif
	if(!_is_number(number))
		return 1;
	len = sizeof(cmd) + strlen(number) + 2;
	if((buf = malloc(len)) == NULL)
		return phone_error(NULL, "malloc", 1);
	snprintf(buf, len, "%s%s\r\n", cmd, number);
	ret = gsm_modem_queue(gsm, buf);
	free(buf);
	return ret;
}


/* gsm_modem_hangup */
int gsm_modem_hangup(GSM * gsm)
{
	return gsm_modem_queue(gsm, "\r\nATH\r\n");
}


/* gsm_modem_queue */
int gsm_modem_queue(GSM * gsm, char const * command)
{
	size_t len = strlen(command);
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, command);
#endif
	if((p = realloc(gsm->wr_buf, gsm->wr_buf_cnt + len)) == NULL)
		return phone_error(NULL, "malloc", 1);
	gsm->wr_buf = p;
	memcpy(&gsm->wr_buf[gsm->wr_buf_cnt], command, len);
	gsm->wr_buf_cnt += len;
	if(gsm->channel != NULL && gsm->wr_buf_cnt > 0 && gsm->wr_io == 0)
		gsm->wr_io = g_io_add_watch(gsm->channel, G_IO_OUT,
				_on_watch_write, gsm);
	return 0;
}


/* gsm_modem_reset */
int gsm_modem_reset(GSM * gsm)
{
	/* FIXME queue commands in sequence and prepend this one to the list */
	return gsm_modem_queue(gsm, "\r\nATZ\r\n");
}


/* gsm_reset */
void gsm_reset(GSM * gsm, unsigned int delay)
{
	GError * error = NULL;

	free(gsm->rd_buf);
	gsm->rd_buf = NULL;
	gsm->rd_buf_cnt = 0;
	free(gsm->wr_buf);
	gsm->wr_buf = NULL;
	gsm->wr_buf_cnt = 0;
	if(gsm->source != 0)
	{
		g_source_remove(gsm->source);
		gsm->source = 0;
	}
	if(gsm->rd_io != 0)
	{
		g_source_remove(gsm->rd_io);
		gsm->rd_io = 0;
	}
	if(gsm->wr_io != 0)
	{
		g_source_remove(gsm->wr_io);
		gsm->wr_io = 0;
	}
	if(gsm->channel != NULL)
	{
		g_io_channel_shutdown(gsm->channel, TRUE, &error);
		g_io_channel_unref(gsm->channel);
		gsm->channel = NULL;
	}
	if(delay > 0)
		gsm->source = g_timeout_add(delay, _on_reset, gsm);
	else
		gsm->source = g_idle_add(_on_reset, gsm);
}


/* private */
/* functions */
/* is_figure */
static int _is_figure(int c)
{
	if(c >= '0' && c <= '9')
		return 1;
	if(c == '*' || c == '+' || c == '#')
		return 1;
	return 0;
}


/* is_number */
static int _is_number(char const * number)
{
	if(number == NULL || number[0] == '\0')
		return 0;
	while(*number != '\0')
		if(!_is_figure(*(number++)))
			return 0;
	return 1;
}


/* callbacks */
/* on_reset */
static int _reset_do(GSM * gsm, int fd);

static gboolean _on_reset(gpointer data)
{
	GSM * gsm = data;
	int fd;
	GError * error = NULL;

	gsm->source = 0;
	if((fd = open(gsm->device, O_RDWR | O_NONBLOCK)) < 0)
		return phone_error(NULL, "open", 1);
	if(_reset_do(gsm, fd) != 0)
	{
		close(fd);
		if(gsm->retry > 0)
			gsm->source = g_timeout_add(gsm->retry, _on_reset, gsm);
		return FALSE;
	}
	gsm->channel = g_io_channel_unix_new(fd);
	if((g_io_channel_set_encoding(gsm->channel, NULL, &error))
			!= G_IO_STATUS_NORMAL)
		/* XXX ugly */
		fprintf(stderr, "ERROR: %s() g_io_channel_set_encoding\n",
				__func__);
	g_io_channel_set_buffered(gsm->channel, FALSE);
	gsm->rd_io = g_io_add_watch(gsm->channel, G_IO_IN, _on_watch_read, gsm);
	gsm_modem_reset(gsm);
	return FALSE;
}

static int _reset_do(GSM * gsm, int fd)
{
	struct stat st;
	int fl;
	struct termios term;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%d)\n", __func__, fd);
#endif
	if(flock(fd, LOCK_EX | LOCK_NB) != 0)
		return phone_error(NULL, "flock", 1);
	if(fstat(fd, &st) != 0)
		return phone_error(NULL, "fstat", 1);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%d) mode 0%o\n", __func__, fd, st.st_mode);
#endif
	if(st.st_mode & S_IFCHR) /* character special */
	{
		if(tcgetattr(fd, &term) != 0)
			return phone_error(NULL, "tcgetattr", 1);
		switch(gsm->baudrate) /* XXX rewrite this a nicer way */
		{
			case 4800:
				term.c_cflag = B4800;
				break;
			case 9600:
				term.c_cflag = B9600;
				break;
			case 19200:
				term.c_cflag = B19200;
				break;
			case 38400:
				term.c_cflag = B38400;
				break;
			case 115200:
				term.c_cflag = B115200;
				break;
			default:
				errno = EINVAL;
				return phone_error(NULL, "baudrate", 1);
		}
		term.c_cflag |= CS8;
		term.c_cflag |= CREAD;
		term.c_cflag |= CLOCAL;
		term.c_iflag = (IGNPAR | IGNBRK);
		term.c_lflag = 0;
		term.c_oflag = 0;
		term.c_cc[VMIN] = 1;
		term.c_cc[VTIME] = 0;
		if(tcsetattr(fd, TCSAFLUSH, &term) != 0)
			return phone_error(NULL, "tcsetattr", 1);
	}
	fl = fcntl(fd, F_GETFL, 0);
	if(fcntl(fd, F_SETFL, fl & ~O_NONBLOCK) == -1)
		return phone_error(NULL, "fcntl", 1);
	return 0;
}


/* on_watch_read */
static gboolean _on_watch_read(GIOChannel * source, GIOCondition condition,
		gpointer data)
{
	GSM * gsm = data;
	char buf[256];
	gsize cnt = 0;
	GError * error = NULL;
	GIOStatus status;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%d)\n", __func__, condition);
#endif
	if(condition != G_IO_IN || source != gsm->channel)
		return FALSE;
	/* FIXME really implement */
	status = g_io_channel_read_chars(source, buf, sizeof(buf), &cnt,
			&error);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() cnt=%lu\n", __func__, cnt);
#endif
	switch(status)
	{
		case G_IO_STATUS_NORMAL:
			break;
		case G_IO_STATUS_ERROR:
			/* XXX report error, do not break */
			fprintf(stderr, "%s%s\n", "phone: read: ",
					error->message);
		case G_IO_STATUS_EOF:
		default: /* should not happen... */
			if(gsm->retry > 0)
				gsm_reset(gsm, gsm->retry);
			return FALSE;
	}
	/* FIXME parse and interpret the output */
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() \"", __func__);
	fwrite(buf, sizeof(*buf), cnt, stderr);
	fputs("\"\n", stderr);
#endif
	return TRUE;
}


/* on_watch_write */
static gboolean _on_watch_write(GIOChannel * source, GIOCondition condition,
		gpointer data)
{
	GSM * gsm = data;
	gsize cnt = 0;
	GError * error = NULL;
	GIOStatus status;
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%d)\n", __func__, condition);
#endif
	if(condition != G_IO_OUT || source != gsm->channel)
		return FALSE;
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() write %zu\n", __func__, gsm->wr_buf_cnt);
#endif
	status = g_io_channel_write_chars(source, gsm->wr_buf, gsm->wr_buf_cnt,
			&cnt, &error);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() wrote %lu \"", __func__, cnt);
	fwrite(gsm->wr_buf, sizeof(*gsm->wr_buf), cnt, stderr);
	fputs("\"\n", stderr);
#endif
	if(cnt != 0) /* data may have be written anyway */
	{
		memmove(gsm->wr_buf, &gsm->wr_buf[cnt], gsm->wr_buf_cnt - cnt);
		gsm->wr_buf_cnt -= cnt;
		if((p = realloc(gsm->wr_buf, gsm->wr_buf_cnt)) != NULL)
			gsm->wr_buf = p;
	}
	switch(status)
	{
		case G_IO_STATUS_NORMAL:
			break;
		case G_IO_STATUS_ERROR:
			/* XXX report error, do not break */
#ifdef DEBUG
			perror("phone: write");
#else
			fprintf(stderr, "%s%s\n", "phone: write: ",
					error->message);
#endif
		case G_IO_STATUS_EOF:
		default: /* should not happen... */
			if(gsm->retry > 0)
				gsm_reset(gsm, gsm->retry);
			return FALSE;
	}
	if(gsm->wr_buf_cnt > 0) /* there is more data to write */
		return TRUE;
	gsm->wr_io = 0;
	return FALSE;
}