/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*  Camel
 *  Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 *  Authors: Jeffrey Stedfast <fejj@novell.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#include <camel/camel-debug.h>

#include "camel-imap4-specials.h"
#include "camel-imap4-stream.h"

#define d(x) (camel_debug ("imap4:stream") ? (x) : 0)

#define IMAP4_TOKEN_LEN  128

static void camel_imap4_stream_class_init (CamelIMAP4StreamClass *klass);
static void camel_imap4_stream_init (CamelIMAP4Stream *stream, CamelIMAP4StreamClass *klass);
static void camel_imap4_stream_finalize (CamelObject *object);

static ssize_t stream_read (CamelStream *stream, char *buffer, size_t n);
static ssize_t stream_write (CamelStream *stream, const char *buffer, size_t n);
static int stream_flush  (CamelStream *stream);
static int stream_close  (CamelStream *stream);
static gboolean stream_eos (CamelStream *stream);


static CamelStreamClass *parent_class = NULL;


CamelType
camel_imap4_stream_get_type (void)
{
	static CamelType type = 0;
	
	if (!type) {
		type = camel_type_register (CAMEL_STREAM_TYPE,
					    "CamelIMAP4Stream",
					    sizeof (CamelIMAP4Stream),
					    sizeof (CamelIMAP4StreamClass),
					    (CamelObjectClassInitFunc) camel_imap4_stream_class_init,
					    NULL,
					    (CamelObjectInitFunc) camel_imap4_stream_init,
					    (CamelObjectFinalizeFunc) camel_imap4_stream_finalize);
	}
	
	return type;
}

static void
camel_imap4_stream_class_init (CamelIMAP4StreamClass *klass)
{
	CamelStreamClass *stream_class = (CamelStreamClass *) klass;
	
	parent_class = (CamelStreamClass *) camel_type_get_global_classfuncs (CAMEL_STREAM_TYPE);
	
	/* virtual method overload */
	stream_class->read = stream_read;
	stream_class->write = stream_write;
	stream_class->flush = stream_flush;
	stream_class->close = stream_close;
	stream_class->eos = stream_eos;
}

static void
camel_imap4_stream_init (CamelIMAP4Stream *imap4, CamelIMAP4StreamClass *klass)
{
	imap4->stream = NULL;
	
	imap4->mode = CAMEL_IMAP4_STREAM_MODE_TOKEN;
	imap4->disconnected = FALSE;
	imap4->have_unget = FALSE;
	imap4->eol = FALSE;
	
	imap4->literal = 0;
	
	imap4->inbuf = imap4->realbuf + IMAP4_READ_PRELEN;
	imap4->inptr = imap4->inbuf;
	imap4->inend = imap4->inbuf;
	
	imap4->tokenbuf = g_malloc (IMAP4_TOKEN_LEN);
	imap4->tokenptr = imap4->tokenbuf;
	imap4->tokenleft = IMAP4_TOKEN_LEN;
}

static void
camel_imap4_stream_finalize (CamelObject *object)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) object;
	
	if (imap4->stream)
		camel_object_unref (imap4->stream);
	
	g_free (imap4->tokenbuf);
}


static ssize_t
imap4_fill (CamelIMAP4Stream *imap4)
{
	unsigned char *inbuf, *inptr, *inend;
	ssize_t nread;
	size_t inlen;
	
	if (imap4->disconnected) {
		errno = EINVAL;
		return -1;
	}
	
	inbuf = imap4->inbuf;
	inptr = imap4->inptr;
	inend = imap4->inend;
	inlen = inend - inptr;
	
	g_assert (inptr <= inend);
	
	/* attempt to align 'inend' with realbuf + SCAN_HEAD */
	if (inptr >= inbuf) {
		inbuf -= inlen < IMAP4_READ_PRELEN ? inlen : IMAP4_READ_PRELEN;
		memmove (inbuf, inptr, inlen);
		inptr = inbuf;
		inbuf += inlen;
	} else if (inptr > imap4->realbuf) {
		size_t shift;
		
		shift = MIN (inptr - imap4->realbuf, inend - inbuf);
		memmove (inptr - shift, inptr, inlen);
		inptr -= shift;
		inbuf = inptr + inlen;
	} else {
		/* we can't shift... */
		inbuf = inend;
	}
	
	imap4->inptr = inptr;
	imap4->inend = inbuf;
	inend = imap4->realbuf + IMAP4_READ_PRELEN + IMAP4_READ_BUFLEN - 1;
	
	if ((nread = camel_stream_read (imap4->stream, inbuf, inend - inbuf)) == -1)
		return -1;
	else if (nread == 0)
		imap4->disconnected = TRUE;
	
	imap4->inend += nread;
	
	return imap4->inend - imap4->inptr;
}

