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

/* Inside transaction we keep messages stored in sequences in uid fields.
   Before they're written to transaction log the sequences are changed to
   UIDs. */

#include "lib.h"
#include "array.h"
#include "time-util.h"
#include "mail-index-private.h"
#include "mail-index-transaction-private.h"

static bool
mail_index_transaction_has_ext_changes(struct mail_index_transaction *t);

struct mail_index_record *
mail_index_transaction_lookup(struct mail_index_transaction *t, uint32_t seq)
{
	i_assert(seq >= t->first_new_seq && seq <= t->last_new_seq);

	return array_idx_modifiable(&t->appends, seq - t->first_new_seq);
}

void mail_index_transaction_reset_v(struct mail_index_transaction *t)
{
	ARRAY_TYPE(seq_array) *rec;
	struct mail_index_transaction_ext_hdr_update *ext_hdr;

	if (array_is_created(&t->ext_rec_updates)) {
		array_foreach_modifiable(&t->ext_rec_updates, rec) {
			if (array_is_created(rec))
				array_free(rec);
		}
		array_free(&t->ext_rec_updates);
	}
	if (array_is_created(&t->ext_rec_atomics)) {
		array_foreach_modifiable(&t->ext_rec_atomics, rec) {
			if (array_is_created(rec))
				array_free(rec);
		}
		array_free(&t->ext_rec_atomics);
	}
	if (array_is_created(&t->ext_hdr_updates)) {
		array_foreach_modifiable(&t->ext_hdr_updates, ext_hdr) {
			i_free(ext_hdr->data);
			i_free(ext_hdr->mask);
		}
		array_free(&t->ext_hdr_updates);
	}

	if (array_is_created(&t->keyword_updates)) {
		struct mail_index_transaction_keyword_update *u;

		array_foreach_modifiable(&t->keyword_updates, u) {
			if (array_is_created(&u->add_seq))
				array_free(&u->add_seq);
			if (array_is_created(&u->remove_seq))
				array_free(&u->remove_seq);
		}
		array_free(&t->keyword_updates);
	}

	if (array_is_created(&t->appends))
		array_free(&t->appends);
	if (array_is_created(&t->modseq_updates))
		array_free(&t->modseq_updates);
	if (array_is_created(&t->expunges))
		array_free(&t->expunges);
	if (array_is_created(&t->updates))
		array_free(&t->updates);
	if (array_is_created(&t->ext_resizes))
		array_free(&t->ext_resizes);
	if (array_is_created(&t->ext_resets))
		array_free(&t->ext_resets);
	if (array_is_created(&t->ext_reset_ids))
		array_free(&t->ext_reset_ids);
	if (array_is_created(&t->ext_reset_atomic))
		array_free(&t->ext_reset_atomic);
	buffer_free(&t->attribute_updates);
	buffer_free(&t->attribute_updates_suffix);

	t->first_new_seq = mail_index_view_get_messages_count(t->view)+1;
	t->last_new_seq = 0;
	t->last_update_idx = 0;
	t->min_flagupdate_seq = 0;
	t->max_flagupdate_seq = 0;
	t->min_highest_modseq = 0;

	memset(t->pre_hdr_mask, 0, sizeof(t->pre_hdr_mask));
	memset(t->post_hdr_mask, 0, sizeof(t->post_hdr_mask));

	t->appends_nonsorted = FALSE;
	t->expunges_nonsorted = FALSE;
	t->drop_unnecessary_flag_updates = FALSE;
	t->pre_hdr_changed = FALSE;
	t->post_hdr_changed = FALSE;
	t->reset = FALSE;
	t->index_deleted = FALSE;
	t->index_undeleted = FALSE;
	t->log_updates = FALSE;
	t->log_ext_updates = FALSE;
	t->tail_offset_changed = FALSE;
}

void mail_index_transaction_set_log_updates(struct mail_index_transaction *t)
{
	/* flag updates aren't included in log_updates */
	t->log_updates = array_is_created(&t->appends) ||
		array_is_created(&t->modseq_updates) ||
		array_is_created(&t->expunges) ||
		array_is_created(&t->keyword_updates) ||
		t->attribute_updates != NULL ||
		t->pre_hdr_changed || t->post_hdr_changed ||
		t->min_highest_modseq != 0;
}

void mail_index_update_day_headers(struct mail_index_transaction *t,
				   time_t day_stamp)
{
	struct mail_index_header hdr;
	const struct mail_index_record *rec;
	const int max_days = N_ELEMENTS(hdr.day_first_uid);
	time_t stamp;
	int i, days;

	hdr = *mail_index_get_header(t->view);
	rec = array_front(&t->appends);

	stamp = time_to_local_day_start(day_stamp);
	if ((time_t)hdr.day_stamp >= stamp)
		return;

	/* get number of days since last message */
	days = (stamp - hdr.day_stamp) / (3600*24);
	if (days > max_days)
		days = max_days;

	/* @UNSAFE: move days forward and fill the missing days with old
	   day_first_uid[0]. */
	if (days > 0 && days < max_days)
		memmove(hdr.day_first_uid + days, hdr.day_first_uid,
			(max_days - days) * sizeof(hdr.day_first_uid[0]));
	for (i = 1; i < days; i++)
		hdr.day_first_uid[i] = hdr.day_first_uid[0];

	hdr.day_stamp = time_to_uint32_trunc(stamp);
	hdr.day_first_uid[0] = rec->uid;

	mail_index_update_header(t,
		offsetof(struct mail_index_header, day_stamp),
		&hdr.day_stamp, sizeof(hdr.day_stamp), FALSE);
	mail_index_update_header(t,
		offsetof(struct mail_index_header, day_first_uid),
		hdr.day_first_uid, sizeof(hdr.day_first_uid), FALSE);
}

