/*
** Copyright 2011-2026 S. Varshavchik.  See COPYING for
** distribution information.
*/

#include "rfc2045_config.h"
#include	"rfc3676parser.h"
#include <courier-unicode.h>
#include	<stdlib.h>
#include	<string.h>

#define NONFLOWED_WRAP_REDUCE	74

#define NONFLOWED_THRESHOLD_EXCEEDED	30

#define EMIT_LINE_BEGIN() do {			\
		(this->*line_begin_handler)();	\
	} while (0)

#define EMIT_LINE_CONTENTS(uc, cnt) do {			\
		(this->*line_content_handler)(uc, cnt);	\
	} while (0)

#define EMIT_LINE_END() do {			\
		(this->*line_end_handler)();	\
	} while (0)

mail::textplainparser::textplainparser(
	const std::string &charset,
	bool isflowed,
	bool isdelsp
): isflowed{isflowed},
   isdelsp{isdelsp}
{
	if (begin(charset))
		begun=true;
	else
	{
		unknown_charset=true;

		if (begin(unicode::iso_8859_1))
			begun=true;
		else
			return;
	}

	if (!isflowed)
		isdelsp=false; // Sanity check

	line_handler=&textplainparser::scan_crlf;
	content_handler=&textplainparser::start_of_line;
	has_previous_quote_level=false;
	previous_quote_level=0;

	line_begin_handler=&textplainparser::emit_line_begin;
	line_content_handler=&textplainparser::emit_line_contents;
	line_end_handler=&textplainparser::emit_line_end;

	nonflowed_line.clear();
	nonflowed_next_word.clear();

	if (!isflowed)
	{
		line_begin_handler=&textplainparser::nonflowed_line_begin;
		line_content_handler=&textplainparser::nonflowed_line_contents;
		line_end_handler=&textplainparser::nonflowed_line_end_default;
	}
}

mail::textplainparser::~textplainparser()
{
	end();
}

void mail::textplainparser::line_begin(size_t quote_level)
{
	if (quote_level)
	{
		std::vector<char32_t> vec;

		vec.reserve(quote_level+1);
		vec.insert(vec.end(), quote_level, '>');
		vec.push_back(' ');
		line_contents(&vec[0], vec.size());
	}
}

void mail::textplainparser::line_contents(const char32_t *data,
					  size_t cnt)
{
}

void mail::textplainparser::line_flowed_notify()
{
}

void mail::textplainparser::line_end()
{
	char32_t nl='\n';

	line_contents(&nl, 1);
}

int mail::textplainparser::converted(const char32_t *p, size_t nbytes)
{
	// Keep going until there's an error, or everything is consumed.

	// Keep feeding it to the current handler

	while (!errflag && nbytes)
	{
		size_t n=(this->*line_handler)(p, nbytes);

		if (!errflag)
		{
			nbytes -= n;
			p += n;
		}
	}

	return errflag ? -1:0;
}

void mail::textplainparser::end(bool &unicode_errflag)
{
	bool rc=false;

	if (!unicode::iconvert::tou::end(rc) || rc)
		errflag=true;

	linebreakc_callback_base::finish();

	(this->*line_handler)(nullptr, 0);

	unicode_errflag=errflag;
}

size_t mail::textplainparser::scan_crlf(const char32_t *ptr, size_t cnt)
{
	size_t i;

	if (ptr == NULL)
	{
		if (!errflag)
			(this->*content_handler)(NULL, 0);
		return 0;
	}

	for (i=0; ptr && i<cnt; ++i)
	{
		if (ptr[i] == '\r')
			break;
	}

	if (i)
	{
		size_t consumed=0;

		while (i && !errflag)
		{
			size_t n=(this->*content_handler)(ptr, i);

			ptr += n;
			consumed += n;
			i -= n;
		}
		return consumed;
	}

	// Consume the first character, the CR

	line_handler=&mail::textplainparser::scan_crlf_seen_cr;
	return 1;
}

size_t mail::textplainparser::scan_crlf_seen_cr(const char32_t *ptr, size_t cnt)
{
	char32_t cr='\r';

	line_handler=&mail::textplainparser::scan_crlf;

	if (ptr == NULL || *ptr != '\n')
	{
		// CR was not followed by a NL.
		// Restore it in the char stream.

		while (!errflag)
			if ((this->*content_handler)(&cr, 1))
				break;
	}

	return scan_crlf(ptr, cnt);
}

// From this point on, CRLF are collapsed into NLs, so don't need to worry
// about them.

