/*
 * jQuery Extension: Jitter Hack
 * jiter hack is to deal with issues in mozilla flickering different previews
 * open we seeem to get mouseleave and mouseenter for near by thumbs just
 * mousing around in the current thumb.
 */
jQuery.fn.extend ({

    jittermouseleave: function (func, delay) {
        var mouse_in;
        return this
            .mouseenter (function () { mouse_in = true; })
            .mouseleave (function (e) {
                mouse_in = false;
                setTimeout (function () {
                    if (mouse_in) { return; }
                    func (e);
                }, delay || 0 );
            });
    },

    jittermouseenter: function (func, delay) {
        var mouse_in;
        return this
            .mouseleave (function () { mouse_in = false; })
            .mouseenter (function (e) {
                mouse_in = true;
                setTimeout (function () {
                    if (!mouse_in) { return; }
                    func (e);
                }, delay || 0 );
            });
    }

});


/*
 * searchMash util  cookie.
 */
(function () {
    var util = searchMash.util,
        dbg = searchMash.dbg;
    function cookie (name, value, config) {
        var d = document,
            expires, path, domain, secure,
            cookies, i, len, match;

        if (value !== undefined) { 
            if (config) { 
                expires  = config.expires;
                path     = config.path;
                domain   = config.domain;
                secure   = config.secure;
            }
            if (value === null) { 
                value = '';
                expires = new Date (1);
            } else { 
                value = encodeURIComponent (value);
            }
            if (typeof expires == 'number' ) { 
                expires = new Date (expires * 86400 + new Date ().getTime ());
            }
            expires = expires.toUTCString ? 
                '; expires =' + expires.toUTCString () : '';
            path    = path   ? '; path='   + path   : '';
            domain  = domain ? '; domain=' + domain : '';
            secure  = secure ? '; secure=' + secure : '';
            cookies = name + '=' + value + expires + path + domain + secure;
            d.cookie = cookies;
        } else {
            cookies = d.cookie;
            re = new RegExp ('^' + name, "i");
            if (cookies) { 
                cookies = cookies.split (';');
                for (i=0, len = cookies.length; i < len; i++) { 
                    match = cookies[i].match ("^ *" + name + "=(.*)");
                    if (match !== null) { 
                        return decodeURIComponent (match[1]);
                    }
                }
            }
            return;
        }
    }
    util.cookie = cookie;
}) ();

/*
 * jquery extension $.toJSON
 * from: http://www.overset.com/2008/04/11/mark-gibsons-json-jquery-updated/
 */
(function ($) {
    var m = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    };
    $.toJSON = function (value, whitelist) {
        var a,          // The array holding the partial texts.
            i,          // The loop counter.
            k,          // The member key.
            l,          // Length.
            r = /["\\\x00-\x1f\x7f-\x9f]/g,
            v;          // The member value.

    switch (typeof value) {
        case 'string':
            return r.test (value) ?

                '"' +
                value.replace (r, function (a) {
                    var c = m[a];
                    if (c) { return c; }
                    c = a.charCodeAt();
                    return '\\u00' +
                        Math.floor(c / 16).toString(16) +
                        (c % 16).toString(16);
                }) + '"' :

                '"' + value + '"';

        case 'number':
            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':
            return String(value);

        case 'object':
            if (!value) { return 'null'; }

            if (typeof value.toJSON === 'function') {
                return $.toJSON(value.toJSON());
            }
            a = [];

            if (typeof value.length === 'number' &&
                !value.propertyIsEnumerable('length')) {

                l = value.length;
                for (i = 0; i < l; i += 1) {
                    a.push($.toJSON(value[i], whitelist) || 'null');
                }
               
                return '[' + a.join(',') + ']';
            }

            if (whitelist) {
                l = whitelist.length;
                for (i = 0; i < l; i += 1) {
                    k = whitelist[i];
                    if (typeof k === 'string') {
                        v = $.toJSON(value[k], whitelist);
                        if (v) {
                            a.push($.toJSON(k) + ':' + v);
                        }
                    }
                }
            } else {
                for (k in value) {
                    if (typeof k === 'string') {
                        v = $.toJSON(value[k], whitelist);
                        if (v) {
                            a.push($.toJSON(k) + ':' + v);
                        }
                    }
                }
            }
            return '{' + a.join(',') + '}';
        }
    };
       
})(jQuery);


/*
 * searchMash util that depends on jQuery
 */
(function ($) {
    var searchMash = window.searchMash,
        mashConfig = searchMash.config,
        util = searchMash.util,
        dbg = searchMash.dbg,
        scaleAndCenter = util.scaleAndCenter,
        log_event;

    function resultPosition (pg, offset) {
        return (pg - 1) * mashConfig.page_size + parseInt(offset,10);
    }
    util.resultPosition = resultPosition;

    function shortenUrlForDisplay (url) {
        var new_url = /[^:]+:\/\/[^\/]+\//.exec (url);
        return new_url ? new_url[0] : '';
    }
    util.shortenUrlForDisplay = shortenUrlForDisplay;

    function makeSadImage () {
        return $("<img/>")
            .addClass ("sad_image")
            .src (mashConfig.sad_image);
    }
    util.makeSadImage = makeSadImage;

    function _logEvent (log_event) {
        // Note: log_event must have a type or the server will ignore it.
        var config = log_event.config,
            ticket = config.log_ticket,
            ajax_config;

        if (!ticket)  { 
            dbg ("event has no logging ticket", log_event) ;
            return;
        }

        // clone it
        log_event = jQuery.extend ({}, log_event);

        // remove the config object so that we don't log it.
        delete log_event.config;

        ajax_config = {
            async: ! config.sync,
            url: "/u/l/" + ticket,
            cache: false,
            timeout: config.timeout || 8000,
            dataType: "json",
            type: "POST",
            //success: function () {dbg ("log event success", log_event);},
            //error: function () {dbg ("log event failure", log_event);},
            complete: config.callback
        };

        if (config.post_json) {
            ajax_config.data = $.toJSON (log_event);
            ajax_config.contentType = 'application/json';
        } else {
            ajax_config.data = log_event;
        }

        $.ajax(ajax_config);
    }

    log_queue = util.log_queue = util.log_queue || [];
    function logFlushQueue () { 
        log_event = log_queue.shift ();
        if (log_event) { 
            _logEvent (log_event);
            logFlushQueue ();
        }
    }
    util.logFlushQueue = logFlushQueue;

    // logEvent is defined in searchmash_min this replaces the logEvent from
    // searchMash min.
    function logEvent (log_event) { 
        log_queue.push (log_event);
        logFlushQueue ();
    }
    // flush any 
    logFlushQueue ();
    util.logEvent = logEvent;

}) (jQuery);


/*
 * jquery extension: helpers for getting/setting some specific attributes
 */
(function () {
    function make (name) {
        return function (val) {
            if (val !== undefined ) {
                this.each (function () {
                    this.setAttribute (name, val);
                });
                return this;
            } else {
                return this.get (0).getAttribute (name);
            }
        };
    }

    jQuery.fn.extend ({
        src:    make ("src"),
        href:   make ("href"),
        title:  make ("title"),
        alt:    make ("alt"),
        id:     make ("id"),
        target: make ("target")
    });
}) ();


/*
 * searchmash main.  
 */ 
