/* $Id: hayes.c,v 1.1.2.13 2011/01/21 15:29:59 khorben Exp $ */
/* Copyright (c) 2011 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 <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <termios.h>
#include <errno.h>
#include <glib.h>
#include <System.h>
#include <Phone/modem.h>


/* Hayes */
/* private */
/* types */
typedef enum _HayesMode { HAYES_MODE_INIT = 0, HAYES_MODE_COMMAND } HayesMode;

typedef struct _Hayes
{
	char * device;
	unsigned int baudrate;
	unsigned int retry;
	unsigned int hwflow;
	unsigned int quirks;

	/* modem */
	guint source;
	guint timeout;
	GIOChannel * channel;
	char * rd_buf;
	size_t rd_buf_cnt;
	guint rd_source;
	char * wr_buf;
	size_t wr_buf_cnt;
	guint wr_source;

	/* queue */
	HayesMode mode;
	GSList * queue;

	/* internal */
	ModemEvent events[MODEM_EVENT_TYPE_COUNT];
	char * authentication_name;
	char * model_name;
	char * model_vendor;
	char * model_version;
	char * registration_media;
	char * registration_operator;
} Hayes;

typedef enum _HayesCommandPriority
{
	HCP_NORMAL = 0,
	HCP_IMMEDIATE
} HayesCommandPriority;

typedef enum _HayesCommandStatus
{
	HCS_PENDING = 0,
	HCS_QUEUED,
	HCS_ACTIVE,
	HCS_TIMEOUT,
	HCS_ERROR,
	HCS_SUCCESS
} HayesCommandStatus;

typedef struct _HayesCommand HayesCommand;

typedef HayesCommandStatus (*HayesCommandCallback)(HayesCommand * command,
		HayesCommandStatus status, void * priv);

struct _HayesCommand
{
	HayesCommandPriority priority;
	HayesCommandStatus status;

	/* request */
	char * attention;
	unsigned int timeout;
	HayesCommandCallback callback;
	void * priv;

	/* answer */
	char * answer;
};

typedef enum _HayesQuirk
{
	HAYES_QUIRK_BATTERY_69		= 0x1,
	HAYES_QUIRK_CPIN_QUOTES		= 0x2,
	HAYES_QUIRK_WANT_SMSC_IN_PDU	= 0x4
} HayesQuirk;

typedef struct _HayesRequestHandler
{
	unsigned int type;
	char const * attention;
	HayesCommandCallback callback;
} HayesRequestHandler;

typedef struct _HayesTriggerHandler
{
	char const * trigger;
	void (*callback)(ModemPlugin * modem, char const * answer);
} HayesTriggerHandler;


/* constants */
enum
{
	HAYES_REQUEST_ALIVE = MODEM_REQUEST_COUNT,
	HAYES_REQUEST_BATTERY_LEVEL,
	HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_DISABLE,
	HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_ENABLE,
	HAYES_REQUEST_CONTACT,
	HAYES_REQUEST_EXTENDED_ERRORS,
	HAYES_REQUEST_EXTENDED_RING_REPORTS,
	HAYES_REQUEST_FUNCTIONAL,
	HAYES_REQUEST_LOCAL_ECHO_DISABLE,
	HAYES_REQUEST_LOCAL_ECHO_ENABLE,
	HAYES_REQUEST_MESSAGE,
	HAYES_REQUEST_MODEL,
	HAYES_REQUEST_OPERATOR,
	HAYES_REQUEST_OPERATOR_FORMAT_SHORT,
	HAYES_REQUEST_OPERATOR_FORMAT_LONG,
	HAYES_REQUEST_OPERATOR_FORMAT_NUMERIC,
	HAYES_REQUEST_PHONE_ACTIVE,
	HAYES_REQUEST_REGISTRATION,
	HAYES_REQUEST_REGISTRATION_UNSOLLICITED_DISABLE,
	HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE,
	HAYES_REQUEST_SIGNAL_LEVEL,
	HAYES_REQUEST_SIM_PIN_VALID,
	HAYES_REQUEST_VENDOR,
	HAYES_REQUEST_VERBOSE,
	HAYES_REQUEST_VERSION
};


/* prototypes */
/* modem */
static int _hayes_init(ModemPlugin * modem);
static int _hayes_destroy(ModemPlugin * modem);
static int _hayes_start(ModemPlugin * modem);
static int _hayes_stop(ModemPlugin * modem);
static int _hayes_request(ModemPlugin * modem, ModemRequest * request);
static int _hayes_trigger(ModemPlugin * modem, ModemEventType event);

/* parser */
static int _hayes_parse(ModemPlugin * modem);
static int _hayes_parse_trigger(ModemPlugin * modem, char const * answer,
		HayesCommand * command);

/* queue */
static int _hayes_queue_command(ModemPlugin * modem, HayesCommand * command);
static int _hayes_queue_command_full(ModemPlugin * modem,
		char const * attention, HayesCommandCallback callback);
static void _hayes_queue_flush(ModemPlugin * modem);
static int _hayes_queue_pop(ModemPlugin * modem);
static int _hayes_queue_push(ModemPlugin * modem);

static int _hayes_reset(ModemPlugin * modem);

/* commands */
static HayesCommand * _hayes_command_new(char const * attention);
static void _hayes_command_delete(HayesCommand * command);
static char const * _hayes_command_get_answer(HayesCommand * command);
static char const * _hayes_command_get_attention(HayesCommand * command);
static char * _hayes_command_get_line(HayesCommand * command,
		char const * prefix);
static HayesCommandStatus _hayes_command_get_status(HayesCommand * command);
static unsigned int _hayes_command_get_timeout(HayesCommand * command);
static void _hayes_command_set_callback(HayesCommand * command,
		HayesCommandCallback callback, void * priv);
static void _hayes_command_set_priority(HayesCommand * command,
		HayesCommandPriority priority);
static void _hayes_command_set_status(HayesCommand * command,
		HayesCommandStatus status);
