/*
**************************************************************
* JsonSerializer Class
**************************************************************
*  Author: Rick Strahl 
*          (c) West Wind Technologies
*          http://www.west-wind.com/
* 
* Created: 05/01/2005
* 
* Credits:
* Based on original work from Jason Diamond (Anthem.NET)
* 
* Thanks to Joe McLain for his re-work of ParseArray()
* and space parsing.
* 
* Update History: 
* 
* 06/01/2008 
*  - Better support for white space in JSON
*  - Updated ParseArray Routine based on fixes from
*    Joe McLain to handle arrays of any type.
* 
**************************************************************  
*/

using System;
using System.Data;

using System.Text;
using System.Globalization;
using System.Reflection;
using System.Collections;
using System.IO;
using System.Collections.Specialized;

using System.Text.RegularExpressions;
using Westwind.Tools;

namespace Westwind.Web.Controls
{
    /// <summary>
    /// More text is a basic JSON serializer and deserializer that 
    /// deals with standard .NET types. Unlike the MS Ajax JSONSerializer
    /// parser this parser support serialization and deserialization without 
    /// explicit type markup in the JSON resulting in a simpler two-way model.
    /// 
    /// The inbound model for complex types is based on Reflection parsing
    /// of properties.
    /// </summary>
    public class JSONSerializer
    {
        /// <summary>
        /// The JavaScript base date on which all date time values
        /// in JavaScript are based. Offsets are given in milliseconds
        /// (ie. new Date(99990123) where the number is the offset in ms
        /// from the base date)
        /// </summary>
        static DateTime DAT_BASEDATE = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// White space checking for char values
        /// </summary>
        const string STR_WHITESPACE = " \t\n\r";

        /// <summary>
        /// Encodes Dates as a JSON string value that is compatible
        /// with MS AJAX and is safe for JSON validators. If false
        /// serializes dates as new Date() expression instead.
        /// 
        /// The default is true.
        /// </summary>
        public bool SerializeDateAsString
        {
            get { return _SerializeDateAsFormatString; }
            set { _SerializeDateAsFormatString = value; }
        }
        private bool _SerializeDateAsFormatString = true;


        /// <summary>
        /// Serializes a .NET object reference into a JSON string.
        /// 
        /// The serializer supports:
        /// &lt;&lt;ul&gt;&gt;
        /// &lt;&lt;li&gt;&gt; All simple types
        /// &lt;&lt;li&gt;&gt; POCO objects and hierarchical POCO objects
        /// &lt;&lt;li&gt;&gt; Arrays
        /// &lt;&lt;li&gt;&gt; IList based collections
        /// &lt;&lt;li&gt;&gt; DataSet
        /// &lt;&lt;li&gt;&gt; DataTable
        /// &lt;&lt;li&gt;&gt; DataRow
        /// &lt;&lt;/ul&gt;&gt;
        /// 
        /// The serializer works off any .NET type - types don't have to be explicitly 
        /// serializable.
        /// 
        /// DataSet/DataTable/DataRow parse into a special format that is essentially 
        /// array based of just the data. These objects can be serialized but cannot be
        ///  passed back in via deserialization.
        /// <seealso>Class JSONSerializer</seealso>
        /// </summary>
        /// <param name="value">
        /// The strongly typed value to parse
        /// </param>
        public string Serialize(object value)
        {
            StringBuilder sb = new StringBuilder();
            WriteValue(sb, value);
            return sb.ToString();
        }

        /// <summary>
        /// Takes a JSON string and attempts to create a .NET object from this  
        /// structure. An input type is required and any type that is serialized to  
        /// must support a parameterless constructor.
        /// 
        /// The de-serializer instantiates each object and runs through the properties
        /// 
        /// The deserializer supports &lt;&lt;ul&gt;&gt; &lt;&lt;li&gt;&gt; All simple 
        /// types &lt;&lt;li&gt;&gt; Most POCO objects and Hierarchical POCO objects 
        /// &lt;&lt;li&gt;&gt; Arrays and Object Arrays &lt;&lt;li&gt;&gt; IList based 
        /// collections &lt;&lt;/ul&gt;&gt;
        /// 
        /// Note that the deserializer doesn't support DataSets/Tables/Rows like the  
        /// serializer as there's no type information available from the client to  
        /// create these objects on the fly.
        /// <seealso>Class JSONSerializer</seealso>
        /// </summary>
        /// <param name="JSONText">
        /// A string of JSON text passed from the client.
        /// </param>
        /// <param name="valueType">
        /// The type of the object that is to be created from the JSON text.
        /// </param>
        public object Deserialize(string jsonText, Type valueType)
        {

#if false
            JSONDeserializer deserializer = new JSONDeserializer();
            return deserializer.Parse(jsonText, valueType);
#else            
            return this.ParseValueString(jsonText, valueType) as object;
#endif
        }