static ssize_t
stream_read (CamelStream *stream, char *buffer, size_t n)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) stream;
	ssize_t len, nread = 0;
	
	if (imap4->mode == CAMEL_IMAP4_STREAM_MODE_LITERAL) {
		/* don't let our caller read past the end of the literal */
		n = MIN (n, imap4->literal);
	}
	
	if (imap4->inptr < imap4->inend) {
		len = MIN (n, imap4->inend - imap4->inptr);
		memcpy (buffer, imap4->inptr, len);
		imap4->inptr += len;
		nread = len;
	}
	
	if (nread < n) {
		if ((len = camel_stream_read (imap4->stream, buffer + nread, n - nread)) == 0)
			imap4->disconnected = TRUE;
		else if (len == -1)
			return -1;
		
		nread += len;
	}
	
	if (imap4->mode == CAMEL_IMAP4_STREAM_MODE_LITERAL) {
		imap4->literal -= nread;
		
		if (imap4->literal == 0) {
			imap4->mode = CAMEL_IMAP4_STREAM_MODE_TOKEN;
			imap4->eol = TRUE;
		}
	}
	
	return nread;
}

static ssize_t
stream_write (CamelStream *stream, const char *buffer, size_t n)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) stream;
	ssize_t nwritten;
	
	if (imap4->disconnected) {
		errno = EINVAL;
		return -1;
	}
	
	if ((nwritten = camel_stream_write (imap4->stream, buffer, n)) == 0)
		imap4->disconnected = TRUE;
	
	return nwritten;
}

static int
stream_flush (CamelStream *stream)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) stream;
	
	return camel_stream_flush (imap4->stream);
}

static int
stream_close (CamelStream *stream)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) stream;
	
	if (camel_stream_close (imap4->stream) == -1)
		return -1;
	
	camel_object_unref (imap4->stream);
	imap4->stream = NULL;
	
	imap4->disconnected = TRUE;
	
	return 0;
}

static gboolean
stream_eos (CamelStream *stream)
{
	CamelIMAP4Stream *imap4 = (CamelIMAP4Stream *) stream;
	
	if (imap4->eol)
		return TRUE;
	
	if (imap4->disconnected && imap4->inptr == imap4->inend)
		return TRUE;
	
	if (camel_stream_eos (imap4->stream))
		return TRUE;
	
	return FALSE;
}


/**
 * camel_imap4_stream_new:
 * @stream: tcp stream
 *
 * Returns: a new imap4 stream
 **/
CamelStream *
camel_imap4_stream_new (CamelStream *stream)
{
	CamelIMAP4Stream *imap4;
	
	g_return_val_if_fail (CAMEL_IS_STREAM (stream), NULL);
	
	imap4 = (CamelIMAP4Stream *) camel_object_new (CAMEL_TYPE_IMAP4_STREAM);
	camel_object_ref (stream);
	imap4->stream = stream;
	
	return (CamelStream *) imap4;
}



#define token_save(imap4, start, len) G_STMT_START {                         \
	if (imap4->tokenleft <= len) {                                       \
		unsigned int tlen, toff;                                    \
		                                                            \
		tlen = toff = imap4->tokenptr - imap4->tokenbuf;              \
		tlen = tlen ? tlen : 1;                                     \
		                                                            \
		while (tlen < toff + len)                                   \
			tlen <<= 1;                                         \
		                                                            \
		imap4->tokenbuf = g_realloc (imap4->tokenbuf, tlen + 1);      \
		imap4->tokenptr = imap4->tokenbuf + toff;                     \
		imap4->tokenleft = tlen - toff;                              \
	}                                                                   \
	                                                                    \
	memcpy (imap4->tokenptr, start, len);                                \
	imap4->tokenptr += len;                                              \
	imap4->tokenleft -= len;                                             \
} G_STMT_END

