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

#include "lib.h"
#include "ioloop.h"
#include "istream.h"
#include "bsearch-insert-pos.h"
#include "str.h"
#include "sort.h"
#include "strnum.h"
#include "index-mail.h"
#include "pop3c-client.h"
#include "pop3c-storage.h"
#include "pop3c-sync.h"
#include "mailbox-recent-flags.h"

struct pop3c_sync_msg {
	uint32_t seq;
	const char *uidl;
};
ARRAY_DEFINE_TYPE(pop3c_sync_msg, struct pop3c_sync_msg);

int pop3c_sync_get_uidls(struct pop3c_mailbox *mbox)
{
	ARRAY_TYPE(const_string) uidls;
	struct istream *input;
	const char *error, *cline;
	char *line, *p;
	unsigned int seq, line_seq;

	if (mbox->msg_uidls != NULL)
		return 0;
	if ((pop3c_client_get_capabilities(mbox->client) &
	     POP3C_CAPABILITY_UIDL) == 0) {
		mail_storage_set_error(mbox->box.storage,
				       MAIL_ERROR_NOTPOSSIBLE,
				       "UIDLs not supported by server");
		return -1;
	}

	if (pop3c_client_cmd_stream(mbox->client, "UIDL\r\n",
				    &input, &error) < 0) {
		mailbox_set_critical(&mbox->box, "UIDL failed: %s", error);
		return -1;
	}

	mbox->uidl_pool = pool_alloconly_create("POP3 UIDLs", 1024*32);
	p_array_init(&uidls, mbox->uidl_pool, 64); seq = 0;
	while ((line = i_stream_read_next_line(input)) != NULL) {
		seq++;
		p = strchr(line, ' ');
		if (p == NULL) {
			mailbox_set_critical(&mbox->box,
				"Invalid UIDL line: %s", line);
			break;
		}
		*p++ = '\0';
		if (str_to_uint(line, &line_seq) < 0 || line_seq != seq) {
			mailbox_set_critical(&mbox->box,
				"Unexpected UIDL seq: %s != %u", line, seq);
			break;
		}

		cline = p_strdup(mbox->uidl_pool, p);
		array_push_back(&uidls, &cline);
	}
	i_stream_destroy(&input);
	if (line != NULL) {
		pool_unref(&mbox->uidl_pool);
		return -1;
	}
	if (seq == 0) {
		/* make msg_uidls non-NULL */
		array_append_zero(&uidls);
	}
	mbox->msg_uidls = array_front(&uidls);
	mbox->msg_count = seq;
	return 0;
}

int pop3c_sync_get_sizes(struct pop3c_mailbox *mbox)
{
	struct istream *input;
	const char *error;
	char *line, *p;
	unsigned int seq, line_seq;

	i_assert(mbox->msg_sizes == NULL);

	if (mbox->msg_uidls == NULL) {
		if (pop3c_sync_get_uidls(mbox) < 0)
			return -1;
	}
	if (mbox->msg_count == 0) {
		mbox->msg_sizes = i_new(uoff_t, 1);
		return 0;
	}

	if (pop3c_client_cmd_stream(mbox->client, "LIST\r\n",
				    &input, &error) < 0) {
		mailbox_set_critical(&mbox->box, "LIST failed: %s", error);
		return -1;
	}

	mbox->msg_sizes = i_new(uoff_t, mbox->msg_count); seq = 0;
	while ((line = i_stream_read_next_line(input)) != NULL) {
		if (++seq > mbox->msg_count) {
			mailbox_set_critical(&mbox->box,
				"Too much data in LIST: %s", line);
			break;
		}
		p = strchr(line, ' ');
		if (p == NULL) {
			mailbox_set_critical(&mbox->box,
				"Invalid LIST line: %s", line);
			break;
		}
		*p++ = '\0';
		if (str_to_uint(line, &line_seq) < 0 || line_seq != seq) {
			mailbox_set_critical(&mbox->box,
				"Unexpected LIST seq: %s != %u", line, seq);
			break;
		}
		if (str_to_uoff(p, &mbox->msg_sizes[seq-1]) < 0) {
			mailbox_set_critical(&mbox->box,
				"Invalid LIST size: %s", p);
			break;
		}
	}
	i_stream_destroy(&input);
	if (line != NULL) {
		i_free_and_null(mbox->msg_sizes);
		return -1;
	}
	return 0;
}