        //public TType Deserialize<TType>(string jsonText) 
        //{
        //    System.Web.Script.Serialization.JavaScriptSerializer ser 
        //            = new System.Web.Script.Serialization.JavaScriptSerializer();
            
        //    return (TType) ser.Deserialize<TType>(jsonText);
        //}


        #region Serialization Methods
        // This serioalization code is based on Jason Diamonds JSON parsing 
        // routines part of MyAjax.NET (aka Anthem).
        /// <summary>
        /// Serialization routine that takes any value and serializes
        /// it into JSON. 
        /// 
        /// Date formatting follows Microsoft ASP.NET AJAX format which
        /// represents dates as strings in the format of: "\/Date(231231231)\/"
        ///
        /// This code is based originally on Jason Diamond's JSON code
        /// in Anthem.NET although heavy modifications have been made.
        /// </summary>
        /// <param name="sb"></param>
        /// <param name="val"></param>
        public void WriteValue(StringBuilder sb, object val)
        {
            if (val == null || val == System.DBNull.Value)
            {
                sb.Append("null");
            }
            else if (val is string)
            {
                WriteString(sb, (string)val);
            }
            else if (val is bool)
            {
                sb.Append(val.ToString().ToLower());
            }
            else if (val is long ||
                     val is int ||
                     val is short ||
                     val is byte
                )
            {
                sb.Append(val);
            }
            else if (val is decimal)
            {
                sb.Append(((decimal)val).ToString(CultureInfo.InvariantCulture.NumberFormat));
            }
            else if (val is float)
            {
                sb.Append(((float)val).ToString(CultureInfo.InvariantCulture.NumberFormat));
            }
            else if (val is double)
            {
                sb.Append(((double)val).ToString(CultureInfo.InvariantCulture.NumberFormat));
            }
            else if (val is Single)
            {
                sb.Append(((Single)val).ToString(CultureInfo.InvariantCulture.NumberFormat));
            }
            else if (val.GetType().IsEnum)
            {
                sb.Append((int)val);
            }
            else if (val is DateTime)
            {
                WriteDate(sb, (DateTime)val);
            }
            else if (val is DataSet)
            {
                WriteDataSet(sb, val as DataSet);
            }
            else if (val is DataTable)
            {
                WriteDataTable(sb, val as DataTable);
            }
            else if (val is DataRow)
            {
                WriteDataRow(sb, val as DataRow);
            }
            else if (val is IEnumerable)
            {
                WriteEnumerable(sb, val as IEnumerable);
            }
            else if (val is Guid)
                WriteObject(sb, val.ToString());
            else
            {
                WriteObject(sb, val);
            }
        }

        /// <summary>
        /// Writes a string as a JSON string
        /// </summary>
        /// <param name="sb"></param>
        /// <param name="s"></param>
        void WriteString(StringBuilder sb, string s)
        {
            sb.Append("\"");
            foreach (char c in s)
            {
                switch (c)
                {
                    case '\"':
                        sb.Append("\\\"");
                        break;
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '\b':
                        sb.Append("\\b");
                        break;
                    case '\f':
                        sb.Append("\\f");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        int i = (int)c;
                        if (i < 32 || i > 127)
                        {
                            sb.AppendFormat("\\u{0:X04}", i);
                        }
                        else
                        {
                            sb.Append(c);
                        }
                        break;
                }
            }
            sb.Append("\"");
        }

