/*
 * Copyright (c) 2004-2009, Luiz Otavio O Souza <loos.br@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

static const char rcsid[] = "$Id: command.c 112 2009-03-15 17:30:28Z loos-br $";

#include <errno.h>
#include <stdlib.h>
#include <string.h>

#include "io.h"
#include "return.h"
#include "command.h"
#include "protocol.h"
#include "msn-proxy.h"

command *
command_alloc(void) {
 command	*rtrn;

    /* alloc a new command storage */
    rtrn = (command *)malloc(sizeof(command));
    if (rtrn == (command *)0)
	die_nomem();

    /* zero everything */
    memset(rtrn, 0, sizeof(command));
    return(rtrn);
}

void
has_trid(command *cmd) {
 string		*trid;
 char		*ep;

    /* at least one arg is needed here */
    if (cmd->args_len < 1) return;

    trid = get_arg(cmd, 0);
    if (trid == NULL || trid->s == NULL || trid->len == 0) return;

    cmd->trid = strtoul((char *)trid->s, &ep, 10);
    if (ep && *ep)
	cmd->trid = 0;
}

__uint32_t
has_payload(command *cmd) {
 long 		rtrn;

    if (cmd->cmd.len == 0)
	return(0);

    if (strcasecmp((char *)cmd->cmd.s, "241") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "508") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "509") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "511") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "ADL") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "FQY") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "GCF") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "MSG") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "NFY") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "NOT") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "PAG") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "PUT") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "QRY") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "RML") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UBM") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UBN") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UBX") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UUM") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UUN") != 0 &&
	strcasecmp((char *)cmd->cmd.s, "UUX") != 0) {

	return(0);
    }

    /* to match only client qry and not the server reply */
    if (strcasecmp((char *)cmd->cmd.s, "QRY") == 0 && cmd->args_len != 3) {
	return(0);
    }

    if (cmd->args_len < 1)
	return(0);

    rtrn = (__uint32_t)atoi((char *)cmd->args[cmd->args_len - 1]->s);
    return(rtrn);
}

string **
args_alloc(void *ptr, size_t len) {
 string		**rtrn = (string **)0;

    if (len == 0)
	; /* do nothing */
    else if (len == 1)
	rtrn = (string **)malloc(sizeof(string *) * (len + 1));
    else if (len > 1)
	rtrn = (string **)realloc(ptr, sizeof(string *) * (len + 1));

    return(rtrn);
}

string *
command_add_arg(command *cmd) {
 string			*arg;

    /* alloc string */
    arg = str_alloc();
    if (arg == (string *)0)
	die_nomem();

    /* increase args size and realloc */
    cmd->args_len++;
    cmd->args = args_alloc(cmd->args, cmd->args_len);
    if (cmd->args == (string **)0)
	die_nomem();

    cmd->args[cmd->args_len - 1] = arg;
    cmd->args[cmd->args_len] = (string *)0;

    return(arg);
}

/*
 * return the number os bytes read or
 *  0 - connection closed
 * -1 - error
 * -2 - try again later
 */
int
command_fill_buffer(int fd, string *buf) {
 log_			*log = &config.log;

    /* empty buffer */
    if (buf->len == 0) {

	/* read buffer */
	buf->len = io_read(fd, (char *)buf->s, buf->a);
	if (buf->len == -1) {

	    if (errno == EAGAIN)
		return(-2);

	    str_reset(buf);
	    log->debug("read fail: [%S]\n", strerror(errno));
	    return(-1);
	}

	buf->p = buf->s;
    }

    if (buf->len == 0)
	return(0);

    return(buf->len);
}

void
shift_buffer(string *buf) {
    buf->p++;
    buf->len--;
}

