var debug = false;

$(document).ready(function() {
    $('[tagData]').cascade().bindHtml(true);
});

(function(jQuery) {
    var $ = jQuery.sub();

    jQuery.fn.cascade = function() {
        return $(this);
    };

    $.fn.extend({
        bindHtml: bindHtml,
        triggerCascade: triggerCascade,
        createEventX: createEventX
    });

    /** jQuery */ var context = new Context();
    /** jQuery */ var eventQueue = [];

    /** set handlers from html attributes */
    function bindHtml(/** Boolean */ cascade) {
        log('bindHtml() cascade=%s', cascade);
        context.clear();
        this.each(function() {
            for (var i = 0; i < this.attributes.length; i++) {
                var attrX = new AttrX(this.attributes[i]);
                if (attrX.isEventHandler) {
                    $(this).unbind(attrX.event);
                    $(this).bind(attrX.event, getBindHandler(cascade, attrX.handler, attrX.params));
                    context.push(attrX.event.toLowerCase(), this);
                }
            }
            $(this).data('_path', $(this).parents().andSelf().get());
        });
        log('context count:%d', context.count());
    }

    function getBindHandler(cascade, handlerName, handlerParams) {
        var handler = window[handlerName];
        if (!cascade) {
            return $.proxy(handler, this); // not uses handlerParams
        } else if (handler == undefined) {
            return function(e) {
                e.params = handlerParams;
                triggerCascade(createEventX(handlerName.toLowerCase(), this, e));
                return false;
            }
        } else {
            return function(e, p) {
                log('call %s %s', handlerName, handlerParams);
                var result = handler.call(this, e, handlerParams);
                if (typeof result == 'string') {
                    triggerCascade(createEventX(result.toLowerCase(), this, e));
                }
                return false;
            }
        }
    }

    /** trigger event to all document tag (in some context)  */
    function triggerCascade(/** Event */ event) {
        log('add event %s',event.type);
        eventQueue.push(event);
        if (eventQueue.length > 1) {
            return;
        }
        log('begin triggerCascade');
        while (eventQueue.length > 0) {
            /** Event */ var e = eventQueue[0];

            var subContext = context.get(e.type);
            if (subContext == undefined) {
                eventQueue.shift();
                continue;
            }
            for (i = 0; i < subContext.length; i++) {
                var tag = subContext[i];
                tag._position = comparePaths($(e.target).data('_path'), $(tag).data('_path'))
            }
            subContext.sort(function($1, $2) {
                return Position.compare($1._position, $2._position)
            });

            for (var i = 0; i < subContext.length; i++) {
                $.event.trigger(e, e.result, subContext[i]);
                if (e.isImmediatePropagationStopped()) {
                    break;
                }
            }
            eventQueue.shift();
        }
        log('end triggerCascade');
    }

    function createEventX(/**String*/name, /**HTMLElement*/target, /**Event*/e) {
        var event = $.Event(name);
        event.preventDefault();
        event.stopPropagation();
        event.target = target;
        event.resultX = e?e.resultX:{};
        event.params = e?e.params:{};
        event.tagData = e?e.tagData:{};
        // log('createEventX() name=%s,target=%s', event.type, event.target);
        return event;
    }

    /** Class extract event-handler data from html elements */
    function AttrX(/**Attr*/attr) {
        this.name = attr.name;
        this.value = attr.value;

        this.isEventHandler = this.name.substring(0, 3) == 'on_';
        if (this.isEventHandler) {
            this.event = this.name.substring(3).toLowerCase();

            var rgx = /(\w+)\((.+)\)/.exec(this.value);
            if (rgx != null && rgx.length == 3) {
                this.handler = rgx[1];
                this.params = rgx[2].split(',');
            } else {
                this.handler = this.value;
            }
            //log('constructor AttrX() is=%s,event=%s,handler=%s,params=%s', this.isEventHandler, this.event, this.handler, this.params);
        }
    }

    /** Class contains cascade tag handlers */
    function Context() {
        this.bag = {};
    }

    Context.prototype.clear = function() {
        var countBefore = this.count();
        for (var events in this.bag) {
            var tags = this.bag[events];
            for (var i = tags.length - 1; i >= 0; i--) {
                var $root = $(tags[i]).parents('html');
                if ($root.length == 0) {
                    tags.splice(i, 1);
                }
            }
        }
        log('Context.clear() %d', countBefore - this.count());
    };
    Context.prototype.count = function() {
        var x = 0;
        for (var eventTags in this.bag) {
            x += this.bag[eventTags].length;
        }
        return x;
    };
    Context.prototype.get = function(event) {
        return this.bag[event];
    };
    Context.prototype.push = function(event, tag) {
        if (this.bag[event] == undefined) {
            this.bag[event] = [];
        }
        for (var i = 0; i < this.bag[event].length; i++) {
            if (this.bag[event][i] == tag) {
                return;
            }
        }
        this.bag[event].push(tag);
    };

    /** Class define relative position beetwin two elements */
    function Position(direction1, distance1, indirect_deep1) {
        this.direction = direction1;
        this.distance = distance1;
        this.indirect_deep = indirect_deep1;
    }

    Position.prototype.SELF = 0;
    Position.prototype.CHILD = 1;
    Position.prototype.PARENT = 2;
    Position.prototype.INDIRECT = 3;
    Position.compare = function(p1, p2) {
        if (p1.direction != p2.direction) {
            return p1.direction - p2.direction;
        } else if (p1.distance != p2.distance) {
            return p1.distance - p2.distance;
        } else {
            return p1.indirect_deep - p2.indirect_deep;
        }
    };
    function comparePaths($path0, $path2) {
        if ($path0 == undefined) {
            return new Position(0, 0, 0); //Position.SELF - event were generated without target
        }

        if (($path0 == undefined) || ($path2 == undefined))  {
            log('Using html tag with not set tagData=""');
        }
        var l = 0;
        while (l < $path0.length && l < $path2.length && $path0[l] == $path2[l]) {
            l++;
        }
        var distance = Math.abs($path0.length - l);
        if (distance == 0) {
            return new Position(0, 0, 0); //Position.SELF
        } else if (l == $path0.length) {
            return new Position(1, distance, 0); //Position.CHILD
        } else if (l == $path2.length) {
            return new Position(2, distance, 0); // Position.PARENT
        } else {
            return new Position(3, distance, $path2.length); //Position.INDIRECT
        }
    }
})($);