        /// <summary>
        /// Writes a date out as a JSON string into the strnig builder passed in
        /// Date format follows Microsoft ASP.NET AJAX format which is represented
        /// as a string in the format:
        /// "\/Date(99912311)\/"
        /// </summary>
        /// <param name="sb"></param>
        /// <param name="date"></param>
        void WriteDate(StringBuilder sb, DateTime date)
        {
            double milliseconds = 0;

            // must always return dates in UTC
            date = date.ToUniversalTime();

            if (date.CompareTo(DAT_BASEDATE) < 0)
                milliseconds = 0D;
            else
            {
                // *** Convert to JavaScript specific milliseconds
                TimeSpan tspan = date.Subtract(DAT_BASEDATE);
                milliseconds = Math.Floor(tspan.TotalMilliseconds);
            }

            if (this.SerializeDateAsString)
            {
                sb.Append(@"""\/Date(");
                sb.Append(milliseconds);
                sb.Append(@")\/""");
            }
            else
            {
                sb.Append(@"new Date(");
                sb.Append(milliseconds);
                sb.Append(")");
            }
        }
        void WriteDataSet(StringBuilder sb, DataSet ds)
        {
            sb.Append("{\"Tables\":{");
            foreach (DataTable table in ds.Tables)
            {
                sb.AppendFormat("\"{0}\":", table.TableName);
                WriteDataTable(sb, table);
                sb.Append(",");
            }
            // Remove the trailing comma.
            if (ds.Tables.Count > 0)
                sb.Length--;
            sb.Append("}}");
        }

        void WriteDataTable(StringBuilder sb, DataTable table)
        {
            sb.Append("{\"Rows\":[");
            foreach (DataRow row in table.Rows)
            {
                this.WriteDataRow(sb, row);
                sb.Append(",");
            }
            // Remove the trailing comma.
            if (table.Rows.Count > 0)
                --sb.Length;
            sb.Append("]}");
        }

        void WriteDataRow(StringBuilder sb, DataRow row)
        {
            sb.Append("{");
            foreach (DataColumn column in row.Table.Columns)
            {
                sb.AppendFormat("\"{0}\":", column.ColumnName);
                WriteValue(sb, row[column]);
                sb.Append(",");
            }
            if (row.Table.Rows.Count > 0)
                sb.Length--; // strip trailing ,

            sb.Append("}");
        }

        void WriteEnumerable(StringBuilder sb, IEnumerable e)
        {
            bool hasItems = false;
            sb.Append("[");

            foreach (object val in e)
            {
                WriteValue(sb, val);
                sb.Append(",");
                hasItems = true;
            }
            // Remove the trailing comma.
            if (hasItems)
                --sb.Length;
            sb.Append("]");
        }

        void WriteObject(StringBuilder sb, object o)
        {
            MemberInfo[] members = o.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public);
            sb.Append("{");
            bool hasMembers = false;
            foreach (MemberInfo member in members)
            {
                bool hasValue = false;
                object val = null;
                if ((member.MemberType & MemberTypes.Field) == MemberTypes.Field)
                {
                    FieldInfo field = (FieldInfo)member;
                    val = field.GetValue(o);
                    hasValue = true;
                }
                else if ((member.MemberType & MemberTypes.Property) == MemberTypes.Property)
                {
                    PropertyInfo property = (PropertyInfo)member;
                    if (property.CanRead && property.GetIndexParameters().Length == 0)
                    {
                        val = property.GetValue(o, null);
                        hasValue = true;
                    }
                }
                if (hasValue)
                {
                    sb.Append("\"");
                    sb.Append(member.Name);
                    sb.Append("\":");
                    WriteValue(sb, val);
                    sb.Append(",");
                    hasMembers = true;
                }
            }
            if (hasMembers)
            {
                --sb.Length;
            }
            sb.Append("}");
        }

        #endregion

        #region Deserialization methods
        /// <summary>
        /// Unescapes string encoded Unicode character in the format of \u03AF
        /// </summary>
        /// <param name="match"></param>
        /// <returns></returns>
        private string UnicodeEscapeMatchEvaluator(Match match)
        {
            // *** last 4 digits are hex value
            string hex = match.Value.Substring(2);
            char val = (char)ushort.Parse(hex, System.Globalization.NumberStyles.HexNumber);
            return val.ToString();
        }


        /// <summary>
        /// High level parsing method that takes a JSON string and tries to
        /// convert it to the appropriate type. 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="valueType"></param>
        /// <returns></returns>
        private object ParseValueString(string value, Type valueType)
        {
            if (valueType == typeof(string))
            {
                return this.ParseString(value);
            }


            // *** Most types are parsed as straight strings
            else if (valueType == typeof(decimal) ||
                     valueType == typeof(int) ||
                     valueType == typeof(float) ||
                     valueType == typeof(Single) ||
                     valueType == typeof(double) ||
                     valueType == typeof(long) ||
                     valueType == typeof(bool) ||
                     valueType == typeof(short) ||
                     valueType == typeof(byte))
            {
                return this.ParseNumber(value,valueType) ;                
            }
            else if (valueType == typeof(DateTime))
            {
                return ParseDate(value);
            }
            else if (valueType.IsArray || valueType.GetInterface("IList") != null)
            {
                StringReader Reader = new StringReader(value);
                return this.ParseArray(Reader, valueType);
            }
            else if (valueType.IsClass)
            {
                StringReader Reader = new StringReader(value);
                return this.ParseObject(Reader, valueType, false);
            }

