namespace Php {
    public enum ValueType {
        NULL,
        BOOL,
        INT,
        DOUBLE,
        STRING,
        ARRAY,
        OBJECT
    }

    public class zval {
        private ValueType value_type;
        private bool bool_value;
        private int64 int_value;
        private double double_value;
        private string string_data = "";
        private Gee.ArrayList<zval> array_value = new Gee.ArrayList<zval> ();
        private Gee.HashMap<string,zval> object_value = new Gee.HashMap<string,zval> ();
        private Gee.ArrayList<string> object_keys = new Gee.ArrayList<string> ();

        private zval (ValueType value_type) {
            this.value_type = value_type;
        }

        public static zval null_value () {
            return new zval (ValueType.NULL);
        }

        public static zval boolean (bool value) {
            var result = new zval (ValueType.BOOL);
            result.bool_value = value;
            return result;
        }

        public static zval integer (int64 value) {
            var result = new zval (ValueType.INT);
            result.int_value = value;
            return result;
        }

        public static zval double_number (double value) {
            var result = new zval (ValueType.DOUBLE);
            result.double_value = value;
            return result;
        }

        public static zval string_value (string value) {
            var result = new zval (ValueType.STRING);
            result.string_data = value;
            return result;
        }

        public static zval array () {
            return new zval (ValueType.ARRAY);
        }

        public static zval object () {
            return new zval (ValueType.OBJECT);
        }

        public ValueType get_value_type () {
            return value_type;
        }

        public bool is_null () {
            return value_type == ValueType.NULL;
        }

        public bool as_bool () {
            return bool_value;
        }

        public int64 as_int () {
            return int_value;
        }

        public double as_double () {
            return double_value;
        }

        public string as_string () {
            return string_data;
        }

        public void append (zval value) {
            if (value_type != ValueType.ARRAY)
                return;
            array_value.add (value);
        }

        public void set (string key, zval value) {
            if (value_type != ValueType.OBJECT)
                return;
            if (!object_value.has_key (key))
                object_keys.add (key);
            object_value[key] = value;
        }

        public zval? get (string key) {
            if (value_type != ValueType.OBJECT)
                return null;
            return object_value.get (key);
        }

        public zval? get_index (int index) {
            if (value_type != ValueType.ARRAY || index < 0 || index >= array_value.size)
                return null;
            return array_value[index];
        }

        public int count () {
            if (value_type == ValueType.ARRAY)
                return array_value.size;
            if (value_type == ValueType.OBJECT)
                return object_value.size;
            return 0;
        }

        public Gee.ArrayList<zval> get_array_values () {
            return array_value;
        }

        public Gee.HashMap<string,zval> get_object_values () {
            return object_value;
        }

        public Gee.ArrayList<string> get_object_keys () {
            return object_keys;
        }

        public int get_key_count () {
            return object_keys.size;
        }

        public string? get_key_at (int index) {
            if (index < 0 || index >= object_keys.size)
                return null;
            return object_keys[index];
        }

        private string escape_json_string (string value) {
            var builder = new StringBuilder ();
            for (int i = 0; i < value.length; i++) {
                char ch = value[i];
                switch (ch) {
                case '"':
                    builder.append ("\\\"");
                    break;
                case '\\':
                    builder.append ("\\\\");
                    break;
                case '\b':
                    builder.append ("\\b");
                    break;
                case '\f':
                    builder.append ("\\f");
                    break;
                case '\n':
                    builder.append ("\\n");
                    break;
                case '\r':
                    builder.append ("\\r");
                    break;
                case '\t':
                    builder.append ("\\t");
                    break;
                default:
                    if ((uchar) ch < 0x20)
                        builder.append ("\\u%04x".printf ((uint) (uchar) ch));
                    else
                        builder.append_c (ch);
                    break;
                }
            }
            return builder.str;
        }

        private void add_json_indent (StringBuilder builder, int indent) {
            for (int i = 0; i < indent; i++)
                builder.append_c (' ');
        }

        private void add_to_json_builder (StringBuilder builder, bool pretty, int indent) {
            switch (value_type) {
            case ValueType.NULL:
                builder.append ("null");
                break;
            case ValueType.BOOL:
                builder.append (bool_value ? "true" : "false");
                break;
            case ValueType.INT:
                builder.append (("%" + int64.FORMAT).printf (int_value));
                break;
            case ValueType.DOUBLE:
                builder.append ("%g".printf (double_value));
                break;
            case ValueType.STRING:
                builder.append_c ('"');
                builder.append (escape_json_string (string_data));
                builder.append_c ('"');
                break;
            case ValueType.ARRAY:
                builder.append_c ('[');
                for (int i = 0; i < array_value.size; i++) {
                    if (i > 0)
                        builder.append_c (',');
                    if (pretty) {
                        builder.append_c ('\n');
                        add_json_indent (builder, indent + 2);
                    }
                    array_value[i].add_to_json_builder (builder, pretty, indent + 2);
                }
                if (pretty && array_value.size > 0) {
                    builder.append_c ('\n');
                    add_json_indent (builder, indent);
                }
                builder.append_c (']');
                break;
            case ValueType.OBJECT:
                builder.append_c ('{');
                for (int i = 0; i < object_keys.size; i++) {
                    string key = object_keys[i];
                    if (i > 0)
                        builder.append_c (',');
                    if (pretty) {
                        builder.append_c ('\n');
                        add_json_indent (builder, indent + 2);
                    }
                    builder.append_c ('"');
                    builder.append (escape_json_string (key));
                    builder.append (pretty ? "\": " : "\":");
                    object_value[key].add_to_json_builder (builder, pretty, indent + 2);
                }
                if (pretty && object_keys.size > 0) {
                    builder.append_c ('\n');
                    add_json_indent (builder, indent);
                }
                builder.append_c ('}');
                break;
            }
        }

        public string to_json (bool pretty = false) {
            var builder = new StringBuilder ();
            add_to_json_builder (builder, pretty, 0);
            return builder.str;
        }

    }
}