/* todo
 поддержка нескольких очередей событий
 сделать getBindHandler(cascade, handlerName) внешним интерфейсом, для добавления в каскад не html-обработчиков
 зачистить переводы DOM <-> $
 // для отмены продолжения исполнения событий другими тэгами используется isImmediatePropagationStopped (другой независимый аспект)
 */


function log() {
//    var h = arguments.callee.history;
//    h || (h = "");
    if (debug == false) return;
    var log = document.getElementById('log') || $('<div id="log" style="float:right;" onclick="$(this).empty()"><h3>Log:</h3></div>').appendTo($('body:first')).get(0);
    var now = new Date();
    var date = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds() + '.' + now.getMilliseconds();
    log.innerHTML += '<br/>' + date + ' ' + sprintf.apply(this, arguments);
    // arguments.callee.history +=
}

//log('handle ' + e.target.id + ':' + e.type + ' (' + this.id + ')');
//log('call- ' + e.target.id + ':' + e.type + ':' + subContext[i].id + '(' + eventQueue.length + ')');

/** обновление блока на странице */
function reload(/**Event*/e, param) {
    log('reload %s %s %s', typeof this, e.result, param);
    var tag = this;
    var $tag = $(this);
    // e.stopImmediatePropagation();
    var data = this.attributes['tagData'].value; //$tag.attr('tagData')
    data = e.tagData ? data + '&' + e.tagData : data;
    $.post(url, {reload: 'reload', data: data}, function (data) {
        $tag.html($('#' + tag.id, data).html());
        $('[tagData]', $tag).cascade().bindHtml(true);
        if (param && param[0]) {
            var e = $(document).cascade().createEventX(param[0].toLowerCase(), tag);
            $(document).cascade().triggerCascade(e);
        }
    });
    // return param[0];
}
function filterReload(tag, param, id) {
    var $tag = $(tag);
    var data1 = $tag.serialize();

    var rel_el = $('#' + id);
    var data = rel_el.attr('tagData');
    var a=1;
    $.post(url, {reload: 'reload', data1: data1, data:data}, function (data) {
        if(!data) return false;
        rel_el.html($('#' + id, data).html());
        $('[tagData]', rel_el).cascade().bindHtml(true);
        if (param && param[0]) {
            var e = $(document).cascade().createEventX(param[0].toLowerCase(), tag);
            $(document).cascade().triggerCascade(e);
        }
        return false;
    });
    return false;
}
    /** выполнение команды на сервере и генерация одноименного события на странице */
