function SearchWhereHelper(
    strJsonEndpoint,
    nodeForm,
    nodeInput,
    nodeCountry,
    strTextClose,
    nodeAddSuggest,
    strCoordType,
    boolAutoSubmit
)
{
    var
        // private properties
        thisRef = this,
        boolContainerInitialized = false,
        nodeResultList = null,
        nodeClose = null,
        nodeContainer = null,
        strLastSearch = null,
        strLastInput = null,
        intResultSize = 0,
        intSelectedEntry = null,
        arrRowClasses = ['odd','even'],
        boolOpen = false,
        boolCloseForced = false,
        funcCloseLaterCallback = null
    ;
    
    // private methods
    function init() {
        var nodeCountrySet;
        
        // init nodes
        nodeForm    = jQuery('#'+nodeForm);
        nodeInput   = jQuery('#'+nodeInput);
        nodeAddSuggest = jQuery('#'+nodeAddSuggest);
        
        nodeCountrySet = jQuery('#'+nodeCountry);
        if (nodeCountrySet.length > 0) {
            nodeCountry = nodeCountrySet;
        }
        
        // assure other types
        if (boolAutoSubmit === undefined) {
            boolAutoSubmit = true;
        } else {
            boolAutoSubmit = Boolean(boolAutoSubmit);
        }
        
        // add listener
        nodeForm.bind('submit', function(){thisRef.insertData();});
        nodeInput.bind('keyup', function(e){thisRef.keyPress(e);});
        nodeInput.bind('focus', function(){thisRef.search();});
        
        jQuery(document)
            .bind('click',function(e){thisRef.bodyClick();})
            .ready(function(){thisRef.initContainer();});
    }
    function positionContainer() {
        var offset;
        thisRef.initContainer();
        offset = nodeInput.offset();
        offset.top = parseInt(offset.top + nodeInput.outerHeight() + 1, 10);
        offset.left = parseInt(offset.left, 10);
        nodeContainer.offset(offset);
    }
    function getSplitText(mixSplit, boolMarkMatch) {
        if (typeof mixSplit == 'object') {
            if (boolMarkMatch) {
                return mixSplit.pre +
                    '<span class="match">' +
                    mixSplit.match +
                    '</span>' +
                    mixSplit.post;
            } else {
                return mixSplit.pre + mixSplit.match + mixSplit.post;
            }
        } else {
            return mixSplit;
        }
    }
    function getDisplayValue(objData) {
        var strValue = '';
        if (objData.zipDsp) {
            strValue += getSplitText(objData.zip, true);
            strValue += ', ';
        }
        if (objData.cityDsp) {
            strValue += getSplitText(objData.city, true);
        }
        if( objData.regionDsp && objData.region != '') {
            strValue += ' (' + getSplitText(objData.region, true) + ')';
        }
        return strValue;
    }
    function getSearchValue(objData) {
        var strSearch = 
            getSplitText(objData.zip, false) +
            ', ' + getSplitText(objData.city, false)
        ;
        if (objData.region != '') {
            strSearch += ', ' + getSplitText(objData.region, false);
        }
        return strSearch;
    }
    function buildRow(strText, strSearch, intIdx) {
        var nodeText, intClassIdx;
        intClassIdx = intIdx % arrRowClasses.length;
        nodeText = jQuery('<span />').html(strText);
        return jQuery('<div />')
            .addClass(arrRowClasses[intClassIdx])
            .attr('swh_searchvalue', nodeText.text())
            .append(nodeText)
            .bind('mouseover', function(){jQuery(this).addClass('hover');})
            .bind('mouseout', function(){jQuery(this).removeClass('hover');})
            .bind('click', function(){thisRef.resultListClick(intIdx);})
        ;
    }
    function refreshNodeCountry() {
        var nodeCountryNew = [];
        if (jQuery.isArray(nodeCountry) && nodeCountry.length > 0) {
            nodeCountryNew = jQuery('#'+nodeCountry.attr('id'));
        } else if(typeof nodeCountry == 'string') {
            nodeCountryNew = jQuery('#'+nodeCountry);
        }
        if (nodeCountryNew.length > 0) {
            nodeCountry = nodeCountryNew;
        }
    }
    
    // public methods
    this.initContainer = function() {
        if (boolContainerInitialized) {
            return;
        } else {
            boolContainerInitialized = true;
        }
        
        // disable autocomplete on input node
        nodeInput.attr('autocomplete', 'off');
        // 
        nodeResultList = jQuery(document.createElement('div'))
            .addClass('resultList');
        
        nodeClose = jQuery(document.createElement('div'))
            .addClass('close')
            .html(strTextClose);
        nodeClose.click(function(e){thisRef.close(true);});
        
        nodeContainer = jQuery(document.createElement('div'))
            .append(nodeResultList)
            .append(nodeClose)
            .addClass('SearchWhereHelperContainer')
            .hide()
            .css('position', 'absolute');
        
        jQuery('body').append(nodeContainer);
    };
    this.search = function() {
        var strSearchVal = nodeInput.val(), request;
        if (strSearchVal.length > 2) {
            if (strLastSearch != strSearchVal && !boolCloseForced) {
                strLastSearch = strSearchVal;
                request = {
                    url: strJsonEndpoint,
                    data: {
                        value: strSearchVal
                    },
                    success: function(data, status, request) {
                        thisRef.fillContainer(data);
                    }
                };
                // country node refresh
                refreshNodeCountry();
                if (nodeCountry) {
                    request.data[nodeCountry.attr('name')] = nodeCountry.val();
                }
                // coordType
                if (strCoordType) {
                    request.data.coordType = strCoordType;
                }
                jQuery.ajax(request);
            } else {
                this.open();
            }
        }
        
        return this;
    };
    this.searchLater = function(boolNowOnCheck) {
        if (Boolean(boolNowOnCheck) && strLastInput == nodeInput.val()) {
            this.search();
        } else {
            window.setTimeout(
                function(){thisRef.searchLater(true);},
                750
            );
        }
        strLastInput = nodeInput.val();
        return this;
    };
    this.open = function() {
        if(!boolCloseForced && intResultSize > 0) {
            nodeContainer.show();
            positionContainer();
            boolOpen = true;
        }
        return this;
    };
    this.close = function(boolForced) {
        nodeContainer.hide();
        intSelectedEntry = null;
        boolOpen = false;
        if (Boolean(boolForced)) {
            boolCloseForced = true;
        }
        return this;
    };
    this.insertData = function(){
        var nodeSelected = this.getSelectedEntry();
        if (nodeSelected) {
            nodeInput.val(jQuery(nodeSelected).attr('swh_searchvalue'));
            if (nodeAddSuggest) {
                nodeAddSuggest.val(1);
            }
        }
        return this;
    };
    this.submit = function() {
        nodeForm.submit();
        return this;
    };
    this.getSelectedEntry = function() {
        if (intResultSize > 0 && intSelectedEntry !== null ) {
            return nodeResultList.get(0).childNodes[intSelectedEntry];
        } else {
            return null;
        }
    };
    this.setSelectedEntry = function(intIdx) {
        var node, intIdxMax;
        if (intResultSize <= 0) {
            return;
        }
        // adjust idx
        intIdxMax = intResultSize - 1;
        if (intIdx < 0) {
            intIdx = intIdxMax;
        } else if(intIdx > intIdxMax) {
            intIdx = 0;
        }
        // drop old
        node = this.getSelectedEntry();
        if (node) {
            jQuery(node).removeClass('hover');
        }
        // update index
        intSelectedEntry = intIdx;
        // set new
        node = this.getSelectedEntry();
        if (node) {
            jQuery(node).addClass('hover');
        }
    };
    
    // listener
    this.keyPress = function(event) {
        if (typeof event != 'object') {
            return;
        }
        
        switch (event.keyCode) {
            case 38:
                if (intSelectedEntry === null) {
                    thisRef.setSelectedEntry(-1);
                } else {
                    thisRef.setSelectedEntry(intSelectedEntry - 1);
                }
                break;
            case 40:
                if (intSelectedEntry === null) {
                    thisRef.setSelectedEntry(0);
                } else {
                    thisRef.setSelectedEntry(intSelectedEntry+1);
                }
                break;
            default:
                thisRef.searchLater();
                break;
        }
    };
    this.fillContainer = function(arrData) {
        if (typeof arrData != 'object') {
            return;
        }
        nodeResultList.empty();
        intSelectedEntry = null;
        intResultSize = arrData.length;
        for (var i=0; i < intResultSize; ++i) {
            nodeResultList.append(buildRow(
                getDisplayValue(arrData[i]), getSearchValue(arrData[i]), i
            ));
        }
        if (intResultSize > 0) {
            this.open();
        } else {
            this.close();
        }
    };
    this.resultListClick = function(intIdx) {
        this.setSelectedEntry(intIdx);
        this.insertData();
        if (boolAutoSubmit) {
            this.submit();
        }
        this.close();
    };
    this.bodyClick  = function() {
        if (boolOpen &&
            !nodeInput.is(':focus') &&
            !nodeResultList.is(':focus') &&
            nodeResultList.find(':focus').length == 0
        ) {
            this.close();
        }
    };
    
    init();
    return this;
}
SearchWhereHelper.prototype = {};

