﻿

jQuery.fn.magnify = function(options) {

    var settings = {
        lensWidth: 100,
        lensHeight: 100,
        showEvent: 'mouseover',
        hideEvent: 'mouseout',
        stagePlacement: 'right',
        preload: true,
        loadingImage: '',
        stageCss: {},
        lensCss: {},
        onBeforeShow: function() { },
        onAfterShow: function() { },
        onBeforeHide: function() { },
        onAfterHide: function() { }
    };

    options ? jQuery.extend(settings, options) : null;

    return this.each(function() {

        var img = jQuery("img:first", this),
			smallimage = new Smallimage(img),
			a = jQuery(this),
			lens = new Lens(),
			largeimage = new Largeimage(a[0].href),
			largeimageloaded = false,
			smallimagedata = {},
			stage = null,
			scale = 1,
			running = false,
			loader = null,
			noimage = false,
			self = this;

        smallimage.loadimage();

        img.bind(settings.showEvent, activate);

        function activate(e) {
            if (!running) {
                running = true;
                settings.onBeforeShow.apply(smallimage, [settings]);
                if (!largeimage) { // a re-show
                    largeimage = new Largeimage(a[0].href);
                }
                lens.activate(e);
                lens.setposition(e);
                jQuery(lens.node)
					.bind('mousemove', lens.setposition)
					.bind(settings.hideEvent, deactivate);
                if (largeimageloaded) { // preload true
                    stage = new Stage();
                    stage.activate();
                }
                else {
                    // preload false or re-show
                    largeimage.loadimage();
                }
                settings.onAfterShow.apply(smallimage, [settings]);
                a[0].blur();
                return false;
            }
            else deactivate(e);
        }

        function deactivate(e) {
            settings.onBeforeHide.apply(self, [settings]);
            jQuery(lens.node).unbind('mousemove').unbind('mouseover').remove();
            jQuery(stage.node).remove();
            jQuery(largeimage.node).remove();
            largeimage = null;
            largeimageloaded = false;
            settings.onAfterHide.apply(self, [settings]);
            running = false;

            // a brutal hack to prevent the annoying re-firing of mouseover 
            // when mouseout over top of small image.
            // unbind and rebind show event after 50ms.
            img.unbind(settings.showEvent);
            var s = setTimeout(function() { img.bind(settings.showEvent, activate); }, 50);
        }

        /********* classes: Smallimage, Largeimage, Lens, Stage, Loader ***********/

        function Smallimage(image) {
            this.node = image[0];

            this.loadimage = function() {
                this.node.src = img[0].src;
            };

            this.node.onload = function() {
                smallimagedata.w = jQuery(this).width();
                smallimagedata.h = jQuery(this).height();
                smallimagedata.pos = jQuery(this).offset();
                smallimagedata.pos.r = smallimagedata.w + smallimagedata.pos.left;
                smallimagedata.pos.b = smallimagedata.h + smallimagedata.pos.top;

                if (settings.preload) {
                    largeimage.loadimage();
                }
            };

            return this;
        }

        function Largeimage(url) {

            this.url = url;
            this.node = new Image();


            this.loadimage = function() {
                if (!this.node)
                    this.node = new Image();
                this.node.style.position = 'absolute';
                this.node.style.display = 'none';
                this.node.style.left = '-5000px';
                loader = new Loader();
                document.body.appendChild(this.node);

                this.node.src = this.url; // fires off async
            };

            this.node.onload = function() {
                // context is largeimage.node

                largeimageloaded = true;
                // set to block, get width & height, then hide again
                this.style.display = 'block';
                var w = jQuery(this).width(),
					h = jQuery(this).height();
                this.style.display = 'none';
                scale = (w / smallimagedata.w + h / smallimagedata.h) / 2;

                jQuery(loader.node).remove();

                if (running) { // loaded on show event, so must activate stage
                    stage = new Stage();
                    stage.activate();
                }
            };

            this.node.onerror = function() {
                jQuery(loader.node).remove();
                noimage = true;
                if (running) {
                    stage = new Stage();
                    stage.activate();
                }
            };
            return this;
        }

        Largeimage.prototype.setposition = function() {
            this.node.style.left = Math.ceil(-scale * parseInt(lens.getoffset().left)) + 'px';
            this.node.style.top = Math.ceil(-scale * parseInt(lens.getoffset().top)) + 'px';
        };

        function Lens() {
            this.node = document.createElement("div");
            jQuery(this.node)
				.css({
				    width: settings.lensWidth + 'px',
				    height: settings.lensHeight + 'px',
				    backgroundColor: 'white',
				    opacity: 0.6,
				    position: 'absolute',
				    border: '1px dashed #bbbbbb',
				    zIndex: 1000,
				    cursor: 'crosshair'
				})
				.css(settings.lensCss);

            return this;
        }

        Lens.prototype.activate = function() {
            document.body.appendChild(this.node);
            this.node.style.display = 'block';
        };

        Lens.prototype.setposition = function(e) {

            var self = e.type == 'mousemove'
					? this
					: this.node,
				lensleft = e.pageX - settings.lensWidth / 2,
				lenstop = e.pageY - settings.lensHeight / 2,
				realwidth = parseInt(self.style.width)
								+ parseInt(self.style.borderLeftWidth)
								+ parseInt(self.style.borderRightWidth),
				realheight = parseInt(self.style.height)
								+ parseInt(self.style.borderTopWidth)
								+ parseInt(self.style.borderBottomWidth),
				incorner = incorner();

            if (!incorner) {
                // use 'self' so the context is right whether setting mouseover or on mousemove
                if (againstleft()) {
                    self.style.top = lenstop + 'px';
                    self.style.left = smallimagedata.pos.left + 'px';
                }
                else if (againstright()) {
                    //console.log(self.style.width);
                    self.style.top = lenstop + 'px';
                    self.style.left = smallimagedata.pos.r - realwidth + 'px';
                }
                else if (againsttop()) {
                    self.style.left = lensleft + 'px';
                    self.style.top = smallimagedata.pos.top + 'px';
                }
                else if (againstbottom()) {
                    self.style.left = lensleft + 'px';
                    self.style.top = smallimagedata.pos.b - realheight + 'px';
                }
                else {
                    self.style.top = lenstop + 'px';
                    self.style.left = lensleft + 'px';
                }
            }
            else {
                self.style.top = incorner == 'topleft' || incorner == 'topright'
					? smallimagedata.pos.top + 'px'
					: smallimagedata.pos.b - realheight + 'px';
                self.style.left = incorner == 'topleft' || incorner == 'bottomleft'
					? smallimagedata.pos.left + 'px'
					: smallimagedata.pos.r - realwidth + 'px';
            }

            largeimage.setposition();

            function againstleft() {
                return lensleft < smallimagedata.pos.left;
            }

            function againstright() {
                return lensleft + realwidth > smallimagedata.pos.r;
            }

            function againsttop() {
                return lenstop < smallimagedata.pos.top;
            }

            function againstbottom() {
                return lenstop + realheight > smallimagedata.pos.b;
            }

            function incorner() {
                if (againstbottom() && againstright())
                    return 'bottomright';
                else if (againstbottom() && againstleft())
                    return 'bottomleft';
                else if (againsttop() && againstright())
                    return 'topright';
                else if (againsttop() && againstleft())
                    return 'topleft';
                else
                    return false;
            }
        };

        Lens.prototype.getoffset = function() {
            var o = {};
            o.left = parseInt(this.node.style.left) - parseInt(smallimagedata.pos.left) + 'px';
            o.top = parseInt(this.node.style.top) - parseInt(smallimagedata.pos.top) + 'px';
            return o;
        };

        function Stage() {
            this.node = document.createElement("div");
            jQuery(this.node)
				.css({
				    position: 'absolute',
				    width: Math.round(settings.lensWidth * scale) + 'px',
				    height: Math.round(settings.lensHeight * scale) + 'px',

				    zIndex: 2000,
				    overflow: 'hidden',
				    border: '1px solid #999999',
				    display: 'none',
				    backgroundColor: 'white'
				})
				.css(settings.stageCss);

            this.realheight = parseInt(this.node.style.height)
								+ parseInt(this.node.style.borderTopWidth)
								+ parseInt(this.node.style.borderBottomWidth);
            this.realwidth = parseInt(this.node.style.width)
								+ parseInt(this.node.style.borderLeftWidth)
								+ parseInt(this.node.style.borderRightWidth);
            var st = jQuery.browser.safari
						? document.body.scrollTop
						: document.documentElement.scrollTop;
            var screenbottom = document.documentElement.clientHeight + st;
            this.node.style.top = smallimagedata.pos.top + this.realheight > screenbottom
									? screenbottom - this.realheight - 10 + 'px'
									: smallimagedata.pos.top + 'px';
            this.node.style.left = settings.stagePlacement == 'right'
							? smallimagedata.pos.r + 10 + 'px'
							: smallimagedata.pos.left - this.realwidth - 10 + 'px';
            return this;
        }

        Stage.prototype.activate = function() {
            if (noimage)
                this.node.appendChild(document.createTextNode('Large Image Not Found.'));
            else {
                if (!this.node.firstChild)
                    this.node.appendChild(largeimage.node);
                largeimage.node.style.display = 'block';
                largeimage.setposition();
            }
            document.body.appendChild(this.node);
            jQuery(this.node).fadeIn(300);
        };

        function Loader() {
            this.node = settings.loadingImage
				? new Image()
				: document.createElement("div");
            jQuery(this.node)
				.appendTo("body")
				.css({
				    top: smallimagedata.pos.top + 10 + 'px',
				    left: smallimagedata.pos.left + 10 + 'px',
				    position: 'absolute'
				});
            settings.loadingImage
				? this.node.src = settings.loadingImage
				: this.node.appendChild(document.createTextNode('Loading...'));
            return this;
        }
    });
};


