OFFICE = 12;
RETAIL = 17;
INDUSTRIAL = 5;
MULTIFAMILY = 10;
VACANTLAND = 52;
HOSPITALITY = 28;
FARMRANCH = 35;
RETAIL_COMMERCIAL = 49;
SPECIAL_PURPOSE = 43;
BIZ_OPP = 46;

MAIN_TITLE = "Search Results";

COMPARABLE = 110;
LISTING = 55;
SUITE = 80;
PROPERTY = 117;


ENTITY_MODE = LISTING;

FOR_SALE = 1006;
FOR_LEASE = 1007;

IS_IE = (navigator.userAgent.toLowerCase().indexOf("msie") > -1);

IS_LOGGED_IN = false;
IS_NATIONAL = false;

AJAXMethodCaller.ALWAYS_NOCACHE = true;

var callTime;
var queryAndLoadTime;
var renderTime;

DEBUG = false;
function toggleDebug() {$('#debug-log').toggleClass('hidden')};
function debug(msg) {
    if (!DEBUG) return;
    try {
        var logDiv = $("#debug-log");
        
        var now = new Date();
        var time = now.getHours() + ":" + pad(now.getMinutes(),2) + ":" + pad(now.getSeconds(),2) + "." + pad(now.getMilliseconds(),3);
        
        logDiv.append('<div><strong>' + time + '</strong> ' + msg + '<div>');
        if (logDiv.children().length > 20) {
             $("#debug-log").find("div:first").remove();
        }
    } catch (e) {alert(e)}
}
function pad(val, size) {
    val = '' + val;
    while (val.length < size) {
        val = '0' + val;
    }
    return val;
}

var errorMsgTimeout;
function showUserError(msg) {
    if (errorMsgTimeout) {
        clearTimeout(errorMsgTimeout);
    }
    
    $('#error-message').html(msg);
    
    $('#error-message').show();
    errorMsgTimeout = setTimeout(function() { $('#error-message').slideUp() }, 10000);
}

function EntityManager(searchProvider) {
    if (EntityManager.instance) {
        return EntityManager.instance;
    }
    EntityManager.instance = this;
    var thisManager = this;
    
    this.searchProvider = searchProvider;
    this.searchCriteria = null;

    this.overrideEntityIDs = null;

    this.entitiesByName = new Array();
    this.entityIDsInOrder = new Array();
    this.entityIndexByName = new Array();
    
    this.searchResultsCount = 0;
    
    this.setSearchCriteria = function(searchCriteria) {
        this.searchCriteria = searchCriteria;
        this.entityIDsInOrder = new Array();
        this.entityIndexByName = new Array();
    }

    this.getResultsAndCallBack = function(methodName, methodParameters, callback, errorCallback) {
        var thisManager = this;
        var caller = null;

        callTime = new Date().getTime();
        var handler = function() {
            var req = caller.ajax.requestor;
            if (req.readyState == 4) {
                try {
                    if (req.status == 200) {
                        if (req.responseXML) {
                            var root = req.responseXML.documentElement;
                            var count = root.getAttribute("count")
                            if (count) {
                                thisManager.searchResultsCount = count;
                            }
                            try {
                                var entities = null;
                                var entitiesNode = root.getElementsByTagName("entities");
                                if (entitiesNode && entitiesNode.length) {
                                    entities = eval(entitiesNode[0].childNodes[0].nodeValue);                           
                                }
                                if (entities) {
                                    for (var i=0;i<entities.length;i++) {
                                        thisManager.addEntity(new Mappable(entities[i]));
                                    }
                                }
                            } catch (err) {
                                if (DEBUG) debug("Unable to parse entities: " + err);
                            }
                            queryAndLoadTime = new Date().getTime();
                            if (callback) {
                                callback(req.responseXML);
                            }
                        }
                    } else if (req.status == 0) {
                        if (DEBUG) debug("AJAX response status 0");
                    } else {
                        if (DEBUG) debug("An AJAX response was not OK: " + req.statusText);
                        if (errorCallback) {
                            errorCallback();
                        }
                    }
                } catch (e) {
                    if (DEBUG) debug("An AJAX error has occurred: " + e);
                    if (errorCallback) {
                        errorCallback();
                    }
                }
            }
        }
        if (!this.searchProvider && methodName == "getEntity") {
        	var id = methodParameters[0].value;
        	var typeAndID = getEntityTypeAndID(id);
        	this.searchProvider = "com.catylist.ajax.{0}SearchProvider".format((typeAndID.type == COMPARABLE)?"Comparable":"Listing");
        }
        caller = new AJAXMethodCaller(this.searchProvider, methodName, handler);
        for (var i=0;i<methodParameters.length;i++) {
            caller.addParameter(methodParameters[i].type, methodParameters[i].value);
        }
        if (this.searchCriteria) {
            for (var key in this.searchCriteria) {
                caller.addRequestParameter(key, this.searchCriteria[key]);
            }
            delete this.searchCriteria["newSearch"];
        }
        if (methodName == "map") {
            new MapManager().stopListeningForEvents();
        }
        return caller.call();
    }

    this.clearEntities = function() {
        this.entitiesByName = new Array();
        this.entityIDsInOrder = new Array();
        this.entityIndexByName = new Array();
    }

    this.addEntity = function(entity) {
        var name = entity.ID.toString();
        if (!this.entitiesByName[name] || ! this.entitiesByName[name].isComplete() || (this.entityIndexByName[name] == null)) {
            var oldEntity = this.entitiesByName[name];
            //copy some stuff from oldEntity to the new Entity
            if (oldEntity && (oldEntity.index != null) && (entity.index == null)) {
                entity.index = oldEntity.index;
            }
            if (oldEntity && (oldEntity.marker) && (!entity.marker)) {
                entity.marker = oldEntity.marker;
            }
            this.entitiesByName[name] = entity;
            if ((entity["index"] != null) && entity.index >= 0) {
                this.entityIDsInOrder[entity.index] = entity.ID;
                this.entityIndexByName[name] = entity.index;
            }
        }
    }

    this.getEntity = function(name) {
        return this.entitiesByName[name.toString()];
    }

    this.retrieveEntityAndCallBack = function(name,callback) {
        var entity = this.getEntity(name);
        if (!entity || !entity.isComplete()) {
            this.getResultsAndCallBack("getEntity",[new Parameter("java.lang.String",name)],callback);
        } else {
            if (callback) {
                callback();
            }
        }
    }
    
}

var MARKER_HOME = "http://" + window.location.host + "/images/map/";

function Mappable(json) {
    for (key in json) {
        this[key] = json[key];
    }
    if (this.latitude < -90 || this.latitude > 90 || this.longitude < -180 || this.longitude > 180) {
        this.latitude = 0;
        this.longitude = 0;
    }
    
    this.point = new GLatLng(this.latitude,this.longitude);
    this.marker = null;
    
    this.isComplete = function() {
        return (this.overview || this.address);
    }

    var thisMappable = this;

    this.getName = function() {
        return this.ID.toString();
    }

    this.getMarker = function(icon) {
        var marker = this.marker;
        var markerIcon = icon||this.getIcon(this.propertyTypeID);
        var map = new MapManager().getMap();
        if (this.point == null || (this.point.lat() == 0 && this.point.lng() == 0)) {
            marker = null;
        } else if (! this.marker){
            marker = new GMarker(this.point, markerIcon);
            this.marker = marker;
            marker.mappableID = this.ID;
            marker.openInfoWindow = function() {
                //stop listening for map change events so that opening info windows won't cause inconveniently timed searches
                new MapManager().stopListeningForEvents();
                
                var mappable = new EntityManager().getEntity(marker.mappableID);
                var html = (mappable?mappable.getInfoHTML():"<img src=\"/images/search/loading.gif\" />");
                marker.openInfoWindowHtml(html,{maxWidth:330});
                if (!mappable || !mappable.isComplete()) {
                    //if not complete then get the mappable, and have the callback open the info window
                    var callback = function(xml) {
                        map.closeInfoWindow();
                        mappable = new EntityManager().getEntity(marker.mappableID);
                        marker.openInfoWindowHtml(mappable.getInfoHTML(),{maxWidth:330});

                        //check the checkbox
                        $("input:checkbox[value={0}]".format(marker.mappableID)).each(function(){this.checked=new ActionManager().isSelected(marker.mappableID)});
                        //start listening for events again
                        window.setTimeout(function(){new MapManager().startListeningForEvents()},4000);
                        
                        map.openInfoWindowMarker = marker;
                        new WindowState().save();
                    }
                    new EntityManager().retrieveEntityAndCallBack(marker.mappableID,callback);
                } else {
                    //check the checkbox
                    $("input:checkbox[value={0}]".format(marker.mappableID)).each(function(){this.checked=new ActionManager().isSelected(marker.mappableID)});
                    //start listening for events again
                    window.setTimeout(function(){new MapManager().startListeningForEvents()},4000);

                    map.openInfoWindowMarker = marker;
                    new WindowState().save();
                }
            }
            GEvent.addListener(marker, "click", function() {
                marker.openInfoWindow();
            });
            GEvent.addListener(marker, "infowindowclose", function() {
                if (map.openInfoWindowMarker) {
                    map.openInfoWindowMarker = null;
                    new WindowState().save();
                }
            });
        }
        return marker;
    }

    this.getIcon = function(propertyTypeID, ID) {
        var icon = Mappable.ICON;
        var entityTypeAndID = getEntityTypeAndID(this.ID);
        var type = (entityTypeAndID.type == PROPERTY)?"offmarket":"listing";
        if (propertyTypeID == 12) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_office.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 17) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_shoppingcenter.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 5) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_industrial.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 10) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_multifamily.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 52) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_vacantland.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 28) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_hospitality.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 35) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_farmranch.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 49) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_retailcommercial.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 43) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_specialpurpose.png".format(MARKER_HOME,type);
        } else if (propertyTypeID == 46) {
            icon = new GIcon(Mappable.ICON);
            icon.image = "{0}{1}_bizopp.png".format(MARKER_HOME,type);
        }
        return icon;
    }     

    this.showDetails = function() {
        new DetailsView().showDetails(this.getName());
    }
    this.getInfoHTML = function() {
        var html = "<img src=\"/images/search/loading.gif\" />";

        if (this.isComplete()) {
            var entityTypeAndID = getEntityTypeAndID(this.ID);

            html = '<ul class="map-details">';
            html += '   <li id="bubble-title-{0}" class="title {8}">';
            html += '       <span class="input"><input type="checkbox" class="selectableEntity" value="{0}" title="Highlight this {9}" onclick="new ActionManager().toggleSelected(\'{0}\')" {10} /></span>';
            html += '       <a href="javascript:new DetailsView().showDetails(\'{0}\')">{6}</a><br />';
            html += '       <span class="address">{1}</span>';
            html += '   </li>';
            html += '   <li class="map-type">';
            html += '       <a href="javascript:new MapManager().getMap().zoomTo(new EntityManager().getEntity(\'{0}\'))" class="zoom">Zoom to {9}</a>{3}';
            if (entityTypeAndID.type != PROPERTY) {
                html += ' for {11}';
            }
            html += '   </li>';
            if (this.thumbnail) {
                html += '               <li class="image"><a href="javascript:new DetailsView().showDetails(\'{0}\')"><img class="mapInfoWindowImage" src="{2}" alt="{1}" border="0" /></a></li>\n';
            }
            if (this.price) {
                html += '   <li>{4}</li>';
            }
            html += '   <li>{5}</li>';
            html += '   <li>ID #{0}</li>';
            html += '   <li class="overview">{7}</li>';
            html += '   </ul>';
            var selectedString = (new ActionManager().isSelected(this.ID)?" checked='true' ":"");
            var leaseOrSale = eval(this.isLeaseAndSale)?"Sale or Lease":(eval(this.isLease)?"Lease":"Sale");
            
            var desc = "";
            if (entityTypeAndID.type == COMPARABLE) {
                desc = "Comparable";
            } else if (entityTypeAndID.type == LISTING) {
                desc = "Listing";
            } else if (entityTypeAndID.type == PROPERTY) {
                desc = "Property";
            }  else if (entityTypeAndID.type == SUITE) {
                desc = "Suite";
            } 
            html = html.format(this.ID,this.address,this.thumbnail,this.propertyType,this.price,this.size,this.title, this.overview,(new ActionManager().isSelected(this.ID)?" highlighted ":""),desc,selectedString, leaseOrSale);
        }
        return html
    }
}
Mappable.ICON = new GIcon();
Mappable.ICON.image = MARKER_HOME + "marker_red.png";
Mappable.ICON.iconSize = new GSize(28, 32);
Mappable.ICON.shadow = MARKER_HOME + "marker_shadow.png";
Mappable.ICON.shadowSize = new GSize(48, 31); 
Mappable.ICON.iconAnchor = new GPoint(14, 31);
Mappable.ICON.infoWindowAnchor = new GPoint(9, 2);
Mappable.ICON.infoShadowAnchor = new GPoint(18, 25);