static void _hayes_command_set_timeout(HayesCommand * command,
		unsigned int timeout);
static int _hayes_command_answer_append(HayesCommand * command,
		char const * answer);
static HayesCommandStatus _hayes_command_callback(HayesCommand * command);

/* callbacks */
static gboolean _on_reset(gpointer data);
static gboolean _on_timeout(gpointer data);
static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition,
		gpointer data);
static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition,
		gpointer data);

static HayesCommandStatus _on_request_authentication(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_battery_level(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_call(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_contacts(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_functional(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_generic(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_messages(HayesCommand * command,
		HayesCommandStatus status, void * priv);
static HayesCommandStatus _on_request_model(HayesCommand * command,
		HayesCommandStatus status, void * priv);

static void _on_trigger_cbc(ModemPlugin * modem, char const * answer);
static void _on_trigger_cgmi(ModemPlugin * modem, char const * answer);
static void _on_trigger_cgmm(ModemPlugin * modem, char const * answer);
static void _on_trigger_cgmr(ModemPlugin * modem, char const * answer);
static void _on_trigger_cops(ModemPlugin * modem, char const * answer);
static void _on_trigger_cpas(ModemPlugin * modem, char const * answer);
static void _on_trigger_cpin(ModemPlugin * modem, char const * answer);
static void _on_trigger_creg(ModemPlugin * modem, char const * answer);
static void _on_trigger_cring(ModemPlugin * modem, char const * answer);
static void _on_trigger_csq(ModemPlugin * modem, char const * answer);


/* variables */
static const struct
{
	char const * model;
	unsigned int quirks;
} _hayes_quirks[] =
{
	{ "\"Neo1973 Embedded GSM Modem\"",
		HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU	},
	{ "\"Neo1973 GTA01/GTA02 Embedded GSM Modem\"",
		HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU	},
	{ "\"Neo1973 GTA02 Embedded GSM Modem\"",
		HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU	},
	{ "Nokia N900",		HAYES_QUIRK_BATTERY_69			},
	{ NULL,			0					}
};

static HayesRequestHandler _hayes_request_handlers[] =
{
	{ HAYES_REQUEST_ALIVE,	"AT",		_on_request_generic },
	{ HAYES_REQUEST_BATTERY_LEVEL, "AT+CBC", _on_request_battery_level },
	{ HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_DISABLE, "AT+CCWA=1",
		_on_request_generic },
	{ HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_ENABLE, "AT+CCWA=1",
		_on_request_generic },
	{ HAYES_REQUEST_EXTENDED_ERRORS, "AT+CMEE=1", _on_request_generic },
	{ HAYES_REQUEST_EXTENDED_RING_REPORTS, "AT+CRC=1",
		_on_request_generic },
	{ HAYES_REQUEST_FUNCTIONAL, "AT+CFUN?", _on_request_functional },
	{ HAYES_REQUEST_LOCAL_ECHO_DISABLE, "ATE0", _on_request_generic },
	{ HAYES_REQUEST_LOCAL_ECHO_ENABLE, "ATE1", _on_request_generic },
	{ HAYES_REQUEST_MODEL,	"AT+CGMM",	_on_request_model },
	{ HAYES_REQUEST_OPERATOR, "AT+COPS?",	_on_request_generic },
	{ HAYES_REQUEST_OPERATOR_FORMAT_LONG, "AT+COPS=3,0",
		_on_request_generic },
	{ HAYES_REQUEST_OPERATOR_FORMAT_NUMERIC, "AT+COPS=3,2",
		_on_request_generic },
	{ HAYES_REQUEST_OPERATOR_FORMAT_SHORT, "AT+COPS=3,1",
		_on_request_generic },
	{ HAYES_REQUEST_PHONE_ACTIVE,	"AT+CPAS",_on_request_call },
	{ HAYES_REQUEST_REGISTRATION, "AT+CREG?",_on_request_generic },
	{ HAYES_REQUEST_REGISTRATION_UNSOLLICITED_DISABLE, "AT+CREG=0",
		_on_request_generic },
	{ HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE, "AT+CREG=2",
		_on_request_generic },
	{ HAYES_REQUEST_SIGNAL_LEVEL, "AT+CSQ",	_on_request_generic },
	{ HAYES_REQUEST_SIM_PIN_VALID, "AT+CPIN?", _on_request_authentication },
	{ HAYES_REQUEST_VENDOR,	"AT+CGMI",	_on_request_model },
	{ HAYES_REQUEST_VERBOSE,"ATV1",		_on_request_generic },
	{ HAYES_REQUEST_VERSION,"AT+CGMR",	_on_request_model },
	{ MODEM_REQUEST_CALL_ANSWER, "ATA",	_on_request_call },
	{ MODEM_REQUEST_CALL_HANGUP, "ATH",	_on_request_call },
	{ MODEM_REQUEST_CONTACT_LIST, "AT+CPBR=?", _on_request_contacts },
	{ MODEM_REQUEST_MESSAGE_LIST, "AT+CMGL=4", _on_request_messages }
};

static HayesTriggerHandler _hayes_trigger_handlers[] =
{
	{ "+CBC",	_on_trigger_cbc		},
	{ "+CGMI",	_on_trigger_cgmi	},
	{ "+CGMM",	_on_trigger_cgmm	},
	{ "+CGMR",	_on_trigger_cgmr	},
	{ "+COPS",	_on_trigger_cops	},
	{ "+CPAS",	_on_trigger_cpas	},
	{ "+CPIN",	_on_trigger_cpin	},
	{ "+CREG",	_on_trigger_creg	},
	{ "+CRING",	_on_trigger_cring	},
	{ "+CSQ",	_on_trigger_csq		}
};


/* public */
/* variables */
ModemPlugin plugin =
{
	NULL,
	"Hayes",
	NULL,
	_hayes_init,
	_hayes_destroy,
	_hayes_start,
	_hayes_stop,
	_hayes_request,
	_hayes_trigger,
	NULL
};


/* private */
/* functions */
static int _hayes_init(ModemPlugin * modem)
{
	Hayes * hayes;
	char const * p;
	size_t i;

	if((hayes = object_new(sizeof(*hayes))) == NULL)
		return -1;
	memset(hayes, 0, sizeof(*hayes));
	modem->priv = hayes;
	if((p = modem->helper->config_get(modem->helper->modem, NULL,
					"modem_device")) == NULL)
		p = "/dev/ttyU0";
	hayes->device = string_new(p);
	hayes->baudrate = 115200;	/* FIXME fetch from configuration */
	hayes->retry = 1000;
	hayes->hwflow = 0;		/* FIXME fetch from configuration */
	hayes->mode = HAYES_MODE_INIT;
	for(i = 0; i < sizeof(hayes->events) / sizeof(*hayes->events); i++)
	{
		hayes->events[i].type = i;
		if(i == MODEM_EVENT_TYPE_REGISTRATION)
			hayes->events[i].registration.signal = 0.0 / 0.0;
	}
	if(hayes->device == NULL)
		return _hayes_destroy(modem);
	return 0;
}


/* hayes_destroy */
static int _hayes_destroy(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;

	_hayes_stop(modem);
	string_delete(hayes->authentication_name);
	string_delete(hayes->model_name);
	string_delete(hayes->model_vendor);
	string_delete(hayes->model_version);
	string_delete(hayes->registration_media);
	string_delete(hayes->registration_operator);
	string_delete(hayes->device);
	object_delete(hayes);
	return 0;
}


/* hayes_request */
static char * _request_attention(ModemPlugin * modem, ModemRequestType request);

static int _hayes_request(ModemPlugin * modem, ModemRequest * request)
{
	int ret;
	size_t i;
	size_t count = sizeof(_hayes_request_handlers)
		/ sizeof(*_hayes_request_handlers);
	char const * attention;
	char * p = NULL;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%u)\n", __func__, (request != NULL)
			? request->type : (unsigned)-1);
#endif
	if(request == NULL)
		return -1;
	for(i = 0; i < count; i++)
		if(_hayes_request_handlers[i].type == request->type)
			break;
	if(i == count)
		return -1;
	if((attention = _hayes_request_handlers[i].attention) == NULL)
	{
		if((p = _request_attention(modem, request->type)) == NULL)
			return -1;
		attention = p;
	}
	ret = _hayes_queue_command_full(modem, attention,
			_hayes_request_handlers[i].callback);
	free(p);
	return ret;
}

static char * _request_attention(ModemPlugin * modem, ModemRequestType request)
{
	/* FIXME only useful if some requests have to be built on the fly */
	switch(request)
	{
		default:
			break;
	}
	return NULL;
}


/* hayes_start */
static int _hayes_start(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;

	hayes->source = g_idle_add(_on_reset, modem);
	return 0;
}


/* hayes_stop */
static int _hayes_stop(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	GError * error = NULL;

	_hayes_queue_flush(modem);
	if(hayes->channel != NULL)
	{
		/* XXX should the file descriptor also be closed? */
		g_io_channel_shutdown(hayes->channel, TRUE, &error);
		g_io_channel_unref(hayes->channel);
		hayes->channel = NULL;
	}
	/* FIXME some more? */
	return 0;
}


/* hayes_trigger */
static int _hayes_trigger(ModemPlugin * modem, ModemEventType event)
{
	int ret = 0;
	ModemRequest request;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%u)\n", __func__, event);
#endif
	memset(&request, 0, sizeof(request));
	switch(event)
	{
		case MODEM_EVENT_TYPE_AUTHENTICATION:
			request.type = HAYES_REQUEST_SIM_PIN_VALID;
			return _hayes_request(modem, &request);
		case MODEM_EVENT_TYPE_BATTERY_LEVEL:
			request.type = HAYES_REQUEST_BATTERY_LEVEL;
			return _hayes_request(modem, &request);
		case MODEM_EVENT_TYPE_CALL:
			request.type = HAYES_REQUEST_PHONE_ACTIVE;
			return _hayes_request(modem, &request);
		case MODEM_EVENT_TYPE_MODEL:
			request.type = HAYES_REQUEST_VENDOR;
			ret |= _hayes_request(modem, &request);
			request.type = HAYES_REQUEST_MODEL;
			ret |= _hayes_request(modem, &request);
			request.type = HAYES_REQUEST_VERSION;
			ret |= _hayes_request(modem, &request);
			break;
		case MODEM_EVENT_TYPE_REGISTRATION:
			request.type = HAYES_REQUEST_REGISTRATION;
			ret |= _hayes_request(modem, &request);
			break;
		case MODEM_EVENT_TYPE_CONTACT: /* do not make sense */
		case MODEM_EVENT_TYPE_ERROR:
		case MODEM_EVENT_TYPE_MESSAGE:
		case MODEM_EVENT_TYPE_MESSAGE_SENT:
			ret = -1;
			break;
	}
	return ret;
}


/* parser */
static int _parse_do(ModemPlugin * modem);

static int _hayes_parse(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	int ret = 0;
	size_t i = 0;
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() cnt=%lu\n", __func__,
			(unsigned long)hayes->rd_buf_cnt);
#endif
	while(i < hayes->rd_buf_cnt)
	{
		if(hayes->rd_buf[i++] != '\r' && hayes->rd_buf[i - 1] != '\n')
			continue;
		hayes->rd_buf[i - 1] = '\0';
		if(i < hayes->rd_buf_cnt && hayes->rd_buf[i] == '\n')
			i++;
		if(hayes->rd_buf[0] != '\0')
			ret |= _parse_do(modem);
		hayes->rd_buf_cnt -= i;
		memmove(hayes->rd_buf, &hayes->rd_buf[i], hayes->rd_buf_cnt);
		if((p = realloc(hayes->rd_buf, hayes->rd_buf_cnt)) != NULL)
			hayes->rd_buf = p; /* we can ignore errors... */
		else if(hayes->rd_buf_cnt == 0)
			hayes->rd_buf = NULL; /* ...except when it's not one */
		i = 0;
	}
#if 0
	if(hayes->mode == HAYES_MODE_PDU)
		return _parse_pdu(modem);
#endif
	return ret;
}

static int _parse_do(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
		: NULL;
	HayesCommandStatus status;

	/* FIXME only when non-empty, and not OK/ERROR/NO CARRIER... */
	if(command == NULL)
		/* this was most likely unsollicited */
		return _hayes_parse_trigger(modem, hayes->rd_buf, NULL);
	if(_hayes_command_answer_append(command, hayes->rd_buf) != 0)
		return -1;
	if((status = _hayes_command_callback(command)) == HCS_SUCCESS
			|| status == HCS_ERROR)
	{
		_hayes_queue_pop(modem);
		_hayes_queue_push(modem);
	}
	else if(status == HCS_ACTIVE)
		return _hayes_parse_trigger(modem, hayes->rd_buf, command);
	return 0;
}


/* hayes_parse_trigger */
static int _hayes_parse_trigger(ModemPlugin * modem, char const * answer,
		HayesCommand * command)
{
	size_t i;
	size_t count = sizeof(_hayes_trigger_handlers)
		/ sizeof(*_hayes_trigger_handlers);
	size_t len;
	HayesTriggerHandler * th;
	char const * p;
	int j;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(modem, \"%s\", command)\n", __func__,
			answer);
#endif
	/* if the trigger is obvious return directly */
	for(i = 0; i < count; i++)
	{
		th = &_hayes_trigger_handlers[i];
		len = strlen(th->trigger);
		if(strncmp(th->trigger, answer, len) != 0 || answer[len] != ':')
			continue;
		if(answer[++len] == ' ') /* skip the optional space */
			len++;
		th->callback(modem, &answer[len]);
		return 0;
	}
	/* if the answer has no prefix choose it from the command issued */
	if(command == NULL
			|| (p = _hayes_command_get_attention(command)) == NULL
			|| strncmp(p, "AT", 2) != 0)
		return 0;
	for(i = 0; i < count; i++)
	{
		th = &_hayes_trigger_handlers[i];
		len = strlen(th->trigger);
		if(strncmp(th->trigger, &p[2], len) != 0
				|| isalnum((j = p[2 + len])))
			continue;
		th->callback(modem, answer);
		return 0;
	}
	return 0;
}


/* queue */
/* hayes_queue_command */
static int _hayes_queue_command(ModemPlugin * modem, HayesCommand * command)
{
	Hayes * hayes = modem->priv;
	GSList * queue;

	switch(hayes->mode)
	{
		case HAYES_MODE_INIT:
			/* FIXME should ignore commands sent at this point */
		case HAYES_MODE_COMMAND:
			queue = hayes->queue;
			hayes->queue = g_slist_append(hayes->queue, command);
			_hayes_command_set_status(command, HCS_QUEUED);
			if(queue == NULL)
				_hayes_queue_push(modem);
			break;
	}
	return 0;
}


/* hayes_queue_command_full */
static int _hayes_queue_command_full(ModemPlugin * modem,
		char const * attention, HayesCommandCallback callback)
{
	HayesCommand * command;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, attention);
#endif
	if((command = _hayes_command_new(attention)) == NULL)
		return -modem->helper->error(modem->helper->modem, error_get(),
				1);
	_hayes_command_set_callback(command, callback, modem);
	if(_hayes_queue_command(modem, command) != 0)
	{
		_hayes_command_delete(command);
		return -1;
	}
	return 0;
}


/* hayes_queue_flush */
static void _hayes_queue_flush(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;

	g_slist_foreach(hayes->queue, (GFunc)_hayes_command_delete, NULL);
	g_slist_free(hayes->queue);
	hayes->queue = NULL;
	free(hayes->rd_buf);
	hayes->rd_buf = NULL;
	hayes->rd_buf_cnt = 0;
	if(hayes->rd_source != 0)
		g_source_remove(hayes->rd_source);
	hayes->rd_source = 0;
	hayes->wr_buf = NULL;
	hayes->wr_buf_cnt = 0;
	if(hayes->wr_source != 0)
		g_source_remove(hayes->wr_source);
	hayes->wr_source = 0;
	if(hayes->source != 0)
		g_source_remove(hayes->source);
	hayes->source = 0;
	if(hayes->timeout != 0)
		g_source_remove(hayes->timeout);
	hayes->timeout = 0;
}


/* hayes_queue_pop */
static int _hayes_queue_pop(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	HayesCommand * command;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(hayes->timeout != 0)
		g_source_remove(hayes->timeout);
	hayes->timeout = 0;
	if(hayes->queue == NULL) /* nothing to send */
		return 0;
	command = hayes->queue->data; /* XXX assumes it's valid */
	_hayes_command_delete(command);
	hayes->queue = g_slist_remove(hayes->queue, command);
	return 0;
}


/* hayes_queue_push */
static int _hayes_queue_push(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	HayesCommand * command;
	char const * attention;
	const char suffix[2] = "\r\n";
	size_t size;
	char * p;

	if(hayes->queue == NULL) /* nothing to send */
		return 0;
	command = hayes->queue->data;
	attention = _hayes_command_get_attention(command);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() pushing \"%s\"\n", __func__, attention);
#endif
	/* FIXME prepend escape sequence if we're in PPP mode */
	size = strlen(attention) + sizeof(suffix);
	if((p = realloc(hayes->wr_buf, hayes->wr_buf_cnt + size)) == NULL)
		return -modem->helper->error(modem->helper->modem, strerror(
					errno), 1);
	hayes->wr_buf = p;
	snprintf(&hayes->wr_buf[hayes->wr_buf_cnt], size, "%s%s", attention,
			suffix);
	hayes->wr_buf_cnt += size;
	_hayes_command_set_status(command, HCS_ACTIVE);
	if(hayes->channel != NULL && hayes->wr_source == 0)
		hayes->wr_source = g_io_add_watch(hayes->channel, G_IO_OUT,
				_on_watch_can_write, modem);
	if(hayes->timeout != 0)
		g_source_remove(hayes->timeout);
	if((hayes->timeout = _hayes_command_get_timeout(command)) != 0)
		hayes->timeout = g_timeout_add(hayes->timeout, _on_timeout,
				modem);
	return 0;
}