(function ($) {
    var mashConfig = searchMash.config,
        dbg = searchMash.dbg,
        util = searchMash.util,
        isFunc = util.isFunc,
        resultPosition = util.resultPosition,
        logEvent = util.logEvent;

    function handleOutboundClick (config) {
        var e            = $(config.e),
            // NOTE: the log_event _may_ be a reference shared with other 
            // click handlers.  [ say if outboundClick were called a 
            // jquery collection of length > 1. ]  so we don't modify it.
            log_event    = config.log_event,
            log_config   = log_event.config,
            white_out    = log_config.white_out,
            handler      = log_config.handler,
            logged_event = 0;


        // NOTE: config will double as our state object. it should be unique
        // to the current element for which we are handling the click. 

        // no double trigger.  no trigger while whiteout.
        if (config.handling_click) { 
            dbg ("got click while handling click");
            return false;
        }
        config.handling_click = 1;

        // whiteout this element 
        if (white_out) { 
            e.whiteOut (); 
        }

        // call any callback handler.
        if (isFunc (handler)) {
            handler.call (config.e);
        }

        // 3 seconds of no clicking allowed.  then when we've returned from
        // logEvent allow clicking and remove whiteout.
        function doneHandling () {
            if (logged_event) { 
                if (white_out) { 
                    e.whiteOff ();
                }
                config.handling_click = 0;
            } else { 
                setTimeout (doneHandling, 100);
            }
        }
        setTimeout (doneHandling, 3000);

        // because it's an outbound click this  must be synchronous
        log_event.config.sync = true;
        logEvent (log_event); 
        logged_event = 1;

        // return true so command splat and other things work
        return true;
    }

    // TODO mv the generic jquery extensions into their own block.
    // misc jquery extensions
    jQuery.fn.extend ({
        display: function (val) {
            this.each (function () {
                this.style.display = val;
            });
            return this;
        },
        // using jQuery's hide()  causes  slow jQuery.data calls
        fastHide: function () {
            this.display ("none");
            return this;
        },
        // using jQuery's show()  causes  slow jQuery.data calls
        blockShow: function () {
            this.display ("block");
            return this;
        },
        visible: function () { 
            return this.css ({visibility: "visible"});
        },
        hidden: function () { 
            return this.css ({visibility: "hidden"});
        },

        // using jQuery's load causes a ton slow jQuery.data calls
        myLoad: function (fn) {
            this.each (function () { this.onload = fn; });
            return this;
        },
        myError: function (fn) {
            return this.each (function () {
                if (this.addEventListener) { 
                    this.addEventListener("error", fn, false);
                } else if (this.attachEvent) { 
                    this.attachEvent("onerror", fn);
                } else { 
                    this.onerror = fn;
                }
            });
            
            // just setting onerror is slightly faster but didn't
            // seem to work in ie6: this.each (function () { this.onerror = fn; });
        },

        outboundClick: function (log_event) {
            var url = log_event.url;

            if ( ! url ) {
                if (mashConfig.debug) {
                    throw "outboundClick bad url: " + url;
                } else {
                    // TODO
                    return this;
                }
            }

            this.each (function () { 
                // provide independant state for each element we setup.
                var config = {
                    e: this, 
                    log_event: log_event,
                    handling_click: 0
                };
                this.onclick = function () { 
                    return handleOutboundClick (config);
                };
            });
            this.href (url);

            return this;
        },

        thumbToPageClick: function (item, click_event) {
            var config = click_event && click_event.config || {};

            config.log_ticket = item.log_ticket;
            config.white_out = true;
            click_event = $.extend ({
                    config: config,
                    type: "result click",
                    i: resultPosition (item.pg, item.offset),
                    searcher: item.searcher,
                    srid: searchMash.presearch.srid,
                    image: item.image, // TODO this isn't always there now.
                    event: "outbound from thumbnail",
                    url: item.page
                }, click_event);

            this.outboundClick (click_event);
            this.addClass ("thumb_to_page");
            return this;
        },

        whiteOut: function () {
            this.each (function () {
                var e = $(this),
                    css_pos     = e.css ("position"),
                    width       = e.width (),
                    height      = e.height (),
                    position, top, left;

                if (css_pos == "relative" || css_pos == "absolute") {
                    top = left = 0;
                } else { 
                    position = e.position ();
                    top      = position.top;
                    left     = position.left;
                }

                $("<div/>")
                    .addClass ("white_out")
                    .append ("<div/>")
                    .append ("<img/>")
                    .appendTo (this)
                    .css ({
                        position: "absolute",
                        top: top,
                        left: left,
                        height: height,
                        width: width,
                        overflow: "hidden"
                    })
                    .find ("div")
                        .css ({ opacity: 0.5 })
                    .end()
                    .find ("img")
                        .src (mashConfig.spinner_image)
                        .addClass ("spinner");
            });
            return this;
        },

        whiteOff: function () {
            $(this).find (".white_out")
                .remove ();
            return this;
        }

    });


}) (jQuery);


/*
 * addthis
 * configure addthis
 */
(function () {
    var loaded = 0;

    window.addthis_config = {
        services_exclude: 'favorites',
        data_use_flash: false
    };

    function onload () { 
        var share = document.getElementById ("share");
        if (share && window._adr) { 
            // window._adr comes from addthis, they have bugs for safari and
            // firefox that prevent them from loading if we loaded them past
            // the onload event.  so instead we tell them we're ready as long
            // as we can find the share elemnt.
            window._adr.onReady ();
            share.style.visibility = "visible";
        } else { 
            setTimeout(onload, 100);
        }
    }

    function load_addthis (callback) {
        if ( loaded ) {
            if (callback) { callback (); }
            return; 
        } 
        loaded = 1;
        searchMash.util.loadList (
            //"//s7.addthis.com/js/250/addthis_widget.js#username=prama",
            //"/scripts/addthis/addthis_widget.min.js",
            "/scripts/addthis/addthis_widget.js",
            onload,
            callback );
    }

    searchMash.load_addthis = load_addthis;

}) ();


/*
 *  the display fun
 */
