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

namespace Ink.Runtime
{
    /// <summary>
    /// Simple custom JSON serialisation implementation that takes JSON-able System.Collections that
    /// are produced by the ink engine and converts to and from JSON text.
    /// </summary>
    public static class SimpleJson
    {
        public static Dictionary<string, object> TextToDictionary (string text)
        {
            return new Reader (text).ToDictionary ();
        }

        public static List<object> TextToArray(string text)
        {
            return new Reader(text).ToArray();
        }

        class Reader
        {
            public Reader (string text)
            {
                _text = text;
                _offset = 0;

                SkipWhitespace ();

                _rootObject = ReadObject ();
            }

            public Dictionary<string, object> ToDictionary ()
            {
                return (Dictionary<string, object>)_rootObject;
            }

            public List<object> ToArray()
            {
                return (List<object>)_rootObject;
            }

            bool IsNumberChar (char c)
            {
                return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e';
            }

            bool IsFirstNumberChar(char c)
            {
                return c >= '0' && c <= '9' || c == '-' || c == '+';
            }

            object ReadObject ()
            {
                var currentChar = _text [_offset];

                if( currentChar == '{' )
                    return ReadDictionary ();
                
                else if (currentChar == '[')
                    return ReadArray ();

                else if (currentChar == '"')
                    return ReadString ();

                else if (IsFirstNumberChar(currentChar))
                    return ReadNumber ();

                else if (TryRead ("true"))
                    return true;

                else if (TryRead ("false"))
                    return false;

                else if (TryRead ("null"))
                    return null;

                throw new System.Exception ("Unhandled object type in JSON: "+_text.Substring (_offset, 30));
            }

            Dictionary<string, object> ReadDictionary ()
            {
                var dict = new Dictionary<string, object> ();

                Expect ("{");

                SkipWhitespace ();

                // Empty dictionary?
                if (TryRead ("}"))
                    return dict;

                do {

                    SkipWhitespace ();

                    // Key
                    var key = ReadString ();
                    Expect (key != null, "dictionary key");

                    SkipWhitespace ();

                    // :
                    Expect (":");

                    SkipWhitespace ();

                    // Value
                    var val = ReadObject ();
                    Expect (val != null, "dictionary value");

                    // Add to dictionary
                    dict [key] = val;

                    SkipWhitespace ();

                } while ( TryRead (",") );

                Expect ("}");

                return dict;
            }

            List<object> ReadArray ()
            {
                var list = new List<object> ();

                Expect ("[");

                SkipWhitespace ();

                // Empty list?
                if (TryRead ("]"))
                    return list;

                do {

                    SkipWhitespace ();

                    // Value
                    var val = ReadObject ();

                    // Add to array
                    list.Add (val);

                    SkipWhitespace ();

                } while (TryRead (","));

                Expect ("]");

                return list;
            }

            string ReadString ()
            {
                Expect ("\"");

                var sb = new StringBuilder();

                for (; _offset < _text.Length; _offset++) {
                    var c = _text [_offset];

                    if (c == '\\') {
                        // Escaped character
                        _offset++;
                        if (_offset >= _text.Length) {
                            throw new Exception("Unexpected EOF while reading string");
                        }
                        c = _text[_offset];
                        switch (c)
                        {
                            case '"':
                            case '\\':
                            case '/': // Yes, JSON allows this to be escaped
                                sb.Append(c);
                                break;
                            case 'n':
                                sb.Append('\n');
                                break;
                            case 't':
                                sb.Append('\t');
                                break;
                            case 'r':
                            case 'b':
                            case 'f':
                                // Ignore other control characters
                                break;
                            case 'u':
                                // 4-digit Unicode
                                if (_offset + 4 >=_text.Length) {
                                    throw new Exception("Unexpected EOF while reading string");
                                }
                                var digits = _text.Substring(_offset + 1, 4);
                                int uchar;
                                if (int.TryParse(digits, System.Globalization.NumberStyles.AllowHexSpecifier, System.Globalization.CultureInfo.InvariantCulture, out uchar)) {
                                    sb.Append((char)uchar);
                                    _offset += 4;
                                } else {
                                    throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
                                }
                                break;
                            default:
                                // The escaped character is invalid per json spec
                                throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
                        }
                    } else if( c == '"' ) {
                        break;
                    } else {
                        sb.Append(c);
                    }
                }

                Expect ("\"");
                return sb.ToString();
            }

            object ReadNumber ()
            {
                var startOffset = _offset;