/* hayes_reset */
static int _hayes_reset(ModemPlugin * modem)
{
	Hayes * hayes = modem->priv;
	unsigned int retry = hayes->retry;

	/* FIXME risky */
	_hayes_destroy(modem);
	if(_hayes_init(modem) != 0)
		return -1;
	hayes = modem->priv;
	hayes->retry = retry;
	return 0;
}


/* commands */
static HayesCommand * _hayes_command_new(char const * attention)
{
	HayesCommand * command;

	if((command = object_new(sizeof(*command))) == NULL)
		return NULL;
	command->priority = HCP_NORMAL;
	command->status = HCS_PENDING;
	command->attention = string_new(attention);
	command->timeout = 30000;
	command->callback = NULL;
	command->priv = NULL;
	command->answer = NULL;
	if(command->attention == NULL)
	{
		_hayes_command_delete(command);
		return NULL;
	}
	return command;
}


/* hayes_command_delete */
static void _hayes_command_delete(HayesCommand * command)
{
	string_delete(command->attention);
	string_delete(command->answer);
	object_delete(command);
}


/* hayes_command_get_answer */
static char const * _hayes_command_get_answer(HayesCommand * command)
{
	return command->answer;
}


/* hayes_command_get_attention */
static char const * _hayes_command_get_attention(HayesCommand * command)
{
	return command->attention;
}


