/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <log4cxx/logstring.h>
#include <log4cxx/helpers/transform.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/helpers/widelife.h>
#include <functional>

using namespace LOG4CXX_NS;
using namespace LOG4CXX_NS::helpers;

namespace
{
using CharProcessor = std::function<bool(LogString&, int)>;

// Allowable XML 1.0 characters are:
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
void appendValidCharacters(LogString& buf, const LogString& input, CharProcessor handler = {}, bool handleValidCharacters = false)
{
	static const unsigned int specials[] =
		{ 0x22 /* " */
		, 0x26 /* & */
		, 0x3C /* < */
		, 0x3E /* > */
		, 0x00
		};
	auto start = input.begin();
	for (auto nextCodePoint = start; input.end() != nextCodePoint; )
	{
		auto lastCodePoint = nextCodePoint;
		auto ch = Transcoder::decode(input, nextCodePoint);
		if (nextCodePoint == lastCodePoint) // failed to decode input?
		{
			// Skip the undecodable run and keep escaping the remaining input
			// instead of discarding it; the run collapses to one replacement.
			for (++nextCodePoint; nextCodePoint != input.end(); ++nextCodePoint)
			{
				auto probe = nextCodePoint;
				Transcoder::decode(input, probe);
				if (probe != nextCodePoint) // next unit starts a decodable sequence
					break;
			}
		}
		else if (0xD800 <= ch && ch <= 0xDFFF)
		{
			// RFC 3629 §3 explicitly forbids surrogate-half values in UTF-8
			ch = 0xFFFF;
		}
		else if (((0x20 <= ch && ch <= 0xD7FF) &&
				specials[0] != ch &&
				specials[1] != ch &&
				specials[2] != ch &&
				specials[3] != ch) ||
			(0x9 == ch || 0xA == ch || 0xD == ch) ||
			(0xE000 <= ch && ch <= 0xFFFD) ||
			(0x10000 <= ch && ch <= 0x10FFFF))
		{
			LogString escaped;
			if (handleValidCharacters && handler && handler(escaped, ch))
			{
				if (start != lastCodePoint)
					buf.append(start, lastCodePoint);
				buf.append(escaped);
				start = nextCodePoint;
			}
			continue;
		}

		if (start != lastCodePoint)
			buf.append(start, lastCodePoint);
		start = nextCodePoint;
		switch (ch)
		{
			case 0: // Do not output a NUL character
				break;
			case 0x22:
				buf.append(LOG4CXX_STR("&quot;"));
				break;

			case 0x26:
				buf.append(LOG4CXX_STR("&amp;"));
				break;

			case 0x3C:
				buf.append(LOG4CXX_STR("&lt;"));
				break;

			case 0x3E:
				buf.append(LOG4CXX_STR("&gt;"));
				break;

			case 0xFFFF: // invalid sequence
				Transform::appendCharacterReference(buf, 0xFFFD); // The Unicode replacement character
				break;

			default:
				if (handler && !handler(buf, ch))
					Transform::appendCharacterReference(buf, ch);
				break;
		}
	}
	buf.append(start, input.end());
}

bool appendCharacterReferenceHandler(LogString& buf, int ch)
{
	Transform::appendCharacterReference(buf, ch);
	return true;
}

bool appendAttributeCharacterReference(LogString& buf, int ch)
{
	if (0x27 == ch || 0x9 == ch || 0xA == ch || 0xD == ch)
	{
		Transform::appendCharacterReference(buf, ch);
		return true;
	}

	return false;
}

} // namespace

void Transform::appendEscapingCDATA(
	LogString& buf, const LogString& input)
{
	static const LogString CDATA_END(LOG4CXX_STR("]]>"));
	const LogString::size_type CDATA_END_LEN = 3;
	static const LogString CDATA_EMBEDED_END(LOG4CXX_STR("]]&gt;<![CDATA["));
	auto start = input.begin();
	for (auto nextCodePoint = start; input.end() != nextCodePoint; )
	{
		bool cdataEnd = false;
		auto lastCodePoint = nextCodePoint;
		auto ch = Transcoder::decode(input, nextCodePoint);
		if (nextCodePoint == lastCodePoint) // failed to decode input?
		{
			// Skip the undecodable run and keep escaping the remaining input
			// instead of discarding it; the run collapses to one replacement.
			for (++nextCodePoint; nextCodePoint != input.end(); ++nextCodePoint)
			{
				auto probe = nextCodePoint;
				Transcoder::decode(input, probe);
				if (probe != nextCodePoint) // next unit starts a decodable sequence
					break;
			}
			ch = 0xFFFD; // The Unicode replacement character
		}
		else if (CDATA_END[0] == ch && input.end() != nextCodePoint)
		{
			lastCodePoint = nextCodePoint;
			if (CDATA_END[1] != Transcoder::decode(input, nextCodePoint) ||
				input.end() == nextCodePoint ||
				CDATA_END[2] != Transcoder::decode(input, nextCodePoint))
			{
				nextCodePoint = lastCodePoint;
				continue;
			}
			lastCodePoint = nextCodePoint;
			cdataEnd = true;
		}
		else if ((0x20 <= ch && ch <= 0xD7FF) ||
				(0x9 == ch || 0xA == ch || 0xD == ch) ||
				(0xE000 <= ch && ch <= 0xFFFD) ||
				(0x10000 <= ch && ch <= 0x10FFFF))
		{
			continue;
		}

		if (start != lastCodePoint)
			buf.append(start, lastCodePoint);
		if (cdataEnd)
			buf.append(CDATA_EMBEDED_END);
		else if (0 != ch)
			appendCharacterReference(buf, ch);
		start = nextCodePoint;
	}
	buf.append(start, input.end());
}

void Transform::appendCharacterReference(LogString& buf, unsigned int ch)
{
	auto toHexDigit = [](int ch) -> int
	{
		return (10 <= ch ? (0x61 - 10) : 0x30) + ch;
	};
	buf.push_back('&');
	buf.push_back('#');
	buf.push_back('x');
	if (0xFFFFFFF < ch)
		buf.push_back(toHexDigit((ch & 0xF0000000) >> 28));
	if (0xFFFFFF < ch)
		buf.push_back(toHexDigit((ch & 0xF000000) >> 24));
	if (0xFFFFF < ch)
		buf.push_back(toHexDigit((ch & 0xF00000) >> 20));
	if (0xFFFF < ch)
		buf.push_back(toHexDigit((ch & 0xF0000) >> 16));
	if (0xFFF < ch)
		buf.push_back(toHexDigit((ch & 0xF000) >> 12));
	if (0xFF < ch)
		buf.push_back(toHexDigit((ch & 0xF00) >> 8));
	if (0xF < ch)
		buf.push_back(toHexDigit((ch & 0xF0) >> 4));
	buf.push_back(toHexDigit(ch & 0xF));
	buf.push_back(';');
}

void Transform::appendEscapingTags(LogString& buf, const LogString& input)
{
	appendValidCharacters(buf, input, appendCharacterReferenceHandler);
}

void Transform::appendEscapingAttribute(LogString& buf, const LogString& input)
{
	appendValidCharacters(buf, input, appendAttributeCharacterReference, true);
}

void Transform::appendLegalCharacters(LogString& buf, const LogString& input)
{
	appendValidCharacters(buf, input);
}