void mail_index_append(struct mail_index_transaction *t, uint32_t uid,
		       uint32_t *seq_r)
{
	struct mail_index_record *rec;

	i_assert(!t->no_appends);

	t->log_updates = TRUE;

	if (!array_is_created(&t->appends))
		i_array_init(&t->appends, 32);

	/* sequence number is visible only inside given view,
	   so let it generate it */
	if (t->last_new_seq != 0)
		*seq_r = ++t->last_new_seq;
	else
		*seq_r = t->last_new_seq = t->first_new_seq;

	rec = array_append_space(&t->appends);
	if (uid != 0) {
		rec->uid = uid;
		if (!t->appends_nonsorted &&
		    t->last_new_seq != t->first_new_seq) {
			/* if previous record's UID is larger than this one,
			   we'll have to sort the appends later */
			rec = mail_index_transaction_lookup(t, *seq_r - 1);
			if (rec->uid > uid)
				t->appends_nonsorted = TRUE;
			else if (rec->uid == uid)
				i_panic("Duplicate UIDs added in transaction");
		}
		if (t->highest_append_uid < uid)
			t->highest_append_uid = uid;
	}
}

void mail_index_append_finish_uids(struct mail_index_transaction *t,
				   uint32_t first_uid,
				   ARRAY_TYPE(seq_range) *uids_r)
{
	return mail_index_append_finish_uids_full(t, first_uid, first_uid, uids_r);
}

void mail_index_append_finish_uids_full(struct mail_index_transaction *t,
					uint32_t min_allowed_uid,
					uint32_t first_new_uid,
					ARRAY_TYPE(seq_range) *uids_r)
{
	struct mail_index_record *recs;
	unsigned int i, count;
	struct seq_range *range;
	uint32_t next_uid;

	if (!array_is_created(&t->appends))
		return;

	i_assert(min_allowed_uid <= first_new_uid);
	i_assert(first_new_uid < (uint32_t)-1);

	/* first find the highest assigned uid */
	recs = array_get_modifiable(&t->appends, &count);
	i_assert(count > 0);

	next_uid = first_new_uid;
	for (i = 0; i < count; i++) {
		if (next_uid <= recs[i].uid)
			next_uid = recs[i].uid + 1;
	}
	i_assert(next_uid > 0 && next_uid < (uint32_t)-1);

	/* assign missing uids */
	for (i = 0; i < count; i++) {
		if (recs[i].uid == 0 || recs[i].uid < min_allowed_uid) {
			i_assert(next_uid < (uint32_t)-1);
			recs[i].uid = next_uid++;
			if (t->highest_append_uid < recs[i].uid)
				t->highest_append_uid = recs[i].uid;
		} else {
			t->appends_nonsorted = TRUE;
		}
	}

	/* write the saved uids range */
	array_clear(uids_r);
	range = array_append_space(uids_r);
	range->seq1 = range->seq2 = recs[0].uid;
	for (i = 1; i < count; i++) {
		if (range->seq2 + 1 == recs[i].uid)
			range->seq2++;
		else {
			range = array_append_space(uids_r);
			range->seq1 = range->seq2 = recs[i].uid;
		}
	}
}

void mail_index_update_modseq(struct mail_index_transaction *t, uint32_t seq,
			      uint64_t min_modseq)
{
	struct mail_transaction_modseq_update *u;

	/* modseq=1 is the minimum always and it's only for mails that were
	   created/modified before modseqs were enabled. */
	if (min_modseq <= 1)
		return;

	if (!array_is_created(&t->modseq_updates))
		i_array_init(&t->modseq_updates, 32);

	u = array_append_space(&t->modseq_updates);
	u->uid = seq;
	u->modseq_low32 = min_modseq & 0xffffffff;
	u->modseq_high32 = min_modseq >> 32;

	t->log_updates = TRUE;
}

void mail_index_update_highest_modseq(struct mail_index_transaction *t,
				      uint64_t min_modseq)
{
	/* modseq=1 is the minimum always and it's only for mails that were
	   created/modified before modseqs were enabled. */
	if (min_modseq <= 1)
		return;

	if (t->min_highest_modseq < min_modseq)
		t->min_highest_modseq = min_modseq;

	t->log_updates = TRUE;
}

static void
mail_index_revert_ext(ARRAY_TYPE(seq_array_array) *ext_updates,
		      uint32_t seq)
{
	ARRAY_TYPE(seq_array) *seqs;
	unsigned int idx;

	if (!array_is_created(ext_updates))
		return;

	array_foreach_modifiable(ext_updates, seqs) {
		if (array_is_created(seqs) &&
		    mail_index_seq_array_lookup(seqs, seq, &idx))
			array_delete(seqs, idx, 1);
	}
}

static void
mail_index_revert_changes_common(struct mail_index_transaction *t, uint32_t seq)
{
	struct mail_index_transaction_keyword_update *kw_update;
	unsigned int i;

	/* remove extension updates */
	mail_index_revert_ext(&t->ext_rec_updates, seq);
	mail_index_revert_ext(&t->ext_rec_atomics, seq);
	t->log_ext_updates = mail_index_transaction_has_ext_changes(t);

	/* remove keywords */
	if (array_is_created(&t->keyword_updates)) {
		array_foreach_modifiable(&t->keyword_updates, kw_update) {
			if (array_is_created(&kw_update->add_seq)) {
				seq_range_array_remove(&kw_update->add_seq,
						       seq);
			}
			if (array_is_created(&kw_update->remove_seq)) {
				seq_range_array_remove(&kw_update->remove_seq,
						       seq);
			}
		}
	}
	/* remove modseqs */
	if (array_is_created(&t->modseq_updates) &&
	    mail_index_seq_array_lookup((void *)&t->modseq_updates, seq, &i))
		array_delete(&t->modseq_updates, i, 1);
}

void mail_index_revert_changes(struct mail_index_transaction *t, uint32_t seq)
{
	mail_index_revert_changes_common(t, seq);
	(void)mail_index_cancel_flag_updates(t, seq);
}