size_t mail::textplainparser::start_of_line(const char32_t *ptr, size_t cnt)
{
	if (ptr == NULL)
	{
		if (has_previous_quote_level)
			EMIT_LINE_END(); // Last line was flowed

		return cnt; // EOF
	}

	// Begin counting the quote level

	content_handler=&mail::textplainparser::count_quote_level;
	quote_level=0;
	return count_quote_level(ptr, cnt);
}

size_t mail::textplainparser::count_quote_level(const char32_t *ptr, size_t cnt)
{
	size_t i;

	if (ptr == NULL) // EOF, pretend that the quote level was counted
	{
		content_handler=&mail::textplainparser::counted_quote_level;
		return counted_quote_level(ptr, cnt);
	}

	for (i=0; i<cnt; ++i)
	{
		if (ptr[i] != '>' || !isflowed)
		{
			content_handler=&mail::textplainparser::counted_quote_level;

			if (i == 0)
				return counted_quote_level(ptr, cnt);
			break;
		}
		++quote_level;
	}

	return i;
}

// This line's quote level has now been counted.

size_t mail::textplainparser::counted_quote_level(const char32_t *ptr, size_t cnt)
{
	was_previous_quote_level=0;

	// If the previous line was flowed and this line has the same
	// quote level, make the flow official.

	if (has_previous_quote_level &&
	    quote_level == previous_quote_level)
	{
		// Remember that this line was flowed into
		was_previous_quote_level=1;
	}
	else
	{
		// If the previous line was flowed, but this line carries
		// a different quote level, force-terminate the previous
		// line, before beginning this line.

		if (has_previous_quote_level)
			EMIT_LINE_END();

		EMIT_LINE_BEGIN();
	}

	has_previous_quote_level=0;
	// Assume this line won't be flowed, until shown otherwise


	if (!isflowed)
	{
		// No space-stuffing, or sig block checking, if this is not
		// flowed content.

		content_handler=&mail::textplainparser::scan_content_line;
		return scan_content_line(ptr, cnt);
	}


	content_handler=&mail::textplainparser::start_content_line;

	if (ptr != NULL && *ptr == ' ')
		return 1; // Remove stuffed space

	return start_content_line(ptr, cnt);
}

// Minor deviation from RFC3676, but this fixes a lot of broken text.
//
// If the previous line was flowed, but this is an empty line (optionally
// space-stuffed), unflow the last line (make it fixed), and this becomes
// a fixed line too. Example:
//
// this is the last end of a paragraph[SPACE]
// [SPACE]
// This is the first line of the next paragraph.
//
// Strict RFC3676 rules will parse this as a flowed line, then a fixed line,
// resulting in no paragraph breaks.

size_t mail::textplainparser::start_content_line(const char32_t *ptr, size_t cnt)
{
	// We'll start scanning for the signature block, as soon as
	// this check is done.

	content_handler=&mail::textplainparser::check_signature_block;
	sig_block_index=0;

	if (ptr && *ptr == '\n' && was_previous_quote_level)
	{
		EMIT_LINE_END();
		EMIT_LINE_BEGIN();
		was_previous_quote_level=0;
	}

	return check_signature_block(ptr, cnt);
}


static const char32_t sig_block[]={'-', '-', ' '};

size_t mail::textplainparser::check_signature_block(const char32_t *ptr, size_t cnt)
{
	if (ptr && *ptr == sig_block[sig_block_index])
	{
		if (++sig_block_index == sizeof(sig_block)
		    /sizeof(sig_block[0]))

			// Well, it's there, but does a NL follow?
			content_handler=&mail::textplainparser::seen_sig_block;
		return 1;
	}

	return seen_notsig_block(ptr, cnt);
}

size_t mail::textplainparser::seen_sig_block(const char32_t *ptr, size_t cnt)
{
	if (ptr == NULL || *ptr == '\n')
	{
		// If the previous line was flowed, the sig block is not
		// considered to be flowable-into content, so terminate
		// the previous line before emitting the sig block.

		if (was_previous_quote_level)
		{
			EMIT_LINE_END();
			EMIT_LINE_BEGIN();
			was_previous_quote_level=0;
		}

		// Pass through the sig block

		content_handler=&mail::textplainparser::start_of_line;

		EMIT_LINE_CONTENTS(sig_block,
				   sizeof(sig_block)/sizeof(sig_block[0]));
		EMIT_LINE_END();
		return ptr ? 1:0;
	}

	return seen_notsig_block(ptr, cnt);
}

// This is not a sig block line

