// cross-browser thumbnail generator
// (c) Thomas Rosenau 2009
// usage:
//
// var tn = new TomsThumbnailer();
// tn.addThumbnail(someElement, preloadImage, showSizeTag, template);
// tn.addThumbnailsToAllLinks(someElementContainingHyperlinks, preloadImages, showSizeTags, template);
//
// all parameters but the first one are optional


function TomsThumbnailer(preload) {

    var PREFERREDTHUMBNAILPOS = {
        HORIZONTAL:  1, // >=0: right;  <0: left
        VERTICAL:  1    // >=0: bottom; <0: top
    };
    var PREFERREDSIZETAGPOS = {
        HORIZONTAL: -1, // >=0: right;  <0: left
        VERTICAL:  1    // >=0: bottom; <0: top
    };
    var SPACING = { TOP: 5, BOTTOM: 5, LEFT: 5, RIGHT: 5 }; // between thumbnail and viewport
    var CURSORMARGIN = { TOP: 10, BOTTOM: 25, LEFT: 15, RIGHT: 20 }; // between thumbnail and cursor hotspot

    var ie = /*@cc_on!@*/false;
    var ie5 = /*@cc_on@if(@_jscript_version < 5.6)!@end@*/false;
    var ie6 = /*@cc_on@if(@_jscript_version == 5.6)!@end@*/false;
    var ie7 = /*@cc_on@if(@_jscript_version == 5.7)!@end@*/false;

    var CONST = {
        CHECKERS: (ie5 || ie6 || ie7) ? 'url(http://thomas-rosenau.de/scripts/thumbnailer/checkers.gif)'
                                      : 'url(data:image/gif;base64,R0lGODlhBAAEAKECAMzMzP///wAAAAAAACH5BAEKAAIALAAAAAAEAAQAAAIGTACGqBkFADs=)',
        ERRORCURSOR: (ie5 || window.opera) ? 'auto' :
                    ie ? 'url(http://thomas-rosenau.de/scripts/thumbnailer/error.cur), auto' :
                    'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAoFJREFUOMt'+
                    '11M9rXGUUxvHPvXduJjdTY/NDYlsbRyeNiQTFARW7Coi0ULFamuDKTdXSbf0LFFy0IKhY1IqgUVDQRReCIIiiWF1IG03BWKmmFklj'+
                    'lcmkdNokk1wXvZNca3w27+K875dzzvPwRv6tEDtwM2o2VhEjaOLqRheCgNGYz4tMBFQycF5xxFiRLyLeQPd/ILgn4fRDrDxMo8SxD'+
                    'LYGCXmkzKk9NO9kMeRoHhYGjCacfoz0BOlnpPto9PBeyN0oxoyVOfUi6dekr5IOsFjgOPoiDIQcq1J9jmAbSugnblCepXeZngqHxq'+
                    'nuIuzAVnQR/cDwFeICVmJ+2cS9MUmAVZTxJJ2LjE+y+3F69hAVkaKANmzi0hx/RKit8uMcyTwj/cRdGazruj2F+yntJGzPdtHESbz'+
                    'J2fMcWeHdKKvVm0z/SV+D8m0UW7AEvVkHwTpk9X3OT3NkmY9wuQVKUVvk51l6a9w1QiG5wVb4DhNcOMMrS7yDy27MySrn6nw7ycJs'+
                    '1lGaQVIsY4r0V2aW+BJXWm+jfE5i9lY4tJ+BnYSFHCTIxruFoEHXRTZfYwp1pFEubKPbeX6c6qNE7dnjGCvZ2TJgO8UFBn8naTKNW'+
                    'pTFfm8/LzzDfbty7qT4Hp+QbiPozMGGiBcYuUjfVX6KkLQz9iBPPEWUZIAUX5G+zYVvmKzRezttPRms87qj8Rk6/uJklO3wtwLDZX'+
                    'ZsQUfmzuvMTXH0Gq/NcGudoQeIbsLf+JT6WV6e5+O1TZfoq3LiJZaOk97BuYBn0Zq0HPDWfhofkh5kfisHSuv1dXUzOMQHFWbaeDp'+
                    'gc/6vCqh0MzHMTJnDG0Jy2oJ9/v/SIHZnaVjTP6JKtGDAViSYAAAAAElFTkSuQmCC) 9 9, auto'
    };

    var globalPreload = !!preload;
    var quirksMode = document.compatMode == 'BackCompat';

    var setInnerHTML = function(el, string) {
        // element.innerHTML does not work on plain text files in FF; this restriction is similar to
        // http://groups.google.com/group/mozilla.dev.extensions/t/55662db3ea44a198
        var self = arguments.callee;
        if (typeof self.supportsInnerHTML == 'undefined') {
            var testParent = document.createElement('div');
            testParent.innerHTML = '<p/>';
            self.supportsInnerHTML = (testParent.firstChild.nodeType == 1);
        }
        if (self.supportsInnerHTML) {
            el.innerHTML = string;
            return el;
        } else if (typeof XSLTProcessor == 'undefined') {
            return undefined;
        } else {
            if (typeof self.cleanDocument == 'undefined')
                self.cleanDocument = createHTMLDocument();

            if (el.parentNode) {
                var cleanEl = self.cleanDocument.importNode(el, false);
                cleanEl.innerHTML = string;
                el.parentNode.replaceChild(document.adoptNode(cleanEl), el);
            } else {
                var cleanEl = self.cleanDocument.adoptNode(el);
                cleanEl.innerHTML = string;
                el = document.adoptNode(cleanEl);
            }

            return el;
        }

        function createHTMLDocument() {
            // Firefox does not support document.implementation.createHTMLDocument()
            // cf. http://www.quirksmode.org/dom/w3c_html.html#t12
            // The following is taken from http://gist.github.com/49453
            var templ = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">'
                    + '<xsl:output method="html"/><xsl:template match="/">'
                    + '<html><head><title/></head><body/></html>'
                    + '</xsl:template></xsl:stylesheet>';
            var proc = new XSLTProcessor();
            proc.importStylesheet(new DOMParser().parseFromString(templ,'text/xml'));
            return proc.transformToDocument(document.implementation.createDocument('', 'fooblar', null));
        }
    };

    function addEventWatcher(el, evt, fun, cap) {
        if (typeof el.addEventListener != 'undefined') {
            el.addEventListener(evt, fun, cap);
        } else if (typeof el.attachEvent != 'undefined'){
            return el.attachEvent('on' + evt, function() { fun.call(el, window.event); });
        }
        else return undefined;
    }

    var getNaturalSize = function(img) {
        if (img.naturalWidth) {
            return { w: img.naturalWidth, h: img.naturalHeight };
        } else {
            var t = document.createElement('img');
            t.src = img.src;
            t.style.visibility = 'hidden';
            t.style.position = 'absolute';
            t.style.left = t.style.top = '0';
            document.body.appendChild(t);
            var result = { w: t.width, h: t.height };
            document.body.removeChild(t);
            return result;
        }
    };

    var cacheSizes = function() {
        var scrollbarTest = quirksMode ? document.body : document.documentElement;
        return {
            windowInnerWidth : window.innerWidth || document.documentElement.offsetWidth,
            windowInnerHeight : window.innerHeight || document.documentElement.offsetHeight,
            bodyHasVerticalScrollbar: (scrollbarTest.scrollHeight != scrollbarTest.clientHeight),
            bodyHasHorizontalScrollbar: (scrollbarTest.scrollWidth != scrollbarTest.clientWidth)
        };
    };
    var cachedSizes = cacheSizes();
    addEventWatcher(window, 'resize', function() { cachedSizes = cacheSizes() }, false);
    // faster than repeatedly accessing window.innerWidth etc.

    var moveToPos = function(el, cursorX, cursorY, alignH, alignV) {
        if (!el) return;
        var iW = cachedSizes.windowInnerWidth;
        var iH = cachedSizes.windowInnerHeight;
        var elWidth = el.cachedWidth;
        if (!elWidth) elWidth = el.cachedWidth = el.offsetWidth || el.naturalWidth || el.width || parseInt(el.style.width) || 0;
        var elHeight = el.cachedHeight;
        if (!elHeight) elHeight = el.cachedHeight = el.offsetHeight || el.naturalHeight || el.height || parseInt(el.style.height) || 0;
        var minX = SPACING.LEFT;
        var maxX = iW - SPACING.RIGHT - (cachedSizes.bodyHasVerticalScrollbar ? 20 : 0);
        if (maxX <= minX) { minX = 0; maxX = iW; }
        if (cursorX < minX) cursorX = minX;
        if (cursorX > maxX) cursorX = maxX;
        var rightX = cursorX + CURSORMARGIN.RIGHT;
        var leftX = cursorX - CURSORMARGIN.LEFT - elWidth;
        var minY = SPACING.TOP;
        var maxY = iH - SPACING.BOTTOM - (cachedSizes.bodyHasHorizontalScrollbar ? 20 : 0);
        if (maxY <= minY) { minY = 0; maxY = iH; }
        if (cursorY < minY) cursorY = minY;
        if (cursorY > maxY) cursorY = maxY;
        var bottomY = cursorY + CURSORMARGIN.BOTTOM;
        var topY = cursorY - CURSORMARGIN.TOP - elHeight;
        var midX; //= (minX + maxX - elWidth) / 2;
        var midY; //= (minY + maxY - elHeight) / 2;

        var newX;
        var newY;
        if (rightX + elWidth <= maxX && (alignH >= 0 || (alignH < 0 && leftX < minX))) {
            newX = rightX;
        } else if (leftX >= minX) {
                newX = leftX;
        } else 
            newX = midX = (minX + maxX - elWidth) / 2;
        if (bottomY + elHeight <= maxY && (alignV >= 0  || (alignV < 0 && topY < minY))) {
                newY = bottomY;
        } else if (topY >= minY) {
            newY = topY;
        } else
            newY = midY = (minY + maxY - elHeight) / 2;
        if (newX == midX && newY == midY) { // push away, leave biggest visible area
            var areaTop = Math.min(elWidth, iW) * (topY + elHeight);
            var areaBottom = Math.min(elWidth, iW) * (iH - bottomY);
            var areaLeft = (leftX + elWidth) * Math.min(elHeight, iH);
            var areaRight = (iW - rightX) * Math.min(elHeight, iH);
            switch(Math.max(areaTop, areaBottom, areaLeft, areaRight)) {
                case areaTop:
                    newY = topY;
                    break;
                case areaBottom:
                    newY = bottomY;
                    break;
                case areaLeft:
                    newX = leftX;
                    break;
                default:
                    newX = rightX;
                    break;
            }
        }
        if (ie5 || ie6) { // hack vs. non-functioning 'position: fixed'
            newX += document.documentElement.scrollLeft;
            newY += document.documentElement.scrollTop;
        }
        el.style.left = newX + 'px';
        el.style.top = newY + 'px';
    };

    var Thumbnail = function(DOMString, url, showSizeTags, errorHandler, waitHandler, readyHandler) {
        this.mainContent = {};
        this.size = {w:0, h:0};
        if (DOMString) {
            this.mainContent = document.createElement('div');
            setInnerHTML(this.mainContent, DOMString.replace(/src="/, 'id="').replace(/%checkers%/g, CONST.CHECKERS), true);
        } else {
            if (!url) return undefined;
            this.mainContent = document.createElement('img');
            this.mainContent.id = url;
            this.mainContent.style.border = '1px solid gray';
        }
        if (showSizeTags) {
            this.sizeTag =  document.createElement('label');
            this.sizeTag.style.visibility = 'hidden';
        }
        var imgs = (this.mainContent.tagName.toLowerCase() == 'img') ? [this.mainContent] : this.mainContent.getElementsByTagName('img');
        if (imgs.length > 1) throw new Error('Thumbnail must not have more that one img element');
        var img = imgs[0];
        if (img) {
            img.thumbnail = this;
            this.mainContent.style.visibility = 'hidden';
            this.isLoading = true;
            if (waitHandler) waitHandler();
            this.failedLoading = false;
            addEventWatcher(img, 'load', function() {
                var thn = this.thumbnail;
                thn.size = getNaturalSize(this);
                if (showSizeTags) {
                    with (thn.sizeTag.style) {
                        color = 'black';
                        backgroundColor = 'lightyellow';
                        position = (ie5 || ie6) ? 'absolute' : 'fixed';  // hack vs. non-functioning 'position: fixed'
                        border = '1px solid black';
                        padding = '.2em .5em';
                        fontFamily = 'monospace';
                        border = '1px inset gray';
                        zIndex = '32010';
                        borderRadius = MozBorderRadius = WebkitBorderRadius = '3px';
                    }
                    thn.sizeTag.appendChild(document.createTextNode(thn.size.w + ' \u00D7 ' + thn.size.h));
                    thn.sizeTag.cachedWidth = thn.sizeTag.cachedHeight = undefined;
                }
                if(thn.mainContent.innerHTML)
                    setInnerHTML(thn.mainContent, thn.mainContent.innerHTML.replace(/%width%/g, thn.size.w).replace(/%height%/g, thn.size.h));
                thn.isLoading = false;
                if (readyHandler) readyHandler();
                if (thn.showAfterLoading) thn.showAt(thn.currentCoords.x, thn.currentCoords.y);
            }, false);
            if (errorHandler) {
                addEventWatcher(img, 'error', function(e) {
                    this.failedLoading = true;
                    this.isLoading = false;
                    errorHandler(this, e);
                }, false);
            }
            img.src = img.id.replace(/%href%/g, url);
            img.id = '';
        }
        this.mainContent.className = 'TomsThumbnailer';
        this.mainContent.style.position = (ie5 || ie6) ? 'absolute' : 'fixed';  // hack vs. non-functioning 'position: fixed'
        this.mainContent.style.zIndex = '32000';

        this.addToDOM = function() {
            if (!this.mainContent.parentNode || this.mainContent.parentNode.tagName != 'BODY')
                document.body.appendChild(this.mainContent);
            if (this.sizeTag && (!this.sizeTag.parentNode || this.sizeTag.parentNode.tagName != 'BODY'))
                document.body.appendChild(this.sizeTag);
        };
        this.removeFromDOM = function() {
            if (this.mainContent.parentNode)
                this.mainContent.parentNode.removeChild(this.mainContent);
            if (this.sizeTag && this.sizeTag.parentNode)
                this.sizeTag.parentNode.removeChild(this.sizeTag);
        };
        this.showAt = function(x, y) {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.showAfterLoading = true;
                this.currentCoords = {x: x, y: y};
                return;
            }
            this.hide();
            this.addToDOM();
            this.moveTo(x,y);
            this.show();
        };
        this.moveTo = function(x,y) {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.currentCoords = {x: x, y: y};
                return;
            }
            moveToPos(this.mainContent, x, y, PREFERREDTHUMBNAILPOS.HORIZONTAL, PREFERREDTHUMBNAILPOS.VERTICAL);
            moveToPos(this.sizeTag, x, y, PREFERREDSIZETAGPOS.HORIZONTAL, PREFERREDSIZETAGPOS.VERTICAL);
        };
        this.show = function() {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.showAfterLoading = true;
                return;
            }
            this.mainContent.style.visibility = '';
            if (this.sizeTag)
                this.sizeTag.style.visibility = '';
        };
        this.hide = function() {
            if (this.isLoading) {
                this.showAfterLoading = false;
                return;
            }
            this.removeFromDOM();
            this.mainContent.style.visibility = 'hidden';
            if (this.sizeTag)
                this.sizeTag.style.visibility = 'hidden';
        };
    };

    this.addThumbnail = function(el, preloadOne, showSizeTags, DOMString) {
        if (typeof el == 'string')
            el = document.getElementById(el);
        var preload = (/^(undefined|null)$/.test(typeof preloadOne)) ? preloadOne : globalPreload;
        var loadHandler = preload ? null : function() { el.style.cursor = ie5 ? 'wait' : 'progress'; };
        var readyHandler = preload ? null : function() { el.style.cursor = ''; };
        var errorHandler = function() { el.style.cursor = CONST.ERRORCURSOR; el.style.textDecoration = 'line-through'; };
        if (preload)
            el.thumbnail = new Thumbnail(DOMString, el.href, showSizeTags, errorHandler);

        addEventWatcher(el, 'mouseover', function(e) {
            if (this.removeAttribute) this.removeAttribute('title'); else this.title = '';
            if (!this.thumbnail)
                this.thumbnail = new Thumbnail(DOMString, el.href, showSizeTags, errorHandler, loadHandler, readyHandler);
            this.thumbnail.showAt(e.clientX, e.clientY);
        }, false);

        addEventWatcher(el, 'mousemove', function(e) {
            if (this.thumbnail) {
                this.thumbnail.moveTo(e.clientX, e.clientY);
            }
        }, false);

        addEventWatcher(el, 'click', function() {
            if (this.thumbnail)
                this.thumbnail.hide();
        }, false);

        addEventWatcher(el, 'mouseout', function() {
            if (this.thumbnail)
                this.thumbnail.hide();
        }, false);
    };

    this.addThumbnailsToAllLinks = function(el, preload, showSizeTags, DOMString) {
        el = el || document;
        var links = el.getElementsByTagName('a');
        for (var i = 0; i < links.length; i++) {
            this.addThumbnail(links[i], preload, showSizeTags, DOMString);
        }
    };

    return this;
}

