namespace ProtonPlus.Models.VDF {
    enum BIN_TYPES {
        BIN_TYPE_NODE = 0x00,
        BIN_TYPE_STRING = 0x01,
        BIN_TYPE_INT32 = 0x02,
        BIN_TYPE_FLOAT32 = 0x03,
        BIN_TYPE_POINTER = 0x04,
        BIN_TYPE_WIDESTRING = 0x05,
        BIN_TYPE_COLOR = 0x06,
        BIN_TYPE_UINT64 = 0x07,
        BIN_TYPE_END = 0x08,
        BIN_TYPE_INT64 = 0x0A,
        BIN_TYPE_END_ALT = 0x0B,
    }

    public class Binary {
        private VDF.Node[] _nodes;
        public Gee.TreeMap<string, unowned VDF.Node> nodes;
        private GLib.File file;
        private InputStream input_stream;
        private DataInputStream reader;
        private int current_node_index;
        private DataOutputStream writer;

        private string eat_string () throws Error {
            size_t len;
            try {
                string str = reader.read_upto ("\0", 1, out len);
                reader.read_byte ();
                return str;
            } catch (Error e) {
                warning ("Error: %s", e.message);
                throw e;
            }
        }

        private void parse_node (string current_node) throws Error {
            try {
                _nodes += new VDF.Node(current_node);
                current_node_index = _nodes.length - 1;
                while (true) {
                    uint8 type = reader.read_byte ();
                    switch (type) {
                        case BIN_TYPES.BIN_TYPE_NODE:
                            parse_node (current_node + "." + eat_string ());
                            break;
                        case BIN_TYPES.BIN_TYPE_STRING:
                        case BIN_TYPES.BIN_TYPE_INT32:
                        case BIN_TYPES.BIN_TYPE_FLOAT32:
                        case BIN_TYPES.BIN_TYPE_POINTER:
                        case BIN_TYPES.BIN_TYPE_WIDESTRING:
                        case BIN_TYPES.BIN_TYPE_COLOR:
                        case BIN_TYPES.BIN_TYPE_UINT64:
                        case BIN_TYPES.BIN_TYPE_INT64:
                            get_data (type);
                            break;
                        case BIN_TYPES.BIN_TYPE_END:
                        case BIN_TYPES.BIN_TYPE_END_ALT:
                            return;
                        default:
                            throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Unexpected byte");
                    }
                }
            } catch (Error e) {
                warning ("Error: %s", e.message);
                throw e;
            }
        }

        private void get_data (BIN_TYPES type) throws Error {
            if (current_node_index < 0) {
                throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Not in a node");
            }

            switch (type) {
                case BIN_TYPES.BIN_TYPE_STRING:
                case BIN_TYPES.BIN_TYPE_WIDESTRING:
                    _nodes[current_node_index].set (eat_string (), new GLib.Variant.string(eat_string ()));
                    break;
                case BIN_TYPES.BIN_TYPE_INT32:
                    _nodes[current_node_index].set (eat_string (), new GLib.Variant.int32(reader.read_int32 ()));
                    break;
                case BIN_TYPES.BIN_TYPE_FLOAT32:
                    throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Floats not supported yet");
                case BIN_TYPES.BIN_TYPE_POINTER:
                    throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Pointers not supported yet");
                case BIN_TYPES.BIN_TYPE_COLOR:
                    throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Colors not supported yet");
                case BIN_TYPES.BIN_TYPE_UINT64:
                    _nodes[current_node_index].set (eat_string (), new GLib.Variant.uint64(reader.read_uint64 ()));
                    break;
                case BIN_TYPES.BIN_TYPE_INT64:
                    _nodes[current_node_index].set (eat_string (), new GLib.Variant.int64(reader.read_int64 ()));
                    break;
                default:
                    throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Unexpected byte");
            }
        }

