/*
   Copyright (C) 2013 André Stösel <andre@stoesel.de>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   See the file COPYING for the full license text.
*/

namespace Tabby {
    int IDLE_RESTORE_COUNT = 13;
    /* FixMe: don't use a global object */
    Midori.App? APP;

    /* function called from Manager object */
    public interface IStorage : GLib.Object {
        public abstract Katze.Array get_saved_sessions ();
        public abstract Base.Session get_new_session ();
        public abstract void restore_last_sessions ();
        public abstract void import_session (Katze.Array tabs);
    }

    public interface ISession : GLib.Object {
        public abstract Katze.Array get_tabs ();
        /* Add one tab to the database */
        public abstract void add_item (Katze.Item item);
        /* Attach to a browser */
        public abstract void attach (Midori.Browser browser);
        /* Attach to a browser and populate it with tabs from the database */
        public abstract void restore (Midori.Browser browser);
        /* Remove all tabs from the database */
        public abstract void remove ();
        /* Run when a browser is closed */
        public abstract void close ();
    }

    public enum SessionState {
        OPEN,
        CLOSED,
        RESTORING
    }

    namespace Base {
        /* each base class should connect to all necessary signals and provide an abstract function to handle them */

        public abstract class Storage : GLib.Object, IStorage {
            public Midori.App app { get; construct; }

            public abstract Katze.Array get_saved_sessions ();
            public abstract Base.Session get_new_session ();

            public void start_new_session () {
                Katze.Array sessions = new Katze.Array (typeof (Session));
                this.init_sessions (sessions);
            }

            public void restore_last_sessions () {
                Katze.Array sessions = this.get_saved_sessions ();
                this.init_sessions (sessions);
            }

            private void init_sessions (Katze.Array sessions) {
                if (sessions.is_empty ()) {
                    sessions.add_item (this.get_new_session ());
                }

                GLib.List<unowned Katze.Item> items = sessions.get_items ();
                foreach (Katze.Item item in items) {
                    Session session = item as Session;
                    Midori.Browser browser = this.app.create_browser ();

                    /* FixMe: tabby-session should be set in .restore and .attch */
                    browser.set_data<Base.Session> ("tabby-session", session as Base.Session);

                    app.add_browser (browser);
                    browser.show ();

                    session.restore (browser);
                }
            }

            public virtual void import_session (Katze.Array tabs) {
                Session session = this.get_new_session ();
                GLib.List<unowned Katze.Item> items = tabs.get_items ();
                double i = 0;
                foreach (Katze.Item item in items) {
                    item.set_meta_string ("sorting", i.to_string());
                    // See midori_browser_step_history: don't add to history
                    item.set_meta_string ("history-step", "ignore");
                    i += 1024;
                    session.add_item (item);
                }
            }
        }

        public abstract class Session : GLib.Object, ISession {
            protected GLib.SList<double?> tab_sorting;

            public Midori.Browser? browser { get; protected set; default = null; }
            public SessionState state { get; protected set; default = SessionState.CLOSED; }

            public abstract void add_item (Katze.Item item);
            public abstract void uri_changed (Midori.View view, string uri);
            public abstract void data_changed (Midori.View view);
            public abstract void tab_added (Midori.Browser browser, Midori.View view);
            public abstract void tab_removed (Midori.Browser browser, Midori.View view);
            public abstract void tab_switched (Midori.View? old_view, Midori.View? new_view);
            public abstract void tab_reordered (Gtk.Widget tab, uint pos);

            public abstract void remove ();

            public abstract Katze.Array get_tabs ();
            public abstract double get_max_sorting ();

            public void attach (Midori.Browser browser) {
                this.browser = browser;

                browser.add_tab.connect_after (this.tab_added);
                browser.add_tab.connect (this.helper_data_changed);

                browser.remove_tab.connect (this.tab_removed);
                browser.switch_tab.connect (this.tab_switched);
                browser.delete_event.connect_after(this.delete_event);
                browser.notebook.page_reordered.connect_after (this.tab_reordered);

                this.state = SessionState.OPEN;

                foreach (Midori.View view in browser.get_tabs ()) {
                    this.tab_added (browser, view);
                    this.helper_data_changed (browser, view);
                }
            }