static void
mail_index_expunge_last_append(struct mail_index_transaction *t, uint32_t seq)
{
	i_assert(seq == t->last_new_seq);

	mail_index_revert_changes_common(t, seq);

	/* and finally remove the append itself */
	array_delete(&t->appends, seq - t->first_new_seq, 1);
	t->last_new_seq--;
	if (t->first_new_seq > t->last_new_seq) {
		t->last_new_seq = 0;
		t->appends_nonsorted = FALSE;
		array_free(&t->appends);
	}
	mail_index_transaction_set_log_updates(t);
}

void mail_index_expunge(struct mail_index_transaction *t, uint32_t seq)
{
	static guid_128_t null_guid =
		{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
	mail_index_expunge_guid(t, seq, null_guid);
}

void mail_index_expunge_guid(struct mail_index_transaction *t, uint32_t seq,
			     const guid_128_t guid_128)
{
	const struct mail_transaction_expunge_guid *expunges;
	struct mail_transaction_expunge_guid *expunge;
	unsigned int count;

	i_assert(seq > 0);
	if (seq >= t->first_new_seq) {
		/* we can handle only the last append. otherwise we'd have to
		   renumber sequences and that gets tricky. for now this is
		   enough, since we typically want to expunge all the
		   appends. */
		mail_index_expunge_last_append(t, seq);
	} else {
		t->log_updates = TRUE;

		/* ignore duplicates here. drop them when committing. */
		if (!array_is_created(&t->expunges))
			i_array_init(&t->expunges, 64);
		else if (!t->expunges_nonsorted) {
			/* usually expunges are added in increasing order. */
			expunges = array_get(&t->expunges, &count);
			if (count > 0 && seq < expunges[count-1].uid)
				t->expunges_nonsorted = TRUE;
		}
		expunge = array_append_space(&t->expunges);
		expunge->uid = seq;
		memcpy(expunge->guid_128, guid_128, sizeof(expunge->guid_128));
	}
}

static void update_minmax_flagupdate_seq(struct mail_index_transaction *t,
					 uint32_t seq1, uint32_t seq2)
{
	if (t->min_flagupdate_seq == 0) {
		t->min_flagupdate_seq = seq1;
		t->max_flagupdate_seq = seq2;
	} else {
		if (t->min_flagupdate_seq > seq1)
			t->min_flagupdate_seq = seq1;
		if (t->max_flagupdate_seq < seq2)
			t->max_flagupdate_seq = seq2;
	}
}

unsigned int
mail_index_transaction_get_flag_update_pos(struct mail_index_transaction *t,
					   unsigned int left_idx,
					   unsigned int right_idx,
					   uint32_t seq)
{
	const struct mail_index_flag_update *updates;
	unsigned int idx, count;

	updates = array_get(&t->updates, &count);
	i_assert(left_idx <= right_idx && right_idx <= count);
	i_assert(count < INT_MAX);

	/* find the first update with either overlapping range,
	   or the update which will come after our insert */
	idx = left_idx;
	while (left_idx < right_idx) {
		idx = (left_idx + right_idx) / 2;

		if (updates[idx].uid2 < seq)
			left_idx = idx+1;
		else if (updates[idx].uid1 > seq)
			right_idx = idx;
		else
			break;
	}
	if (left_idx > idx)
		idx++;
	return idx;
}

static void
mail_index_insert_flag_update(struct mail_index_transaction *t,
			      struct mail_index_flag_update u,
			      unsigned int idx)
{
	struct mail_index_flag_update *updates, tmp_update;
	unsigned int count, first_idx, max;

	updates = array_get_modifiable(&t->updates, &count);

	/* overlapping ranges, split/merge them */
	i_assert(idx == 0 || updates[idx-1].uid2 < u.uid1);
	i_assert(idx == count || updates[idx].uid2 >= u.uid1);

	/* first we'll just add the changes without trying to merge anything */
	first_idx = idx;
	for (; idx < count && u.uid2 >= updates[idx].uid1; idx++) {
		i_assert(u.uid1 <= updates[idx].uid2);
		if (u.uid1 != updates[idx].uid1 &&
		    (updates[idx].add_flags != u.add_flags ||
		     updates[idx].remove_flags != u.remove_flags)) {
			if (u.uid1 < updates[idx].uid1) {
				/* insert new update */
				tmp_update = u;
				tmp_update.uid2 = updates[idx].uid1 - 1;
			} else {
				/* split existing update from beginning */
				tmp_update = updates[idx];
				tmp_update.uid2 = u.uid1 - 1;
				updates[idx].uid1 = u.uid1;
			}

			i_assert(tmp_update.uid1 <= tmp_update.uid2);
			i_assert(updates[idx].uid1 <= updates[idx].uid2);

			array_insert(&t->updates, idx, &tmp_update, 1);
			updates = array_get_modifiable(&t->updates, &count);
			idx++;
		} else if (u.uid1 < updates[idx].uid1) {
			updates[idx].uid1 = u.uid1;
		}

		if (u.uid2 < updates[idx].uid2 &&
		    (updates[idx].add_flags != u.add_flags ||
		     updates[idx].remove_flags != u.remove_flags)) {
			/* split existing update from end */
			tmp_update = updates[idx];
			tmp_update.uid2 = u.uid2;
			updates[idx].uid1 = u.uid2 + 1;

			i_assert(tmp_update.uid1 <= tmp_update.uid2);
			i_assert(updates[idx].uid1 <= updates[idx].uid2);

			array_insert(&t->updates, idx, &tmp_update, 1);
			updates = array_get_modifiable(&t->updates, &count);
		}

		updates[idx].add_flags =
			(updates[idx].add_flags | u.add_flags) &
			~u.remove_flags;
		updates[idx].remove_flags =
			(updates[idx].remove_flags | u.remove_flags) &
			~u.add_flags;
		u.uid1 = updates[idx].uid2 + 1;

		if (updates[idx].add_flags == 0 &&
		    updates[idx].remove_flags == 0) {
			/* we can remove this update completely */
			array_delete(&t->updates, idx, 1);
			updates = array_get_modifiable(&t->updates, &count);
		}

		if (u.uid1 > u.uid2) {
			/* break here before idx++ so last_update_idx is set
			   correctly */
			break;
		}
	}
	i_assert(idx <= count);

	if (u.uid1 <= u.uid2) {
		i_assert(idx == 0 || updates[idx-1].uid2 < u.uid1);
		i_assert(idx == count || updates[idx].uid1 > u.uid2);
		array_insert(&t->updates, idx, &u, 1);
	}
	updates = array_get_modifiable(&t->updates, &count);
	t->last_update_idx = idx == count ? count-1 : idx;

	/* merge everything */
	idx = first_idx == 0 ? 0 : first_idx - 1;
	max = count == 0 ? 0 : I_MIN(t->last_update_idx + 1, count-1);
	for (; idx < max; ) {
		if (updates[idx].uid2 + 1 == updates[idx+1].uid1 &&
		    updates[idx].add_flags == updates[idx+1].add_flags &&
		    updates[idx].remove_flags == updates[idx+1].remove_flags) {
			/* merge */
			updates[idx].uid2 = updates[idx+1].uid2;
			array_delete(&t->updates, idx + 1, 1);
			max--;
			if (t->last_update_idx > idx)
				t->last_update_idx--;
			updates = array_get_modifiable(&t->updates, &count);
		} else {
			idx++;
		}
	}
}

static void mail_index_record_modify_flags(struct mail_index_record *rec,
					   enum modify_type modify_type,
					   enum mail_flags flags)
{
	switch (modify_type) {
	case MODIFY_REPLACE:
		rec->flags = flags;
		break;
	case MODIFY_ADD:
		rec->flags |= flags;
		break;
	case MODIFY_REMOVE:
		rec->flags &= ENUM_NEGATE(flags);
		break;
	}
}

void mail_index_update_flags_range(struct mail_index_transaction *t,
				   uint32_t seq1, uint32_t seq2,
				   enum modify_type modify_type,
				   enum mail_flags flags)
{
	struct mail_index_record *rec;
	struct mail_index_flag_update u, *last_update;
	unsigned int idx, first_idx, count;

	update_minmax_flagupdate_seq(t, seq1, seq2);
	if (seq2 >= t->first_new_seq) {
		/* updates for appended messages, modify them directly */
		uint32_t seq;

		for (seq = I_MAX(t->first_new_seq, seq1); seq <= seq2; seq++) {
			rec = mail_index_transaction_lookup(t, seq);
			mail_index_record_modify_flags(rec, modify_type, flags);
		}
		if (seq1 >= t->first_new_seq)
			return;

		/* range contains also existing messages. update them next. */
		seq2 = t->first_new_seq - 1;
	}

	i_assert(seq1 <= seq2 && seq1 > 0);
	i_assert(seq2 <= mail_index_view_get_messages_count(t->view));

	if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES) != 0)
		t->drop_unnecessary_flag_updates = TRUE;

	i_zero(&u);
	u.uid1 = seq1;
	u.uid2 = seq2;

	switch (modify_type) {
	case MODIFY_REPLACE:
		u.add_flags = flags;
		u.remove_flags = ENUM_NEGATE(flags) & MAIL_INDEX_FLAGS_MASK;
		break;
	case MODIFY_ADD:
		if (flags == 0)
			return;
		u.add_flags = flags;
		break;
	case MODIFY_REMOVE:
		if (flags == 0)
			return;
		u.remove_flags = flags;
		break;
	}

	if (!array_is_created(&t->updates)) {
		i_array_init(&t->updates, 256);
		array_push_back(&t->updates, &u);
		return;
	}

	last_update = array_get_modifiable(&t->updates, &count);
	if (t->last_update_idx < count) {
		/* fast path - hopefully we're updating the next message,
		   or a message that is to be appended as last update */
		last_update += t->last_update_idx;
		if (seq1 - 1 == last_update->uid2) {
			if (u.add_flags == last_update->add_flags &&
			    u.remove_flags == last_update->remove_flags &&
			    (t->last_update_idx + 1 == count ||
			     last_update[1].uid1 > seq2)) {
				/* we can just update the UID range */
				last_update->uid2 = seq2;
				return;
			}
		} else if (seq1 > last_update->uid2) {
			/* hopefully we can just append it */
			t->last_update_idx++;
			last_update++;
		}
	}

	if (t->last_update_idx == count)
		array_push_back(&t->updates, &u);
	else {
		i_assert(t->last_update_idx < count);

		/* slow path */
		if (seq1 > last_update->uid2) {
			/* added after this */
			first_idx = t->last_update_idx + 1;
		} else {
			/* added before this or on top of this */
			first_idx = 0;
			count = t->last_update_idx + 1;
		}
		idx = mail_index_transaction_get_flag_update_pos(t, first_idx,
								 count, u.uid1);
		mail_index_insert_flag_update(t, u, idx);
	}
}