            return null;
        }

        /// <summary>
        /// Parses a JSON string into a string value
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public string ParseString(string value)
        {
            if (value == "null")
                return null;

            value = value.Replace(@"\r", "\r");
            value = value.Replace(@"\n", "\n");
            value = value.Replace(@"\\", "\\");
            value = value.Replace(@"\""", "\"");
            value = value.Replace(@"\t", "\t");
            value = value.Replace(@"\b", "\b");
            value = value.Replace(@"\f", "\f");

            if (value.Contains("\\u"))
                value = Regex.Replace(value, @"\\u....",
                                      new MatchEvaluator(this.UnicodeEscapeMatchEvaluator));

            // *** strip off leading and trailing quote chars
            string val = value.Substring(1, value.Length - 2);
            
            return val;
        }

        public object ParseNumber(string value,Type valueType)
        {
            return wwUtils.StringToTypedValue(value, valueType, CultureInfo.InvariantCulture);
        }
        public bool ParseNumber(string value, out object result)
        {
            result = null;

            result = wwUtils.StringToTypedValue(value, result.GetType(), CultureInfo.InvariantCulture);
            
            if (result == null)
                return false;

            return true;
        }


        /// <summary>
        /// Parses Date Time values. Supports parsing values in various formats:
        /// 
        /// new Date()
        /// MS Ajax Date Form (\/Date(xxxx)\/)
        /// ISO Date Format
        /// </summary>
        /// <param name="Value"></param>
        /// <returns></returns>
        private DateTime ParseDate(string Value)
        {
            if (Value == null)
                return DateTime.MinValue;

            // *** Strip off string wrapper since dates are embedded as strings
            // *** This should only be the case if a raw Date value is parsed
            // *** Inside of objects the value will be treated like a result string
            // *** and so lack the quotes.
            Value = Value.Trim('\"');

            try
            {
                // *** ASP.NET Ajax date serialization format (\/Date(ticks)\/)
                if (Value.StartsWith(@"\/Date(", StringComparison.Ordinal))
                {
                    Value = wwUtils.ExtractString(Value, @"\/Date(", @")\/");
                    double ticks = 0;
                    if (double.TryParse(Value, out ticks))
                    {
                        if (ticks == 0)
                            return DateTime.MinValue;

                        return DAT_BASEDATE.AddMilliseconds(ticks).ToLocalTime();
                    }
                    return DateTime.MinValue;
                }

                // *** Parse out what's between the date parens
                Value = wwUtils.ExtractString(Value, "new Date(", ")");

                // new Date() should never happen - error
                if (string.IsNullOrEmpty(Value))
                    return DateTime.MinValue;

                // *** 1999,12,1,12,00,59
                if (Value.Contains(","))
                {
                    string[] DateParts = Value.Split(',');

                    return new DateTime(int.Parse(DateParts[0]),
                                                                int.Parse(DateParts[1]) + 1,
                                                                int.Parse(DateParts[2]),
                                                                int.Parse(DateParts[3]),
                                                                int.Parse(DateParts[4]),
                                                                int.Parse(DateParts[5]),
                                                                DateTimeKind.Utc).ToLocalTime();
                }
                // *** JavaScript .GetTime() style date based on milliseconds since 1970
                if (!Value.Contains("-") && !Value.Contains("/"))
                {
                    double ticks = 0;
                    if (double.TryParse(Value, out ticks))
                    {
                        if (ticks == 0)
                            return DateTime.MinValue;

                        return DAT_BASEDATE.AddMilliseconds(ticks).ToLocalTime();
                    }
                    return DateTime.MinValue;
                }

                // *** Try to parse with UTC round trip format or invariant auto-detection
                Value = Value.Replace("\"", "");
                return DateTime.Parse(Value, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.RoundtripKind).ToLocalTime();
            }
            catch(Exception ex)
            {
                return DateTime.MinValue;
            }
        }