static void
pop3c_get_local_msgs(pool_t pool, ARRAY_TYPE(pop3c_sync_msg) *local_msgs,
		     uint32_t messages_count,
		     struct mail_cache_view *cache_view,
		     unsigned int cache_idx)
{
	string_t *str = t_str_new(128);
	struct pop3c_sync_msg msg;
	uint32_t seq;

	i_zero(&msg);
	for (seq = 1; seq <= messages_count; seq++) {
		str_truncate(str, 0);
		if (mail_cache_lookup_field(cache_view, str, seq,
					    cache_idx) > 0)
			msg.uidl = p_strdup(pool, str_c(str));
		msg.seq = seq;
		array_idx_set(local_msgs, seq-1, &msg);
	}
}

static void
pop3c_get_remote_msgs(ARRAY_TYPE(pop3c_sync_msg) *remote_msgs,
		      struct pop3c_mailbox *mbox)
{
	struct pop3c_sync_msg *msg;
	uint32_t seq;

	for (seq = 1; seq <= mbox->msg_count; seq++) {
		msg = array_append_space(remote_msgs);
		msg->seq = seq;
		msg->uidl = mbox->msg_uidls[seq-1];
	}
}

static int pop3c_sync_msg_uidl_cmp(const struct pop3c_sync_msg *msg1,
				   const struct pop3c_sync_msg *msg2)
{
	return null_strcmp(msg1->uidl, msg2->uidl);
}

static void
pop3c_sync_messages(struct pop3c_mailbox *mbox,
		    struct mail_index_view *sync_view,
		    struct mail_index_transaction *sync_trans,
		    struct mail_cache_view *cache_view)
{
	struct index_mailbox_context *ibox =
		INDEX_STORAGE_CONTEXT(&mbox->box);
	const struct mail_index_header *hdr;
	struct mail_cache_transaction_ctx *cache_trans;
	ARRAY_TYPE(pop3c_sync_msg) local_msgs, remote_msgs;
	const struct pop3c_sync_msg *lmsg, *rmsg;
	uint32_t seq1, seq2, next_uid;
	unsigned int lidx, ridx, lcount, rcount;
	unsigned int cache_idx = ibox->cache_fields[MAIL_CACHE_POP3_UIDL].idx;
	pool_t pool;

	i_assert(mbox->msg_uids == NULL);

	/* set our uidvalidity */
	hdr = mail_index_get_header(sync_view);
	if (hdr->uid_validity == 0) {
		uint32_t uid_validity = ioloop_time32;
		mail_index_update_header(sync_trans,
			offsetof(struct mail_index_header, uid_validity),
			&uid_validity, sizeof(uid_validity), TRUE);
	}

	pool = pool_alloconly_create(MEMPOOL_GROWING"pop3c sync", 10240);
	p_array_init(&local_msgs, pool, hdr->messages_count);
	pop3c_get_local_msgs(pool, &local_msgs, hdr->messages_count,
			     cache_view, cache_idx);
	p_array_init(&remote_msgs, pool, mbox->msg_count);
	pop3c_get_remote_msgs(&remote_msgs, mbox);

	/* sort the messages by UIDLs, because some servers reorder messages */
	array_sort(&local_msgs, pop3c_sync_msg_uidl_cmp);
	array_sort(&remote_msgs, pop3c_sync_msg_uidl_cmp);

	/* skip over existing messages with matching UIDLs and expunge the ones
	   that no longer exist in remote. */
	mbox->msg_uids = mbox->msg_count == 0 ?
		i_new(uint32_t, 1) : /* avoid malloc(0) assert */
		i_new(uint32_t, mbox->msg_count);
	cache_trans = mail_cache_get_transaction(cache_view, sync_trans);

