/*
 * $Id: JSONReader.cs 439 2007-11-26 13:26:10Z spocke $
 *
 * Copyright © 2007, Moxiecode Systems AB, All rights reserved. 
 */

using System;
using System.IO;
using System.Text;
using System.Collections;

namespace umbraco.presentation.plugins.tinymce3
{
    /// <summary>
	/// 
	/// </summary>
	public enum JSONToken {
		/// <summary> </summary>
		Boolean,

		/// <summary> </summary>
		Integer,

		/// <summary> </summary>
		String,

		/// <summary> </summary>
		Null,

		/// <summary> </summary>
		Float,

		/// <summary> </summary>
		StartArray,

		/// <summary> </summary>
		EndArray,

		/// <summary> </summary>
		PropertyName,

		/// <summary> </summary>
		StartObject,

		/// <summary> </summary>
		EndObject
	}

	/// <summary>
	///  Description of JSONReader.
	/// </summary>
	public class JSONReader {
		private TextReader reader;
		private JSONToken token;
		private object val;
		private JSONLocation location;
		private Stack lastLocations;
		private bool needProp;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="reader"></param>
		public JSONReader(TextReader reader) {
			this.reader = reader;
			this.val = null;
			this.token = JSONToken.Null;
			this.location = JSONLocation.Normal;
			this.lastLocations = new Stack();
			this.needProp = false;
		}

		/// <summary>
		/// 
		/// </summary>
		public JSONLocation Location {
			get { return location; }
		}

		/// <summary>
		/// 
		/// </summary>
		public JSONToken TokenType {
			get {
				return this.token;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public object Value {
			get {
				return this.val;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public bool Read() {
			int chr = this.reader.Read();

			if (chr != -1) {
				switch ((char) chr) {
					case '[':
						this.lastLocations.Push(this.location);
						this.location = JSONLocation.InArray;
						this.token = JSONToken.StartArray;
						this.val = null;
						this.ReadAway();
						return true;

					case ']':
						this.location = (JSONLocation) this.lastLocations.Pop();
						this.token = JSONToken.EndArray;
						this.val = null;
						this.ReadAway();

						if (this.location == JSONLocation.InObject)
							this.needProp = true;

						return true;

					case '{':
						this.lastLocations.Push(this.location);
						this.location = JSONLocation.InObject;
						this.needProp = true;
						this.token = JSONToken.StartObject;
						this.val = null;
						this.ReadAway();
						return true;

					case '}':
						this.location = (JSONLocation) this.lastLocations.Pop();
						this.token = JSONToken.EndObject;
						this.val = null;
						this.ReadAway();

						if (this.location == JSONLocation.InObject)
							this.needProp = true;

						return true;

					// String
					case '"':
					case '\'':
						return this.ReadString((char) chr);

					// Null
					case 'n':
						return this.ReadNull();

					// Bool
					case 't':
					case 'f':
						return this.ReadBool((char) chr);

					default:
						// Is number
						if (Char.IsNumber((char) chr) || (char) chr == '-' || (char) chr == '.')
							return this.ReadNumber((char) chr);

						return true;
				}
			}

			return false;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public override string ToString() {
			switch (this.token) {
				case JSONToken.Boolean:
					return "[Boolean] = " + ((bool) this.Value ? "true" : "false");

				case JSONToken.EndArray:
					return "[EndArray]";

				case JSONToken.EndObject:
					return "[EndObject]";

				case JSONToken.Float:
					return "[Float] = " + Convert.ToDouble(this.Value);

				case JSONToken.Integer:
					return "[Integer] = " + ((int) this.Value);

				case JSONToken.Null:
					return "[Null]";

				case JSONToken.StartArray:
					return "[StartArray]";

				case JSONToken.StartObject:
					return "[StartObject]";

				case JSONToken.String:
					return "[String]" + (string) this.Value;

				case JSONToken.PropertyName:
					return "[PropertyName]" + (string) this.Value;
			}

			return base.ToString();
		}
		
		#region private methods

		private bool ReadString(char quote) {
			StringBuilder buff = new StringBuilder();
			this.token = JSONToken.String;
			bool endString = false;
			int chr;

			while ((chr = this.reader.Peek()) != -1) {
				switch (chr) {
					case '\\':
						// Read away slash
						chr = this.reader.Read();

						// Read escape code
						chr = this.reader.Read();
						switch (chr) {
								case 't':
									buff.Append('\t');
									break;

								case 'b':
									buff.Append('\b');
									break;

								case 'f':
									buff.Append('\f');
									break;

								case 'r':
									buff.Append('\r');
									break;

								case 'n':
									buff.Append('\n');
									break;

								case 'u':
									buff.Append((char) Convert.ToInt32(ReadLen(4), 16));
									break;

								default:
									buff.Append((char) chr);
									break;
						}

						break;

						case '\'':
						case '"':
							if (chr == quote)
								endString = true;

							chr = this.reader.Read();
							if (chr != -1 && chr != quote)
								buff.Append((char) chr);

							break;

						default:
							buff.Append((char) this.reader.Read());
							break;
				}

				// String terminated
				if (endString)
					break;
			}

			this.ReadAway();

			this.val = buff.ToString();

			// Needed a property
			if (this.needProp) {
				this.token = JSONToken.PropertyName;
				this.needProp = false;
				return true;
			}

			if (this.location == JSONLocation.InObject && !this.needProp)
				this.needProp = true;

			return true;
		}

		private bool ReadNull() {
			this.token = JSONToken.Null;
			this.val = null;

			this.ReadAway(3); // ull
			this.ReadAway();

			if (this.location == JSONLocation.InObject && !this.needProp)
				this.needProp = true;

			return true;
		}

		private bool ReadNumber(char start) {
			StringBuilder buff = new StringBuilder();
			int chr;
			bool isFloat = false;

			this.token = JSONToken.Integer;
			buff.Append(start);

			while ((chr = this.reader.Peek()) != -1) {
				if (Char.IsNumber((char) chr) || (char) chr == '-' || (char) chr == '.') {
					if (((char) chr) == '.')
						isFloat = true;

					buff.Append((char) this.reader.Read());
				} else
					break;
			}

			this.ReadAway();

			if (isFloat) {
				this.token = JSONToken.Float;
				this.val = Convert.ToDouble(buff.ToString().Replace('.', ','));
			} else
				this.val = Convert.ToInt32(buff.ToString());

			if (this.location == JSONLocation.InObject && !this.needProp)
				this.needProp = true;

			return true;
		}

		private bool ReadBool(char chr) {
			this.token = JSONToken.Boolean;
			this.val = chr == 't';

			if (chr == 't')
				this.ReadAway(3); // rue
			else
				this.ReadAway(4); // alse

			this.ReadAway();

			if (this.location == JSONLocation.InObject && !this.needProp)
				this.needProp = true;

			return true;
		}

		private void ReadAway() {
			int chr;
			
			while ((chr = this.reader.Peek()) != -1) {
				if (chr != ':' && chr != ',' && !Char.IsWhiteSpace((char) chr))
					break;

				this.reader.Read();
			}
		}

		private string ReadLen(int num) {
			StringBuilder buff = new StringBuilder();
			int chr;

			for (int i=0; i<num && (chr = this.reader.Read()) != -1; i++)
				buff.Append((char) chr);

			return buff.ToString();
		}
		
		private void ReadAway(int num) {
			for (int i=0; i<num && this.reader.Read() != -1; i++) ;
		}

		#endregion
	}
}