int
command_prepare(commands *cmds) {
 command		*cmd;
 string			*buf;

    if (cmds->state & PAYLOAD)
	/* not ready */
	return(ROK);

    /* alloc command buffer if needed */
    if (cmds->tmp == (command *)0) {
	cmds->tmp = command_alloc();

	cmds->state &= ~RECVMASK;
	cmds->state |= COMMAND;
	cmds->arg   = (string *)0;
    }
    cmd = cmds->tmp;
    buf = &cmds->buf;

    while (buf->len > 0) {

	/* end of command */
	if (*buf->p == '\n') {

	    shift_buffer(buf);

	    /* reset */
	    cmds->state &= ~RECVMASK;
	    cmds->state |= COMMAND;
	    cmds->arg   = (string *)0;

	    return(ROK);
	}

	/* \r\n - end of command */
	if (*buf->p == '\r') {
	    shift_buffer(buf);
	    cmds->state &= ~RECVMASK;
	    cmds->state |= EOC;
	    continue;
	}

	switch (cmds->state & RECVMASK) {

	    case COMMAND:

		/* new arg */
		if (*buf->p == ' ') {
		    cmds->state &= ~RECVMASK;
		    cmds->state |= ARG;
		    cmds->arg   = (string *)0;
		    shift_buffer(buf);
		    continue;
		}

		if (str_cat(&cmd->cmd, buf->p, 1) == 0)
		    die_nomem();

		break;

	    case ARG:

		/* new arg */
		if (*buf->p == ' ') {
		    cmds->arg = (string *)0;
		    shift_buffer(buf);
		    continue;
		}

		/* alloc new arg */
		if (cmds->arg == (string *)0)
		    cmds->arg = command_add_arg(cmd);

		if (str_cat(cmds->arg, buf->p, 1) == 0)
		    die_nomem();

		break;
	}

	/* buf << 1 */
	shift_buffer(buf);
    }

    return(AGAIN);
}

int
command_payload(commands *cmds) {
 long int		len;
 command		*cmd = cmds->tmp;
 string			*buf = &cmds->buf;

    if ((cmds->state & COMMAND) == 0 &&	(cmds->state & PAYLOAD) == 0) {
	/* not ready */
	return(ROK);
    }

    /* alloc payload */
    if (cmds->state & COMMAND) {

	/* get payload size */
	cmds->payload_size = has_payload(cmd);
	if (cmds->payload_size == 0)
	    /* no payload */
	    return(ROK);

	if (str_ready(&cmd->payload, cmds->payload_size + 1) == 0)
	    die_nomem();

	cmds->state &= ~RECVMASK;
	cmds->state |= PAYLOAD;
    }

    while (cmds->payload_size) {

	if (buf->len == 0)
	    /* again */
	    return(AGAIN);

	if (cmds->payload_size < buf->len)
	    len = cmds->payload_size;
	else
	    len = buf->len;

	if (str_cat(&cmd->payload, buf->p, len) == 0)
	    die_nomem();

	cmds->payload_size -= len;
	buf->len -= len;
	buf->p   += len;
    }

    /* end of payload */
    cmds->state &= ~RECVMASK;
    cmds->state |= COMMAND;
    return(ROK);
}

int
read_command(commands *cmds, int fd, void (*sched_write)(), void *ev_write) {
 log_			*log = &config.log;

    for (;;) {

	/* fill buffer */
	switch (command_fill_buffer(fd, &cmds->buf)) {

	    case 0:
		/* connection closed by peer */
		return(END);

	    case -1:
		/* error */
		return(RFAIL);

	    case -2:
		/* schedule another read */
		return(AGAIN);

	}

#ifdef CMDDEBUG
	string slog;
	slog.s = cmds->buf.p;
	slog.len = cmds->buf.len;
	log->debug("command read buf: [%s]\n", &slog);
#endif

	/* decode command */
	if (command_prepare(cmds) == AGAIN) {
	    /* schedule another read */
	    return(AGAIN);
	}

	/* oops - empty commands are not allowed */
	if (cmds->tmp->cmd.len == 0) {
	    log->debug("null command buf: [%s]\n", &cmds->buf);
	    return(RFAIL);
	}

	/* TrID */
	/* has_trid(cmds->tmp); */

	/* payload */
	if (command_payload(cmds) == AGAIN) {
	    /* schedule another read */
	    return(AGAIN);
	}

	/* end of command */
	/* add command to queue */
	commands_add_command(cmds, cmds->tmp);
	cmds->tmp = (command *)0;

	/*
	 * command read ok - schedule write to the client
	 */
	sched_write(ev_write);

	/* done */
	if (cmds->buf.len == 0)
	    return(ROK);
    }
}

