/**
 * # `ts.js`: TimeStamper Web UI Client-side
 * @author Jonathan Bernard <jdb@jdb-labs.com>
 **/

// TimeStamper namespace
var TS = {};

/* Setup after the document is ready for manipulation. */
$(document).ready(function(){

    // ======== DEFINE MODELS ========//

    /* Entry model.
     * Attributes
     *  - user_id
     *  - timeline_id
     *  - id
     *  - mark
     *  - uuid
     *  - notes
     *  - timestamp
     */
    TS.EntryModel = Backbone.Model.extend({

        initialize: function(attrs, options) {
            _.bindAll(this, 'get', 'set', 'urlRoot'); },

        get: function(attribute) {
            if (attribute == "timestamp") {
                if (!this.timestampDate) {
                    this.timestampDate = new Date(
                        Backbone.Model.prototype.get.call(this, attribute)); }
                return this.timestampDate; }
            else {
                return Backbone.Model.prototype.get.call(this, attribute); } },

        set: function(attributes, options) {
            var attrsToSet = {}
            _.each(attributes, function(val, key) {
                if (key == "timestamp") {
                    if (val instanceof Date) {
                        this.timestampDate = val;
                        attrsToSet.timestamp = dateToJSON(val); }
                    else {
                        this.timestampDate = new Date(val);
                        attrsToSet.timestamp = dateToJSON(this.timestampDate); } }
                else { attrsToSet[key] = val; } });

            return Backbone.Model.prototype.set.call(
                this, attrsToSet, options); },

        urlRoot: function() { return '/ts_api/entries/' + this.get('user_id') +
            '/' + this.get('timeline_id') + '/'; } });

    /* Timeline model.
     * Attributes:
     *  - user_id
     *  - id
     *  - description
     *  - created
     */
    TS.TimelineModel = Backbone.Model.extend({
        urlRoot: function() {
            return '/ts_api/timelines/' + this.get('user_id') + '/'; },

        initialze: function(attrs, options) { _.bindAll(this, 'urlRoot'); } });

    /* User model.
     * Attributes:
     *  - id
     *  - name
     *  - email
     *  - join_date
     */
    TS.UserModel = Backbone.Model.extend({
        url: function() { return '/ts_api/users/' + this.get('id'); },

        initialize: function(attrs, options) { _.bindAll(this, 'url'); } });

    TS.EntryList = Backbone.Collection.extend({
        model: TS.EntryModel,

        comparator: function(entry) { return entry.get('timestamp'); },

        initialize: function(model, options) {
            if (options.timelineModel == undefined) {
                throw "Cannot create an EntryList without a " +
                    "TimelineModel reference." }
            else { this.timelineModel = options.timelineModel; }

            _.bindAll(this, "url", "create"); },

        url: function() {
            return "/ts_api/entries/" + this.timelineModel.get('user_id') + "/"
                + this.timelineModel.get('id'); } });

    TS.TimelineList = Backbone.Collection.extend({
        model: TS.TimelineModel,

        initialize: function(models, options) {
            if (options.user == undefined) {
                throw "Cannot create a TimelineList without a UserModel reference.";
            } else { this.user = options.user; }

            _.bindAll(this, 'url'); },

        comparator: function(timeline) { return timeline.get('id'); },

        url: function() {
            return "/ts_api/timelines/" + this.user.get('id'); } });


    // ======== DEFINE VIEWS ========//

    /* Entry view
     */
    TS.EntryView = Backbone.View.extend({

        model: TS.EntryModel,

        className: 'entry',

        notesCache: false,

        events: {
            "click img.expand-entry"    : "showNotes",
            "click img.collapse-entry"  : "hideNotes",
            "dblclick div.mark"         : "editMark",
            "dblclick div.timestamp"    : "editTimestamp",
            "dblclick div.notes"        : "editNotes",
            "keypress .mark-input"      : "saveOnEnter",
            "keypress .timestamp-input" : "saveOnEnter",
            "keypress .notes-input"     : "saveOnCtrlEnter",
            "blur .mark-input"          : "save",
            "blur .timestamp-input"     : "save",
            "blur .notes-input"         : "save" },

        initialize: function(options) {
            _.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update',
                'saveOnEnter', 'saveOnCtrlEnter', 'getViewModel',
                'renderNotes', 'showNotes', 'hideNotes', 'save');

            this.markdownConverter = options.markdownConverter;

            this.model.bind('change', this.update);
            this.model.view = this;

            this.nextModel = options.nextModel; },

        /**
         * Refresh the display based on the model replacing the existing
         * HTML content. Add new `blur` listeners to the input fields.
         */
        render: function() {
            // render the HTML
            $(this.el).html(ich.entryTemplate(this.getViewModel()));

            // invalidate the notes display cache
            this.notesCache = false;

            return this; },

        /**
         * Refresh the display based on the model using the existing DOM
         * elements.
         */
        update: function() {
            var data = this.getViewModel();
            this.$('.mark span').text(data.mark);
            this.$('.mark-input').val(data.mark);
            this.$('.timestamp').text(data.start);
            this.$('.timestamp-input').val(data.timestamp);
            this.$('.duration').text(data.duration);
            this.$('.notes-text').html(this.renderNotes(data.notes));
            this.$('.notes-input').val(data.notes);
            return this; },

        renderNotes: function(source) {
            if (!this.notesCache) {
                this.notesCache = this.markdownConverter.makeHtml(source); }

            return this.notesCache },

        editMark: function() {
            $(this.el).addClass('edit-mark');
            this.$('.mark-input').focus();
            return this; },

        editTimestamp: function() {
            $(this.el).addClass('edit-timestamp');
            this.$('timestamp-input').focus();
            return this; },

        editNotes: function() {
            // invalidate notes HTML cache
            this.notesCache = false;

            // show notes textarea, hide display
            $(this.el).addClass('edit-notes');

            // focus input
            this.$('.notes-input').focus();
            return this; },

        /**
         * Translate the model data into a form suitable to be displayed.
         * @return a map including display-able `start` and `duration` values.
         */
        getViewModel: function() {
            var data = this.model.toJSON();

            // create start and duration values
            var tsDate = new Date(data.timestamp);
            data.start = this.formatStart(tsDate);
            data.duration = this.formatDuration(this.model, this.nextModel);
            data.notes = data.notes ? data.notes : '*No notes for this entry.*';
            return data; },

        /** Save and close editable fields.  */
        save: function() {
            this.model.save({
                mark: this.$('.mark-input').val(),
                timestamp: new Date(this.$('.timestamp-input').val()),
                notes: this.$('.notes-input').val()});

            this.$('.notes-text').html(this.renderNotes(this.model.get('notes')));
            $(this.el).removeClass('edit-mark edit-timestamp edit-notes');
            this.update(); },

        /** Event handler for keypresses on entry input fields. */
        saveOnEnter: function(e) { if(e.keyCode == 13) { this.save(); } },

        saveOnCtrlEnter: function(e) {
            if (e.keyCode == 13 && e.ctrlKey) { this.save(); } },

        /**
         * Get the display-able start time from the entry timestamp.
         * @param startDate a Date object, the entry timestamp.
         * @return display-able start time in HH:MM format.
         */
        formatStart: function(startDate) {
            /* Code is written for both 24hr and 12hr formats. I still need to
             * create a mechanism for selecting between them. For now, use the
             * 12hr format. */

            // 24 hour
            // return startDate.getHours() + ":" + startDate.getMinutes();

            // 12 hour
            var hour = startDate.getHours() % 12;
            return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() +
                " " + (startDate.getHours() > 11 ? "pm" : "am"); },

        /**
         * Get the duration of the entry based on this entry's timestamp and
         * and the next entry's timestamp in a display-able form. If nextModel
         * is `null` or `undefined` it is assumed that `model` is the most
         * recent model and duration is calculated against the current time.
         *
         * @param model EntryModel representing this entry.
         * @param nextModel EntryModel representing the next entry.
         * @return the duration between model and nextModel, formatted for
         *         display: `Xd Yhr Zm`. */
        formatDuration: function(model, nextModel) {
            var d1 = model.get('timestamp');
            var d2, diff;
            var day, hr, min;

            // if no next model, assume it's an onoing task
            if (nextModel) { d2 = nextModel.get('timestamp'); }
            else { d2 = new Date(); }

            diff= d2.getTime() - d1.getTime();
            day = Math.floor(diff / 86400000); // milliseconds in a day
            diff %= 86400000;

            hr = Math.floor(diff / 3600000); // millis in an hour
            diff %= 3600000;

            min = Math.floor(diff / 60000); // millis in a minute

            return (day > 0 ? day + "d " : "") +
                   (hr  > 0 ? hr + "hr " : "") +
                   min + "m "; },

        showNotes: function() {
            if (!this.notesCache) {
                this.$('.notes-text').html(
                    this.renderNotes(this.model.get('notes'))); }

            this.$('.notes').slideDown();
            $(this.el).addClass('show-notes'); },

        hideNotes: function() {
            this.$('.notes').slideUp();
            $(this.el).removeClass('show-notes'); } });

    TS.EntryListView = Backbone.View.extend({

        el: $("#entry-list")[0],

        events: { "keypress #new-entry-input"    : "createNewEntryOnEnter" },

        initialize: function() {
            _.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne');
            this.collection.bind('add', this.addOne);
            this.collection.bind('refresh', this.render);
            this.collection.view = this;
            this.entryContainer = this.$("#entries")
            this.markdownConverter = new Showdown.converter(); },

        addOne: function(entry) {
            var lastEntry = this.collection.at(this.collection.length - 2);
            lastEntry.view.nextModel = entry;
            lastEntry.view.update();
            this.renderOne(entry, null);
			$(".entry.current").removeClass("current");
			$(entry.view.el).addClass("current"); },

        renderOne: function(entry, nextEntry) {
            // exclude if any exclusion RegExps match
            var excluded = _.any(this.entryExclusions,
                function(exclusion) { return exclusion.test(entry.get("mark"))});

            // create the view if it does not exist
            if (!entry.view) { new TS.EntryView(
                    {model: entry, markdownConverter: this.markdownConverter}); }
            entry.view.nextModel = nextEntry

            // render the element
            var el = entry.view.render().el;

            // add it to the container after the topmost separator ("Today")
            this.topSeparator.after(el);

            // If this entry and the next entry are not on the same day, put a
            // day separator between them.
            var nextDay = nextEntry ? nextEntry.get("timestamp") : new Date();
            if (entry.get("timestamp").getDate() != nextDay.getDate()) {
                this.topSeparator.after(ich.daySeparatorTemplate(
                    {separatorLabel: this.formatDaySeparator(
                        TS.app.currentDay, entry.get("timestamp")) })); }

            // hide it if excluded
            if (excluded) {
                $(el).fadeOut('slow');
                $(el).addClass('excluded'); } },

        createNewEntryOnEnter: function(e) {

            if (e.keyCode == 13) {

                // grab the mark data
                var entryMark = this.$("#new-entry-input").val();

                // create the mark. Immediately fetch to get server-side timestamp
                this.collection.create(
                    {mark: entryMark,
                     notes: '',
                     timestamp: new Date()},
                    {wait: true,
                     success: function(model, resp, options) {
                        model.set(resp); }});

                // clear the input for the next entry
                this.$("#new-entry-input").val(""); } },

        render: function() {

            // get our user exclusions
            var userExclusions = TS.app.user.model.get("entry_exclusions") || [];

            // get the current timeline exclusions
            var timelineExclusions =
                this.collection.timelineModel.get("entry_exclusions") || [];

            // turn them into RegExps and store them
            this.entryExclusions = _.map(
                userExclusions.concat(timelineExclusions),
                function(exclusion) { return new RegExp(exclusion)} );

            // clear existing elements in the view container
            this.entryContainer.empty();

            // add the top-most day separator; should always be "Today"
            this.topSeparator = ich.daySeparatorTemplate({
                separatorLabel: this.formatDaySeparator(
                    TS.app.currentDay, new Date()) });
            this.entryContainer.prepend(this.topSeparator);

            // iterate through the collection and render the elements.
            for (var i = 0, len = this.collection.length; i < len; i++) {
                var entry = this.collection.at(i);
                var nextEntry = (i + 1 < len ? this.collection.at(i + 1) : null);

                this.renderOne(entry, nextEntry); }
                
            $("#new-entry-input").focus() },

        formatDaySeparator: function(today, labelDay) {

            var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
                        'Friday', 'Saturday'];
            var months = ['January', 'February', 'March', 'April', 'May',
                          'June', 'July', 'August', 'September', 'October',
                          'November', 'December'];

            var yearDiff = today.getFullYear() - labelDay.getFullYear();
            var monthDiff = today.getMonth() - labelDay.getMonth();
            var dayDiff = today.getDate() - labelDay.getDate();

            // more than a calendar year old: Weekday, Month Date, Year
            if (yearDiff > 0) {

                return days[labelDay.getDay()] + ", " +
                    months[labelDay.getMonth()] + " " + labelDay.getDate() +
                    ", " + labelDay.getFullYear(); }

            // same calendar year, more than a week ago: Weekday, Month Date
            else if (monthDiff > 0 || dayDiff > 7) {

                return days[labelDay.getDay()] + ", " +
                    months[labelDay.getMonth()] + " " + labelDay.getDate(); }

            // less than a week ago, more than yesterday, Last Weekday
            else if (dayDiff > 1) {
                return "Last " + days[labelDay.getDay()]; }

            // Yesterday
            else if (dayDiff == 1) { return "Yesterday"; }

            // Today
            else if (dayDiff == 0) { return "Today"; } } });

    TS.TimelineListView = Backbone.View.extend({
        el: $("#timeline")[0],

        collection: TS.TimelineList,

        events: {
            "dblclick .timeline-id"         : "editId",
            "dblclick .timeline-desc"       : "editDesc",
            "keypress .timeline-id-input"   : "saveOnEnter",
            "keypress .timeline-desc-input" : "saveOnEnter",
            "click .new-timeline-link"      : "showNewTimelineDialog" },

        initialize: function(options) {
            _.bindAll(this, 'render', 'renderOne', 'editId',
                'editDesc', 'saveOnEnter', 'save');

            if (options.initialTimelineId == undefined) {
                throw "Can not create a TimelineListView without an " +
                    "initial timeline." }
            else {
                this.selectedModel =
                    this.collection.get(options.initialTimelineId); }

            this.collection.bind('add', this.renderOne);
            this.collection.bind('refresh', this.render); },

        renderOne: function(timeline) {
            this.$('.drop-menu-items').append(
                ich.timelineLinkTemplate(timeline.toJSON())); },

        render: function() {
            // render the basic template
            $(this.el).html(ich.timelineTemplate(this.selectedModel.toJSON()));

            // render the selection list
            _.each(this.collection.without([this.selectedModel]), this.renderOne); },

        editId: function() {
            $(this.el).addClass('edit-id');
            this.$('.timeline-id-input').focus();
            return this; },

        editDesc: function() {
            $(this.el).addClass('edit-desc');
            this.$('.timeline-desc-input').focus();
            return this; },

        save: function() {
            this.selectedModel.save({
                id: this.$('.timeline-id-input').val(),
                description: this.$('.timeline-desc-input').val()});
            $(this.el).removeClass('edit-id edit-desc');
            this.render(); },

        showNewTimelineDialog: function() { TS.app.newTimelineDialog.show(); },

        saveOnEnter: function(e) { if (e.keyCode == 13) { this.save(); } } });

    TS.UserView = Backbone.View.extend({

        el: $("#user")[0],

        model: TS.UserModel,

        events: {
            'dblclick .fullname':       'editFullname',
            'keypress .fullname-input': 'saveOnEnter' },

        initialize: function() {
            _.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter');
            this.model.bind('change', this.render);
            this.model.view = this; },

        render: function() {
            $(this.el).html(ich.userTemplate(this.model.toJSON()));
            return this; },

        editFullname: function() {
            $(this.el).addClass('edit-fullname');
            this.$('.fullname-input').focus();
            return this; },

        save: function() {
            this.model.set({name: this.$('fullname-input').val()});
            this.model.save();
            $(this.el).removeClass('edit-fullname'); },

        saveOnEnter: function(e) { if (e.keyCode == 13) this.save(); } });

    TS.AppView = Backbone.View.extend({

        el: $("body")[0],

        events: { 'click #timeline .drop-menu-items a': 'selectTimeline' },

        initialize: function() {

            _.bindAll(this, 'initializeViews', 'loadInitialData',
                'periodicRefresh', 'selectTimeline');

            TS.app = this;
            TS.app.currentDay = new Date();

            // create the login dialog
            this.loginDialog = new TS.LoginView

            // initialize data, either from boostrapped data, or via user login
            if (window.bootstrap) { this.initializeData(window.bootstrap()) }
            else {
                // this is async (waiting for user input)
                this.loginDialog.authenticate(function() {
                    TS.app.initializeData(TS.app.loadInitialData())}); }

            // Schedule our update function.
            setInterval(this.periodicRefresh, 60000); },

        initializeData: function(data) {

            // create user data
            this.user = {};
            this.user.model = new TS.UserModel(data.user);
            this.user.view = new TS.UserView({model: this.user.model});

            // create timeline models from the bootstrapped data
            var tlModels = _.map(data.timelines, function(timeline) {
                return new TS.TimelineModel(timeline); });

            // create the timeline list collection
            this.timelines = {};
            this.timelines.collection = new TS.TimelineList(
                tlModels, {user: this.user.model});
            this.timelines.view = new TS.TimelineListView(
                {collection: this.timelines.collection,
                 initialTimelineId: data.initialTimelineId});

            // create the new timeline dialog
            this.newTimelineDialog = new TS.NewTimelineView(
                {timelineCollection: this.timelines.collection});

            // create entry models from the bootstrapped data
            var entryModels = _.map(data.entries, function(entry) {
                return new TS.EntryModel(entry); });

            // create the entry collection
            this.entries = {};
            this.entries.collection = new TS.EntryList(entryModels,
                {timelineModel: this.timelines.view.selectedModel});
            this.entries.view = new TS.EntryListView(
                {collection: this.entries.collection});

            // render views
            this.user.view.render();
            this.timelines.view.render();
            this.entries.view.render();

            // create our WebSocket for receiving push notifications of changed
            // content.
            if (false) { // (Modernizr.websockets)
                TS.updateConnection = new WebSocket('wss://' +
                    window.location.hostname +
                    (window.location.port ? ":" + window.location.port : "") +
                    '/ts_api/update_service?user_id=' +
                    this.user.model.get("id"));
                TS.updateConnection.onopen = this.syncConnected;
                TS.updateConnection.onmessage = this.syncUpdate;
                TS.updateConnection.onclose = this.syncDisconnected; } },

        loadInitialData: function() {
            // assume we are authenticated

            var username = $("#username-input").val(); // hackish
            var data = jQuery.parseJSON($.ajax({
                url: '/ts_api/app/user_summary/' + username,
                async: false}).responseText);

            // look for the last used timeline, default to first timeline
            data.initialTimelineId =
                data.user.last_timeline || data.timelines[0].id

            data.entries = jQuery.parseJSON($.ajax({
                url: '/ts_api/entries/' + username + '/' +
                    data.initialTimelineId,
                async: false}).responseText);

            return data; },

        periodicRefresh: function() {

            var now = new Date();
            var models = this.entries.collection.models;

            // First we check if it is a new day. If it is, we need to
            // re-render the whole list.
            if (this.currentDay.getDate() != now.getDate()) {
                // If the user is editing any one of the models, don't rerender
                if (_.any(models, function(model) {
                    return ($(model.view.el).hasClass('edit-mark') ||
                            $(model.view.el).hasClass('edit-timestamp') ||
                            $(model.view.el).hasClass('edit-notes')) })) {
                    return }

                // It's a new day! Rerender our whole list.
                else { this.entries.render(); } }

            // Otherwise we only need to re-render the most recent item.
            else {
                var currentModel = models[models.length - 1]

                if ($(currentModel.view.el).hasClass('edit-mark') ||
                    $(currentModel.view.el).hasClass('edit-timestamp') ||
                    $(currentModel.view.el).hasClass('edit-notes')) { return }

                // Refresh our latest entry view so the duration is up to date.
                else { currentModel.view.update(); } } },

        selectTimeline: function(e) {
            if (e) {
                // get the timeline model
                var tl = this.timelines.collection.get(e.srcElement.text);

                // set the on the timeline view
                this.timelines.view.selectedModel = tl;

                // set the timeline on the EntryList
                this.entries.collection.timelineModel = tl;

                // update the last_timeline field of the user model
                this.user.model.set({last_timeline: tl.get('id')});
                this.user.model.save();

                // re-render the TimelineListView
                this.timelines.view.render();

                // fetch the new EntryList records
                this.entries.collection.fetch() } },

        syncConnected: function() { },

        syncDisconnected: function(err) {
            // TODO: reconnect. Possibly display a button for the user to
            // manually reconnect if we've already tried and failed too many
            // times.
        },

        syncUpdate: function(message) {
            if (!message.data) return;

            var data = JSON.parse(message.data);

            if (data.record_type == "ts_entry") {
                // Check to see if the record is for the same timeline we are
                // currently using.

                //
            }

            if (data.record_type == "ts_timeline") {

            }
        } });

    TS.LoginView = Backbone.View.extend({
        el: $("#login")[0],

        events: {
            "keypress #password-input"  : "loginOnEnter",
            "click #login-button a"     : "doLogin" },

        initialize: function() {
            _.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter',
                            'show'); },

        action: function() {},

        authenticate: function(nextAction) {
            this.action = nextAction;
            this.show(); },

        doLogin: function(){
            var viewThis = this;
            var name = this.$("#username-input");
            var pwd = $("#password-input");

            // call the API via AJAX
            $.ajax({
                url: "/ts_api/login",
                processData: false,
                data: JSON.stringify({username: name.val(), password: pwd.val()}),
                type: "POST",
                async: false,

                error: function(jqXHR, textStatus, error) {
                    // assuming bad credentials (possible server error or bad request),
                    // we should check that, FIXME
                    var tips = $(".validate-tips");
                    tips.text("Incorrect username/password combination.");
                    tips.slideDown(); },

                success: function(data, textStatus, jqXHR) {
                    viewThis.hide();
                    viewThis.action(); } }); },

        hide: function() { $(this.el).addClass('hidden'); },

        show: function() {
            $(this.el).removeClass('hidden');
            this.$("#username-input").focus(); },

        loginOnEnter: function(e) {
            if (e.keyCode == 13) { this.doLogin(); } } });

    TS.NewTimelineView = Backbone.View.extend({
        el: $("#new-timeline")[0],

        events: {
            "click #new-timeline-create a"  : "createTimeline",
            "click #new-timeline-cancel a"  : "hide" },

        initialize: function(options) {
            _.bindAll(this, 'createTimeline', 'hide', 'show');

            if (options.timelineCollection == undefined) {
                throw "Can not create the NewTimelineView without the " +
                    "timeline collection." }
            else { this.timelineCollection = options.timelineCollection; } },

        createTimeline: function() {
            var timelineId = this.$("#new-timeline-id").val();
            var timelineDesc = this.$("#new-timeline-desc").val();
            this.timelineCollection.create(
                {id: timelineId, description: timelineDesc,
                 created: dateToJSON(new Date())});
            this.hide(); },

        hide: function() { $(this.el).addClass('hidden'); },

        show: function() {
            this.$("#new-timeline-id").val("");
            this.$("#new-timeline-desc").val("");
            $(this.el).removeClass('hidden');
            this.$("#new-timeline-id").focus(); } });

    new TS.AppView();

});

function dateToJSON(d) {
    function pad(n){return n<10 ? '0'+n : n}

    return d.getUTCFullYear()+'-'
        + pad(d.getUTCMonth()+1)+'-'
        + pad(d.getUTCDate())+'T'
        + pad(d.getUTCHours())+':'
        + pad(d.getUTCMinutes())+':'
        + pad(d.getUTCSeconds())+'Z';
}