size_t mail::textplainparser::seen_notsig_block(const char32_t *newptr, size_t newcnt)
{
	const char32_t *ptr;
	size_t i;

	if (was_previous_quote_level)
		emit_line_flowed_wrap();

	content_handler=&mail::textplainparser::scan_content_line;

	ptr=sig_block;
	i=sig_block_index;

	while (i && !errflag)
	{
		size_t n=(this->*content_handler)(ptr, i);

		ptr += n;
		i -= n;
	}

	return (this->*content_handler)(newptr, newcnt);
}

// Pass through the line, until encountering an NL, or a space in flowable
// content.

size_t mail::textplainparser::scan_content_line(const char32_t *ptr, size_t cnt)
{
	size_t i;

	for (i=0; ptr && i<cnt && ptr[i] != '\n' &&
	     (ptr[i] != ' ' || !isflowed); ++i)
	{
		;
	}

	// Pass through anything before the NL or potentially flowable SP

 	if (i)
		EMIT_LINE_CONTENTS(ptr, i);

	if (i)
		return i;

	if (ptr && ptr[i] == ' ')
	{
		content_handler=&mail::textplainparser::seen_content_sp;
		return 1;
	}

	// NL. This line does not flow

	EMIT_LINE_END();

	content_handler=&mail::textplainparser::start_of_line;

	return ptr ? 1:0;
}

size_t mail::textplainparser::seen_content_sp(const char32_t *ptr, size_t cnt)
{
	char32_t sp=' ';

	content_handler=&mail::textplainparser::scan_content_line;

	if (ptr == NULL || *ptr != '\n')
	{
		// SP was not followed by the NL. Pass through the space,
		// then resume scanning.
		EMIT_LINE_CONTENTS(&sp, 1);
		return (this->*content_handler)(ptr, cnt);
	}

	// NL after a SP -- flowed line

	if (!isdelsp)
		EMIT_LINE_CONTENTS(&sp, 1);

	has_previous_quote_level=1;
	previous_quote_level=quote_level;
	content_handler=&mail::textplainparser::start_of_line;
	return ptr ? 1:0;
}

// -----------------------------------------------------------------------------
// At this point, the processing has reduced to the following API:
//
// + begin logical line
//
// + contents of the logical line (multiple consecutive invocations)
//
// + the logical line has flowed onto the next physical line
//
// + end of logical line
//
// The third one, logical line flowed, is normally used for flowed text,
// by definition. But, it may also be get used if non-flowed text gets
// rewrapped when broken formatting is detected.
//
// Provide default implementations of the other three API calls that
// simply invoke the corresponding user callback.

void mail::textplainparser::emit_line_begin()
{
	if (!errflag)
		line_begin(quote_level);
}

void mail::textplainparser::emit_line_flowed_wrap()
{
	if (!errflag)
		line_flowed_notify();
}

void mail::textplainparser::emit_line_contents(const char32_t *uc, size_t cnt)
{
	if (!errflag)
		line_contents(uc, cnt);
}

void mail::textplainparser::emit_line_end()
{
	if (!errflag)
		line_end();
}

void mail::textplainparser::operator<<(const std::string_view &text)
{
	if (!begun)
		return;

	if (!unicode::iconvert::tou::operator()(text.data(), text.size()))
		errflag=true;
}

// When processing a non-flowed text, handle broken mail formatters (I'm
// looking at you, Apple Mail) that spew out quoted-printable content with
// each decoded line forming a single paragraph. This is heuristically
// detected by looking for lines that exceed a wrapping threshold, then
// rewrapping them.
// Redefine the three line API calls to launder the logical line via
// the linebreak API.

// A non-flowed line begins. Initialize the linebreaking module.
void mail::textplainparser::nonflowed_line_begin()
{
	unicode::linebreakc_callback_base::finish();
	unicode::linebreakc_callback_base::set_opts(UNICODE_LB_OPT_PRBREAK);

	nonflowed_line.clear();
	nonflowed_next_word.clear();

	nonflowed_line_width=0;
	nonflowed_next_word_width=0;

	nonflowed_line_process=&mail::textplainparser::initial_nonflowed_line;
	nonflowed_line_end=&mail::textplainparser::initial_nonflowed_end;
	emit_line_begin(); // Fallthru - user callback

	nonflowed_line_target_width=
		quote_level < NONFLOWED_WRAP_REDUCE - 20 ?
		NONFLOWED_WRAP_REDUCE - quote_level:20;
}

// Process contents of non-flowed lines. The contents are submitted to the
// linebreaking API.