function remote(e, commands) {
    log('remote %s %s %s', typeof this, e.result, commands);
    var tag = this;
    var $tag = $(this);
    e.stopImmediatePropagation();
    var data = $tag.attr('tagData');
    if (this instanceof HTMLFormElement) {
        data = $tag.serialize();
    }
    $.post(url, {command: commands[0], data: data}, function (data) {
        var next_command = data.indexOf('Exception') < 0 ? commands[0] : commands[1];
        var e = $(document).cascade().createEventX(next_command.toLowerCase(), tag, data);
        $(document).cascade().triggerCascade(e);
    });
}

function submit(e, commands) {
    if (this instanceof HTMLFormElement) {
        if (commands != undefined) {
            this.action = url+"?command="+commands[0];
        }
        this.submit();
    }
}

//for sso 
//function remoteRedirect(e, commands) {
//    log('remote %s %s %s', typeof this, e.result, commands);
//    var tag = this;
//    var $tag = $(this);
//    e.stopImmediatePropagation();
//    var data = $tag.attr('tagData');
//    if (this instanceof HTMLFormElement) {
//        data = $tag.serialize();
//    }
//    $.post(url, {command: commands[0], data: data}, function (data) {
//        var next_command = data.indexOf('Exception') < 0 ? commands[0] : commands[1];
//        var e = $(document).cascade().createEventX(next_command.toLowerCase(), tag, data);
//        // $(document).cascade().triggerCascade(e);
//        location.href(data);
//    });
//}

/** поглощение события  */
function capture(e) {
    e.stopImmediatePropagation();
}

/** регенерация нового события с передачей параметров от текущего тэга  */
function carry(/**Event*/e, event) {
    e.stopImmediatePropagation();
    e.resultX = {
            data:this.attributes['tagData'].value,
            text:this.innerHTML
    };
    return event[0];
}

function fillForm(e) {
    e.stopImmediatePropagation();
    this.text.value = e.resultX.text;
    $('[name=id]', this).get(0).value = e.resultX.data;
}

function js(e, params) {
    e.stopImmediatePropagation();
    var next_command = eval(params[0]) ? params[1] : params[2];
    if (next_command != undefined) {
        var e = $(document).cascade().createEventX(next_command.toLowerCase(), this);
        $(document).cascade().triggerCascade(e);
    }
}

function clear(e) {
    this.reset();
}

function show(e) {
    $(this).css('display', 'block');
}

function showStop(e) {
    e.stopImmediatePropagation();
    $(this).css('display', 'block');
}

function hide(e) {
    $(this).css('display', 'none');
}

function tabswitch(e, params) {
    if (e && e.params && params && e.params[0] == params[0]) {
        $(this).addClass('active');
    } else {
        $(this).removeClass('active');
    }
}

function pageswitch(e, params) {
    var anchor = $(this).attr('href');
    e.tagData = e.tagData ? e.tagData + '&' : '';
    e.tagData += 'attrFrameTag.current=' + params[0];
    eventName = 'PageSwitch';
    var e = $(document).cascade().createEventX(eventName.toLowerCase(), this, e);
    $(document).cascade().triggerCascade(e);
    if(anchor != null && anchor!="")
        document.location.href=anchor;
}

