/*
* Copyright (c) 2010 Michal Hruby <michal.mhr@gmail.com>
*               2017 elementary LLC.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Michal Hruby <michal.mhr@gmail.com>
*/

using Json;

namespace Synapse {
    public abstract class ConfigObject : GLib.Object { }

    public class ConfigService : GLib.Object {
        // singleton that can be easily destroyed
        private static unowned ConfigService? instance;

        public static ConfigService get_default () {
            return instance ?? new ConfigService ();
        }

        private ConfigService () { }

        ~ConfigService () {
            // useless cause the timer takes a reference on self
            if (save_timer_id != 0) save ();
            instance = null;
        }

        private Json.Node root_node;
        private string config_file_name;
        private uint save_timer_id = 0;

        construct {
            instance = this;

            var parser = new Parser ();
            config_file_name =
            GLib.Path.build_filename (Environment.get_user_config_dir (), "synapse", "config.json");
            try {
                parser.load_from_file (config_file_name);
                root_node = parser.get_root ().copy ();
                if (root_node.get_node_type () != NodeType.OBJECT) {
                    root_node = new Json.Node (NodeType.OBJECT);
                    root_node.take_object (new Json.Object ());
                }
            } catch (Error err) {
                root_node = new Json.Node (NodeType.OBJECT);
                root_node.take_object (new Json.Object ());
            }
        }

        /**
        * Creates an instance of an object derived from ConfigObject class, which
        * will have its public properties set to values stored in config file, or
        * to the default values if this object wasn't yet stored.
        *
        * @param group A group name.
        * @param key A key name.
        * @param config_type Type of the object (must be subclass of ConfigObject)
        * @return An instance of config_type.
        */
        public ConfigObject get_config (string group, string key, Type config_type) {
            unowned Json.Object obj = root_node.get_object ();
            unowned Json.Node group_node = obj.get_member (group);

            if (group_node != null) {
                if (group_node.get_node_type () == NodeType.OBJECT) {
                    unowned Json.Object group_obj = group_node.get_object ();
                    unowned Json.Node key_node = group_obj.get_member (key);

                    if (key_node != null && key_node.get_node_type () == NodeType.OBJECT) {
                        var result = Json.gobject_deserialize (config_type, key_node);
                        return result as ConfigObject;
                    }
                }
            }

            return GLib.Object.new (config_type) as ConfigObject;
        }

        /**
        * Behaves in a similar way to get_config, but it also watches for changes
        * in the returned config object and saves them back to the config file
        * (without the need of calling set_config).
        *
        * @param group A group name.
        * @param key A key name.
        * @param config_type Type of the object (must be subclass of ConfigObject)
        */
        public ConfigObject bind_config (string group, string key, Type config_type) {
            ConfigObject config_object = get_config (group, key, config_type);
            // make sure the lambda doesn't take a ref on the config_object
            unowned ConfigObject co = config_object;
            co.notify.connect (() => { this.set_config (group, key, co); });

            return config_object;
        }

        /**
        * Stores all public properties of the object to the config file under
        * specified group and key names.
        *
        * @param group A group name.
        * @param key A key name.
        * @param cfg_obj ConfigObject instance.
        */
        public void set_config (string group, string key, ConfigObject cfg_obj) {
            unowned Json.Object obj = root_node.get_object ();
            if (!obj.has_member (group) || obj.get_member (group).get_node_type () != NodeType.OBJECT) {
                // why set_object_member works, but set_member doesn't ?!
                obj.set_object_member (group, new Json.Object ());
            }

            unowned Json.Object group_obj = obj.get_object_member (group);
            // why the hell is this necessary?
            if (group_obj.has_member (key)) {
                group_obj.remove_member (key);
            }

            Json.Node node = Json.gobject_serialize (cfg_obj);
            group_obj.set_object_member (key, node.get_object ());

            if (save_timer_id != 0) {
                Source.remove (save_timer_id);
            }
            // on crap, this takes a reference on self
            save_timer_id = Timeout.add (30000, this.save_timeout);
        }

        private bool save_timeout () {
            save_timer_id = 0;
            save ();

            return false;
        }

        /**
        * Forces immediate saving of the configuration file to the filesystem.
        */
        public void save () {
            if (save_timer_id != 0) {
                Source.remove (save_timer_id);
                save_timer_id = 0;
            }

            var generator = new Generator ();
            generator.pretty = true;
            generator.set_root (root_node);

            DirUtils.create_with_parents (GLib.Path.get_dirname (config_file_name), 0755);
            try {
                generator.to_file (config_file_name);
            } catch (Error err) {
                warning ("%s", err.message);
            }
        }
    }
}