void mail::textplainparser::nonflowed_line_contents(
	const char32_t *uc,
	size_t cnt
)
{
	linebreakc_callback_base::operator()(uc, uc+cnt);
}

// End of non-flowed content. Terminate the linebreaking API, then invoke
// the current end-of-line handler.

void mail::textplainparser::nonflowed_line_end_default()
{
	unicode::linebreakc_callback_base::finish();
	(this->*nonflowed_line_end)();
	emit_line_end(); // FALLTHRU
}

// Callback from the linebreaking API, gives us the next unicode character
// and its linebreak property. Look up the unicode character's width, then
// invoke the current handler.
int mail::textplainparser::callback(int opportunity, char32_t ch)
{
	// Forward the request to the current handler.
	(this->*nonflowed_line_process)(
		opportunity,
		ch,
		unicode_wcwidth(ch)
	);

	return 0;
}

void mail::textplainparser::initial_nonflowed_line(
	int linebreak_opportunity,
	char32_t ch,
	size_t ch_width
)
{
	// Collect words into nonflowed_line as long as it fits within the
	// targeted width.

	if (linebreak_opportunity != UNICODE_LB_NONE &&
	    nonflowed_line_width +
	    nonflowed_next_word_width <= nonflowed_line_target_width)
	{
		nonflowed_line.append(nonflowed_next_word);
		nonflowed_line_width +=
			nonflowed_next_word_width;

		nonflowed_next_word.clear();
		nonflowed_next_word_width=0;
	}

	// Add the character to the growing word.
	// If the line's size now exceeds the target width by quite a bit,
	// we've had enough!

	nonflowed_next_word.push_back(ch);
	nonflowed_next_word_width += ch_width;

	if (nonflowed_line_width + nonflowed_next_word_width
	    > nonflowed_line_target_width
	    + NONFLOWED_THRESHOLD_EXCEEDED)
		begin_forced_rewrap();
}

void mail::textplainparser::initial_nonflowed_end()
{
	emit_line_contents(nonflowed_line.data(), nonflowed_line.size());
	emit_line_contents(
		nonflowed_next_word.data(),
		nonflowed_next_word.size()
	);
}

void mail::textplainparser::check_abnormal_line()
{
	size_t n, i;
	const char32_t *p;

	if (nonflowed_line.size() > 0)
		return;

	// Extreme times call for extreme measures

	n=nonflowed_next_word.size();
	p=nonflowed_next_word.data();

	for (i=n; i>0; --i)
	{
		if (i < n && unicode_grapheme_break(p[i-1], p[i]))
		{
			n=i;
			break;
		}
	}

	nonflowed_line.append(p, n);
	nonflowed_next_word.erase(0, n);

	// Recalculate the width of the growing word, now.

	nonflowed_next_word_width=0;
	p=nonflowed_next_word.data();

	for (i=0; i<nonflowed_next_word.size(); ++i)
		nonflowed_next_word_width +=
			unicode_wcwidth(p[i]);
}

void mail::textplainparser::emit_rewrapped_line()
{
	check_abnormal_line();
	emit_line_contents(nonflowed_line.data(), nonflowed_line.size());
	emit_line_flowed_wrap();

	// nonflowed_line is now empty
	nonflowed_line.clear();
	nonflowed_line_width=0;
}

void mail::textplainparser::begin_forced_rewrap()
{
	nonflowed_line_process=&mail::textplainparser::forced_rewrap_line;
	nonflowed_line_end=&mail::textplainparser::forced_rewrap_end;
	emit_rewrapped_line();
}

void mail::textplainparser::forced_rewrap_line(
	int linebreak_opportunity,
	char32_t ch,
	size_t ch_width
)
{
	if (linebreak_opportunity != UNICODE_LB_NONE)
	{
		// Found a linebreaking opportunity

		if (nonflowed_line_width
		    + nonflowed_next_word_width
		    > nonflowed_line_target_width)
		{
			// Accumulated word is too long
			emit_rewrapped_line();
		}

		nonflowed_line.append(nonflowed_next_word);
		nonflowed_line_width +=
			nonflowed_next_word_width;
		nonflowed_next_word.clear();
		nonflowed_next_word_width=0;
	}

	// Check for another excessively long line.

	if (nonflowed_line_width == 0 &&
	    nonflowed_next_word_width + ch_width
	    > nonflowed_line_target_width)
	{
		emit_rewrapped_line();
	}

	nonflowed_next_word.push_back(ch);
	nonflowed_next_word_width += ch_width;
}

void mail::textplainparser::forced_rewrap_end()
{
	initial_nonflowed_end(); // Same logic, for now
}