        /// <summary>
        /// Parsing routine specific to parsing an object. Note the recursive flag which 
        /// allows skipping over prefix information.
        /// </summary>
        /// <param name="Reader"></param>
        /// <param name="ValueType"></param>
        /// <param name="RecursiveCall"></param>
        /// <returns></returns>
        private object ParseObject(StringReader Reader, Type ValueType, bool RecursiveCall)
        {
            // {"Company":"West Wind Technologies","Count":2,"DollarTotal":111.22,"TestValue":true,"Entered":"\/Date(912112)\/"}            

            int Val = 0;
            char NextChar = '{';

            if (!RecursiveCall)
            {
                Val = Reader.Read();
                if (Val < 0)
                    return null;
                NextChar = (char)Val;

                // *** check for null return - non recursed 
                if (NextChar == 'n' && Reader.Peek() == 'u')
                    return null;
                
                // *** otherwise it needs to be an object ref
                if (NextChar != '{')
                    throw new InvalidOperationException("ParseObject(): JSON type " +  ValueType.Name + " doesn't start with {.");
            }
            else
            {
                
                // if it's a recursive call the reader sits already on
                // the opening bracket so just set it but don't read from the reader.
                NextChar = '{';
            }

            char LastChar = '|';

            ParseStates State = ParseStates.None;

            string CurrentProperty = "";
            StringBuilder sb = new StringBuilder();
            NameValueCollection Properties = new NameValueCollection();

            object ResultObject = null;

            if (ValueType == typeof(string))
                ResultObject = string.Empty;
            else if (ValueType == typeof(DateTime))
                ResultObject = DateTime.MinValue;
            else if (ValueType == typeof(int) || ValueType == typeof(byte))
                ResultObject = 0;
            else if (ValueType == typeof(decimal))
                ResultObject = 0M;
            else if (ValueType == typeof(float) || ValueType == typeof(double))
                ResultObject = 0F;
            else
      
            // Objects start out null until we find the opening tag
            ResultObject = Activator.CreateInstance(ValueType);