            public void restore (Midori.Browser browser) {
                this.browser = browser;

                Katze.Array tabs = this.get_tabs ();
                unowned Katze.Array? open_uris = browser.get_data ("tabby-open-uris");

                if(tabs.is_empty () && open_uris == null) {
                    /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */
                    int load_on_startup;
                    APP.settings.get ("load-on-startup", out load_on_startup);

                    Katze.Item item = new Katze.Item ();

                    if (load_on_startup == Midori.MidoriStartup.BLANK_PAGE) {
                        item.uri = "about:dial";
                    } else {
                        item.uri = "about:home";
                    }

                    tabs.add_item (item);
                }

                browser.add_tab.connect_after (this.tab_added);
                browser.add_tab.connect (this.helper_data_changed);

                browser.remove_tab.connect (this.tab_removed);
                browser.switch_tab.connect (this.tab_switched);
                browser.delete_event.connect_after(this.delete_event);
                browser.notebook.page_reordered.connect_after (this.tab_reordered);

                GLib.List<unowned Katze.Item> items = new GLib.List<unowned Katze.Item> ();
                if (open_uris != null) {
                    items.concat (open_uris.get_items ());
                }
                items.concat (tabs.get_items ());
                unowned GLib.List<unowned Katze.Item> u_items = items;

                bool delay = false;
                bool should_delay = false;

                int load_on_startup;
                APP.settings.get ("load-on-startup", out load_on_startup);
                should_delay = load_on_startup == Midori.MidoriStartup.DELAYED_PAGES;

                if (APP.crashed == true) {
                    delay = true;
                    should_delay = true;
                }

                this.state = SessionState.RESTORING;

                GLib.Idle.add (() => {
                    /* Note: we need to use `items` for something to maintain a valid reference */
                    GLib.PtrArray new_tabs = new GLib.PtrArray ();
                    if (items.length () > 0) {
                        for (int i = 0; i < IDLE_RESTORE_COUNT; i++) {
                            if (u_items == null) {
                                this.helper_reorder_tabs (new_tabs);
                                this.state = SessionState.OPEN;
                                return false;
                            }

                            Katze.Item t_item = u_items.data<Katze.Item>;

                            t_item.set_meta_integer ("append", 1);

                            if (delay && should_delay)
                                t_item.set_meta_integer ("delay", Midori.Delay.DELAYED);
                            else
                                delay = true;

                            unowned Gtk.Widget tab = browser.add_item (t_item);
                            new_tabs.add (tab);

                            u_items = u_items.next;
                        }
                        this.helper_reorder_tabs (new_tabs);
                    }
                    if (u_items == null) {
                        this.state = SessionState.OPEN;
                        return false;
                    }
                    return true;
                });
            }

