/// <reference path="jquery.js" />
//parseTemplate from John Resig, modified for .NET by Rick Strahl http://www.west-wind.com/Weblog/posts/509108.aspx
//microAjax .NET JSON/REST and client-side data binding extensions by Jason DiOrio
// Version 1.1
// Change Log:
// 1.1 -
//       Upgraded to support jQuery 1.6.1
//       Removed MetaData plugin support; no longer needed with built-in HTML 5 data support
//       Removed Watermark plugin support because it is now defunct

(function ($) {
    var undef = (function (un) { return un; })();
    var tcache = window.templateCache || {},
        efunc = function () { },
        esfunc = function () { return ''; },
        ejqfunc = function () { return this; },
        validate = function (elem, evt) {
            $('.validationGroup label.error').remove();
            $('.validationGroup :input.error').removeClass('error');

            var $group = $(elem).parents('.validationGroup');

            var isValid = true;

            $group.find(':input').each(function (i, item) {
                if (!$(item).valid() && isValid && $(item).hasClass('error')) {
                    evt.preventDefault();
                    evt.stopImmediatePropagation();
                    isValid = false;
                }
            });

            // If any fields failed validation, prevent the event from being processed
            return isValid;
        }, tmplSet = function (defaults, override, data, context) { //normalized template settings
            var ret = $.isString(override) ? { templateId: override} : (override || {});
            if (data) ret.data = data;
            if (context) ret.context = context;
            return $.extend(defaults, ret);
        };

    //TODO: add noConflict style support within templates and custom jQuery events

    $.htmlEncode = function (value, encodeQuotes) {
        var ret = $('<div/>').text(value).html();
        if (encodeQuotes) ret = ret.replace('"', '&quot;').replace("'", "&#039;");

        return ret;
    };

    $.isString = function (test) {
        return test !== undef && typeof (test) === "string";
    }

    $.isJQ = function (test) {
        return (test instanceof jQuery);
    }

    $.tryCall = function (func, ctx, etc) {
        if ($.isFunction(func)) {
            var args = $.makeArray(arguments).slice(2);
            return func.apply(ctx, args);
        }
        return undef;
    }

    $.tryApply = function (func, ctx, args) {
        if ($.isFunction(func)) {
            return func.apply(ctx, args);
        }
        return undef;
    }

    //TODO: add/use default settings configuration with defaults set to .NET standard
    $.microAjax = function (url, data, successCallback, errorCallback, context) {
        /// <summary>
        /// ajax call designed specifically for calling ASP.NET web services and page methods
        /// </summary>    
        /// <param name="url" type="string">
        /// The url of the web method 
        /// ie: "CurrentPage.aspx/SomePageMethod</param>
        /// <param name="data" type="var">
        /// Optional JSON formatted data to send as parameters in the post
        /// </param>
        /// <param name="successCallback" type="function(returnData, xhr)">
        /// Optional callback function on a successful ajax call.
        /// returnData: JSON formatted data received in the response
        /// xhr: The XMLHttpRequest object
        /// </param>
        /// <param name="errorCallback" type="function(xhr, errorType, ex)">
        /// The callback function on an unsuccessful ajax call.
        /// xhr: The XMLHttpRequest object
        /// errorType: a string of the error type
        /// ex: the exception thrown (if applicable)
        /// </param>
        /// <param name="context" type="DOM">
        /// This object will be made the context of all Ajax-related callbacks.
        /// </param>

        var ctx = (this !== $) ? this : $.microAjax;

        if ($.isFunction(ctx.urlResolver)) {
            url = ctx.urlResolver(url);
        }

        var success = function (returnData) {
            var args = arguments;

            //in case we want to do some debug logic or generic processing later
            if ($.isFunction(ctx.ajaxDeserializer)) {
                args = $.makeArray(args);
                args[0] = ctx.ajaxDeserializer(returnData);
            }

            return $.tryApply(successCallback || $.microAjax.ajaxDefault.success, this, args);
        };


        var error = function () {
            //in case we want to do some debug logic or generic processing later
            return $.tryApply(errorCallback || $.microAjax.ajaxDefault.error, this, arguments);
        };

        var newSet = tmplSet({}, { url: url, success: success, error: error }, data, context);

        var set = $.extend({}, $.microAjax.ajaxDefault, newSet);

        if ($.isFunction(ctx.ajaxSerializer)) {
            set.data = ctx.ajaxSerializer(set.data);
        }

        $.ajax(set);
    };

    //TODO: consider additional default option capabilities
    $.extend($.microAjax, {
        ajaxDefault: {
            type: "POST",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        },
        templateProcessor: function () {
            var $this = $.isJQ(this) ? this : $(this);

            if ($.fn.validate) {
                $this.find(':input[data-validate]').each(function () {
                    $(this).validate($(this).data('validate'));
                });
                $this.find('.causesValidation').click(
                    function (evt) {
                        return Validate(this, evt);
                    });
                //htmp.find('.validationGroup :text').keydown(function (evt) {
                //    if (evt.keyCode == 13) return Validate(this, evt);
                //});
                //can't bind to keydown on input? only works on document? live attaches to document...
            }
        },
        defaultApplicator: function (newChild, animCompleteCallback) {
            $(this).fadeOut('normal', function () {
                $(this).html(newChild).fadeIn('normal', animCompleteCallback);
            });
        },
        appendApplicator: function (newChild, animCompleteCallback) {
            var item = $(this);
            var htmp = newChild.wrap('<div class="microAjaxWrapper"></div>');
            htmp.hide().appendTo(item).show('slow', function () {
                //newChild.unwrap();
                if ($.isFunction(animCompleteCallback)) animCompleteCallback.call(item, newChild);
            });
        },
        prependApplicator: function (newChild, animCompleteCallback) {
            var item = $(this);
            var htmp = newChild.wrap('<div class="microAjaxWrapper"></div>');
            htmp.hide().prependTo(item).show('slow', function () {
                //newChild.unwrap();
                if ($.isFunction(animCompleteCallback)) animCompleteCallback.call(item, newChild);
            });
            return item;
        },
        callApplicator: ejqfunc,
        urlResolver: function (url) {
            return ($.isString(url) && url.indexOf('/') === -1) ? (location.pathname + '/' + url) : url;
        },
        ajaxDeserializer: function (data) {
            var ret = data;
            if (ret && (ret.d || ret.d === false)) {
                ret = ret.d; //ASP.NET AJAX 3.5 WebMethod results
            }
            //TODO: .NET datetime deserialization
            return ret;
        },
        ajaxSerializer: function (data) {
            var ret = data;
            if (ret !== false && !ret) ret = "{}"; //.NET chokes on post data with zero length
            else if (!$.isString(ret)) ret = JSON.stringify(data);
            //TODO: .NET datetime serialization
            return ret;
        }
    });

    //TODO: allow custom start/stop/evaluation identifiers, fix multiline and comment issues 
    $.compileTemplate = function (str) {
        ///<summary>
        ///compiles micro-template string source into a function that, when called, will apply any specified data to the template and return the
        ///template output.
        ///</summary>
        ///<remarks>
        ///Generally this should not be used directly. The template cache is not used by this function
        ///</remarks>
        ///<param name="source" type="string">
        ///The template source to transform
        ///</param>
        ///<returns type="function(data)" />

        var strFunc = "var p=[],print=function(){p.push.apply(p,arguments);};p.push('" +
                        str.replace(/[\r\t\n]/g, " ")
                       .replace(/'(?=[^#]*#>)/g, "\t")
                       .split("'").join("\\'")
                       .split("\t").join("'")
                       .replace(/<#=(.+?)#>/g, "',$1,'")
                       .split("<#").join("');")
                       .split("#>").join("p.push('") +
                       "');return p.join('');";

        //if (window.console) window.console.log(strFunc);

        return new Function("data", strFunc);
    }

    $.getTemplateFunction = function (settings) {
        ///<summary>
        ///returns a function designed to transform a template to a string optionally based on a data parameter
        ///</summary>
        ///<param name="settings" type="var">
        ///generally a standard jQuery settings object optionally including a combination of a 
        ///templateId, templateHtml, and templateFunction. If the settings object is just a string
        ///it will be treated as the templateId
        ///</param>
        ///<returns type="function(data)" />
        ///<renarks>
        ///if no templateId is specified and a jQuery object is passed for templateHtml we try to use the 
        ///jQuery element ID as the templateId and set the templateHtml to the raw html of the element
        ///the following logic prioritizes a cached function if a templateId is available. 
        ///In the event that no cached function is available and a templateFunction is provided that will 
        ///be used otherwise if templateHtml was provided it will be compiled to a function to be used. 
        ///If a templateId and a function was obtained the final result will be cached for future calls. 
        ///If no function is available nothing will be cached and a function that returns an empty string 
        ///will be used.
        ///</remarks>
        var set = {
            templateId: null,
            templateHtml: null,
            templateFunction: null
        }, ret = esfunc;

        set = $.extend(set, $.isString(settings) ? { templateId: settings} : settings);

        var tid = set.templateId;
        var thtml = set.templateHtml;
        var tfn = set.templateFunction;

        if (!tid) {
            if ($.isJQ(thtml)) {
                var eid = thtml.attr('id');
                if ($.isString(eid) && eid !== '') tid = eid;
                thtml = thtml.html();
            }
        }

        if (tid) {
            var t = tcache[tid];
            if ($.isFunction(t)) {
                ret = t;
            } else if ($.isFunction(tfn)) {
                tcache[tid] = ret = tfn;
            } else if (!$.isString(thtml)) {
                thtml = $.trim($('#' + tid).html());
            }
        }

        if (ret === esfunc) {
            if ($.isFunction(tfn)) {
                ret = tfn;
            } else if ($.isString(thtml) && thtml !== '') {
                ret = $.compileTemplate(thtml);
                if (tid) tcache[tid] = ret;
            }
        }

        return ret;
    }

    $.parseTemplate = function (settings, data) {
        //other settings perhaps?
        var set = $.extend({}, settings);
        if (data) set.data = data;

        var d = set.data || {};
        var tfn = $.getTemplateFunction(settings);

        try {
            return tfn.call(d, d);
        } catch (e) {
            throw $.extend(e, { settings: set, template: tfn.toString() });
        }
    };

    $.processTemplate = function (settings, data) {
        var tmp = $.parseTemplate(settings, data);
        var htmp = $(tmp);
        var set = { templateProcessor: $.microAjax.templateProcessor }
        set = $.extend(set, $.isString(settings) ? { templateId: settings} : settings);

        if ($.isFunction(set.templateProcessor)) set.templateProcessor.apply(htmp, arguments);

        return htmp;
    }

    $.fn.applyTemplate = function (settings, data, context) {
        var set = {
            applicator: $.microAjax.defaultApplicator,
            animationComplete: null
        };
        set = tmplSet(set, settings, data, context);
        var appl = set.applicator;
        if ($.isString(appl)) appl = $.microAjax[appl + "Applicator"] || $.fn[appl];

        var newChild = $.processTemplate(set);
        return $.tryCall(appl, this, newChild, set.animationComplete);
    }

    var tfns = ["append", "prepend", "call"];
    $.each(tfns, function (idx, name) {
        var func = $.microAjax[name + "Applicator"] || $.fn[name];
        $.fn[name + "Template"] = function (settings, data) {
            var set = tmplSet({ applicator: func }, settings);
            return $(this).applyTemplate(set, data);
        }
    });

    $.fn.microAjax = function (settings) {
        var ctx = $(this);

        var config = {
            url: null,
            data: null,
            template: {
                templateId: null,
                templateHtml: null,
                templateFunction: null,
                applicator: $.microAjax.defaultApplicator
            },
            errorTemplate: { //TODO: evaluate pattern?
                templateId: null,
                templateHtml: null,
                templateFunction: null,
                applicator: $.microAjax.defaultApplicator
            },
            errorCallback: null,
            successCallback: null,
            completeCallback: null,
            urlResolver: $.microAjax.urlResolver,
            ajaxDeserializer: $.microAjax.ajaxDeserializer,
            ajaxSerializer: $.microAjax.ajaxSerializer,
            context: this
        };

        if (settings) { $.extend(config, settings); }

        var suc = function () {
            $.tryApply(config.successCallback, config.context, arguments);
            $.tryApply(config.completeCallback, config.context, arguments);
        };

        var err = function () {
            $.tryApply(config.errorCallback, config.context, arguments);
            $.tryApply(config.completeCallback, config.context, arguments);
        }

        $.microAjax.call(config, config.url, config.data,
            function (data, xhr) {
                if (config.template) {
                    ctx.applyTemplate(config.template, data);
                    suc.apply(ctx, arguments);
                } else {
                    suc.apply(ctx, arguments);
                }
            },
            function (xhr, errorType, ex) {
                if (config.errorTemplate) {
                    var data = { xhr: xhr, errorType: errorType, exception: ex };
                    ctx.applyTemplate(config.errorTemplate, data, function () {
                        err.apply(ctx, arguments);
                    });
                } else {
                    err.apply(ctx, arguments);
                }
            },
            ctx
        );

        return ctx;
    };

    if ($.fn.validate) {

        //this is a generic take on the validation group emulation described here: 
        // http://encosia.com/2009/11/24/asp-net-webforms-validation-groups-with-jquery-validation/
        // paired with auto-applying validation settings on templated elements above this can be pretty handy
        $(function () {
            $("form").validate({
                // This prevents validation from running on every form submission by default.
                onsubmit: false,
                onfocusout: false,
                onkeyup: false,
                onclick: false
            });

            //$('.validationGroup .causesValidation').live('click',Validate);
            $('.validationGroup :text').live('keydown', function (evt) {
                if (evt.keyCode == 13) return Validate(this, evt);
            });
        });

        //TODO: add custom validators, 
    }

    //trim11 from http://blog.stevenlevithan.com/archives/faster-trim-javascript
    $.microTrim = function (str) {
        str = str.replace(/^\s+/, '');
        for (var i = str.length - 1; i >= 0; i--) {
            if (/\S/.test(str.charAt(i))) {
                str = str.substring(0, i + 1);
                break;
            }
        }
        return str;
    }

    $.ensureArray = function (itemOrList) {
        /// <summary>
        /// Convenience function for templates (and as needed) to return either:<ol>
        /// <li>The original parameter if it is a non-zero length array</li>
        /// <li>The parameter wrapped in an array if it is a single non-null object</li>
        /// <li>null if the argument is null or a zero length array</li></ol>
        /// </summary>
        return (itemOrList && //not null
            ((itemOrList.length && itemOrList.length > 0 && itemOrList) ||  //non-zero array
            (itemOrList.length !== 0 && [itemOrList]))) || //single item converted to array
            null; //if all else fails return null
    }

    //todo: is it possible to use font measuring to make this more consistent? alternatively we could use a monospaced font.
    //perhaps measuring can be done using JS in advance and a character->width map can be used?
    $.microMore = function (text, max, wordMax) {
        if (!max) max = 250;
        if (!wordMax) wordMax = 75;

        var wrap = function (text) {
            var ret = text;
            var words = text.split(/\s/);
            for (var widx in words) {
                var word = words[widx];
                if (word.length > wordMax) {
                    var newWord = '';
                    var lww = 0;
                    for (var ww = wordMax; ww < word.length; lww = ww, ww += wordMax) {
                        newWord += word.substring(lww, ww) + '<wbr />';
                    }
                    newWord += word.substring(lww);
                    ret = ret.replace(word, newWord);
                }
            }
            return ret;
        }
        var trunc = function (text, idx) {
            return wrap(text.substring(0, idx)) + '<span class="more"><span class="hidden">' + wrap(text.substring(idx)) +
                '</span>... <span class="a show">Read More</span><span class="a unshow hidden">Read Less</span></span>';
        }

        if (text.length > max) {
            for (var lidx = max; lidx > max / 2; lidx--) {
                var ch = text.charAt(lidx);
                if (/\s/.test(ch)) {
                    return trunc(text, lidx);
                }
            }
            return trunc(text, max);
        } else {
            return wrap(text);
        }
    }

    if ($.ui.autocomplete) {
        //clone autocomplete functionality, we'll override specific parts of the prototype
//        $.ui.microcomplete = function () {
//            $.tryApply($.ui.autocomplete, this, arguments);
//        }

        var acproto = $.ui.autocomplete.prototype,
                initSource = acproto._initSource,
                renderItem = acproto._renderItem;

        /*
        * Modified from jQuery UI Autocomplete HTML Extension
        *
        * Copyright 2010, Scott González (http://scottgonzalez.com)
        * Dual licensed under the MIT or GPL Version 2 licenses.
        *
        * http://github.com/scottgonzalez/jquery-ui-extensions
        */

        //$.ui.microcomplete.prototype = $.extend({}, acproto, {
        $.extend(acproto, {
            _initSource: function () {
                if (this.options.template &&
                    $.isFunction(this.options.template.filter) &&
                    $.isArray(this.options.source)) {
                    this.source = function (request, response) {
                        response(this.options.template.filter(this.options.source, request.term));
                    };
                } else {
                    initSource.call(this);
                }
            },

            _renderItem: function (ul, item) {
                if (this.options.template) {
                    var set = { template: this.options.template };
                    var htmp = $.processTemplate(set, item);

                    if (set.autowrap !== false) {
                        htmp = $("<li></li>")
                            .data("item.autocomplete", item)
                            .append($("<a></a>").html(htmp));
                    }

                    return htmp.appendTo(ul);
                } else {
                    renderItem.call(this, ul, item);
                }
            }
        });
    }

    $(function () {
        $('span.more .show, span.more .unshow').live('click', function () {
            $(this).closest('span.more').children('span').toggleClass('hidden');
        });
    });


} (jQuery));