            while (true)
            {
                LastChar = NextChar;
                Val = Reader.Read();
                if (Val == -1)
                    break;

                NextChar = (char)Val;

                // *** Check for Property Name first
                if (State == ParseStates.None &&
                    NextChar == '"')
                {
                    State = ParseStates.InPropertyName;
                    CurrentProperty = "";
                    continue;
                }


                // *** Check for end of Property name
                if (State == ParseStates.InPropertyName && NextChar == '"')
                {
                    State = ParseStates.InPropertyValueTransition;
                }
                // *** Collect property the property name chars as a string
                else if (State == ParseStates.InPropertyName)
                {
                    CurrentProperty += NextChar;
                }
                // *** Look for property/value separator and skip over
                else if (State == ParseStates.InPropertyValueTransition && NextChar == ':')
                    // *** Skip over the separator delimiter
                    continue;
                // *** Skip over any white space between properties/values
                else if (State == ParseStates.InPropertyValueTransition && STR_WHITESPACE.IndexOf(NextChar) >= 0)
                    // *** Skip over white space
                    continue;

                // *** Check for string and then skip over the string delimiter
                else if (State == ParseStates.InPropertyValueTransition && NextChar == '"')
                {
                    // *** Entering a string value
                    State = ParseStates.InStringValue;

                    // *** Have to append the leading quote
                    sb.Append(NextChar);
                }
                // *** Array Handling
                else if (State == ParseStates.InPropertyValueTransition && NextChar == '[')
                {
                    MemberInfo[] mi = ResultObject.GetType()
                                                  .GetMember(CurrentProperty, 
                                                             BindingFlags.Instance | BindingFlags.Instance | 
                                                             BindingFlags.GetField | BindingFlags.GetProperty | 
                                                             BindingFlags.Public);
                    if (mi == null || mi.Length < 1)
                        this.AssignProperty(ResultObject, CurrentProperty, null);
                    else
                    {
                        Type ObjectType = null;
                        if (mi[0].MemberType == MemberTypes.Field)
                            ObjectType = ((FieldInfo)mi[0]).FieldType;
                        else
                            ObjectType = ((PropertyInfo)mi[0]).PropertyType;

                        object Result = this.ParseArray(Reader, ObjectType /*, false */);

                        if (mi[0].MemberType == MemberTypes.Field)
                            ((FieldInfo)mi[0]).SetValue(ResultObject, Result);
                        else
                            if ( ((PropertyInfo)mi[0]).CanWrite)
                                ((PropertyInfo)mi[0]).SetValue(ResultObject, Result, null);

                        NextChar = ']';
                    }
                    State = ParseStates.None;
                }
                // *** Nested Object - recursively read characters
                else if (State == ParseStates.InPropertyValueTransition && NextChar == '{')
                {
                    State = ParseStates.InObject;


                    MemberInfo[] mi = ResultObject.GetType().GetMember(CurrentProperty, BindingFlags.Instance | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public);
                    if (mi == null || mi.Length < 1)
                        this.AssignProperty(ResultObject, CurrentProperty, null);
                    else
                    {
                        Type ObjectType = null;
                        if (mi[0].MemberType == MemberTypes.Field)
                            ObjectType = ((FieldInfo)mi[0]).FieldType;
                        else
                            ObjectType = ((PropertyInfo)mi[0]).PropertyType;

                        object Result = this.ParseObject(Reader, ObjectType, true);

                        if (mi[0].MemberType == MemberTypes.Field)
                            ((FieldInfo)mi[0]).SetValue(ResultObject, Result);
                        else
                            if (((PropertyInfo)mi[0]).CanWrite)
                                ((PropertyInfo)mi[0]).SetValue(ResultObject, Result, null);

                        NextChar = '}';
                    }
                    State = ParseStates.None;
                }
                else if (State == ParseStates.InPropertyValueTransition)
                {
                    State = ParseStates.InValue;
                    sb.Append(NextChar);
                }
                // *** deal with string value content
                else if (State == ParseStates.InStringValue)
                {
                    if (NextChar == '"')
                        this.SerializeDateAsString = true;

                    // *** check for terminating quote
                    if (NextChar == '"' && LastChar != '\\')
                    {
                        sb.Append(NextChar); // add quote

                        // *** Assign the value to the string property
                        State = ParseStates.None;
                        this.AssignProperty(ResultObject, CurrentProperty, sb.ToString());
                        sb.Length = 0;
                    }
                    else
                        // *** just write out the next character
                        sb.Append(NextChar);
                }
                // *** Check for end of object - if found assign object
                else if (State == ParseStates.InValue && NextChar == '}')
                {
                    this.AssignProperty(ResultObject, CurrentProperty, sb.ToString());
                    sb.Length = 0;
                    State = ParseStates.EndOfObject;
                    return ResultObject;
                }
                // *** Check for end of value (,)
                else if (State == ParseStates.InValue && NextChar == ',')
                {
                    this.AssignProperty(ResultObject, CurrentProperty, sb.ToString());
                    sb.Length = 0;

                    // *** Skip over any white space after comma
                    while (Char.IsWhiteSpace((char)Reader.Peek()))
                        Reader.Read();

                    // *** next property or end of 
                    State = ParseStates.None;
                }
                // *** pick up any values
                else if (State == ParseStates.InValue)
                    sb.Append(NextChar);

                // *** this should never really fire, but just in case
                else if (State == ParseStates.EndOfObject || NextChar == '}')
                {
                    return ResultObject;
                }
            }

            return ResultObject;
        }

        /// <summary>
        /// Parses a array and IList subtype 
        /// 
        /// Supports only Objects as array items
        /// </summary>
        /// <param name="Reader"></param>
        /// <param name="ArrayType"></param>
        /// <returns></returns>
#if false        /// 
        private object ParseArray(StringReader Reader, Type ArrayType, bool RecursiveCall)
        {
            // *** Retrieve the type of child elements - Can be Array, Collection etc.
            Type ElementType = this.GetArrayType(ArrayType);

            // *** Start by parsing each of the items
            List<object> Items = new List<object>();

            char NextChar = '[';
            char LastChar = '|';
            ParseStates State = ParseStates.None;

