/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; fill-column: 160 -*-
 *
 * Author:
 *  Michael Zucchi <notzed@ximian.com>
 *
 * Copyright 1999, 2000 Ximian, Inc. (www.ximian.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

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

#include <errno.h>

#include <string.h>
#include <stdio.h>

#include <glib.h>

#include "camel-pop3-engine.h"
#include "camel-pop3-stream.h"
#include "camel-service.h"
#include "camel-sasl.h"
#include "camel-i18n.h"

/* max 'outstanding' bytes in output stream, so we can't deadlock waiting
   for the server to accept our data when pipelining */
#define CAMEL_POP3_SEND_LIMIT (1024)


extern int camel_verbose_debug;
#define dd(x) (camel_verbose_debug?(x):0)

static void get_capabilities(CamelPOP3Engine *pe);

static CamelObjectClass *parent_class = NULL;

/* Returns the class for a CamelStream */
#define CS_CLASS(so) CAMEL_POP3_ENGINE_CLASS(CAMEL_OBJECT_GET_CLASS(so))

static void
camel_pop3_engine_class_init (CamelPOP3EngineClass *camel_pop3_engine_class)
{
	parent_class = camel_type_get_global_classfuncs( CAMEL_OBJECT_TYPE );
}

static void
camel_pop3_engine_init(CamelPOP3Engine *pe, CamelPOP3EngineClass *peclass)
{
	e_dlist_init(&pe->active);
	e_dlist_init(&pe->queue);
	e_dlist_init(&pe->done);
	pe->state = CAMEL_POP3_ENGINE_DISCONNECT;
}

static void
camel_pop3_engine_finalise(CamelPOP3Engine *pe)
{
	/* FIXME: Also flush/free any outstanding requests, etc */

	if (pe->stream)
		camel_object_unref(pe->stream);
}

CamelType
camel_pop3_engine_get_type (void)
{
	static CamelType camel_pop3_engine_type = CAMEL_INVALID_TYPE;

	if (camel_pop3_engine_type == CAMEL_INVALID_TYPE) {
		camel_pop3_engine_type = camel_type_register(camel_object_get_type(),
							     "CamelPOP3Engine",
							     sizeof( CamelPOP3Engine ),
							     sizeof( CamelPOP3EngineClass ),
							     (CamelObjectClassInitFunc) camel_pop3_engine_class_init,
							     NULL,
							     (CamelObjectInitFunc) camel_pop3_engine_init,
							     (CamelObjectFinalizeFunc) camel_pop3_engine_finalise );
	}

	return camel_pop3_engine_type;
}

static int
read_greeting (CamelPOP3Engine *pe)
{
	extern CamelServiceAuthType camel_pop3_password_authtype;
	extern CamelServiceAuthType camel_pop3_apop_authtype;
	unsigned char *line, *apop, *apopend;
	unsigned int len;
	
	/* first, read the greeting */
	if (camel_pop3_stream_line (pe->stream, &line, &len) == -1
	    || strncmp (line, "+OK", 3) != 0)
		return -1;
	
	if ((apop = strchr (line + 3, '<'))
	    && (apopend = strchr (apop, '>'))) {
		apopend[1] = 0;
		pe->apop = g_strdup (apop);
		pe->capa = CAMEL_POP3_CAP_APOP;
		pe->auth = g_list_append (pe->auth, &camel_pop3_apop_authtype);
	}
	
	pe->auth = g_list_prepend (pe->auth, &camel_pop3_password_authtype);
	
	return 0;
}

/**
 * camel_pop3_engine_new:
 * @source: source stream
 * @flags: engine flags
 *
 * Returns a NULL stream.  A null stream is always at eof, and
 * always returns success for all reads and writes.
 *
 * Return value: the stream
 **/
CamelPOP3Engine *
camel_pop3_engine_new(CamelStream *source, guint32 flags)
{
	CamelPOP3Engine *pe;

	pe = (CamelPOP3Engine *)camel_object_new(camel_pop3_engine_get_type ());

	pe->stream = (CamelPOP3Stream *)camel_pop3_stream_new(source);
	pe->state = CAMEL_POP3_ENGINE_AUTH;
	pe->flags = flags;
	
	if (read_greeting (pe) == -1) {
		camel_object_unref (pe);
		return NULL;
	}
	
	get_capabilities (pe);
	
	return pe;
}


/**
 * camel_pop3_engine_reget_capabilities:
 * @engine: pop3 engine
 *
 * Regets server capabilities (needed after a STLS command is issued for example).
 **/
void
camel_pop3_engine_reget_capabilities (CamelPOP3Engine *engine)
{
	g_return_if_fail (CAMEL_IS_POP3_ENGINE (engine));
	
	get_capabilities (engine);
}


/* TODO: read implementation too?
   etc? */
static struct {
	char *cap;
	guint32 flag;
} capa[] = {
	{ "APOP" , CAMEL_POP3_CAP_APOP },
	{ "TOP" , CAMEL_POP3_CAP_TOP },
	{ "UIDL", CAMEL_POP3_CAP_UIDL },
	{ "PIPELINING", CAMEL_POP3_CAP_PIPE },
	{ "STLS", CAMEL_POP3_CAP_STLS },  /* STARTTLS */
};

static void
cmd_capa(CamelPOP3Engine *pe, CamelPOP3Stream *stream, void *data)
{
	unsigned char *line, *tok, *next;
	unsigned int len;
	int ret;
	int i;
	CamelServiceAuthType *auth;

	dd(printf("cmd_capa\n"));

	do {
		ret = camel_pop3_stream_line(stream, &line, &len);
		if (ret >= 0) {
			if (strncmp(line, "SASL ", 5) == 0) {
				tok = line+5;
				dd(printf("scanning tokens '%s'\n", tok));
				while (tok) {
					next = strchr(tok, ' ');
					if (next)
						*next++ = 0;
					auth = camel_sasl_authtype(tok);
					if (auth) {
						dd(printf("got auth type '%s'\n", tok));
						pe->auth = g_list_prepend(pe->auth, auth);
					} else {
						dd(printf("unsupported auth type '%s'\n", tok));
					}
					tok = next;
				}
			} else {
				for (i=0;i<sizeof(capa)/sizeof(capa[0]);i++) {
					if (strcmp(capa[i].cap, line) == 0)
						pe->capa |= capa[i].flag;
				}
			}
		}
	} while (ret>0);
}

static void
get_capabilities(CamelPOP3Engine *pe)
{
	CamelPOP3Command *pc;
	
	if (!(pe->flags & CAMEL_POP3_ENGINE_DISABLE_EXTENSIONS)) {
		pc = camel_pop3_engine_command_new(pe, CAMEL_POP3_COMMAND_MULTI, cmd_capa, NULL, "CAPA\r\n");
		while (camel_pop3_engine_iterate(pe, pc) > 0)
			;
		camel_pop3_engine_command_free(pe, pc);
		
		if (pe->state == CAMEL_POP3_ENGINE_TRANSACTION && !(pe->capa & CAMEL_POP3_CAP_UIDL)) {
			/* check for UIDL support manually */
			pc = camel_pop3_engine_command_new (pe, CAMEL_POP3_COMMAND_SIMPLE, NULL, NULL, "UIDL 1\r\n");
			while (camel_pop3_engine_iterate (pe, pc) > 0)
				;
			
			if (pc->state == CAMEL_POP3_COMMAND_OK)
				pe->capa |= CAMEL_POP3_CAP_UIDL;
			
			camel_pop3_engine_command_free (pe, pc);
		}
	}
}

/* returns true if the command was sent, false if it was just queued */
static int
engine_command_queue(CamelPOP3Engine *pe, CamelPOP3Command *pc)
{
	if (((pe->capa & CAMEL_POP3_CAP_PIPE) == 0 || (pe->sentlen + strlen(pc->data)) > CAMEL_POP3_SEND_LIMIT)
	    && pe->current != NULL) {
		e_dlist_addtail(&pe->queue, (EDListNode *)pc);
		return FALSE;
	} else {
		/* ??? */
		if (camel_stream_write((CamelStream *)pe->stream, pc->data, strlen(pc->data)) == -1) {
			e_dlist_addtail(&pe->queue, (EDListNode *)pc);
			return FALSE;
		}

		pe->sentlen += strlen(pc->data);

		pc->state = CAMEL_POP3_COMMAND_DISPATCHED;

		if (pe->current == NULL)
			pe->current = pc;
		else
			e_dlist_addtail(&pe->active, (EDListNode *)pc);

		return TRUE;
	}
}

/* returns -1 on error (sets errno), 0 when no work to do, or >0 if work remaining */
int
camel_pop3_engine_iterate(CamelPOP3Engine *pe, CamelPOP3Command *pcwait)
{
	unsigned char *p;
	unsigned int len;
	CamelPOP3Command *pc, *pw, *pn;

	if (pcwait && pcwait->state >= CAMEL_POP3_COMMAND_OK)
		return 0;

	pc = pe->current;
	if (pc == NULL)
		return 0;

	/* LOCK */

	if (camel_pop3_stream_line(pe->stream, &pe->line, &pe->linelen) == -1)
		goto ioerror;

	p = pe->line;
	switch (p[0]) {
	case '+':
		dd(printf("Got + response\n"));
		if (pc->flags & CAMEL_POP3_COMMAND_MULTI) {
			pc->state = CAMEL_POP3_COMMAND_DATA;
			camel_pop3_stream_set_mode(pe->stream, CAMEL_POP3_STREAM_DATA);

			if (pc->func)
				pc->func(pe, pe->stream, pc->func_data);

			/* Make sure we get all data before going back to command mode */
			while (camel_pop3_stream_getd(pe->stream, &p, &len) > 0)
				;
			camel_pop3_stream_set_mode(pe->stream, CAMEL_POP3_STREAM_LINE);
		} else {
			pc->state = CAMEL_POP3_COMMAND_OK;
		}
		break;
	case '-':
		pc->state = CAMEL_POP3_COMMAND_ERR;
		break;
	default:
		/* what do we do now?  f'knows! */
		g_warning("Bad server response: %s\n", p);
		pc->state = CAMEL_POP3_COMMAND_ERR;
		break;
	}

	e_dlist_addtail(&pe->done, (EDListNode *)pc);
	pe->sentlen -= strlen(pc->data);

	/* Set next command */
	pe->current = (CamelPOP3Command *)e_dlist_remhead(&pe->active);
	
	/* check the queue for sending any we can now send also */
	pw = (CamelPOP3Command *)pe->queue.head;
	pn = pw->next;
	while (pn) {
		if (((pe->capa & CAMEL_POP3_CAP_PIPE) == 0 || (pe->sentlen + strlen(pw->data)) > CAMEL_POP3_SEND_LIMIT)
		    && pe->current != NULL)
			break;

		if (camel_stream_write((CamelStream *)pe->stream, pw->data, strlen(pw->data)) == -1)
			goto ioerror;

		e_dlist_remove((EDListNode *)pw);

		pe->sentlen += strlen(pw->data);
		pw->state = CAMEL_POP3_COMMAND_DISPATCHED;

		if (pe->current == NULL)
			pe->current = pw;
		else
			e_dlist_addtail(&pe->active, (EDListNode *)pw);

		pw = pn;
		pn = pn->next;
	}

	/* UNLOCK */

	if (pcwait && pcwait->state >= CAMEL_POP3_COMMAND_OK)
		return 0;

	return pe->current==NULL?0:1;
ioerror:
	/* we assume all outstanding commands are gunna fail now */
	while ( (pw = (CamelPOP3Command*)e_dlist_remhead(&pe->active)) ) {
		pw->state = CAMEL_POP3_COMMAND_ERR;
		e_dlist_addtail(&pe->done, (EDListNode *)pw);
	}

	while ( (pw = (CamelPOP3Command*)e_dlist_remhead(&pe->queue)) ) {
		pw->state = CAMEL_POP3_COMMAND_ERR;
		e_dlist_addtail(&pe->done, (EDListNode *)pw);
	}

	if (pe->current) {
		pe->current->state = CAMEL_POP3_COMMAND_ERR;
		e_dlist_addtail(&pe->done, (EDListNode *)pe->current);
		pe->current = NULL;
	}

	return -1;
}

CamelPOP3Command *
camel_pop3_engine_command_new(CamelPOP3Engine *pe, guint32 flags, CamelPOP3CommandFunc func, void *data, const char *fmt, ...)
{
	CamelPOP3Command *pc;
	va_list ap;

	pc = g_malloc0(sizeof(*pc));
	pc->func = func;
	pc->func_data = data;
	pc->flags = flags;
	
	va_start(ap, fmt);
	pc->data = g_strdup_vprintf(fmt, ap);
	pc->state = CAMEL_POP3_COMMAND_IDLE;

	/* TODO: what about write errors? */
	engine_command_queue(pe, pc);

	return pc;
}

void
camel_pop3_engine_command_free(CamelPOP3Engine *pe, CamelPOP3Command *pc)
{
	if (pe->current != pc)
		e_dlist_remove((EDListNode *)pc);
	g_free(pc->data);
	g_free(pc);
}