//function activate(e, param) {
//    $(this).addClass('active');
////    if (param && param[0]) {
////        var e = $(document).cascade().createEventX(param[0].toLowerCase(), this);
////        $(document).cascade().triggerCascade(e);
////    }
//}
//
//function deactivate(e, param) {
//    $(this).removeClass('active');
////    if (param && param[0]) {
////        var e = $(document).cascade().createEventX(param[0].toLowerCase(), this);
////        $(document).cascade().triggerCascade(e);
////    }
//}

//function sso(e) {
//    $.post(url, {command: commands[0], data: data}, function (data) {
//        var next_command = data.indexOf('Exception') < 0 ? commands[0] : commands[1];
//        var e = $(document).cascade().createEventX(next_command.toLowerCase(), tag, data);
//        $(document).cascade().triggerCascade(e);
//    });
//}

function sprintf( ) {	// Return a formatted string
	// 
	// +   original by: Ash Searle (http://hexmen.com/blog/)
	// + namespaced by: Michael White (http://crestidg.com)

	var regex = /%%|%(\d+\$)?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegEG])/g;
	var a = arguments, i = 0, format = a[i++];

	// pad()
	var pad = function(str, len, chr, leftJustify) {
		var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
		return leftJustify ? str + padding : padding + str;
	};

	// justify()
	var justify = function(value, prefix, leftJustify, minWidth, zeroPad) {
		var diff = minWidth - value.length;
		if (diff > 0) {
			if (leftJustify || !zeroPad) {
			value = pad(value, minWidth, ' ', leftJustify);
			} else {
			value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
			}
		}
		return value;
	};

	// formatBaseX()
	var formatBaseX = function(value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
		// Note: casts negative numbers to positive ones
		var number = value >>> 0;
		prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
		value = prefix + pad(number.toString(base), precision || 0, '0', false);
		return justify(value, prefix, leftJustify, minWidth, zeroPad);
	};

	// formatString()
	var formatString = function(value, leftJustify, minWidth, precision, zeroPad) {
		if (precision != null) {
			value = value.slice(0, precision);
		}
		return justify(value, '', leftJustify, minWidth, zeroPad);
	};

	// finalFormat()
	var doFormat = function(substring, valueIndex, flags, minWidth, _, precision, type) {
		if (substring == '%%') return '%';

		// parse flags
		var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false;
		for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) {
			case ' ': positivePrefix = ' '; break;
			case '+': positivePrefix = '+'; break;
			case '-': leftJustify = true; break;
			case '0': zeroPad = true; break;
			case '#': prefixBaseX = true; break;
		}

		// parameters may be null, undefined, empty-string or real valued
		// we want to ignore null, undefined and empty-string values
		if (!minWidth) {
			minWidth = 0;
		} else if (minWidth == '*') {
			minWidth = +a[i++];
		} else if (minWidth.charAt(0) == '*') {
			minWidth = +a[minWidth.slice(1, -1)];
		} else {
			minWidth = +minWidth;
		}

		// Note: undocumented perl feature:
		if (minWidth < 0) {
			minWidth = -minWidth;
			leftJustify = true;
		}

		if (!isFinite(minWidth)) {
			throw new Error('sprintf: (minimum-)width must be finite');
		}

		if (!precision) {
			precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
		} else if (precision == '*') {
			precision = +a[i++];
		} else if (precision.charAt(0) == '*') {
			precision = +a[precision.slice(1, -1)];
		} else {
			precision = +precision;
		}

		// grab value using valueIndex if required?
		var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];

		switch (type) {
			case 's': return formatString(String(value), leftJustify, minWidth, precision, zeroPad);
			case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
			case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
			case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'i':
			case 'd': {
						var number = parseInt(+value);
						var prefix = number < 0 ? '-' : positivePrefix;
						value = prefix + pad(String(Math.abs(number)), precision, '0', false);
						return justify(value, prefix, leftJustify, minWidth, zeroPad);
					}
			case 'e':
			case 'E':
			case 'f':
			case 'F':
			case 'g':
			case 'G':
						{
						var number = +value;
						var prefix = number < 0 ? '-' : positivePrefix;
						var method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
						var textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
						value = prefix + Math.abs(number)[method](precision);
						return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
					}
			default: return substring;
		}
	};

	return format.replace(regex, doFormat);
}