/* hayes_command_get_line */
static char * _hayes_command_get_line(HayesCommand * command,
		char const * prefix)
{
	/* FIXME also return the other lines matching */
	char * ret;
	char const * answer = command->answer;
	size_t len;
	char * p;

	if(prefix == NULL)
		return NULL;
	len = strlen(prefix);
	while(answer != NULL)
		if(strncmp(answer, prefix, len) == 0 && strncmp(&answer[len],
					": ", 2) == 0)
		{
			if((ret = string_new(&answer[len + 2])) != NULL
					&& (p = strchr(ret, '\n')) != NULL)
				*p = '\0';
			return ret;
		}
		else if((answer = strchr(answer, '\n')) != NULL)
			answer++;
	return NULL;
}


/* hayes_command_get_status */
static HayesCommandStatus _hayes_command_get_status(HayesCommand * command)
{
	return command->status;
}


/* hayes_command_get_timeout */
static unsigned int _hayes_command_get_timeout(HayesCommand * command)
{
	return command->timeout;
}


/* hayes_command_set_callback */
static void _hayes_command_set_callback(HayesCommand * command,
		HayesCommandCallback callback, void * priv)
{
	command->callback = callback;
	command->priv = priv;
}


/* hayes_command_set_priority */
static void _hayes_command_set_priority(HayesCommand * command,
		HayesCommandPriority priority)
{
	command->priority = priority;
}