(function ($,SM) {

    var jQuery = $,
        dbg                     = SM.dbg,
        util                    = SM.util,
        mashConfig              = SM.config,
        delta                   = util.delta,
        isFunc                  = util.isFunc,
        logEvent                = util.logEvent,
        makeSadImage            = util.makeSadImage,
        resultPosition          = util.resultPosition,
        shortenUrlForDisplay    = util.shortenUrlForDisplay,
        scaleAndCenterWhenReady = util.scaleAndCenterWhenReady,
        window = this,
        document = window.document,
        location = document.location,
        hostname = location.hostname,
        base_url = location.protocol + "//" + location.host,
        PreviewWindow;
        
    function placeViewer (thumb_container, preview_container) {
        var $window = $(window),
            offset = thumb_container.offset (),
            width  = thumb_container.outerWidth (true),
            height = thumb_container.outerHeight (true),

            preview_container_height = preview_container.outerHeight (true),
            preview_container_width  = preview_container.outerWidth (true),

            viewport_width = $window.width(),
            viewport_height = $window.height(),
            scroll_left = $window.scrollLeft(),
            scroll_top = $window.scrollTop(),

            center_to_top = (offset.top - scroll_top) + height / 2,
            center_to_bot = viewport_height - center_to_top,

            preview_container_top;


        if ((offset.left - scroll_left + width / 2) > viewport_width / 2) {
            // if it's right of center and there is room put it on the left
            preview_container.css ({
                left: offset.left - preview_container_width + "px",
                right: "auto"
            });
        } else {
            // otherwise put it on the right
            preview_container.css ({
                left: offset.left + width + "px",
                right: "auto"
            });
        }

        preview_container_top =
            offset.top + height / 2 - preview_container_height / 2;

        if ( preview_container_top + preview_container_height >
            scroll_top + viewport_height ) {
            preview_container_top =
                scroll_top + viewport_height - preview_container_height;
        }
        if ( preview_container_top < scroll_top ) {
            preview_container_top =  scroll_top;
        }

        preview_container.css ({
             top: preview_container_top + "px"
        });
    }


    function round (value, places)  {
        var factor = Math.pow (10,places);

        return Math.round (value * factor) / factor;
    }

    function mouseSpeedMonitor (e) {
        if (mashConfig.preview_disabled || e.data.item.preview_disabled) {
            return;
        }

        var now = new Date ().getTime (),
            d = e.data,
            X = e.clientX,
            Y = e.clientY,
            last_time = d.last_time,
            lunge_settings = d.lunge_settings,
            slow_time = lunge_settings.slow_time,
            slow_threshold = lunge_settings.slow_threshold,
            lastX,
            lastY,
            delta_p,
            delta_t,
            speed;

        // if mouse speed drops below threshold set pause_triggered by calling
        // d.slow ()
        if (last_time) {
            if ( ! d.pause_triggered ) {

                lastX = d.lastX;
                lastY = d.lastY;

                delta_p =
                    Math.sqrt (Math.pow (X - lastX,2) + Math.pow (Y - lastY,2));
                delta_p = round (delta_p, 2);

                delta_t = now - last_time;
                speed = round (delta_p / delta_t * 1000, 2);

                if ( speed > slow_threshold ) {
                    if (d.slow_timer) { clearTimeout (d.slow_timer); }
                    d.slow_timer = setTimeout (d.slow, slow_time);
                }
            }
        } else {
            //dbg ("mousemove:", d.id, "skipping cause last_time is", last_time);
        }

        // set values needed for next speed calculation
        d.lastX = X;
        d.lastY = Y;
        d.last_time = now;
    }

    function lungeCondition (cond, state, targets) {
        var label = cond + "_triggered";
        if (/^(stay|pause)$/.exec (cond)) {
            // dbg ( cond, "triggered");
            if (mashConfig.debug) {
                $.each (targets, function () {
                    this.addClass (label);
                });
            }
            state[label] = true;
        }

        if (state.stay_triggered && state.pause_triggered) {
            $.each (targets, function () {
                this.addClass ("lunge_triggered");
            });
            state.lunge_triggered = true;
        }
    }


    /*
     * setup mouseover actions
     */
    function setupMouseOvers (item, thumb, preview) {

        var lunge_settings = mashConfig.lunge_settings,
            jitter_delay = $.browser.mozilla ?  0 : 0, // ms
            need_high_rez = 1,
            state = {
                item: item,
                last_time: 0,
                id: thumb.container.id (),
                lunge_settings: lunge_settings,
                thumb: thumb,
                mouse_in: false
            };


        // after that's loaded set mouseovers to show the preview window
        thumb.img_container
            .jittermouseenter (function () {
                if (mashConfig.preview_disabled || item.preview_disabled) { 
                    return;
                }
                if (need_high_rez) { 
                    preview.high_rez (item.preview_urls);
                    need_high_rez = 0;
                }
                // TODO maybe also verify the preview image is not larger
                // than the viewport and scale it down if it is
                if (!state.stay_timer) {
                    state.stay_timer = setTimeout (function () {
                        lungeCondition ("stay", state, [thumb,preview]);
                    }, lunge_settings.stay_time);
                }
                state.mouse_in = true;
                preview.show ();
            }, jitter_delay);

        state.slow = function () {
            lungeCondition ("pause", state, [thumb,preview]);
        };

        thumb.img_container
            // mouse tracking for lunge
            .bind ("mousemove", state, mouseSpeedMonitor)

            // set mouseleave to hide any thumb actions and the preview image
            .jittermouseleave (function () {
                if (mashConfig.preview_disabled || item.preview_disabled) { 
                    return;
                }
                var lunge_settings = mashConfig.lunge_settings,
                    lunge_active = lunge_settings.active,
                    do_lunge = state.lunge_triggered;

                state.mouse_in = false;

                // TODO mv this somewhere more sensible
                function hidePreview () {
                    thumb.removeClass ("lunge_triggered");
                    preview.removeClass ("lunge_triggered");
                    preview.hide ();
                    state.lunge_triggered = false;
                }

                if (lunge_active && do_lunge) {

                    // wait to close preview to see if lunge happens
                    setTimeout (function () {
                        // state.mouse_in_preview is set by preview.mouse*
                        // if the mouse is back in, don't close
                        if ( state.mouse_in_preview) { return; }
                        if ( state.mouse_in ) { return; }
                        hidePreview ();
                    }, lunge_settings.wait_time );

                    // dbg ("jittermouseleave wait for preview");
                } else {
                    // dbg ("jittermouseleave no wait");
                    // just close it right away.
                    hidePreview ();
                }

                // reset lunge speed related settings
                $.each (["slow_timer", "stay_timer"], function () {
                    if (state[this]) {
                        clearInterval (state[this]);
                        state[this] = false;
                    }
                });

                // dbg ("mouseleave:", state.id, "lunge_triggered?", !! do_lunge);
                $.each (
                    [ "pause_triggered", "stay_triggered"],
                    function () {
                        state[this] = false;
                        thumb
                            .removeClass (this);
                        preview
                            .removeClass (this);
                    }
                );
                state.last_time = 0;
            }, jitter_delay );

        preview.container
            .mouseenter (function () {
                state.mouse_in_preview = true;
            })
            .jittermouseenter (function () {
                if ( state.lunge_triggered ) {
                    preview.trigger ("preview_lunged");
                }
            }, jitter_delay)
            .mouseleave (function () {
                // dbg ("preview leave");
                state.mouse_in_preview = false;
            })
            .jittermouseleave (function () {
                thumb.img_container
                    .trigger ("mouseleave");
            }, jitter_delay);
    }

    /*
     *  ImageHolder Object
     *  parent of Thumb and Preview
     */
    function ImageHolder () {
        this.ns = {};
        return this;
    }

    function apply (func, obj, args, notfunc) {
        if (func) {
            return func.apply(obj || this, args || []);
        } else {
            return notfunc;
        }
    }

    function naturalDims (img) { 
        if (img.naturalWidth === undefined) {
            img.naturalWidth = img.width;
            img.naturalHeight = img.height;
        }
        return img;
    }
    ImageHolder.prototype = {

        _tryLoadLastError: function (last_success, img, config) {
            var success = config.success,
                filter = config.filter,
                error = config.error,
                done = config.done;

            if (last_success &&
                apply (filter, last_success, [config, true], true)) {

                apply (success, last_success, [config]) ;
            } else {
                apply (error,img, [config]);
            }
            apply (done, img);
        },

        _tryLoadImages: function (config, last_success) {
            var images = config.images,
                success = config.success,
                filter = config.filter,
                spinner = config.spinner,
                done = config.done,
                ImageHolder = this,
                image;

            if ( images.length === 0) {

                // we get here if the success handler passed on every
                // successfull value, we also get here if we were passed no
                // images or all undefined images or combo of the above.

                ImageHolder._tryLoadLastError (last_success, {}, config);

                apply (done, this);
                return false;
            }

            image = images.shift ();

            if ( typeof image !== "string" ) {
                //dbg ("passing on image:", image);
                ImageHolder._tryLoadImages (config, last_success);
            }

            function _tliload () { 
                var img = naturalDims (this),
                    $img = $(img);

                if (apply (filter, img, [config, images.length === 0], true)){

                    apply (success, img, [config]);
                    apply (done, img);
                } else {
                    ImageHolder._tryLoadImages (config, img);
                }
            }

            function _tlierror () { 
                var img = naturalDims (this);
                if ( images.length === 0 ) {
                    ImageHolder._tryLoadLastError (last_success, img, config);
                } else {
                    ImageHolder._tryLoadImages (config, last_success);
                }
            }

            var img = new Image ();
            img.style.display = "none";
            img.onload = _tliload;
            img.onerror = _tlierror;
            img.src = image;
            // WTF  this didn't work reliabliy on IE6: !!!! 
            // $("<img/>")
            //     .fastHide ()
            //     .myLoad   (_tliload)
            //     .myError  (_tlierror)
            //     .src      (image);
        },

        loadImages: function (config) {
            var ImageHolder = this;

            if ( ! config.images || !config.container) {
                throw "loadImages bad config"; 
            }

            if ( typeof config.images === "string") {
                // it's a url.  make an array
                config.images = [ config.images ] ;
            } else {
                // clone it since we pop through it.
                config.images = config.images.slice (0);
            }
            if ( ! config.images instanceof Array ) {
                throw "loadImages config.images is not Array?"; 
            }

            function draw () {

                if (config.spinner) { 
                    config.spinner.blockShow ();
                }
                ImageHolder._tryLoadImages (config);
            }

            draw ();

            return this;
        }
    };

    /*
     * Thumb[nail] Object
     */
    function Thumb (elem) {

        ImageHolder.apply (this, arguments);
        var ns = this.ns,
            $elem = elem && $(elem),
            e;

        e = this.ns.elements = {
            container:     $elem,
            classes:       $elem.children ("div:first"),
            img_container: $elem.find (".images:first"),
            img_link:      $elem.find ("a:first"),
            spinner:       $elem.find (".spinner:last"),
            sad_image:     $elem.find (".sad_image:last")
        };

        this.container = e.container;
        this.img_container = e.img_container;
        this.anchor = e.img_link;

        return this;
    }

    Thumb.prototype = new ImageHolder ();
    $.extend (Thumb.prototype, {

        removeClass: function (name) {
            this.ns.elements.classes
                .removeClass (name);
            return this;
        },

        addClass: function (name) {
            this.ns.elements.classes
                .addClass (name);
            return this;
        },
        id: function (value) {
            return this.ns.elements.container.id (value);
        }
    });


    /*
     * PreviewWindow Object
     */
    PreviewWindow = function () {
        var open_previews = {},
            pinned_previews = {},
            count = 0,
            preview_size;

        return function () {
            ImageHolder.apply (this, arguments);

            var preview = this,
                ns = preview.ns,
                e,
                html,
                container;

            // TODO cache the dimensions not just the div.
            preview_size = preview_size || $("#preview_max_size");

            this.container = container = $([
                '<div class="searchmash_preview" ',
                    'style="display:none">',
                    '<div class="shadow"></div>',
                    '<div class="inner">',
                        '<p class="title"></p>',
                        '<div class="grayarea">',
                            '<div class="images">',
                                '<a class="low_rez"></a>',
                                '<a class="high_rez"></a>',
                                '<img class="spinner" src="',
                                    mashConfig.spinner_image,
                                    '" style="display:none" />',
                            '</div>',
                        '</div>',
                        '<p class="description"></p>',
                        '<p class="source_page"></p>',
                    '</div>',
                '</div>'
                ].join (""));

            e = this.ns.elements = {
                container:    container,
                shadow:       container.find (".shadow"),
                inner:        container.find (".inner"),
                title:        container.find (".title"),
                grayarea:     container.find (".grayarea"),
                images:       container.find (".images"),
                low_rez:      container.find (".low_rez"),
                high_rez:     container.find (".high_rez"),
                description:  container.find (".description"),
                source_page:  container.find (".source_page"),
                spinner:      container.find (".spinner"),
                preview_size: preview_size
            };


            // global to all Preview Objects
            ns.open_previews = open_previews;
            ns.pinned_previews = pinned_previews;

            this.count = count++;
            this.pinned = false;

            ns.scaled = false;
            ns.callbacks = {};
            return this;
        };
    }();

    PreviewWindow.prototype = new ImageHolder ();

    // copy trigger () and bind () from jQuery.
    $.each (["trigger", "one", "bind"], function (k,v) {
        PreviewWindow.prototype[v] = function () {
            $.fn[v].apply (this.container, arguments);
            return this;
        };
    });

    $.extend (PreviewWindow.prototype, {

        _set_p: function (p, text, dom) {
            var ns = this.ns,
                e = ns.elements;

            text = text || "";

            // todo? more structured throw
            if ( !p || ! e[p] ) { throw ("previewwindow._set_p bad args"); }

            ns[p] = text;
            if (dom) {
                e[p].append (dom);
            } else { 
                e[p].text (text ? text : "");
            }
            return this;
        },

        callbacks: function (callbacks) {
            this.ns.callbacks = callbacks;
        },

        title: function (item) { 
            return this._set_p ("title", item.title, item.title_dom); 
        },

        description: function (item) {
            return this._set_p (
                "description",
                item.description,
                item.description_dom);
        },

        source_page: function (source_page) {
            return this._set_p ("source_page", source_page);
        },

        low_rez: function (images, callbacks) {
            var preview = this,
                e = this.ns.elements;
            callbacks = callbacks || {};
            return this.loadImages ({
                images: images,
                spinner: this.ns.elements.spinner,
                container: this.ns.elements.low_rez,
                success: function () {
                    var $this = $(this);
                    if ( ! preview.ns.scaled ) {
                        preview.scale (this);
                    }
                    e.low_rez.append (this);
                    e.spinner.fastHide ();
                    $this.blockShow ();

                    apply (callbacks.success);
                },
                error: function () {
                    dbg ("low rez load  failed:", this);
                    apply (callbacks.error);
                },
                done: function () {
                    apply (callbacks.done);
                }
            });
        },

        high_rez: function (images, callback) {
            var preview = this,
                e = this.ns.elements;

            callback = callback || {};

            if ( images.length === 0 ) {
              // we were called with no high rez images
              // TODO ?what?  right now this happens with the denormalized
              // searchers that don't have an item.image
              return this;
            }

            return this.loadImages ({
                images: images,
                spinner: this.ns.elements.spinner,
                container: this.ns.elements.high_rez,
                filter: function (config, last) {
                    // it's big enough or it's the last/largest available
                    if (last || preview.isBiggerThanPreviewSize (this)) {
                        return true;
                    }
                },
                success: function (config) {
                    var $this = $(this);
                    // have to scale using the image prior to sticking it
                    // in the dom, or we loose it's current size with ie.
                    preview.scale (this, true);
                    e.high_rez.append (this);
                    e.spinner.fastHide ();
                    $this.blockShow ();
                    apply (callback.success, this);
                    return true;
                },
                error: function (config) {
                    makeSadImage ().insertAfter (config.spinner);
                    dbg ("high rez load  failed:", this);
                    apply (callback.error);
                },
                done: function () {
                    preview.place ();
                    apply (callback.done);
                }
            });
        },

        place: function () {
            var ns = this.ns,
                e = ns.elements,
                container = e.container;
            placeViewer (container.parent(), container);
        },

        show: function (success) {
            var ns = this.ns,
                open_previews = ns.open_previews,
                e = ns.elements,
                container = e.container,
                p;

            for (p in open_previews) {
                if (open_previews[p] !== this) {
                    p = open_previews[p];
                    if (p.pinned) {
                        // someone else is pinned open. we dont open.
                        return this;
                    } else {
                        // otherwise no one else should be open
                        p.hide ();
                    }
                }
            }

            open_previews[this.count] = this;
            this.place ();
            container.blockShow ();
            this.previewUseStart ();
            apply (success);
            apply (this.ns.callbacks.show, this);
            this.trigger ("preview_show");
            return this;
        },

        hide: function () {
            var ns = this.ns,
                time_open;

            if (this.pinned) {
                return this;
            }
            ns.elements.container.fastHide ();
            this.previewUseEnd ();

            delete ns.open_previews[this.count];
            this.trigger ("preview_hide");
            apply (this.ns.callbacks.hide, this);
            return this;
        },

        previewUseStart:  function () {
            this.trigger ("preview_use_start");
            return this;
        },

        previewUseEnd:  function (outbound) {
            this.trigger ("preview_use_end", [outbound]);
            return this;
        },

        close: function () {
            this.unpin ();
            this.hide ();
            return this;
        },

        unpin: function () {
            this.pinned = false;
            this.ns.elements.inner
                .removeClass ("pinned");
            delete this.ns.pinned_previews[this.count];
            apply (this.ns.callbacks.unpin, this);
            return this;
        },

        pin: function () {
            var ns = this.ns,
                e = ns.elements,
                inner = e.inner,
                close = e.close,
                pinned_previews = ns.pinned_previews,
                p;

            this.pinned = true;
            for (p in pinned_previews) {
                if (pinned_previews[p] !== this) {
                    p = pinned_previews[p];
                    p.close ();
                }
            }

            this.show ();

            inner
                .addClass ("pinned");


            dbg (close.outerWidth (true),  parseFloat(inner.css ("padding-right")));
            inner.children (".title")
                .css ({
                    width: parseFloat (
                        inner.children (".description").css ("width")
                    ) - (
                        close.outerWidth (true) -
                        parseFloat (inner.css ("padding-right"))
                    )
                });

            pinned_previews[this.count] = this;
            apply (this.ns.callbacks.pin, this);
            return this;
        },

        togglePin: function () {
            return this.pinned ?  this.unpin () : this.pin ();
        },

        addClass: function (name) {
            this.container
                .addClass (name);
            return this;
        },

        removeClass: function (name) {
            this.container
                .removeClass (name);
            return this;
        },

        appendTo: function (to) {
            this.ns.elements.container.appendTo (to);
            return this;
        },

        isBiggerThanPreviewSize: function (cur_size, factor ) {
            var ns = this.ns,
                e = ns.elements,
                preview_size = e.preview_size;

            factor = factor ? factor : 1;
            return ( cur_size.width > preview_size.width() * factor  ||
                cur_size.height > preview_size.height () * factor );
        },

        toPageClick: function (item) {
            var ns = this.ns,
                _this = this,
                e = ns.elements,
                high_rez = e.high_rez,
                low_rez = e.low_rez,
                log_event = {
                    clicked_on: "image",
                    white_out: true
                };

            this.clickOut (low_rez, item, log_event);
            this.clickOut (high_rez, item, log_event);

            return this;
        },

        clickOut: function (elem, item, log_event) {
            var preview = this,
                config = log_event.config || {};

            // TODO maybe merge with any passed in handler?
            config.handler = function () {
                preview
                    .previewUseEnd (true)
                    .trigger ("preview_click_out");
                return true;
            };
            config.log_ticket = item.log_ticket;

            $(elem)
                .outboundClick ($.extend ({
                    // anything in log_event overrides these
                    config: config,
                    type: "result click",
                    i: resultPosition (item.pg, item.offset),
                    searcher: item.searcher,
                    srid: searchMash.presearch.srid,
                    event: "outbound from preview",
                    url: item.page
                }, log_event));
        },

        rescaleToImage: function (pass) {
            var ns = this.ns,
                e = ns.elements,
                images = e.images.get (0),
                grayarea = e.grayarea,
                inner = e.inner,
                container = e.container,
                container_style = container.get (0).style,
                display    = container_style.display,
                visibility, top, bottom, left, right,
                preview    = this,
                width, height;


            // if the preview is display none we can't get the
            // offsetWidth/Height of the contained images div so we make it
            // display block temporarily.
            if (display == "none") { 
                visibility = container_style.visibility;
                position   = container_style.position;
                top        = container_style.top;
                bottom     = container_style.bottom;
                left       = container_style.left;
                right      = container_style.right;

                container_style.visibility = "hidden";
                container_style.position   = "absolute";
                container_style.display    = "block";
                container_style.bottom     = "";
                container_style.right      = "";
                container_style.left       = "0";
                container_style.top        = "0";
                width  = images.offsetWidth;
                height = images.offsetHeight;
                container_style.top        = top;
                container_style.left       = left;
                container_style.right      = right;
                container_style.bottom     = bottom;
                container_style.display    = display;
                container_style.position   = position;
                container_style.visibility = visibility;
            } else { 
                width  = images.offsetWidth;
                height = images.offsetHeight;
            }

            pass = pass+1 || 2;
            if (!height || !width) { 
                if (pass > 10 ) { 
                    dbg ("rescaleToImage, too many retries");
                    return;
                }
                dbg ("rescaleToImage, image has no height, will try later");
                setTimeout (function () { 
                    preview.rescaleToImage (pass);
                }, 50);
            }
            inner
                .css ({width: width});
            grayarea
                .css ({
                    width: width,
                    height: height
                });
            ns.scaled = true;
        },

        max_height: function () { 
            var prototype = PreviewWindow.prototype;
            if (!prototype._max_height) { 
                prototype._max_height = this.ns.elements.preview_size.height ();
            } 
            return prototype._max_height;
        },
        max_width: function () { 
            var prototype = PreviewWindow.prototype;
            if (!prototype._max_width) { 
                prototype._max_width = this.ns.elements.preview_size.width ();
            }
            return prototype._max_width;
        },

        scale: function (to, shrink) {
            var ns = this.ns,
                e = ns.elements,
                preview_size = e.preview_size,
                images = e.images.get(0),
                preview = this,
                size;

            if (ns.scaled && shrink && !this.isBiggerThanPreviewSize (to, 0.9)){
                // if we've alredy scaled once, we've been asked to shrink if
                // neccesary and the "to size" is at least 10% smaller we
                // shrink the images container
                scaleAndCenterWhenReady ({
                    target: images,
                    max_size: to,
                    cur_size:  to,
                    unit: "px"
                });
                // images.css ("border-color", "orange");
            } else {
                // otherwise we scale everything.

                scaleAndCenterWhenReady ({
                    target: images,
                    cur_size: to,
                    max_height: this.max_height (),
                    max_width: this.max_width (),
                    scale: "fit",
                    center_horizontal: false,
                    center_vertical: false,
                    unit: "px"
                }, function (size) { 
                    preview.rescaleToImage ();
                });
            }

            return this;
        },

        customizeForSearcher: function (item) {
            var customize = this [item.searcher + "Customize"];
            if (isFunc (customize)) {
                customize.call (this, item);
            }
        },

        id: function (value) { 
            return this.container.id (value);
        }

    });

    function setupPreviewLogging (preview, item) {

        var preview_start_time;
        var lunged = false;
        preview
            .bind ("preview_lunged", function () {
                dbg ("preview_lunged callback called");
                lunged = true;
            })
            .bind ("preview_use_start", function () {
                preview_start_time = new Date () .getTime ();
            })
            .bind ("preview_use_end", function (e, outbound) {
                var total_time = new Date () .getTime () - preview_start_time;

                // if outbound is true then we've been triggered from some
                // outbound click condition and we need to do synchronous
                // logging
                oubound = !! outbound;  // force boolean

                if (total_time > 700) {
                    logEvent ({
                        config: {
                            log_ticket: item.log_ticket,
                            sync: outbound
                        },
                        type: "ui",
                        i: resultPosition (item.pg, item.offset),
                        searcher: item.searcher,
                        srid: searchMash.presearch.srid,
                        hover_time: total_time,
                        lunged: lunged,
                        outbound: outbound,
                        event: "preview use end"
                    });
                }
                lunged = false;
                return true;
            });
    }

    function handleShareLinkError (div) { 
        div.empty ();
        // put a sad image there
        makeSadImage ()
            .appendTo (div);
        // after 5 seconds remove the sad image too.
        // TODO only do this on actual failure.  for timeouts
        // put the addthis stuff back in place. 
        setTimeout (function () {div.remove ();}, 5000);
        return false;
    }

    function makeAddThis (url_function, title) {
        var div,
            cached_url,
            cached_url_function,
            addthis_conf,
            anchors;

        addthis_conf = { 
            ui_cobrand: "Pictures.com",
            ui_delay: 30,
            username: "prama"
        };

        cached_url_function = function () {
            return cached_url || (cached_url = url_function ());
        };
        cached_url_function = url_function;

        div = $([
            '<div class="addthis_default_style">',
                '<a class="addthis_button_expanded" ',
                    'href="http://www.addthis.com/bookmark.php?v=250&amp;username=prama">',
                    'Share',
                '</a> ',
                '<span class="addthis_separator">|</span> ',
                '<a class="addthis_button_preferred_1"></a>',
                '<a class="addthis_button_preferred_2"></a>',
                '<a class="addthis_button_preferred_3"></a>',
                '<a class="addthis_button_preferred_4"></a>',
            '</div>'
        ].join (""));


        // the magic click intercepter 
        anchors = div.find ("a");

        function addthis_intercept (e) {
            var url = cached_url_function ();

            if (!url) { 
                e.stopImmediatePropagation();
                return handleShareLinkError (div);
            }

            anchors
                .unbind (".addthis_intercept");

            // Ok, now update the toolbox with the new url
            // TODO is this the best way to update this?  an alternate approach
            // could be to emulate what addthis.update does but only local to
            // the current element.
            try { 
                addthis.toolbox (
                    div.get(0),
                    undefined, 
                    { url: url, title: title });
            } catch (f) { 
            }

            return true;
        }
        
        // add the intercept on click.
        // also add it on mouseover for the compact or expanded button.
        anchors 
            .one ("mouseover.addthis_intercept", addthis_intercept);

        // Ok now add our own custom "see share page" button.
        $("<a/>")
            .title ("See the share page for this image")
            .target ("_blank")
            .href ("/share/...")
            // masquerade as an adthis button.
            .addClass ("at300b")
            .append ("<img/>")
            .children ("img")
                // TODO update this.
                .src (mashConfig.seesharepage_image)
                .end ()
            .one ("click", function (e) { 
                var url = cached_url_function ();
                if (!url) { 
                    e.stopImmediatePropagation();
                    return handleShareLinkError (div);
                }
                $(this).href (url);
                return true;
            })
            .appendTo (div);

        // HACK
        // in the event that addthis fails to load, or that it loads
        // but fails to decorate this box we add classes and stuff so 
        // it will display nicely.
        // TODO  this is all mixed up trying to copy addthis defualt style stuff
        // instead try to make somethign that's styled correctly from 
        // from the beginning.
        $("<div/>")
            .addClass ("atclear")
            .appendTo (div);

        div.children ("a:eq(0)")
            .addClass ("at300m")
            .prepend ("<span/>")
            .children (":first")
                .addClass ("at300bs")
                .addClass ("at15t_compact");

        try { 
            addthis.toolbox (div.get(0), addthis_conf, {title: title} );
        } catch (f) { 
        }
        return div;
    }


    // TODO cache the results? will we query this more than once? 
    function getShareLink (data) {
        var ret = false;

        // rely on sycnhronous ajax call.  hope this is portable.
        $.ajax ({
            async: false,
            url: "/u/sharelink",
            timeout: 8000,
            type: "POST",
            data: data,
            success: function (data) {
                if ( data )  {
                    if (data.match (/^Error: /i)) { 
                        dbg ("getSharelink " + data);
                    } else { 
                        ret = "/share/" + data;
                    }
                } else { 
                    dbg ("got empty data for share link");
                }
            },
            error: function (req, text_status, error) {
                dbg ("make share link fail. ",
                    "Status: ", text_status,
                    " [error] ", error,
                    "data:", data);
            }
        });
       
        return ret;
    }

    function getTopImage (preview) { 
        return preview.ns.elements.images
            .find ("img:not(.sad_image, .spinner)")
            .slice (-1)
                .src ();
    }

    function makeShareLink (item, preview) {
        var link = $("<a/>");

        link
            .fastHide ()
            .text ("shareable link")
            .href ("#")
            .click (function () {
                var ret = true,
                    sharelink;

                sharelink = getShareLink ({ 
                    image: getTopImage (preview),
                    q: item.query,
                    page: item.page
                });

                if (sharelink) { 
                    $(this).href (sharelink);
                    dbg ("click through goes to: ", $(this).href());
                    return true;
                } else { 
                    return false;
                }
                
            })
            .show (); // TODO remove this show?

        return link;
    }

    // LAME but terse
    function substituteItemImages (list, dict) {
        var ret = [];
        $.each (list, function (k,v) { 
            if (dict[v]) { 
                ret[k] = dict[v]; 
            }
        });
        return ret;
    }

    function decodeAnnotations (input) {
        var ret_dom = $("<div/>"),
            anchor;

        if (util.isArray (input)) {
            $.each (input, function (k,v) {
                var text,
                    path,
                    base = "http://en.wikipedia.org/wiki/";

                if (isArray (v)) {
                    text =  v[0] || "";
                    path = v[1] || "Error";

                    ret_dom 
                        .append (" <a/>")
                        .children (":last")
                            .href (base + path)
                            .text (text);

                } else { 
                    ret_dom
                        .append (v ? " " + v : " ");
                }
            });
        } else { 
            ret_dom 
                .text  (input ? "" + input : "");
        }
        return ret_dom;
    }

    // TODO this function naming is weird.
    function decodeAnnotation (item, what) { 
        var dom = what + "_dom",
            decode = decodeAnnotations (item[what]);

        item[dom] = decode.contents ();
        item[what] = decode.text ();
    }

    function canonItem (item) {
        var tmp,
            noop = function (val) { return val;};

        // convert from the list of indexes to list of images
        item.preview_urls =
            substituteItemImages (item.preview_images, item.images);
        item.biggest_image = item.images[item.biggest_image];
        
        // display_page
        if (item.searcher == "yahoo") {
            tmp = /\/\*\*(.*)$/.exec(item.page);
            if ( tmp ) {
                item.display_page = decodeURIComponent (tmp[1]);
            }
        }

        if ( ! item.display_page ) {
            item.display_page = item.page;
        }

        decodeAnnotation (item, "title");
        decodeAnnotation (item, "description");

        return item;
    }

    function getItem (elem) { 
        // TODO tuck this stuff away somewhere better.
        return elem.parentNode.parentNode.items[elem.id];
    }

    function extendFromMin (div) { 
        $(div).find(".thumb_container:not(.empty)")
            .each ( function  () { 
                extendElemFromMin (this);
            });
    }

    function bg (func, delay) { setTimeout (func, delay || 0); }

    function rightClickLogger (e) {
        if (e.button === 2) { 
            var data = e.data, 
                item = data.item;
            dbg ("right click ", data);
            logEvent ({ 
                config: { log_ticket: item.log_ticket },
                type: "ui",
                event: "right click",
                on: data.what,
                searcher: item.searcher,
                page: item.pg,
                i: item.offset,
                title: item.title
            });
        }
    }
    function extendElemFromMin (elem) {
        var 
            thumb    = new Thumb (elem),
            item     = canonItem (getItem (elem)),
            preview  = new PreviewWindow (),
            q        = $("#q") ;

        bg (function () { 
            var id = item.searcher + "_" + item.offset,
                addthis_made = 0;

            // setup right click logging
            $(elem).bind ("mousedown", {
                item: item,
                what: "thumb"
            }, rightClickLogger);
            preview.container.bind ("mousedown", {
                item: item,
                what: "preview"
            }, rightClickLogger);

            preview.container.insertAfter (thumb.container);
            preview
                .title (item)
                .description (item)
                .toPageClick (item)
                .source_page (shortenUrlForDisplay (item.display_page))
                .customizeForSearcher (item);
            preview.callbacks ({
                pin: function () { thumb.addClass ("pinned"); },
                unpin: function () { thumb.removeClass ("pinned"); },
                hide: function () { thumb.removeClass ("hover"); },
                show: function () {
                    thumb.addClass ("hover");

                    // clear any google autocomplete.
                    searchMash.GAC.clear ();

                    if (addthis_made) { return; } 
                    addthis_made=1;

                    bg (function () {
                        var container;
                        container = preview.ns.elements.inner;


                        searchMash.load_addthis (function () { 
                            makeAddThis (function () { 
                                    var sharelink;

                                    sharelink = getShareLink ({ 
                                        image: getTopImage (preview),
                                        q: item.query,
                                        page: item.page
                                    });

                                    return sharelink ? base_url + sharelink : false;
                                }, "Pictures.com: " + item.title)
                                .appendTo (container);
                        });
                    });
                }
            });

            preview.low_rez (item.images[item.thumb_images[0]], { 
                done: function () {
                    // TODO don't block on first low_rez to setup mouseovers
                    setupMouseOvers (item, thumb, preview);
                    setupPreviewLogging (preview, item);
                    util.delta ("mouseovers setup", id);
                }
            });
        });


        thumb.anchor
            .thumbToPageClick (item, {
                config: { handler: function () {
                    preview.previewUseEnd (true);
                    return true;
                }}
            });
    }

    $.extend (SM, {
        extendFromMin: extendFromMin,
        PreviewWindow: PreviewWindow
    });

}) (jQuery, searchMash);