#define token_clear(imap4) G_STMT_START {                                    \
	imap4->tokenleft += imap4->tokenptr - imap4->tokenbuf;                 \
	imap4->tokenptr = imap4->tokenbuf;                                    \
	imap4->literal = 0;                                                  \
} G_STMT_END


/**
 * camel_imap4_stream_next_token:
 * @stream: imap4 stream
 * @token: imap4 token
 *
 * Reads the next token from the imap4 stream and saves it in @token.
 *
 * Returns: 0 on success or -1 on fail.
 **/
int
camel_imap4_stream_next_token (CamelIMAP4Stream *stream, camel_imap4_token_t *token)
{
	register unsigned char *inptr;
	unsigned char *inend, *start, *p;
	gboolean escaped = FALSE;
	size_t literal = 0;
	guint32 nz_number;
	int ret;
	
	g_return_val_if_fail (CAMEL_IS_IMAP4_STREAM (stream), -1);
	g_return_val_if_fail (stream->mode != CAMEL_IMAP4_STREAM_MODE_LITERAL, -1);
	g_return_val_if_fail (token != NULL, -1);
	
	if (stream->have_unget) {
		memcpy (token, &stream->unget, sizeof (camel_imap4_token_t));
		stream->have_unget = FALSE;
		return 0;
	}
	
	token_clear (stream);
	
	inptr = stream->inptr;
	inend = stream->inend;
	*inend = '\0';
	
	do {
		if (inptr == inend) {
			if ((ret = imap4_fill (stream)) < 0) {
				token->token = CAMEL_IMAP4_TOKEN_ERROR;
				return -1;
			} else if (ret == 0) {
				token->token = CAMEL_IMAP4_TOKEN_NO_DATA;
				return 0;
			}
			
			inptr = stream->inptr;
			inend = stream->inend;
			*inend = '\0';
		}
		
		while (*inptr == ' ' || *inptr == '\r')
			inptr++;
	} while (inptr == inend);
	
	do {
		if (inptr < inend) {
			if (*inptr == '"') {
				/* qstring token */
				escaped = FALSE;
				start = inptr;
				
				/* eat the beginning " */
				inptr++;
				
				p = inptr;
				while (inptr < inend) {
					if (*inptr == '"' && !escaped)
						break;
					
					if (*inptr == '\\' && !escaped) {
						token_save (stream, p, inptr - p);
						escaped = TRUE;
						inptr++;
						p = inptr;
					} else {
						inptr++;
						escaped = FALSE;
					}
				}
				
				token_save (stream, p, inptr - p);
				
				if (inptr == inend) {
					stream->inptr = start;
					goto refill;
				}
				
				/* eat the ending " */
				inptr++;
				
				/* nul-terminate the atom token */
				token_save (stream, "", 1);
				
				token->token = CAMEL_IMAP4_TOKEN_QSTRING;
				token->v.qstring = stream->tokenbuf;
				
				d(fprintf (stderr, "token: \"%s\"\n", token->v.qstring));
				
				break;
			} else if (strchr ("+*()[]\n", *inptr)) {
				/* special character token */
				token->token = *inptr++;
				
				if (camel_debug ("imap4:stream")) {
					if (token->token != '\n')
						fprintf (stderr, "token: %c\n", token->token);
					else
						fprintf (stderr, "token: \\n\n");
				}
				
				break;
			} else if (*inptr == '{') {
				/* literal identifier token */
				if ((p = strchr (inptr, '}')) && strchr (p, '\n')) {
					inptr++;
					
					while (isdigit ((int) *inptr) && literal < UINT_MAX / 10)
						literal = (literal * 10) + (*inptr++ - '0');
					
					if (*inptr != '}') {
						if (isdigit ((int) *inptr))
							g_warning ("illegal literal identifier: literal too large");
						else if (*inptr != '+')
							g_warning ("illegal literal identifier: garbage following size");
						
						while (*inptr != '}')
							inptr++;
					}
					
					/* skip over '}' */
					inptr++;
					
					/* skip over any trailing whitespace */
					while (*inptr == ' ' || *inptr == '\r')
						inptr++;
					
					if (*inptr != '\n') {
						g_warning ("illegal token following literal identifier: %s", inptr);
						
						/* skip ahead to the eoln */
						if (!(inptr = strchr (inptr, '\n'))) {
							stream->inptr = start;
							goto refill;
						}
					}
					
					/* skip over '\n' */
					inptr++;
					
					token->token = CAMEL_IMAP4_TOKEN_LITERAL;
					token->v.literal = literal;
					
					d(fprintf (stderr, "token: {%u}\n", literal));
					
					stream->mode = CAMEL_IMAP4_STREAM_MODE_LITERAL;
					stream->literal = literal;
					stream->eol = FALSE;
					
					break;
				} else {
					stream->inptr = inptr;
					goto refill;
				}
			} else if (*inptr >= '0' && *inptr <= '9') {
				/* number token */
				*inend = '\0';
				nz_number = strtoul ((char *) inptr, (char **) &start, 10);
				if (start == inend)
					goto refill;
				
				if (*start == ':' || *start == ',') {
					/* workaround for 'set' tokens (APPENDUID / COPYUID) */
					goto atom_token;
				}
				
				inptr = start;
				token->token = CAMEL_IMAP4_TOKEN_NUMBER;
				token->v.number = nz_number;
				
				d(fprintf (stderr, "token: %u\n", nz_number));
				
				break;
			} else if (is_atom (*inptr)) {
			atom_token:
				/* simple atom token */
				start = inptr;
				
				while (inptr < inend && is_atom (*inptr))
					inptr++;
				
				if (inptr == inend) {
					stream->inptr = start;
					goto refill;
				}
				
				token_save (stream, start, inptr - start);
				
				/* nul-terminate the atom token */
				token_save (stream, "", 1);
				
				if (!strcmp (stream->tokenbuf, "NIL")) {
					/* special atom token */
					token->token = CAMEL_IMAP4_TOKEN_NIL;
					d(fprintf (stderr, "token: NIL\n"));
				} else {
					token->token = CAMEL_IMAP4_TOKEN_ATOM;
					token->v.atom = stream->tokenbuf;
					d(fprintf (stderr, "token: %s\n", token->v.atom));
				}
				
				break;
			} else if (*inptr == '\\') {
				/* possible flag token ("\" atom) */
				start = inptr++;
				
				while (inptr < inend && is_atom (*inptr))
					inptr++;
				
				if (inptr == inend) {
					stream->inptr = start;
					goto refill;
				}
				
				/* handle the \* case */
				if ((inptr - start) == 1 && *inptr == '*')
					inptr++;
				
				if ((inptr - start) > 1) {
					token_save (stream, start, inptr - start);
					
					/* nul-terminate the flag token */
					token_save (stream, "", 1);
					
					token->token = CAMEL_IMAP4_TOKEN_FLAG;
					token->v.atom = stream->tokenbuf;
					d(fprintf (stderr, "token: %s\n", token->v.atom));
				} else {
					token->token = '\\';
					d(fprintf (stderr, "token: %c\n", token->token));
				}
				break;
			} else if (is_lwsp (*inptr)) {
				inptr++;
			} else {
				/* unknown character token? */
				token->token = *inptr++;
				d(fprintf (stderr, "token: %c\n", token->token));
				break;
			}
		} else {
		refill:
			token_clear (stream);
			
			if (imap4_fill (stream) <= 0) {
				token->token = CAMEL_IMAP4_TOKEN_ERROR;
				return -1;
			}
			
			inptr = stream->inptr;
			inend = stream->inend;
			*inend = '\0';
		}
	} while (inptr < inend);
	
	stream->inptr = inptr;
	
	return 0;
}