void
print_command(command *cmd) {
 log_ 			*log = &config.log;
 string			**arg;

    log->debug("%s ", &cmd->cmd);

    arg = cmd->args;
    while (arg && *arg) {

	log->debug("%s ", *arg);
	++arg;
    }

    log->debug("\npayload: [%s]\n", &cmd->payload);
}

void
print_client_command(string *email, command *cmd) {
 log_ 			*log = &config.log;

    if (email)
	log->debug("[%s] from client: ", email);
    else
	log->debug("(unknown) from client: ");
    print_command(cmd);
}

void
print_server_command(string *email, command *cmd) {
 log_ 			*log = &config.log;

    if (email)
	log->debug("[%s] from server: ", email);
    else
	log->debug("(unknown) from server: ");
    print_command(cmd);
}

void
free_command_args(command *cmd) {
 string		*arg;

    while (cmd->args_len > 0 && cmd->args) {
	if ((arg = cmd->args[cmd->args_len - 1])) {
	    str_free(arg);
	    free(arg);
	}
	cmd->args_len--;
    }
    if (cmd->args)
	free(cmd->args);
}

void
free_command(command *cmd) {
    str_free(&cmd->cmd);
    str_free(&cmd->obuf);
    str_free(&cmd->payload);
    free_command_args(cmd);
    free(cmd);
}

int
send_command(int tofd, command *cmd, string *email, const char *to) {
 log_ 			*log = &config.log;
 string			**arg = cmd->args;

    switch (cmd->fase) {

	case 0:

	    /* put command on buffer */
	    str_zero(&cmd->obuf);
	    if (str_copy(&cmd->obuf, cmd->cmd.s, cmd->cmd.len) == 0)
		die_nomem();

	    /* send args */
	    while (arg && *arg) {

		if (str_cat(&cmd->obuf, (unsigned char *)" ", 1) == 0)
		    die_nomem();
		if (str_cat(&cmd->obuf, (*arg)->s, (*arg)->len) == 0)
		    die_nomem();

		++arg;
	    }

	    /* end of line */
	    if (str_cat(&cmd->obuf, (unsigned char *)"\r\n", 2) == 0)
		die_nomem();

	    if (cmd->payload.len > 0) {
		if (str_cat(&cmd->obuf, cmd->payload.s, cmd->payload.len) == 0)
		    die_nomem();
	    }

	    cmd->fase = 1;
	    /* fallthrough */

	case 1:

	    /* write command */
	    switch (io_write(tofd, (char *)cmd->obuf.s, cmd->obuf.len)) {

		case -2:
		    return(AGAIN);

		case -1:
		    log->debug("write fail: [%S] buf: [%s][%d]\n", strerror(errno),
				&cmd->obuf, cmd->obuf.len);
		    str_free(&cmd->obuf);
		    cmd->fase = 0;
		    return(RFAIL);

		default: break;
	    }

	    cmd->fase = 2;
	    /* fallthrough */

	case 2:

	    log->protocol("[%s] send to %S ==> %s", email, to, &cmd->obuf);
	    log->protocol("\n");
	    str_free(&cmd->obuf);
	    cmd->fase = 0;
    }

    return(ROK);
}

void
commands_add_command(commands *cmds, command *cmd) {
    /* tailq add */
    if (cmds->cmd_last)
	cmds->cmd_last->next = cmd;
    else
	cmds->cmd = cmd;

    /* update last */
    cmds->cmd_last = cmd;
}

void
commands_init(commands *cmds) {
    /* alloc buffer */
    if (str_ready(&cmds->buf, CMD_BUF) == 0)
	die_nomem();

    /* zero everything */
    memset(cmds->buf.s, 0, cmds->buf.a);
    cmds->state = COMMAND;
}
