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

#include "lib.h"
#include "str.h"
#include "istream.h"
#include "istream-private.h"
#include "qp-encoder.h"
#include <ctype.h>

enum qp_encoder_last_char {
	QP_ENCODER_LAST_ANY = 0,
	QP_ENCODER_LAST_CR,
	QP_ENCODER_LAST_WHITE_SPACE,
};

struct qp_encoder {
	const char *linebreak;
	string_t *dest;
	size_t line_len;
	size_t max_len;
	enum qp_encoder_flag flags;
	enum qp_encoder_last_char last_char;
	bool add_header_preamble:1;
};

struct qp_encoder *
qp_encoder_init(string_t *dest, unsigned int max_len, enum qp_encoder_flag flags)
{
	i_assert(max_len > 0);

	if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
	    (flags & QP_ENCODER_FLAG_BINARY_DATA) != 0)
		i_panic("qp encoder cannot do header format with binary data");

	struct qp_encoder *qp = i_new(struct qp_encoder, 1);
	qp->flags = flags;
	qp->dest = dest;
	qp->max_len = max_len;

	if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
		qp->linebreak = "?=\r\n =?utf-8?Q?";
		qp->add_header_preamble = TRUE;
	} else {
		qp->linebreak = "=\r\n";
	}
	return qp;
}

void qp_encoder_deinit(struct qp_encoder **qp)
{
	i_free(*qp);
}

static inline void
qp_encode_or_break(struct qp_encoder *qp, unsigned char c)
{
	bool encode = FALSE;

	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
		if (c == ' ')
			c = '_';
		else if (c != '\t' &&
			 (c == '?' || c == '_' || c == '=' || c < 33 || c > 126))
			encode = TRUE;
	} else if (c != ' ' && c != '\t' &&
		   (c == '=' || c < 33 || c > 126)) {
		encode = TRUE;
	}

	/* Include terminating = as well */
	if ((c == ' ' || c == '\t') && qp->line_len + 4 >= qp->max_len) {
		const char *ptr = strchr(qp->linebreak, '\n');
		str_printfa(qp->dest, "=%02X%s", c, qp->linebreak);
		if (ptr != NULL)
			qp->line_len = strlen(ptr+1);
		else
			qp->line_len = 0;
		return;
	}

	/* Include terminating = as well */
	if (qp->line_len + (encode?4:2) >= qp->max_len) {
		str_append(qp->dest, qp->linebreak);
		qp->line_len = 0;
	}

	if (encode) {
		str_printfa(qp->dest, "=%02X", c);
		qp->line_len += 3;
	} else {
		str_append_c(qp->dest, c);
		qp->line_len += 1;
	}
}

void qp_encoder_more(struct qp_encoder *qp, const void *_src, size_t src_size)
{
	const unsigned char *src = _src;
	i_assert(_src != NULL || src_size == 0);
	if (src_size == 0)
		return;
	if (qp->add_header_preamble) {
		size_t used = qp->dest->used;
		qp->add_header_preamble = FALSE;
		str_append(qp->dest, "=?utf-8?Q?");
		qp->line_len = qp->dest->used - used;
	}
	for(unsigned int i = 0; i < src_size; i++) {
		unsigned char c = src[i];
		/* if input is not binary data and we encounter newline
		   convert it as crlf, or if the last byte was CR, preserve
		   CRLF */
		if (c == '\n' &&
		    ((qp->flags & (QP_ENCODER_FLAG_BINARY_DATA|QP_ENCODER_FLAG_HEADER_FORMAT)) == 0 ||
		      qp->last_char == QP_ENCODER_LAST_CR)) {
			str_append_c(qp->dest, '\r');
			str_append_c(qp->dest, '\n');
			/* reset line length here */
			qp->line_len = 0;
			qp->last_char = QP_ENCODER_LAST_ANY;
			continue;
		} else if (qp->last_char == QP_ENCODER_LAST_CR) {
			qp_encode_or_break(qp, '\r');
			qp->last_char = QP_ENCODER_LAST_ANY;
		}

		if (c == ' ' || c == '\t')
			qp->last_char = QP_ENCODER_LAST_WHITE_SPACE;
		else if (c == '\r')
			qp->last_char = QP_ENCODER_LAST_CR;
		else
			qp->last_char = QP_ENCODER_LAST_ANY;

		if (c != '\r')
			qp_encode_or_break(qp, c);
	}
}

void qp_encoder_finish(struct qp_encoder *qp)
{
	switch (qp->last_char) {
	case QP_ENCODER_LAST_CR:
		/* Last char was CR which was not yet encoded */
		qp_encode_or_break(qp, '\r');
		break;
	case QP_ENCODER_LAST_WHITE_SPACE:
		/* Last char was a white space, it must be followed by
		 * a printable char. */
		str_append_c(qp->dest, '=');
		break;
	default:
		break;
	}

	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
	    !qp->add_header_preamble)
		str_append(qp->dest, "?=");
	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0)
		qp->add_header_preamble = TRUE;
	qp->line_len = 0;
	qp->last_char = QP_ENCODER_LAST_ANY;
}