/* 
 * youtube preview window customization 
 */
(function ($, PreviewWindow) { 
    var dbg = searchMash.dbg,
        util = searchMash.util;

    PreviewWindow.prototype.youtubeCustomize = function (item) { 
        var config = searchMash.config.youtube, 
            matches,
            youtube_id,
            preview = this,
            swfurl,
            grayarea = preview.ns.elements.grayarea,
            youtube_div = $('<div class="embedded_youtube"/>'),
            id = [ "youtube_embed", item.pg, item.offset].join ("_");

        if (!config.enabled) { 
            return;
        }

        // TODO have this info passed cleanly from youtube data api
        // "http://www.youtube.com/watch?v=q7nxaT7h8CE&feature=youtube_gdata"

        youtube_div
            .appendTo (grayarea);

        $("<div/>") 
            .id (id)
            .appendTo (youtube_div);

        matches = /.*?youtube.com\/watch\?v=([^&]+)/.exec (item.page);
        if (matches) { 
            youtube_id = matches[1];
            swfurl = [ 
                "//www.youtube.com/v/", 
                youtube_id,
                '?autoplay=' ,
                config.autoplay ? 1 : 0,
                '&fs=1'
            ].join ("");
        }

        util.loadList (
            searchMash.scripts.swfobject,
            function () { 
                var none = undefined;
                swfobject.embedSWF (
                    swfurl, // url of flash
                    id,     // id of content to replace with flash
                    "0",     // width
                    "0",     // height
                    "8",    // flash version
                    none,   // express install url
                    none,   // flashvars
                    // params 
                    { allowfullscreen: true }, 
                    none,   // attributes
                    none);  // callback function
            }
        );
    };
}) (jQuery,searchMash.PreviewWindow);