            while (true)
            {
                LastChar = NextChar;
                int Val = Reader.Read();
                if (Val == -1)
                    break;
                NextChar = (char)Val;


                // *** Root array
                if (!RecursiveCall && State == ParseStates.None && NextChar == '[')
                { State = ParseStates.InPropertyValueTransition; }
                // *** Adding an object
                else if ((State == ParseStates.None || State == ParseStates.InPropertyValueTransition) && NextChar == '{')
                {
                    object ParsedObject = this.ParseObject(Reader, ElementType, true);
                    Items.Add(ParsedObject);
                    State = ParseStates.InPropertyValueTransition;
                }
                // *** Nested Array/List
                else if ((State == ParseStates.None || State == ParseStates.InPropertyValueTransition) && NextChar == '[')
                {
                    object ParsedObject = this.ParseArray(Reader, ElementType, true);
                    if (ParsedObject != null)
                        Items.Add(ParsedObject);

                    State = ParseStates.InPropertyValueTransition;
                }
                else if ((State == ParseStates.InPropertyValueTransition || State == ParseStates.None) && NextChar == ']')
                {
                    if (ArrayType.IsArray)
                    {
                        Array ElementArray = Activator.CreateInstance(ArrayType, Items.Count) as Array;
                        for (int i = 0; i < Items.Count; i++)
                        {
                            object Item = Activator.CreateInstance(ElementType);
                            ElementArray.SetValue(Items[i], i);
                        }
                        return ElementArray;
                    }
                    //else if (ArrayType is DataRow)
                    //{

                    //}
                    else if (ArrayType.GetInterface("IList") != null)
                    {
                        IList Col = Activator.CreateInstance(ArrayType) as IList;
                        foreach (object Item in Items)
                        {
                            Col.Add(Item);
                        }
                        return Col;
                    }
                }

            }

            return null;
        }
#endif

#if true
        static string arrayTerminatorChar = "[{]";
        static char arrayItemTerminatorChar = ',';


        /// <summary>
        /// Parses a JSON array string to its underlying array type.
        /// Requires:
        /// - Array must be of a single element type (ie. an object, or string or decimal)
        /// </summary>
        /// <remarks>
        /// Re-written by Joe McLain with modifications by Rick Strahl
        /// 06/01/2008
        /// </remarks>
        /// <param name="Reader"></param>
        /// <param name="ArrayType"></param>
        /// <returns></returns>
        public object ParseArray(StringReader Reader, Type ArrayType)
        {
            StringBuilder sb = new StringBuilder();
            bool IsInvalid = false;
            
            int Val = Reader.Peek();
            if (Val < 0)
                return null;

            char NextChar = (char)Val;

            // *** check for valid 
            if ('[' == Val)
            {
                // *** skip over the open Array tag
                Reader.Read();
            }


            // *** Retrieve the type of child elements - Can be an object, another Array, Collection, a Value etc.
            //
            // note: unless 'ArrayType' is an array of arrays 'ElementType' will not be an array type
            // which is the reason we never want to immedatly recurse into ourself passing 'ElementType' as
            // the 'ArrayType' parameter (see above)
            Type ElementType = this.GetArrayType(ArrayType);

            // *** Temporary storage for what gets parsed
            ArrayList Items = new ArrayList();

            // *** Partial value storage
            sb.Length = 0;  

            bool inQuote = false;

            // *** Start off in 'neutral'
            ParseStates State = ParseStates.None;

            // *** Set next char here so Lastchar will be set to the [
            NextChar = '[';

