#include "plist.h"
#include <oak/oak.h>
#include <text/format.h>
#include <text/hexdump.h>
#include <text/utf8.h>

namespace
{
	struct is_composite : boost::static_visitor<bool>
	{
		bool operator() (bool flag) const                       { return false; }
		bool operator() (int32_t i) const                       { return false; }
		bool operator() (uint64_t i) const                      { return false; }
		bool operator() (std::string const& str) const          { return false; }
		bool operator() (std::vector<char> const& data) const   { return false; }
		bool operator() (oak::date_t const& date) const         { return false; }
		bool operator() (plist::array_t const& array) const     { return true;  }
		bool operator() (plist::dictionary_t const& dict) const { return true;  }
	};

	struct fits_single_line : boost::static_visitor<bool>
	{
		bool operator() (bool flag) const                     { return true; }
		bool operator() (int32_t i) const                     { return true; }
		bool operator() (uint64_t i) const                    { return true; }
		bool operator() (std::string const& str) const        { return true; }
		bool operator() (std::vector<char> const& data) const { return true; }
		bool operator() (oak::date_t const& date) const       { return true; }

		bool operator() (plist::array_t const& array) const
		{
			bool singleLineItems = true;
			bool compositeItems = false;
			iterate(it, array)
			{
				singleLineItems = singleLineItems && boost::apply_visitor(*this, *it);
				compositeItems = compositeItems || boost::apply_visitor(is_composite(), *it);
			}
			return singleLineItems && (array.size() <= 1 || !compositeItems);
		}

		bool operator() (plist::dictionary_t const& dict) const
		{
			bool res = dict.size() <= 1;
			iterate(it, dict)
				res = res && boost::apply_visitor(*this, it->second);
			return res;
		}
	};
}

static std::string double_quote_escape (std::string const& ch)
{
	uint32_t const val = utf8::to_ch(ch);
	switch(val)
	{
		case '\0': return "\\000";
		case '\t': return "\\t"  ;
		case '\n': return "\\n"  ;
		case '\f': return "\\f"  ;
		case '\r': return "\\r"  ;
		case '\e': return "\\033";
		case '\\': return "\\\\" ;
		case '"':  return "\\\"" ;

		default:
		{
			static CFCharacterSetRef const illegal_set = CFCharacterSetGetPredefined(kCFCharacterSetIllegal);
			static CFCharacterSetRef const control_set = CFCharacterSetGetPredefined(kCFCharacterSetControl);

			if(0x20 <= val && val <= 0x7E)
				return ch;
			else if(iscntrl(val) || 0x7F < val && CFCharacterSetIsLongCharacterMember(control_set, val))
				return text::format("\\x%02X", val);
			else if(0xE000 <= val && val <= 0xF8FF)
				return text::format("\\U%04X", val);
			else if(0x0F0000 <= val && val <= 0x0FFFFD || 0x100000 <= val && val <= 0x10FFFD)
				return text::format("\\U%06X", val);
			else if(CFCharacterSetIsLongCharacterMember(illegal_set, val))
				return text::format("\\U%04X", val);
		}
	};

	return ch;
}

static std::string single_quote_escape (std::string const& ch)
{
	return ch == "'" ? "''" : ch;
}

static std::string escape (std::string const& str, std::string (*escapeFunction) (std::string const& ch), std::string const& quoteChar = "")
{
	std::string escaped("");
	citerate(it, diacritics::make_range(str.data(), str.data() + str.size()))
		escaped.append(escapeFunction(std::string(&it, &it + it.length())));
	return quoteChar + escaped + quoteChar;
}

static std::string pretty_string (std::string const& str, int flags)
{
	if((flags & plist::kPreferSingleQuotedStrings) == plist::kPreferSingleQuotedStrings && (str.find('\'') == std::string::npos || str != escape(str, &double_quote_escape)))
			return escape(str, &single_quote_escape, "'");
	else	return escape(str, &double_quote_escape, "\"");
}

static std::string pretty_data (std::vector<char> const& data)
{
	std::string res = "<";
	size_t i = 3;
	iterate(byte, data)
	{
		if(++i != 4 && i % 4 == 0)
			res.push_back(' ');
		text::int_to_hex(*byte, back_inserter(res), 2);
	}
	return res + ">";
}