/*
 * ebay preview window customization
 */
(function ($,PreviewWindow) {

    function ebayFormatPrice (price, currency) {
        if ( currency == "USD" ) {
            return "$" + price.toFixed (2);
        } else {
            return currency + " " + price.toFixed (2);
        }

    }

    function ebayMakeCategoryURL (category_id) {
        // guessed from urls produced by:
        // https://publisher.ebaypartnernetwork.com/PublisherToolsLinkGenPage
        return "http://rover.ebay.com/rover/1/711-53200-19255-0/1?" +
            "icep_ff3=9" +
            "&pub=5574709275" +
            "&campid=5336489495" +
            "&customid=" +
            "&icep_sellerId=" +
            "&icep_ex_kw=" +
            "&icep_sortBy=12" +
            "&ipn=psmain" +
            "&icep_vectorid=229466" +
            "&kwid=902099" +
            "&mtid=824" +
            "&icep_catId=" +
            category_id;
    }

    function ebayMakeCategoryLink (preview, item) {
        var data = item.vdata,
            cat_name = data.PrimaryCategoryName,
            cat_id = data.PrimaryCategoryID,
            link,
            href;
        if ( cat_name && cat_id ) {
            href = ebayMakeCategoryURL (cat_id);
            link = $("<a/>").text (cat_name);
            preview.clickOut (link, item, {
                clicked_on: "category label",
                url: href
            });
            return link;
        } else {
            // have to return something that can be appended to the dom or
            // you'll get a nasty error when you try. ;)
            return $("<span/>");
        }
    }

    function ebayUpdateTimeRemaining (till, div, interval) {
        var remain = till.getTime () - new Date ().getTime (),
            msPerSec = 1000,
            msPerMin = msPerSec * 60,
            msPerHour = msPerMin * 60,
            msPerDay = 24 * msPerHour,
            days, hours, mins, secs;

        if ( remain <= 0 ) {
            div.text ("Auction Ended");
            clearInterval (interval);
            return;
        }
        days = Math.floor (remain / msPerDay);
        remain = remain - days * msPerDay;
        if (days) {
            div.text (days + " days");
        } else {
            hours = Math.floor (remain / msPerHour);
            remain = remain - hours * msPerHour;
            mins = Math.floor (remain / msPerMin);
            remain = remain - mins * msPerMin;
            secs = Math.floor (remain / msPerSec);
            div.text (
                (hours ? (hours + "h")  : "") +
                ((mins || hours ) ? (mins + "m")  : "") +
                secs + "s");
            remain = remain - secs * msPerSec;
        }
            // div.text ("Time: " +
            //     ( days ? (days + "d ") : ""  ) +
            //     ((hours || days) ? (hours + "h")  : "") +
            //     ((mins || hours || days) ? (mins + "m")  : "") +
            //     secs + "s");

    }

    $.extend (PreviewWindow.prototype, {

        ebayCustomize: function (item) {
            var ns         = this.ns,
                e          = ns.elements,
                preview    = this,
                data       = item.vdata,
                price      = data.ConvertedCurrentPrice,
                ebay       = $("<div/>") .addClass ("ebay_preview"),
                e_price    = $("<div/>") .addClass ("price"),
                e_remain   = $("<div/>") .addClass ("time_remaining"),
                e_remain_a = $("<a/>"),
                e_bids     = $("<div/>") .addClass ("bid_count"),
                e_sidebox  = $("<div/>") .addClass ("sidebox"),
                e_logo     = $("<a/>")   .addClass ("logo"),
                e_desc     = $("<div/>") .addClass ("description"),
                e_category = ebayMakeCategoryLink (preview, item)
                    .addClass ("category"),
                e_title    = e.title,
                end_time   = new Date (),
                end_seconds,
                interval_id;

            end_seconds = new Date ().getTime () / 1000 + data.timeRemaining;
            end_time.setTime (end_seconds * 1000);

            ns.ebay_end_time = end_time;


            $("<img/>")
                .src ("/images/eBay/US/Right Now 108x45.gif")
                .appendTo (e_logo);

            preview.clickOut (e_logo, item, {
                clicked_on: "logo",
                url: "http://www.ebay.com/api/index.html"
            });

            $("<a/>")
                .text (ebayFormatPrice (price.Value, price.CurrencyID))
                .each (function () {
                    preview.clickOut (this, item, { clicked_on: "price" });
                })
                .appendTo (e_price);

            preview.clickOut (e_remain_a, item, {clicked_on: "time remaining"});

            e_title
                .wrapInner ("<a/>")
                .children ("a")
                    .each (function () {
                        preview.clickOut (this, item, {clicked_on: "title"});
                    });


            // TODO we could  periodically query ebay and update bid count
            if ("BidCount" in data) {
                $("<a/>")
                    .each (function () {
                        preview.clickOut (this, item, { clicked_on: "bids" });
                    })
                    .text (
                        data.BidCount +
                        " bid" +
                        (data.BidCount == 1 ? "" : "s")
                    )
                    .appendTo (e_bids);
            }

            // HACK: GAH! resorting to a table so that we can have the image on
            // the right without having the  text of the title wrap unerneath
            // the logo.  tried floating a description box to the left of the
            // left floated logo but depending on the content size it sometimes
            // dropped below the logo what we really wanted here was two column
            // layout, but css two column layout mostly requires setting widths
            // of things, and we want the second column to be fluid.  so we
            // probably need to dynamically size them when we size the other
            // preview container items.  gave up for now.
            $("<tr/>")
                .append ("<td/>")
                .append ("<td/>")
                .appendTo ("<tbody/>")
                .children ("td:first")
                    .addClass ("left")
                    .append (e_logo)
                .end ()
                .children ("td:last")
                    .addClass ("right")
                    .append (e_desc)
                .end ()
                .parent ()
                    .appendTo ("<table/>")
                    .parent ()
                        .appendTo (ebay);

            ebay
                .insertBefore (e.grayarea);

            e_remain
                .append (e_remain_a);

            e_sidebox
                .append (e_price)
                .append (e_remain)
                .append (e_bids);

            e_desc
                .append (e_sidebox)
                .append (e_category)
                .append (e_title);

            function update () {
                ebayUpdateTimeRemaining (end_time, e_remain_a, interval_id);
            }

            this
                .bind ("preview_show", function () {
                    update ();
                    interval_id = setInterval (update, 1000);
                })
                .bind ("preview_hide", function () {
                    clearInterval (interval_id);
                });

            update ();
        }

    });

}) (jQuery, searchMash.PreviewWindow);