        private uint count_dots (string str) {
            uint ret = 0;
            for (int i = 0; i < str.length; i++) {
                if (str[i] == '.') {
                    ret++;
                }
            }
            return ret;
        }

        private void save_node (string node_name, uint layer, string[] _nodes) throws Error {
            try {
                writer.put_byte (BIN_TYPES.BIN_TYPE_NODE);
                var splitted_name = node_name.split (".");
                writer.put_string (splitted_name[splitted_name.length - 1]);
                writer.put_byte ('\0');

                var node = nodes.get (node_name);
                foreach (var elem in node.entries) {
                    if (elem.value.is_of_type (VariantType.STRING)) {
                        writer.put_byte (BIN_TYPES.BIN_TYPE_STRING);
                        writer.put_string (elem.key);
                        writer.put_byte ('\0');
                        writer.put_string (elem.value.get_string ());
                        writer.put_byte ('\0');
                    } else if (elem.value.is_of_type (VariantType.UINT64)) {
                        writer.put_byte (BIN_TYPES.BIN_TYPE_UINT64);
                        writer.put_string (elem.key);
                        writer.put_byte ('\0');
                        writer.put_uint64 (elem.value.get_uint64 ());
                    } else if (elem.value.is_of_type (VariantType.INT64)) {
                        writer.put_byte (BIN_TYPES.BIN_TYPE_INT64);
                        writer.put_string (elem.key);
                        writer.put_byte ('\0');
                        writer.put_int64 (elem.value.get_int64 ());
                    } else if (elem.value.is_of_type (VariantType.INT32)) {
                        writer.put_byte (BIN_TYPES.BIN_TYPE_INT32);
                        writer.put_string (elem.key);
                        writer.put_byte ('\0');
                        writer.put_int32 (elem.value.get_int32 ());
                    }
                }

                foreach (var node_i in _nodes) {
                    if (node_i.contains (node_name)) {
                        if (count_dots (node_i) == layer + 1) {
                            save_node (node_i, layer + 1, _nodes);
                        }
                    }
                }
                writer.put_byte (BIN_TYPES.BIN_TYPE_END);
            } catch (Error e) {
                throw e;
            }
        }

        public void save () throws Error {
            try {
                var output_stream = file.replace (null, false, FileCreateFlags.PRIVATE);
                writer = new DataOutputStream(output_stream);
                writer.set_byte_order (DataStreamByteOrder.LITTLE_ENDIAN);

                var keys = nodes.keys.to_array ();
                string[] root_nodes = {};
                foreach (var elem in keys) {
                    if (!elem.contains (".")) {
                        root_nodes += elem;
                    }
                }

                foreach (var rn in root_nodes) {
                    save_node (rn, 0, keys);
                }

                writer.put_byte (BIN_TYPES.BIN_TYPE_END);

                output_stream.flush ();
                output_stream.close (null);
            } catch (Error e) {
                throw e;
            }
        }

        public Binary(string path) {
            try {
                file = GLib.File.new_for_path (path);
                input_stream = file.read ();
                reader = new DataInputStream(input_stream);
                current_node_index = -1;
                reader.set_byte_order (DataStreamByteOrder.LITTLE_ENDIAN);
                nodes = new Gee.TreeMap<string, unowned VDF.Node> ();

                while (true) {
                    uint8 type = reader.read_byte ();
                    switch (type) {
                        case BIN_TYPES.BIN_TYPE_NODE:
                            parse_node (eat_string ());
                            break;
                        case BIN_TYPES.BIN_TYPE_END:
                        case BIN_TYPES.BIN_TYPE_END_ALT:
                            foreach (var node in _nodes) {
                                nodes.set (node.node_name, node);
                            }
                            input_stream.close (null);
                            return;
                        default:
                            throw new GLib.Error(GLib.Quark.from_string ("vala-vdf"), 0, "Unexpected byte");
                    }
                }
            } catch (Error e) {
                warning ("%s\n", e.message);
            }
        }
    }
}