/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */

#include "imap-common.h"
#include "istream.h"
#include "ostream.h"
#include "crc32.h"
#include "mail-storage-settings.h"
#include "imap-commands.h"
#include "imap-keepalive.h"
#include "imap-sync.h"

struct cmd_idle_context {
	struct client *client;
	struct client_command_context *cmd;

	struct imap_sync_context *sync_ctx;
	struct timeout *keepalive_to, *to_hibernate;

	bool manual_cork:1;
	bool sync_pending:1;
};

static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx);
static bool cmd_idle_continue(struct client_command_context *cmd);

static void
idle_finish(struct cmd_idle_context *ctx, bool done_ok, bool free_cmd)
{
	struct client *client = ctx->client;

	timeout_remove(&ctx->keepalive_to);
	timeout_remove(&ctx->to_hibernate);

	if (ctx->sync_ctx != NULL) {
		/* we're here only in connection failure cases */
		(void)imap_sync_deinit(ctx->sync_ctx, ctx->cmd);
	}

	bool orig_corked = o_stream_is_corked(client->output);
	o_stream_cork(client->output);
	io_remove(&client->io);

	if (client->mailbox != NULL)
		mailbox_notify_changes_stop(client->mailbox);

	if (done_ok)
		client_send_tagline(ctx->cmd, "OK Idle completed.");
	else
		client_send_tagline(ctx->cmd, "BAD Expected DONE.");

	if (!orig_corked)
		o_stream_uncork(client->output);
	if (free_cmd)
		client_command_free(&ctx->cmd);
}

static bool
idle_client_handle_input(struct cmd_idle_context *ctx, bool free_cmd)
{
	const char *line;

	while ((line = i_stream_next_line(ctx->client->input)) != NULL) {
		if (ctx->client->input_skip_line)
			ctx->client->input_skip_line = FALSE;
		else {
			idle_finish(ctx, strcasecmp(line, "DONE") == 0,
				    free_cmd);
			return TRUE;
		}
	}
	return FALSE;
}

static bool idle_client_input_more(struct cmd_idle_context *ctx)
{
	struct client *client = ctx->client;

	client->last_input = ioloop_time;
	timeout_reset(client->to_idle);

	switch (i_stream_read(client->input)) {
	case -1:
		/* disconnected */
		client_disconnect(client, NULL);
		return TRUE;
	case -2:
		client->input_skip_line = TRUE;
		idle_finish(ctx, FALSE, TRUE);
		return TRUE;
	}

	if (ctx->sync_ctx != NULL) {
		/* we're still sending output to client. wait until it's all
		   sent so we don't lose any changes. */
		io_remove(&client->io);
		return FALSE;
	}

	return idle_client_handle_input(ctx, TRUE);
}

static void idle_client_input(struct cmd_idle_context *ctx)
{
	struct client *client = ctx->client;

	if (idle_client_input_more(ctx))
		client_continue_pending_input(client);
}

static void keepalive_timeout(struct cmd_idle_context *ctx)
{
	if (ctx->client->output_cmd_lock != NULL) {
		/* it's busy sending output */
		return;
	}

	if (o_stream_get_buffer_used_size(ctx->client->output) == 0) {
		/* Sending this keeps NATs/stateful firewalls alive.
		   Sending this also catches dead connections. Don't send
		   anything if there is already data waiting in output
		   buffer. */
		o_stream_cork(ctx->client->output);
		client_send_line(ctx->client, "* OK Still here");
		o_stream_uncork(ctx->client->output);
	}
	/* Make sure idling connections don't get disconnected. There are
	   several clients that really want to IDLE forever and there's not
	   much harm in letting them do so. */
	timeout_reset(ctx->client->to_idle);
	/* recalculate time for the next keepalive timeout */
	idle_add_keepalive_timeout(ctx);
}

static bool idle_sync_now(struct mailbox *box, struct cmd_idle_context *ctx)
{
	i_assert(ctx->sync_ctx == NULL);

	/* hibernation can't happen while sync is running.
	   the timeout is added back afterwards. */
	timeout_remove(&ctx->to_hibernate);

	ctx->sync_pending = FALSE;
	ctx->sync_ctx = imap_sync_init(ctx->client, box, 0, 0, ctx->cmd->utf8);
	return cmd_idle_continue(ctx->cmd);
}

static void idle_callback(struct mailbox *box, struct cmd_idle_context *ctx)
{
	struct client *client = ctx->client;

	if (ctx->sync_ctx != NULL)
		ctx->sync_pending = TRUE;
	else {
		ctx->manual_cork = TRUE;
		(void)idle_sync_now(box, ctx);
		if (client->disconnected)
			client_destroy(client, NULL);
	}
}