            public virtual void close () {
                if (this.state == SessionState.CLOSED) {
                    assert (this.browser == null);
                } else {
                    this.state = SessionState.CLOSED;

                    this.browser.add_tab.disconnect (this.tab_added);
                    this.browser.add_tab.disconnect (this.helper_data_changed);
                    this.browser.remove_tab.disconnect (this.tab_removed);
                    this.browser.switch_tab.disconnect (this.tab_switched);
                    this.browser.delete_event.disconnect (this.delete_event);
                    this.browser.notebook.page_reordered.disconnect (this.tab_reordered);

                    this.browser = null;
                }
            }

#if HAVE_GTK3
            protected bool delete_event (Gtk.Widget widget, Gdk.EventAny event) {
#else
            protected bool delete_event (Gtk.Widget widget, Gdk.Event event) {
#endif

                this.close ();
                return false;

            }

            protected double get_tab_sorting (Midori.View view) {
                int this_pos = this.browser.notebook.page_num (view);
                Midori.View prev_view = this.browser.notebook.get_nth_page (this_pos - 1) as Midori.View;
                Midori.View next_view = this.browser.notebook.get_nth_page (this_pos + 1) as Midori.View;

                string prev_meta_sorting = null;
                string next_meta_sorting = null;
                double prev_sorting, next_sorting, this_sorting;

                if (prev_view != null) {
                    unowned Katze.Item prev_item = prev_view.get_proxy_item ();
                    prev_meta_sorting = prev_item.get_meta_string ("sorting");
                }

                if (prev_meta_sorting == null)
                    if (this.state == SessionState.RESTORING)
                        prev_sorting = this.get_max_sorting ();
                    else
                        prev_sorting = double.parse ("0");
                else
                    prev_sorting = double.parse (prev_meta_sorting);

                if (next_view != null) {
                    unowned Katze.Item next_item = next_view.get_proxy_item ();
                    next_meta_sorting = next_item.get_meta_string ("sorting");
                }

                if (next_meta_sorting == null)
                    next_sorting = prev_sorting + 2048;
                else
                    next_sorting = double.parse (next_meta_sorting);

                this_sorting = prev_sorting + (next_sorting - prev_sorting) / 2;

                return this_sorting;
            }

            private void load_status (GLib.Object _view, ParamSpec pspec) {
                Midori.View view = (Midori.View)_view;

                if (view.load_status == Midori.LoadStatus.PROVISIONAL) {
                    unowned Katze.Item item = view.get_proxy_item ();

                    int64 delay = item.get_meta_integer ("delay");
                    if (delay == Midori.Delay.UNDELAYED) {
                        view.web_view.notify["uri"].connect ( () => {
                                this.uri_changed (view, view.web_view.uri);
                            });
                        view.web_view.notify["title"].connect ( () => {
                                this.data_changed (view);
                            });

                    }

                    view.notify["load-status"].disconnect (load_status);
                }
            }

            private void helper_data_changed (Midori.Browser browser, Midori.View view) {
               view.notify["load-status"].connect (load_status);

               view.new_view.connect (this.helper_duplicate_tab);
            }

            private void helper_reorder_tabs (GLib.PtrArray new_tabs) {
                CompareDataFunc<double?> helper_compare_data = (a, b) => {
                    if (a > b)
                        return 1;
                    else if(a < b)
                        return -1;
                    return 0;
                };

                GLib.CompareFunc<double?> helper_compare_func = (a,b) => {
                    return a == b ? 0 : -1;
                };

                this.browser.notebook.page_reordered.disconnect (this.tab_reordered);
                for(var i = 0; i < new_tabs.len; i++) {
                    Midori.View tab = new_tabs.index(i) as Midori.View;

                    unowned Katze.Item item = tab.get_proxy_item ();

                    double sorting;
                    string? sorting_string = item.get_meta_string ("sorting");
                    if (sorting_string != null) { /* we have to use a seperate if condition to avoid a `possibly unassigned local variable` error */
                        if (double.try_parse (item.get_meta_string ("sorting"), out sorting)) {
                            this.tab_sorting.insert_sorted_with_data (sorting, helper_compare_data);

                            int index = this.tab_sorting.position (this.tab_sorting.find_custom (sorting, helper_compare_func));

                            this.browser.notebook.reorder_child (tab, index);
                        }
                    }
                }
                this.browser.notebook.page_reordered.connect_after (this.tab_reordered);
            }

            private void helper_duplicate_tab (Midori.View view, Midori.View new_view, Midori.NewView where, bool user_initiated) {
                unowned Katze.Item item = view.get_proxy_item ();
                unowned Katze.Item new_item = new_view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                int64 new_tab_id = new_item.get_meta_integer ("tabby-id");

                if (tab_id > 0 && tab_id == new_tab_id) {
                    new_item.set_meta_integer ("tabby-id", 0);
                }
            }

            construct {
                this.tab_sorting = new GLib.SList<double?> ();
            }
        }
    }

    namespace Local {
        private class Session : Base.Session {
            public int64 id { get; private set; }
            private Midori.Database database;

