/**
 * @desc This plugin will transform Zend Forms into ajaxable forms with
 * hints. When using the Zend_Form_Element::setDescription() method, the
 * plugin will automatically take that element as an hint tooltip
 * and use it for hinting and validation errors returned from the server.
 * 
 * Validation errors response must be a json encoded list of elements
 * with their respective error, which can be accomplished by by the
 * {@link Zend_Form::processAjax($_POST)} in a controller.
 *
 * Currently only supported on Majisti form table layout.
 * Currently works only on input fields and textareas
 * 
 * @author Steven Rosato
 */
;(function($) {

$.fn.ajaxForm = function(options) {

    /* on error, construct the error message hint for each element data */
    function errorCallback(elements, data) {
        /* retrieve all elements with errors */
        $allErrorElements = $form
                                .find('.element')
                                .children()
                                .filter('.error')
                                .filter(elements);

        /* refresh captcha if any */
        if( 'captchaId' in data && 'captchaUrl' in data ) {
            $captcha = $form.find('.element.captcha');

            $captcha.find('input:hidden').attr('value', data.captchaId);
            $captcha.find('img').attr('src', data.captchaUrl);
            $captcha.find('input:text').val('');

            delete data.captchaId;
            delete data.captchaUrl;
        }

        /* show hint */
        $.each(data, function(elementName, errors) {
            if ( $(errors).size() > 0 ) {
                /* error message in an ul/li fashion */
                $errorMessage = $('<ul />');
                $.each(errors, function(type, message) {
                    $('<li/>').html(message).appendTo($errorMessage);
                });

                $el = $form.find('#' + elementName + ',#' + elementName + '-input');
                $el
                    .removeClass('valid')
                    .addClass('error');

                clearErrors($allErrorElements.not($el));

                showHint($el, $errorMessage);
            }
        });


        if( elements.is('form') ) {
            elements.find('.element .error:first').focus();
        }
    }

    /* on elements success */
    function successCallback(elements, data) {
        if( elements.is('form') ) {
            elements = $(elements).find('.element .error');
            conf.successCallback(data);
        }

        clearErrors(elements);
    }

    /* clears the errors from the sets of jquery elements */
    function clearErrors(elements) {
        $.each(elements, function(key, element) {
            $(element)
                .removeClass('error')
                .addClass('valid')
                .parent().parent().find('.hint')
                .removeClass('error')
                .html('')
                .hide();
        });
    }

    /* show hint */
    function showHint($element, message, isError) {
        $hint = $element.parent().parent().find('.hint');

        if( !isError && false !== isError ) {isError = true;}

        /* based on message */
        if( message ) {
            $hint.html(message);

            /* based on error */
            if( isError ) {
                $element.addClass('error');
                $hint.addClass('error');
            } else {
                $element.removeClass('error');
                $hint.removeClass('error');
            }
        }

        /* do not show hint if its the current element or if disabled in config */
        if( !($currentElement.attr('id') == $element.attr('id')
            && $hint.html() && conf.useHint) ) {
            return;
        }

        $hint.fadeIn();
    };

    /* validate the form */
	function validate($elements) {
        /* server side ajax call to retrieve error messages */
		$.ajax({
            url:        conf.action,
            data:       $elements.serialize(),
            type:       'post',
            dataType:   'json',
            success: function(data) {
                if( true === data || 'string' === typeof(data) ) {
                    successCallback($elements, data);
                } else {
                    errorCallback($elements, data);
                    $form.find(':submit').attr('disabled', false);
                }
            }
        });
	}

    /* auto validate the form */
    function autoValidate($elements, delay) {
        var timer = null;
        var els = new Array();
        $elements.keyup(function(e) {

            /* ignore [ctrl], [shift], [tab], [alt] */
            if( 17 === e.keyCode || 0 === e.keyCode || 9 === e.keyCode
                || 18 === e.keyCode )
            {
                return;
            }
            
            element = this;

            /*
             * if timer already started, we gather the
             * elements for faster validation
             */
            if (timer) {
                window.clearTimeout(timer);

                if( -1 === els.indexOf(element, els) ) {
                    els.push(element);
                }
            } else {
                els.push(element);
            }

            /* on timer, we validate all the gathered elements */
            timer = window.setTimeout(function() {
                timer = null;
                validate($(els));
                els = new Array()
            }, delay);
        });
    }

	var $form  = $(this);

    if( 0 === $form.size() ) {
        return;
    }

    /* default options */
    var defaults = {
        action:         majisti.app.currentUrl,
        successUrl:     majisti.app.currentUrl,
        delay:          800,
        useHint:        true,
        successCallback: function() {
            valid = true;
            window.location.replace(conf.successUrl);
        }
    };

    /* global variables */
    var conf   = $.extend(defaults, options);
    var valid  = false;

    $form.css({position: 'relative'});
    
    /* position each hints */
    $form.find('.element').each(function() {
        $element = $(this);
        $input  = $element
                     .children(':input, textarea')
                     .not(':input[type=hidden], :submit');
        $hint   = $element.next();

        /* append hint if description not found */
        if( 0 === $hint.length && !$hint.is('submit') ) {
            $hint = $element
                .clone()
                .empty()
                .addClass('hint')
                .removeClass('element')
                .appendTo($element.parent())
            ;
        }

        if( 0 === $input.size() ) {
            return;
        }

        /* absolute position offset */
        left  = $input.offset().left - $form.offset().left;
        top   = $input.offset().top  - $form.offset().top;

        $hint.css({
            position: 'absolute',
            top:  top  - $hint.height() / 2,
            left: left + $input.width(),
            'z-index': 9999
        });
    });
    
    /*
     * If a bare form is submitted, it will validate
     * the entire form and return errors, highlighting the
     * elements and showing the first hint.
     * 
     * If errors are already present, it will show the hint
     * for the first one and focus on the element.
     */
    $form.submit(function() {
        /* form is valid */
        if( valid ) {
            return true;
        }

        $error = $form.find('.hint.error:first');

        if( $error.size() > 0 && !$error.prev().hasClass('captcha') ) {;
            $error.show();
            $('.element .error:first').focus();
            return false;
        }

        $currentElement.parent().parent().find('.hint').hide();

        $form.find(':submit').attr('disabled', true);
        validate($(this));

        return false;
    });

    /* onFocus and onBlur will toggle hint */
    var $currentElement = null;
    $form.find('.element').children().focus(function() {
        $currentElement = $(this);
        showHint($currentElement);
    });
    
    $form.find('.element').children().blur(function(){
        $(this).parent().parent().find('.hint').hide();
    });

    /* auto validate every time a key is pressed (using a delay) */
    autoValidate($form.find('.element').not('.captcha').children(), conf.delay);
};

})(jQuery);