static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx)
{
	struct client *client = ctx->client;
	unsigned int interval = client->set->imap_idle_notify_interval;

	if (interval == 0)
		return;

	interval = imap_keepalive_interval_msecs(client->user->username,
						 client->user->conn.remote_ip,
						 interval);

	timeout_remove(&ctx->keepalive_to);
	ctx->keepalive_to = timeout_add(interval, keepalive_timeout, ctx);
}

static void idle_hibernate_timeout(struct cmd_idle_context *ctx)
{
	struct client *client = ctx->client;
	const char *reason;

	i_assert(ctx->sync_ctx == NULL);
	i_assert(!ctx->sync_pending);

	if (imap_client_hibernate(&client, &reason)) {
		/* client may be destroyed now */
	} else {
		/* failed - don't bother retrying */
		timeout_remove(&ctx->to_hibernate);
	}
}

static void idle_add_hibernate_timeout(struct cmd_idle_context *ctx)
{
	unsigned int secs = ctx->client->set->imap_hibernate_timeout;

	i_assert(ctx->to_hibernate == NULL);

	if (secs == 0)
		return;

	ctx->to_hibernate =
		timeout_add(secs * 1000, idle_hibernate_timeout, ctx);
}

static bool cmd_idle_continue(struct client_command_context *cmd)
{
	struct client *client = cmd->client;
	struct cmd_idle_context *ctx = cmd->context;
	uoff_t orig_offset = client->output->offset;

	if (cmd->cancel) {
		idle_finish(ctx, FALSE, FALSE);
		return TRUE;
	}

	if (ctx->to_hibernate != NULL)
		timeout_reset(ctx->to_hibernate);

	if (ctx->manual_cork)  {
		/* we're coming from idle_callback instead of a normal
		   I/O handler, so we'll have to do corking manually */
		o_stream_cork(client->output);
	}

	if (ctx->sync_ctx != NULL) {
		if (imap_sync_more(ctx->sync_ctx) == 0) {
			/* unfinished */
			if (ctx->manual_cork) {
				ctx->manual_cork = FALSE;
				o_stream_uncork(client->output);
			}
			cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
			return FALSE;
		}

		if (imap_sync_deinit(ctx->sync_ctx, ctx->cmd) < 0) {
			client_send_untagged_storage_error(client,
				mailbox_get_storage(client->mailbox));
			mailbox_notify_changes_stop(client->mailbox);
		}
		ctx->sync_ctx = NULL;
	}
	if (client->output->offset != orig_offset &&
	    ctx->keepalive_to != NULL)
		idle_add_keepalive_timeout(ctx);

	if (ctx->sync_pending) {
		/* more changes occurred while we were sending changes to
		   client.

		   NOTE: this recurses back to this function,
		   so we return here instead of doing everything twice. */
		return idle_sync_now(client->mailbox, ctx);
	}
	if (ctx->to_hibernate == NULL)
		idle_add_hibernate_timeout(ctx);
	cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;

	if (ctx->manual_cork) {
		ctx->manual_cork = FALSE;
		o_stream_uncork(client->output);
	}

	if (client->output->closed) {
		idle_finish(ctx, FALSE, FALSE);
		return TRUE;
	}
	if (client->io == NULL) {
		/* input is pending. add the io back and mark the input as
		   pending. we can't safely read more input immediately here. */
		client->io = io_add_istream(client->input,
					    idle_client_input, ctx);
		i_stream_set_input_pending(client->input, TRUE);
	}
	return FALSE;
}

bool cmd_idle(struct client_command_context *cmd)
{
	struct client *client = cmd->client;
	struct cmd_idle_context *ctx;

	ctx = p_new(cmd->pool, struct cmd_idle_context, 1);
	ctx->cmd = cmd;
	ctx->client = client;
	idle_add_keepalive_timeout(ctx);
	idle_add_hibernate_timeout(ctx);

	if (client->mailbox != NULL)
		mailbox_notify_changes(client->mailbox, idle_callback, ctx);
	if (!client->state_import_idle_continue)
		client_send_line(client, "+ idling");
	else {
		/* continuing an IDLE after hibernation */
		client->state_import_idle_continue = FALSE;
	}

	io_remove(&client->io);
	client->io = io_add_istream(client->input, idle_client_input, ctx);

	cmd->func = cmd_idle_continue;
	cmd->context = ctx;

	/* check immediately if there are changes. if they came before we
	   added mailbox-notifier, we wouldn't see them otherwise. */
	if (client->mailbox != NULL) {
		if (idle_sync_now(client->mailbox, ctx))
			return TRUE;
	}
	return idle_client_handle_input(ctx, FALSE);
}