function TabsManager(tabsContainerID) {
    if (TabsManager.instance) {
        return TabsManager.instance;
    }
    TabsManager.instance = this;
    
    this.tabsContainerID = tabsContainerID;
    
    this.active = 1;
    this.lastActive = 1;
    
    this.tabs = new Array();
    this.tabs.push(new Object()); // the indices are 1-based
    
    this.onShow = function(tabIndex) {}
    this.onEnable = function(tabIndex) {}
    this.onDisable = function(tabIndex) {}
    
    this._doOnShow = function(tabIndex) {
        this.onShow(tabIndex);
    }
    this._doOnEnable = function(tabIndex) {
        this.onEnable(tabIndex);
    }
    this._doOnDisable = function(tabIndex) {
        this.onDisable(tabIndex);
    }
    
    this.triggerTab = function(tabIndex) {
        if (DEBUG) debug('triggering tab: ' + tabIndex);
        this.enableTab(tabIndex);
        
        var isSelected = false;
        var tab = this.tabs[tabIndex];
        if (tab) {
            isSelected = tab.tab.hasClass('active');
        }
        
        if (!isSelected) {
            for (var i = 1; i < this.tabs.length; i++) {
                this.tabs[i].tab.removeClass('active');
                this.tabs[i].container.addClass('tabs-hide');
            }
            
            if (tab) {
                tab.tab.addClass('active');
                tab.container.removeClass('tabs-hide');
                
                this.lastActive = this.active;
                this.active = tab.index;
                
                this._doOnShow(tab.index);
            }
        }
        return this;
    }
    this.disableTab = function(tabIndex) {
        var tab = this.tabs[tabIndex];
        if (tab) {
            if (!tab.tab.hasClass('tabs-disabled')) {
                tab.tab.addClass('tabs-disabled');
                this._doOnDisable(tab.index);
            }
        }
        return this;
    }
    this.enableTab = function(tabIndex) {
        var tab = this.tabs[tabIndex];
        if (tab) {
            if (tab.tab.hasClass('tabs-disabled')) {
                tab.tab.removeClass('tabs-disabled');
                this._doOnEnable(tab.index);
            }
        }
        return this;
    }
    
    this.getTabIndex = function(tabID) {
        var tab = this.tabs[tabID];
        return tab ? tab.index : null;
    }
    
    this.lastActiveTab = function() {
        return this.lastActive;
    }
    this.activeTab = function() {
        return this.active;
    }
    this.getActiveTabID = function() {
        var tab = this.tabs[this.active];
        return tab ? tab.id : null;
    }

    var me = this;
    $('#' + this.tabsContainerID).find('ul:first li a').each(function(i) {
        var containerID = this.getAttribute('href');
        if (containerID.indexOf('#') > -1) {
            containerID = containerID.substring(containerID.indexOf('#') + 1);
        }

        $(this).parent().parent().addClass('tabs');
        
        var tab = ({
            container: $('#' + containerID), 
            tab: $(this).parent(),
            index: i + 1,
            id: containerID
        });
        
        tab.container.addClass('tabs-container');
        
        me.tabs.push(tab);
        me.tabs[containerID] = tab;

        $(this).click(function() {
            me.triggerTab(containerID);
            return false;
        });
    });

    this.triggerTab(1);
}

function getEntityTypeAndID(entityID) {
    var split = entityID.split('_');
    return ({"id": parseInt(split[1]), "type": parseInt(split[0]) });
}

function DetailsView() {
    if (DetailsView.instance) {
        return DetailsView.instance;
    }
    DetailsView.instance = this;
    
    this.currentEntityID = null;
    
    this.getCurrentEntityID = function() {
        if ($('#details-view-tab:visible').length > 0) {
            return this.currentEntityID;
        } else {
            return null;
        }
    }
    
    this.showDetails = function(entityID) {
        this._showDetails(entityID);
        
        // triggering tab will set a new details context
        new ActionManager().switchToTab('details-view');
    }
    
    // show details w/o changing tab, setting context, etc.
    this._showDetails = function(entityID) {
        if (!$('#details-tab').length) {
        	window.location = "/jsp/common/overview.jsp?ID={0}".format(entityID);
        } else if (this.currentEntityID != entityID) {
            this.currentEntityID = entityID;
            
            $('#details-tab').html('<div class="tab-cover"></div><img src="/images/search/loading_graphic.gif" alt="Loading" class="beavis" />');

            var typeAndID = getEntityTypeAndID(entityID);
            
            var url = "/jsp/search/overview.jsp?ID={0}".format(entityID);

            var callback = function() {
                var actionMgr = new ActionManager();
                actionMgr.unthrobTab("details-view");
                actionMgr.updateHighlight(entityID, actionMgr.isSelected(entityID)); 
                //window.widget = new Widget('addToCatalog_frame');
                
                SlideManager.__instance = null;
                new SlideManager($("div.singleLightboxImage"),0,"imagenumber","imagetotal");
                tb_init('a.thickbox, area.thickbox, input.thickbox');
                
                var id = new DetailsView().getCurrentEntityID();
                $('#map-action-link').click(function() { new MapManager().showEntity(id); return false; });
                $('#email-action-link').click(function() { actionMgr.email(id); return false; });
                $('#catalog-action-link').unbind('click');
                $('#catalog-action-link').click(function() { actionMgr.addToCatalog(id); return false; });
                $('#brochure-action-link').click(function() { actionMgr.createReport(id); return false; });
                var entityCallback = function() {
                    var entity = new EntityManager().getEntity(entityID);
                    var point = entity.point;
                    var lat = point.lat();
                    var lng = point.lng();
                    if (!(lat == -909090 && lng == -9090909) && !(lat == 0 && lng == 0)) {
                        var mapExtras = new MapExtras();
                        mapExtras.loadMSMap("msMap",point);
                        mapExtras.loadGStreetView("gStreetView",point);
                        mapExtras.loadGMap("gMap", "gMapDiv",point);
                    }
                }
                new EntityManager().retrieveEntityAndCallBack(entityID, entityCallback);

            }
            new ActionManager().throbTab("details-view");
            new ScriptManager().loadDetailsScripts(function() {
                new AHAH().load($('#details-tab')[0],url, callback);
            });
        }
        popupOnOverviewIfNotLoggedIn();
    }
}

function EmailView() {
    if (EmailView.instance) {
        return EmailView.instance;
    }
    EmailView.instance = this;
    
    this.currentEntityIDs = null;

    this.getCurrentEntityIDs = function() {
        if ($('#email-view-tab:visible').length > 0) {
            return this.currentEntityIDs;
        } else {
            return null;
        }
    }
    
    this.show = function(ids) {
        this._show(ids);

        // triggering tab will set a new context
        new ActionManager().switchToTab('email-view');
    }
    this._show = function(ids) {
        ids = isArray(ids)?ids:[ids];
        
        this.currentEntityIDs = ids;
        
        var url = "/jsp/search/email_compose.jsp?newEmail=true&ID=" + ids.join("&ID=");
        this._load(url);
        
        popupIfNotLoggedIn();
    }
    this._load = function(url) {
        var actionMgr = new ActionManager();
        actionMgr.throbTab('email-view');
        
        var me = this;
        var callback = function() {
            me.initEmailForm();
            actionMgr.unthrobTab('email-view');
        }
        new AHAH().load($('#email-tab')[0],url, callback);
    }
    
    // used by find recipients popup
    this._render = function(html) {
        $('#email-tab')[0].innerHTML = html;
        this.initEmailForm();
    }
    
    this.initEmailForm = function() {
        var emailURL = '/jsp/email/email_compose.jsp?ID=' + this.currentEntityIDs.join('&ID=');
        $("#email-file-attach").html('<a href="' + emailURL + '">Attach Files from Your Computer</a>');
        
        $("#email-file-attach").find("a").click(function() { 
            var newURL = emailURL;
            
            var rewriteFields = ['additionalRecipientString', 'subject', 'message'];
            for (var i = 0; i < rewriteFields.length; i++) {
                if (document.forms.emailForm[rewriteFields[i]].value) {
                    newURL += "&" + rewriteFields[i] + "=" + escape(document.forms.emailForm[rewriteFields[i]].value);
                }
            }
            this.href = newURL;
        });
        
        var form = new AJAXForm("emailForm");
        form.getAction = function () {
            return "/jsp/search/email_compose.jsp";
        }
        form.handleSuccess = function(req) {
            if (req.responseText.indexOf("ERRORMODE") > 0) {
                $('#email-tab').html(req.responseText);
                new EmailView().initEmailForm();
            } else {
                $('#email-tab').html("<div id='container'><div id='main'><h2>Your Email Has Been Sent.</h2></div></div>");
                var html = "<ul style=\"margin-top: 3px; padding-left: 0; list-style: none;\">";
                //html += "<li><a href=\"javascript:new ActionManager().emailSelected()\">Send another email</a></li>";
                html += "<li><a href=\"javascript:new ActionManager().switchToTab('list-view')\">Back to List View</a></li>";
                html += "<li><a href=\"javascript:new ActionManager().switchToTab('map-view')\">Back to Map View</a></li>";
                html += "</ul>";
                $('#email-tab #main').append(html);
                new EmailView().currentEntityIDs = null;
                new WindowState().save();
            } 

            new ActionManager().unthrobTab("email-view");
            
        }
        $("#emailForm").submit(function(){
            if (recipientsWindow) {
                recipientsWindow.close();
            }
            
            new ActionManager().throbTab("email-view");
            form.submit();
            return false;
        });    
    }
}

function ReportsView() {
    if (ReportsView.instance) {
        return ReportsView.instance;
    }
    ReportsView.instance = this;
    
    this.currentEntityIDs = null;
    
    this.getCurrentEntityIDs = function() {
        if ($('#reports-view-tab:visible').length > 0) {
            return this.currentEntityIDs;
        } else {
            return null;
        }
    }
    
    this.show = function(ids) {
        this._show(ids);

        // triggering tab will set a new context
        new ActionManager().switchToTab('reports-view');
    }
    this._show = function(ids) {
        ids = isArray(ids)?ids:[ids];
        
        this.currentEntityIDs = ids;
        
        var url = "/jsp/search/report_choosetype.jsp?compareReportType=false&action=newReport&initialLoad=initialLoad&ID=" + ids.join("&ID=");
        new AHAH().load($('#reports-tab')[0],url,null,true);        

        if (ids.length > 1) {
            popupIfNotLoggedIn();
        }
    }
}
function ExportView() {
    if (ExportView.instance) {
        return ExportView.instance;
    }
    ExportView.instance = this;
    
    this.currentEntityIDs = null;

    this.show = function(ids) {
        this._show(ids);

        // triggering tab will set a new context
        new ActionManager().switchToTab('export-view');
    }
    this._show = function(ids) {
        ids = isArray(ids)?ids:[ids];
        
        this.currentEntityIDs = ids;
        // TODO: HANDLE HETEROGENEOUS IDS
        var exportedEntityType = (ENTITY_MODE==LISTING)?"LISTING":"COMPARABLE";
        var url = "/jsp/search/export.jsp?exportType=1&exportedEntityType={0}&ID={1}".format(exportedEntityType,ids.join("&ID="));
        var callback = function() {
            //a little glue to get legacy check-all to work
            new CheckAllHandler("toggleFields","fieldNames");
            CheckAllHandler.handlers["toggleFields"].initialize();
            
            window["popUpExportWindow"] = function() {
                // Open the popup window
                window.open('','exportPopup','toolbar=no,location=no,directories=no,status=no,menubar=no,resizable=no,copyhistory=no,scrollbars=no,width=400,height=250');
                document.exportForm.target = 'exportPopup';
            }
        }
        new AHAH().load($('#export-tab')[0],url,callback,true);        
        popupIfNotLoggedIn();
    }
}

function AddToCatalogView() {
    if (AddToCatalogView.instance) {
        return AddToCatalogView.instance;
    }
    AddToCatalogView.instance = this;

    this.currentEntityIDs = null;

    this.getCurrentEntityIDs = function() {
        if ($('#catalog-view-tab:visible').length > 0) {
            return this.currentEntityIDs;
        } else {
            return null;
        }
    }
    
    this.show = function(ids) {
        this._show(ids);

        // triggering tab will set a new context
        new ActionManager().switchToTab('catalog-view');
    }
    this._show = function(ids) {
        ids = isArray(ids)?ids:[ids];
        
        this.currentEntityIDs = ids;

        var url = "/jsp/search/console_addto_catalog.jsp?action=prepareForAdd&ID=" + ids.join("&ID=");
        var callback = function() {
            var form = new AJAXForm("catalogForm");
            form.handleSuccess = function(req) {
                $('#catalog-tab').html(req.responseText);
                $('#addToCatalogGoBackLink').html("Back to list view");
                $('#addToCatalogGoBackLink').attr('href','javascript:new ActionManager().viewAll()');
                
                new AddToCatalogView().currentEntityIDs = null;
                new WindowState().save();
            }
            document.forms.catalogForm.action = '/jsp/search/console_addto_catalog_confirm.jsp';
            $(document.forms.catalogForm.addToCatalogButton)[0].onclick = function(){form.submit()};
            initAddToCatalog();
        }
        
        $('#catalog-tab').html("");
        new ScriptManager().loadCatalogScripts(function() { 
            new AHAH().load($('#catalog-tab')[0],url, callback);
        });
        
        popupIfNotLoggedIn();
    }
}