void mail_index_update_flags(struct mail_index_transaction *t, uint32_t seq,
			     enum modify_type modify_type,
			     enum mail_flags flags)
{
	mail_index_update_flags_range(t, seq, seq, modify_type, flags);
}

static void
mail_index_attribute_set_full(struct mail_index_transaction *t,
			      const char *key, bool pvt, char prefix,
			      time_t timestamp, uint32_t value_len)
{
	uint32_t ts = time_to_uint32_trunc(timestamp);

	if (t->attribute_updates == NULL) {
		t->attribute_updates = buffer_create_dynamic(default_pool, 64);
		t->attribute_updates_suffix = buffer_create_dynamic(default_pool, 64);
	}
	buffer_append_c(t->attribute_updates, prefix);
	buffer_append_c(t->attribute_updates, pvt ? 'p' : 's');
	buffer_append(t->attribute_updates, key, strlen(key)+1);

	buffer_append(t->attribute_updates_suffix, &ts, sizeof(ts));
	if (prefix == '+') {
		buffer_append(t->attribute_updates_suffix,
			      &value_len, sizeof(value_len));
	}
	t->log_updates = TRUE;
}

void mail_index_attribute_set(struct mail_index_transaction *t,
			      bool pvt, const char *key,
			      time_t timestamp, uint32_t value_len)
{
	mail_index_attribute_set_full(t, key, pvt, '+', timestamp, value_len);
}

