/* hobo-jQuery initialization & utility functions */

(function($) {
    var page_data = {};

    //used for javascript testing.
    var num_updates = 0;

    var methods = {

        init : function() {
            var top = this;
            this.find("[data-rapid-page-data]").each(function() {
                page_data = $(this).data('rapid-page-data');
            });
            this.find("[data-rapid]").each(function() {
                var that = jQuery(this);
                jQuery.each(jQuery(this).data('rapid'), function(tag, annotations) {
                    tag = "hjq_"+tag.replace(/-/g, '_');
                    if(that[tag]) {
                        that[tag](annotations);
                    }
                });
            });
            return top;
        },

        /* return the ID from the typed-id in the data-rapid-context attribute */
        contextId: function() {
            return this.data('rapid-context').split(":")[1];
        },

        /* given annotations, turns the values in the _events_ object into functions, merges them into _options_ and returns _options_ */
        getOptions: function(annotations) {
            for(var key in annotations.events) {
                if(annotations.events.hasOwnProperty(key)) {
                    annotations.options[key] = methods.createFunction.call(this, annotations.events[key]);
                }
            }
            return annotations.options;
        },

        /* return the global page_data:  hobo_parts, form_auth_token, etc. */
        pageData: function() { return page_data; },

        /* return the number of current updates.   Useful for
           javascript/integration testing */
        numUpdates: function() { return num_updates; },

        /* this function is called on an update of a part via Ajax. */
        update: function(innerHtml) {
            num_updates += 1;
            var that=this;
            var replacement=this.clone().html(innerHtml).hide();
            var hide_o, show_o;
            if(this.data('hjq-ajax')) {
                hide_o = this.data('hjq-ajax')['hide'];
                show_o = this.data('hjq-ajax')['show'];
            }
            methods.hideAndRemove.call(this, hide_o,  function () {
                that.before(replacement);
                methods.show.call(replacement, show_o, function() {num_updates -= 1;});
            });
            methods.init.call(replacement);
            return replacement;
        },

        /* this function is called on ajax update to update part
        context information */
        updatePartContexts: function(contexts) {
            $.extend(page_data.hobo_parts, contexts);
        },

        /* hide and removes the element.  options is an array or
         * comma-separated string corresponding to the jQuery-UI hide
         * arguments: effect, options, speed & callback.  The callback
         * argument is an additional callback called after the one in
         * the options hash.  Removal is done after both callbacks. */
        hideAndRemove: function(o, callback) {
            methods.hide.call(this, o, callback, true);
        },

        /* hides the element.  options is an array or
         * comma-separated string corresponding to the jQuery-UI hide
         * arguments: effect, options, speed & callback.  The callback
         * argument is an additional callback called after the one in
         * the options hash.  Removal is done after both callbacks. */
        hide: function(o, callback, andRemove) {
            var that=this;
            var args = o;
            if(args===undefined) args=page_data.hide;
            if(args===undefined) args=[];
            if (typeof args=='string') args=args.split(',');
            else if($.isArray(args)) args=args.slice(0); //shallow clone
            else args=[];
            o_cb = args[3];
            args[3] = function() {
                if(o_cb) methods.createFunction.call(that, o_cb).apply(this, arguments);
                if(callback) callback.apply(this, arguments);
                if(andRemove) that.remove();
            };
            if(args[0]) {
                that.hide.apply(that, args);
            } else {
                that.hide();
                args[3].call(that);
            }
            return this;
        },
        /* show the element.  options is an array or
         * comma-separated string corresponding to the jQuery-UI show
         * arguments: effect, options, speed & callback.  The callback
         * argument is an additional callback called after the one in
         * the options hash.  */
        show: function(o, callback) {
            var that=this;
            var args = o;
            if(args===undefined) args=page_data.show;
            if(args===undefined) args=[];
            if (typeof args=='string') args=args.split(',');
            else if($.isArray(args)) args=args.slice(0); //shallow clone
            else args=[];
            o_cb = args[3];
            args[3] = function() {
                if(o_cb) methods.createFunction.call(that, o_cb).apply(this, arguments);
                if(callback) callback.apply(this, arguments);
            };
            if(args[0]) {
                that.show.apply(that, args);
            } else {
                that.show();
                args[3].call(that);
            }
            return this;
        },

        /* given a global function name, find the function */
        functionByName: function(name) {
            var descend = window;  // find function by name on the root object
            jQuery.each(name.split("."), function() {
                if(descend) descend = descend[this];
            });
            return descend;
        },

	/* Given a function name or javascript fragment, return a function */
	createFunction: function(script) {
            if(!script) return function() {};
            if($.isFunction(script)) return script;
            var f=methods.functionByName.call(this, script);
            if(f) return f;
	    return function() { return eval(script); };
	},

            /* Build an options object suitable for sending to
             * jQuery.ajax.  (Note that the before & confirm callbacks
             * are called from this function, and the spinner is shown)
             *
             * The returned object will include a 'data' value
             * populated with a hash.
             *
             * Options:
             *  type: POST, GET
             *  attrs: a hash containing the standard Hobo ajax attributes & callbacks
             *  extra_options: merged into the hash sent to jQuery.ajax
             *  extra_callbacks: the callbacks in attrs are generally specified by the DRYML; this allows the framework to add their own
             *  function: passed to Hobo's ajax_update_response
             *  preamble: passed to Hobo's ajax_update_response
             *  postamble: passed to Hobo's ajax_update_response
             *  content_type: passed to Hobo's ajax_update_response
             *
            */
        buildRequest: function(o) {
            var that = this;
            if (!o.attrs) o.attrs = {};
            if (!o.extra_callbacks) o.extra_callbacks = {};
            var options = {};

            if(o.attrs.before) {
                if(!methods.createFunction.call(that, o.attrs.before).call(this)) {
                    return false;
                }
            }

            var before_evt=jQuery.Event("rapid:ajax:before");
            that.trigger(before_evt, [that]);
            if(before_evt.isPropagationStopped()) {
                return false;
            }

            if(o.attrs.confirm) {
                if(!confirm(o.attrs.confirm)) {
                    return false;
                }
            }

            options.context = this;
            options.type = o.type || 'GET';
            options.data = {"render_options[preamble]": o.preamble || '',
                            "render_options[contexts_function]": 'hjq.ajax.updatePartContexts'
                           };
            if(o.postamble) options.data["render_options[postamble]"] = o.postamble;
            if(o.content_type) options.data["render_options[content_type]"] = o.content_type;
            if(o.attrs.errors_ok) options.data["render_options[errors_ok]"] = 1;
            options.dataType = 'script';
            o.spec = jQuery.extend({'function': 'hjq.ajax.update', preamble: ''}, o.spec);

            var part_data = {};
            if(o.attrs.hide!==undefined) part_data.hide = o.attrs.hide;
            if(o.attrs.show!==undefined) part_data.show = o.attrs.show;
            if($.isEmptyObject(part_data)) part_data = undefined;

            // we tell our controller which parts to return by sending it a "render" array.
            var ids=methods.getUpdateIds.call(this, o.attrs);
            for(var i=0; i<ids.length; i++) {
                if(part_data) $("#"+ids[i]).data('hjq-ajax', part_data);
                options.data["render["+i+"][part_context]"] = page_data.hobo_parts[ids[i]];
                options.data["render["+i+"][id]"] = ids[i];
                options.data["render["+i+"][function]"] = o['function'] || 'hjq.ajax.update';
            }

            this.hjq_spinner(o.attrs, "Saving...");

            var success_dfd = jQuery.Deferred();
            if(o.attrs.success) success_dfd.done(methods.createFunction.call(that, o.attrs.success));
            if(o.extra_callbacks.success) success_dfd.done(methods.createFunction.call(that, o.extra_callbacks.success));
            success_dfd.done(function() {
                if(o.attrs.reset_form) that[0].reset();
                // if we've been removed, all event handlers on us
                // have already been removed and we don't bubble
                // up, so triggering on that won't do any good
                if(that.parents("body").length==0) $(document).trigger('rapid:ajax:success', [that]);
                else  that.trigger('rapid:ajax:success', [that]);
            });
            options.success = success_dfd.resolve;

            var error_dfd = jQuery.Deferred();
            if(o.attrs.error) error_dfd.done(methods.createFunction.call(that, o.attrs.error));
            if(o.extra_callbacks.error) error_dfd.done(methods.createFunction.call(that, o.extra_callbacks.error));
            error_dfd.done(function() {
                if(that.parents("body").length==0) $(document).trigger('rapid:ajax:error', [that]);
                else  that.trigger('rapid:ajax:error', [that]);
            });
            options.error = error_dfd.resolve;

            var complete_dfd = jQuery.Deferred();
            if(o.attrs.complete) complete_dfd.done(methods.createFunction.call(that, o.attrs.complete));
            if(o.extra_callbacks.complete) complete_dfd.done(methods.createFunction.call(that, o.extra_callbacks.complete));
            complete_dfd.done(function() {
                if(that.parents("body").length==0) $(document).trigger('rapid:ajax:complete', [that]);
                else  that.trigger('rapid:ajax:complete', [that]);
                that.hjq_spinner('remove');
                if(o.attrs.refocus_form) that.find(":input[type!=hidden]:first").focus();
            });
            options.complete = complete_dfd.resolve;

            jQuery.extend(options, o.extra_options);

            return options;
        },

        // given ajax_attrs (update, updates and/or ajax), return DOM id's.
        getUpdateIds: function(attrs) {
            var ids = attrs.update || [];
            if (typeof ids=='string') ids=ids.split(',');

            jQuery(attrs.updates).each(function () {
                ids.push(jQuery(this).attr('id'));
            });

            if(attrs.ajax) {
                for(var el=this; el.length && !page_data.hobo_parts[el.attr("id")]; el=el.parent());
                if(el.length) ids.push(el.attr('id'));
            }

            // validate
            for (var i=0; i<ids.length; i++) {
                if(!page_data.hobo_parts[ids[i]]) {
                    ids.splice(i, 1);
                    i -= 1;
                }
            }

            return ids;
        }


    };


    $.fn.hjq = function( method ) {

        if ( methods[method] ) {
            return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof method === 'object' || ! method ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist on hjq' );
        }
    };

})( jQuery );


// to make the Ajax interface cleaner, these provide direct access to
// a couple of plugin functions.
var hjq=(function($) {
    return {
        ajax: {
            update: function (dom_id, innerHtml) {
                $("#"+dom_id).hjq('update',innerHtml);
            },

            updatePartContexts: function(contexts) {
                $(document).hjq('updatePartContexts', contexts);
            }
        }
    };
})(jQuery);