/**
 * camel_imap4_stream_unget_token:
 * @stream: imap4 stream
 * @token: token to 'unget'
 *
 * Ungets an imap4 token (as in ungetc()).
 *
 * Note: you may *ONLY* unget a single token. Trying to unget another
 * token will fail.
 *
 * Returns: 0 on success or -1 on fail.
 **/
int
camel_imap4_stream_unget_token (CamelIMAP4Stream *stream, camel_imap4_token_t *token)
{
	if (stream->have_unget)
		return -1;
	
	if (token->token != CAMEL_IMAP4_TOKEN_NO_DATA) {
		memcpy (&stream->unget, token, sizeof (camel_imap4_token_t));
		stream->have_unget = TRUE;
	}
	
	return 0;
}


/**
 * camel_imap4_stream_line:
 * @stream: imap4 stream
 * @line: line pointer
 * @len: line length
 *
 * Reads a single line from the imap4 stream and points @line at an
 * internal buffer containing the line read and sets @len to the
 * length of the line buffer.
 *
 * Returns: -1 on error, 0 if the line read is complete, or 1 if the
 * read is incomplete.
 **/
int
camel_imap4_stream_line (CamelIMAP4Stream *stream, unsigned char **line, size_t *len)
{
	register unsigned char *inptr;
	unsigned char *inend;
	
	g_return_val_if_fail (CAMEL_IS_IMAP4_STREAM (stream), -1);
	g_return_val_if_fail (stream->mode != CAMEL_IMAP4_STREAM_MODE_LITERAL, -1);
	g_return_val_if_fail (line != NULL, -1);
	g_return_val_if_fail (len != NULL, -1);
	
	inptr = stream->inptr;
	inend = stream->inend;
	
	*inend = '\0';
	if (inptr == inend || ((inend - inptr) < 2 && *inptr != '\n')) {
		if (imap4_fill (stream) == -1 && stream->inptr == stream->inend)
			return -1;
	}
	
	*line = stream->inptr;
	inptr = stream->inptr;
	inend = stream->inend;
	*inend = '\n';
	
	while (*inptr != '\n')
		inptr++;
	
	*len = (inptr - stream->inptr);
	
	if (inptr > stream->inptr && inptr[-1] == '\r')
		inptr[-1] = '\0';
	
	if (inptr < inend) {
		/* got the eoln */
		inptr[0] = '\0';
		*len += 1;
		
		stream->inptr = inptr + 1;
		
		return 0;
	}
	
	stream->inptr = inptr;
	
	return 1;
}