void mail_index_attribute_unset(struct mail_index_transaction *t,
				bool pvt, const char *key,
				time_t timestamp)
{
	mail_index_attribute_set_full(t, key, pvt, '-', timestamp, 0);
}

void mail_index_update_header(struct mail_index_transaction *t,
			      size_t offset, const void *data, size_t size,
			      bool prepend)
{
	i_assert(offset < sizeof(t->pre_hdr_change));
	i_assert(size <= sizeof(t->pre_hdr_change) - offset);
	i_assert(size > 0);

	t->log_updates = TRUE;

	if (prepend) {
		t->pre_hdr_changed = TRUE;
		memcpy(t->pre_hdr_change + offset, data, size);
		for (; size > 0; size--)
			t->pre_hdr_mask[offset++] = 1;
	} else {
		t->post_hdr_changed = TRUE;
		memcpy(t->post_hdr_change + offset, data, size);
		for (; size > 0; size--)
			t->post_hdr_mask[offset++] = 1;
	}
}

static void
mail_index_ext_rec_updates_resize(struct mail_index_transaction *t,
				  uint32_t ext_id, uint16_t new_record_size)
{
	ARRAY_TYPE(seq_array) *array, old_array;
	unsigned int i;

	if (!array_is_created(&t->ext_rec_updates))
		return;
	array = array_idx_modifiable(&t->ext_rec_updates, ext_id);
	if (!array_is_created(array))
		return;

	old_array = *array;
	i_zero(array);
	mail_index_seq_array_alloc(array, new_record_size);

	/* copy the records' beginnings. leave the end zero-filled. */
	for (i = 0; i < array_count(&old_array); i++) {
		const void *old_record = array_idx(&old_array, i);

		memcpy(array_append_space(array), old_record,
		       old_array.arr.element_size);
	}
	array_free(&old_array);
}

void mail_index_ext_resize(struct mail_index_transaction *t, uint32_t ext_id,
			   uint32_t hdr_size, uint16_t record_size,
			   uint16_t record_align)
{
	const struct mail_index_registered_ext *rext;
	const struct mail_transaction_ext_intro *resizes;
	unsigned int resizes_count;
	struct mail_transaction_ext_intro intro;
	uint32_t old_record_size = 0, old_record_align, old_header_size;

	i_zero(&intro);
	rext = array_idx(&t->view->index->extensions, ext_id);

	/* get ext_id from transaction's map if it's there */
	if (!mail_index_map_get_ext_idx(t->view->map, ext_id, &intro.ext_id)) {
		/* have to create it */
		intro.ext_id = (uint32_t)-1;
		old_record_align = rext->record_align;
		old_header_size = rext->hdr_size;
	} else {
		const struct mail_index_ext *ext;

		ext = array_idx(&t->view->map->extensions, intro.ext_id);
		old_record_align = ext->record_align;
		old_header_size = ext->hdr_size;
	}

	/* get the record size. if there are any existing record updates,
	   they're using the registered size, not the map's existing
	   record_size. */
	if (array_is_created(&t->ext_resizes))
		resizes = array_get(&t->ext_resizes, &resizes_count);
	else {
		resizes = NULL;
		resizes_count = 0;
	}
	if (ext_id < resizes_count && resizes[ext_id].name_size != 0) {
		/* already resized once. use the resized value. */
		old_record_size = resizes[ext_id].record_size;
	} else {
		/* use the registered values. */
		old_record_size = rext->record_size;
	}

	if (record_size != old_record_size && record_size != (uint16_t)-1) {
		/* if record_size grows, we'll just resize the existing
		   ext_rec_updates array. it's not possible to shrink
		   record_size without data loss. */
		i_assert(record_size > old_record_size);
		mail_index_ext_rec_updates_resize(t, ext_id, record_size);
	}

	t->log_ext_updates = TRUE;

	if (!array_is_created(&t->ext_resizes))
		i_array_init(&t->ext_resizes, ext_id + 2);

	intro.hdr_size = hdr_size != (uint32_t)-1 ? hdr_size : old_header_size;
	if (record_size != (uint16_t)-1) {
		i_assert(record_align != (uint16_t)-1);
		intro.record_size = record_size;
		intro.record_align = record_align;
	} else {
		i_assert(record_align == (uint16_t)-1);
		intro.record_size = old_record_size;
		intro.record_align = old_record_align;
	}
	intro.name_size = 1;
	array_idx_set(&t->ext_resizes, ext_id, &intro);
}

void mail_index_ext_resize_hdr(struct mail_index_transaction *t,
			       uint32_t ext_id, uint32_t hdr_size)
{
	mail_index_ext_resize(t, ext_id, hdr_size, (uint16_t)-1, (uint16_t)-1);
}

void mail_index_ext_reset(struct mail_index_transaction *t, uint32_t ext_id,
			  uint32_t reset_id, bool clear_data)
{
	struct mail_transaction_ext_reset reset;

	i_assert(reset_id != 0);

	i_zero(&reset);
	reset.new_reset_id = reset_id;
	reset.preserve_data = clear_data ? 0 : 1;

	mail_index_ext_set_reset_id(t, ext_id, reset_id);

	if (!array_is_created(&t->ext_resets))
		i_array_init(&t->ext_resets, ext_id + 2);
	array_idx_set(&t->ext_resets, ext_id, &reset);
	t->log_ext_updates = TRUE;
}

