// ---------------------------------------------------------------------------
// The FuzzyToast project aims to easily combine data from standard REST
// calls with "templates" and insert the results back into the HTML layout.

// This goal removes xSP processing from the server to the client, making
// the server work significantly more straight-forward and easier. However,
// to help with the added complexity of the client, we've created this
// project.
// ---------------------------------------------------------------------------
// Created using an MIT Open Source Licence (similar to jQuery).
// (c) 2010, Howard Abrams
// ---------------------------------------------------------------------------

// The following black magic wrapper is a safe way to create a jQuery plugin.

(function($) {

    /**
     * The `$.fuzzytoast()` function is the primary way to kick off the download
     * of a template and REST data for combining and inserting into a DOM
     * element.
     * 
     * The first parameter is either the options that contains all of the needed
     * information to perform this task, as in:
     * 
     * $.fuzzytoast({ template : 'templates/profile.html', data :
     * 'data/user/'+$(this).attr('id')+'.json', destination : '#main' });
     * 
     * Or you can create an store the options separately, and then call the
     * `$.fuzzytoast()` function using the returned identification, as in:
     * 
     * var id = $.fuzzytoast.create({ template: 'templates/details.html', data :
     * '/person/4', method : 'GET', destination: '#main' });
     * 
     * And then trigger it later:
     * 
     * $('#some-button).fuzzytoast('unique-id-value');
     */
    $.fuzzytoast = function(idx, parameters) {
        var id = idx;
        if (typeof (idx) !== 'string') {
            id = $.fuzzytoast.create(idx);
        }

        // Allow option properties in the link (and passed in parameters)
        // will override default values set elsewhere.

        var linkdata = $.extend({
            id          : id,
            destination : $.fuzzytoast.default_destination,
            method      : $.fuzzytoast.default_method,
            append      : $.fuzzytoast.default_append,
            before      : $.fuzzytoast.default_before,
            finished    : $.fuzzytoast.default_finished,
            complete    : $.fuzzytoast.default_complete,
            success     : $.fuzzytoast.default_success,
            parameters  : parameters
        }, $.fuzzytoast.linkdata[id]);

        debug("Starting fuzzytoast request", id);

        // If the `before` parameter is set to a function, we call it, giving
        // it the destination. This allows people to put a spinner in the
        // destination area. If, this returns a function, we'll pass that 
        // function to jQuery's $.ajax() as a `beforeSend` callback.
        
        if ( linkdata.before && typeof linkdata.before === 'function' ) {
            var beforeSend = linkdata.before(linkdata.destination, linkdata);
            if (beforeSend) {
                linkdata.beforeSend = beforeSend;
            }
        }
        
        // Start work on gathering up the data sources
        // while we start work on the getting the template.
        linkdata.sourceCount = -1;
        
        getDataSources(linkdata);
        
        // Currently, we are caching all templates in memory. This might be
        // too harsh for a large application. How should determine if we
        // should or shouldn't cache the template?

        if (! linkdata.templateData) {
            // Download the template, and on success, call the 'getdata' to
            // download the data from a web service.
            if( typeof linkdata.template === "string") { //assume it is a URL
                $.ajax({
                    url : linkdata.template,
                    dataType : 'text',
                    beforeSend : linkdata.beforeSend ? linkdata.beforeSend : null,
                    success : function(templateData) {
                        debug("Retrieved '", + id + "' template:", $.fuzzytoast.linkdata[id].template);
                        // debug(templateData);
                        processTemplate(templateData, function(completeTemplate){
                            linkdata.templateData = completeTemplate;
                            processDataAndTemplate(linkdata);
                        });
                    },
                    error : function(jqXHR, textStatus, errorThrown) {
                        $.fuzzytoast.errorHandler('template', linkdata, jqXHR, textStatus, errorThrown);
                    }
                });
            }
            else { //assume it is a jQuery object
                debug("Retrieved '" + id + "' template from jQuery object:", $.fuzzytoast.linkdata[id].template);
                processTemplate($(linkdata.template).html(), function(completeTemplate){
                    linkdata.templateData = completeTemplate;
                    //TODO: is this compatible with the above method? do we need an outerhtml method?
                    processDataAndTemplate(linkdata);
                });
            }
        }
    };

    /**
     * This stores the collection of links. Required to make it easy to keep all
     * of the utility functions (below) coordinated.
     */
    $.fuzzytoast.linkdata = {};

    /**
     * Default values that can be overridden for the needs of the application
     * using this plugin.
     */
    $.fuzzytoast.default_destination = "#main";
    $.fuzzytoast.default_method = "GET";
    $.fuzzytoast.default_append = false;

    /**
     * The user can change this value in order to display more debugging
     * information.
     */
    $.fuzzytoast.debug = false;

    $.fuzzytoast.idCount = 0;
    
    /**
     * Predefine a fuzzytoast that will be referred to later by a unique
     * identifier:
     * 
     * $.fuzzytoast.create( 'unique-id-value', { template:
     * 'templates/details.html', data : '/person/4', method : 'GET',
     * destination: '#main' });
     * 
     * Select the triggering button, as in:
     * 
     * $('#some-button).fuzzytoast('unique-id-value');
     * 
     * The `id` is optional, for you could do this:
     * 
     * var id = $.fuzzytoast.create({ template: 'templates/details.html', data :
     * '/person/4' }); $('#some-button).fuzzytoast(id);
     */
    $.fuzzytoast.create = function(idx, options) {

        var id = idx;

        // No ID is given, just parameters, so let's first create an ID
        // based on idCount and the contents of the parameters:
        if (typeof (idx) !== 'string') {
            options = idx;
            id = ($.fuzzytoast.idCount++).toString();
            
            if (typeof options.template === 'string') {
                id += "-" + options.template;
            }
            if (typeof options.data === 'string') {
                id += "-" + options.data;
            }
        }

        if ($.fuzzytoast.default_error && !options.error) {
            options.error = $.fuzzytoast.default_error;
        }

        $.fuzzytoast.linkdata[id] = options;

        return id;
    };

    /**
     * Changes the template engine settings (instead of the default feature of
     * auto discovery). The options specified by the `engine` variable must
     * include a `type`, and one of the following:
     * 
     *  - `engine`: The name of the template engine. Only used in log messages.
     *  - `type`: The type of template engine. See below for accepted values.
     *  - `render`: Function that takes both template and data and returns results (e.g. HTML)
     *  - `compiler`: Function that compiles the template into an intermediate form.
     *  - `processor`: Function that takes the compiled template and the data. Currently, only used for the `global-variables` type.
     *  
     * The behavior for how the engine is called is based on its `type`:
     * 
     *  - `functional`: The approach uses the `render` function with both the template and the model at once, and returns the generated results, e.g. Mustache.
     *  - `variable`: This approach uses the return value of the `compiler` as a function and passes the "data" to it for the output results, e.g. Handlebars.
     *  - `global-storage`: This stores the results of the `compiler` in a global variable string and then calls the `processor` function, e.g. jQuery Templates.
     */
    $.fuzzytoast.setTemplateEngine = function(engine) {
        if (typeof engine === 'string') {
            discoverTemplateSetup(engine);
        }
        else if (engine.type) {
            $.fuzzytoast.template = engine;
        }
        else {
            throw "Unsupport template engine";
        }
    };

    // Sure, this regular expression could be overwritten:
    $.fuzzytoast.templateIncludes = /\{\{\s*INCLUDE\s+(\S+)\s*\}\}/m;

    
    // -------------------------------------------------------------
    // Following functions are not intended to be called directly.
    // -------------------------------------------------------------

    /**
     * Once we have a template, we want to be able to include other templates
     * in it. This allows us to reuse templates, so we parse the template data
     * for keywords:  {{INCLUDE url}}
     */
    processTemplate = function( templateData, callback ) {
        var match = $.fuzzytoast.templateIncludes.exec(templateData);
        // console.log("completeTemplate:", templateData, "match():", match);
        if (match) {
            debug("Retrieving sub-template:", match[1]);
            $.ajax({
                url : match[1],
                beforeSend : linkdata.beforeSend ? linkdata.beforeSend : null,
                success : function( templateString ) {
                    var newTemplate = 
                        templateData.substring(0, match.index) +
                        templateString +
                        templateData.substring(match.index + match[0].length);
                    processTemplate(newTemplate, callback);
                },
                error : function( err ) {
                    console.log("Could not retrieve embedded template: ", err);
                }
            });
        }
        else {
            callback(templateData);
        }
    };

    /**
     * Function called to retrieve the data sources specified in the `data`
     * property. This can either be a single string to a single URL, or it
     * can be an object containing multiple sources and the name used in 
     * inserting the results back into the model, as in:
     * 
     *     data: {
     *       user: '/user/45.json',
     *       account: '/account/3.json'
     *     }
     * 
     * Will result in a model containing some like this:
     * 
     *     {
     *       'user': {
     *         'first': 'Bob',
     *         'last': 'Barker', //... More data from the REST call
     *       },
     *       'account': {
     *         '
     *       }
     *     }
     */
    getDataSources = function(linkdata) {
        if ( typeof linkdata.data === 'string' ) {
            linkdata.sourceCount = 1;
            getDataSource(linkdata, linkdata.data);
        }
        else {
            linkdata.model = {};
            linkdata.sourceCount = dataSourcesLength(linkdata.data);
            for (i in linkdata.data) {
                // debug("Asking for ", i, "in", linkdata.data[i], "Count", linkdata.sourceCount);
                getDataSource(linkdata, linkdata.data[i], i);
            }
        }
    };
    
    dataSourcesLength = function(obj){
        var count = 0;
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                count++;
            }
        };
        return count;
    };
    
    getDataSource = function(linkdata, src, dataid) {
        // Download the data from a web service, and on success,
        // call the 'process' to combine the template and results
        // and insert back into the DOM.

        $.ajax({
            url : src,
            type : linkdata.method,
            dataType : 'text',
            beforeSend : linkdata.beforeSend ? linkdata.beforeSend : null,
            data : linkdata.parameters,
            success : function(data, textStatus, jqXHR) {
                debug("Retrieved data: ", data);
		try {
                // Safari is stupid and can't seem to parse anything, so
                // we use this clever approach stolen from jQuery's source.
                var model;
                if ($.browser.safari) {
                    model = (new Function("return " + data))();
                }
                else {
                    model = $.parseJSON(data);
                }
                }
		catch (err) {
		    console.log("Could not parse data from ", src, " - ", err);
		    return;
		}

                // Did we just get the results of our of many data sources?
                if ( dataid ) {
                    linkdata.model[dataid] = model;
                }
                else {   // We just got the only data source...
                    // JQuery tmpl plugin will iterate all the templates if the json
                    // in an Array, you can't simply use the each to iterate all
                    // the values, to makes it works for each, add a default wrapper
                    // 'data' outside of Array, that you can use ${{each(index, value) data}}
                    // for this case.
                    // JSON example: [ { name: 1 }, { name: 2} ]
                    if( typeof model !== 'object' || model.length >= 0 ) {
                        model = { data : model };
                    }
                    linkdata.model = model;
                }
                linkdata.sourceCount--;
                processDataAndTemplate(linkdata);
            },
            error : function(jqXHR, textStatus, errorThrown) {
                $.fuzzytoast.errorHandler('model', linkdata, jqXHR, textStatus,
                        errorThrown);
            }
        });
    };

    // Once each data source and the template has been retrieved (and 
    // individually processed), we call this function. It doesn't do anything
    // unless everything was successfully gathered and processed...
    
    processDataAndTemplate = function(linkdata) {
        debug("We are in ", linkdata, "with", linkdata.sourceCount, "and", linkdata.templateData);
        // Only process once we have the template data and all data sources:
        if ( linkdata.sourceCount === 0 && linkdata.templateData ) {
            // The template passed in could be either a string (which
            // works fine) or an HTML Object.
            
            var destination;
            if (typeof linkdata.destination === 'string') { //assume it is a selector
                destination = $(linkdata.destination);
            } else{
                destination = linkdata.destination; //assume it is a jQuery object
            } 
            
            debug("Processing into", destination);
            
            // Clear out the destination section:
            if (!linkdata.append) {
                destination.empty();
            }
            
            var html = templateRender(linkdata.templateData, linkdata.model);
            debug( $.fuzzytoast.template.engine, html);

            destination.append(html);
            
            if (linkdata.success) {
                linkdata.success(destination, linkdata.model);
            }
            if (linkdata.finished) {   // NOTE: This approach is obsolete and will
                linkdata.finished();   //       be removed in version 2.0
            }
            if (linkdata.complete) {
                linkdata.complete();
            }
            
            // If the link contains a "refresh" tag, we will reget the 
            // data sources and process things again, without bothering
            // to get the template.
            if (linkdata.refresh) {
                setTimeout( function(){
                    getDataSources(linkdata);
                }, linkdata.refresh);
            }
        }
    };

    /**
     * Called when an AJAX error happens. We build up a data object with details
     * about the error, and then call the user's error handler.
     */
    $.fuzzytoast.errorHandler = function(requestType, linkdata, jqXHR,
            textStatus, errorThrown) {
        var url = "unknown";
        switch (requestType) {
        case 'template':
            url = linkdata.template;
            break;
        case 'model':
            url = linkdata.data;
            break;
        }

        console.error("The '" + linkdata.id + "' request for the "
                + requestType + ", " + url + ", returned a " + jqXHR.status
                + ".");
        console.error("jqXHR", jqXHR);

        if (linkdata.error) {
            var errorDetails = {
                type : requestType,
                url : url,
                link : linkdata,
                status : jqXHR.status,
                statusText : jqXHR.statusText,
                jqXHR : jqXHR
            };

            linkdata.error(errorDetails);
        }
        
        if (linkdata.finished) {   // NOTE: This approach is obsolete and will
            linkdata.finished();   //       be removed in version 2.0
        }
        if (linkdata.complete) {
            linkdata.complete();
        }
    };

    /**
     * Given the data source (the `model` variable), and the template source,
     * we process everything based on the type of the template engine.
     */
    templateRender = function(template_source, model) {
        discoverTemplate();
        
        with ( $.fuzzytoast.template ) {
            // A "functional" approach is the easiest, as this function takes 
            // both the template and the model at once, and returns the 
            // generated results.
            if ( type === 'functional' ) {
                return render(template_source, model);
            }

            // Global storage is obnoxious, because it isn't functional. We'll
            // store all of our templates with the key: fuzzytoast-template
            if ( type === 'global-storage' ) {
                compiler('fuzzytoast-template', template_source);
                return processor('fuzzytoast-template', model);
            }

            // The "variable" approach compiles the template and returns it.
            // We then use that to process the "model"...
            if ( type === 'variable' ) {
                var template = compiler(template_source);
                return template(model);
            }
        }
    };

    /**
     * Analyze the current JavaScript environment and guess which
     * template engine we should use.
     * 
     * If the template engine has already been setup, calling this function
     * is a noop.
     */
    discoverTemplate = function() {
        if ( ! $.fuzzytoast.template ) {
            // First preference: Handlebars
            if (typeof Handlebars !== 'undefined') {
                discoverTemplateSetup("Handlebars");
            }
            // Second preference: Mustache
            else if (typeof Mustache !== 'undefined' ) {
                discoverTemplateSetup("Mustache");
            }
            else if (typeof _ !== 'undefined') {
                discoverTemplateSetup("Underscore");
            }
            else {
                discoverTemplateSetup("jQuery Templates");
            }
            console.log("Template setup: ", $.fuzzytoast.template.engine, "(",
                    $.fuzzytoast.template.type, ")");
        }
    };
    
    /**
     * Given a single letter key, set up our template processing based
     * on the template engine we will be using.
     * 
     * This is called by `discoverTemplate()` (which picks what is available),
     * as well as by `setTemplateEngine()` (which allows the user to pick one). 
     */
    discoverTemplateSetup = function(type) {
        $.fuzzytoast.template = {};

        switch(type.substring(0,1).toLowerCase()) {
            case "h":
                $.fuzzytoast.template.engine    = "Handlebars";
                $.fuzzytoast.template.type      = "variable";
                $.fuzzytoast.template.compiler  = Handlebars.compile;
                $.fuzzytoast.template.processor = null;
                break;
            case "m":
                $.fuzzytoast.template.engine    = "Mustache";
                $.fuzzytoast.template.type      = "functional";
                $.fuzzytoast.template.render    = Mustache.render;
                break;
            case "_":
            case "u":
                $.fuzzytoast.template.engine    = "Underscore";
                $.fuzzytoast.template.type      = "functional";
                $.fuzzytoast.template.render    = _.template;
                break;
            default:
                $.fuzzytoast.template.engine    = "jQuery Template";
                $.fuzzytoast.template.type      = "global-storage";
                $.fuzzytoast.template.compiler  = $.template;
                $.fuzzytoast.template.processor = $.tmpl;
                break;
        }
    };

    /**
     * A way to display log messages only if the user tells us too, as in:
     * 
     *         $.fuzzytoast.debug = false;
     */
    debug = function() {
        if($.fuzzytoast.debug) {
            var log = Function.prototype.bind.call(console.log, console);
            var args = Array.prototype.slice.call(arguments);
            args.unshift("FuzzyToast:");

            log.apply( this, args );
        }
    };

})(jQuery);