/* hayes_command_set_status */
static void _hayes_command_set_status(HayesCommand * command,
		HayesCommandStatus status)
{
	command->status = status;
}


/* hayes_command_set_timeout */
static void _hayes_command_set_timeout(HayesCommand * command,
		unsigned int timeout)
{
	command->timeout = timeout;
}


/* hayes_command_answer_append */
static int _hayes_command_answer_append(HayesCommand * command,
		char const * answer)
{
	char * p;

	if(answer == NULL)
		return 0;
	if(command->answer == NULL)
		p = string_new_append(answer, "\n", NULL);
	else
		p = string_new_append(command->answer, answer, "\n", NULL);
	if(p == NULL)
		return -1;
	free(command->answer);
	command->answer = p;
	return 0;
}


/* hayes_command_callback */
static HayesCommandStatus _hayes_command_callback(HayesCommand * command)
{
	if(command->callback == NULL)
	{
		if(command->status == HCS_ACTIVE)
			return HCS_SUCCESS;
		return command->status;
	}
	return command->callback(command, command->status, command->priv);
}


/* callbacks */
/* on_reset */
static int _reset_open(Hayes * hayes);
static int _reset_configure(Hayes * hayes, int fd);
static gboolean _reset_settle(gpointer data);
static HayesCommandStatus _on_reset_callback(HayesCommand * command,
		HayesCommandStatus status, void * priv);

static gboolean _on_reset(gpointer data)
{
	ModemPlugin * modem = data;
	Hayes * hayes = modem->priv;
	GError * error = NULL;
	int fd;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	hayes->source = 0;
	if(hayes->channel != NULL)
	{
		/* XXX should the file descriptor also be closed? */
		g_io_channel_shutdown(hayes->channel, TRUE, &error);
		g_io_channel_unref(hayes->channel);
		hayes->channel = NULL;
	}
	if((fd = _reset_open(hayes)) < 0)
	{
		modem->helper->error(NULL, error_get(), 1);
		if(hayes->retry > 0)
			hayes->source = g_timeout_add(hayes->retry, _on_reset,
					modem);
		return FALSE;
	}
	hayes->channel = g_io_channel_unix_new(fd);
	if((g_io_channel_set_encoding(hayes->channel, NULL, &error))
			!= G_IO_STATUS_NORMAL)
		modem->helper->error(modem->helper->modem, error->message, 1);
	g_io_channel_set_buffered(hayes->channel, FALSE);
	hayes->rd_source = g_io_add_watch(hayes->channel, G_IO_IN,
			_on_watch_can_read, modem);
	_reset_settle(modem);
	return FALSE;
}

static int _reset_open(Hayes * hayes)
{
	int fd;

	if((fd = open(hayes->device, O_RDWR | O_NONBLOCK)) < 0)
		return -error_set_code(1, "%s: %s", hayes->device, strerror(
					errno));
	if(_reset_configure(hayes, fd) != 0)
	{
		close(fd);
		return -1;
	}
	return fd;
}

