/*! Copyright 2008 360-Systems
 * $Id$ */
/*jslint strict: true, undef: true, eqeqeq: true, immed: true, browser: true */
/*globals $, $defined, Class, Hash, Options, Request, Element */
/*globals QueryString, Debug */
/* ----------------------------------------------------------------------------
 * @author  Andy Kernahan
 * @created 28/10/2008
 * @depends utils.js, MooTools Core (>=1.2).
 * ------------------------------------------------------------------------- */

 /* ----------------------------- SearcherMode ----------------------------- */
/**
 * 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
};
/* ------------------------------- Searcher -------------------------------- */
var Searcher = new Class({
    Implements: [Options, Debug],
    options: {
        /* The base database URL. */
        url: null,
        /*
         * True to enable query caching, otherwise; false.
         */
        enableCache: true,
        /**
         * The @see SearcherMode mode.
         */
        mode: SearcherMode.ADVANCED,
        /**
         * The default control values. Values can be specified by thier name or
         * actual value. E.g. Drivers or ([JOBCATEGORY]=23). The object
         * properties should be equal to that of the ID of the control to set.
         * For example: sb_type: "West Midlands"
         */
        defaults: { }
    },
    /**
     * 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_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();
                }
            });
            kwEl.addEvent("blur", function(evt) {
                addKwEl.fireEvent("click");
            });
        }
    },
    /**
     * Initialises the display.
     */
    _initDisplay: function() {

        this._idle();
        if(!this._isModeEnabled(SearcherMode.ADVANCED)) {                      
            $("sb_sub_location").hide();
            $("sb_keyword").hide();
            $("sb-kws").hide();
        }
        if(this._setControlValuesFromOptions() ||
            this._setControlValuesFromQueryString()) {
            this.doExpandLocation();
        }
    },
    /* Sets the control values based on the options. This method will override
     * the control's current value.
     * @returns True if one or more controls have had thier value set, otherwise;
     * false.
     */
    _setControlValuesFromOptions: function() {

        var el;
        var value;
        var setValue = false;
        var defaults = this.options.defaults;

        for(var name in defaults) {
            if(defaults.hasOwnProperty(name) && (value = defaults[name])) {
                el = $(name);
                if(el.set("value", value).get("value") !== value) {
                    // Try to select the option based on its name.
                    for(var i = 0, l = el.options.length; i < l; ++i) {
                        if(el.options[i].text === value) {
                            el.options[i].selected = true;
                            setValue = true;
                        }
                    }
                }
            }
        }

        return setValue;
    },
    /* Sets the control values based on the query string. This method will
     * override the control's current value.
     * @returns True if one or more controls have had thier value set, otherwise;
     * false.
     */
    _setControlValuesFromQueryString: function() {

        var el;
        var setValue = false;
        var qs = QueryString.parse();

        for(var name in qs) {
            if(qs.hasOwnProperty(name)) {
                if((el = $(name))) {
                    el.set("value", qs[name]);
                    setValue = 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]);
                    setValue = true;
                }
            }.bind(this));
        }

        return setValue;
    },
    /**
     * 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;

        this.debug("doing-action, action=%s", action);

        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: this.options.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\")");
        terms.push("((TERMWEIGHT 90 SW_01)or(TERMWEIGHT 1 Vacancy)or(TERMWEIGHT 5 ClientVacancy))");
        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", "3");
        query.set("Start", "1");
        query.set("Count", "15");
        query.set("SearchMax", "10000");

        return query.toQueryString();
    },
    /**
     * Navigates to the current search.
     */
    doNavigate: function() {

        document.location.replace(this.options.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.debug("count-callback, count=%s", response.result);
        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.debug("render-callback");
        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");

        this.debug("expand-location-callback, count=%s", items.length);
        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.debug("cache-hit");
            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 if(count === 1) {
            html = "One job matches your search!";
        } else {
            html = count + " jobs match your search";
        }
        $("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 === "~";
    }
});