void mail_index_ext_reset_inc(struct mail_index_transaction *t, uint32_t ext_id,
			      uint32_t prev_reset_id, bool clear_data)
{
	uint32_t expected_reset_id = prev_reset_id + 1;

	mail_index_ext_reset(t, ext_id, (uint32_t)-1, clear_data);

	if (!array_is_created(&t->ext_reset_atomic))
		i_array_init(&t->ext_reset_atomic, ext_id + 2);
	array_idx_set(&t->ext_reset_atomic, ext_id, &expected_reset_id);
}

static bool
mail_index_transaction_has_ext_updates(const ARRAY_TYPE(seq_array_array) *arr)
{
	const ARRAY_TYPE(seq_array) *array;

	if (array_is_created(arr)) {
		array_foreach(arr, array) {
			if (array_is_created(array))
				return TRUE;
		}
	}
	return FALSE;
}

static bool
mail_index_transaction_has_ext_changes(struct mail_index_transaction *t)
{
	if (mail_index_transaction_has_ext_updates(&t->ext_rec_updates))
		return TRUE;
	if (mail_index_transaction_has_ext_updates(&t->ext_rec_atomics))
		return TRUE;

	if (array_is_created(&t->ext_hdr_updates)) {
		const struct mail_index_transaction_ext_hdr_update *hdr;

		array_foreach(&t->ext_hdr_updates, hdr) {
			if (hdr->alloc_size > 0)
				return TRUE;
		}
	}
	if (array_is_created(&t->ext_resets)) {
		const struct mail_transaction_ext_reset *reset;

		array_foreach(&t->ext_resets, reset) {
			if (reset->new_reset_id != 0)
				return TRUE;
		}
	}
	if (array_is_created(&t->ext_resizes)) {
		const struct mail_transaction_ext_intro *resize;

		array_foreach(&t->ext_resizes, resize) {
			if (resize->name_size > 0)
				return TRUE;
		}
	}
	return FALSE;
}

static void
mail_index_ext_update_reset(ARRAY_TYPE(seq_array_array) *arr, uint32_t ext_id)
{
	if (array_is_created(arr) && ext_id < array_count(arr)) {
		/* if extension records have been updated, clear them */
		ARRAY_TYPE(seq_array) *array;

		array = array_idx_modifiable(arr, ext_id);
		if (array_is_created(array))
			array_clear(array);
	}
}

static void
mail_index_ext_reset_changes(struct mail_index_transaction *t, uint32_t ext_id)
{
	mail_index_ext_update_reset(&t->ext_rec_updates, ext_id);
	mail_index_ext_update_reset(&t->ext_rec_atomics, ext_id);
	if (array_is_created(&t->ext_hdr_updates) &&
	    ext_id < array_count(&t->ext_hdr_updates)) {
		/* if extension headers have been updated, clear them */
		struct mail_index_transaction_ext_hdr_update *hdr;

		hdr = array_idx_modifiable(&t->ext_hdr_updates, ext_id);
		if (hdr->alloc_size > 0) {
			i_free_and_null(hdr->mask);
			i_free_and_null(hdr->data);
		}
		hdr->alloc_size = 0;
	}
	if (array_is_created(&t->ext_resets) &&
	    ext_id < array_count(&t->ext_resets)) {
		/* clear resets */
		array_idx_clear(&t->ext_resets, ext_id);
	}
	if (array_is_created(&t->ext_resizes) &&
	    ext_id < array_count(&t->ext_resizes)) {
		/* clear resizes */
		array_idx_clear(&t->ext_resizes, ext_id);
	}

	t->log_ext_updates = mail_index_transaction_has_ext_changes(t);
}

void mail_index_ext_using_reset_id(struct mail_index_transaction *t,
				   uint32_t ext_id, uint32_t reset_id)
{
	uint32_t *reset_id_p;
	bool changed;

	if (!array_is_created(&t->ext_reset_ids))
		i_array_init(&t->ext_reset_ids, ext_id + 2);
	reset_id_p = array_idx_get_space(&t->ext_reset_ids, ext_id);
	changed = *reset_id_p != reset_id && *reset_id_p != 0;
	*reset_id_p = reset_id;
	if (changed) {
		/* reset_id changed, clear existing changes */
		mail_index_ext_reset_changes(t, ext_id);
	}
}

void mail_index_ext_set_reset_id(struct mail_index_transaction *t,
				 uint32_t ext_id, uint32_t reset_id)
{
	mail_index_ext_using_reset_id(t, ext_id, reset_id);
	/* make sure the changes get reset, even if reset_id doesn't change */
	mail_index_ext_reset_changes(t, ext_id);
}

void mail_index_update_header_ext(struct mail_index_transaction *t,
				  uint32_t ext_id, size_t offset,
				  const void *data, size_t size)
{
	struct mail_index_transaction_ext_hdr_update *hdr;
	size_t new_size;

	i_assert(size > 0);
	i_assert(offset <= (uint32_t)-1 && size <= (uint32_t)-1 &&
		 offset + size <= (uint32_t)-1);

	if (!array_is_created(&t->ext_hdr_updates))
		i_array_init(&t->ext_hdr_updates, ext_id + 2);

	hdr = array_idx_get_space(&t->ext_hdr_updates, ext_id);
	if (hdr->alloc_size < offset || hdr->alloc_size - offset < size) {
		i_assert(size < SIZE_MAX - offset);
		new_size = nearest_power(offset + size);
		hdr->mask = i_realloc(hdr->mask, hdr->alloc_size, new_size);
		hdr->data = i_realloc(hdr->data, hdr->alloc_size, new_size);
		hdr->alloc_size = new_size;
	}
	memset(hdr->mask + offset, 1, size);
	memcpy(hdr->data + offset, data, size);

	t->log_ext_updates = TRUE;
}