static int _reset_configure(Hayes * hayes, int fd)
{
	struct stat st;
	int fl;
	struct termios term;

	if(flock(fd, LOCK_EX | LOCK_NB) != 0)
		return 1;
	fl = fcntl(fd, F_GETFL, 0);
	if(fcntl(fd, F_SETFL, fl & ~O_NONBLOCK) == -1)
		return 1;
	if(fstat(fd, &st) != 0)
		return 1;
	if(st.st_mode & S_IFCHR) /* character special */
	{
		if(tcgetattr(fd, &term) != 0)
			return 1;
		term.c_cflag &= ~(CSIZE | PARENB);
		term.c_cflag |= CS8;
		term.c_cflag |= CREAD;
		term.c_cflag |= CLOCAL;
		if(hayes->hwflow != 0)
			term.c_cflag |= CRTSCTS;
		else
			term.c_cflag &= ~CRTSCTS;
		term.c_iflag = (IGNPAR | IGNBRK);
		term.c_lflag = 0;
		term.c_oflag = 0;
		term.c_cc[VMIN] = 1;
		term.c_cc[VTIME] = 0;
		if(cfsetispeed(&term, 0) != 0) /* same speed as output speed */
			error_set("%s", hayes->device); /* go on anyway */
		if(cfsetospeed(&term, hayes->baudrate) != 0)
			error_set("%s", hayes->device); /* go on anyway */
		if(tcsetattr(fd, TCSAFLUSH, &term) != 0)
			return 1;
	}
	return 0;
}

static gboolean _reset_settle(gpointer data)
{
	ModemPlugin * modem = data;
	HayesCommand * command;

	if((command = _hayes_command_new("ATZ")) == NULL)
	{
		modem->helper->error(modem->helper->modem, error_get(), 1);
		return FALSE;
	}
	_hayes_command_set_callback(command, _on_reset_callback, modem);
	_hayes_command_set_priority(command, HCP_IMMEDIATE);
	_hayes_command_set_timeout(command, 500);
	if(_hayes_queue_command(modem, command) != 0)
	{
		modem->helper->error(modem->helper->modem, error_get(), 1);
		_hayes_command_delete(command);
	}
	return FALSE;
}

static HayesCommandStatus _on_reset_callback(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	Hayes * hayes = modem->priv;
	ModemRequest request;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(hayes->timeout != 0)
		g_source_remove(hayes->timeout);
	hayes->timeout = 0;
	switch(status)
	{
		case HCS_PENDING: /* should not happen */
		case HCS_QUEUED:
		case HCS_SUCCESS:
		case HCS_ERROR:
			break;
		case HCS_ACTIVE:
			/* a reply was obtained */
			status = _on_request_generic(command, status, modem);
			if(status != HCS_SUCCESS && status != HCS_ERROR)
				return HCS_ACTIVE;
			hayes->mode = HAYES_MODE_COMMAND;
			request.type = HAYES_REQUEST_LOCAL_ECHO_DISABLE;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_VERBOSE;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_MODEL;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_EXTENDED_ERRORS;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_FUNCTIONAL;
			_hayes_request(modem, &request);
			return HCS_SUCCESS;
		case HCS_TIMEOUT:
			/* try again */
			_reset_settle(modem);
			break;
	}
	return HCS_ERROR; /* destroy and queue again */
}


/* on_timeout */
static gboolean _on_timeout(gpointer data)
{
	ModemPlugin * modem = data;
	Hayes * hayes = modem->priv;
	HayesCommand * command;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	hayes->timeout = 0;
	if(hayes->queue == NULL || (command = hayes->queue->data) == NULL)
		return FALSE;
	_hayes_command_set_status(command, HCS_TIMEOUT);
	_hayes_command_callback(command);
	_hayes_queue_pop(modem);
	_hayes_queue_push(modem);
	return FALSE;
}


/* on_watch_can_read */
static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition,
		gpointer data)
{
	ModemPlugin * modem = data;
	Hayes * hayes = modem->priv;
	gsize cnt = 0;
	GError * error = NULL;
	GIOStatus status;
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(condition != G_IO_IN || source != hayes->channel)
		return FALSE; /* should not happen */
	if((p = realloc(hayes->rd_buf, hayes->rd_buf_cnt + 256)) == NULL)
		return TRUE; /* XXX retries immediately (delay?) */
	hayes->rd_buf = p;
	status = g_io_channel_read_chars(source,
			&hayes->rd_buf[hayes->rd_buf_cnt], 256, &cnt, &error);
#ifdef DEBUG
	fprintf(stderr, "%s", "DEBUG: MODEM: ");
	fwrite(&hayes->rd_buf[hayes->rd_buf_cnt], sizeof(*p), cnt, stderr);
#endif
	hayes->rd_buf_cnt += cnt;
	switch(status)
	{
		case G_IO_STATUS_NORMAL:
			break;
		case G_IO_STATUS_ERROR:
			modem->helper->error(modem->helper->modem,
					error->message, 1);
		case G_IO_STATUS_EOF:
		default: /* should not happen... */
			if(hayes->retry > 0)
				_hayes_reset(modem);
			hayes->rd_source = 0;
			return FALSE;
	}
	_hayes_parse(modem);
	return TRUE;
}


/* on_watch_can_write */
static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition,
		gpointer data)
{
	ModemPlugin * modem = data;
	Hayes * hayes = modem->priv;
	gsize cnt = 0;
	GError * error = NULL;
	GIOStatus status;
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() cnt=%lu\n", __func__,
			(unsigned long)hayes->wr_buf_cnt);
#endif
	if(condition != G_IO_OUT || source != hayes->channel)
		return FALSE; /* should not happen */
	status = g_io_channel_write_chars(source, hayes->wr_buf,
			hayes->wr_buf_cnt, &cnt, &error);