            // *** Loop through the characters
            while (true)
            {
                // *** Keep track of the last character
                char LastChar = NextChar;

                // *** Get the next input character
                Val = Reader.Read();
                if (Val < 0)
                {
                    // *** Must check for null first
                    if (sb.ToString() == "null")
                        return null;

                    // *** Array failed to close
                    throw new InvalidOperationException("ParseArray(): the array failed to close.");
                }
                NextChar = (char)Val;

                // *** wait for array termination or end of content
                if ((arrayTerminatorChar.IndexOf(NextChar) >= 0 || NextChar == arrayItemTerminatorChar) && sb.Length > 0)
                {
                    // gathering chars for a value
                    //
                    if (inQuote)
                    {
                        //
                        // then we allow the character to pass through and become part of the value
                        //
                    }
                    else
                    {

                        // *** Process the value - ignore if null
                        string value = sb.ToString();

                        if (value != "null")
                        {
                            object item = ParseValueString(value, ElementType);
                            if (null != item)
                            {
                                Items.Add(item);
                            }
                        }

                        // *** Reset
                        sb.Length = 0;
                        State = ParseStates.InPropertyValueTransition;
                    }
                }

                // ******
                // ParseStates.None indicates we're looking for some value (or object or array) or the end of the array
                // ParseStates.InPropertyValueTransition indicates we've parsed an object or another array
                // and now we're looking for a comma or the end of the array
                //
                // ParseStates.InValue says were gathering up characters for a value
                if (State == ParseStates.None && NextChar == '{')
                {
                   
                    // Add an object - but only when ParseState.None, presumably ParseObject() will check
                    // for the correct type of object                    
                    // TODO: This doesn't work correctly currently - 
                    object ParsedObject = this.ParseObject(Reader, ElementType, true);
                    if (ParsedObject != null)
                        Items.Add(ParsedObject);

                    // *** waiting on a comma or end of array
                    State = ParseStates.InPropertyValueTransition;
                }

                // *** Check for nested Array/List - only when ParseState.None
                else if (State == ParseStates.None && NextChar == '[')
                {
                    object ParsedObject = this.ParseArray(Reader, ElementType);
                    if (ParsedObject != null)
                        Items.Add(ParsedObject);

                    // *** wait on a comma or end of array
                    State = ParseStates.InPropertyValueTransition;
                }
                // *** Check for end of arrayType
                else if ((State == ParseStates.None || State == ParseStates.InPropertyValueTransition) && NextChar == ']')
                {
                    if (ArrayType.IsArray)
                    {
                        Array ElementArray = Activator.CreateInstance(ArrayType, Items.Count) as Array;
                        for (int i = 0; i < Items.Count; i++)
                        {
                            ElementArray.SetValue(Items[i], i);
                        }
                        return ElementArray;
                    }

                    // *** Not handling DataRow retrievals
                    //else if (ArrayType is DataRow)
                    //{
                    //}

                    else if (ArrayType.GetInterface("IList") != null)
                    {
                        IList Col = Activator.CreateInstance(ArrayType) as IList;
                        foreach (object Item in Items)
                            Col.Add(Item);

                        return Col;
                    }

                    // *** Not an array or IList
                    throw new InvalidOperationException("ParseArray(): Array type " + ArrayType.Name + " is not an Array or an IList.");
                }

                // *** White space elimination between properties/values
                else if (State == ParseStates.InPropertyValueTransition)
                {
                    // *** Skip over white space
                    while (Char.IsWhiteSpace((char)Reader.Peek()))
                        Reader.Read();

                    // *** if we don't find a comma something's wrong
                    if (',' != NextChar)
                        throw new InvalidOperationException("ParseArray(): Comma (',') expected while parsing JSON array.");

                    // *** look for next property
                    State = ParseStates.None;
                }
                else
                {
                    // *** Check for next property
                    if (State == ParseStates.None)
                    {
                        // *** Skip over white space and check next character
                        if (Char.IsWhiteSpace(NextChar))
                            continue;

                        // *** Otherwise we must be in a non-delimited value
                        State = ParseStates.InValue;
                    }

                    // *** This code should never really fire, but it's a safeguard

                    // *** Handle quotes special - we won't need to skip the quote here
                    // *** because it's simply passed to ParseValueString()                    
                    if ('"' == NextChar && '\\' != LastChar)
                        inQuote = !inQuote;

                    // *** just append to value
                    sb.Append(NextChar);
                }
            }
        }
#endif


        /// <summary>
        /// Returns the type of item type of the array/collection
        /// </summary>
        /// <param name="ArrayType"></param>
        /// <returns></returns>
        private Type GetArrayType(Type ArrayType)
        {
            if (ArrayType.IsArray)
                return ArrayType.GetElementType();

            if (ArrayType == typeof(DataTable))
                return typeof(DataRow);

            if (ArrayType.GetInterface("IList") != null)
            {
                MethodInfo Method = ArrayType.GetMethod("Add");
                ParameterInfo Parameter = Method.GetParameters()[0];
                Type ResultType = Parameter.ParameterType;
                return ResultType;
            }

            return null;

        }

        private enum ParseStates
        {
            None,
            InPropertyName,
            InPropertyValueTransition,
            InStringValue,
            InValue,
            InDate,
            InObject,
            EndOfObject
        }

        private void AssignProperty(object ResultObject, string Property, string Value)
        {
            MemberInfo[] mi = ResultObject.GetType().GetMember(Property,
                                                    BindingFlags.Instance |
                                                    BindingFlags.GetField | BindingFlags.GetProperty |
                                                    BindingFlags.IgnoreCase | BindingFlags.Public);
            if (mi == null || mi.Length < 1)
                return;

            if (mi[0].MemberType == MemberTypes.Field)
            {
                FieldInfo FInfo = mi[0] as FieldInfo;
                FInfo.SetValue(ResultObject, this.ParseValueString(Value, FInfo.FieldType));
            }
            else
            {
                PropertyInfo PInfo = mi[0] as PropertyInfo;
                if (PInfo.CanWrite)
                    PInfo.SetValue(ResultObject, this.ParseValueString(Value, PInfo.PropertyType), null);
            }
        }
        #endregion

    }
}