                bool isFloat = false;
                for (; _offset < _text.Length; _offset++) {
                    var c = _text [_offset];
                    if (c == '.' || c == 'e' || c == 'E') isFloat = true;
                    if (IsNumberChar (c))
                        continue;
                    else
                        break;
                }

                string numStr = _text.Substring (startOffset, _offset - startOffset);

                if (isFloat) {
                    float f;
                    if (float.TryParse (numStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) {
                        return f;
                    }
                } else {
                    int i;
                    if (int.TryParse (numStr, out i)) {
                        return i;
                    }
                }

                throw new System.Exception ("Failed to parse number value: "+numStr);
            }

            bool TryRead (string textToRead)
            {
                if (_offset + textToRead.Length > _text.Length)
                    return false;
                
                for (int i = 0; i < textToRead.Length; i++) {
                    if (textToRead [i] != _text [_offset + i])
                        return false;
                }

                _offset += textToRead.Length;

                return true;
            }

            void Expect (string expectedStr)
            {
                if (!TryRead (expectedStr))
                    Expect (false, expectedStr);
            }

            void Expect (bool condition, string message = null)
            {
                if (!condition) {
                    if (message == null) {
                        message = "Unexpected token";
                    } else {
                        message = "Expected " + message;
                    }
                    message += " at offset " + _offset;

                    throw new System.Exception (message);
                }
            }

            void SkipWhitespace ()
            {
                while (_offset < _text.Length) {
                    var c = _text [_offset];
                    if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
                        _offset++;
                    else
                        break;
                }
            }

            string _text;
            int _offset;

            object _rootObject;
        }


        public class Writer
        {
            public Writer()
            {
                _writer = new StringWriter();
            }

            public Writer(Stream stream)
            {
                _writer = new System.IO.StreamWriter(stream, Encoding.UTF8);
            }

            public void WriteObject(Action<Writer> inner)
            {
                WriteObjectStart();
                inner(this);
                WriteObjectEnd();
            }

            public void WriteObjectStart()
            {
                StartNewObject(container: true);
                _stateStack.Push(new StateElement { type = State.Object });
                _writer.Write("{");
            }

            public void WriteObjectEnd()
            {
                Assert(state == State.Object);
                _writer.Write("}");
                _stateStack.Pop();
            }

            public void WriteProperty(string name, Action<Writer> inner)
            {
                WriteProperty<string>(name, inner);
            }

            public void WriteProperty(int id, Action<Writer> inner)
            {
                WriteProperty<int>(id, inner);
            }

            public void WriteProperty(string name, string content)
            {
                WritePropertyStart(name);
                Write(content);
                WritePropertyEnd();
            }

            public void WriteProperty(string name, int content)
            {
                WritePropertyStart(name);
                Write(content);
                WritePropertyEnd();
            }

            public void WriteProperty(string name, bool content)
            {
                WritePropertyStart(name);
                Write(content);
                WritePropertyEnd();
            }

            public void WritePropertyStart(string name)
            {
                WritePropertyStart<string>(name);
            }

            public void WritePropertyStart(int id)
            {
                WritePropertyStart<int>(id);
            }

            public void WritePropertyEnd()
            {
                Assert(state == State.Property);
                Assert(childCount == 1);
                _stateStack.Pop();
            }

            public void WritePropertyNameStart()
            {
                Assert(state == State.Object);

                if (childCount > 0)
                    _writer.Write(",");

                _writer.Write("\"");

                IncrementChildCount();

                _stateStack.Push(new StateElement { type = State.Property });
                _stateStack.Push(new StateElement { type = State.PropertyName });
            }

            public void WritePropertyNameEnd()
            {
                Assert(state == State.PropertyName);

                _writer.Write("\":");

                // Pop PropertyName, leaving Property state
                _stateStack.Pop();
            }

            public void WritePropertyNameInner(string str)
            {
                Assert(state == State.PropertyName);
                _writer.Write(str);
            }

            void WritePropertyStart<T>(T name)
            {
                Assert(state == State.Object);

                if (childCount > 0)
                    _writer.Write(",");

                _writer.Write("\"");
                _writer.Write(name);
                _writer.Write("\":");

                IncrementChildCount();

                _stateStack.Push(new StateElement { type = State.Property });
            }


            // allow name to be string or int
            void WriteProperty<T>(T name, Action<Writer> inner)
            {
                WritePropertyStart(name);

                inner(this);

                WritePropertyEnd();
            }