#ifdef DEBUG
	fprintf(stderr, "%s", "DEBUG: PHONE: ");
	fwrite(hayes->wr_buf, sizeof(*p), cnt, stderr);
#endif
	if(cnt != 0) /* some data may have been written anyway */
	{
		hayes->wr_buf_cnt -= cnt;
		memmove(hayes->wr_buf, &hayes->wr_buf[cnt], hayes->wr_buf_cnt);
		if((p = realloc(hayes->wr_buf, hayes->wr_buf_cnt)) != NULL)
			hayes->wr_buf = p; /* we can ignore errors... */
		else if(hayes->wr_buf_cnt == 0)
			hayes->wr_buf = NULL; /* ...except when it's not one */
	}
	switch(status)
	{
		case G_IO_STATUS_NORMAL:
			break;
		case G_IO_STATUS_ERROR:
			modem->helper->error(modem->helper->modem,
					error->message, 1);
		case G_IO_STATUS_EOF:
		default: /* should not happen */
			hayes->wr_source = 0;
			if(hayes->retry > 0)
				_hayes_reset(modem);
			return FALSE;
	}
	if(hayes->wr_buf_cnt > 0) /* there is more data to write */
		return TRUE;
	hayes->wr_source = 0;
	return FALSE;
}


/* on_request_authentication */
static HayesCommandStatus _on_request_authentication(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_AUTHENTICATION];
	ModemRequest request;

	if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
		return status;
	modem->helper->event(modem->helper->modem, event);
	if(event->authentication.status == MODEM_AUTHENTICATION_STATUS_OK)
	{
		memset(&request, 0, sizeof(&request));
		request.type = HAYES_REQUEST_OPERATOR_FORMAT_LONG;
		_hayes_request(modem, &request);
		request.type = HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE;
		_hayes_request(modem, &request);
		request.type = MODEM_EVENT_TYPE_CALL;
		_hayes_request(modem, &request);
		_hayes_trigger(modem, MODEM_EVENT_TYPE_REGISTRATION);
	}
	return status;
}


/* on_request_battery_level */
static HayesCommandStatus _on_request_battery_level(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_BATTERY_LEVEL];

	if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
		return status;
	modem->helper->event(modem->helper->modem, event);
	return status;
}


/* on_request_call */
static HayesCommandStatus _on_request_call(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];

	if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
		return status;
	modem->helper->event(modem->helper->modem, event);
	return status;
}


/* on_request_contacts */
static HayesCommandStatus _on_request_contacts(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	/* FIXME implement */
	return _on_request_generic(command, status, priv);
}


/* on_request_functional */
static HayesCommandStatus _on_request_functional(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	ModemRequest request;

	memset(&request, 0, sizeof(request));
	switch((status = _on_request_generic(command, status, priv)))
	{
		case HCS_SUCCESS:
			request.type = HAYES_REQUEST_EXTENDED_RING_REPORTS;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_SIM_PIN_VALID;
			_hayes_request(modem, &request);
			break;
		case HCS_TIMEOUT:
			/* repeat request */
			request.type = HAYES_REQUEST_FUNCTIONAL;
			_hayes_request(modem, &request);
			break;
		default:
			break;
	}
	return status;
}


/* on_request_generic */
static HayesCommandStatus _on_request_generic(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	char const * answer;

	if((answer = _hayes_command_get_answer(command)) == NULL)
		return HCS_ERROR;
	while(answer != NULL)
		/* FIXME also handle BUSY/NO CARRIER/CONNECT/etc */
		if(strncmp(answer, "OK\n", 3) == 0)
			return HCS_SUCCESS;
		else if(strncmp(answer, "ERROR\n", 6) == 0)
			return HCS_ERROR;
		else if((answer = strchr(answer, '\n')) != NULL)
			answer++;
	return HCS_ACTIVE;
}


/* on_request_messages */
static HayesCommandStatus _on_request_messages(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	/* FIXME implement */
	return _on_request_generic(command, status, priv);
}


/* on_request_model */
static HayesCommandStatus _on_request_model(HayesCommand * command,
		HayesCommandStatus status, void * priv)
{
	ModemPlugin * modem = priv;
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];

	if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
		return status;
	modem->helper->event(modem->helper->modem, event);
	return status;
}


/* on_trigger_cbc */
static void _on_trigger_cbc(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_BATTERY_LEVEL];
	int res;
	unsigned int u;
	unsigned int v;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, answer);
#endif
	if((res = sscanf(answer, "%u,%u", &u, &v)) != 2)
		return;
	event->battery_level.status = MODEM_BATTERY_STATUS_UNKNOWN;
	if(u == 0)
		u = MODEM_BATTERY_STATUS_CONNECTED;
	else if(u == 1)
		u = MODEM_BATTERY_STATUS_CHARGING;
	else if(u == 2)
		u = MODEM_BATTERY_STATUS_NONE;
	else if(u == 3)
		u = MODEM_BATTERY_STATUS_ERROR;
	else
		u = MODEM_BATTERY_STATUS_UNKNOWN;
	event->battery_level.status = u;
	event->battery_level.level = v;
	if(hayes->quirks & HAYES_QUIRK_BATTERY_69)
		event->battery_level.level /= 69.0;
	else
		event->battery_level.level /= 100.0;
}


/* on_trigger_cgmi */
static void _on_trigger_cgmi(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, answer);
#endif
	if((p = strdup(answer)) == NULL)
		return; /* XXX report error? */
	free(hayes->model_vendor);
	hayes->model_vendor = p;
	event->model.vendor = p;
}


/* on_trigger_cgmm */
static void _on_trigger_cgmm(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
	char * p;
	size_t i;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, answer);
#endif
	if((p = strdup(answer)) == NULL)
		return; /* XXX report error? */
	free(hayes->model_name);
	hayes->model_name = p;
	event->model.name = p;
	/* determine known quirks */
	for(i = 0; _hayes_quirks[i].model != NULL; i++)
		if(strcmp(_hayes_quirks[i].model, p) == 0)
		{
			hayes->quirks = _hayes_quirks[i].quirks;
#ifdef DEBUG
			fprintf(stderr, "DEBUG: %s() quirks=%u\n", __func__,
					hayes->quirks);
#endif
			break;
		}
}