            public override void add_item (Katze.Item item) {
                GLib.DateTime time = new DateTime.now_local ();
                string? sorting = item.get_meta_string ("sorting") ?? "1";
                string sqlcmd = "INSERT INTO `tabs` (`crdate`, `tstamp`, `session_id`, `uri`, `title`, `sorting`) VALUES (:crdate, :tstamp, :session_id, :uri, :title, :sorting);";

                int64 tstamp = item.get_meta_integer ("tabby-tstamp");
                if (tstamp < 0) { // new tab without focus
                    tstamp = 0;
                }

                try {
                    var statement = database.prepare (sqlcmd,
                        ":crdate", typeof (int64), time.to_unix (),
                        ":tstamp", typeof (int64), tstamp,
                        ":session_id", typeof (int64), this.id,
                        ":uri", typeof (string), item.uri,
                        ":title", typeof (string), item.name,
                        ":sorting", typeof (double), double.parse (sorting));
                    statement.exec ();
                    int64 tab_id = statement.row_id ();
                    item.set_meta_integer ("tabby-id", tab_id);
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            protected override void uri_changed (Midori.View view, string uri) {
                unowned Katze.Item item = view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                string sqlcmd = "UPDATE `tabs` SET uri = :uri WHERE session_id = :session_id AND id = :tab_id;";
                try {
                    database.prepare (sqlcmd,
                                      ":uri", typeof (string), uri,
                                      ":session_id", typeof (int64), this.id,
                                      ":tab_id", typeof (int64), tab_id).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            protected override void data_changed (Midori.View view) {
                unowned Katze.Item item = view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                string sqlcmd = "UPDATE `tabs` SET title = :title WHERE session_id = :session_id AND id = :tab_id;";
                try {
                    database.prepare (sqlcmd,
                                      ":title", typeof (string), view.get_display_title (),
                                      ":session_id", typeof (int64), this.id,
                                      ":tab_id", typeof (int64), tab_id).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            protected override void tab_added (Midori.Browser browser, Midori.View view) {
                unowned Katze.Item item = view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                if (tab_id < 1) {
                    double sorting = this.get_tab_sorting (view);
                    item.set_meta_string ("sorting", sorting.to_string ());
                    this.add_item (item);
                }
           }

            protected override void tab_removed (Midori.Browser browser, Midori.View view) {
                unowned Katze.Item item = view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                /* FixMe: mark as deleted */
                string sqlcmd = "DELETE FROM `tabs` WHERE session_id = :session_id AND id = :tab_id;";
                try {
                    database.prepare (sqlcmd,
                                      ":session_id", typeof (int64), this.id,
                                      ":tab_id", typeof (int64), tab_id).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            protected override void tab_switched (Midori.View? old_view, Midori.View? new_view) {
                GLib.DateTime time = new DateTime.now_local ();
                unowned Katze.Item item = new_view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                int64 tstamp = time.to_unix();
                item.set_meta_integer ("tabby-tstamp", tstamp);
                string sqlcmd = "UPDATE `tabs` SET tstamp = :tstamp WHERE session_id = :session_id AND id = :tab_id;";
                try {
                    database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id,
                        ":tab_id", typeof (int64), tab_id,
                        ":tstamp", typeof (int64), tstamp).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            protected override void tab_reordered (Gtk.Widget tab, uint pos) {
                Midori.View view = tab as Midori.View;

                double sorting = this.get_tab_sorting (view);
                unowned Katze.Item item = view.get_proxy_item ();
                int64 tab_id = item.get_meta_integer ("tabby-id");
                string sqlcmd = "UPDATE `tabs` SET sorting = :sorting WHERE session_id = :session_id AND id = :tab_id;";
                try {
                    database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id,
                        ":tab_id", typeof (int64), tab_id,
                        ":sorting", typeof (double), sorting).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }

                item.set_meta_string ("sorting", sorting.to_string ());
            }

            public override void remove() {
                string sqlcmd = """
                    DELETE FROM `tabs` WHERE session_id = :session_id;
                    DELETE FROM `sessions` WHERE id = :session_id;
                    """;
                try {
                    database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id). exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            public override void close () {
                /* base.close may unset this.browser, so hold onto it */
                Midori.Browser? my_browser = this.browser;
                base.close ();

                bool should_break = true;
                if (my_browser != null && !my_browser.destroy_with_parent) {
                    foreach (Midori.Browser browser in APP.get_browsers ()) {
                        if (browser != my_browser && !browser.destroy_with_parent) {
                            should_break = false;
                            break;
                        }
                    }

                    if (should_break) {
                        return;
                    }
                }

                GLib.DateTime time = new DateTime.now_local ();
                string sqlcmd = "UPDATE `sessions` SET closed = 1, tstamp = :tstamp WHERE id = :session_id;";
                try {
                    database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id,
                        ":tstamp", typeof (int64), time.to_unix ()).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            public override Katze.Array get_tabs() {
                Katze.Array tabs = new Katze.Array (typeof (Katze.Item));

                string sqlcmd = "SELECT id, uri, title, sorting FROM tabs WHERE session_id = :session_id ORDER BY tstamp DESC";
                try {
                    var statement = database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id);
                    while (statement.step ()) {
                        Katze.Item item = new Katze.Item ();
                        int64 id = statement.get_int64 ("id");
                        string uri = statement.get_string ("uri");
                        string title = statement.get_string ("title");
                        double sorting = statement.get_double ("sorting");
                        item.uri = uri;
                        item.name = title;
                        item.set_meta_integer ("tabby-id", id);
                        item.set_meta_string ("sorting", sorting.to_string ());
                        // See midori_browser_step_history: don't add to history
                        item.set_meta_string ("history-step", "ignore");
                        tabs.add_item (item);
                    }
                } catch (Error error) {
                    critical (_("Failed to select from database: %s"), error.message);
                }
                return tabs;
            }

            public override double get_max_sorting () {
                string sqlcmd = "SELECT MAX(sorting) FROM tabs WHERE session_id = :session_id";
                try {
                    var statement = database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id);
                    statement.step ();
                    double sorting;
                    string? sorting_string = statement.get_int64 ("MAX(sorting)").to_string ();
                    if (sorting_string != null) {
                        /* we have to use a seperate if condition to avoid
                           a `possibly unassigned local variable` error */
                        if (double.try_parse (sorting_string, out sorting)) {
                            return sorting;
                        }
                    }
                } catch (Error error) {
                    critical (_("Failed to select from database: %s"), error.message);
                }

                 return double.parse ("0");
            }

            internal Session (Midori.Database database) {
                this.database = database;

                GLib.DateTime time = new DateTime.now_local ();

                string sqlcmd = "INSERT INTO `sessions` (`tstamp`) VALUES (:tstamp);";

                try {
                    var statement = database.prepare (sqlcmd,
                        ":tstamp", typeof (int64), time.to_unix ());
                    statement.exec ();
                    this.id = statement.row_id ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }

            internal Session.with_id (Midori.Database database, int64 id) {
                this.database = database;
                this.id = id;

                GLib.DateTime time = new DateTime.now_local ();
                string sqlcmd = "UPDATE `sessions` SET closed = 0, tstamp = :tstamp WHERE id = :session_id;";

                try {
                    database.prepare (sqlcmd,
                        ":session_id", typeof (int64), this.id,
                        ":tstamp", typeof (int64), time.to_unix ()).exec ();
                } catch (Error error) {
                    critical (_("Failed to update database: %s"), error.message);
                }
            }
        }

        private class Storage : Base.Storage {
            private Midori.Database database;

            public override Katze.Array get_saved_sessions () {
                Katze.Array sessions = new Katze.Array (typeof (Session));

                string sqlcmd = """
                    SELECT id, closed FROM sessions WHERE closed = 0
                    UNION
                    SELECT * FROM (SELECT id, closed FROM sessions WHERE closed = 1 ORDER BY tstamp DESC LIMIT 1)
                    ORDER BY closed;
                """;
                try {
                    var statement = database.prepare (sqlcmd);
                    while (statement.step ()) {
                        int64 id = statement.get_int64 ("id");
                        int64 closed = statement.get_int64 ("closed");
                        if (closed == 0 || sessions.is_empty ()) {
                            sessions.add_item (new Session.with_id (this.database, id));
                        }
                    }
                } catch (Error error) {
                    critical (_("Failed to select from database: %s"), error.message);
                }

                if (sessions.is_empty ()) {
                    sessions.add_item (new Session (this.database));
                }

                return sessions;
            }

            public override void import_session (Katze.Array tabs) {
                try {
                    database.transaction (()=>{
                        base.import_session(tabs); return true;
                    });
                } catch (Error error) {
                    critical (_("Failed to select from database: %s"), error.message);
                }
            }

            public override Base.Session get_new_session () {
                return new Session (this.database) as Base.Session;
            }

            internal Storage (Midori.App app) {
                GLib.Object (app: app);

                try {
                    database = new Midori.Database ("tabby.db");
                } catch (Midori.DatabaseError schema_error) {
                    error (schema_error.message);
                }

                if (database.first_use) {
                    string config_file = Midori.Paths.get_config_filename_for_reading ("session.xbel");
                    try {
                        Katze.Array old_session = new Katze.Array (typeof (Katze.Item));
                        Midori.array_from_file (old_session, config_file, "xbel-tiny");
                        this.import_session (old_session);
                    } catch (GLib.FileError file_error) {
                        /* no old session.xbel -> could be a new profile -> ignore it */
                    } catch (GLib.Error error) {
                        critical (_("Failed to import legacy session: %s"), error.message);
                    }
                }
            }
        }
    }

    private class Manager : Midori.Extension {
        private Base.Storage storage;
        private bool load_session () {
            /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */
            int load_on_startup;
            APP.settings.get ("load-on-startup", out load_on_startup);
            if (load_on_startup == Midori.MidoriStartup.BLANK_PAGE
             || load_on_startup == Midori.MidoriStartup.HOMEPAGE) {
                this.storage.start_new_session ();
            } else {
                this.storage.restore_last_sessions ();
            }

            /* FIXME: execute_commands should be called before session creation */
            GLib.Idle.add (this.execute_commands);

            return false;
        }

        private bool execute_commands () {
            Midori.App app = this.get_app ();
            unowned string?[] commands = app.get_data ("execute-commands");

            if (commands != null) {
                app.send_command (commands);
            }

            return false;
        }

        private void set_open_uris (Midori.Browser browser) {
            Midori.App app = this.get_app ();
            unowned string?[] uris = app.get_data ("open-uris");

            if (uris != null) {
                Katze.Array tabs = new Katze.Array (typeof (Katze.Item));

                for(int i = 0; uris[i] != null; i++) {
                    Katze.Item item = new Katze.Item ();
                    item.name = uris[i];
                    item.uri = Midori.Sokoke.magic_uri (uris[i], true, true);
                    if (item.uri != null) {
                        tabs.add_item (item);
                    }
                }
                if (!tabs.is_empty()) {
                    browser.set_data ("tabby-open-uris", tabs);
                }
            }

            app.add_browser.disconnect (this.set_open_uris);
        }

        private void browser_added (Midori.Browser browser) {
            Base.Session? session = browser.get_data<Base.Session> ("tabby-session");
            if (session == null) {
                session = this.storage.get_new_session () as Base.Session;
                browser.set_data<Base.Session> ("tabby-session", session);
                session.attach (browser);
            }
        }

        private void browser_removed (Midori.Browser browser) {
            Base.Session? session = browser.get_data<Base.Session> ("tabby-session");
            if (session == null) {
                GLib.warning ("missing session");
            } else {
                session.close ();

                /* Using get here to avoid MidoriMidoriStartup in generated C with Vala 0.20.1 */
                int load_on_startup;
                APP.settings.get ("load-on-startup", out load_on_startup);

                if (browser.destroy_with_parent
                 || load_on_startup < Midori.MidoriStartup.LAST_OPEN_PAGES) {
                    /* Remove js popups and close if not restoring on startup */
                    session.remove ();
                }
            }
        }

        private void activated (Midori.App app) {
            APP = app;
            unowned string? restore_count = GLib.Environment.get_variable ("TABBY_RESTORE_COUNT");
            if (restore_count != null) {
                int count = int.parse (restore_count);
                if (count >= 1) {
                    IDLE_RESTORE_COUNT = count;
                }
            }

            /* FixMe: provide an option to replace Local.Storage with IStorage based Objects */
            this.storage = new Local.Storage (this.get_app ()) as Base.Storage;

            app.add_browser.connect (this.set_open_uris);
            app.add_browser.connect (this.browser_added);
            app.remove_browser.connect (this.browser_removed);

            GLib.Idle.add (this.load_session);
        }

        private void deactivated () {
            /* set_open_uris will disconnect itself if called,
               but it may have been called before we are deactivated */
            APP.add_browser.disconnect (this.set_open_uris);
            APP.add_browser.disconnect (this.browser_added);
            APP.remove_browser.disconnect (this.browser_removed);
            APP = null;

            this.storage = null;
        }

        internal Manager () {
            GLib.Object (name: _("Tabby"),
                         description: _("Tab and session management."),
                         version: "0.1",
                         authors: "André Stösel <andre@stoesel.de>");

            this.activate.connect (this.activated);
            this.deactivate.connect (this.deactivated);
        }
    }
}

public Midori.Extension extension_init () {
    return new Tabby.Manager ();
}