(function ($) { 
    var searchMash  = window.searchMash,
        mashConfig  = searchMash.config,
        util        = searchMash.util,
        logEvent    = util.logEvent,
        conf        = mashConfig.preview_toggle,
        cookie_name = conf.cookie_name,
        dbg         = searchMash.dbg;


    function isPreviewOn () {
        return !mashConfig.preview_disabled;
    }

    function setPreview (indicator, on) {
        var checkbox = indicator.checkbox,
            span = indicator.status_span,
            a = indicator.a,
            text = on ? "on" : "off",
            style = on ? "hover" : "simple",
            title = on ? conf.features_disable_text : conf.features_enable_text;

        // update the image
        a
            .title (title);

        checkbox
            .attr ({checked: on});

        span
            .text (text);

        // update the global setting.
        mashConfig.preview_disabled = style == "simple";
        return style;
    }

    function setDisplayCookie (value, timestamp) {
        timestamp = timestamp || new Date () .getTime ();
        util.cookie (
            cookie_name,
            value + ":" + timestamp, {
                // the end of time.  a y2038 bug.
                expires: new Date ("Tue, 19 Jan 2038 00:00:00 GMT"),
                path: "/"
            });
    }

    function getDisplayCookie () {
        var cookie = util.cookie (cookie_name),
            matches = /(.*):(\d+)$/.exec (cookie);
        if ( matches ) {
            return {
                style: matches[1],
                timestamp: matches[2]
            };
        } else {
            return undefined;
        }
    }


    function initLightBulbFeatureSwitcher () {
        var container =   $("#preview_toggle"),
            checkbox =    container.find ("input"),
            status_span = container.find ("span"),
            anchor =      container.find ("a"),
            indicator = {
                status_span: status_span,
                a: anchor,
                checkbox: checkbox
            },
            cookie,
            on;

        // initially check cookie.
        cookie = getDisplayCookie ();
        if (cookie) {
            mashConfig.preview_disabled = cookie.style == "simple";
        }


        function handle_click () { 
            var state = !isPreviewOn (),
                style = setPreview  (indicator, state);

            // if they clicked. set a cookie to remember it.
            setDisplayCookie (style);

            logEvent ({
                config: { 
                    log_ticket: "1SwI-JQnew+features+selecte__" +
                        "DE977wWqFZEdxu5MCEYIpL-c"
                },
                event: "icon click",
                type: "user config act",
                what: "previews toggle",
                state: state
            });

            /* for some reason returning false for a click on the checkbox
             * seems to return the checkbox to it's original state. even though
             * we seem to succefully changed it's state in setPreview.  we need
             * to return false for the anchor click though. */
            return this.nodeName != "A";
        }

        checkbox 
            .click (handle_click);
        anchor
            .click (handle_click);

        setPreview (indicator, isPreviewOn ());
    }

    initLightBulbFeatureSwitcher ();

}) (jQuery);