static std::string pretty_key (std::string const& key, int flags)
{
	bool should_quote = false;
	bool all_digits = true;
	bool first_char = true;
	citerate(it, diacritics::make_range(key.data(), key.data() + key.size()))
	{
		uint32_t const val = utf8::to_ch(std::string(&it, &it + it.length()));
		if(!isdigit(val))
			all_digits = false;

		bool local_should_quote = true;
		struct range_t { char first; char last; } const ranges[] = { { 'a', 'z' }, { 'A', 'Z' }, { '_', '_' } };
		for(size_t i = 0; i < sizeofA(ranges); ++i)
		{
			if(ranges[i].first <= val && val <= ranges[i].last)
				local_should_quote = false;
		}

		// Also allow "".-" when not the first char
		if(!first_char && (val == '.' || val == '-'))
			local_should_quote = false;
		first_char = false;

		should_quote = should_quote || local_should_quote;
	}
	return should_quote && !all_digits ? pretty_string(key, flags) : key;
}

namespace
{
	struct pretty : boost::static_visitor<std::string>
	{
		pretty (int flags, size_t indent = 0, bool single_line = true, bool is_key = false) : flags(flags), indent(indent), single_line(single_line), is_key(is_key) { }

		int flags;
		size_t indent;
		bool single_line;
		bool is_key;

		std::string indent_string () const                           { return std::string(indent, '\t'); }
		std::string operator() (bool flag) const                     { return flag ? ":true" : ":false"; }
		std::string operator() (int32_t i) const                     { return text::format("%d", i); }
		std::string operator() (uint64_t i) const                    { return text::format("%llu", i); }
		std::string operator() (std::string const& str) const        { return pretty_string(str, flags); }
		std::string operator() (std::vector<char> const& data) const { return pretty_data(data); }
		std::string operator() (oak::date_t const& date) const       { return "@" + to_s(date); }

		std::string operator() (plist::array_t const& array) const
		{
			std::string res = "";
			if(array.empty())
			{
				res = " ";
			}
			else if(fits_single_line()(array))
			{
				size_t wrap = 0;
				iterate(it, array)
				{
					if(!res.empty())
						res += ", ";

					if(res.size() - wrap > 80)
					{
						res[res.size()-1] = '\n';
						res += indent_string() + '\t';
						wrap = res.size();
					}

					res += boost::apply_visitor(pretty(flags), *it);
				}

				res = " " + res + " ";
				
				if(res.find('\n') != std::string::npos)
				{
					res[res.size()-1] = '\n';
					res += indent_string();
				}
			}
			else
			{
				iterate(it, array)
				{
					if(!res.empty())
						res += "\n";
					res += indent_string() + '\t';
					res += boost::apply_visitor(pretty(flags, indent+1, false), *it);
					res += ",";
				}
				res = "\n" + res + "\n" + indent_string();
			}
			return "(" + res + ")";
		}

		std::string operator() (plist::dictionary_t const& dict) const
		{
			std::string res = "";
			if(dict.empty())
			{
				res = " ";
			}
			else if(fits_single_line()(dict))
			{
				std::string const& key = pretty_key(dict.begin()->first, flags);
				std::string const& value = boost::apply_visitor(pretty(flags), dict.begin()->second);
				res = text::format("%c%s = %s; ", single_line || is_key ? ' ' : '\t', key.c_str(), value.c_str());
			}
			else
			{
				iterate(it, dict)
				{
					if(!res.empty())
						res += indent_string();
					std::string const& key = pretty_key(it->first, flags);
					std::string const& value = boost::apply_visitor(pretty(flags, indent+1, false, true), it->second);
					res += text::format("\t%s = %s;\n", key.c_str(), value.c_str());
				}
				res = (is_key ? "\n" + indent_string() : "") + res + indent_string();
			}
			return "{" + res + "}";
		}
	};
}

namespace boost
{
	std::string to_s (plist::any_t const& plist, int flags)
	{
		return boost::apply_visitor(pretty(flags), plist);
	}

} /* boost */