function ActionManager() {
    if (ActionManager.instance) {
        return ActionManager.instance;
    }
    ActionManager.instance = this;

    this.selectedEntityIDHash = new Array();
    
    this.viewSelected = function() {
        if (this._viewSelected()) {
            new ListView().show(1);
            var currentTabID = new TabsManager().getActiveTabID();
            if (currentTabID.indexOf("map") == -1) {
                this.switchToTab("list-view");
                //tell the map to redraw
                new MapManager("myMap").redraw.needs = true;
            } else {
                new MapManager("myMap").show();
            }
        }
    }
    this._viewSelected = function() {
        if (this.hasSelectedEntities()) {
            new EntityManager().overrideEntityIDs = this.getSelectedEntityIDs();
            this.updateDisplay();
            $('#search-filters,#primary-sitelink-filters').addClass("disabled");
            $('#left').append('<div id="criteria-disabler" class="disabled-overlay"></div>');
            
            $('#list-view').find('.hollow').removeClass("hidden");
            $('#list-view').addClass("blue-gray");
            
            new WindowState().save();
            return true;
        } else {
            return false;
        }
    }
    
    this.viewAll = function() {
        var tabToShow = "list-view";
        var currentTabID = new TabsManager().getActiveTabID();
        if (currentTabID.indexOf("map") != -1) {
            tabToShow = "map-view";
        }
        var list = new ListView();
        new EntityManager().overrideEntityIDs = null;
        this.updateDisplay();
        $('#criteria-disabler').remove();
        $('#search-filters,#primary-sitelink-filters').removeClass("disabled");
        
        $('#list-view').find('.hollow').addClass("hidden");
        $('#list-view').removeClass("blue-gray");

        list.show(1);
        this.switchToTab(tabToShow)
        new MapManager().show();

        new WindowState().save();
    }
    
    this.compareSelected = function() {
        var url = "/jsp/reports/report_comparelistings.jsp?compareReportType=true&ID={0}".format(this.getSelectedEntityIDs().join("&ID="));
        window.open(url,'comparePopUp','toolbar=yes,location=yes,directories=no,status=yes,menubar=yes,resizable=yes,copyhistory=no,scrollbars=yes,width=800,height=600');
    }
    this.emailSelected = function() {
        this.email(this.getSelectedEntityIDs());
    }
    this.email = function(ids) {
        new EmailView().show(ids);
    }
    this.exportSelected = function() {
       new ExportView().show(this.getSelectedEntityIDs())   
    }
    this.createReportWithSelected = function() {
        this.createReport(this.getSelectedEntityIDs());        
    }
    this.createReport = function(ids) {
        new ReportsView().show(ids);
    }
    
    this.addSelectedToCatalog = function() {
        this.addToCatalog(this.getSelectedEntityIDs());
    }
    this.addToCatalog = function(ids) {
        new AddToCatalogView().show(ids);
    }
    
    this.switchToTab = function(id) {
        //first hide the ones to be hidden
        var tabs = new TabsManager();
        tabs.disableTab('email-view').disableTab('reports-view').disableTab('catalog-view');   
        if (DEBUG) debug("Switching to tab: " + id);
        tabs.enableTab(id).triggerTab(id);
        new MapManager().setVisible((new TabsManager().getActiveTabID() == 'map-view'));
    }
    this.getActiveTab = function() {
        return new TabsManager().activeTab();
    }
    this.throbTab = function(id) {
        var tabs = $("#view-tabs");
        $("#{0}-tab > a".format(id)).prepend("<img src='/images/search/loading_circle.gif' class='tab-loading-graphic' />").parent().addClass("throbbing");
        tabs.find("li a span").hide();
    }
    this.unthrobTab = function(id) {
        var tabs = $("#view-tabs");
        $("#{0}-tab > a > img.tab-loading-graphic".format(id)).fadeOut("fast", function() { $(this).remove() });
        tabs.find("li a span").fadeIn();
        tabs.find(".throbbing").removeClass("throbbing");
    }

    this.isSelected = function(entityID) {
        return this.selectedEntityIDHash[entityID];
    }

    this.toggleSelected = function(entityID) {
        if (this.isSelected(entityID)) {
            this.deselectEntity(entityID);
        } else {
            this.selectEntity(entityID);
        }
    }

    this.getSelectedEntityIDs = function() {
        var ids = new Array();
        for (var i in this.selectedEntityIDHash) {
            ids.push(i);
        }
        return ids;
    }
    
    this.hasSelectedEntities = function() {
        var hasSelected = false;
        for (var i in this.selectedEntityIDHash) {
            hasSelected = true;
            break;
        }
        return hasSelected;
    }
    
    this.selectEntity = function(entityID, doNotUpdateDisplay) {
        if (!this.isSelected(entityID)) {
            this.selectedEntityIDHash[entityID] = true;
            this.updateHighlight(entityID, true);
            
            if (! doNotUpdateDisplay) {
                this.updateDisplay();
                this.dazzle();
                
                new WindowState().save();
            }
        }
    }
    this.selectEntities = function(entityIDs) {
        for (var i = 0; i < entityIDs.length; i++) {
            this.selectEntity(entityIDs[i],true);
        }
        this.updateDisplay();
        this.dazzle();

        new WindowState().save();
    }

    this.deselectEntity = function(entityID, doNotUpdateDisplay) {
        this.updateHighlight(entityID, false);
        if (this.isSelected(entityID)) {
            delete this.selectedEntityIDHash[entityID];

            if (! doNotUpdateDisplay) {
                this.updateDisplay();
                this.dazzle();
                
                new WindowState().save();    
            }
        } 
    }

    this.deselectEntities = function(entityIDs) {
        for (var i = 0; i < entityIDs.length; i++) {
            this.deselectEntity(entityIDs[i],true);
        }
        this.updateDisplay();
        this.dazzle();

        new WindowState().save();
    }

    this.clearSelected = function() {
        var unhighlightConfirm = confirm("Unhighlight all " + (ENTITY_MODE == COMPARABLE ? "comparables" : "properties") + "?")
        if (unhighlightConfirm) {
            if (this.hasSelectedEntities()) {
                if (new EntityManager().overrideEntityIDs) {
                    var currentTabID = new TabsManager().getActiveTabID();
                    var tabToShow = "list-view";
                    if (currentTabID.indexOf("map") != -1) {
                        var tabToShow = "map-view";
                    }
                    this.viewAll(tabToShow);
                } else {
                    var ids = new Array();
                    for (var id in this.selectedEntityIDHash) {
                        ids.push(id);
                    }
                    this.deselectEntities(ids);
                }
                this.selectedEntityIDHash = new Array();
                this.updateDisplay();
                this.dazzle();
                new WindowState().save();
            }
        }
    }
    
    this.highlightSelected = function() {
        var self = this;
        var selected = '|' + this.getSelectedEntityIDs().join('|') + '|';
        $("#list-results").find("input:checkbox").each(function() {
            this.checked = selected.indexOf('|' + this.value + '|') > -1;
            if (this.checked) {
                self.updateHighlight(this.value, true);
            }
        });
        this._updateCheckAll();
    }
    
    this.dazzle = function() {
        if (this.hasSelectedEntities()) {
            $('#highlight-message').addClass("hidden");
            $('#highlight-fade').fadeIn(500);
            $("#highlight-bar").find("ul,p").removeClass("hidden");
            $("#sitelink-header").find("ul").removeClass("hidden");
        } else {
            $("#highlight-bar").find("ul,p").addClass("hidden");
            $("#sitelink-header").find("ul").addClass("hidden");
            $('#highlight-fade').fadeOut(500);
            $('#highlight-message').removeClass("hidden");
        }
    }
    
    this.updateHighlights = function() {
        for (var id in this.selectedEntityIDHash) {
            this.updateHighlight(id,true);
        }
    }
    
    this.updateHighlight = function(entityID, highlighted) {
        var selector = '#list-result-{0},#bubble-title-{0},#overview-title,#details-highlight'.format(entityID);
        var className = 'highlighted';
        if (highlighted) {
            $(selector).addClass(className);
        } else {
            $(selector).removeClass(className);
        }
        $(selector).find("input:checkbox[value={0}]".format(entityID)).each(function(){this.checked=highlighted});
        
        //change the map icon
        var map = new MapManager().getMap();
        var mappable = new EntityManager().getEntity(entityID);
        if (map && mappable) {
            var marker = mappable.getMarker();
            var icon = marker.getIcon();
            if (highlighted && !icon.image.endsWith("_high.png")) {
                icon.image = icon.image.replace(".png","_high.png");
                $(selector).addClass(className);
            } else if (!highlighted && icon.image.endsWith("_high.png")) {
                icon.image = icon.image.replace("_high.png",".png");
                $(selector).removeClass(className);
            }
            map.swapMappable(mappable);
        }
    }
    
    this._updateCheckAll = function() {
        var countChecked = 0;
        var countTotal = 0;
        
        $("#list-results").find("input:checkbox").each(function() {
            countTotal++;
            if (this.checked) {
                countChecked++;
            }
        });

        $("#check-all")[0].checked = (countTotal && countChecked == countTotal);
    }
    
    this.updateDisplay = function() {
        var numberOfIDs = this.getSelectedEntityIDs().length;
        
        var desc = numberOfIDs + ' ';
        if (ENTITY_MODE == COMPARABLE) {
            desc += (numberOfIDs != 1 ? 'comparables' : 'comparable');
        } else {
            desc += (numberOfIDs != 1 ? 'properties' : 'property');
        }
        $('#num-selected').text(desc + ' selected');
        $('#num-selected-sitelink').text(numberOfIDs + ' selected');
        if (new EntityManager().overrideEntityIDs) {
            $('#view-all').removeClass("hidden");
            $('#view-highlighted').addClass("hidden");
        } else {
            $('#view-all').addClass("hidden");
            $('#view-highlighted').removeClass("hidden");
        }
        this._updateCheckAll();
    }
}

