/*! Copyright 2008 360-Systems */
/*jslint undef: true, eqeqeq: true, browser: true */
/*globals $, $defined, Class, Hash, Options, Request, Element */
/*globals QueryString, db_url */
/* ----------------------------------------------------------------------------
 * @author  Andy Kernahan
 * @created 28/10/2008
 * @depends utils.js, MooTools Core (>=1.2).
 * ------------------------------------------------------------------------- */
/**
 * Defines the @see Searcher load modes.
 */
var SearcherMode = {
    /**
     * The searcher loads in basic mode. In this mode the advanced search
     * options are hidden from users.
     */
    BASIC: 1,
    /**
     * The searcher loads in advanced mode. In this mode the advanced search
     * options are displayed allowing the user to select sub-locations and
     * add / remove keywords from the search.
     */
    ADVANCED: 2,
    /**
     * The searcher loads in auto-render mode. In this mode the searcher will
     * automagically render the hits as the criteria changes as well as
     * updating the hit count.
     */
    AUTO_RENDER: 4
};
var Searcher = new Class({
    Implements: Options,
    options: {
        /*
         * True to enable query caching, otherwise; false.
         */
        enableCache: true,
        /**
         * The @see SearcherMode mode.
         */
        mode: SearcherMode.ADVANCED
    },
    /**
     * The current request.
     */
    _request: null,
    /**
     * The query cache.
     */
    _cache: {},
    /**
     * Initialises a new instance of the Searcher class.
     * @param options the options.
     */
    initialize: function(options) {
        this.setOptions(options);
        this._fields = [
            $("sb_job_sector"),
            $("sb_job_category"),
            $("sb_location"),
            $("sb_sub_location"),
            $("sb_type"),
            $("sb_salary"),
            $("sb_keywords")
        ].clean();
        this._hookUpHandlers();
        this._initCache();
        this._initDisplay();        
    },
    /**
     * Hooks up the event handlers the the form controls.
     */
    _hookUpHandlers: function() {
        var handler = this._criteriaChanged.bind(this);
        this._fields.each(function(field) {
            field.addEvent("change", handler);
        });
        if(this._isModeEnabled(SearcherMode.ADVANCED)) {
            var locationEl = $("sb_location");
            // Re-wire the location change handler.
            locationEl.removeEvent("change", handler);
            locationEl.addEvent("change", this.doExpandLocation.bind(this));
            var kwEl = $("sb_keyword");
            var addKwEl = $("sb-btn-add-kw");
            // Respond to keywords additions.
            addKwEl.addEvent("click", function() {
                this._addKeyword(kwEl.get("value"));
                kwEl.set("value", "");
                this._criteriaChanged();
            }.bind(this));
            kwEl.addEvent("keypress", function(evt) {
                if(evt.key === "enter") {
                    addKwEl.fireEvent("click");
                    evt.stop();
                }
            }.bindWithEvent(this));
        }
    },
    /**
     * Initialises the display.
     */
    _initDisplay: function() {
        this._idle();
        if(!this._isModeEnabled(SearcherMode.ADVANCED)) {
            $("sb_job_sector").hide();
            var el = $("sb_sub_location");
            if(el) { el.hide(); }
            $("sb_keyword").hide();
            $("sb-kws").hide();
        }
        
        var el;
        var haveCriteria = false;
        var qs = QueryString.parse();        
        
        for(var prop in qs) {
            if(qs.hasOwnProperty(prop)) {
                if((el = $(prop))) {                    
                    el.set("value", qs[prop]);
                    haveCriteria = true;
                }
            }
        }
        
        if($defined(qs.sb_keywords) && qs.sb_keywords !== "") {            
            this._splitKeywords(qs.sb_keywords).each(function(term) {
                if((term = term.match(/(?:\")([^\"]+)(?:\")/))) {                    
                    this._addKeyword(term[1]);
                    haveCriteria = true;
                }
            }.bind(this));
        }
        if(haveCriteria) {
            this.doExpandLocation();
        }
    },
    /**
     * Removes the specified keyword from the list.
     * @param word the keyword to remove.
     */
    _removeKeyword: function(word) {
        var term = "\"" + word + "\"";
        var wordsEl = $("sb_keywords");
        var words = this._splitKeywords(wordsEl.get("value"));
        words = words.filter(function(item) {
            return item.indexOf(term) === -1;
        });
        wordsEl.set("value", this._joinKeywords(words));
        $(this._makeKeywordId(word)).destroy();        
    },
    /**
     * Adds the specified keyword to the internal list and updates the
     * interface.
     * @param word the keyword to add.
     */
    _addKeyword: function(word) {
        if((word = this._sanitiseKeyword(word)) === "") {
            return;
        }
        var wordId = this._makeKeywordId(word);
        if($(wordId)) {
            // The keyword already exists.
            return;
        }
        var wordsEl = $("sb_keywords");
        var words = this._splitKeywords(wordsEl.get("value"));
        words.push("([SF]contains\"" + word + "\")");
        wordsEl.set("value", this._joinKeywords(words));
        var kwEl = new Element("div", {
            "id": wordId,
            "class": "kw clearfix"
        });
        kwEl.appendChild(
            new Element("div", {
                "class": "kw-title",
                "html": word
        }));
        kwEl.appendChild(
            new Element("a", {
                "class": "kw-remove",
                "html": "Remove",
                "href": "javascript:void(0)",
                "title": "Remove this keyword",
                "events": {
                    "click": function() {
                        this._removeKeyword(word);
                        this._criteriaChanged();
                    }.bind(this)
                }
        }));
        $("sb-kws").appendChild(kwEl);        
    },
    /**
     * Makes an ID for the specified keyword.
     * @param word the keyword.
     * @return the ID for the specified keyword.
     */
    _makeKeywordId: function(word) {
        var hash = word.toLowerCase().sdbmHash();
        return "kw-" + (hash < 0 ? hash * -1 : hash);
    },
    /**
     * Sanitises the specified keyword.
     * @param word the keyword.
     * @return the sanitised keyword.
     */
    _sanitiseKeyword: function(word) {
        return word.replace(/['\"\[\]]/g, "").trim();
    },
    /**
     * Splits the specified joined keywords.
     * @param s the joined keywords.
     * @return the splits keywords.
     */
    _splitKeywords: function(s) {
        /* RegExp doesn't support positive look behind so just match using a
         * positive look ahead to (. */ 
        return s !== "" ? s.split(/and(?=\()/) : [];
    },
    /**
     * Joins the specified keywords.
     * @param words the keywords.
     * @return the joined keywords.
     */
    _joinKeywords: function(words) {
        return words.length > 0 ? words.join("and") : "";
    },
    /**
     * Callback method invoked when the search criteria has changed.
     */
    _criteriaChanged: function() {
        if(this._isModeEnabled(SearcherMode.AUTO_RENDER)) {
            this.doRender();
        } else {
            this.doCount();
        }
    },
    /**
     * Executes the specified action, invoking the specified callback on
     * completion.
     * @param callback the completion callback.
     */
    _doAction: function(action, callback) {
        var query;
        if(this._request) {
            this._request.cancel();
            this._request = null;
        }
        query = this._getActionQuery(action);
        if(!this._loadFromCache(query)) {
            this._request = new Request.JSON({
                onCancel: this._idle.bind(this),
                onFailure: this._idle.bind(this),
                onRequest: this._busy.bind(this),
                onComplete: function(response) {
                    this._idle();
                    this._request = null;
                    callback.bind(this)(response);
                    this._saveToCache(query, response);
                }.bind(this),
                url: g_db_url + "/(webVacancySearch)?OpenAgent&" + query
            }).get();
        }
    },
    /**
     * Returns the query for the specified action.
     * @param action the action.
     * @return the query for the specified action.
     */
    _getActionQuery: function(action) {
        var terms = [];
        var query = new Hash();
        terms.push("([Form]=\"Vacancy\")");
        terms.push("(([PublishonWeb]=\"Publish\")or([PublishonWeb]=\"Update\"))");
        this._fields.each(function(field) {
            var value = field.get("value");
            if(!this._isEmptySelectValue(value)) {
                terms.push(value);
            }
            query.set(field.get("id"), value);
        }.bind(this));
        
        query.set("action", action);
        query.set("query", terms.join("and"));
        return query.toQueryString();
    },
    /**
     * Returns the query for the current search criteria.
     * @return the query for the current search criteria.
     */
    _getNavigateQuery: function(action) {
        var terms = [];
        var query = new Hash();
        
        terms.push("([Form]=\"Vacancy\")");
        this._fields.each(function(field) {
            var value = field.get("value");
            if(!this._isEmptySelectValue(value)) {
                terms.push(value);
            }
            query.set(field.get("id"), value);
        }.bind(this));
        
        query.set("Query", terms.join("and"));
        query.set("SearchOrder", "1");
        query.set("Start", "1");
        query.set("Count", "15");
        query.set("SearchMax", "10000");
        return query.toQueryString();
    },
    doNavigate: function() {        
        
        document.location.replace(g_db_url + "/vacancies?SearchView&" + this._getNavigateQuery());
    },
    /**
     * Executes the count action.
     */
    doCount: function() {
        $("sb-count").set("html", "");
        this._doAction("doCount", this._doCountCallback.bind(this));
    },
    /**
     * @see doCount callback.
     * @param the response.
     */
    _doCountCallback: function(response) {
        this._updateCount(response.result);
    },
    /**
     * Executes the render action.
     */
    doRender: function() {
        this._doAction("doRender", this._doRenderCallback.bind(this));
    },
    /**
     * @see doRender callback.
     * @param the response.
     */
    _doRenderCallback: function(response) {
        var result = response.result;
        this._updateCount(result.total);
        this._updateResults(result.html);
    },
    /**
     * Executes the expand location action.
     */
    doExpandLocation: function() {
        var locationEl = $("sb_location");
        var subLocationEl = $("sb_sub_location");
        
        if(!$defined(subLocationEl)) {
            return;
        }
        subLocationEl.options.length = 0;
        if(!this._isEmptySelectValue(locationEl.get("value"))) {
            subLocationEl.set("disabled", true);
            subLocationEl.addOption("Loading...", "~");
            this._doAction("doExpandLocation",
                this._doExpandLocationCallback.bind(this));
        } else {
            subLocationEl.addOption("Sub Location", "~");
            subLocationEl.disabled = true;
            this.doCount();
        }
    },
    /**
     * @see doExpandLocation callback.
     * @param the response.
     */
    _doExpandLocationCallback: function(response) {
        var items = response.result;
        var subLocationEl = $("sb_sub_location");
        subLocationEl.options.length = 0;
        if(items.length > 0) {
            subLocationEl.set("disabled", false);
            subLocationEl.addOption("All Sub Locations", "~").selected = true;
            items.sort(function(x, y) {
                if(x.name > y.name) {
                    return 1;
                } else if(x.name < y.name) {
                    return -1;
                } else {
                    return 0;
                }
            });
            items.each(function(item) {
                subLocationEl.addOption(item.name, item.value);
            });
        } else {
            subLocationEl.addOption("No Sub Locations", "~").selected = true;
        }
        this.doCount();
    },
    /**
     * Initialises the query and history cache (if enabled).
     */
    _initCache: function() {
        // void
    },
    /**
     * Attemps to load the specified query from the cache and returns a value
     * indicating success.
     * @param query the query.
     * @return true if the query was loaded, otherwise; false.
     */
    _loadFromCache: function(query) {
        if(!this.options.enableCache) {
            return;
        }
        var key = query.sdbmHash().toString();
        if(this._cache[key]) {
            var response = this._cache[key];
            this["_" + response.action + "Callback"](response);
            return true;
        }
        return false;
    },
    /**
     * Saves the specified query and response to the cache.
     * @param query the query.
     * @param response the response to cache.
     */
    _saveToCache: function(query, response) {
        if(!this.options.enableCache) {
            return;
        }
        this._cache[query.sdbmHash().toString()] = response;
    },
    /**
     * Updates the form count text given the specified count.
     * @param count the number of matches.
     */
    _updateCount: function(count) {
        var html;
        if(count === 0) {
            html = "No jobs match your search";
        } else {
            html = "<span class=\"count\">" + count + "</span> job" +
                (count > 1 ? "s" : "") +
                " match your search<br/>" +
                "to view press 'view' button below";
        }
        $("sb-count").set("html", html);
        $("sb-btn-view").set("disabled", count === 0);
    },
    /**
     * Updates the inner HTML of the results element given the specified HTML.
     * @param html the new content of the element.
     */
    _updateResults: function(html) {
        $("sb-results").set("html", html);
    },
    /**
     * Sets the status of the form to be busy.
     */
    _busy: function() {
        $("sb-count").hide();
        $("sb-loading").show();
    },
    /**
     * Sets the status of the form to be idle.
     */
    _idle: function() {
        $("sb-count").show();
        $("sb-loading").hide();
    },
    /**
     * Returns a value indicating if the specified mode is enabled.
     * @param mode the mode to test.
     * @return true if the mode is enabled, otherwise; false.
     */
    _isModeEnabled: function(mode) {
        return (this.options.mode & mode) === mode;
    },
    /**
     * Returns a value indicating the specified select value is classified
     * as empty.
     * @param value the select value.
     * @return true if the value is classified as empty, otherwise; false.
     */
    _isEmptySelectValue: function(value) {
        return value === "" || value === "~";
    }
});