/**
 * jQuery method for attaching a fuzzytoast to the results of a selector. For
 * example:
 * 
 * $('#landing_link').fuzzytoast ({ template: 'templates/about.html', // The
 * template we should load data : 'data/nothing.json' // The REST call for the
 * data. });
 */
$.fn.fuzzytoast = function(options) {

    var id;

    // Calling a pre-created fuzzytoast...
    if (typeof (options) === 'string') {
        id = options;
        options = $.fuzzytoast.linkdata[id];
    }
    // Need to create a fuzzytoast first...
    else {
        // Does the selected 'element' have an ID attribute?
        // If so, we will use that as the ID into the
        // fuzzytoast hashmap.

        if (this.attr('id')) {
            id = $.fuzzytoast.create('#' + this.attr('id'), options);
        }
        // If not, we'll make an ID based on the template and
        // REST URL, which should be unique enough... and may
        // be reusable.

        else {
            id = $.fuzzytoast.create(options);
        }
    }
    // Make sure the template and data are specified, since the 'options'
    // given to us go right into the map for us to use later.

    if (options.template && options.data) {
        if ($.fuzzytoast.debug) {
            console.log("Processing", id, "data", options.data);
        }
        $.fuzzytoast.linkdata[id] = options;
        this.css('cursor', 'pointer').click(function() {
            $.fuzzytoast(id);
        });
    }
    else {
        console.log("The 'fuzzytoast' method requires both a 'data' and 'template' option.");
    }
};


$.fuzzytoast.loadcache = {};

/**
 * Similar in behavior to the standard jQuery `.load()` function. The difference
 * is that if some HTML has been loaded before, it won't be re=downloaded from
 * the server, but instead will come out of an in-memory cache.
 * 
 * This function is useful for static HTML files that you know won't change
 * very often.
 * 
 * The differences with the *second load* include:
 * 
 *   - The `data` value (if any) is ignored.
 *   - The callback is only given the data, not a `textStatus` or the `XMLHttpRequest` object
 * 
 * **Note:** The cache is cleared when the browser page is refreshed.
 * 
 * @see http://api.jquery.com/load/
 */
$.fn.loadWithCache = function(url, data, callback)
{
    if (typeof data === 'function') {
        callback = data;
        data = null;
    }
    
    if ( $.fuzzytoast.loadcache[url] ) {
        this.html( $.fuzzytoast.loadcache[url] );
        if (callback) {
            callback($.fuzzytoast.loadcache[url]);
      }
    }
    else {
      this.load(url, data, function( text, status, jx ) {
          $.fuzzytoast.loadcache[url] = text;
          if (callback) {
                callback(text, status, jx);
          }
      });
    }
    
    return this;
 };