function MapManager(containerID) {
    if (MapManager.instance && MapManager.instance.containerID) {
        return MapManager.instance;
    }
    MapManager.instance = this;
    var mapManager = this;
    this.containerID = containerID;
    this.map = null;

    this.redraw = {
            'needs':true,
            'centerLatLng':null,
            'zoom':null,
            'reset':function() {this.needs = false; this.centerLatLng = null; this.zoom = null;}
    };
    
    this.isVisible = false;
    this.moveListener = null;
    this.zoomListener = null;
    this.lastSearchRequest = null;

    this.display = function(doNotZoom) {
        if (this.map) {
            this.map.clearMappables();
            var entityManager = new EntityManager();
            var mappables = new Array();
            
            var entityIDs;
            if (entityManager.overrideEntityIDs && entityManager.overrideEntityIDs.length > 0) {
                entityIDs = entityManager.overrideEntityIDs;
            } else {
                entityIDs = entityManager.entityIDsInOrder;
            }
            for (var i=0;i<entityIDs.length;i++) {
                mappables.push(entityManager.getEntity(entityIDs[i]));
            }
            this.map.addOverlaysForMappables(mappables, doNotZoom);
            this.map.refresh();
        }
    }

    this.setVisible = function(vis) {
        if (vis && this.redraw.needs) {
            this._show(this.redraw.centerLatLng, this.redraw.zoom);
        }
        this.isVisible = vis;
    }

    this.show = function(centerLatLng, zoom) {
        if (DEBUG) debug("MapManager.show(" + centerLatLng + "," + zoom + ")");
        if (this.isVisible) {
            this._show(centerLatLng, zoom);
        } else {
            this.redraw.needs = true;
            if (centerLatLng) {
                this.redraw.centerLatLng = centerLatLng;
            }
            if (zoom) {
                this.redraw.zoom = zoom;
            }
        }
    }

    this.throb = function() {
        //create a new hidden div and put the loading graphic in it
        var toThrob = $("#map-view").find(".tab-cover,.beavis");
        if (toThrob.size() < 2) {
            $('#map-view').append('<div class="tab-cover"></div><img src="/images/search/loading_graphic.gif" alt="Loading" class="beavis" />');
        } else {
            toThrob.show();
        }
    }
    this.unthrob = function() {
        new ActionManager().unthrobTab("map-view")
        
        //hide the div
        $("#map-view").find(".tab-cover,.beavis").hide();
    }
    
    this.onMapChange = function(){
        var entityMgr = new EntityManager();
        if (entityMgr.searchResultsCount > 250) {
            this.throb();
            this.stopListeningForEvents();
            var tempCriteria = new Object();
            var caller = null;
            var handler = function() {
                var req = caller.ajax.requestor;
                if (req.readyState == 4) {
                    if (!req.status || req.status == 200) {
                        var root = req.responseXML.documentElement;
                        //add the guys to the map without adding them to entityManager
                        var entities = null;
                        var entitiesNode = root.getElementsByTagName("entities");
                        if (entitiesNode && entitiesNode.length) {
                            entities = eval(entitiesNode[0].childNodes[0].nodeValue);                           
                        }
                        var mapMgr = new MapManager()
                        if (entities) {
                            var mappables = new Array();
                            for (var i=0;i<entities.length;i++) {
                                var entity = new Mappable(entities[i]);
                                //if we already had this entity, then let's use it instead
                                var existingEntity = entityMgr.getEntity(entity.ID);
                                if (existingEntity) {
                                    //some mojo since entities don't always come with propertyTypeID
                                    existingEntity.propertyTypeID = entity.propertyTypeID
                                    entity = existingEntity;
                                }
                                entity = (existingEntity?existingEntity:entity);
                                mappables.push(entity);
                            }
                            mapMgr.getMap().clearMappables();
                            mapMgr.getMap().removeShapeByName("error");
                            mapMgr.getMap().removeLabel();
                            
                            if (mappables.length) {
                                mapMgr.getMap().addOverlaysForMappables(mappables ,true);
                            }
                        }
                        mapMgr.mapCallback(req.responseXML, true);
                    }
                }
            }
            caller = new AJAXMethodCaller(entityMgr.searchProvider, "map", handler);
            //add all known search criteria
            var existingCriteria = new CriteriaManager().getCriteriaValues();
            for (var key in existingCriteria) {
                caller.addRequestParameter(key, existingCriteria[key]);
            }
            
            //now add additional map criteria
            var bounds = this.getMap().getBounds();
            var ne = bounds.getNorthEast();
            var sw = bounds.getSouthWest();
            caller.addRequestParameter("lattitude_north",ne.lat());
            caller.addRequestParameter("lattitude_south",sw.lat());
            caller.addRequestParameter("longitude_east",ne.lng());
            caller.addRequestParameter("longitude_west",sw.lng());
            if (sw.lat() != NaN && sw.lng() != NaN && ne.lat() != NaN && ne.lng() != NaN) {
                caller.call();
            } else {
                //try again
                if (DEBUG) debug("Map bounds: sw({0},{1}) ne({2},{3})".format(sw.lat(),sw.lng(),ne.lat(),ne.lng()));
                window.setTimeout(function(){new MapManager().onMapChange()},200);
            }
        }
    }
    
    this.setPolygon = function(polygon) {
        if (DEBUG) debug("drawing polygon on map");
        this.getMap().drawPolygon("user",polygon, true);
    }
    
    this.mapCallback = function(xml, doNotCallDisplay, centerLatLng, zoom) {
        var exceptions;
        var errorMessage;
        if (xml) {
            exceptions = xml.documentElement.getElementsByTagName("exceptions");
        }
        if (exceptions && exceptions.length) {
            try {
                errorMessage = exceptions[0].childNodes[0].childNodes[0].nodeValue;
            } catch (e) {
            }
        }
        var map = this.getMap() 
        var shapeElements = xml.documentElement.getElementsByTagName("shape");
        if (shapeElements && shapeElements.length > 0 && !map.hasUserShape()) {
            var bEl = shapeElements[0];
            var pointsElements = shapeElements[0].getElementsByTagName("point");
            var polygon = new Polygon();
            
            var northLat = null;
            var southLat = null;
            var eastLng = null;
            var westLng = null;
            for (var i=0;i<pointsElements.length;i++) {
                var p = pointsElements[i];
                var lat = eval(p.getAttribute("latitude"));
                var lng = eval(p.getAttribute("longitude"));
                polygon.addPoint(new GLatLng(lat,lng));
            }
            map.clearMappables();
            map.drawPolygon("error", polygon, true);
        } else {
            this.getMap().removeShapeByName("error");
            this.getMap().removeLabel();
        } 
        if (!doNotCallDisplay && !centerLatLng) {
            this.display();
        }

        var boundsElements = xml.documentElement.getElementsByTagName("bounds");
        if (this.isVisible) {
            var map = this.map;
            if (boundsElements && boundsElements.length && !doNotCallDisplay && !centerLatLng) {
                var el = boundsElements[0];
                var north = el.getAttribute("north");
                var south = el.getAttribute("south"); 
                var east = el.getAttribute("east"); 
                var west = el.getAttribute("west"); 
                if (north != -1 || east != -1 || south != 0 || west != 0) {
                    map.zoomToFit(north,south,east,west);
                    this.display(true);
                }
            } else if (!doNotCallDisplay && !centerLatLng) {
                map.zoomToFitMappables();   
            } else if (centerLatLng) {
                map.setCenter(centerLatLng, zoom);
                this.display(true);
            }           
            
            if (errorMessage) {
                var label = new Label(map.getCenter(),errorMessage,"searchClass");
                map.addLabel(label);
            }
        }
           
        this.unthrob();
        this.startListeningForEvents();
    }
    
    this.startListeningForEvents = function() {
        if (!this.moveListener && !this.zoomListener) {
            
            var eventCallback = function() {
                new MapManager().onMapChange();
            }
            this.moveListener = GEvent.addListener(this.getMap(), "moveend", eventCallback);
            this.zoomListener = GEvent.addListener(this.getMap(), "zoomend", eventCallback);
        }
    
    }
    
    this.stopListeningForEvents = function() {
        if (this.moveListener && this.zoomListener) {
            GEvent.removeListener(this.moveListener);
            GEvent.removeListener(this.zoomListener);
            this.moveListener = null;
            this.zoomListener = null;
        }
    }
    
    this._show = function(centerLatLng, zoom) {
        this.redraw.reset();
        
        this.throb();

        new ScriptManager().loadMapScripts(function() { 
            var mapMgr = new MapManager();
            if (!mapMgr.map) {
                if (centerLatLng) {
                    mapMgr.map = new CMap(mapMgr.containerID, centerLatLng.lat(), centerLatLng.lng(), zoom);
                } else {
                    mapMgr.map = new CMap(mapMgr.containerID);
                }
            
                GEvent.addListener(mapMgr.map, "moveend", function(){new WindowState().save()});
                GEvent.addListener(mapMgr.map, "zoomend", function(){new WindowState().save()});

            } else if (centerLatLng) {
                mapMgr.map.specifiedCenter = centerLatLng;
                mapMgr.map.zoom = zoom;
            }
            
            if (! mapMgr.isVisible) {
                mapMgr.map.reset()
            }
            var entityManager = new EntityManager();
            var parameters = new Array();
            if (entityManager.overrideEntityIDs && entityManager.overrideEntityIDs.length > 0) {
                parameters.push(new Parameter(AJAXMethodCaller.STRING_ARRAY_TYPE, entityManager.overrideEntityIDs));
            }
            entityManager.clearEntities();
            if (mapMgr.lastSearchRequest) {
                mapMgr.lastSearchRequest.abort();
            }
            mapMgr.lastSearchRequest = entityManager.getResultsAndCallBack("map",parameters,
                function(xml, doNotCallDisplay){
                    if ((centerLatLng || zoom) && new EntityManager().searchResultsCount > 250) {
                        // trigger the drilldown logic
                        new MapManager().getMap().setCenter(centerLatLng, zoom);
                        new MapManager().onMapChange();
                    } else {
                        new MapManager().mapCallback(xml, doNotCallDisplay, centerLatLng, zoom); 
                        new ActionManager().updateHighlights();
                        //draw the user polygon if there is one in the search criteria, but the Map doesn't own one
                        var locationCriteria = new CriteriaManager().getCurrentCriterionByName("Location");
                        if (locationCriteria) {
                            var items = locationCriteria.locationItems;
                            if (items) {
                                for (var i = 0; i < items.length; i++) {
                                    if (items[i].locationType == 'polygon') {
                                        new MapManager().setPolygon(items[i].polygon);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                },
                function() {
                    new MapManager().unthrob();
                    showUserError("We're sorry, but there was an unexpected error.");
                });
        });
    }
    
    this.addSearchByMapCriteria = function() {
        //frist get the location criteria guy
        var mgr = new CriteriaManager();
        var locationCriterion = mgr.getCurrentCriterionByName("Location");
        
        //if there is no guy, then create him
        if (! locationCriterion) {
            mgr.addCurrentCriterionByName("Location");
            locationCriterion = mgr.getCurrentCriterionByName("Location");
        }
        
        //add the map data to the guy
        locationCriterion.addMapAsCriteria();
        
        //remove the error polygon
        this.map.removeLabel();
        this.map.removeShapeByName("error");
        new WindowState().save();
    }
    this.addSearchByPolygonCriteria = function() {
        //frist get the location criteria guy
        var mgr = new CriteriaManager();
        var locationCriterion = mgr.getCurrentCriterionByName("Location");
        
        //if there is no guy, then create him
        if (! locationCriterion) {
            mgr.addCurrentCriterionByName("Location");
            locationCriterion = mgr.getCurrentCriterionByName("Location");
        }
        
        //get the points from the polygon control and translate them into a catylist polygon
        var controlledPolygon = this.map.getUserShape();
        var polygon = new Polygon();
        for (var i=0;i<controlledPolygon.getVertexCount();i++) {
            polygon.addPoint(controlledPolygon.getVertex(i));
        }
        
        //add the map data to the guy
        locationCriterion.addPolygonAsCriteria(polygon);

        //remove the error polygon
        this.map.removeLabel();
        this.map.removeShapeByName("error");
        new WindowState().save();
    }    
    
    this.showEntity = function(id) {
        var entity = new EntityManager().getEntity(id);
        if (entity) {
            this.redraw.centerLatLng = entity.point;
            this.redraw.zoom = 17;
        }
        new ActionManager().switchToTab("map-view");
        if (entity) {
            try {
                entity.getMarker().openInfoWindow();
            } catch (e) {
                //recursively try to show the info window
                window.setTimeout(function(){new MapManager().showEntity(id)},1000)
            }
            this.map.zoomTo(entity);
        }
    }
    
    this.getMap = function() {
        return this.map;
    }
    
}

function ListView(containerID) {
    if (ListView.instance) {
        return ListView.instance;
    }
    ListView.instance = this;

    this.sortColumn = "";
    this.sortReverse = false;

    this.expandAll = false;

    this.containerID = containerID;
    this.isVisible = false;

    this.lastSearchRequest = null;
    
    this.setExpandAllFromCookie = function() {
        this.expandAll = false;
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            cookies[i] = cookies[i].trim();
            if (cookies[i].indexOf('ListViewExpandAll=') == 0) {
                this.expandAll = cookies[i].substring('ListViewExpandAll='.length) == 'true';
            }
        }
    }
    this.setExpandAllFromCookie();

    this.setVisible = function(vis) {
        this.isVisible = vis;
    }
    
    this.setExpandAll = function(expanded) {
        this.expandAll = expanded;
        if (this.expandAll) {
            document.cookie='ListViewExpandAll=true; expires=1 Jan 2999; path=/';
        } else {
            document.cookie='ListViewExpandAll=false; path=/';
        }
    }

    this.toggleSummaryForAll = function() {
        var start = new Date().getTime();
        if (this.expandAll) {
            this.hideAllSummaries();
        } else {
            this.showAllSummaries();
        }
        if (DEBUG) debug('<strong>Toggle Summaries: </strong>' + (new Date().getTime() - start) + ' ms');
    }
    this.showAllSummaries = function(dontFetch) {
        this.setExpandAll(true);
        $("#expand-all").addClass("expanded");

        var listResults = $("#list-results");
        listResults.find("> div").addClass("expanded");
        listResults.find(".line-item").addClass("hidden");
        listResults.find(".summary").removeClass("hidden");
        
        $("#list-results-header").find(".sort-header").addClass("hidden");
        
        if (!dontFetch) {
            var entitiesToFetch = new Array();
            listResults.find(".summary-placeholder").each(function() {
                var id = $(this).attr("rel");
                $('#list-result-' + id).find(".line-item").removeClass("hidden");
                entitiesToFetch.push(id);
            });
            this.renderSummaries(entitiesToFetch);
        }
    }
    this.hideAllSummaries = function() {
        this.setExpandAll(false);
        $("#expand-all").removeClass("expanded");

        var listResults = $("#list-results");
        listResults.find("> div").removeClass("expanded");
        listResults.find(".line-item").removeClass("hidden");
        listResults.find(".summary").addClass("hidden");

        $("#list-results-header").find(".sort-header").removeClass("hidden");
    }

    this.toggleSummary = function(entityID) {
        var listResult = $("#list-result-" + entityID);
        listResult.toggleClass("expanded");
        listResult.find(".line-item").toggleClass("hidden");
        listResult.find(".summary").toggleClass("hidden");

        if (listResult.find(".summary-placeholder").length > 0) {
            this.renderSummaries([entityID]);
        }
    }

    this.styleSortCols = function(dontExpand) {
        $("#list-results-header").find(".sort").find("img").remove();
        $("#list-results-header").find(".sort-header").removeClass("sort");

        var sortCol = $("#header-" + this.sortColumn);
        sortCol.addClass("sort").append(this.sortReverse ? "<img src='/images/search/sort-up.png' alt='Sorted Up' />" : "<img src='/images/search/sort-down.png' alt='Sorted Down' />");
        
        if (sortCol.length == 0) {
            if (!dontExpand && !this.expandAll) {
                this.showAllSummaries(true);
            }
        }

        var sortSelect = $('#list-sort-menu')[0];
        for (var i = 0; i < sortSelect.options.length; i++) {
            if (sortSelect.options[i].value == this.sortColumn) {
                sortSelect.selectedIndex = i;
                break;
            }
        }
        $('#list-sort-direction')[0].selectedIndex = this.sortReverse ? 1 : 0;
    }

    this.sort = function(sortBy, sortReverse) {
        if (sortReverse == undefined) {
            if (this.sortColumn == sortBy) {
                this.sortReverse = !this.sortReverse;
            } else {
                this.sortReverse = false;
            }
        } else {
            this.sortReverse = sortReverse;
        }
        this.sortColumn = sortBy;

        this.styleSortCols();

        this.show(1);
    }

    this.show = function(page) {
        $("#check-all")[0].checked = false;
        
        var parent = $("#list-results");
        parent.empty();
        parent.append('<div class="tab-cover"></div><img src="/images/search/loading_graphic.gif" alt="Loading" class="beavis" />');

        var mgr = new EntityManager();

        var parameters = new Array();
        if (mgr.overrideEntityIDs && mgr.overrideEntityIDs.length > 0) {
            parameters.push(new Parameter(AJAXMethodCaller.STRING_ARRAY_TYPE,mgr.overrideEntityIDs));
        }

        var perPage = new Paginator().itemsPerPage;
        var end = (page * perPage);
        var start = end - perPage;
        parameters.push(new Parameter("java.lang.Integer",start));
        parameters.push(new Parameter("java.lang.Integer",end));

        var sortString = "null";
        if (this.sortColumn) {
            sortString = this.sortColumn;
            if (this.sortReverse) {
                sortString += '|0';
            } else {
                sortString += '|1';
            }
        }
        parameters.push(new Parameter("java.lang.String",sortString));

        var self = this;
        var callback = function(xml) {
            self.lastSearchRequest = null;
            
            var parent = $("#list-results");
            parent.empty();
            
            //Remove loading graphic from blurred input
            $("#search-filters,#primary-sitelink-filters").find("li .input-loading").removeClass("input-loading");

            var listHTML = xml.documentElement.childNodes[1].childNodes[0].nodeValue;
            if (listHTML) {
                parent.html(listHTML);
            }
            self.styleResults();
            renderTime = new Date().getTime();

            var root = xml.documentElement;
            var count = root.getAttribute("count");
            $("#number-results").find("span").empty()
            $("#number-results").find("span").html(count)

            if (DEBUG) {
                var timeStr = '<strong>Query:</strong> ' + root.getAttribute("seconds") + ' s&nbsp;&nbsp;&nbsp;&nbsp;<strong>Query + Load:</strong> ' + (queryAndLoadTime - callTime)/1000 + ' s&nbsp;&nbsp;&nbsp;&nbsp;<strong>Render:</strong> ' + (renderTime - queryAndLoadTime)/1000 + ' s&nbsp;&nbsp;&nbsp;&nbsp;<strong>Total:</strong> ' + (new Date().getTime() - callTime)/1000 + ' s';
                debug("" + timeStr + "");
            }

            new Paginator().setCurrentPage(page);
            new Paginator().updateDisplay(eval(count));
            
            new ActionManager().highlightSelected();
            
            new WindowState().save();
            new WindowState().updateTitle();
        }

        if (this.lastSearchRequest) {
            this.lastSearchRequest.abort();
        }
        this.lastSearchRequest = new EntityManager().getResultsAndCallBack("search",parameters,callback, 
                function() {
                    $("#search-filters,#primary-sitelink-filters").find("li .input-loading").removeClass("input-loading");
                    $("#list-results").empty();
                    $("#number-results span").html("0")
                    new Paginator().updateDisplay(0);
                    showUserError("We're sorry, but there was an unexpected error.");
                });
    }
    
    this.styleResults = function() {
        var evenOdd = "even";
        $("#list-results").find("> div").each(function(i) {
            if (evenOdd == "even") {
                evenOdd = "odd";
            } else {
                evenOdd = "even";
            }
            $(this).addClass(evenOdd);
        });
    }
    
    this.renderSummaries = function(entityIDs) {
        if (!entityIDs || entityIDs.length == 0) return;
        
        var mgr = new EntityManager();

        var parameters = new Array();
        parameters.push(new Parameter(AJAXMethodCaller.STRING_ARRAY_TYPE, entityIDs));

        var self = this;
        var callback = function(xml) {
            var start = new Date().getTime();
            var root = xml.documentElement;
            $(root).children().each(function() {
                var id = this.getAttribute('id');
                var summaryHTML = this.childNodes[0].nodeValue;
                var listResult = $("#list-result-" + id);
                listResult.find(".line-item").addClass('hidden');
                listResult.find(".summary-placeholder").replaceWith(summaryHTML);
            });
            if (DEBUG) debug('<strong>Render Summaries: </strong>' + (new Date().getTime() - start) + ' ms');
        }
        mgr.getResultsAndCallBack("populateSummaries", parameters, callback, function() {showUserError("We're sorry, but there was an unexpected error.");});
    }
}


function CriteriaManager() {
    if (CriteriaManager.instance) {
        return CriteriaManager.instance;
    }
    CriteriaManager.instance = this;

    this.allCriteria = new Array();
    this.allCriteriaIndex = new Array();
    this.currentCriteria = new Array();
    this.currentCriteriaIndex = new Array();

    this.isCriterionApplicable = function(name) {
        return this.allCriteriaIndex[name].isApplicable(this.getSelectedPropertyTypes());
    }

    //get a hash of all key/values of user entered search criteria.
    //keys are strings, values are arrays
    this.getCriteriaValues = function() {
        var values = new Array();
        for (var i=0;i<this.currentCriteria.length;i++) {
            var criterion = this.currentCriteria[i];
            var criterionValues = criterion.getCriterionValues();
            for (var key in criterionValues) {
                if (!values[key]) {
                    values[key] = new Array();
                }
                for (var j=0;j<criterionValues[key].length;j++) {
                    values[key].push(criterionValues[key][j]);
                }

            }

        }
        return values;
    }
    
    this.isPropertyTypeSelected = function(propTypeID) {
        var selected = false;
        var propCriterion = this.getCurrentCriterionByName("PropertyType");
        if (propCriterion) {
            selected = propCriterion.isSelected(propTypeID);
        }
        return selected;
    }
    
    this.getSelectedPropertyTypes = function() {
        var propertyTypeIDs = new Array();
        var propCriterion = this.getCurrentCriterionByName("PropertyType");
        if (propCriterion) {
            propertyTypeIDs = propCriterion.getPropertyTypes();
        }
        return propertyTypeIDs;
    }
    
    this.isSaleOnly = function() {
        var saleOnly = false;
        var saleLease = this.getCurrentCriterionByName(ENTITY_MODE == LISTING ? "SaleLease" : "SaleLeaseComps");
        if (saleLease) {
            saleOnly = saleLease.isSaleOnly();
        }
        return saleOnly;
    }
    this.isLeaseOnly = function() {
        var leaseOnly = false;
        var saleLease = this.getCurrentCriterionByName(ENTITY_MODE == LISTING ? "SaleLease" : "SaleLeaseComps");
        if (saleLease) {
            leaseOnly = saleLease.isLeaseOnly();
        }
        return leaseOnly;
    }
    this.isOffMarketOnly = function() {
        var offmarket = false;
        var saleLease = this.getCurrentCriterionByName("SaleLease");
        if (saleLease) {
            offmarket = saleLease.isOffMarketOnly();
        }
        return offmarket;
    }
    

    this.addCriterion = function(criterion) {
        this.allCriteria.push(criterion);
        this.allCriteriaIndex[criterion.getName()] = criterion;
    }
    this.addCurrentCriterionByName = function(name, overrideParentID) {
        //we would need to clone these criteria if we want to add them more than once
        var criterion = this.getCriterionByName(name);
        if (criterion) {
            this.currentCriteria.push(criterion);
            this.currentCriteriaIndex[criterion.getName()] = criterion;
            new CriteriaView().add(criterion, overrideParentID);
        }
        return criterion;
    }
    this.removeCurrentCriterionByIndex = function(index) {
        var criterion = this.currentCriteria[index];
        if (criterion.clear) {
            criterion.clear();
        }
        this.currentCriteriaIndex[this.currentCriteria[index].name] = null;
        this.currentCriteria.splice(index,1);
        new CriteriaView().remove(criterion);

    }
    this.getCriterionByName = function(name) {
        return this.allCriteriaIndex[name];
    }
    this.getCurrentCriterionByName = function(name) {
        return this.currentCriteriaIndex[name];
    }
    this.currentContains = function(criterion) {
        var contains = false;
        for (var i in this.currentCriteria) {
            if (this.currentCriteria[i] == criterion) {
                contains = true;
                break;
            }
        }
        return contains;
    }

    this.getCriteriaBySearchParameters = function(params) {
        var criteria = new Array();

        for (var i = 0; i < this.allCriteria.length; i++) {
            if (this.allCriteria[i].isInCriteria(params)) {
                criteria.push(this.allCriteria[i]);
            }
        }

        return criteria;
    }
}

function CriteriaController(criteriaNames) {
    this.criteriaNames = criteriaNames;
    if (CriteriaController.instance) {
        return CriteriaController.instance;
    }
    CriteriaController.instance = this;

    this.lastSearchValues = null;

    this.initializeCriteria = function() {
        for (var i=0;this.criteriaNames && i<this.criteriaNames.length;i++) {
            var name = this.criteriaNames[i];
            new CriteriaManager().addCriterion(eval("new " + this.criteriaNames[i].toTitleCase() + "()"));
        }
    }

    this.searchCriteriaChanged = function(newCriterionValues) {
        var changed = false;
        if (this.lastSearchValues) {
            var keys = "";
            for (var key in newCriterionValues) {
                keys += key + ",";
            }
            for (var key in this.lastSearchValues) {
                if (keys.indexOf(key) < 0) {
                    keys += key + ",";
                }
            }
            keys = keys.split(",");
            keys.pop(); // remove the last empty element

            for (var i = 0; i < keys.length; i++) {
                var key = keys[i];

                if (!this.lastSearchValues[key]) {
                    changed = true;
                    break;
                }
                if (!newCriterionValues[key]) {
                    changed = true;
                    break;
                }

                var lastVals = this.lastSearchValues[key];
                var newVals = newCriterionValues[key];
                if (lastVals.length != newVals.length) {
                    changed = true;
                    break;
                }

                for (var j = 0; j < lastVals.length; j++) {
                    if (lastVals[j] != newVals[j]) {
                        changed = true;
                        break;
                    }
                }
            }

        } else {
            changed = true;
        }
        return changed;
    }

    this.forceNewSearch = false;
    this.search = function(initPageNum) {
        var didSearch = false;
        
        var criteriaValues = new CriteriaManager().getCriteriaValues();
        if (this.searchCriteriaChanged(criteriaValues)) {
            $("#number-results").find("span").empty().prepend("<img src='/images/search/loading_circle.gif' alt='Loading...' />");

            this.lastSearchValues = criteriaValues;
            var map = new MapManager().getMap()

            if (this.forceNewSearch) {
                this.forceNewSearch = false;
                criteriaValues['newSearch'] = ['1'];
            }
            new EntityManager().setSearchCriteria(criteriaValues);

            var list = new ListView();
            new EntityManager().overrideEntityIDs = null;
            new ActionManager().updateDisplay();

            list.show(initPageNum||1);

            new MapManager().show();
            didSearch = true;

            // update rss link
            var queryString = 'search=true';
            for (var key in this.lastSearchValues) {
                var vals = this.lastSearchValues[key];
                if (vals && vals.length > 0) {
                    queryString += '&' + mapFunction(function(val) { return key + "=" + escape(val); }, vals).join("&");
                }
            }
            $("#rsslink").each(function() { 
                this.setAttribute('href', '/feed/listings/?' + queryString);
            });
        }
        return didSearch;
    }
    this.initializeCriteria();
}

function CriteriaView(criteriaParentID) {
    if (CriteriaView.instance) {
        return CriteriaView.instance;
    }
    CriteriaView.instance = this;

    this.criteriaParentID = criteriaParentID;
    
    this.redraw = true;

    function isEnabled(criterion) {
        var mgr = new CriteriaManager();
        var enabled = criterion.isApplicable(mgr.getSelectedPropertyTypes());
        if (enabled) {
            if ((mgr.isSaleOnly() && criterion.leaseOrSale == FOR_LEASE) || 
                (mgr.isLeaseOnly() && criterion.leaseOrSale == FOR_SALE) ||
                (mgr.isOffMarketOnly() && criterion.notForOffMarket)) {
                enabled = false;
            }
        }
        return enabled;
    }

    this.add = function(criterion, overrideParentID) {
        criterion.renderIn($("#" + (overrideParentID ? overrideParentID : this.criteriaParentID))[0]);
        
        if (this.redraw) {
            criterion.setEnabled(isEnabled(criterion));
            criterion.updateDisplay();
            
            this.updateAddMenu();
            new WindowState().save();
        }
    }
    
    this.remove = function(criterion) {
        criterion.renderOut();
        
        if (this.redraw) {
            this.updateAddMenu();
            new WindowState().save();
        }
    }
    
    this.updateAddMenu = function() {
        var start = new Date().getTime();
        
        var mgr = new CriteriaManager();

        criteria = mgr.allCriteria;

        var mgr = new CriteriaManager();
        var selectedPropertyTypes = mgr.getSelectedPropertyTypes();
        var saleOnly = mgr.isSaleOnly();
        var leaseOnly = mgr.isLeaseOnly();
        var offMarketOnly = mgr.isOffMarketOnly();

        for (var i=0;i<criteria.length;i++) {
            var enabled = true;
            if (mgr.currentContains(criteria[i])) {
                enabled = false;
            } else if (!criteria[i].isApplicable(selectedPropertyTypes)) {
                enabled = false;
            } else if (saleOnly && criteria[i].leaseOrSale == FOR_LEASE) {
                enabled = false;
            } else if (leaseOnly && criteria[i].leaseOrSale == FOR_SALE) {
                enabled = false;
            } else if (offMarketOnly && criteria[i].notForOffMarket) {
                enabled = false;
            }
            
            if (enabled) {
                $("#add-" + criteria[i].getName()).removeClass("disabled");
            } else {
                $("#add-" + criteria[i].getName()).addClass("disabled");
            }
        }
        
        //TODO: this is pretty brittle
        $("#select-filter").find("ul").each(function() {
            if ($(this).find("li:not(.disabled)").length > 0) {
                $(this).prev().removeClass("disabled");
            } else {
                $(this).prev().addClass("disabled");
            }
        });
        if ($("#select-filter").find("li:not(.disabled)").length > 0) {
            $("#add-filter").removeClass("disabled");
            $("#add-filter-disabler").addClass("hidden");
        } else {
            $("#add-filter").addClass("disabled");
            $("#add-filter-disabler").removeClass("hidden");
        }
        
        if (DEBUG) debug("Update criteria menu: " + (new Date().getTime() - start) + " ms");
    }

    //asynchronously update the criteria view
    this.updateDisplay = function() {
        //make sure controller is initialized
        new CriteriaController();

        var criteria = new CriteriaManager().currentCriteria;
        for (var i=0;i<criteria.length;i++) {
            criteria[i].setEnabled(isEnabled(criteria[i]));
            criteria[i].updateDisplay();
        }

        this.updateAddMenu();

        this.eventLoopID = null;
    }
}

function Criterion(name, config) {
    this.name = name;
    
    if (config == null) config = new Object();
    
    this.notForOffMarket = config.notForOffMarket;
    this.leaseOrSale = config.leaseOrSale;
    this.propertyTypes = config.propertyTypes;
    this.entityType = config.entityType;
    this.defaultFilter = config.defaultFilter||false;
    
    this.parentElement = null;
    this.enabled = true;

    this.getName = function() {
        return this.name;
    }

    // a) when no property type is selected, only those criteria that apply to all or have "defaultFilter" set to true are applicable
    // b) if this criterion has no property types specified, it is applicable to all
    // c) if the criterion's property types intersect with the selected property types, it is applicable
    this.isApplicable = function(selectedPropertyTypes) {
        if (this.entityType && this.entityType != ENTITY_MODE) {
            return false;
        } else if (!this.propertyTypes || this.propertyTypes.length == 0 || this.propertyTypes.length == 9) {
            return true;
        } else if (!selectedPropertyTypes || selectedPropertyTypes.length == 0) {
            return this.defaultFilter;
        } else {
            for (var i = 0; i < selectedPropertyTypes.length; i++) {
                for (var j = 0; j < this.propertyTypes.length; j++) {
                    if (selectedPropertyTypes[i] == this.propertyTypes[j]) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    this.setEnabled = function(enabled) {
        if (this.enabled != enabled) {
            this.enabled = enabled;
            if (this.parentElement) {
                if (!this.enabled) {
                    $(this.parentElement).addClass('disabled');
                    $(this.parentElement).prepend('<div class="disabled-overlay"/>');
                } else {
                    $(this.parentElement).removeClass('disabled');
                    $(this.parentElement).find('div.disabled-overlay').remove();
                }
            }
        }
    }

    this.clear = function() {
        this._clear();
    }
    this._clear = function() {}

    this.getCriterionValues = function() {
        return this._getCriterionValues();
    }
    this._getCriterionValues = function() {
        var map = new Array();
        if (this.enabled && this.parentElement) {
            $(this.parentElement).find("input:text,input[type='hidden']").each(function(i) {
                if (this.value) {
                    map[this.name] = [this.value];
                }
            });
            $(this.parentElement).find("input:checkbox,input:radio").each(function(i) {
                if (this.checked) {
                    var vals = map[this.name];
                    if (!vals) {
                        vals = new Array();
                        map[this.name] = vals;
                    }
                    vals.push(this.value);
                }
            });
            $(this.parentElement).find("select").each(function(i) {
                if (this.value) {
                    map[this.name] = [this.value];
                }
            });
        }
        return map;
    }

    this.updateDisplay = function() {
        this._updateDisplay();
    }
    this._updateDisplay = function() {
        var self = this;

        var normalize = function(val) {
            var mdy = val.split("/");
            if (mdy.length >= 3 && mdy[2].length < 4) {
                while (mdy[2].length < 3) {
                    mdy[2] = '0' + mdy[2];
                }
                mdy[2] = '2' + mdy[2];
            }
            if (mdy.length > 0) {
                val = mdy.join("/");
            }
            return val;
        }
        
        var txts = $(this.parentElement).find("input:text");
        txts.unbind();
        txts.blur(function() {
            if ($(this).hasClass('date')) {
                this.value = normalize(this.value);
            }
            
            if (self.doSearch()) {
                $(this).addClass('input-loading');
            }
        });
        txts.keypress(function(e) {
            if (e.keyCode == 13) {
                if ($(this).hasClass('date')) {
                    this.value = normalize(this.value);
                }

                self.doSearch();
            }
        });


        $(this.parentElement).find("input:checkbox,input:radio").unbind().click(function() {
            self.doSearch();
        });
        $(this.parentElement).find("select").unbind().change(function() {
            self.doSearch();
        });
    }

    this.getParentElement = function() {
        return this.parentElement;
    }

    // check to see if this criterion is represented in the given criteria
    this.isInCriteria = function(criteria) {
        if (this.entityType && this.entityType != ENTITY_MODE) {
            return false;
        }
        
        var foundNames = false;
        
        var parent;
        if (this.parentElement) {
            parent = this.parentElement;
        } else {
            parent = $("#module-"+this.name.toUnTitleCase()).find('li');
        }
        
        // by default, let's ignore the select drop-downs cuz they probably
        // are just qualifiers and are meaningless on their own
        parent.find("input").each(function(i) {
            if (criteria[this.name]) {
                foundNames = true;
            }
        });
        return foundNames;
    }

    this.setStateFromCriteriaValues = function(values) {
        this._setStateFromCriteriaValues(values);
    }
    this._setStateFromCriteriaValues = function(values) {
        if (this.parentElement && values) {
            for (var name in values) {
                $(this.parentElement).find("input:text,input[type='hidden']").each(function() {
                    if (this.name == name) {
                        this.value = values[name][0];
                    }
                });
                $(this.parentElement).find("input:checkbox,input:radio").each(function() {
                    if (this.name == name) {
                        for (var i = 0; i < values[name].length; i++) {
                            if (values[name][i] == this.value) {
                                this.checked = true;
                                break;
                            }
                        }
                    }
                });
                $(this.parentElement).find('select').each(function() {
                    if (this.name == name) {
                        this.value = values[name][0];
                    }
                });
            }
        }
    }
    
    this.closed = function() {}

    this.renderOut = function() {
        if (this.parentElement) {
            this.parentElement.remove();
            this.parentElement = null;
        }
    }
    
    this.renderIn = function(parentElement) {
        this._renderIn(parentElement);
    }
    this._renderIn = function(parentElement) {
        $(parentElement).append($("#module-"+this.getName().toUnTitleCase()).html());

        //get the last li
        this.parentElement = $(parentElement).children("li:last");

        var self = this;
        var xFunc = function () {
            var index = 0;
            var lis = $(parentElement).children("li");
            for (var i=0;i<lis.length;i++) {
                if (lis[i] == $(this).parents("li")[0]) {
                    index = i;
                    break;
                }
            }
            new CriteriaManager().removeCurrentCriterionByIndex(index);
            self.closed();
            
            new CriteriaController().search();
        }
        $(this.parentElement).find(".x").click(xFunc);

        //do something to load any data into the form widgets
        if (this.updateDisplay) {
            this.updateDisplay();
        }
    }

    this.doSearch = function() {
        return new CriteriaController().search();
    }
}
function MultiSelectCriterion(name, config) {
    Criterion.call(this, name, config);

    this.renderIn = function(parentElement) {
        this._renderIn(parentElement);
        
        var criteria = $(this.parentElement).find(".criteria");
        var options = $(this.parentElement).find("form.criteria-options");
        var openButton = $(this.parentElement).find("button.options-open");
        var applyButton = $(this.parentElement).find("button.options-apply");
        
        openButton.unbind().click(function() {
            criteria.addClass("hidden");
            openButton.addClass("hidden");
            applyButton.removeClass("hidden");
            options.slideDown()
        });

        var self = this;
        function apply() {
            self.applyOptionsForm();
            
            openButton.removeClass("hidden");
            applyButton.addClass("hidden");
            if (!MultiSelectCriterion.APPLY_ON_TYPE_SELECT) {
                options.slideUp("normal", function() {criteria.removeClass("hidden")});
            }
        }
        applyButton.unbind().click(function() {
            apply();
        });
        
        if (MultiSelectCriterion.APPLY_ON_TYPE_SELECT) {
            $(parentElement).find("form.criteria-options input:checkbox").click(function() { apply(); });
        }
    }

    this.updateDisplay = function() {
        var parentEl = this.getParentElement();
        if (parentEl) {
            if ($(parentEl).find(".criteria")) {
                var displayable;
                
                var selected = new Array();
                $(parentEl).find('form.criteria-options input:checked').each(function() { 
                    selected.push(this.value);
                });
                if (selected.length == 0) {
                    displayable = "Any";
                } else {
                    displayable = selected.join(", ");
                }
                $(parentEl).find(".criteria").html(displayable);
            }
        }
    }

    this.setStateFromCriteriaValues = function(values) {
        this._setStateFromCriteriaValues(values);
        
        if (this.parentElement) {
            var criteria = $(this.parentElement).find(".criteria");
            var options = $(this.parentElement).find("form.criteria-options");
            var openButton = $(this.parentElement).find("button.options-open");
            var applyButton = $(this.parentElement).find("button.options-apply");

            if ($(this.parentElement).find('form.criteria-options input:checked').length > 0) {
                options.hide();
                applyButton.addClass("hidden");

                openButton.removeClass("hidden");
                criteria.removeClass("hidden");
            } else {
                options.show();
                applyButton.removeClass("hidden");

                openButton.addClass("hidden");
                criteria.addClass("hidden");
            }
        }
    }
    
    this.applyOptionsForm = function() {
        this.updateDisplay();
        this.doSearch();
    }
}
MultiSelectCriterion.APPLY_ON_TYPE_SELECT = false;

function Paginator(parentID) {
    if (Paginator.instance) {
        return Paginator.instance;
    }
    Paginator.instance = this;

    this.parentID = parentID;
    this.currentPage = 1;
    this.oldCurrentPage = 0;
    this.numberOfResults = 0;
    
    this.itemsPerPage = 50;
    
    this.setItemsPerPage = function(count) {
        $("#per-page-select").val(count);
        this.itemsPerPage = count;
    }

    this.setCurrentPage = function(page) {
        this.oldCurrentPage = this.currentPage;
        this.currentPage = page;
    }
    
    this.showPrevious = function() {
        if (this.currentPage > 1) {
            new ListView().show(this.currentPage - 1);
        }
    }
    this.showNext = function() {
        var numberOfPages = Math.ceil(this.numberOfResults / this.itemsPerPage);
        if (this.currentPage < numberOfPages) {
            new ListView().show(this.currentPage + 1);
        }
    }
    
    this.updateDisplay = function(numberOfResults) {
        this.numberOfResults = numberOfResults;
        $("#" + this.parentID).find(".page-num").remove();
        $("#pagination-drop").empty();

        var numberOfPages = Math.ceil(this.numberOfResults / this.itemsPerPage);
        if (this.currentPage == 1 || this.numberOfResults == 0) {
            $("#prev-page").addClass("disabled");
        } else {
            $("#prev-page").removeClass("disabled");
        }

        if (this.currentPage == numberOfPages || this.numberOfResults == 0) {
            $("#next-page").addClass("disabled");
        } else {
            $("#next-page").removeClass("disabled");
        }
        
        if (this.numberOfResults > 0) {
            var i = 0;
            var template = '<li class="page-num" id="pagination{0}"><a href="javascript:new ListView().show({1})">{2}</a></li>';
            for (i=0;(i<numberOfPages && i<Paginator.MAX_PAGE_NUMS_TO_SEE);i++) {
                var pageNum = i+1;
                $("#pagination-drop-start").before(template.format(pageNum,pageNum,pageNum));
            }
            
            for (i=i;i<numberOfPages;i++) {
                var pageNum = i+1;
                $("#pagination-drop").append(template.format(pageNum,pageNum,pageNum));
            }
            $("#pagination" + this.oldCurrentPage).find("> a").removeClass("current");
            $("#pagination" + this.currentPage).find("> a").addClass("current");
        }

        if (numberOfPages > Paginator.MAX_PAGE_NUMS_TO_SEE) {
            $("#pagination-drop-start").removeClass("hidden");
        } else {
            $("#pagination-drop-start").addClass("hidden");
        }
        
        var end = (this.currentPage * this.itemsPerPage);
        var start = end - this.itemsPerPage + 1;
        if (end > this.numberOfResults) {
            end = this.numberOfResults;
        }
        if (start > end) start = end;
        $("#results-current").html(start + '-' + end);
        $("#results-total").html(this.numberOfResults);
    }
}
Paginator.MAX_PAGE_NUMS_TO_SEE = 5;

function WindowState() {
    if (WindowState.instance) {
        return WindowState.instance;
    }
    WindowState.instance = this;
    
    this.moveListener = null;
    this.zoomListener = null;

    this.dontSetContext = false;
    new WindowStateHistoryManager().onHistoryChange = function(stateHash) {
        var ws = WindowState.instance;
        
        var subtitle = '';
        var state = null;
        if (stateHash) {
            stateHash = stateHash.replace(/\b[a]?c=[^\|]*\|/g, ''); // don't restore search criteria
            stateHash = stateHash.replace(/\bh=[^\|]*\|/g, ''); // don't restore highlighted
            stateHash = stateHash.replace(/\bss=[^\|]*\|/g, ''); // don't restore saved search
            state = ws.deserialize(stateHash);
            
            subtitle = state.subtitle;
        }
        ws.updateTitle(subtitle);

        if (state) {
            if (DEBUG) debug('restoring: ' + stateHash);
            ws.restore(false, state);
        }
    }
    
    this.updateTitle = function(subtitle) {
        if (!subtitle) {
            subtitle = this.getSubtitle();
        }
        if (!subtitle || subtitle.length == 0) {
            var ss = new SavedSearchManager();
            if (ss.isSavedSearch()) {
                subtitle = ss.savedSearchName;
            }
        }
        if (subtitle && subtitle.length > 0) {
            document.title = MAIN_TITLE + " : " + subtitle;
        } else {
            document.title = MAIN_TITLE;
        }
    }
    
    this.getSubtitle = function() {
        var subtitle;
        
        switch (new TabsManager().getActiveTabID()) {
        case 'list-view':
            subtitle = 'Page ' + new Paginator().currentPage;
            break;
            
        case 'map-view':
            subtitle = 'Mapped';
            break;
            
        case 'details-view':
            var entity = new EntityManager().getEntity(new DetailsView().currentEntityID);
            if (entity) {
                subtitle = entity.title||"";
                subtitle = subtitle.replace(/&amp;/g, '&');
            } else {
                subtitle = 'Property Details';
            }
            break;
            
        case 'email-view':
            subtitle = 'Email';
            break;
            
        case 'reports-view':
            subtitle = 'Create Report';
            break;
            
        case 'catalog-view':
            subtitle = 'Add To Catalog';
            break;
            
        default:
            subtitle = '';
            break;
        }
        return subtitle;
    }
    
    
    this.addHistory = function() {
        if (this.dontSetContext) return;
        new WindowStateHistoryManager().addHistoryStackFrame();
    }

    /* if the client has a search-state cookie, let's use that to set up the
     * state of this search session.
     */
    this.redirectOnCookie = function() {
        var serializedState = getCookieValue("SearchPageState");
        //TODO: implement this if we're going to use a cookie to save page state
    }
    
    /* the server doesn't see any anchor ("#...") info on the URL, so we need
     * to hack this by reading the hash info via javascript and re-encoding this
     * data into a query string and redirecting to this page using the query 
     * string. We only need to make a server call to handle criteria, other stately
     * stuff can be handed via the anchor mechanism
     */
    this.redirectOnHash = function() {
        if (window.location.hash.search(/\bc=/) > -1) {
            var queryString = "?override=true";
            if (ENTITY_MODE == COMPARABLE) {
                queryString += "&comps=1";
            }
            
            if (window.location.href.indexOf('investment=1') > -1) {
                queryString += "&investment=1";
            }
            
            var state = this.deserialize(window.location.hash.substring(1));
            if (state.savedSearchID > 0) {
                queryString += "&savedSearchID=" + state.savedSearchID;
            }
            
            for (var key in state.criteria) {
                var vals = state.criteria[key];
                queryString += "&" + mapFunction(function(val) { return key + "=" + escape(val); }, vals).join("&");
            }

            // the non-criteria stuff is parsed via javascript anyway, so we can leave
            // that as hash information (we'll strip out the criteria element)
            var hash = window.location.hash.replace(/(\b)c=[^\|]*\|/g, '$1');
            if (hash.length > 1) {
                queryString += hash;
                if (window.location.href.indexOf('forceNewSearch=1') > -1) {
                    queryString += "force=1|";
                }
            }
            window.location.replace(queryString);
        }
    }
    
    /* Save the current state of the window for later recall.  This method is fairly
     * lightweight and transparent to the user so it can be called often.
     * See addHistory() to add user-noticeable state (i.e. state that can be restored via
     * the browser's forward/back buttons)
     */
    this.save = function() {
        if (this.dontSetContext) return;
        new WindowStateHistoryManager().saveState();
    }
    
    this.getState = function() {
        var state;
        if (window.location.hash.indexOf("#") == 0) {
            state = this.deserialize(window.location.hash.substring(1));
        } else {
            state = new Object();
        }
        return state; 
    }
    this.restore = function(doSearch, state) {
        this.dontSetContext = true;
        
        if (!state) {
            if (DEBUG) {
                var stateHash = '';
                if (window.location.hash.indexOf("#") == 0) {
                    stateHash = window.location.hash.substring(1);
                }
                debug('restoring: ' + stateHash);
            }
            state = this.getState();
        }
        
        var viewSelected = false;
        var changePage = false;
        var page = 1;
        if (state) {
            if (state.sort) {
                var list = new ListView();
                if (list.sortColun != state.sort.column) {
                    list.sortColumn = state.sort.column;
                    changePage = true;
                }
                if (list.sortReverse != state.sort.reverse) {
                    list.sortReverse = state.sort.reverse;
                    changePage = true;
                }
                list.styleSortCols(true);
            }

            // state.criteria -- this should be handled as query string params
            // state.activeCriteria -- this is better handled in results.jsp

            var activeTab = 1;
            if (state.activeTab > 0) {
                activeTab = state.activeTab;
            }
            
            var paginator = new Paginator();
            if (state.page) {
                if (state.page.current > 1) {
                    page = state.page.current;
                }
                if (state.page.itemsPerPage > 0 && state.page.itemsPerPage != paginator.itemsPerPage) {
                    changePage = true;
                    paginator.setItemsPerPage(state.page.itemsPerPage);
                }
            }
            if (page != paginator.currentPage) {
                changePage = true;
            }
            
            if (state.forceSearch) {
                new CriteriaController().forceNewSearch = true;
            }
            
            if (state.highlighted && state.highlighted.length > 0) {
                new ActionManager().selectEntities(state.highlighted);
            }
            if (state.viewHighlighted) {
                new ActionManager()._viewSelected();
                doSearch = false;
                viewSelected = true;
            }

            if (doSearch) {
                new CriteriaController().search(page);
            } else if (changePage || viewSelected) {
                new ListView().show(page);
            }
            
            if (state.map) {
                new MapManager().show(new GLatLng(state.map.latitude, state.map.longitude), eval(state.map.zoom));
                if (state.openInfoWindowID || state.polygon) {
                   var id = state.openInfoWindowID;
                   var fn = function() {
                       if (new MapManager().getMap() && new MapManager().getMap().isLoaded()) {
                           if (state.openInfoWindowID) {
                               try {
                                   new EntityManager().getEntity(id).getMarker().openInfoWindow();
                               } catch (e) {
                                   // if the search has too many results to map, EntityManager might not any entities (see onMapChange)
                               }
                           }
                           /*
                           if (state.polygon) {
                               var color = "#FF0000";
                               var newPolygon = new MapManager().getMap().polygonControl.createPolygon(state.polygon,color, 4, 0.5,  color,  0.1);
                               new MapManager().getMap().drawPolygon("user",newPolygon);
                           }
                           */
                       } else {
                           window.setTimeout(fn,1000);
                       }
                   }
                   fn();
                }
            }
            
            // state.savedSearchID -- handled via query string params
            
            var tabs = new TabsManager();
            if (state.detailsEntityID) {
                new DetailsView()._showDetails(state.detailsEntityID);
                tabs.enableTab('details-view');
            } else {
                tabs.disableTab('details-view');
                if (activeTab == tabs.getTabIndex('details-view')) activeTab = 1;
            }
            
            if (state.emailEntityIDs && state.emailEntityIDs.length > 0) {
                new EmailView()._show(state.emailEntityIDs);
                tabs.enableTab('email-view');
            } else {
                tabs.disableTab('email-view');
                if (activeTab == tabs.getTabIndex('email-view')) activeTab = 1;
            }
            
            if (state.reportEntityIDs && state.reportEntityIDs.length > 0) {
                new ReportsView()._show(state.reportEntityIDs);
                tabs.enableTab('reports-view');
            } else {
                tabs.disableTab('reports-view');
                if (activeTab == tabs.getTabIndex('reports-view')) activeTab = 1;
            }
            
            if (state.catalogEntityIDs && state.catalogEntityIDs.length > 0) {
                new AddToCatalogView()._show(state.catalogEntityIDs);
                tabs.enableTab('catalog-view');
            } else {
                tabs.disableTab('catalog-view');
                if (activeTab == tabs.getTabIndex('catalog-view')) activeTab = 1;
            }
            
            if (state.exportEntityIDs && state.exportEntityIDs.length > 0) {
                new ExportView()._show(state.exportEntityIDs);
                tabs.enableTab('export-view');
            } else {
                tabs.disableTab('export-view');
                if (activeTab == tabs.getTabIndex('export-view')) activeTab = 1;
            }

            if (state.historyID) {
                var wshm = new WindowStateHistoryManager();
                wshm.historyStackCounter = state.historyID;
            }
            
            var actionMan = new ActionManager();
            if (actionMan.getActiveTab() != activeTab) {
                tabs.enableTab(activeTab).triggerTab(activeTab);
                function zoomIt() {
                    if (DEBUG) debug("zoomIt");
                    try {
                        var north = 0
                        var south = 0
                        var east = 0
                        var west = 0;
                        var ids = new ActionManager().getSelectedEntityIDs();
                        for (var i=0;i<ids.length;i++) {
                            var id = ids[i];
                            var entity = new EntityManager().getEntity(id);
                            var pt = entity.point;
                            if (DEBUG) debug("zoomIt pt {0}".format(pt));
                            if ((!north || pt.lat() > north)) {
                                north = parseFloat(pt.lat());
                            }
                            if ((!south || pt.lat() < south)) {
                                south = parseFloat(pt.lat());
                            }
                            if ((!east || pt.lng() > east)) {
                                east = parseFloat(pt.lng());
                            }
                            if ((!west || pt.lng() < west)) {
                                west = parseFloat(pt.lng());
                            }
                        }
                        var mgr = new MapManager();
                        if (mgr.getMap()) {
                            mgr.getMap().zoomToFit(north,south,east,west);
                        } else {
                            window.setTimeout(zoomIt,1000);
                        }
                    } catch (e) {
                        window.setTimeout(zoomIt,1000);
                    }
                }
                if (state.highlighted && state.highlighted.length > 0 && !state.map && activeTab == 2) {
                	zoomIt();
                }
            }
        }
        
        if (DEBUG) debug("restoration complete");
        
        this.dontSetContext = false;
    }
    
    this.serialize = function() {
        var hash = "";
        // tab
        if (new TabsManager().activeTab() > 1) {
            hash += "t=" + new TabsManager().activeTab() + "|";
        }

        // page
        var pg = new Paginator();
        hash += "p=" + pg.currentPage + "," + pg.itemsPerPage + "|";

        // sort
        var list = new ListView();
        if (list.sortColumn) {
            hash += "s=" + list.sortColumn + "," + (list.sortReverse ? "0" : "1") + "|";
        }

        // map
        var map = new MapManager().getMap();
        if (map && map.getCenter()) {
            hash += "m=" + map.getZoom() + "," + map.getCenter().lat() + "," + map.getCenter().lng() + "|";
        }

        // info window
        if (map && map.openInfoWindowMarker) {
            hash += "iw=" + map.openInfoWindowMarker.mappableID + "|";
        }

        // polygon
        if (map && map.getUserShape()) {
            var coords = new Array();
            for (var i=0;i<map.getUserShape().getVertexCount();i++) {
                var v = map.getUserShape().getVertex(i);
                coords.push(v.lat());
                coords.push(v.lng());
            }
            hash += "poly=" + coords.join(",") + "|";
        }

        // search criteria
        var criteriaVals = new CriteriaController().lastSearchValues;
        if (criteriaVals) {
            hash += "c=";
            for (var name in criteriaVals) {
                var vals = mapFunction(function(val) { return hashEscape(val) }, criteriaVals[name]);
                hash += name + ":" + vals.join(",") + ";"
            }
            hash += "|";
        }
        // active criteria
        var criteria = new CriteriaManager().currentCriteria;
        if (criteria && criteria.length > 0) {
            var names = mapFunction(function(criterion) { 
                var val = criterion.getName();
                if (criterion.serialize) {
                    val += ":" + hashEscape(criterion.serialize());
                }
                return val;
            }, criteria);
            hash += "ac=" + names.join(",") + "|";
        }
        
        
        // highlighted
        var highlighted = new ActionManager().getSelectedEntityIDs();
        if (highlighted.length > 0) {
            hash += "h=" + highlighted.join(",") + "|";
        }
        
        // saved search
        var savedSearch = new SavedSearchManager();
        if (savedSearch.savedSearchID > 0) {
            hash += "ss=" + savedSearch.savedSearchID + "|";
        }
        
        // details tab
        var currentDetailsEntityID = new DetailsView().getCurrentEntityID();
        if (currentDetailsEntityID) {
            hash += "d=" + currentDetailsEntityID + "|"
        }
        
        var subtitle = this.getSubtitle();
        if (subtitle.length > 0) {
            hash += "tl=" + hashEscape(subtitle) + "|"
        }
        
        // email tab
        var emailIDs = new EmailView().getCurrentEntityIDs();
        if (emailIDs && emailIDs.length > 0) {
            hash += "e=" + emailIDs.join(",") + "|";
        }
        
        // reports tab
        var reportIDs = new ReportsView().getCurrentEntityIDs();
        if (reportIDs && reportIDs.length > 0) {
            hash += "r=" + reportIDs.join(",") + "|";
        }
        
        // add to catalog tab
        var catalogIDs = new AddToCatalogView().getCurrentEntityIDs();
        if (catalogIDs && catalogIDs.length > 0) {
            hash += "ct=" + catalogIDs.join(",") + "|";
        }
        
        // export tab
        var exportIDs = new ExportView().currentEntityIDs;
        if (exportIDs && exportIDs.length > 0) {
            hash += "ex=" + exportIDs.join(",") + "|";
        }
        
        // viewing highlighted
        if (new EntityManager().overrideEntityIDs) {
            hash += "hv=1|";
        }
        
        return hash;
    }

    this.deserialize = function(serialized) {
        var state = new Object();
        
        var items = serialized.split('|');
        for (var i = 0; i < items.length; i++) {
            var keyValue = items[i].split('=');
            var key = keyValue[0];
            var value = keyValue[1];
            switch (key) {
            case 't':
                state.activeTab = parseInt(value);
                break;

            case 'p':
                var pageItems = value.split(',');
                state.page = {'current':parseInt(pageItems[0]), 'itemsPerPage':parseInt(pageItems[1])};
                break;

            case 's':
                var sortItems = value.split(',');
                state.sort = {'column':sortItems[0], 'reverse':sortItems[1] == '0'}
                break;

            case 'm':
                var mapItems = value.split(',');
                state.map = {'zoom': mapItems[0], 'latitude':mapItems[1], 'longitude':mapItems[2]};
                break;
                
            case 'iw':
                state.openInfoWindowID = value;
                break;
                
            case 'poly':
                var coords = value.split(',');
                var polygon = new Array();
                for (var j=0;j<coords.length;j++) {
                    var lat = coords[j];
                    j++;
                    var lng = coords[j];
                    polygon.push(new GLatLng(lat,lng));
                }
                state.polygon = polygon;
                break;
                
            case 'c':
                state.criteria = new Object();
                
                var criteria = value.split(';');
                for (var j = 0; j < criteria.length; j++) {
                    if (criteria[j].indexOf(':') < 0) continue;
                    
                    var criteriaKeyVal = criteria[j].split(':');
                    var values = mapFunction(function(val) { return hashUnescape(val) }, criteriaKeyVal[1].split(','));
                    state.criteria[criteriaKeyVal[0]] = values;
                }
                break;
                
            case 'ac':
                state.activeCriteria = new Object();
                var criteria = value.split(',');
                for (var j = 0; j < criteria.length; j++) {
                    var nameState = criteria[j].split(':');
                    if (nameState.length == 1) {
                        nameState.push('');
                    }
                    state.activeCriteria[nameState[0]] = hashUnescape(nameState[1]);
                }
                
                break;
                
            case 'h':
                var ids = value.split(',');
                state.highlighted = ids;
                break;

            case 'ss':
                state.savedSearchID = parseInt(value);
                break;

            case 'd':
                state.detailsEntityID = value;
                break;

            case 'tl':
                state.subtitle = hashUnescape(value);
                break;

            case 'e':
                state.emailEntityIDs = value.split(',');
                break;

            case 'r':
                state.reportEntityIDs = value.split(',');
                break;

            case 'ct':
                state.catalogEntityIDs = value.split(',');
                break;

            case 'ex':
                state.exportEntityIDs = value.split(',');
                break;

            case 'hv':
                state.viewHighlighted = (value == '1');
                break;

            case 'hid':
                state.historyID = parseInt(value);
                break;

            case 'force':
                state.forceSearch = true;
                break;
                
            default:
                break;
            }
        }
        
        return state;
    }
    
    // escape [=|:,;/] w/ "/HH"
    function hashEscape(val) {
        val = val||"";
        val = val.toString();
        val = val.replace(/\//g, '/2F');
                
        val = val.replace(/=/g, '/3D');
        val = val.replace(/\|/g, '/7C');
        val = val.replace(/:/g, '/3A');
        val = val.replace(/,/g, '/2C');
        val = val.replace(/;/g, '/3B');
        return val;
    }
    function hashUnescape(val) {
        val = val.replace(/\/3D/g, '=');
        val = val.replace(/\/7C/g, '|');
        val = val.replace(/\/3A/g, ':');
        val = val.replace(/\/2C/g, ',');
        val = val.replace(/\/3B/g, ';');
        
        val = val.replace(/\/2F/g, '/');
        return val;
    }
}
function getCookieValue(name) {
    var val = '';
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
        cookies[i] = cookies[i].trim();
        if (cookies[i].indexOf(name + '=') == 0) {
            val = cookies[i].substring((name + '=').length) == 'true';
        }
    }
    return val;
}

function WindowStateHistoryManager() {
    if (WindowStateHistoryManager.instance) {
        return WindowStateHistoryManager.instance;
    }
    WindowStateHistoryManager.instance = this;
    
    this.useIframeHistory = IS_IE;
    this.iframe = null;
    this.historyStackCounter = 0;
    
    this.ignoreHistoryEvent = false;
    
    this.init = function(iframeID) {
        this.iframe = $('#' + iframeID)[0];
        this._checkHistoryChange('' + this.historyStackCounter);
    }
    this._checkHistoryChange = function(lastVal) {
        var newVal;
        if (this.useIframeHistory) {
            var id = /\bid=(\d+)\b/.exec(this.iframe.contentWindow.location.search);
            newVal = ((id != null) && (id.length > 0)) ? id[1] : '0';
        } else {
            var id = /\bhid=(\d+)\b/.exec(window.location.hash);
            newVal = ((id != null) && (id.length > 0)) ? id[1] : '0';
        }
        
        if (newVal != lastVal && newVal.length > 0) {
            if (this.ignoreHistoryEvent) {
                this.ignoreHistoryEvent = false;
            } else {
                var serialized;
                if (this.useIframeHistory) {
                    var hash = this.iframe.contentWindow.location.hash;
                    if (hash.indexOf('#') > -1) {
                        serialized = hash.substring(hash.indexOf('#') + 1);
                    }
                } else {
                    var hash = window.location.hash;
                    if (hash.indexOf('#') > -1) {
                        serialized = hash.substring(hash.indexOf('#') + 1);
                    }
                }
                
                if (serialized) {
                    if (DEBUG) debug('history change: ' + serialized);
                    if (this.onHistoryChange) {
                        this.onHistoryChange(serialized);
                    }
                }
            }
        }

        setTimeout(function() {new WindowStateHistoryManager()._checkHistoryChange(newVal)}, 250);
    }
    // this guy is called from the iframe
    this._iframeLoaded = function(id) {
        //if (DEBUG) debug('iframe loaded: ' + id);
    }
    
    this._saveState = function(noIframe) {
        var now = new Date().getTime();
        
        var serialized = new WindowState().serialize();
        window.location.replace("#" + serialized + 'hid=' + this.historyStackCounter + '|');
        
        if (!noIframe && this.useIframeHistory && this.iframe) {
            var newLoc = this.iframe.contentWindow.location.href;
            if (newLoc.indexOf('#') > 0) {
                newLoc = newLoc.substring(0, newLoc.indexOf('#'));
            }
            newLoc += '#' + serialized;
            this.iframe.contentWindow.location.replace(newLoc);
        }
        
        if (DEBUG) debug('saving state (' + (new Date().getTime() - now) + 'ms): ' + serialized);
        
        return serialized;
    }

    this.saverID = null;
    this.saveState = function() {
        if (this.saverID) {
            clearTimeout(this.saverID);
        }
        this.saverID = setTimeout(function() {new WindowStateHistoryManager()._saveState()}, IS_IE ? 2000 : 500);
    }

    this._addHistoryStackFrame = function() {
        this.historyStackCounter++;
        
        if (this.useIframeHistory) {
            var serialized = this._saveState(true);
            new WindowState().updateTitle();

            if (DEBUG) debug('history checkpoint[' + this.historyStackCounter + ']');

            this.ignoreHistoryEvent = true;
            var newLocation = "results_context.jsp?subtitle=" + escape(new WindowState().getSubtitle()) + "&id=" + this.historyStackCounter + "#" + serialized;
            this.iframe.src = newLocation;
            
        } else {
            var now = new Date().getTime();
            
            var serialized =  new WindowState().serialize();

            this.ignoreHistoryEvent = true;
            window.location = '#' + serialized + 'hid=' + this.historyStackCounter + '|';
            
            new WindowState().updateTitle();
            
            if (DEBUG) debug('saving state (' + (new Date().getTime() - now) + 'ms): ' + serialized);
            if (DEBUG) debug('history checkpoint[' + this.historyStackCounter + ']');
        }
    }
    
    this.historyAdderID = null;
    this.addHistoryStackFrame = function() {
        if (this.saverID) {
            clearTimeout(this.saverID);
            this.saverID = null;
        }
        if (this.historyAdderID) {
            clearTimeout(this.historyAdderID);
        }
        this.historyAdderID = setTimeout(function() {new WindowStateHistoryManager()._addHistoryStackFrame()}, 100);       
    }
    
    this.onHistoryChange = function(newState) {}
}

function SavedSearchManager(savedSearchID, savedSearchName, additionalEmail) {
    if (SavedSearchManager.instance) {
        return SavedSearchManager.instance;
    }
    SavedSearchManager.instance = this;

    this.savedSearchID = savedSearchID;
    this.savedSearchName = savedSearchName;
    this.additionalEmail= additionalEmail;
    
    this.isSavedSearch = function() {
        return this.savedSearchID > 0;
    }
    
    this.callSaveSearch = function(savedSearchID, savedSearchName, alertFrequencyDays, savedSearchAdditionalEmail) {
        var handler = function(responseXML) {
            var el = responseXML.documentElement;
            var savedID = parseInt(el.getAttribute("id"));
            var savedName = el.getAttribute("name");
            var savedAdditionalEmail = el.getAttribute("additional-email-address");
            var errorMsg = el.getAttribute("error-message");

            //TODO: error handling, revert saved search name if the call errors out?

            var me = new SavedSearchManager();
            if (savedID != me.savedSearchID) {
                me.savedSearchID = savedID;
                me.savedSearchName = savedName;
                me.savedSearchAdditionalEmail = savedAdditionalEmail;
                
                $('#load-searches, #to-save').removeClass('hidden');
                $("#load-searches").append('<a href="?savedSearchID={0}" title="Load this Saved Search">{1}</a>'.format(savedID, savedName));
                $('#load-searches').find('a:odd:last').addClass("odd");
                $('#load-searches').find('a:even:last').addClass("even");
                
                new WindowState().save();
            }
        }
        
        caller = new AJAXMethodCaller("com.catylist.ajax.SavedSearchProvider", "saveSearch");
        caller.setSuccessfulResponseXMLHandler(handler);
        
        caller.addParameter("java.lang.Integer", savedSearchID);
        caller.addParameter("java.lang.String", savedSearchName);
        caller.addParameter("java.lang.String", ENTITY_MODE == COMPARABLE ? "COMPARABLES" : "LISTINGS_AND_PROPERTIES");
        caller.addParameter("java.lang.String", savedSearchAdditionalEmail);

        var criteriaVals = new CriteriaController().lastSearchValues;
        if (criteriaVals) {
            for (var key in criteriaVals) {
                caller.addRequestParameter(key, criteriaVals[key]);
            }
            if (alertFrequencyDays) {
                caller.addRequestParameter("frequency", alertFrequencyDays);
            }
            if (savedSearchAdditionalEmail) {
                caller.addRequestParameter("frequency", alertFrequencyDays);
            }
            
            caller.call();
        }
    }
    
    this.saveAs = function(savedSearchID, newName, freq, additionalEmail) {
        this.callSaveSearch(savedSearchID, newName, freq, additionalEmail);
        
        document.title = MAIN_TITLE + ' : ' + newName;
        $('#saved-search-title, #saved-search-to-save').html(newName);
    }
}

// note: scripts w/ document.write()'s (like google maps) in them are not working w/ firefox 2
function ScriptManager() {
    if (ScriptManager.instance) {
        return ScriptManager.instance;
    }
    ScriptManager.instance = this;

    function Script(url) {
        this.url = url;
        
        this.added = false;
        this.loaded = false;
        
        this._loadCallbacks = new Array();
        
        this.load = function(callback) {
            this._loadCallbacks.push(callback);
            
            if (!this.added) {
                this.added = true;
                
                var scriptNode = document.createElement('script');
                scriptNode.setAttribute('type', 'text/javascript');
                scriptNode.setAttribute('src', this.url);
                
                var me = this;
                // !IE:
                scriptNode.onload = function() {
                    me.loaded = true;
                    me._callLoadCallbacks();
                }
                // IE:
                scriptNode.onreadystatechange = function() {
                    // I'm seeing either "complete" or "loaded", but the specs say there should be both so we'll handle that.
                    if (!me.loaded && 
                            (this.readyState == 'complete' || this.readyState == 'loaded')) {
                        me.loaded = true;
                        me._callLoadCallbacks();
                    }
                }
                
                document.documentElement.appendChild(scriptNode);
            } else if (this.loaded) {
                this._callLoadCallbacks();
            }
        }
        this._callLoadCallbacks = function() {
            var callbacks = this._loadCallbacks;
            this._loadCallbacks = new Array();
            
            for (var i = 0; i < callbacks.length; i++) {
                callbacks[i]();
            }
        }
        
        this.toString = function() { return this.url };
    }
    
    this.detailsScripts = [
        //new Script("/js/widget.js"),
        new Script("/js/slideshow.js")
    ];

    this.mapScripts = [
        new Script('/js/label.js'),
        new Script('/js/maps.js'),
        new Script("/jsp/search/js/search-map.js"),
        new Script('/js/BDCCPolyline.js'),
        new Script('/js/BDCCPolygon.js'),
        new Script('/js/CPolygonControl.js')
    ];
   
    this.catalogScripts = [
         new Script("/js/catalog.js")
    ];
    
    this._loadScripts = function(scripts, callback) {
        if (scripts && scripts.length > 0) {
            var calledback = false;
            var checkLoaded = function() {
                var allLoaded = true;
                for (var i = 0; i < scripts.length; i++) {
                    if (!scripts[i].loaded) {
                        allLoaded = false;
                        break;
                    }
                }
                if (allLoaded && !calledback) {
                    calledback = true;
                    if (DEBUG) debug('scripts loaded: ' + scripts.join(', '));
                    callback();
                }
            }
            
            for (var i = 0; i < scripts.length; i++) {
                scripts[i].load(checkLoaded);
            }
        } else {
            callback();
        }
    }
    
    this.loadMapScripts = function(callback) {
        this._loadScripts(this.mapScripts, callback);
        //this.mapScripts = new Array();
    }
    
    this.loadDetailsScripts = function(callback) {
        this._loadScripts(this.detailsScripts.concat(this.mapScripts), callback);
        //this.detailsScripts = new Array();
        //this.mapScripts = new Array();
    }
    
    this.loadCatalogScripts = function(callback) {
        this._loadScripts(this.catalogScripts, callback);
        //this.catalogScripts = new Array();
    }
}

function Polygon() {
    this.points = new Array();
    
    this.addPoint = function(gLatLng) {
        this.points.push(gLatLng);
    }

    this.getSerializedPoints = function() {
        var pointsSerializedInArray = new Array();

        for (var i=0;i<this.points.length;i++) {
            var point = this.points[i];
            pointsSerializedInArray.push(point.x + ":" + point.y);
        }
        return pointsSerializedInArray.join(",");
    }

    this.close = function() {
        var first = this.points[0];
        var last = this.points[this.points.length - 1];
        var isClosed =  (this.points.length > 1 && first.lat() == last.lat() && first.lng() == last.lng());
        if (!isClosed) {
            this.addPoint(first);
        }

    }

    this.clear = function() {
        this.points = new Array();
    }

    this.containsPoint = function(testPoint) {
         //based on work in http://www.scottandrew.com/js/js_util.js
         var lngnew,latnew,lngold,latold,lng1,lat1,lng2,lat2,i;
         var inside=false;
        
         if (this.points.length >= 3 && testPoint) { // points don't describe a polygon, or there is no valid testPoint
            plng = testPoint.lng();
            plat = testPoint.lat();
            var lastPoint = this.points[this.points.length - 1];
            lngold = lastPoint.lng();
            latold = lastPoint.lat();

            for (var i=0 ; i < this.points.length ; i++) {
                var point = this.points[i];
                lngnew = point.lng();
                latnew = point.lat();
                if (lngnew > lngold) {
                    lng1=lngold;
                    lng2=lngnew;
                    lat1=latold;
                    lat2=latnew;
                }else {
                    lng1=lngnew;
                    lng2=lngold;
                    lat1=latnew;
                    lat2=latold;
                }
                if ((lngnew < plng) == (plng <= lngold) && ((plat-lat1)*(lng2-lng1) < (lat2-lat1)*(plng-lng1))) {
                    inside=!inside;
                }
                lngold=lngnew;
                latold=latnew;
            }
        }
        return inside;
    }
    
    this.getBounds = function() {
        var north;
        var south;
        var east;
        var west;
        
        for (var i=0 ; i < this.points.length ; i++) {
            var point = this.points[i];
            if (north == null || north < point.lat()) {
                north = point.lat();
            }
            if (south == null || south > point.lat()) {
                south = point.lat();
            }
            if (east == null || east < point.lng()) {
                east = point.lng();
            }
            if (west == null || west > point.lng()) {
                west = point.lng();
            }
        }
        return new GLatLngBounds(new GLatLng(south,west), new GLatLng(north,east));
    }
    
    this.equals = function(other) {
        var equals = (this.points.length == other.points.length);
        for (var i=0;equals && i<this.points.length;i++) {
            equals = (this.points[i].equals(other.points[i]))
        }
        return equals;
    }
    
    for (var i=0;i<arguments.length;i++) {
        this.addPoint(arguments[i]);
    }
}
function show_calendar(fieldName) {
    var field = $('#search-filters,#primary-sitelink-filters').find('input:text[name=' + fieldName + ']');

    var cal = new YAHOO.widget.Calendar('theCalendar', 'calendar-container', { close:true });
    
    var mdy = field.val().split("/");
    if (mdy.length == 3) {
        cal.select(mdy.join("/"));
        cal.cfg.setProperty("pagedate", mdy[0] + "/" + mdy[2]);
    }
    cal.selectEvent.subscribe(function(type,args,obj) {
        var dates = args[0];
        var date = dates[0];
        var year = date[0], month = date[1], day = date[2];

        field.val(month + "/" + day + "/" + year);
        
        cal.hide();
        
        new CriteriaController().search();
    }, cal, true);
    
    cal.hideEvent.subscribe(function() {
        $("body").removeClass("thick");
        showPageSelects();
    });

    cal.render();

    $("#thickbox-mimic").unbind().click(function() {
        cal.hide();
    });
    $("body").addClass("thick");
    hidePageSelects();
    
    cal.show();
}


/*
 * functions defined in email_compose_include.jsp that are overridden for new
 * style searches
 */
function openRecipientPopup() {
    window.open('/jsp/email/email_recipients_list_popup.jsp','emailPopup','toolbar=no,location=no,directories=no,status=no,menubar=no,resizable=yes,copyhistory=no,scrollbars=yes,width=300,height=500');
}
function showEntityDetails(id, context) {
    var typeAndID;
    if (context.indexOf('listing') > -1) {
        typeAndID = LISTING + '_' + id;
    } else if (context.indexOf('property') > -1) {
        typeAndID = PROPERTY + '_' + id;
    }
    
    new DetailsView().showDetails(typeAndID);
}
function removeRecipients() {
    new EmailView()._load("/jsp/search/email_compose.jsp?removeRecipients=removeRecipients");
}

/* functions defined in global.js that are overridden for new style searches */
/*email recipient search submit*/
var recipientsWindow = null;
function emailFormSaveInfo() {
    recipientsWindow = window.open("","recipientsPopup","toolbar=" + (IS_IE ? "no" : "yes") + ",location=" + (IS_IE ? "yes" : "no") + ",directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=yes,width=640,height=480");

    var emailForm = $("#emailForm")[0];
    emailForm.action = "email_recipients_search.jsp";
    emailForm.target = "recipientsPopup";
    emailForm.enctype = "";
    emailForm.submit();
}

var customizeWindow = null;
function submitReportSelectionForm(which, where, what) {
    // If "Customize Report" button was clicked
    if (which == 'customize') {
        // Set the action of the form
        if ( what == 'comp') {
            document.choosetypeform.action = '/jsp/search/report_customization.jsp';
        } else {
            document.choosetypeform.action = '/jsp/search/report_customization.jsp';
        }
    
        customizeWindow = window.open("","customizePopup","toolbar=" + (IS_IE ? "no" : "yes") + ",location=" + (IS_IE ? "yes" : "no") + ",directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=yes,width=640,height=680");
        
        document.choosetypeform.target = 'customizePopup';
        document.choosetypeform.submit();
    
    // If "Generate PDF" button was clicked
    } else if (which == 'generate') {
        // Open the popup window
        window.open('','pdfPopup','toolbar=no,location=no,directories=no,status=no,menubar=no,resizable=no,copyhistory=no,scrollbars=no,width=400,height=300');
    
        var pathDir = 'multi_listing'
        if ( what == 'comp') {
            pathDir = 'multi_comp';
        }

        var form;
        if (where == 'customize') {
            form = document.customizeForm;
        } else if (where == 'reorder') {
            form = document.reorder_form;
        } else {
            form = document.choosetypeform;
        }
        form.action = '/jsp/reports/report_viewpdf.jsp';
        form.target = 'pdfPopup';
        form.submit();
        
        if (customizeWindow) {
            customizeWindow.close();
        }
    }
}

// used by overview tab (see listing_suites_include.jsp)
function expandAllSuiteDetails() {
    $('.moreSuitesInfo').removeClass('hidden');
    $('#expandAllSuites').addClass('hidden');
    $('#hideAllSuites').removeClass('hidden');
}
function hideAllSuiteDetails() {
    $('.moreSuitesInfo').addClass('hidden');
    $('#hideAllSuites').addClass('hidden');
    $('#expandAllSuites').removeClass('hidden');
}
function toggleSuiteDetails(suiteID) {
    $('.suite-details-' + suiteID).toggleClass('hidden');
}

// overview tab (see listing_overview_actions_include.jsp)
function toggleLink() {
    var el = document.getElementById('directLink');
    var display = el.style.display;
    el.style.display=(display?'':'none');
 }