/*
 * auto suggest for the input box
 */
(function ($) {
    var GAC,
        input;

    GAC = searchMash.GAC = {};

    // provide a function to clear the suggestion overlay. 
    // we do this by simulating hitting the escape key. since autosuggest
    // doesn't provide us any other interface.
    function clear () { 
        if (input) { 
            var ev = new jQuery.Event (
                $.browser.opera ? "keypress" : "keydown");
            ev.keyCode = 27;
            $(input).trigger (ev);
        }
    }
    GAC.clear = clear;

    function parseGAC (data) {
        var ret = [];
        $.each (data[1], function () {
            ret[ret.length] = {
                data: this,
                value: this[0],
                result: this[0]
            };
        });
        return ret;
    }

    function initTapHiglightFixup () {
        var UA = navigator.userAgent;
            if (! (/AppleWebKit/.test (UA) && /Mobile/.test(UA)) ) { 
                return;
            }

        // some real trickery here.  

        // the goal is to have the li's in the autocompletion div have a click
        // handler so that in iphone os, we can trigger the Tap Higlight feature

        // step 1: find the <ul> that has the suggestions it's only created on
        // first display of the suggestions.
        // TODO maybe don't start this polling until a keypress happens in the
        // search input box.
        function findAcResultsUL () { 
            var ul = $(".ac_results ul"); 
            if (ul.length == 0) { 
                return setTimeout (findAcResultsUL, 100);
            }

            // step 2: put a handler on the <ul> so that every time
            // new <li>'s are added to it we can add a no op click handler. 
            function click () { }

            ul.eq(0) // should only be one though, on our page.
                // add click handlers to any li's that get added.
                .bind ("DOMNodeInserted", function (event) { 
                    $(event.target).filter ("li").click (click);
                })
                // now add click handler to an li already there.
                .find ("li") 
                    .click (click);

        }
        findAcResultsUL ();
    }

    function init () { 
        var searchform = $('#search_form'),
            options = {
                dataType: "jsonp",
                parse: parseGAC,
                matchContains: true,
                matchSubset: false,
                selectFirst: false,
                scrollHeight: 300,
                extraParams: {
                    ds: "i",
                    client: "pdc"
                }
            };

        GAC.options = options;
        GAC.input = input = searchform.find('#q');
        searchMash.dbg ("autocomplete init attempt", searchform.get(0), input.get(0));

        input
            .attr ("autocomplete", "off")
            .autocomplete ("http://clients1.google.com/complete/search", options)
            .result (function () { searchform.submit (); });

        initTapHiglightFixup ();

        searchMash.util.addEvent (window, "scroll", clear);
        searchMash.util.delta ("autocomplete configured");
    }

    // so that we can be loaded before, after or with jquery.autocomplete we
    // just poll hwere until jquery autocmplete is available.  we load after
    // jquery autocomplete on a no search page, but on the search results page
    // we may load in parallel with or before.
    function tryInit () { 
        if ($.fn.autocomplete) { 
            init ();
        } else { 
            setTimeout (tryInit, 100);
        }
    }

    tryInit ();

})(jQuery);
