(function ($) {
	var instances = [];
	var instance = function (container, settings, index) {
		// container
		container = $(container).addClass("zoomer-container").empty();
		var container_width		= container.width(), 
			container_height	= container.height(), 
			images_container	= $("<div class='zoomer-images'></div>"),
			fit_zoom = Math.floor( Math.min(container_width, container_height) / settings.tile_size ) - 1,
			// current state
			state = { 
				zoom	: (settings.initial_zoom !== false) ? settings.initial_zoom : fit_zoom, 
				left	: 0, 
				top		: 0 
			},
			grid_container			= $("<div class='zoomer-grid'></div>"),
			navigation_container	= $("<div class='zoomer-navigator'></div>"),
			navigation_marker		= $("<div class='zoomer-navigator-marker'></div>"),
			tags_container			= $("<div class='zoomer-tags'></div>");

		return {
			init : function () {
				var _this = this;
				this.setup_events();
				this.check_max_zoom(function() { _this.prepare_view(); }, 1);
			},
			destroy : function () {
				if(settings.callback.destroy) settings.callback.destroy.call(window, state, settings);
				$.removeData(container, "zoomer-instance-id");
				container.empty().unbind(".zoomer").removeClass("zoomer-container");
				instances[index] = null;
				delete instances[index];
			},
			get_image : function (zoom, x, y) {
				return settings.tiles.replace("%z", zoom).replace("%x", x).replace("%y", y);
			},
			setup_events : function () {
				var data = {};
				var _this = this;
				container
					.bind("mousedown.zoomer", function (event) {
						if($(event.target).is(".zoomer-navigator, .zoomer-navigator-marker")) return;

						$.zoomer.drag = index;
						data.init_x = event.pageX;
						data.init_y = event.pageY;

						if(settings.tags.allow_move && $(event.target).is(".zoomer-tag, .zoomer-tag-hover")) {
							if(settings.mode != "remove") {
								data.tag = $(event.target);
								data.mode = "tag_move";
								data.init_l = parseInt(data.tag.css("left"));
								data.init_t = parseInt(data.tag.css("top"));
							}
							else { $.zoomer.drag = -1; }
							event.preventDefault(); event.stopPropagation(); return false;
						}
						if(settings.tags.allow_move && $(event.target).is(".zoomer-tag-resize")) {
							data.tag = $(event.target).parent().addClass("zoomer-tag-resizing");
							data.mode = "tag_resize";
							var o = tags_container.offset(); 
							data.init_l = (event.pageX - o.left);
							data.init_t = (event.pageY - o.top);
							data.init_w = data.tag.width();
							data.init_h = data.tag.height();
							event.preventDefault(); event.stopPropagation(); return false;
						}

						switch(settings.mode) {
							case "zoom":
							case "draw":
								var o = tags_container.offset(), l = (event.pageX - o.left), t = (event.pageY - o.top);

								if(l > 0 && l < settings.tile_size * (state.zoom + 1) && t > 0 && t < settings.tile_size * (state.zoom + 1)) {
									data.init_l = l;
									data.init_t = t;
									data.init_w = 0;
									data.init_h = 0;
									data.tag = $("<div id='tmp' class='zoomer-tag-hover' />").css({
										'width'  : '1px',
										'height' : '1px',
										'left'   : data.init_l + 'px',
										'top'    : data.init_t + 'px'
									}).appendTo(tags_container).show();
									data.mode = "tag_create";
								}
								if(settings.mode == "zoom") { data.tag.addClass("zoomer-loupe").css("opacity","0.3").hide(); data.mode = "loupe"; }
								break;
							case "move":
							default:
								data.mode = "move";
								data.st_lft = state.left;
								data.st_top = state.top;
								break;
						}
						event.preventDefault(); event.stopPropagation(); return false;
					})
					.bind("mousemove.zoomer", function (event) {
						if($.zoomer.drag != index) return;

						switch(data.mode) {
							case "tag_move":
								var l = data.init_l - (data.init_x - event.pageX), t = data.init_t - (data.init_y - event.pageY), s = settings.tile_size * (state.zoom + 1);
								l = Math.max(0, l); t = Math.max(0,t); l = Math.min(l, s); t = Math.min(t, s);
								data.tag.css({ 'left' : l + "px", 'top' : t + "px" });
								break;
							case "tag_resize":
							case "tag_create":
							case "loupe":
								if(data.mode == "loupe" && (Math.abs(event.pageX - data.init_x) > 5 || Math.abs(event.pageY - data.init_y) > 5)) {
									data.tag.show();
								}
								var css = { };
								if(event.pageX + data.init_w < data.init_x) { 
									css.left = data.init_l - (data.init_x - event.pageX);
									css.width = (data.init_x - event.pageX - data.init_w);
								}
								else { 
									css.left = parseInt(data.tag.css("left"));
									css.width = data.init_w + (event.pageX - data.init_x); 
								}
								if(event.pageY + data.init_h < data.init_y) { 
									css.top = data.init_t - (data.init_y - event.pageY);
									css.height = (data.init_y - event.pageY - data.init_h);
								}
								else { 
									css.top = parseInt(data.tag.css("top"));
									css.height = data.init_h + (event.pageY - data.init_y); 
								}
								css.width = Math.max(1,css.width);
								css.height = Math.max(1,css.height);
								if(css.left < 0) { css.width = (css.width + css.left); css.left = 0; }
								if(css.top < 0) { css.height = (css.height + css.top); css.top = 0; }
								var t = settings.tile_size * (state.zoom + 1);
								if(css.left + css.width > t) css.width = css.width - (css.left + css.width - t);
								if(css.top + css.height > t) css.height = css.height - (css.top + css.height - t);
								css.width	+= 'px';
								css.height	+= 'px';
								css.left	+= 'px';
								css.top		+= 'px';
								data.tag.css(css);
								break;
							case "move":
							default:
								_this.set_view(state.zoom, data.st_lft - (data.init_x - event.pageX), data.st_top - (data.init_y - event.pageY) );
								break;
						}
						event.preventDefault();
						event.stopPropagation();
						return false;
					})
					.bind("mouseup.zoomer", function (event) {
						if($.zoomer.drag != index) return;

						switch(data.mode) {
							case "tag_move":
								var id = data.tag.attr("id").replace("zoomer-" + index + "-tag-", "");
								settings.tags.data[id].zoom = state.zoom;
								settings.tags.data[id].left = data.init_l - (data.init_x - event.pageX);
								settings.tags.data[id].top  = data.init_t - (data.init_y - event.pageY);
								settings.tags.data[id].width = data.tag.width();
								settings.tags.data[id].height = data.tag.height();
								break;
							case "tag_resize":
								var id = data.tag.removeClass("zoomer-tag-resizing").attr("id").replace("zoomer-" + index + "-tag-", "");
								settings.tags.data[id].zoom = state.zoom;
								settings.tags.data[id].left = parseInt(data.tag.css("left"));
								settings.tags.data[id].top = parseInt(data.tag.css("top"));
								settings.tags.data[id].width = data.tag.width();
								settings.tags.data[id].height = data.tag.height();
								break;
							case "tag_create":
								var id, txt = prompt("Enter tag description");
								if(txt) {
									do { 
										id = "tag" + Math.random();
									} while(typeof settings.tags.data[id] != "undefined");
									settings.tags.data[id] = {};
									settings.tags.data[id].zoom = state.zoom;
									settings.tags.data[id].left = parseInt(data.tag.css("left"));
									settings.tags.data[id].top = parseInt(data.tag.css("top"));
									settings.tags.data[id].width = data.tag.width();
									settings.tags.data[id].height = data.tag.height();
									settings.tags.data[id].data = txt;
								}
								data.tag.remove();
								_this.refresh_tags();
								break;
							case "loupe":
								var o  = container.offset(), 
									cl = (parseInt(images_container.css("left")) - (data.init_x - o.left)) + (state.zoom + 1) * settings.tile_size / 2, 
									ct = (parseInt(images_container.css("top"))  - (data.init_y - o.top )) + (state.zoom + 1) * settings.tile_size / 2,
									w  = data.tag.width(),
									h  = data.tag.height();

								cl = (data.init_x > event.pageX) ? cl + w/2 + 4 : cl - w/2 + 4;
								ct = (data.init_y > event.pageY) ? ct + h/2 + 4 : ct - h/2 + 4;
								_this.set_view(false, cl, ct);
								if(data.tag.is(":hidden")) _this.zoom_to(state.zoom + 2);
								else {
									var side = container_width/w < container_height/h ? w : h;
									side = settings.tile_size * (state.zoom + 1) / side;
									_this.zoom_to(Math.floor(state.zoom * side));
								}
								data.tag.remove();
								break;
						}
						data = {};
					})
					.bind("dblclick.zoomer", function (event) {
						if($(event.target).is(".zoomer-navigator, .zoomer-navigator-marker")) return;
						var o  = container.offset(), 
							cl = (parseInt(images_container.css("left")) - (event.pageX - o.left)) + (state.zoom + 1) * settings.tile_size / 2, 
							ct = (parseInt(images_container.css("top"))  - (event.pageY - o.top )) + (state.zoom + 1) * settings.tile_size / 2;

						_this.set_view(false, cl, ct);
						_this.zoom_to(state.zoom + 2);
						event.preventDefault();
						event.stopPropagation();
						return false;
					});
			},
			check_max_zoom : function (callback, zoom) {
				if(settings.max_zoom !== false) return callback.call();
				var _this = this; zoom = (zoom) ? zoom : 1;
				$.ajax({ type : "HEAD", url : this.get_image(zoom, 0, 0), 
					success : function () { _this.check_max_zoom(callback, zoom + 1); },
					error : function () { settings.max_zoom = zoom - 1; callback.call();  }
				});
			},
			prepare_view : function () {
				// center images container
				images_container.css({ 'width' : (settings.max_zoom + 1) * settings.tile_size + 'px', 'height' : (settings.max_zoom + 1) * settings.tile_size + 'px', 'left' : (container_width/2 - state.left) + 'px', 'top'  : (container_height/2 - state.top)  + 'px' });
				// fill images container
				var html = "";
				for(var i = 0; i <= settings.max_zoom; i ++) {
					for(var j = 0; j <= settings.max_zoom; j ++) {
						html += "<div class='zoomer-tile zoomer-tile-" + i + "-" + j +"' style='width:" + settings.tile_size + "px; height:" + settings.tile_size + "px;'></div>";
					}
				}
				images_container.html(html);
				container.append(images_container);

				var gc = Math.ceil(container_width/settings.grid.size);
				var gr = Math.ceil(container_height/settings.grid.size);
				grid_container.css({ 'borderColor' : settings.grid.color, 'left' : (container_width - gc * settings.grid.size)/2 + 'px', 'top' : (container_height - gr * settings.grid.size)/2 + 'px', 'width' : gc * settings.grid.size + 'px', 'height' : gr * settings.grid.size + 'px', 'opacity' : settings.grid.opacity });
				html = "";
				for(var i = 0; i <= gc * gr; i ++) {
					html += "<div class='zoomer-grid-tile' style='width:" + (settings.grid.size - 1) + "px; height:" + (settings.grid.size - 1) + "px; border-color:" + settings.grid.color + "'></div>";
				}
				grid_container.html(html);
				container.append(grid_container);
				if(settings.grid.show) { this.show_grid(); };

				tags_container.css({ 'width' : (settings.max_zoom + 1) * settings.tile_size + 'px', 'height' : (settings.max_zoom + 1) * settings.tile_size + 'px', 'left' : (container_width/2 - state.left) + 'px', 'top'  : (container_height/2 - state.top)  + 'px' });
				container.append(tags_container);
				if(settings.tags.show !== false) { this.refresh_tags(); if(settings.tags.show == "compact") this.compact_tags(); else this.show_tags(); }

				// navigator setup
				var _this = this;
				navigation_container
					.css({ 'width' : settings.navigation.size + 'px', 'height' : settings.navigation.size + 'px' }).append('<img src="' + this.get_image(0,0,0) + '" alt="" />')
					.bind("click", function (event) {
						var o  = navigation_container.offset(), 
							cl = event.pageX - o.left - settings.navigation.size/2, 
							ct = event.pageY - o.top  - settings.navigation.size/2,
							cf = settings.navigation.size / ( (state.zoom + 1) * settings.tile_size );
						_this.set_view(state.zoom, -cl / cf, -ct / cf);
					})
					//.bind("mousedown", function (event) { event.stopImmediatePropagation(); })
					.append(navigation_marker.css({ "background" : settings.navigation.color, "opacity" : settings.navigation.opacity }))
					.appendTo(container);
				if(settings.navigation.show) this.show_navigator();

				if(settings.callback.ready) settings.callback.ready.call(window, state, settings);
				this.set_mode(settings.mode);
				this.set_view();
			},
			set_view : function (zoom, left, top) {
				// determine zoom
				if(typeof zoom == "undefined" || zoom === false) zoom = state.zoom; if(zoom > settings.max_zoom) zoom = settings.max_zoom; if(zoom < 0) zoom = 0;
				var zoom_changed = (zoom != state.zoom);

				if(typeof left == "undefined" || left === false) {
					left = ( state.left / (settings.tile_size * (state.zoom + 1) ) ) * (settings.tile_size * (zoom + 1) );
				}
				if(typeof top == "undefined" || top === false) {
					top = ( state.top / (settings.tile_size * (state.zoom + 1) ) ) * (settings.tile_size * (zoom + 1) );
				}
				// set new values and save state
				images_container.css({ 'left' : container_width/2 - (zoom + 1)* settings.tile_size/2 + left + 'px', 'top' : container_height/2 - (zoom + 1) * settings.tile_size/2 + top + 'px' });
				tags_container.css({ 'left' : container_width/2 - (zoom + 1)* settings.tile_size/2 + left + 'px', 'top' : container_height/2 - (zoom + 1) * settings.tile_size/2 + top + 'px' });
				state = { 'zoom' : zoom, 'left' : left, 'top' : top };

				if(zoom_changed) this.refresh_tags();

				if(settings.navigation.show) {
					var coef = settings.navigation.size / (settings.tile_size * (zoom + 1)),
						nw = container_width * coef, 
						nh = container_height * coef, 
						nl = nw/2 - settings.navigation.size/2 + left * coef, 
						nt = nh/2 - settings.navigation.size/2 + top * coef;
					navigation_marker.css({ "left" : -nl + 'px', 'top' : -nt + 'px', 'width' : nw + 'px', 'height' : nh + 'px' });
				}

				// TODO: optimize this? maybe replace html with blank?
				if(zoom_changed) images_container.children(".zoomer-tile").removeClass("loading").css("background","transparent");

				// TODO: optimize css(), maybe use style instead?
				var sx = 0, sy = 0, ex = state.zoom, ey = state.zoom, zl = parseInt(images_container.css("left")), zt = parseInt(images_container.css("top"));
				if(zl < 0) sx = Math.floor(zl/settings.tile_size*-1);
				if(zt < 0) sy = Math.floor(zt/settings.tile_size*-1);
				ex = sx + Math.ceil(container_width/settings.tile_size);
				ey = sy + Math.ceil(container_height/settings.tile_size);

				var ic = images_container.get(0);
				for(var i = sy; i <= ey; i ++) {
					for(var j = sx; j <= ex; j ++) {
						var tmp = $(ic.childNodes[i * (settings.max_zoom + 1) + j]);
						if(!tmp.hasClass("loading")) tmp.addClass("loading").css("background-image","url(" + this.get_image(state.zoom, j, i) + ")");
					}
				}
				if(settings.callback.view_change) settings.callback.view_change.apply(window, [state, zoom_changed]);
			},

			refresh_tags : function () {
				if(settings.tags.show) {
					tags_container.empty();
					var _this = this, internal = settings.tags.allow_move ? "<span class='zoomer-tag-resize'></span>" : "";
					$.each(settings.tags.data, function (i, val) {
						if(val.zoom_range && (state.zoom < val.zoom_range[0] || state.zoom > val.zoom_range[1]) ) return false;
						var coef = ((state.zoom + 1) * settings.tile_size) / ((val.zoom + 1) * settings.tile_size);

						$("<div class='zoomer-tag' id='zoomer-" + index + "-tag-" + i + "'><p>" + val.data + "</p>" + internal + "</div>")
							.bind("mouseover", function () { $(this).addClass("zoomer-tag-hover").removeClass("zoomer-tag"); })
							.bind("mouseout", function () { if($.zoomer.drag == index && $(this).hasClass("zoomer-tag-resizing")) return; $(this).addClass("zoomer-tag").removeClass("zoomer-tag-hover"); if($.zoomer.drag != index) $(this).removeClass("zoomer-tag-resizing"); })
							.css({
								width	: Math.max(Math.floor(val.width  * coef), 1) + 'px',
								height	: Math.max(Math.floor(val.height * coef), 1) + 'px',
								left	: Math.floor(val.left * coef) + 'px',
								top		: Math.floor(val.top  * coef) + 'px'
							}).appendTo(tags_container)[ (val.width  * coef >= container_width || val.height * coef >= container_height) ? "hide" : "show" ]()
								.bind("click", function () { _this.remove_tag(this); });
					});
				}
			},
			remove_tag : function (obj) {
				if(settings.mode != "remove" || !confirm("Are you sure you want to remove this tag?")) return;
				var id = obj.id.replace("zoomer-" + index + "-tag-","");
				settings.tags.data[id] = null;
				delete settings.tags.data[id];
				this.refresh_tags();
			},

			// helper functions
			center_view	: function ( ) { this.set_view(state.zoom, 0, 0); },
			zoom_in		: function ( ) { this.set_view(state.zoom + 1); },
			zoom_out	: function ( ) { this.set_view(state.zoom - 1); },
			zoom_to		: function (i) { this.set_view(i); },
			zoom_fit	: function ( ) { this.set_view(fit_zoom, 0, 0); },
			zoom_full	: function ( ) { this.set_view(settings.max_zoom); },
			
			toggle_grid	: function ( ) { 
				if(!settings.grid.show) this.show_grid(); 
				else this.hide_grid(); 
			},
			show_grid	: function ( ) { 
				grid_container.show(); 
				settings.grid.show = true;  
				if(settings.callback.grid_change) settings.callback.grid_change.call(window, settings.grid.show);
			},
			hide_grid	: function ( ) { 
				grid_container.hide(); 
				settings.grid.show = false; 
				if(settings.callback.grid_change) settings.callback.grid_change.call(window, settings.grid.show);
			},

			toggle_navigator	: function ( ) { 
				if(!settings.navigation.show) this.show_navigator(); else this.hide_navigator(); 
			},
			show_navigator		: function ( ) { 
				navigation_container.show(); 
				settings.navigation.show = true;  
				if(settings.callback.navigator_change) settings.callback.navigator_change.call(window, settings.navigation.show);
			},
			hide_navigator		: function ( ) { 
				navigation_container.hide(); 
				settings.navigation.show = false; 
				if(settings.callback.navigator_change) settings.callback.navigator_change.call(window, settings.navigation.show);
			},

			toggle_tags			: function ( ) { 
				switch(settings.tags.show) {
					case (true):
						this.compact_tags(); 
						break;
					case "compact":
						this.hide_tags();
						break;
					default:
						this.show_tags();
						break;
				}
			},
			show_tags			: function ( ) { 
				tags_container.removeClass("zoomer-tag-compact").show(); 
				settings.tags.show = true;  
				if(settings.callback.tags_change) settings.callback.tags_change.call(window, settings.tags.show);
			},
			compact_tags		: function ( ) { 
				settings.tags.show = "compact";
				tags_container.addClass("zoomer-tag-compact").show(); 
				settings.tags.compact = true; 
				if(settings.callback.tags_change) settings.callback.tags_change.call(window, settings.tags.show);
			},
			hide_tags			: function ( ) { 
				if(settings.mode == "draw") this.set_mode("move");
				tags_container.removeClass("zoomer-tag-compact").hide();  
				settings.tags.show = false; 
				if(settings.callback.tags_change) settings.callback.tags_change.call(window, settings.tags.show);
			},

			set_mode			: function (mode) {
				if(mode == "draw" && settings.tags.allow_create == false) return;
				settings.mode = mode;
				if(mode == "draw" && !settings.tags.show) this.show_tags();
				if(settings.callback.mode_change) settings.callback.mode_change.call(window, mode);
			}
		};
	};
	$.zoomer = {
		prepare : function (container, opts) {
			var new_index = parseInt(instances.push({})) - 1;
			$.data(container, "zoomer-instance-id", new_index);
			instances[new_index] = new instance(container, $.extend(true, {}, $.zoomer.defaults, opts), new_index);
			instances[new_index].init();
			return instances[new_index];
		},
		defaults : {
			max_zoom	: false, 
			initial_zoom: false,
			tiles		: "tiles/tile-%z-%x-%y.png",
			tile_size	: 256,
			tags		: { 
				show : true,
				allow_create : true,
				allow_move : true,
				allow_remove : false,
				data : { }
			},
			grid		: {
				show : true,
				size : 64,
				color : 'silver',
				opacity : '0.2'
			},
			navigation	: {
				show : true,
				size : 128,
				color : "red",
				opacity : 0.5
			},
			callback : {
				ready		: function () { }, 
				view_change	: function () { },

				grid_change			: function (state) { },
				navigator_change	: function (state) { },
				tags_change			: function (state) { },
				tags_compact		: function (state) { }
			}
		},
		drag : -1
	};
	$.fn.zoomer = function (opts) {
		var _args = $.makeArray(arguments);
		return this.each(function () {
			if(typeof opts == "string") {
				var instance_id = $.data(this, "zoomer-instance-id");
				// but only if instance exists
				if(instances[instance_id] && $.isFunction(instances[instance_id][opts])) {
					// remove the first argument (the function name string)
					_args.shift();
					instances[instance_id][opts].apply(instances[instance_id], _args);
				}
			}
			else {
				var instance_id = $.data(this, "zoomer-instance-id");
				if(typeof instance_id != "undefined" && instances[instance_id]) instances[instance_id].destroy();
				$.zoomer.prepare(this, opts);
			}
		});
	};
	// drag & css append
	$(function () {
		$(document).bind("mouseup.zoomer", function () { $.zoomer.drag = -1; });
		var tmp = document.createElement("style"), 
			str =	'.zoomer-container { position:relative; overflow:hidden; cursor:pointer; } ' + 
					'.zoomer-images { position:absolute; margin:0; padding:0; } ' + 
					'.zoomer-tile { margin:0; padding:0; float:left; } ' + 
					'.zoomer-navigator { margin:0; padding:0; position:absolute; bottom:5px; right:5px; /*border:2px solid gray; background:black;*/ overflow:hidden; cursor:crosshair; display:none; } ' + 
					'.zoomer-navigator img { margin:0; padding:0; display:block; width:100%; height:100%; } ' + 
					'.zoomer-navigator-marker { position:absolute; margin:0; padding:0; } ' + 
					'.zoomer-grid { margin:0; padding:0; position:absolute; border-width:1px 0 0 1px; border-style:solid; display:none; } ' + 
					'.zoomer-grid-tile { margin:0; padding:0; float:left; border-width:0 1px 1px 0; border-style:solid; } ' + 
					'.zoomer-tags { position:absolute; margin:0; padding:0; display:none; } ' + 
					'.zoomer-tag { position:absolute; display:none; border:2px solid orange; z-index:10; } ' + 
					'.zoomer-tag-resize { position:absolute; display:none; width:6px; height:6px; background:black; left:100%; top:100%; margin-top:-2px; margin-left:-2px; z-index:12; font-size:1px; line-height:1px; overflow:hidden; } ' + 
					'.zoomer-tag p { display:none; } ' + 
					'.zoomer-tag-hover, .zoomer-tag-resizing { position:absolute; display:none; border:2px solid red; z-index:20; } ' + 
					'.zoomer-loupe { border:2px dotted gray; background:silver; } ' + 
					'.zoomer-tag-hover p, .zoomer-tag-resizing p { position:absolute; width:288px; border:1px solid gray; background:#FDFFCB; left:50%; margin:0 auto 5px -150px; bottom:100%; padding:5px; display:block; text-align:center; -moz-border-radius:5px; line-height:14px; color:#333333; font-size:9px; } ' + 
					'.zoomer-tag-hover .zoomer-tag-resize, .zoomer-tag-resizing .zoomer-tag-resize { display:block; } ' + 
					'.zoomer-tag-compact .zoomer-tag { width:1px !important; height:1px !important; border-width:4px; } ';
			tmp.setAttribute('type','text/css');
		if(tmp.styleSheet) { document.getElementsByTagName("head")[0].appendChild(tmp); tmp.styleSheet.cssText = str; }
		else { tmp.appendChild(document.createTextNode(str)); document.getElementsByTagName("head")[0].appendChild(tmp); }
	});
})(jQuery);

// TODO: optional grid? canvas? grid_size setting?

