#include "config.h"

// Declaration
#include "Recoder.hpp"

// STDC++
#include <cassert>

// C libraries
#include <idna.h>

// C++ libraries
#include <ucommon/string.h>

// Local
#include "auto_ptr_compat.hpp"
#include "Exception.hpp"
#include "Text.hpp"
#include "Misc.hpp"

namespace UI {
namespace Util {

Conversion::Conversion(char const * const cStr, size_t const cSize)
	:cStr_(cStr)
	,cSize_(cSize)
{
	assert(cStr);
}

Conversion::~Conversion()
{}

char const * Conversion::get() const
{
	return cStr_;
}

size_t Conversion::getSize() const
{
	return cSize_;
}

std::string Conversion::getString() const
{
	return std::string(cStr_, cSize_);
}



StdFreeConversion::StdFreeConversion(char * cStr, size_t const cSize)
	:Conversion(cStr, cSize)
	,freeObject_(cStr)
{}

StdFreeConversion::~StdFreeConversion()
{
	Sys::free(freeObject_);
}

StringConversion::StringConversion(std::string * str)
	:Conversion(str->c_str(), str->size())
	,delObject_(str)
{}

StringConversion::~StringConversion()
{
	delete delObject_;
}

Converter::Converter(std::string const & inEnc, std::string const & outEnc, bool const & sloppy)
	:inEnc_(inEnc)
	,outEnc_(outEnc)
	,sloppy_(sloppy)
{}

Converter::~Converter()
{}

Conversion const * Converter::make(char const * cStr) const
{
	return make(cStr, std::strlen(cStr));
}

Conversion const * Converter::make(std::string const & str) const
{
	return make(str.c_str(), str.size());
}

std::string Converter::getID() const
{
	return inEnc_ + ".." + outEnc_ + "(" + (sloppy_ ? "sloppy" : "precise") + ")";
}


#ifndef WIN32
LibRecodeConverter::LibRecodeConverter(std::string const & inEnc, std::string const & outEnc, bool const & sloppy)
	:Converter(inEnc, outEnc, sloppy)
	,outer_(0)
	,request_(0)
{
	std::string const encInOut(inEnc + ".." + outEnc);

	if (! ((outer_ = ::recode_new_outer(1)) &&
				 (request_ = ::recode_new_request(outer_)) &&
				 ::recode_scan_request(request_, encInOut.c_str())))
	{
		if (request_) { ::recode_delete_request(request_); };
		if (outer_)   { ::recode_delete_outer(outer_); };
		UI_THROW_CODE(EncUnknown_, "LibRecodeConverter: Some encoding is unknown: " + getID());
	}

	// Non run-dependent task configuration
}

LibRecodeConverter::~LibRecodeConverter()
{
	::recode_delete_request(request_);
	::recode_delete_outer(outer_);
}

// Start: From librecode
/* Guarantee four NULs at the end of the output memory buffer for TASK, yet
   not counting them as data.  The number of NULs should depend on the goal
   charset: often one, but two for UCS-2 and four for UCS-4.  FIXME!  */
void LibRecodeConverter::guarantee_nul_terminator(RECODE_TASK task)
{
	if (task->output.cursor + 4 >= task->output.limit)
	{
		RECODE_OUTER outer = task->request->outer;
		size_t old_size = task->output.cursor - task->output.buffer;
		size_t new_size = task->output.cursor + 4 - task->output.buffer;

		/* FIXME: Rethink about how the error should be reported. */
		if (REALLOC (task->output.buffer, new_size, char))
		{
			task->output.cursor = task->output.buffer + old_size;
			task->output.limit = task->output.buffer + new_size;
		}
	}
	task->output.cursor[0] = NUL;
	task->output.cursor[1] = NUL;
	task->output.cursor[2] = NUL;
	task->output.cursor[3] = NUL;
}
// End: From librecode

Conversion const * LibRecodeConverter::make(char const * const buf, size_t const bufSize) const
{
	class Task
	{
	public:
		Task(RECODE_REQUEST request, char const * const buf, size_t const bufSize, bool const & sloppy)
			:task_(::recode_new_task(request))
		{
			// Task config
			task_->strategy = RECODE_SEQUENCE_IN_MEMORY;
			task_->fail_level = sloppy ? RECODE_INVALID_INPUT : RECODE_NOT_CANONICAL;

			task_->input.buffer = buf;
			task_->input.cursor = buf;
			task_->input.limit = buf + bufSize;
			task_->output.buffer = 0;
			task_->output.cursor = 0;
			task_->output.limit = 0;
		}
		~Task()
		{
			::recode_delete_task(task_);
		}
		Conversion const * make()
		{
			if (!::recode_perform_task(task_))
			{
				// So far allocated buffer must be freed
				Sys::free(task_->output.buffer);
				UI_THROW_CODE(ConversionErr_, "LibRecodeConverter: Task error, \"error_so_far\"=" + tos(task_->error_so_far));
			}
			guarantee_nul_terminator(task_);
			return new StdFreeConversion(task_->output.buffer, task_->output.cursor-task_->output.buffer);
		}
		RECODE_TASK task_;
	};
	return Task(request_, buf, bufSize, sloppy_).make();
}
#endif


IConvConverter::IConvConverter(std::string const & inEnc, std::string const & outEnc, const bool & sloppy )
	:Converter(inEnc, outEnc, sloppy)
	,conversion_(Sys::iconv_open(outEnc_.c_str(), inEnc_.c_str()))
{
// Linux uses gcc with iconv build-in. The non-standard iconvctl feature is in GNU libiconv only.
#ifndef __linux__
	const int transliterate = (int)sloppy;
	iconvctl( conversion_, ICONV_SET_TRANSLITERATE, (void *)&transliterate );
#endif
}

IConvConverter::~IConvConverter()
{
	try { Sys::iconv_close(conversion_); }
	catch (...) {}
}

Conversion const * IConvConverter::make(char const * const buf, size_t const bufSize) const
{
	size_t nLength = bufSize;
	const char* pInput = buf;
	char * dest((char *) Sys::malloc(nLength + 1));

	assert(dest);
	size_t destSize(0);

	int nIteration(1);
	while (true)
	{
		const char* pIn = pInput;
		char* pOut = dest;
		size_t nIn = nLength;
		size_t nOut = nLength * nIteration;

#ifdef WIN32
		size_t code = iconv( conversion_, &pIn, &nIn, &pOut, &nOut );
#else
		size_t code = iconv( conversion_, const_cast<char**>(&pIn), &nIn, &pOut, &nOut );
#endif
		dest[nLength*nIteration-nOut] = 0;
		if( ( (size_t)(-1) == code ) && ( E2BIG == errno ) )
		{
			if (NULL != dest)
			{
				// Huh?
				Sys::free(dest);
				dest = 0;
			}
			nIteration++;
			dest = (char *) Sys::malloc(nLength * nIteration + 1);
		}
		else
		{
			if ((size_t)(-1) == code)
			{
				/** @bug memory leak (malloc)? */
				UI_THROW_CODE(ConversionErr_,
				              "IConvConverter: Conversion error: " + getID());
			}
			destSize = nLength*nIteration-nOut;
			break;
		}
	}

	return new StdFreeConversion(dest, destSize);
}


URLConverter::URLConverter(bool const encode)
	:Converter(encode ? "data" : "url", encode ? "url" : "data")
{}

Conversion const * URLConverter::make(char const * const buf, size_t const bufSize) const
{
	// Note: Encoder and decoder directly from c-str would be nice for efficiency.
	std::string src(buf, bufSize);
	return new StringConversion(new std::string((inEnc_ == "data" ? encode(src) : decode(src))));
}

unsigned char URLConverter::number(unsigned char inNum)
{
	unsigned char outNum = 0;

	if ((inNum >= MinNum && inNum <= MaxNum) || (inNum >= MinAlp && inNum <= MaxAlp) || (inNum >= MinAlp2 && inNum <= MaxAlp2))
	{
		if (inNum > MaxNum)
		{
			if (inNum > MaxAlp)
			{
				outNum = inNum-AlpMod2;
			}
			else
			{
				outNum = inNum-AlpMod;
			}
		}
		else
		{
			outNum = inNum-NumMod;
		}
	}
	return outNum;
}

std::string URLConverter::encode(std::string const & url)
{
	unsigned char check = 0;
	char buf[4];
	int i = 0;

	std::string out("");

	for (i=0 ; i < (int) url.size(); i++)
	{
		check = (url.data())[i];
		switch ( check )
		{
		case 0x20:                            // "(space)"
			out.append( 1, '+' );
			break;
		case 0x2a:                            // "*"
		case 0x2d:                            // "-"
		case 0x2e:                            // "."
		case 0X5f:                            // "_"
			out.append( 1, check );
			break;
		default:
			if ((check >= 0x21 && check <= 0x2f) ||      // "!" - "/"
					(check >= 0x3a && check <= 0x40) ||      // ":" - "@"
					(check >= 0x5b && check <= 0x60) ||      // "[" - "'"
					(check >= 0x7b && check <= 0x7f) ||      // "{" - "~"
					(check > 0x7f))                          // non-ASCII
			{
				std::sprintf(buf, "%%%X", check);
				out.append(buf);
			}
			else
			{
				out.append(1, check);
			}
		}
	}
	return out;
}

std::string URLConverter::decode(std::string const & url)
{
	unsigned char check = 0;
	unsigned char low, high, ascii = 0;
	int i = 0;

	std::string out("");

	for ( i=0 ; i< (int) url.size() ; i++ )
	{
		check = (url.data())[i];
		switch (check)
		{
		case 0x2b:                                // "+"
			out.append( 1, ' ' );
			break;
		case 0x25:                                // "%"
			high = number((url.data())[i+1]);
			low = number((url.data())[i+2]);
			high = (high << 4);
			ascii = (low | high);
			out.append( 1, ascii );
			i = i+2;
			break;
		default:
			out.append( 1, check );
			break;
		}
	}
	return out;
}


Cpp2Base64Converter::Cpp2Base64Converter(bool const encode)
	:Converter(encode ? "data" : "b64", encode ? "b64" : "data")
{}

Conversion const * Cpp2Base64Converter::make(char const * const buf, size_t const bufSize) const
{
	char * dest(0);
	size_t destSize(0);

	if (inEnc_ == "data")
	{
		long lDestSize((bufSize+2) / 3*4 + 1);
		dest = (char*) Sys::malloc(lDestSize);
		ucommon::String::b64encode(dest, (const unsigned char *)buf, bufSize, lDestSize);
		destSize = lDestSize-1;
	}
	else
	{
		long lDestSize(bufSize / 4*3 + 1);
		dest = (char*) Sys::malloc(lDestSize);
		ucommon::String::b64decode((unsigned char *)dest, buf, lDestSize);
		destSize = lDestSize-1;
	}
	return new StdFreeConversion(dest, destSize);
}


Rfc2047Converter::Rfc2047Converter(std::string const & inCharEnc)
	:Converter(inCharEnc, "rfc2047")
{}

Conversion const * Rfc2047Converter::make(char const * const buf, size_t const bufSize) const
{
	std::string * result(new std::string());

	try
	{
		/* Check if further encoding is neccessary */
		bool noEncoding(true);
		for (size_t i(0); noEncoding && i < bufSize; ++i)
		{
			noEncoding = noEncoding && ((unsigned char) buf[i] <= 127);
		}

		if (noEncoding)
		{
			*result = buf;
		}
		else
		{
			/* at least one special character (>127) occurred -> encode */
			/* encoded characters look like this: =XX where XX is hexadecimal*/
			/* this small buffer holds one encoded char.*/
			char const * hexTable="0123456789ABCDEF";
			char hex[4];
			hex[0]='=';
			hex[3]=0;
			/* header with character encoding */
			result->append("=?");
			result->append(inEnc_);
			result->append("?Q?");
			/* and now the content */
			for (size_t i(0); i < bufSize; ++i)
			{
				if (buf[i] == ' ')
				{
					*result += '_';
				}
				else if ((unsigned char) buf[i] > 127 || buf[i] == '\t' || buf[i] == '?' || buf[i] == '=' || buf[i] == '_')
				{
					hex[1]=hexTable[(unsigned char) buf[i] >> 4];
					hex[2]=hexTable[(unsigned char) buf[i] &0xF];
					result->append(hex);
				}
				else
				{
					*result += buf[i];
				}
			}
			result->append("?=");
		}
	}
	catch(...)
	{
		delete result;
		throw;
	}

	return new StringConversion(result);
}


Rfc3490Utf8Converter::Rfc3490Utf8Converter(bool const encode)
	:Converter(encode ? "UTF-8" : "idn", encode ? "idn" : "UTF-8")
{}

Conversion const * Rfc3490Utf8Converter::make(char const * const buf, size_t const bufSize) const
{
	// bufSize not used by ::idna_* funcs (must be 0-terminated); this line avoids compiler warnings only
	if (bufSize) {};

	char * output(0);
	int result;
	if (inEnc_ == "UTF-8")
	{
		result = ::idna_to_ascii_8z(buf, &output, 0);
	}
	else
	{
		result = ::idna_to_unicode_8z8z(buf, &output, 0);
	}
	if (result != IDNA_SUCCESS)
	{
		std::free(output);
		UI_THROW_CODE(ConversionErr_, "IDNA converter error: " + std::string(::idna_strerror((Idna_rc) result)));
	}
	return new StdFreeConversion(output, std::strlen(output));
}


IdnEMailConverter::IdnEMailConverter(std::string const & inEnc)
	:Converter(inEnc, "idn-email")
{}

Conversion const * IdnEMailConverter::make(char const * const buf, size_t const bufSize) const
{
	class T
	{
	public:
		static bool isAllowedChar(char const & c)
		{
			unsigned short int const i(c);

			return (i > 127)           // Non-ASCII (UTF-8 multibyte)
				|| (i >= 48 && i <= 57)  // ASCII: 0-9
				|| (i >= 65 && i <= 90)  // ASCII: A-Z
				|| (i >= 97 && i <= 122) // ASCII: a-z
				|| (c == '-')
				|| (c == '.');
		}
	};

	// First, we need UNICODE: UTF-8
	std::string result(RecoderCache::get(inEnc_, "UTF-8", true).run(buf, bufSize));

	// Find "@"
	std::string::size_type atPos(result.find('@'));
	while (atPos != std::string::npos)
	{
		std::string::size_type spacePos(atPos+1);
		while (spacePos < result.size() && T::isAllowedChar(result[spacePos]))
		{
			++spacePos;
		}

		std::string const domain(result.substr(atPos+1, spacePos == std::string::npos ? std::string::npos : spacePos-atPos));
		std::string const idnDomain(RecoderCache::get("UTF-8", "idn").run(domain));
		result.replace(atPos+1, domain.size(), idnDomain);
		atPos = result.find('@', atPos+1);
	}

	// Lastly, re-encode into original char encoding
	return new StringConversion(new std::string(RecoderCache::get("UTF-8", inEnc_, true).run(result)));
}


bool Recoder::isEnc(std::string const & enc, std::string const & encs) const
{
	return isToken(enc, encs, ",", -1);
}

Recoder::Recoder(std::string const & inEnc, std::string const & outEnc, bool const & sloppy)
	:converter_(0)
{
	if (isEnc(inEnc, "data") && isEnc(outEnc, "base64,b64,64"))
	{
		converter_ = new Cpp2Base64Converter(true);
	}
	else if (isEnc(inEnc, "base64,b64,64") && isEnc(outEnc, "data"))
	{
		converter_ = new Cpp2Base64Converter(false);
	}
	else if (isEnc(inEnc, "data") && isEnc(outEnc, "url,urlenc"))
	{
		converter_ = new URLConverter(true);
	}
	else if (isEnc(inEnc, "url,urlenc") && isEnc(outEnc, "data"))
	{
		converter_ = new URLConverter(false);
	}
	else if (isEnc(outEnc, "rfc2047"))
	{
		converter_ = new Rfc2047Converter(inEnc);
	}
	else if (isEnc(inEnc, "utf-8,utf8") && isEnc(outEnc, "rfc3490,idn"))
	{
		converter_ = new Rfc3490Utf8Converter(true);
	}
	else if (isEnc(inEnc, "rfc3490,idn") && isEnc(outEnc, "utf-8,utf8"))
	{
		converter_ = new Rfc3490Utf8Converter(false);
	}
	else if (isEnc(outEnc, "idn-email"))
	{
		converter_ = new IdnEMailConverter(inEnc);
	}
	else
	{
#ifdef WIN32
		converter_ = new IConvConverter(inEnc, outEnc, sloppy);
#else
		converter_ = new LibRecodeConverter(inEnc, outEnc, sloppy);
#endif
	}
	assert(converter_);
}

Recoder::~Recoder()
{
	delete converter_;
}

Conversion const * Recoder::make(char const * src, size_t srcSize) const
{
	return converter_->make(src, srcSize);
}

Conversion const * Recoder::make(char const * src) const
{
	return make(src, std::strlen(src));
}

std::string Recoder::run(char const * const buf, size_t const bufSize) const
{
	UI::Util::auto_ptr<Conversion const> conv(make(buf, bufSize));
	return conv->getString();
}

std::string Recoder::run(char const * const src) const
{
	UI::Util::auto_ptr<Conversion const> conv(make(src));
	return conv->getString();
}

std::string Recoder::run(std::string const & src) const
{
	UI::Util::auto_ptr<Conversion const> conv(make(src.c_str(), src.size()));
	return conv->getString();
}

RecoderCache * RecoderCache::instance_(0);

Recoder const & RecoderCache::get(std::string const & inEnc, std::string const & outEnc, bool const & sloppy)
{
	if (!instance_) { instance_ = new RecoderCache(); }
	std::string const key(inEnc + ".." + outEnc + "/" + tos(sloppy));

	RecoderMap::const_iterator i(instance_->cache_.find(key));

	if (i == instance_->cache_.end())
	{
		instance_->cache_[key] = new Recoder(inEnc, outEnc, sloppy);
		return *(instance_->cache_[key]);
	}
	else
	{
		return *i->second;
	}
}

RecoderCache::~RecoderCache()
{
	delStringMap(cache_);
}

}}
