/* LICENSE NOTICE
	Copyright (c) 2008, Frederick Emmott <mail@fredemmott.co.uk>

	Permission to use, copy, modify, and/or distribute this software for any
	purpose with or without fee is hereby granted, provided that the above
	copyright notice and this permission notice appear in all copies.

	THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
	WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
	MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
	ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
	WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
	ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
	OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "JsonToVariant.h"

#include <QDebug>

#define FAIL(x) throw ParseException(*m_sym, x, remaining())

namespace JsonQt
{
	JsonToVariant::JsonToVariant(){}

	QVariantMap JsonToVariant::parse(const QString& json) throw(ParseException)
	{
		JsonToVariant parser;
		// Store the start and end of the string
		parser.m_next = json.constBegin();
		parser.m_sym = parser.m_next;
		parser.m_end = json.constEnd();
		// A JSON Object is the top-level item in the parse tree
		return parser.parseObject();
	}

	QList<QVariantMap> JsonToVariant::multiParse(const QString& raw) throw(ParseException)
	{
		QList<QVariantMap> objects;
		QString json(raw.trimmed());

		JsonToVariant parser;
		// Store the start and end of the string
		parser.m_next = json.constBegin();
		parser.m_sym = parser.m_next;
		parser.m_end = json.constEnd();
		// A JSON Object is the top-level item in the parse tree
		do
		{
			objects.append(parser.parseObject());
		}
		while(parser.m_next != parser.m_end && parser.m_sym != parser.m_end);
		return objects;
	}

	QVariantMap JsonToVariant::parseObject()
	{
		/*
		 * object
		 * 	{}
		 * 	{ members }
		 */

		QVariantMap data;

		consume('{');
		if(peekNext() != '}')
			data = parseMembers();
		consume('}');

		return data;
	}

	QVariantMap JsonToVariant::parseMembers()
	{
		/*
		 * members
		 * 	pair
		 * 	pair , members
		 */

		QVariantMap data;
		QPair<QString, QVariant> pair;

		// loop instead of recursing
		do
		{
			// Grab a pair
			pair = parsePair();

			// Store it in our data
			data[pair.first] = pair.second;
		}
		while(tryConsume(',')); // Loop if we've got a list separator

		return data;
	}

	QPair<QString, QVariant> JsonToVariant::parsePair()
	{
		/*
		 * pair
		 * 	string : value
		 */

		QString key = parseString();
		consume(':');
		QVariant value = parseValue();

		return qMakePair(key, value);
	}

	QVariantList JsonToVariant::parseArray()
	{
		/*
		 * array
		 * 	[]
		 * 	[ elements ]
		 */

		QVariantList data;

		consume('[');
		if(peekNext() != ']')
			data = parseElements();
		consume(']');

		return data;
	}

	QVariantList JsonToVariant::parseElements()
	{
		/*
		 * elements
		 * 	value
		 * 	value , elements
		 */
		QVariantList data;

		// loop instead of recursing
		do
		{
			// Grab a value
			data += parseValue();
		}
		while(tryConsume(',')); // repeat if we've got a list separator

		return data;
	}

	QVariant JsonToVariant::parseValue()
	{
		/*
		 * value
		 * 	string
		 * 	number
		 * 	object
		 * 	array
		 * 	bool
		 * 	null
		 */

		tryConsume(':');

		// Lookahead to work out the type of value
		switch(peekNext().toAscii())
		{
			case '"':
				return parseString();
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
			case '-':
				return parseNumber();
			case '{':
				return parseObject();
			case '[':
				return parseArray();
			case 't': // true
			case 'f': // false
				return parseBool();
			case 'n': // null
				return parseNull();
			default:
				FAIL(QObject::tr("string, number, object, array, bool, or null"));
		}
	}

	QString JsonToVariant::parseString()
	{
		/*
		 * string
		 * 	""
		 * 	" chars "
		 */

		QString data;

		// Starting quotation marks
		consume('"');

		// If it's a non-empty string, grab the contents
		if(*m_next != '"')
			data = parseChars();

		// Ending quotation marks
		consume('"');

		return data;
	}

	QString JsonToVariant::parseChars()
	{
		/*
		 * chars
		 * 	char
		 * 	char chars
		 */

		QString data;

		// chars contains at least one char
		data = parseChar();

		// See if there's more...
		Q_FOREVER
		{
			try { data += parseChar(); }
			catch(ParseException e)
			{
				// If not, it doesn't matter - roll back
				m_next = m_sym--;
				break;
			}
		}
		return data;
	}

	QChar JsonToVariant::parseChar()
	{
		/*
		 * char
		 * 	any character except for ", \, or control characters
		 *	\"
		 *	\\
		 *	\/
		 *	\b
		 *	\f
		 *	\n
		 *	\r
		 *	\t
		 *	\u four-hex-digits
		 */

		// Grab the next character, without skipping whitespace
		consume(false);

		// We're not allowed unescaped quotation marks
		if(*m_sym == '"')
			FAIL(QObject::tr("Any unicode character except for \" or JSON escape sequences"));
		
		// But some escape sequences are allowed
		if(*m_sym == '\\')
		{
			QString digits;
			switch(consume().toAscii())
			{
				case '"':
					return '"';
				case '\\':
					return '\\';
				case 'b':
					return '\b';
				case 'f':
					return '\f';
				case 'n':
					return '\n';
				case 'r':
					return '\r';
				case 't':
					return '\t';
				case 'u':
					// Unicode 4-digit hex
					for(int i = 0; i < 4; ++i)
					{
						digits += parseHexDigit();
					}
					return QChar(digits.toInt(0, 16));
				default:
					FAIL("[\"\\/bfnrtu]");
			}
		}
		return *m_sym;
	}

	QVariant JsonToVariant::parseNumber()
	{
		/*
		 * number
		 * 	int
		 * 	int frac
		 * 	int exp
		 * 	int frac exp
		 */

		// Every JSON number starts with an int
		QString data = parseInt();

		// Lookahead for fractions and exponents
	
		// Do we have a fraction?
		if(*m_next == '.') data += parseFrac();
		// Do we have an exponent?
		if(*m_next == 'e' || *m_next == 'E') data += parseExp();

		// Try several return types...
		bool ok;
		QVariant ret;

		ret = data.toInt(&ok); if(ok) return ret;
		ret = data.toLongLong(&ok); if(ok) return ret;
		ret = data.toDouble(&ok); if(ok) return ret;

		// If this point is reached, don't know how to convert the string
		// to an integer.
		Q_ASSERT(false);
		return QVariant();
	}

	QString JsonToVariant::parseInt()
	{
		/*
		 * int
		 * 	digit
		 * 	digit1-9 digits
		 * 	- digit
		 * 	- digit1-9 digits
		 */

		QString data;

		// Match any negation mark
		if(tryConsume('-'))
			data = "-";

		// Grab the first digit...
		QChar firstDigit = parseDigit();
		data += firstDigit;
		// ...if it's not zero...
		if(firstDigit != '0')
		{
			// ... try and add more digits.
			try { data += parseDigits(); }
			catch(ParseException)
			{
				// Catch, as more digits are entirely optional
				// Roll back.
				m_next = m_sym--;
			}
		}
		return data;
	}

	QString JsonToVariant::parseFrac()
	{
		/*
		 * frac
		 * 	. digits
		 */

		consume('.');
		return QString(".%1").arg(parseDigits());
	}

	QString JsonToVariant::parseExp()
	{
		/*
		 * exp
		 * 	e digits
		 */

		QString data;
		data = parseE();
		data += parseDigits();
		return data;
	}

	QString JsonToVariant::parseDigits()
	{
		/*
		 * digits
		 * 	digit
		 * 	digit digits
		 */

		QString data;

		// Digits has at least one digit...
		data += parseDigit();

		// ... try and get some more
		// Loop instead of recurse
		Q_FOREVER
		{
			try { data += parseDigit(); }
			catch(ParseException)
			{
				m_next = m_sym--; // roll back
				break;
			}
		}
		return data;
	}

	QString JsonToVariant::parseE()
	{
		/*
		 * e
		 * 	e
		 * 	e+
		 * 	e-
		 * 	E
		 * 	E+
		 * 	E-
		 */

		// Try and grab an 'e' or 'E'
		if(consume(false).toLower() == 'e')
		{
			// digits in follow[e]
			if(m_next->isDigit())
				return "e";

			// Hopefully the next is a + or -
			// grab another chracter...
			consume(false);
			// If it's not + or -, fail
			if(*m_sym != '+' && *m_sym != '-')
				FAIL("+ | -");

			// Otherwise, return e[+-]
			return QString("e%1").arg(*m_sym);
		}
		else
			FAIL("e | E");
	}


	QChar JsonToVariant::parseDigit()
	{
		/*
		 * digit
		 * 	[0-9]
		 */

		if(!consume(false).isDigit())
			FAIL("[0-9]");
		return *m_sym;
	}

	QChar JsonToVariant::parseHexDigit()
	{
		/*
		 * hexdigit
		 * 	[0-9a-fA-F]
		 */

		QChar character = consume().toLower();
		if(character.isDigit() || (character >= 'a' && character <= 'f'))
			return character;
		FAIL("[0-9a-fA-F]");
	}

	bool JsonToVariant::parseBool()
	{
		/*
		 * bool
		 * 	true
		 * 	false
		 */

		switch(peekNext().toAscii())
		{
			case 't':
				consume(QString("true"));
				return true;
			case 'f':
				consume(QString("false"));
				return false;
			default:
				consume(false);
				FAIL("true | false");
		}
	}

	QVariant JsonToVariant::parseNull()
	{
		/*
		 * null
		 * 	null
		 */

		consume(QString("null"));
		return QVariant();
	}

	QString JsonToVariant::remaining()
	{
		QString data;

		QString::ConstIterator it = m_sym;
		while(it != m_end) data += *it++;

		return data;
	}

	QChar JsonToVariant::consume(bool skipWhitespace) throw(ParseException)
	{
		// Read a character...
		do
		{
			if(m_next == m_end)
			{
				throw ParseException("EOF", "symbol", remaining());
			}
			m_sym = m_next++;
		}
		//...and loop while we get whitespace, if it's being skipped
		while(skipWhitespace && m_sym->isSpace());

		// Just for convenience...
		return *m_sym;
	}

	void JsonToVariant::consume(QChar wanted) throw(ParseException)
	{
		// Grab a char(ignoring whitespace), and if it's not what's
		// expected, throw
		if(consume() != wanted)
		{
			FAIL(wanted);
		}
	}

	void JsonToVariant::consume(char wanted) throw(ParseException)
	{
		// Convenience function for the above
		consume(QChar(wanted));
	}

	void JsonToVariant::consume(QString wanted) throw(ParseException)
	{
		// Keep track of where we start...
		QString::ConstIterator it = m_sym;
		// Grab wanted.length() characters, and compare them to the
		// character in the appropriate position in the parameter
		for(int i = 0; i < wanted.length(); ++i)
			if(consume(false) != wanted[i])
			{
				// If it doesn't match, roll back, and throw a
				// parse exception
				m_sym = it;
				m_next = ++it;
				FAIL(wanted);
			}
	}

	bool JsonToVariant::tryConsume(QChar wanted) throw()
	{
		// Grab the next character
		try
		{
			consume();
		}
		catch(ParseException)
		{
			// End-Of-String, rollback and return false
			m_next = m_sym--;
			return false;
		}

		// Check if it's what we want
		if(*m_sym != wanted)
		{
			// nope, something else, rollback and return false
			m_next = m_sym--;
			return false;
		}
		return true;
	}

	QChar JsonToVariant::peekNext(bool skipWhitespace) throw(ParseException)
	{
		QString::ConstIterator it = m_sym;
		do
		{
			++it;
			if(it == m_end)
			{
				FAIL("symbol");
			}
		}
		while(skipWhitespace && it->isSpace());
		return *it;
	}
}