/**
 * camel_imap4_stream_literal:
 * @stream: IMAP stream
 * @literal: literal pointer
 * @len: literal length
 *
 * Sets @literal to the beginning of the next chunk of the literal
 * buffer from the IMAP stream and sets @len to the length of the
 * @literal buffer.
 *
 * Returns: >0 if more literal data exists, 0 if the end of the literal
 * has been reached or -1 on fail.
 **/
int
camel_imap4_stream_literal (CamelIMAP4Stream *stream, unsigned char **literal, size_t *len)
{
	unsigned char *inptr, *inend;
	size_t nread;
	
	g_return_val_if_fail (CAMEL_IS_IMAP4_STREAM (stream), -1);
	g_return_val_if_fail (stream->mode == CAMEL_IMAP4_STREAM_MODE_LITERAL, -1);
	g_return_val_if_fail (literal != NULL, -1);
	g_return_val_if_fail (len != NULL, -1);
	
	if (stream->eol) {
		*len = 0;
		return 0;
	}
	
	if ((stream->inend - stream->inptr) < 1) {
		/* keep our buffer full to the optimal size */
		if (imap4_fill (stream) == -1 && stream->inptr == stream->inend)
			return -1;
	}
	
	*literal = inptr = stream->inptr;
	inend = stream->inend;
	if ((inend - inptr) > stream->literal)
		inend = inptr + stream->literal;
	else
		inend = stream->inend;
	
	*len = nread = inend - inptr;
	
	stream->literal -= nread;
	stream->inptr += nread;
	
	if (stream->literal == 0) {
		stream->mode = CAMEL_IMAP4_STREAM_MODE_TOKEN;
		stream->eol = TRUE;
		return 0;
	}
	
	return 1;
}