/* on_trigger_cgmr */
static void _on_trigger_cgmr(ModemPlugin * modem, char const * answer)
	/* FIXME the output may be multi-line */
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
	char * p;

	if((p = strdup(answer)) == NULL)
		return; /* XXX report error? */
	free(hayes->model_version);
	hayes->model_version = p;
	event->model.version = p;
}


/* on_trigger_cops */
static void _on_trigger_cops(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
	unsigned int u;
	unsigned int v;
	char buf[32];
	unsigned int w;

	if(sscanf(answer, "%u,%u,\"%31[^\"]\",%u", &u, &v, buf, &w) < 3)
		return; /* FIXME is also valid with 1 result */
	buf[sizeof(buf) - 1] = '\0';
	free(hayes->registration_operator);
	hayes->registration_operator = strdup(buf);
	event->registration._operator = hayes->registration_operator;
	switch(u)
	{
		case 0:
			u = MODEM_REGISTRATION_MODE_AUTOMATIC;
			break;
		case 1:
			u = MODEM_REGISTRATION_MODE_MANUAL;
			break;
		case 2:
			u = MODEM_REGISTRATION_MODE_DISABLED;
			break;
		case 3: /* only for setting the format */
		default:
			u = event->registration.mode;
			break;
	}
	/* this is usually worth an event */
	modem->helper->event(modem->helper->modem, event);
}


/* on_trigger_cpas */
static void _on_trigger_cpas(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
	unsigned int u;

	if(sscanf(answer, "%u", &u) != 1)
		return;
	switch(u)
	{
		case 0:
			event->call.status = MODEM_CALL_STATUS_NONE;
			event->call.direction = MODEM_CALL_DIRECTION_NONE;
			break;
		case 3:
			event->call.status = MODEM_CALL_STATUS_RINGING;
			event->call.direction = MODEM_CALL_DIRECTION_INCOMING;
			/* report event */
			modem->helper->event(modem->helper->modem, event);
			break;
		case 4:
			event->call.status = MODEM_CALL_STATUS_ACTIVE;
			event->call.direction = MODEM_CALL_DIRECTION_NONE;
			break;
		case 2: /* XXX unknown */
		default:
			break;
	}
}


/* on_trigger_cpin */
static void _on_trigger_cpin(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_AUTHENTICATION];
	char * p;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, answer);
#endif
	if(strcmp(answer, "READY") == 0)
		event->authentication.status = MODEM_AUTHENTICATION_STATUS_OK;
	else if(strcmp(answer, "SIM PIN") == 0)
	{
		free(hayes->authentication_name);
		p = strdup(answer);
		hayes->authentication_name = p;
		event->authentication.name = p;
		event->authentication.method = MODEM_AUTHENTICATION_METHOD_PIN;
		event->authentication.status
			= MODEM_AUTHENTICATION_STATUS_REQUIRED;
		/* FIXME also provide remaining retries */
	}
}


/* on_trigger_creg */
static void _on_trigger_creg(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
	int res;
	unsigned int u[4] = { 0, 0, 0, 0 };
	ModemRequest request;

	res = sscanf(answer, "%u,%u,%X,%X", &u[0], &u[1], &u[2], &u[3]);
	if(res == 0)
		return;
	if(res == 1 || res == 3)
		memmove(&u[1], u, sizeof(*u) * 3);
	u[0] = event->registration.mode;
	switch(u[1])
	{
		case 0:
			u[0] = MODEM_REGISTRATION_MODE_DISABLED;
			u[1] = MODEM_REGISTRATION_STATUS_NOT_SEARCHING;
			break;
		case 1:
			u[1] = MODEM_REGISTRATION_STATUS_REGISTERED;
			break;
		case 2:
			u[1] = MODEM_REGISTRATION_STATUS_SEARCHING;
			break;
		case 3:
			u[1] = MODEM_REGISTRATION_STATUS_DENIED;
			break;
		case 5:
			u[1] = MODEM_REGISTRATION_STATUS_REGISTERED;
			break;
		case 4: /* unknown */
		default:
			u[0] = MODEM_REGISTRATION_MODE_UNKNOWN;
			u[1] = MODEM_REGISTRATION_STATUS_UNKNOWN;
			break;
	}
	event->registration.mode = u[0];
	switch((event->registration.status = u[1]))
	{
		case MODEM_REGISTRATION_STATUS_REGISTERED:
			memset(&request, 0, sizeof(request));
			request.type = HAYES_REQUEST_OPERATOR;
			_hayes_request(modem, &request);
			request.type = HAYES_REQUEST_SIGNAL_LEVEL;
			_hayes_request(modem, &request);
			break;
		default:
			/* this is usually an unsollicited event */
			modem->helper->event(modem->helper->modem, event);
			break;
	}
}


/* on_trigger_cring */
static void _on_trigger_cring(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];

	if(strcmp(answer, "VOICE") == 0)
		; /* FIXME implement */
	event->call.status = MODEM_CALL_STATUS_RINGING;
	event->call.direction = MODEM_CALL_DIRECTION_INCOMING;
	event->call.number = "";
	/* this is always an unsollicited event */
	modem->helper->event(modem->helper->modem, event);
}


/* on_trigger_csq */
static void _on_trigger_csq(ModemPlugin * modem, char const * answer)
{
	Hayes * hayes = modem->priv;
	ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
	unsigned int u;
	unsigned int v;

	if(sscanf(answer, "%u,%u", &u, &v) != 2)
		return;
	if(u > 31)
		event->registration.signal = 0.0 / 0.0;
	else
		event->registration.signal = (u / 32) + 0.0;
	/* this is usually worth an event */
	modem->helper->event(modem->helper->modem, event);
}