void mail_index_update_ext(struct mail_index_transaction *t, uint32_t seq,
			   uint32_t ext_id, const void *data, void *old_data_r)
{
	struct mail_index *index = t->view->index;
	const struct mail_index_registered_ext *rext;
	const struct mail_transaction_ext_intro *intro;
	uint16_t record_size;
	ARRAY_TYPE(seq_array) *array;
	unsigned int count;

	i_assert(seq > 0 &&
		 (seq <= mail_index_view_get_messages_count(t->view) ||
		  seq <= t->last_new_seq));
	i_assert(ext_id < array_count(&index->extensions));

	t->log_ext_updates = TRUE;

	if (!array_is_created(&t->ext_resizes)) {
		intro = NULL;
		count = 0;
	} else {
		intro = array_get(&t->ext_resizes, &count);
	}
	if (ext_id < count && intro[ext_id].name_size != 0) {
		/* resized record */
		record_size = intro[ext_id].record_size;
	} else {
		rext = array_idx(&index->extensions, ext_id);
		record_size = rext->record_size;
	}
	i_assert(record_size > 0);

	if (!array_is_created(&t->ext_rec_updates))
		i_array_init(&t->ext_rec_updates, ext_id + 2);
	array = array_idx_get_space(&t->ext_rec_updates, ext_id);

	/* @UNSAFE */
	if (!mail_index_seq_array_add(array, seq, data, record_size,
				      old_data_r)) {
		/* not found, clear old_data if it was given */
		if (old_data_r != NULL)
			memset(old_data_r, 0, record_size);
	}
}

int mail_index_atomic_inc_ext(struct mail_index_transaction *t,
			      uint32_t seq, uint32_t ext_id, int diff)
{
	ARRAY_TYPE(seq_array) *array;
	int32_t old_diff32, diff32 = diff;

	i_assert(seq > 0 &&
		 (seq <= mail_index_view_get_messages_count(t->view) ||
		  seq <= t->last_new_seq));
	i_assert(ext_id < array_count(&t->view->index->extensions));
	/* currently non-external transactions can be applied multiple times,
	   causing multiple increments. FIXME: we need this now and it doesn't
	   actually seem to be a real problem at least right now - why? */
	/*i_assert((t->flags & MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL) != 0);*/

	t->log_ext_updates = TRUE;
	if (!array_is_created(&t->ext_rec_atomics))
		i_array_init(&t->ext_rec_atomics, ext_id + 2);
	array = array_idx_get_space(&t->ext_rec_atomics, ext_id);
	if (mail_index_seq_array_add(array, seq, &diff32, sizeof(diff32),
				     &old_diff32)) {
		/* already incremented this sequence in this transaction */
		diff32 += old_diff32;
		(void)mail_index_seq_array_add(array, seq, &diff32,
					       sizeof(diff32), NULL);
	}
	return diff32;
}

static bool
keyword_update_has_changes(struct mail_index_transaction *t, uint32_t seq,
			   enum modify_type modify_type,
			   struct mail_keywords *keywords)
{
	ARRAY_TYPE(keyword_indexes) existing;
	const unsigned int *existing_idx;
	unsigned int i, j, existing_count;
	bool found;

	t_array_init(&existing, 32);
	if (seq < t->first_new_seq)
		mail_index_transaction_lookup_latest_keywords(t, seq, &existing);
	existing_idx = array_get(&existing, &existing_count);

	if (modify_type == MODIFY_REPLACE && existing_count != keywords->count)
		return TRUE;

	for (i = 0; i < keywords->count; i++) {
		found = FALSE;
		for (j = 0; j < existing_count; j++) {
			if (existing_idx[j] == keywords->idx[i]) {
				found = TRUE;
				break;
			}
		}
		switch (modify_type) {
		case MODIFY_ADD:
		case MODIFY_REPLACE:
			if (!found)
				return TRUE;
			break;
		case MODIFY_REMOVE:
			if (found)
				return TRUE;
			break;
		}
	}
	return FALSE;
}

static struct mail_keywords *
keyword_update_remove_existing(struct mail_index_transaction *t, uint32_t seq)
{
	ARRAY_TYPE(keyword_indexes) keywords;
	uint32_t i, keywords_count;

	t_array_init(&keywords, 32);
	if (t->view->v.lookup_full == NULL) {
		/* syncing is saving a list of changes into this transaction.
		   the seq is actual an uid, so we can't lookup the existing
		   keywords. we shouldn't get here unless we're reading
		   pre-v2.2 keyword-reset records from .log files. so we don't
		   really care about performance that much here, */
		keywords_count = array_count(&t->view->index->keywords);
		for (i = 0; i < keywords_count; i++)
			array_push_back(&keywords, &i);
	} else {
		mail_index_transaction_lookup_latest_keywords(t, seq, &keywords);
	}
	if (array_count(&keywords) == 0)
		return NULL;
	return mail_index_keywords_create_from_indexes(t->view->index,
						       &keywords);
}

void mail_index_update_keywords(struct mail_index_transaction *t, uint32_t seq,
				enum modify_type modify_type,
				struct mail_keywords *keywords)
{
	struct mail_index_transaction_keyword_update *u;
	struct mail_keywords *add_keywords = NULL, *remove_keywords = NULL;
	struct mail_keywords *unref_keywords = NULL;
	unsigned int i;
	bool changed;

	i_assert(seq > 0 &&
		 (seq <= mail_index_view_get_messages_count(t->view) ||
		  seq <= t->last_new_seq));
	i_assert(keywords->index == t->view->index);

	if (keywords->count == 0 && modify_type != MODIFY_REPLACE)
		return;

	update_minmax_flagupdate_seq(t, seq, seq);

