(function() {
    var root = this;

    var PD = root.PersonalDisplay = {};

    PD.hasHTML5LocalStorage = function() {
          try {
              return 'localStorage' in window && window['localStorage'] !== null; }
          catch (e) { return false; } }

    /// ## Models

    PD.TimelineMarkModel = Backbone.Model.extend({
        initialize: function() { _.bindAll(this, "equals"); },
        equals: function(that) { return this.id == that.id; }
    });

    PD.GTDEntryModel = Backbone.Model.extend({
        initialize: function() { _.bindAll(this, "equals"); },
        equals: function(that) { return this.id == that.id; }
    });

    // ## Views

    PD.CurrentActivityView = Backbone.View.extend({
        el: $("#current-task"),

        initialize: function() {
            _.bindAll(this, "render");

            this.model.on('change', this.render, this); },

         render: function() {
            this.$el.find(".task").text(this.model.get("mark"));
            this.$el.find(".task-notes").text(this.model.get("notes"));
            return this;
         }
    });

    PD.GTDNextActionView = Backbone.View.extend({
        className: "next-action",
        tagName: "div",
        ignoredProperties: ["action", "details", "id"],

        initialize: function() { 
            _.bindAll(this, "render");
            $("#priorities").append(this.el);
            
            this.model.on('change', this.render, this); },

        render: function() {
            var elements = [];

            // Look for the "action" property first
            var actionEl = $(document.createElement("span"))
                .addClass("action")
            actionEl.text(this.model.get("action").toString());
            elements.push(actionEl);

            // Add span elements for each of the other attributes.
            _.each(this.model.attributes, function(val, key) {
                if (!_.contains(this.ignoredProperties, key)) {
                    var el = $(document.createElement("span"))
                        .addClass(key.toString());
                    el.text(val);
                    elements.push(el); } }, this);

            // Finally, look for the "details" property
            if (this.model.get("details")) {
                var detailEl = $(document.createElement("span"))
                    .addClass("details");
                detailEl.text(this.model.get("details").toString());
                elements.push(detailEl); }

            // Clear the old data and add our new elements in order
            this.$el.empty();
            _.each(elements, function(el) { this.$el.append(el); }, this);

            return this;
        }
    });

    PD.GTDNextActionCollection = Backbone.Collection.extend({
        model: PD.GTDEntryModel,

        initialize: function() {
            _.bindAll(this, "addNextActionView", "removeNextActionView");
            this.views = {};
            this.on('add', this.addNextActionView);
            this.on('remove', this.removeNextActionView); },

        addNextActionView: function(entryModel) {
            var view = new PD.GTDNextActionView({model: entryModel});
            view.render();
            this.views[entryModel.get("id")] = view; },

        removeNextActionView: function(entryModel) {
            var view = this.views[entryModel.get("id")];
            view.$el.remove(); }
    });

    PD.ConfigDialog = Backbone.View.extend({
        el: $("#config-dialog"),

        events: {
            "blur .timestamper-config .password"    : "tsLogin",
            "blur .gtd-config .password"            : "gtdLogin",

            "change .gtd-config .category"          : "addCategory",
            "click .remove-button"                  : "removeCategory",
            
            "click .save-button"                    : "saveAndClose" },

        initialize: function() {
            _.bindAll(this, "show", "hide", "tsLogin", "gtdLogin",
                "loadTsData", "loadGtdData", "addCategory", "makeCategoryItem",
                "removeCategory", "saveAndClose"); },

        show: function() {
            var $tsSection = this.$el.find(".timestamper-config");
            var $gtdSection = this.$el.find(".gtd-config");

            // Load TimeStamper configuration values.
            if (PD.tsCfg) {
                $tsSection.find(".username").val(PD.tsCfg.username);
                $tsSection.find(".password").val(PD.tsCfg.password);
                $tsSection.find(".host").val(PD.tsCfg.host);

                if (PD.tsAuth) {
                    this.loadTsData(PD.tsCfg.host, PD.tsCfg.username); }

                this.$el.find('.timeline').val(PD.tsCfg.timelineId); }

            // Or suggest a default server.
            else { $tsSection.find(".host").val("timestamper.jdb-labs.com"); }

            // Load GTD configuration values.
            if (PD.gtdCfg) {
                $gtdSection.find(".username").val(PD.gtdCfg.username);
                $gtdSection.find(".password").val(PD.gtdCfg.password);
                $gtdSection.find(".host").val(PD.gtdCfg.host);

                if (PD.gtdAuth) { this.loadGtdData(PD.gtdCfg.host); }

                // Create the items for the selected categories
                $(".category-name").parent().remove();
                _.forEach(PD.gtdCfg.categories, this.makeCategoryItem); }

            this.$el.find('.refresh-period').val(
                PD.refreshPeriod ? PD.refreshPeriod / 1000 : 15);

            this.$el.fadeIn(); },

        hide: function() { this.$el.fadeOut(); },

        tsLogin: function() {
            var username = this.$el.find(".timestamper-config .username").val();
            var password = this.$el.find(".timestamper-config .password").val();
            var host = this.$el.find(".timestamper-config .host").val();

            if (!PD.tsCfg) { PD.tsCfg = {}; }

            // Hide the configuration dialog.
            this.$el.find(".wait-overlay span").text("Connecting to " + host);
            this.$el.find(".wait-overlay").fadeIn();

            // Try to log in to the TimeStamper service.
            $.ajax({
                url: "https://" + host + "/ts_api/login",
                xhrFields: { withCredentials: true },
                processData: false,
                type: 'POST',
                async: false,

                data: JSON.stringify(
                    {"username": username, "password": password}),

                error: function(jqXHR, textStatus, error) {
                    if (jqXHR.status == 401) { $(".validate-tips")
                        .text("Invalid username/password combination for " +
                            "the TimeStamper service."); }
                    else { $(".validate-tips").text("There was an error " +
                        "trying to log into the TimeStamper service: " +
                        error); }
                    PD.tsAuth = false; }, 

                success: function(data, textStatus, jqXHR) {
                    PD.tsAuth = true;

                    $(".validate-tips").text("");

                    // Load the user's timelines.
                    PD.configDialog.loadTsData(host, username);
                }
            });

            // Success or failure we hide the wait overlay.
            this.$el.find(".wait-overlay").fadeOut();

        }, 

        gtdLogin: function() {
            var username = this.$el.find(".gtd-config .username").val();
            var password = this.$el.find(".gtd-config .password").val();
            var host = this.$el.find(".gtd-config .host").val();

            if (!PD.gtdCfg) { PD.gtdCfg = {}; }

            // Hide the configuration dialog.
            this.$el.find(".wait-overlay span").text("Connecting to " + host);
            this.$el.find(".wait-overlay").fadeIn();

            // Try to log in to the GTD service.
            $.ajax({
                url: "http://" + host + "/gtd/login",
                xhrFields: { withCredentials: true },
                processData: false,
                type: 'POST',
                async: false,

                data: JSON.stringify(
                    {"username": username, "password": password}),

                error: function(jqXHR, textStatus, error) {
                    if (jqXHR.status == 401) { $(".validate-tips")
                        .text("Invalid username/password combination for " +
                            "the Getting Things Done service."); }
                    else { $(".validate-tips").text("There was an error " +
                        "trying to log into the Getting Things Done service: " +
                        error); }
                    PD.gtdAuth = false; },

                success: function(data, textStatus, jqXHR) {
                    PD.gtdAuth = true;

                    $(".validate-tips").text("");

                    PD.configDialog.loadGtdData(host); }
            });

            this.$el.find(".wait-overlay").fadeOut();
        },

        loadTsData: function(host, username) {
            // (Re)load the user's timelines.
            PD.tsCfg.timelines = JSON.parse($.ajax({
                url: 'https://' + host + '/ts_api/timelines/' + username,
                xhrFields: { withCredentials: true },
                async: false}).responseText);

            // Populate the available timelines list.
            var $timelineSelectEl = this.$el.find(".timestamper-config .timeline");
            $timelineSelectEl.empty();
            _.forEach(PD.tsCfg.timelines, function(timeline) {
                var $optionEl = $(document.createElement("option"));
                $optionEl.attr("value", timeline.id);
                $optionEl.text(timeline.description);
                $timelineSelectEl.append($optionEl); }); },

        loadGtdData: function(host) {
            // Load the user's contexts
            PD.gtdCfg.contexts = JSON.parse($.ajax({
                url: 'http://' + host + '/gtd/contexts',
                xhrFields: { withCredentials: true },
                async: false }).responseText);

            // Load the user's projects
            PD.gtdCfg.projects = JSON.parse($.ajax({
                url: 'http://' + host + '/gtd/projects',
                xhrFields: { withCredentials: true },
                async: false }).responseText);

            // Populate the available contexts and projects drop-down.
            var $categorySelectEl = $(".gtd-config .category")
            $categorySelectEl.empty();
            $categorySelectEl.append(
                "<option class=default-option value='none'>" +
                    "Add a category...</option>");
            _.forEach(PD.gtdCfg.contexts.concat(PD.gtdCfg.projects),
              function(category) {
                var $optionEl = $(document.createElement("option"));
                $optionEl.attr("value", category.id);
                $optionEl.text(category.id);
                $categorySelectEl.append($optionEl); });
            $categorySelectEl[0].selectedIndex = 0; },

        makeCategoryItem: function(catName) {
            var $liEl = $(
                "<li class><span class=remove-button>remove</span>" +
                "<span class=category-name></span></li>");
            $liEl.find('.category-name').text(catName);
            this.$el.find(".gtd-config ul").append($liEl); },

        addCategory: function(source) {
            var selectEl = source.target;
            var $selectEl = $(selectEl);
            if (selectEl.selectedIndex == 0) { return; }
            this.makeCategoryItem($selectEl.val());
            selectEl.selectedIndex = 0; },
        
        removeCategory: function(source) {
            $(source.target).parent().remove(); },
        
        saveAndClose: function() {
            if (!PD.tsCfg) { PD.tsCfg = {}; }
            if (!PD.gtdCfg) { PD.gtdCfg = {}; }

            // Save TimeStamper configuration.
            var $tsEl = this.$el.find(".timestamper-config");
            PD.tsCfg.host = $tsEl.find(".host").val();
            PD.tsCfg.username = $tsEl.find(".username").val();
            PD.tsCfg.password = $tsEl.find(".password").val();
            PD.tsCfg.timelineId = $tsEl.find(".timeline").val();

            // Save Getting Things Done configuration.
            var $gtdEl = this.$el.find(".gtd-config");
            PD.gtdCfg.host = $gtdEl.find(".host").val();
            PD.gtdCfg.username = $gtdEl.find(".username").val();
            PD.gtdCfg.password = $gtdEl.find(".password").val();
            PD.gtdCfg.categories = _.map(
                this.$el.find(".category-name"),
                function(span) { return $(span).text(); });

            // Save global data
            PD.refreshPeriod = parseInt(this.$el.find(".refresh-period").val()) * 1000;

            if (PD.hasHTML5LocalStorage()) {
                localStorage.setItem("tsCfg", JSON.stringify(PD.tsCfg));
                localStorage.setItem("gtdCfg", JSON.stringify(PD.gtdCfg));
                localStorage.setItem("refreshPeriod",
                    JSON.stringify(PD.refreshPeriod)); }

            this.hide();
        }
    });

    PD.Main = Backbone.View.extend({
        el: $("body"),

        events: {
            "click a.refresh"           : "refresh",
            "click a.pause-continue"    : "toggleSync" },

        initialize: function() {

            _.bindAll(this, "refresh", "toggleSync");

            // Create our config dialog view.
            PD.configDialog = new PD.ConfigDialog();

            // Create our initial models and views.
            PD.currentActivityModel = new PD.TimelineMarkModel({});
            PD.currentActivityView = new PD.CurrentActivityView(
                {model: PD.currentActivityModel})
            
            // Test for localStorage support
            if (!PD.hasHTML5LocalStorage()) {
                alert("Your browser does not support HTML5 localStorage." +
                    "Without this I cannot store your preferences.");
                PD.configDialog.show(); }
            else {
                PD.tsCfg = JSON.parse(localStorage.getItem('tsCfg'));
                PD.gtdCfg = JSON.parse(localStorage.getItem('gtdCfg'));
                PD.refreshPeriod = JSON.parse(
                    localStorage.getItem('refreshPeriod')); }

            PD.gtdNextActionCollection = new PD.GTDNextActionCollection();

            // Perform the initial refresh.
            this.refresh();
            
            // Schedule future refreshes.
            PD.refreshIntervalId = setInterval(this.refresh, PD.refreshPeriod ? PD.refreshPeriod : 15000);
        },

        refresh: function(evt) {
            // If the dialog is still open we skip this sync to give the user
            // a chance to finish configuration.
            if ($("#config-dialog").is(":visible")) { return; }

            // Otherwise, if we do not have configuration information, open the
            // dialog so the user can enter it.
            if (!(PD.tsCfg && PD.gtdCfg)) { PD.configDialog.show(); return; }

            // Check that we are authenticated to the services we need. Try to
            // authenticate if we are not.
            if (!PD.tsAuth) {
                $.ajax({
                    url: "https://" + PD.tsCfg.host + "/ts_api/login",
                    xhrFields: { withCredentials: true },
                    processData: false,
                    type: "POST",
                    async: false,

                    data: JSON.stringify(
                      { "username": PD.tsCfg.username,
                        "password": PD.tsCfg.password }),

                    error: function(jqXHR, textStatus, error) {
                        // TODO: Handle error.
                        PD.tsAuth=false;
                        alert("Unable to authenticate to the TimeStamper " +
                            "service: " + error);
                        PD.configDialog.show(); },

                    success: function(data, textStatus, jqXHR) {
                        PD.tsAuth = true; }}); }

            if (!PD.gtdAuth) {
                $.ajax({
                    url: "http://" + PD.gtdCfg.host + "/gtd/login",
                    xhrFields: { withCredentials: true },
                    processData: false,
                    type: "POST",
                    async: false,

                    data: JSON.stringify(
                      { "username": PD.gtdCfg.username,
                        "password": PD.gtdCfg.password }),

                    error: function(jqXHR, textStatus, error) {
                        // TODO: Handle error.
                        PD.gtdAuth=false;
                        alert("Unable to authenticate to the GTD service: " +
                            error);
                        PD.configDialog.show(); },

                    success: function(data, textStatus, jqXHR) {
                        PD.gtdAuth = true; }}); }


            // Check that we have successfully authenticated to both services.
            // If we are not, we will skip this refresh.
            if (!(PD.tsAuth && PD.gtdAuth)) { return; }

            // Get the latest timestamp from the TimeStamper service.
            $.ajax({
                url: "https://" + PD.tsCfg.host + "/ts_api/entries/" +
                    PD.tsCfg.username + "/" + PD.tsCfg.timelineId,
                xhrFields: { withCredentials: true },
                data: {"order": "asc" },
                dataType: 'json',
                type: 'GET',
                async: true,

                error: function(jqXHR, textStatus, errorText) {
                    if (jqXHR.status == 401) { PD.tsAuth = false; }
                    else {
                        alert("Unable to retrieve current timestamp: " + errorText);
                        PD.configDialog.show(); }
                },

                success: function(data, textStatus, jqXHR) {
                    PD.currentActivityModel.set(data[0]); } 
            });

            // Get the list of GTD entries for each of our categories.
            var categories = _.reduce(
                PD.gtdCfg.categories,
                function(acc, cat) { return acc ? acc + "," + cat : cat; }, "");

            $.ajax({
                url: "http://" + PD.gtdCfg.host + "/gtd/next-actions/" +
                    categories,
                xhrFields: { withCredentials: true },
                dataType: 'json',
                type: 'GET',
                async: true,

                error: function(jqXHR, textStatus, errorText) {
                    if (jqXHR.status == 401) { PD.gtdAtuh = false; }
                    else if (jqXHR.status == 500) { return; }
                    else {
                        alert("Unable to retrieve next actions: " + errorText);
                        PD.configDialog.show(); }
                },

                success: function(data, textStatus, jqXHR) {
                    var collection = PD.gtdNextActionCollection;

                    // Add all the retrieved items to the collection.
                    _.forEach(data, function(actionAttr) {

                        // Try to find this entry in out collection.
                        var model = collection.get(actionAttr.id);
                        // Update it if found
                        if (model) { model.set(actionAttr); }
                        // Insert a new model if not found.
                        else { collection.add(
                            new PD.GTDEntryModel(actionAttr)); }});

                    // Look through our collection for entries that are no
                    // longer in our retrieved data and remove them.
                    collection.forEach(function(model) {
                        if (!_.any(data, model.equals)) {
                            collection.remove(model); }});
                }
            });

            if (evt) evt.preventDefault();
        },

        toggleSync: function(evt) {
            if (PD.refreshIntervalId == null) {
                PD.refreshIntervalId = setInterval(this.refresh,
                    PD.refreshPeriod ? PD.refreshPeriod : 15000);
                $('.pause-continue').text('Pause Monitoring'); }
            else {
                clearInterval(PD.refreshIntervalId);
                PD.refreshIntervalId = null;
                $('.pause-continue').text('Resume Monitoring'); }
                

            if (evt) evt.preventDefault();
        }
    });

    PD.main = new PD.Main();
}).call(this);