            public void WriteArrayStart()
            {
                StartNewObject(container: true);
                _stateStack.Push(new StateElement { type = State.Array });
                _writer.Write("[");
            }

            public void WriteArrayEnd()
            {
                Assert(state == State.Array);
                _writer.Write("]");
                _stateStack.Pop();
            }

            public void Write(int i)
            {
                StartNewObject(container: false);
                _writer.Write(i);
            }

            public void Write(float f)
            {
                StartNewObject(container: false);

                // TODO: Find an heap-allocation-free way to do this please!
                // _writer.Write(formatStr, obj (the float)) requires boxing
                // Following implementation seems to work ok but requires creating temporary garbage string.
                string floatStr = f.ToString(System.Globalization.CultureInfo.InvariantCulture);
                if( floatStr == "Infinity" ) {
                    _writer.Write("3.4E+38"); // JSON doesn't support, do our best alternative
                } else if (floatStr == "-Infinity") {
                    _writer.Write("-3.4E+38"); // JSON doesn't support, do our best alternative
                } else if ( floatStr == "NaN" ) {
                    _writer.Write("0.0"); // JSON doesn't support, not much we can do
                } else {
                    _writer.Write(floatStr);
                    if (!floatStr.Contains(".") && !floatStr.Contains("E")) 
                        _writer.Write(".0"); // ensure it gets read back in as a floating point value
                }
            }

            public void Write(string str, bool escape = true)
            {
                StartNewObject(container: false);

                _writer.Write("\"");
                if (escape)
                    WriteEscapedString(str);
                else
                    _writer.Write(str);
                _writer.Write("\"");
            }

            public void Write(bool b)
            {
                StartNewObject(container: false);
                _writer.Write(b ? "true" : "false");
            }

            public void WriteNull()
            {
                StartNewObject(container: false);
                _writer.Write("null");
            }

            public void WriteStringStart()
            {
                StartNewObject(container: false);
                _stateStack.Push(new StateElement { type = State.String });
                _writer.Write("\"");
            }

            public void WriteStringEnd()
            {
                Assert(state == State.String);
                _writer.Write("\"");
                _stateStack.Pop();
            }

            public void WriteStringInner(string str, bool escape = true)
            {
                Assert(state == State.String);
                if (escape)
                    WriteEscapedString(str);
                else
                    _writer.Write(str);
            }

            void WriteEscapedString(string str)
            {
                foreach (var c in str)
                {
                    if (c < ' ')
                    {
                        // Don't write any control characters except \n and \t
                        switch (c)
                        {
                            case '\n':
                                _writer.Write("\\n");
                                break;
                            case '\t':
                                _writer.Write("\\t");
                                break;
                        }
                    }
                    else
                    {
                        switch (c)
                        {
                            case '\\':
                            case '"':
                                _writer.Write("\\");
                                _writer.Write(c);
                                break;
                            default:
                                _writer.Write(c);
                                break;
                        }
                    }
                }
            }

            void StartNewObject(bool container)
            {

                if (container)
                    Assert(state == State.None || state == State.Property || state == State.Array);
                else
                    Assert(state == State.Property || state == State.Array);

                if (state == State.Array && childCount > 0)
                    _writer.Write(",");

                if (state == State.Property)
                    Assert(childCount == 0);

                if (state == State.Array || state == State.Property)
                    IncrementChildCount();
            }

            State state
            {
                get
                {
                    if (_stateStack.Count > 0) return _stateStack.Peek().type;
                    else return State.None;
                }
            }

            int childCount
            {
                get
                {
                    if (_stateStack.Count > 0) return _stateStack.Peek().childCount;
                    else return 0;
                }
            }

            void IncrementChildCount()
            {
                Assert(_stateStack.Count > 0);
                var currEl = _stateStack.Pop();
                currEl.childCount++;
                _stateStack.Push(currEl);
            }

            // Shouldn't hit this assert outside of initial JSON development,
            // so it's save to make it debug-only.
            [System.Diagnostics.Conditional("DEBUG")]
            void Assert(bool condition)
            {
                if (!condition)
                    throw new System.Exception("Assert failed while writing JSON");
            }

            public override string ToString()
            {
                return _writer.ToString();
            }

            enum State
            {
                None,
                Object,
                Array,
                Property,
                PropertyName,
                String
            };

            struct StateElement
            {
                public State type;
                public int childCount;
            }

            Stack<StateElement> _stateStack = new Stack<StateElement>();
            TextWriter _writer;
        }


    }
}

