// IMDbPro instant search
// created by: Dave Zaccaria
// davez@imdb.com

var InstantSearch = (function($) {
    var obj = {};

    /////////////////////////////////////////////
    // set variables
    /////////////////////////////////////////////
    obj.sequence          = 1;
    obj.previousSequence  = 0;
    obj.cachedSearch      = new Array();
    obj.previousSearchVal = null;
    obj.maxWidth          = 230;
    obj.jsonpTimeout      = {};
    obj.clickFlag         = false;
    obj.clickTimeout      = null;
    obj.redirect          = function(e) {
        window.location.href = e;
    }

    /////////////////////////////////////////////
    // initalize user interactions
    /////////////////////////////////////////////
    obj.init = function() {
        $(document).ready(function() {
            $('#searchField')
                .attr('autocomplete', 'off')
    
                // trigger on a keydown so we can translate a highlight move
                .keydown( obj.keydown )
    
                // trigger on a keyup to get / display results
                .keyup(function() {
                    obj.startSearch();
                })
    
                // if the instant search box is hidden
                // and has at least 1 char in it
                // trigger on a focus to fake a keyup action and get / display results
                .focus(function() {
                    if ( $('#instantSearch').is(':hidden') &&
                         $('#searchField').val().length >= 1 ) {
                        obj.startSearch();
                    }
                });
 
            // hide instant search when searchField loses focus
            // AND the user's mouse is not over the results
            $('#searchField').focusout(function(e) {
                if ( !obj.clickFlag ) {
                    $('#instantSearch').hide();
                }
            });

            // focusout happens before a users click
            // so we must know where the users mouse is before they focusout
            $('#instantSearch')
                .mouseover(function() {
                    obj.clickFlag = true;
                })
                .mouseout(function() {
                    obj.clickFlag = false;

                    // giving focus to the searchField prevents odd behaviour
                    // like clicking a header in the results list
                    $('#searchField').focus();
                });

            // apply hover logic to results that don't exist yet
            $('#instantSearch li a').live("mouseover mouseout",function(event) {
                // add class="hover" to the suggestion that's mouse'd over
                if ( event.type == 'mouseover' ) {
                    $('#instantSearch li a.hover').removeClass('hover');
                    $(this).addClass('hover');
                }
                else {
                    $('#instantSearch li a.hover').removeClass('hover');
                }
            });

        });
    }

    /////////////////////////////////////////////
    // keydown
    /////////////////////////////////////////////
    obj.keydown = function(e) {
        // save the value of the search field before it changes 
        obj.previousSearchVal = $('#searchField').val();

        // build list with all current results
        var results = $('#instantSearch li').not('.section').children('a');

        switch (e.keyCode) {
            // enter - go to link
            case 13:
                if ( $(results).hasClass('hover') ) {
                    e.preventDefault();
                    obj.redirect($('#instantSearch li a.hover').attr('href'));
                }
                break;
            // Up
            case 38:
                $(results).each(function(i) {
                    // If a link is hovered over 
                    // Select the previous element in the results list
                    // unless it's the first element
                    if ( $(this).hasClass('hover') &&
                         i >= 1 ) {

                        $(this).removeClass('hover');
                        $(results[i-1]).addClass('hover');

                        return false;
                    }
                });
                break;
            // Down
            case 40:
                var isSelected;

                $(results).each(function(i) {

                    // If a link is hovered over
                    // Select the next element in the results list
                    // unless it's the last element
                    if ( $(this).hasClass('hover') ) {
                        isSelected = true;

                        if ( i < results.length-1 ) {

                            $(this).removeClass('hover');
                            $(results[i+1]).addClass('hover');

                            return false;
                        }
                    }
                });

                // If there are results and nothing's been selected yet, 
                // select the first result
                if ( typeof(results[0]) != 'undefined' && !isSelected ) {
                    $(results[0]).addClass('hover');
                }

                break;
            default:
                break;
        }
    }

    /////////////////////////////////////////////
    // starting a search
    /////////////////////////////////////////////
    obj.startSearch = function() {
        var searchVal = $('#searchField').val();

        // if the search field is empty
        // clear all resuts, hide and return
        if ( !searchVal ) {
            obj.spinningWheel('hide');
            $('#instantSearch li').not('.more').remove();
            $('#instantSearch').hide();
            return false;
        }

        $('#instantSearch').show();

        // stop if the value hasn't changed AND the cached result isn't error
        if ( obj.previousSearchVal === searchVal && 
             obj.cachedSearch[searchVal] != 'error' ) {
            return false;
        }

        $('#instantSearch .more a').attr('href', '/find?s=all&q='+searchVal);
        $('#searchVal').text(searchVal);

        // it will be a number if the ajax request started and not finished
        // we don't want to send double requests, so do nothing
        // and it will do the right thing when the ajax request finishes
        if ( typeof(obj.cachedSearch[searchVal]) === 'number' ) {
            return false;
        }

        // use the cached version if available
        // and it wasn't an error
        if ( obj.cachedSearch[searchVal] && 
             obj.cachedSearch[searchVal] != 'error'  ) {
            obj.finishCallback(obj.cachedSearch[searchVal], searchVal);
        }
        // this is a brand new search, make an ajax request
        else {
            obj.getJSON(searchVal);
        }
    }

    /////////////////////////////////////////////
    // make ajax calls
    /////////////////////////////////////////////
    obj.getJSON = function(value) {

        // before send
        obj.spinningWheel('show');
        obj.cachedSearch[value] = obj.sequence;
        obj.sequence++;

        $.getJSON('http://'+instantSearchDomain+'/api/instantSearch?callback=?',
            {q:value},
            // success
            function(data) {
                clearTimeout(obj.jsonpTimeout[data.q]);
                obj.displayAndSave(data, data.q);
            });

        // error (if timeout goes all the way, assume there is an error)
        obj.jsonpTimeout[value] = 
            setTimeout(function() {
                (new Image()).src='/rg/instantSearchError/'+value+'/images/b.gif';
                obj.displayAndSave('error', value);
            }, 3000);
    }

    /////////////////////////////////////////////
    // figure out whether to display the result & cache it
    /////////////////////////////////////////////
    obj.displayAndSave = function(json, value) {
        // cachedSearch([value]) is value's sequence number
        // check that it is more recent than the last value we displayed
        // and make sure the value is in the search field
        if ( parseInt(obj.cachedSearch[value]) > parseInt(obj.previousSequence)
             && $('#searchField').val().indexOf(value) >= 0 ) {

            obj.previousSequence = obj.cachedSearch[value];
            obj.finishCallback(json, value);
        }

        // save the result to keep from making duplicate ajax requests
        obj.cachedSearch[value] = json;
    }

    /////////////////////////////////////////////
    // process results
    /////////////////////////////////////////////
    obj.finishCallback = function(json, value) {

        // remove all results and stop spinning wheel
        //  the user isn't gonig to be getting any results
        if ( json === 'error' ) {
            if ( value === $('#searchField').val() ) {
                // hide spinning wheel because this is the last search requested
                obj.spinningWheel('hide');
            }
            $('#instantSearch li').not('.more').remove();
            return false;
        }

        // helper function to process each json return type
        function buildList(jsonObj, title, type, imageName, value) {
            var results = '';
            if ( jsonObj.length > 0 ) {
                var results = '<li class="section"><span class="text">'+title+'</span></li>';
        
                $(jsonObj).each(function() {
                    results += obj.formatResult(
                        type,
                        this,
                        '/images/pro/'+imageName+'-44x32.png',
                        value
                    );
                });
            }
            return results;
        }

        // Build the results list based on returned json
        var results = '';
        results += buildList(json.name, 'People', 'name', 'people', value);
        results += buildList(json.title, 'Titles', 'title', 'films', value);
        results += buildList(json.company, 'Companies', 'company', 'companies', value);

        // remove the old results before displaying new ones
        $('#instantSearch li').not('.more').remove();
        $('#instantSearch li.more').before(results);

        // stop the wheel from spinning after results are displayed 
        // only if the returned text matched the searched text
        var searchVal = $('#searchField').val();
        if ( json.q === searchVal ) { 
            obj.spinningWheel('hide');
        }
    }

    /////////////////////////////////////////////
    // format individual responses
    /////////////////////////////////////////////
    obj.formatResult = function(type, jsonObj, defaultImg, value) {

        var id = jsonObj.id;
        var displayMain = jsonObj.displayMain;
        var displayBottom = jsonObj.displayBottom;
        var displayExtra = jsonObj.displayExtra;
        var displayImage = jsonObj.image;

        // required json args, data is corrupt if they're missing
        if ( !id || !displayMain ) {
            return '';
        }

        var result = '<li>';

        result += '<a href="/'+type+'/'+id+'/">';

        if ( displayImage ) {
            result += '<img src="'+displayImage+'" height="44" width="32" />';
        }
        else {
            result += '<img src="'+defaultImg+'" height="44" width="32" />';
        }

        result += '<span class="text"><span class="title">';

        result += obj.cropText(
                    displayMain,
                    'title',
                    value,
                    type,
                    displayExtra
                  );

        if ( displayExtra ) {
            result += ' '+displayExtra;
        }

        result += '</span>';

        if ( displayBottom ) {
            result += obj.cropText(
                        displayBottom,
                        null,
                        value,
                        type,
                        null
                      );
        }

        result += '</span></a></li>';

        return result;
    }

    /////////////////////////////////////////////
    // show / hide the spinning wheel
    /////////////////////////////////////////////
    obj.spinningWheel = function(state) {
        switch(state) {
            case 'show':
                $('#instantSearch li.more img.mag').hide();
                $('#instantSearch li.more img.wheel').show();
                break;
            case 'hide':
                $('#instantSearch li.more img.wheel').hide();
                $('#instantSearch li.more img.mag').show();
                break;
        }
    }

    /////////////////////////////////////////////
    // crop text
    /////////////////////////////////////////////
    obj.cropText = function(text, className, value, type, displayExtra) {
        // Use this to crop text with an ellipsis so it won't break into 2 lines
        // also to wrap the text that matches the search in a span
        // we do this by creating a temporary list item with all the same styles
        // as the final product and measuring the width in px
        // if the width is larger than our predefined max width, we trim a 
        //letter off the end, replace the text & measure again 
        var trimmed = 0;
        var maxWidth = obj.maxWidth;

        var li = document.createElement('li');
        var a = document.createElement('a');
        var span = document.createElement('span');
        var textWrapper = document.createElement('span');

        $(span)
            .addClass('text cropping');


        // apply the same class the large text has, so we can correctly crop
        if ( className ) { 
            $(textWrapper).addClass(className); 
        }

        var displayExtraWidth;

        if ( displayExtra ) {
            $(textWrapper).append(displayExtra);
            $(span).append(textWrapper);
            $(a).append(span);
            $(li).append(a);
            $('#instantSearch').append(li);

            displayExtraWidth = $(span).width();
        }

        // titles need to make room for the year
        if ( displayExtra ) {
            maxWidth = maxWidth - displayExtraWidth;
        }

        $(textWrapper).empty();
        $(textWrapper).append(text);
        $(span).append(textWrapper);
        $(a).append(span);
        $(li).append(a);
        $('#instantSearch').append(li);
        
        var currentWidth = $(span).width();
        while ( currentWidth > maxWidth ) {
            // tell us it's being trimmed so we can add an ellipsis
            trimmed = 1;

            // trimm the text by 1 character, replace & re-measure it 
            text = text.substr(0,text.length - 1);
            $(textWrapper).html(text);
            currentWidth = $(span).width();
        }

        // clean up display
        $('#instantSearch li:last').remove();

        // add an ellipsis if the string was trimmed
        if ( trimmed ) {
            text += '\u2026';
        }

        var e = value.replace(/([.?*+^$[\]\\(){}-])/g, "\\$1");
        var re = new RegExp('('+e+')', 'gi');
        return text.replace(re, '<span class="matched">$1</span>');
    }

    /////////////////////////////////////////////
    // run init to get things going
    /////////////////////////////////////////////
    obj.init();

    /////////////////////////////////////////////
    // return all obj elements
    /////////////////////////////////////////////
    return obj;
})(jQuery);