	if (!array_is_created(&t->keyword_updates)) {
		uint32_t max_idx = keywords->count == 0 ? 3 :
			keywords->idx[keywords->count-1];

		i_array_init(&t->keyword_updates, max_idx + 1);
	}

	if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES) != 0) {
		T_BEGIN {
			changed = keyword_update_has_changes(t, seq,
							     modify_type,
							     keywords);
		} T_END;
		if (!changed)
			return;
	}

	switch (modify_type) {
	case MODIFY_REPLACE:
		/* split this into add+remove. remove all existing keywords not
		   included in the keywords list */
		if (seq < t->first_new_seq) T_BEGIN {
			/* remove the ones currently in index */
			remove_keywords = keyword_update_remove_existing(t, seq);
			unref_keywords = remove_keywords;
		} T_END;
		/* remove from all changes we've done in this transaction */
		array_foreach_modifiable(&t->keyword_updates, u)
			seq_range_array_remove(&u->add_seq, seq);
		add_keywords = keywords;
		break;
	case MODIFY_ADD:
		add_keywords = keywords;
		break;
	case MODIFY_REMOVE:
		remove_keywords = keywords;
		break;
	}

	/* Update add_seq and remove_seq arrays which describe the keyword
	   changes. First do the removes, since replace removes everything
	   first. */
	if (remove_keywords != NULL) {
		for (i = 0; i < remove_keywords->count; i++) {
			u = array_idx_get_space(&t->keyword_updates,
						 remove_keywords->idx[i]);
			seq_range_array_remove(&u->add_seq, seq);
			/* Don't bother updating remove_seq for new messages,
			   since their initial state is "no keyword" anyway */
			if (seq < t->first_new_seq) {
				seq_range_array_add_with_init(&u->remove_seq,
							      16, seq);
			}
		}
	}
	if (add_keywords != NULL) {
		for (i = 0; i < add_keywords->count; i++) {
			u = array_idx_get_space(&t->keyword_updates,
						 add_keywords->idx[i]);
			seq_range_array_add_with_init(&u->add_seq, 16, seq);
			seq_range_array_remove(&u->remove_seq, seq);
		}
	}
	if (unref_keywords != NULL)
		mail_index_keywords_unref(&unref_keywords);

	t->log_updates = TRUE;
}

bool mail_index_cancel_flag_updates(struct mail_index_transaction *t,
				    uint32_t seq)
{
	struct mail_index_flag_update *updates, tmp_update;
	unsigned int i, count;

	if (!array_is_created(&t->updates))
		return FALSE;

	updates = array_get_modifiable(&t->updates, &count);
	i = mail_index_transaction_get_flag_update_pos(t, 0, count, seq);
	if (i == count)
		return FALSE;
	else {
		i_assert(seq <= updates[i].uid2);
		if (seq < updates[i].uid1)
			return FALSE;
	}

	/* exists */
	if (updates[i].uid1 == seq) {
		if (updates[i].uid2 != seq)
			updates[i].uid1++;
		else if (count > 1)
			array_delete(&t->updates, i, 1);
		else
			array_free(&t->updates);
	} else if (updates[i].uid2 == seq) {
		updates[i].uid2--;
	} else {
		/* need to split it in two */
		tmp_update = updates[i];
		tmp_update.uid1 = seq+1;
		updates[i].uid2 = seq-1;
		array_insert(&t->updates, i + 1, &tmp_update, 1);
	}
	return TRUE;
}

static bool mail_index_cancel_array(ARRAY_TYPE(seq_range) *array, uint32_t seq)
{
	if (array_is_created(array)) {
		if (seq_range_array_remove(array, seq)) {
			if (array_count(array) == 0)
				array_free(array);
			return TRUE;
		}
	}
	return FALSE;
}

bool mail_index_cancel_keyword_updates(struct mail_index_transaction *t,
				       uint32_t seq)
{
	struct mail_index_transaction_keyword_update *kw;
	bool ret = FALSE, have_kw_changes = FALSE;

	if (!array_is_created(&t->keyword_updates))
		return FALSE;

	array_foreach_modifiable(&t->keyword_updates, kw) {
		if (mail_index_cancel_array(&kw->add_seq, seq))
			ret = TRUE;
		if (mail_index_cancel_array(&kw->remove_seq, seq))
			ret = TRUE;
		if (array_is_created(&kw->add_seq) ||
		    array_is_created(&kw->remove_seq))
			have_kw_changes = TRUE;
	}
	if (!have_kw_changes)
		array_free(&t->keyword_updates);
	return ret;
}

void mail_index_transaction_reset(struct mail_index_transaction *t)
{
	t->v.reset(t);
}

void mail_index_reset(struct mail_index_transaction *t)
{
	mail_index_transaction_reset(t);

	t->reset = TRUE;
}

void mail_index_unset_fscked(struct mail_index_transaction *t)
{
	struct mail_index_header new_hdr =
		t->view->index->map->hdr;

	i_assert(t->view->index->log_sync_locked);

	/* remove fsck'd-flag if it exists. */
	if ((new_hdr.flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0) {
		new_hdr.flags &= ENUM_NEGATE(MAIL_INDEX_HDR_FLAG_FSCKD);
		mail_index_update_header(t,
			offsetof(struct mail_index_header, flags),
			&new_hdr.flags, sizeof(new_hdr.flags), FALSE);
	}
}

void mail_index_set_deleted(struct mail_index_transaction *t)
{
	i_assert(!t->index_undeleted);

	t->index_deleted = TRUE;
}

void mail_index_set_undeleted(struct mail_index_transaction *t)
{
	i_assert(!t->index_deleted);

	t->index_undeleted = TRUE;
}

void mail_index_transaction_set_max_modseq(struct mail_index_transaction *t,
					   uint64_t max_modseq,
					   ARRAY_TYPE(seq_range) *seqs)
{
	i_assert(array_is_created(seqs));

	t->max_modseq = max_modseq;
	t->conflict_seqs = seqs;
}