	lmsg = array_get(&local_msgs, &lcount);
	rmsg = array_get(&remote_msgs, &rcount);
	next_uid = hdr->next_uid;
	lidx = ridx = 0;
	while (lidx < lcount || ridx < rcount) {
		uint32_t lseq = lidx < lcount ? lmsg[lidx].seq : 0;
		uint32_t rseq = ridx < rcount ? rmsg[ridx].seq : 0;
		int ret;

		if (lidx >= lcount)
			ret = 1;
		else if (ridx >= rcount || lmsg[lidx].uidl == NULL)
			ret = -1;
		else
			ret = strcmp(lmsg[lidx].uidl, rmsg[ridx].uidl);
		if (ret < 0) {
			/* message expunged in remote, or we didn't have a
			   local message's UIDL in cache. */
			mail_index_expunge(sync_trans, lseq);
			lidx++;
		} else if (ret > 0) {
			/* new message in remote */
			i_assert(mbox->msg_uids[rseq-1] == 0);
			mbox->msg_uids[rseq-1] = next_uid;
			mail_index_append(sync_trans, next_uid++, &lseq);
			mail_cache_add(cache_trans, lseq, cache_idx,
				       rmsg[ridx].uidl,
				       strlen(rmsg[ridx].uidl));
			ridx++;
		} else {
			/* UIDL matched */
			i_assert(mbox->msg_uids[rseq-1] == 0);
			mail_index_lookup_uid(sync_view, lseq,
					      &mbox->msg_uids[rseq-1]);
			lidx++;
			ridx++;
		}
	}

	/* mark the newly seen messages as recent */
	if (mail_index_lookup_seq_range(sync_view, hdr->first_recent_uid,
					hdr->next_uid, &seq1, &seq2))
		mailbox_recent_flags_set_seqs(&mbox->box, sync_view, seq1, seq2);
	pool_unref(&pool);
}

int pop3c_sync(struct pop3c_mailbox *mbox)
{
        struct mail_index_sync_ctx *index_sync_ctx;
	struct mail_index_view *sync_view, *trans_view;
	struct mail_index_transaction *sync_trans;
	struct mail_index_sync_rec sync_rec;
	struct mail_cache_view *cache_view = NULL;
	enum mail_index_sync_flags sync_flags;
	unsigned int idx;
	string_t *str;
	const char *reply;
	int ret;
	bool deletions = FALSE;

	if (pop3c_sync_get_uidls(mbox) < 0)
		return -1;

	sync_flags = index_storage_get_sync_flags(&mbox->box) |
		MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY;

	ret = mail_index_sync_begin(mbox->box.index, &index_sync_ctx,
				    &sync_view, &sync_trans, sync_flags);
	if (ret <= 0) {
		if (ret < 0)
			mailbox_set_index_error(&mbox->box);
		return ret;
	}

	if (mbox->msg_uids == NULL) {
		trans_view = mail_index_transaction_open_updated_view(sync_trans);
		cache_view = mail_cache_view_open(mbox->box.cache, trans_view);
		pop3c_sync_messages(mbox, sync_view, sync_trans, cache_view);
	}

	/* mark expunges messages as deleted in this pop3 session,
	   if those exist */
	str = t_str_new(32);
	while (mail_index_sync_next(index_sync_ctx, &sync_rec)) {
		if (sync_rec.type != MAIL_INDEX_SYNC_TYPE_EXPUNGE)
			continue;

		if (!bsearch_insert_pos(&sync_rec.uid1, mbox->msg_uids,
					mbox->msg_count, sizeof(uint32_t),
					uint32_cmp, &idx)) {
			/* no such messages in this session */
			continue;
		}
		for (; idx < mbox->msg_count; idx++) {
			i_assert(mbox->msg_uids[idx] >= sync_rec.uid1);
			if (mbox->msg_uids[idx] > sync_rec.uid2)
				break;

			str_truncate(str, 0);
			str_printfa(str, "DELE %u\r\n", idx+1);
			pop3c_client_cmd_line_async_nocb(mbox->client, str_c(str));
			deletions = TRUE;
		}
	}

	if (mail_index_sync_commit(&index_sync_ctx) < 0) {
		mailbox_set_index_error(&mbox->box);
		return -1;
	}
	if (cache_view != NULL) {
		mail_cache_view_close(&cache_view);
		mail_index_view_close(&trans_view);
	}
	if (deletions) {
		if (pop3c_client_cmd_line(mbox->client, "QUIT\r\n",
					  &reply) < 0) {
			mail_storage_set_error(mbox->box.storage,
					       MAIL_ERROR_TEMP, reply);
			return -1;
		}
	}
	return 0;
}

struct mailbox_sync_context *
pop3c_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
{
	struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
	int ret = 0;

	if ((flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0 &&
	    mbox->msg_uidls == NULL) {
		/* FIXME: reconnect */
	}

	ret = pop3c_sync(mbox);
	return index_mailbox_sync_init(box, flags, ret < 0);
}
