// $Header: /web/src/rsearch/web/js/rca/common.js,v 1.67 2009/02/06 00:08:01 schau Exp $

//desc   RUI namespace.
//todo   May be removed once the RUI js and css libraries have been incorporated into AD
var RUI = window.RUI || {};

RUI.RCA = {};

//method [static] RUI.initialize
//desc   Initializes behaviours.
//note   This function should be called just before the closing </body> tag,
//       which then acts as an on-DOM-load callback.
//param  [opt] savedSearch: [String]
//param  [opt] type: [String] e.g. bus/res/ren
//param  [opt] agentBrand: [String] agent customisation code
//param  [opt] custom: [String] 'cu' customisation parameter
//param  [opt] returnPath: [String]
//param  [opt] cookieDomain: [String] domain that RUI.Cookie will use
//param  [opt] randomCookie: [String] randomly generated cookie name
RUI.initialize = function (options) {
    // Set globals
    var globals = RUI.Globals.instance();
    // TODO: Refactor this into a cleaner way of adding multiple
    // attributes at once
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.SAVED_SEARCH, options.savedSearch
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.TYPE, options.type
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.AGENT_BRAND, options.agentBrand
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.CUSTOM, options.custom
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.RETURN_PATH, options.returnPath
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.COOKIE_DOMAIN, options.cookieDomain
    );
    globals.setAttribute(
        RUI.Globals.ATTRIBUTES.RANDOM_COOKIE, options.randomCookie
    );

    // Bypass Norton's popup blocker for requested new windows
    if (/MSIE/.test(navigator.userAgent) // IE
        && window.SymRealWinOpen) {
        window.open = window.SymRealWinOpen;
    }

    // Add the behaviour layer for the Global Navigation dropdown
    RUI.GroupNavigation();

    // Initialize the site-specific behaviours
    RUI.initializeSite();
};

//method [static] RUI.initializeSite
//desc   Initializes the RCA site-specific behaviours.
RUI.initializeSite = function () {
    // Create the behavioural layer for the My Tools box
    (new RUI.RCA.MyTools());

    // Initialise the property details page if we are there
    if (RUI.$(RUI.RCA.ListingDetails.ID.LISTING_DETAILS)) {
        (new RUI.RCA.ListingDetails());
    }

    // Initialise the advanced or refine search form page (event handlers on
    // checkboxes etc.)
    if (
        RUI.$(RUI.RCA.AdvancedSearchForm.ID.ADVANCED_SEARCH) ||
        RUI.$(RUI.RCA.AdvancedSearchForm.ID.REFINE_SEARCH)
    ) {
        (new RUI.RCA.AdvancedSearchForm());
    }

    // Add the behaviour layer for the basic search on the homepage
    if (RUI.$(RUI.RCA.BasicSearchForm.ID.BASIC_SEARCH)) {
        (new RUI.RCA.BasicSearchForm());
    }

    // Initialise the seo directory page if we are there
    if (RUI.$(RUI.RCA.SEODirectories.DIRECTORY_ID)) {
        (new RUI.RCA.SEODirectories());
    }

    if (RUI.$(RUI.RCA.Homepage.ID.HOMEPAGE)) {
        (new RUI.RCA.Homepage());
    }

    // Add the behaviour for handling AuctionBooking
    if (RUI.$(RUI.RCA.AuctionBooking.ID.FORM)) {
        (new RUI.RCA.AuctionBooking());
    }

    // TODO: Move to the RUI.RCA namespace
    RUI.RCA.GoogleMap.initialize();
    RUI.RCA.Facebook.initialize();
    // Add the logout onclick event to the logout link on subscriber pages
    var logoutLinks = RUI.$$(RUI.RCA.User.SUBSCRIBER_LOGOUT_CLASS);
    logoutLinks.each(function (link) {
        RUI.Event.observe(
            link, 'click', RUI.RCA.User.logOut
        );
    });
};

RUI.RCA.Facebook = {

    ID: {
        // Id of the <a> element used to share listings on Facebook
        SHARE_ON_FACEBOOK: 'shareOnFacebook'
    },

    //method initialize
    //desc   Set a listener on the Share on Facebook link if it exists.
    initialize: function () {
        var shareLink = RUI.$(RUI.RCA.Facebook.ID.SHARE_ON_FACEBOOK);

        if (!shareLink) {
            return;
        }

        // TODO: Refactor the window opener code here into the RUI namespace
        //       for general usage using the rel="external" attribute
        RUI.Event.observe(shareLink, 'click', function (event) {
            window.open(
                shareLink.href                                +
                    '?u=' + encodeURIComponent(location.href) +
                    '&t=' + encodeURIComponent(document.title),
                '_blank',
                'toolbar=0,status=0,width=626,height=436,resizeable=yes'
            );
            RUI.Event.stop(event);
        });
    }
};


RUI.Object.extend(RUI, {
    //method [static] createElement
    //desc   An IE compatibility layer for generating DOM objects with name
    //       attributes.
    //param  [req] tag [String] element tag name
    //param  [req] name [String] name attribute value
    //return [HTMLElement] element
    //todo   Move to RUI.DOM.
    createElement: function (tag, name) {
        if (name && window.ActiveXObject) {
            element = document.createElement(
                '<' + tag + ' name="' + name + '">'
            );
        }
        else {
            element = document.createElement(tag);
            element.setAttribute('name', name);
        }

        return RUI.$(element); // RUI.$() adds Prototype's methods
    },

    //method [static] getBodyWidth
    //desc   Gets the width of the current page's <body>. Useful for determining
    //       what size of image to use.
    //return [int] Width
    //eg     if (RUI.getBodyWidth() > 800) {
    //           width = 800;
    //       }
    //       else {
    //           ...
    //todo   Deprecate in favour of document.viewport.getWidth once Prototype is
    //       upgraded to >=1.6.0
    getBodyWidth: function () {
        var width = RUI.Element.getDimensions(
            RUI.$A(document.getElementsByTagName('body')).first()
        ).width;

        return width;
    }

});

//desc   Singleton class to hold globals.
//eg     var globals = RUI.Globals.instance();
RUI.Globals = RUI.Class.create();

RUI.Object.extend(RUI.Globals, {
    // Keys used in the attributes hash
    ATTRIBUTES: {
        SAVED_SEARCH : 'savedSearch',
        TYPE         : 'type',
        AGENT_BRAND  : 'agentBrand',
        CUSTOM       : 'custom',
        RETURN_PATH  : 'returnPath',
        COOKIE_DOMAIN: 'cookieDomain',
        RANDOM_COOKIE: 'randomCookie'
    },

    // URL parameter names
    PARAMETERS: {
        SAVED_SEARCH: 'ss',
        TYPE        : 't',
        AGENT_BRAND : 'ag',
        CUSTOM      : 'cu',
        RETURN_PATH : 'ret'
    },

    //method [static] instance
    //return [RUI.Globals] singleton instance
    //eg     var globals = RUI.Globals.instance();
    instance: function () {
        if (!RUI.Globals.singleton) {
            RUI.Globals.singleton = new RUI.Globals();
        }

        return RUI.Globals.singleton;
    },

    singleton: null
});

RUI.Globals.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        this.attributes = RUI.$H();
    },

    //method setAttribute
    //desc   Sets a named, global attribute to be later accessed by
    //       getAttribute.
    //param  [req] name [String]
    //param  [req] value [String]
    setAttribute: function (name, value) {
        this.attributes[name] = value;
    },

    //method getAttribute
    //desc   Returns a named, global attribute.
    //param  [req] name [String]
    //return [*] attribute value
    getAttribute: function (name) {
        return this.attributes[name];
    }
};

//desc   A URL parser inspired by Poly9
//       (https://code.poly9.com/trac/wiki/URLParser).
RUI.URLParser = RUI.Class.create();

RUI.Object.extend(RUI.URLParser, {
    FIELDS: {
        PROTOCOL   : 'Protocol',
        USERNAME   : 'Username',
        PASSWORD   : 'Password',
        HOST       : 'Host',
        PATHNAME   : 'Pathname',
        QUERYSTRING: 'Querystring',
        ANCHOR     : 'Anchor'
    }
});

RUI.URLParser.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] url [String] URL to be parsed
    initialize: function (url) {
        this.values = RUI.$H();

        // Create the getters and initialise the values
        RUI.$H(RUI.URLParser.FIELDS).values().each(function (field) {
            this.values[field] = '';
            var getterName = 'get' + field;
            if (typeof(this[getterName]) !== 'function') {
                this[getterName] = function () {
                    return this.values[field];
                };
            }
        }.bind(this));

        if (url) {
            this.parse(url);
        }
    },

    //method parse
    //desc   Parses a URL into it's constituent parts.
    //param  [req] url [String] URL to parse
    parse: function (url) {
        var regexPosition = RUI.$H();
        regexPosition[RUI.URLParser.FIELDS.PROTOCOL] = 2;
        regexPosition[RUI.URLParser.FIELDS.USERNAME] = 4;
        regexPosition[RUI.URLParser.FIELDS.PASSWORD] = 5;
        regexPosition[RUI.URLParser.FIELDS.HOST] = 6;
        regexPosition[RUI.URLParser.FIELDS.PORT] = 7;
        regexPosition[RUI.URLParser.FIELDS.PATHNAME] = 8;
        regexPosition[RUI.URLParser.FIELDS.QUERYSTRING] = 9;
        regexPosition[RUI.URLParser.FIELDS.ANCHOR] = 10;

        var protocol    = '((\\w+):\\/\\/)?'; // Double-escape regex as string
        var userPass    = '((\\w+):?(\\w+)?@)?';
        var host        = '([^\\/\\?:]+)';
        var port        = ':?(\\d+)?';
        var pathname    = '(\\/?[^?#]+)?';
        var querystring = '\\??([^#]+)?';
        var anchor      = '#?(\\w*)';

        var urlRegex = new RegExp(
            '^' + protocol + userPass + host + port + pathname + querystring
            + anchor + '$'
        );
        var urlMatch = urlRegex.exec(url);
        if (!urlMatch) {
            throw "Invalid URL";
        }

        regexPosition.keys().each(function (field) {
            if (urlMatch[regexPosition[field]]) {
                this.values[field] = urlMatch[regexPosition[field]];
            }
        }.bind(this));
    },

    //method getURL
    //desc   Returns the URL, including any modifications made subsequent to
    //       instantiating the object.
    //return [String] URL
    getURL: function () {
        var url = '';
        var protocol    = this.values[RUI.URLParser.FIELDS.PROTOCOL];
        var username    = this.values[RUI.URLParser.FIELDS.USERNAME];
        var password    = this.values[RUI.URLParser.FIELDS.PASSWORD];
        var host        = this.values[RUI.URLParser.FIELDS.HOST];
        var port        = this.values[RUI.URLParser.FIELDS.PORT];
        var pathname    = this.values[RUI.URLParser.FIELDS.PATHNAME];
        var querystring = this.values[RUI.URLParser.FIELDS.QUERYSTRING];
        var anchor      = this.values[RUI.URLParser.FIELDS.ANCHOR];

        if (protocol) {
            url += protocol + '://';
        }

        if (username) {
            url += username;

            if (password) {
                url += ':' + password;
            }

            url += '@';
        }

        if (host) {
            url += host;
        }

        if (port) {
            url += ':' + port;
        }

        if (pathname) {
            url += pathname;
        }

        if (querystring) {
            url += '?' + querystring;
        }

        if (anchor) {
            url += '#' + anchor;
        }

        return url;
    },

    //method setParameter
    //desc   Adds or replaces a parameter name/value pair to the querystring.
    //param  [req] name [String]
    //param  [req] value [String]
    setParameter: function (name, value) {
        var querystring = this.values[RUI.URLParser.FIELDS.QUERYSTRING];
        var querystringRegex = new RegExp('(^|&(?:amp;)?)' + name
            + '=[^&]*');

        // Check if we already have this parameter in the querystring and
        // replace or append as required
        var querystringMatch = querystringRegex.exec(querystring);
        if (querystringMatch) {
            this.values[RUI.URLParser.FIELDS.QUERYSTRING] = querystring.replace(
                querystringRegex, '$1' + name + '=' + value
            );
        }
        else {
            this.values[RUI.URLParser.FIELDS.QUERYSTRING] += '&' + name
            + '=' + value;
        }
    }
};

//desc   An HTML form validator.
RUI.FormValidator = RUI.Class.create();

RUI.Object.extend(RUI.FormValidator, {
    ERROR_CLASS: 'validationError', // Style class for fields
    ERRORS_CLASS: 'validationErrors' // Style class for the errors <div>
});

RUI.FormValidator.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] form [String] or [HTMLElement] form ID or object
    //param  [req] validatedFields [Array] of RUI.ValidatedField
    //param  [opt] onSubmit: [Boolean] perform validation on form submit
    initialize: function (form, validatedFields, options) {
        this.form = RUI.$(form);
        this.errors = [];
        // Array of RUI.ValidatedField
        this.validatedFields = RUI.$A(validatedFields);
        this._setOptions(options);

        // Validate the form when submitted
        if (this.options.onSubmit) {
            form.observe('submit', function (e) {
                this.validateOnEvent(e);
            }.bind(this), false);
        }
    },

    //method [private] _setOptions
    //desc   Sets default options and assigns named constructor arguments to
    //       this.options.
    //param  [opt] options [Object] See initialize method for details.
    _setOptions: function (options) {
        this.options = {
            onSubmit: true
        };

        RUI.Object.extend(this.options, options || {});
    },

    //method validateOnEvent
    //desc   Performs validation and prevents propagation of the event argument
    //       if validation fails.
    //param  [req] event [Event]
    //return [Boolean] validates
    validateOnEvent: function (e) {
        var validates = this.validate();
        if (!validates) {
            this.renderErrors();
            RUI.Event.stop(e);
        }

        return validates;
    },

    //method validate
    //desc   Populates errors and returns false if any errors exist.
    //return [Boolean] validates
    validate: function () {
        this.hasValidationErrors = false;
        this.errors = [];
        this.validatedFields.each(function (field) {
            if (!field.validate()) {
                RUI.$A(field.getErrors()).each(function (error) {
                    this.errors.push(error);
                }.bind(this));
                this.hasValidationErrors = true;
            }
        }.bind(this));

        return !this.hasValidationErrors;
    },


    //method addValidatedField
    //desc   Adds the required field to validate into the validatedFields array.
    //param  [req] field [String] or [HTMLElement] form ID.
    addValidatedField: function (field) {
        //check if field is not null and if the field is not
        //in array then add it
        if (this.validatedFields.indexOf(field) === -1) {
            this.validatedFields.push(field);
        }
    },

    //method removeValidatedField
    //desc   Removes the required field from the list of validatedFields array.
    //param  [req] field [String] or [HTMLElement] form ID.
    removeValidatedField: function (field) {
        //check if field is not null and if the field is in array then remove it
        //TODO: See if this can be refactored with Enumerable
        var index = this.validatedFields.indexOf(field);
        if (index !== -1) {
            this.validatedFields.splice(index, 1);
        }
    },

    //method hasErrors
    //desc   Returns true if the form failed to validate.
    //return [Boolean]
    hasErrors: function () {
        return this.hasValidationErrors;
    },

    //method renderErrors
    //desc   Creates the HTML required to display errors and inserts it into the
    //       DOM.
    renderErrors: function () {
        // Clear any existing errors
        if (this.errorsDiv) {
            RUI.Element.remove(this.errorsDiv);
        }

        // Clear any validation error class names on fields
        RUI.$A(this.form.getElementsByClassName(
            RUI.FormValidator.ERROR_CLASS
        )).each(function (node) {
            node.removeClassName(RUI.FormValidator.ERROR_CLASS);
        });

        // Create a <ul> to hold the error messages
        var ul = document.createElement('ul');

        this.errors.each(function (error) {
            var field = error.getField();
            var message = error.getMessage();

            // Create an <li> for the error message and add it to our <ul>
            var li = document.createElement('li');
            li.appendChild(document.createTextNode(message));
            ul.appendChild(li);

            // Give the guilty inputs an extra style class
            field.addClassName(RUI.FormValidator.ERROR_CLASS);
        });

        // Create a wrapper <div> for the <ul>
        this.errorsDiv = document.createElement('div');
        this.errorsDiv.className = RUI.FormValidator.ERRORS_CLASS;
        this.errorsDiv.appendChild(ul);

        // Attatch the <div> to the top of the form
        this.form.insertBefore(this.errorsDiv, this.form.firstChild);

        var highlightErrors = function () {
            RUI.Effect.highlight(this.errorsDiv);
        }.bind(this);

        // Scroll to errors div if not within the visible area
        if (!RUI.Effect.scrollTo(this.errorsDiv, {
            scrollIfVisible: false,
            afterFinish: highlightErrors
        })) {
            highlightErrors();
        }

    }
};

//desc   A class used by RUI.FormValidator to represent a validated field.
RUI.ValidatedField = RUI.Class.create();

RUI.ValidatedField.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] field [String] or [HTMLElement] field ID or object
    //param  [req] validationRules [Array] of RUI.ValidationRule
    initialize: function (field, validationRules) {
        this.field = RUI.$(field);
        this.labelText = this._matchLabel();
        // Array of RUI.ValidationRule
        this.validationRules = RUI.$A(validationRules);
    },

    //method validate
    //desc   Populates errors and returns false if any errors exist.
    //return [Boolean] validates
    validate: function () {
        var hasValidationErrors = false;
        this.errors = [];
        this.validationRules.each(function (rule) {
            if (!rule.validate(this.field.value)) {
                hasValidationErrors = true;
                this.errors.push(
                    (new RUI.ValidationError(
                        this.field,
                        this.labelText,
                        rule.getErrorMessage()
                    ))
                );
            }
        }.bind(this));

        return !hasValidationErrors;
    },

    //method getErrors
    //return [Array] array of RUI.ValidationError
    getErrors: function () {
        return this.errors;
    },

    //method [private] _matchLabel
    //desc   Gets the label text by looking at the 'for' attributes of the
    //       labels in the form and then trying to match with the 'id' attribute
    //       of this field.
    //return [String] label text
    _matchLabel: function () {
        var labelText = '';

        // Can't do much if there is no parentNode
        if (!this.field || !this.field.parentNode) {
            return labelText;
        }

        var labels = RUI.$A(
            this.field.parentNode.getElementsByTagName('label')
        );
        labels.each(function (label) {
            // getAttribute() doesn't work here in IE
            var forAttribute = label.attributes['for'];
            if (forAttribute && (forAttribute.value === this.field.id)) {
                labelText = label.innerHTML;
                return; // Break the each loop (not the _matchLabel function)
            }
        }.bind(this));

        // Clean the label up by stripping any trailing colon plus anything that
        // appears after the colon (an asterisk, for example)
        labelText = labelText.replace(/:.*$/, '');

        // Strip any leading 'Your ' ('Your Message', for example)
        labelText = labelText.replace(/^Your /, '');

        return labelText;
    }
};

//desc   Represents a validation error.
RUI.ValidationError = RUI.Class.create();

RUI.ValidationError.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] field [String] ID of field associated with error
    //param  [req] label [String] field label text
    //param  [req] message [String] error message template (see Prototype's
    //       Template class)
    initialize: function (field, label, message) {
        this.field = field;
        this.labelText = label;
        this.message = this._parseMessage(message);
    },

    //method getField
    //return [RUI.ValidatedField] field relating to the error
    getField: function () {
        return this.field;
    },

    //method getMessage
    //return [String] message relating to the error
    getMessage: function () {
        return this.message;
    },

    //method [private] _parseMessage
    //desc   Inserts the label into the error message.
    //param  [req] message [String] message template (see Prototype's Template
    //       class)
    //return [String] parsed message
    _parseMessage: function (message) {
        var messageTemplate = new RUI.Template(message);
        var label = { label: this.labelText.split(':')[0] };
        return messageTemplate.evaluate(label);
    }
};

//desc   Represents a validation rule
RUI.ValidationRule = RUI.Class.create();

RUI.ValidationRule.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] errorMessage: [String] error message template
    //param  [req] dependencies: [Array] of RUI.ValidationRule
    //param  [req] test: [function] A function that takes a single argument, the
    //       value to be tested, and returns true when validation passes.
    initialize: function (validationType) {
        this.errorMessage = validationType.errorMessage;
        this.dependencies = validationType.dependencies;
        this.test = validationType.test;
    },

    //method validate
    //desc   Performs the validations, including dependencies.
    //param  [req] value [String]
    //return [Boolean] validates
    validate: function (v) {
        var validates = true;
        if (this.dependencies) {
            this.dependencies.each(function (dependency) {
                // Dependencies are declared by their constructor constant
                dependency = new RUI.ValidationRule(dependency);
                if (!dependency.validate(v)) {
                    validates = false;
                }
            });
        }

        if (!this.test(v)) {
            validates = false;
        }

        return validates;
    },

    //method getErrorMessage
    //return [String] error message
    getErrorMessage: function () {
        return this.errorMessage;
    }
};

// Valid constructors
RUI.Object.extend(RUI.ValidationRule, {
    REQUIRED: {
        errorMessage: '#{label} is a required field',
        dependencies: [],
        test: function (v) {
            return  (v !== null && v.length > 0);
        }
    }
});

// New extend required here for the 'dependencies' reference
RUI.Object.extend(RUI.ValidationRule, {
    EMAIL: {
        errorMessage: 'Please enter a valid email address',
        dependencies: [RUI.ValidationRule.REQUIRED],
        test: function (v) {
            return (/^.+\@.+\..+$/.test(v));
        }
    }
});

//desc   Animated effects scrollTo and highlight effects based on
//       Scriptaculous functions.
RUI.Effect = {
    //method [static] scrollTo
    //desc   Scrolls to an element.
    //param  [req] element [String] or [HTMLElement] element to scroll to
    //param  [opt] options [Object] Scriptaculous options
    //param  [opt] scrollIfVisible: [Boolean] false causes page not to scroll
    //       if the element is visible (defaults to true)
    //return [Boolean] true if the scroll triggered
    //note   Named arguments can be optionally passed into the Scriptaculous
    //       options
    scrollTo: function (element, options, namedArguments) {
        var defaults = {
            duration       : 0.2,
            scrollIfVisible: true
        };
        options = options || {};
        options = RUI.Object.extend(options, namedArguments);
        options = RUI.Object.extend(defaults, options);
        var realOffset = RUI.Position.realOffset(element)[1];
        var cumulativeOffset = RUI.Position.cumulativeOffset(element)[1];
        if (options.scrollIfVisible || realOffset > cumulativeOffset) {
            return true;
        }

        return false;
    },

    //method [static] highlight
    //desc   Highlights an element.
    //param  [req] element [String] or [HTMLElement] element to highlight
    //param  [opt] duration: [Number] duration of highlight in seconds
    highlight: function (element, options) {
        var defaults = {
            duration      : 1.0,
            startcolor    : '#ffd517'
        };
        options = RUI.Object.extend(defaults, options);
        $J(element).hide().fadeIn();
    }
};

//desc   A form field containing a default value that clears itself when
//       focused.
RUI.ClearingField = RUI.Class.create();

RUI.Object.extend(RUI.ClearingField, {
    DEFAULT_CLASS: 'default' // Class name applied to input when the default
                             // value is populated
});

RUI.ClearingField.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] input [String] or [HTMLElement] input field ID or object
    //param  [req] defaultValue [String] default text
    initialize: function (input, defaultValue) {
        input = RUI.$(input);

        input.value = defaultValue;
        input.addClassName(RUI.ClearingField.DEFAULT_CLASS);

        // Clear the input onfocus if it contains the default value
        RUI.Event.observe(input, 'focus', function () {
            if (input.value === defaultValue) {
                Field.clear(input);
            }
            input.removeClassName(RUI.ClearingField.DEFAULT_CLASS);
        }, false);

        // Put the default value back in if the field is empty onblur
        RUI.Event.observe(input, 'blur', function () {
            if (!input.value) {
                input.value = defaultValue;
            }
            input.addClassName(RUI.ClearingField.DEFAULT_CLASS);
        }, false);
    }
};


//desc   Represents a consumer user of the site.
RUI.RCA.User = RUI.Class.create();

RUI.Object.extend(RUI.RCA.User, {
    SUBSCRIBER_LOGOUT_CLASS: 'a.logoutLink',

    EMAIL_COOKIE_NAME: 'EmailAddress',
    ALERT_COOKIE_NAME: 'AlertEmailAddress',

    LOGIN_PAGE_URL: '/cgi-bin/rsearch?a=sub&static=1',

    //method [static] logOut
    //desc   Logs the user out.
    //param  [opt] event [Event] event to prevent propagation of
    logOut: function (event) {
        var newCookie = RUI.Globals.instance().getAttribute(
            RUI.Globals.ATTRIBUTES.RANDOM_COOKIE
        );

        RUI.Cookie.set(RUI.RCA.User.EMAIL_COOKIE_NAME, newCookie, {
            path    : RUI.Cookie.DEFAULTS.path,
            domain  : RUI.Globals.instance().getAttribute(
                          RUI.Globals.ATTRIBUTES.COOKIE_DOMAIN
            )
        });

        RUI.Cookie.remove(RUI.RCA.User.ALERT_COOKIE_NAME,
                        RUI.Cookie.DEFAULTS.path,
                        RUI.Globals.instance().getAttribute(
                            RUI.Globals.ATTRIBUTES.COOKIE_DOMAIN
                        ));

        // TODO: Refactor to use RUI.URLParser??
        var globals = RUI.Globals.instance();
        document.location = RUI.RCA.User.LOGIN_PAGE_URL
            + '&' + RUI.Globals.PARAMETERS.SAVED_SEARCH + '='
            + globals.getAttribute(RUI.Globals.ATTRIBUTES.SAVED_SEARCH)
            + '&' + RUI.Globals.PARAMETERS.TYPE + '='
            + globals.getAttribute(RUI.Globals.ATTRIBUTES.TYPE)
            + '&' + RUI.Globals.PARAMETERS.AGENT_BRAND + '='
            + globals.getAttribute(RUI.Globals.ATTRIBUTES.AGENT_BRAND)
            + '&' + RUI.Globals.PARAMETERS.CUSTOM + '='
            + globals.getAttribute(RUI.Globals.ATTRIBUTES.CUSTOM)
            + '&' + RUI.Globals.PARAMETERS.RETURN_PATH + '='
            + globals.getAttribute(RUI.Globals.ATTRIBUTES.RETURN_PATH);

        // Stop the screen going to the original link if called from an event
        if (event) {
            RUI.Event.stop(event);
        }
    }
});

RUI.RCA.User.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {}
};

//desc   Controls the Group Navigation dropdown menu.
//note   This is self-contained and must not rely on external
//       code so that it can be distributed to all REA Group sites
//       without any dependencies.
RUI.GroupNavigation = function () {
    // Id's of the html elements used in the dropdown
    var ID = {
        CONTAINER: 'groupNavBgCurve',   // CHANGE THIS?
        GROUP_NAV: 'groupNav',          // Group Nav
        DROPDOWN : 'groupNavCountries', // Dropdown menu UD
        TRIGGER  : 'groupNavTrigger'    // Trigger anchor ID
    };

    // Classes used to display the flippy triangle on the dropdown heading
    var CLASS = {
        EXPANDED  : 'expanded',           // Expanding navigation
        CONTRACTED: 'contracted'          // Contract navigation
    };

    // Arbitrarily large base value for setting z-indices on the shim,
    // the dropdown and the containing element for both
    var Z_INDEX = {
        BASE_VALUE: 999
    };

    // Elements required by the createShim function
    var container = RUI.$(ID.CONTAINER);
    var trigger   = RUI.$(ID.TRIGGER);
    var groupnav  = RUI.$(ID.GROUP_NAV);
    var dropdown  = RUI.$(ID.DROPDOWN);

    var oldOnclick;

    // Check to make sure we've got all the elements we need,
    // in case the group navigation is removed from specific pages
    if (
        !container ||
        !trigger   ||
        !groupnav  ||
        !dropdown
    ) {
        return false;
    }

    //method [static] createShim
    //desc   Creates an invisible iframe to handle the shim effect in IE. This
    //       is used to cover Select Box, ActiveX object, Iframes etc.
    //param  [HTMLElement] element.
    //note   Only create shim if the browser is IE Version >= 5.5 && < 7.
    //todo   Refactor this function to be usable by any DOM element, and to
    //       work around the current requirement to place that element at the
    //       end of the page's source code in order to work around the
    //       stacking order problem of the windowed controls it's attempting
    //       to overlay.
    //       Cf: http://weblogs.asp.net/bleroy/archive/2005/08/09/
    //           how-to-put-a-div-over-a-select-in-ie.aspx
    function createShim() {
        // Browser version for applying proprietary style filter properties
        var ieVersion = parseFloat(navigator.appVersion.split('MSIE')[1]);

        // To prevent windowed controls from appearing above the dropdown list,
        // create an iframe with the same dimensions and location, and place it
        // lower in the browser's stacking context
        //
        // TODO: Refactor to inspect element attributes via RUI.Element methods
        var shim            = document.createElement('iframe');
        shim.scrolling      = 'no';        // no scrolling
        shim.frameBorder    = '0';         // no frame border on iframe
        shim.style.position = 'absolute';  // position iframe absolutely
        shim.style.top      = dropdown.offsetTop + 'px';
        shim.style.width    = dropdown.offsetWidth + 'px';
        shim.style.height   = dropdown.offsetHeight + 'px';
        shim.style.display  = 'block';

        // Apply a css filter for IE to allow transparency in the dropdown
        // (not used in this instance, but likely to be required in future)
        if (ieVersion >= 5.5 && ieVersion < 7) {
            shim.style.filter =
            'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0,' +
            'sizingMethod="crop")';
        }

        // Set the z-indices of the relevant elements so they stack correctly
        groupnav.style.zIndex = Z_INDEX.BASE_VALUE;
        dropdown.style.zIndex = Z_INDEX.BASE_VALUE;
        shim.style.zIndex     = Z_INDEX.BASE_VALUE - 1;

        // Insert the iframe shim into the DOM
        container.appendChild(shim);
    }

    // method [static] removeShim.
    // desc   Remove the created iframe, the first iframe attached to the
    //        element.
    //param  [HTMLElement] element.
    function removeShim(element) {
        var iframe;

        if (element && element.getElementsByTagName('iframe')[0]) {
            iframe = element.getElementsByTagName('iframe')[0];
            element.removeChild(iframe);
        }
    }

    // Removes the display style attribute so that the dropdown is shown
    function show() {
        dropdown.style.display = '';
        trigger.className = CLASS.CONTRACTED;
        createShim();
    }

    // Sets the display style attribute to 'none' so that the dropdown is
    // hidden
    function hide() {
        if (visible()) {
            dropdown.style.display = 'none';
            trigger.className = '';
            removeShim(container);
        }
    }

    // Toggles the style display attribute of the dropdown
    function toggle() {
        if (visible()) {
            hide();
        }
        else {
            show();
        }
    }

    // Returns true if the dropdown is visible
    function visible() {
        return !(dropdown.style.display === 'none');
    }

    // document onclick handler for hiding the dropdown
    function documentOnclick(e) {
        // Hide if dropdown is visible and the click occured anywhere
        // except on the trigger.
        var target = window.event ? window.event.srcElement : e.target;
        if (visible() && target !== trigger) {
            hide();
        }
    }

    // Toggle display of the dropdown onclick
    trigger.onclick = function () {
        toggle();
        return false;
    };

    // Add the document onclick handler
    if (document.onclick) {
        oldOnclick = document.onclick; // Be nice to others
        document.onclick = function (e) {
            oldOnclick(e);
            documentOnclick(e);
        };
    }
    else {
        document.onclick = documentOnclick;
    }
};

// TODO: Refactor from a class to a function
//desc   A class to represent the My Tools box.
RUI.RCA.MyTools = RUI.Class.create();

RUI.Object.extend(RUI.RCA.MyTools, {
    // TODO: Wrap these in a single object
    APPEAR_DURATION: 0.3,
    FADE_DURATION  : 1.5,
    LOCK_DURATION  : 0.5,

    // TODO: Wrap these in a single object
    WRAPPER_ID           : 'mm_tools',
    TRIGGER_ID           : 'myToolsTrigger',
    CONTENT_ID           : 'myToolsContent',
    LOGIN_FORM_ID        : 'loginForm',
    EMAIL_FIELD_ID       : 'emailAddress',
    PASSWORD_FIELD_ID    : 'password',
    FORGOTTEN_PASSWORD_ID: 'forgottenPassword',

    OVERLAP_BUG_CLASS    : 'overlapBug',
    OVERLAP_SORT_SELECT  : 'o'
});

RUI.RCA.MyTools.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        // Add an onclick handler to the My Tools anchor for the dropdown
        var anchor  = RUI.$(RUI.RCA.MyTools.TRIGGER_ID);
        var content = RUI.$(RUI.RCA.MyTools.CONTENT_ID);
        var wrapper = RUI.$(RUI.RCA.MyTools.WRAPPER_ID);

        var emailField, passwordField, passwordAnchor, passwordParser,
            forgottenPasswordValidator;

        // Exit if we don't actually have the My Tools DOM elements
        if (!anchor || !content || !wrapper) {
            return false;
        }

        // Make sure FF doesn't put a border around the anchor if it is clicked
        anchor.style.outline = 'none';

        // Hide the menu by default
        RUI.Element.hide(content);

        // Add the mouse event observers
        wrapper.observe('mouseover', this.show.bind(this), false);
        content.observe('mouseover', function () {
            this.lock();
            this.show();
        }.bind(this), false);
        wrapper.observe('mouseout', this.hide.bind(this), false);
        content.observe('mouseout', function () {
            this.unlock();
        }.bind(this), false);
        anchor.observe('click', function (e) {
            this.toggle();
            RUI.Event.stop(e);
        }.bind(this), false);

        var loginForm = RUI.$(RUI.RCA.MyTools.LOGIN_FORM_ID);
        if (loginForm) {
            // Login form validation
            emailField = new RUI.ValidatedField(
                RUI.RCA.MyTools.EMAIL_FIELD_ID,
                [(new RUI.ValidationRule(RUI.ValidationRule.EMAIL))]
            );
            passwordField = new RUI.ValidatedField(
                RUI.RCA.MyTools.PASSWORD_FIELD_ID,
                [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
            );

            // Create the validator, which will attach itself the the submit
            // event of the form.
            (new RUI.FormValidator(loginForm, [emailField, passwordField]));

            // Forgotten password link
            passwordAnchor = RUI.$(RUI.RCA.MyTools.FORGOTTEN_PASSWORD_ID);
            passwordParser = new RUI.URLParser(passwordAnchor.href);
            forgottenPasswordValidator = new RUI.FormValidator(
                loginForm, [emailField], { onSubmit: false }
            );
            passwordAnchor.observe('click', function (e) {
                passwordParser.setParameter('email', loginForm.email.value);
                passwordAnchor.href = passwordParser.getURL();
                if (forgottenPasswordValidator.validateOnEvent(e)) {
                    window.open(
                        passwordAnchor.href,
                        RUI.RCA.MyTools.FORGOTTEN_PASSWORD_ID,
                        'width=300,height=120'
                    );
                }
                RUI.Event.stop(e);
            }, false);
        }

        // determine what elements will need to be hidden to prevent overlap
        // in browsers <= IE6
        var myShortlistSortSelect = $(RUI.RCA.MyTools.OVERLAP_SORT_SELECT);
        if (!document.body.style.maxHeight) {
            this.overlappingElements = document.getElementsByClassName(
                RUI.RCA.MyTools.OVERLAP_BUG_CLASS
            );
            // On the My Shortlist page, this element overlaps
            // TODO: refactor this to use the RUI.DOM.Shim code
            if (myShortlistSortSelect) {
                this.overlappingElements.push(myShortlistSortSelect);
            }
        }

        return true;
    },

    //method lock
    //desc   Locks the menu so that calls to hide() have no effect.
    lock: function () {
        this.locked = true;
    },

    //method unlock
    //desc   Unlocks the menu so that hide() is able to hide the menu.
    unlock: function () {
        this.locked = false;
    },

    //method show
    //desc   Shows the menu.
    show: function () {
        if (this.timeout) {
            clearTimeout(this.timeout);
        }

        // Cancel any current fade to prevent flickering
        if (this.fadeEffect) {
            this.fadeEffect.cancel();
        }

        $J('#' + RUI.RCA.MyTools.CONTENT_ID).fadeIn('slow');

        // prevent some elements from appearing over the menu
        if (this.overlappingElements) {
            this.overlappingElements.each(function (element) {
                element.style.visibility = 'hidden';
            });
        }

    },

    //method hide
    //desc   Hides the menu.
    hide: function () {
        this.timeout = setTimeout(function () {
            if (!this.locked) {
                // Cancel any current appear effect to prevent flickering
                if (this.appearEffect) {
                    this.appearEffect.cancel();
                }

                $J('#' + RUI.RCA.MyTools.CONTENT_ID).fadeOut('slow');

                // Restore select boxes hidden from IE6
                this.overlapTimeout = setTimeout(function () {
                    clearTimeout(this.overlapTimeout);
                    if (this.overlappingElements) {
                        this.overlappingElements.each(function (element) {
                            element.style.visibility = 'visible';
                        });
                    }
                }.bind(this), (RUI.RCA.MyTools.FADE_DURATION * 1000));
            }
        }.bind(this), parseInt(RUI.RCA.MyTools.LOCK_DURATION * 1000, 10));
    },

    //method toggle
    //desc   Toggles the display of the menu.
    toggle: function () {
        $J('#' + RUI.RCA.MyTools.CONTENT_ID).toggle();
    }
};

//desc   Singleton class for creating adverts.
//eg     var adFactory = RUI.RCA.AdvertFactory.instance();
RUI.RCA.AdvertFactory = RUI.Class.create();

RUI.Object.extend(RUI.RCA.AdvertFactory, {
    //method [static] instance
    //return [RUI.RCA.AdvertFactory] singleton instance
    instance: function () {
        if (!RUI.RCA.AdvertFactory.singleton) {
            RUI.RCA.AdvertFactory.singleton = new RUI.RCA.AdvertFactory();
        }

        return RUI.RCA.AdvertFactory.singleton;
    },

    singleton: null
});

RUI.RCA.AdvertFactory.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        this.pageId = Math.round(Math.random() * 10000000000);
        this.adverts = [];
    },

    //method renderAdverts
    //desc   Loops over each advert registered with the factory and calls its
    //       render method.
    renderAdverts: function () {
        this.adverts.each(function (advert) {
            advert.render();
        });
    },

    //method register
    //desc   Registers an advert with the factory.
    register: function (advert) {
        this.adverts.push(advert);
    },

    //method getPageId
    //return [Number] A unique, randomly-generated, integer page ID.
    getPageId: function () {
        return this.pageId;
    }
};

//desc   Base class for adverts.
RUI.RCA.Advert = RUI.Class.create();

RUI.Object.extend(RUI.RCA.Advert, {
    AD_SERVER: 'http://info.realestate.com.au'
});

RUI.RCA.Advert.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] tag [String] Accipiter tag
    //param  [req] width [Number] or [String] integer width in pixels
    //param  [req] height [Number] or [String] integer height in pixels
    //param  [req] container [String] or [Object] container ID or object
    //param  [opt] id [String] advert ID
    initialize: function (tag, width, height, container, id) {
        this.tag = tag;
        this.width = parseInt(width, 10);
        this.height = parseInt(height, 10);
        this.container = RUI.$(container);
        this.id = id;
    },

    //method render
    //desc   Creates the HTML required to display the advert and inserts it
    //       into the DOM.
    render: function () {
        // Get the singleton instance for adverts that contains global
        // properties such as pageId
        var factory = RUI.RCA.AdvertFactory.instance();
        var pageId = factory.getPageId();
        var target   = '/' + this.tag;
        var random = Math.round(Math.random() * 10000000000);
        var name = id;
        var iframeSrc = RUI.RCA.Advert.AD_SERVER + '/hserver/acc_random='
            + random + target + '/pageid=' + pageId
            + (this.id ? '&thisFrameId=' + this.id : '');
        var scriptSrc = RUI.RCA.Advert.AD_SERVER + '/jnserver/acc_random='
            + random + target + '/pageid=' + pageId;

        // Use our compatibility layer to handle the name attribute
        var iframe = RUI.createElement('iframe', name);
        iframe.id = id;
        iframe.style.width = this.width + 'px';
        iframe.style.height = this.height + 'px';
        iframe.style.background = '#fff';
        // Attributes are case-sensitive
        iframe.setAttribute('src', iframeSrc);
        iframe.setAttribute('scrolling', 'no');
        iframe.setAttribute('frameBorder', '0');
        iframe.setAttribute('allowTransparency', 'true');
        iframe.setAttribute('marginWidth', '0');
        iframe.setAttribute('marginHeight', '0');
        iframe.setAttribute('vspace', '0');
        iframe.setAttribute('hspace', '0');
        iframe.setAttribute('noresize', 'noresize');

        // No DOM node creation here due to JS security
        iframe.text = '<scr' + 'ipt src="'
            + scriptSrc + '" type="text/javascript"></scr' + 'ipt>';

        this.container.appendChild(iframe);
    }
};

//desc   A simple slideshow.
RUI.Slideshow = RUI.Class.create();

RUI.Slideshow.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] container [String] or [HTMLElement] container ID or object
    //param  [req] imageSources [Array] of String image source URLs
    //param  [opt] thumbnails: [Array] of String IDs or HTMLElement objects
    //param  [opt] autoplay:   [Boolean] start playing automatically
    //param  [opt] preload:    [Boolean] preload images before playing
    //param  [opt] preloadNext:[Boolean] preload next image
    //param  [opt] interval:   [Number] time in seconds between transitions
    //param  [opt] duration:   [Number] time in fading, if 0, then no fade
    //param  [opt] delay:      [Number] delay in seconds before starting play
    //param  [opt] indicator:  [String] or [HTMLElement] loading ID or element
    //param  [opt] previous: [String] or [HTMLElement] previous button ID or
    //       element
    //param  [opt] next: [String] or [HTMLElement] next button ID or element
    //param  [opt] play: [String] or [HTMLElement] play button ID or element
    //param  [opt] stop: [String] or [HTMLElement] stop button ID or element
    //param  [opt] pause: [String] or [HTMLElement] pause button ID or element
    //param  [opt] callback: [function] onChange callback function
    //todo   Fix the 'flickering' bug that appears when two images with the same
    //       source are passed into the imageSources array.  It's not a likely
    //       scenario but does cause problems.
    initialize: function (container, imageSources, options) {
        this.container = RUI.$(container);
        this.imageSources = RUI.$A(imageSources);
        this._setOptions(options);
        this.position = 0; // Current index in imageSources
        this.images = RUI.$H(); // Hash of image elements
        this.stalled = false;
        this.playing = false;
        this.setPositionOnImageLoad = false;

        var firstImage =
            RUI.$A(this.container.getElementsByTagName('img')).first();
        if (firstImage) {
            // absolutize first image
            RUI.$(firstImage).setStyle({ position: 'absolute' });
        }

        if (this.imageSources.toArray().length <= 1) {
            // No need to do anything else if we only have one image
            return;
        }

        if (this.options.preload) {
            imageSources.each(function (source) {
                this.loadImage(source);
            }.bind(this));
        }

        if (this.options.preloadNext) {
            this.loadNextImage();
        }

        if (this.options.autoplay) {
            this.play();
        }

        if (!this.options.notRelative) {
            this.container.makePositioned(); // position: relative
        }

        if (this.options.thumbnails) {
            this.options.thumbnails.each(function (thumbnail, index) {
                thumbnail = RUI.$(thumbnail);
                thumbnail.observe('click', function (e) {
                    if (index !== this.position) {
                        // need to set to an image position now
                        this.setPositionOnImageLoad = true;
                        this.pause();
                        this._setPosition(index);
                    }
                    RUI.Event.stop(e);
                }.bind(this));
            }.bind(this));
        }

        ['previous', 'next', 'play', 'stop', 'pause'].each(function (command) {
            var button = RUI.$(this.options[command]);
            if (button) {
                button.observe('click', function (e) {
                    if (command === 'previous' || command === 'next') {
                        this.setPositionOnImageLoad = true;
                    }
                    // call the corresponding function for the button clicked
                    this[command].call(this);
                    RUI.Event.stop(e);
                }.bind(this));
            }
        }.bind(this));
    },

    //method loadNextImage
    //desc   Loads the next image in the imagesource array.
    loadNextImage: function () {
        var sources = this.imageSources.toArray();
        var position = this._getNormalizedPosition(this.position + 1);
        var source = sources[position];
        this.loadImage(sources);
    },

    //method getCurrentImage
    //desc   Returns the image object at the current position.
    //return [Image] current image
    getCurrentImage: function () {
        return this.loadImage(this.imageSources[this.position]);
    },

    //method getCurrentImageElement
    //desc   Returns the image HTML element at the current position.
    //return [HTMLElement] current image
    getCurrentImageElement: function () {
        return RUI.$(RUI.$A(this.container.getElementsByTagName('img')).last());
    },

    //method loadImage
    //desc   Caches the image in the browser if it hasn't been cached already.
    //param  [req] source [String] image source URL
    //return [Image] loaded image
    loadImage: function (source) {
        if (this.images[source]) {
            return this.images[source];
        }

        var image = new Image();

        RUI.Element.extend(image); // Add Prototype's methods

        image.observe('load', function () {
            this.images[source] = image;
            if (this.stalled) {
                this.stalled = false;
                // decide whether to set an image position
                // or play slide show
                if (this.setPositionOnImageLoad) {
                    this.setPositionOnImageLoad = false;
                    this._setPosition(this.position);
                }
                else if (this.playing) {
                    this.play();
                }
            }
            var indicator = RUI.$(this.options.indicator);
            if (indicator) {
                indicator.hide();
            }
        }.bind(this));
        image.src = source; // Source must be set after event attachment for IE
        image.setStyle({ position: 'absolute' });

        return image;
    },

    //method [private] _setOptions
    //desc   Sets default options and assigns named constructor arguments to
    //       this.options.
    //param  [opt] options [Object] See initialize method for details.
    _setOptions: function (options) {
        this.options = {
            thumbnails : [],
            autoplay   : false,
            preload    : false,
            preloadNext: false,
            interval   : 5.0,
            duration   : 2.0,
            delay      : 0.0,
            indicator  : null,
            previous   : null,
            next       : null,
            play       : null,
            stop       : null,
            pause      : null,
            callback   : null
        };

        RUI.Object.extend(this.options, options || {});
    },

    //method next
    //desc   Increments the position by one.
    next: function () {
        this.increment(1);
    },

    //method previous
    //desc   Decrements the position by one.
    previous: function () {
        this.increment(-1);
    },

    //method increment
    //desc   Increments the position.
    //param  [req] increment [Number] integer increment
    //return [Boolean] false if the slideshow stalled
    increment: function (inc) {
        if (this.stalled) {
            // The image from the previous slide is still loading
            return false;
        }
        this.position += parseInt(inc, 10);
        if (
            !(this._setPosition(this.position)) &&
            !this.setPositionOnImageLoad
        ) {
            this.stall(); // Pause the slideshow until the image has loaded
            this.position -= inc; // Make sure we don't skip over this image
        }
        return true;
    },

    //method [private] _getNormalizedPosition
    //desc   Gets the normalized position with the image array.
    //param  [req] unnormalizedPosition [Number] integer unnormalized position
    //return [Number] normalized position within the image array
    _getNormalizedPosition: function (unnormalizedPosition) {
        var sources = this.imageSources.toArray();
        var normPosition, remainder;

        if (unnormalizedPosition > sources.length - 1) {
            remainder = unnormalizedPosition % sources.length;
            normPosition = remainder; // Loop back to the beginning
        }
        else if (unnormalizedPosition < 0) {
            remainder = sources.length % unnormalizedPosition;
            normPosition = sources.length - remainder - 1; // Loop to the end
        }
        else {
            normPosition = unnormalizedPosition;
        }

        return normPosition;
    },

    //method [private] _setPosition
    //desc   Set the position.
    //param  [req] position [Number] integer increment
    //return [Boolean] false if fails to set the position
    _setPosition: function (position) {
        var sources = this.imageSources.toArray(); // So we can use .length
        position = this._getNormalizedPosition(position);
        var source = sources[position];
        var newImage = this.loadImage(source);
        this.position = position;

        var indicator;

        if (this.images[source]) {
            // show the image
            clearInterval(this.interval);
            this.interval = false;

            //load next image
            if (this.options.preloadNext) {
                this.loadNextImage();
            }
            // Fade out any image elements inside the container
            RUI.$A(
                this.container.getElementsByTagName('img')
            ).each(function (img) {
                $J(img).fadeOut(this.options.duration * 1000, function () {
                    if (img.parentNode) {
                        RUI.Element.remove(img);
                    }
                });
            }.bind(this));

            RUI.Element.hide(newImage);
            this.container.appendChild(newImage);
            $J(newImage).fadeIn(this.options.duration * 1000);

            // provide call back
            if (this.options.callback) {
                this.invokeCallback();
            }
        }
        else {
            // show the indicator
            this.stalled = true;
            indicator = RUI.$(this.options.indicator);
            if (indicator) {
                indicator.show();
            }
        }
        return !this.stalled;
    },

    //method play
    //desc   Periodically calls the next() method, rotating through
    //       the slideshow at a configurable interval.
    play: function () {
        this.playing = true;
        // setTimeout ensures the slideshow doesn't switch slides immediately
        this.timeout = setTimeout(function () {
            // Total period = Interval + Fade duration
            this.periodicalExecuter = new RUI.PeriodicalExecuter(
                this.next.bind(this),
                this.options.interval + this.options.duration
            );
            clearTimeout(this.timeout); // Prevent memory leaks
        }.bind(this), this.options.delay * 1000);
    },

    //method stall
    //desc   Pauses and sets the status to stalled to indicate that the
    //       slideshow should be started again when possible (e.g. when an
    //       image has finished downloading).
    stall: function () {
        if (this.periodicalExecuter) {
            this.periodicalExecuter.stop();
        }
        this.stalled = true;
    },

    //method pause
    //desc   Stops the slideshow but retains the current position.
    pause: function () {
        if (this.periodicalExecuter) {
            this.periodicalExecuter.stop();
        }
        this.playing = false;
    },

    //method stop
    //desc   Stops the slideshow and returns the position to zero.
    stop: function () {
        this.pause();
        this.position = 0;
    },

    //method invokeCallback
    //desc   Calls the callback function.
    invokeCallback: function () {
        this.options.callback.call(this);
    }
};

//desc   Represents a group of radiobutton elements.
RUI.RadioButtonGroup = RUI.Class.create();

RUI.RadioButtonGroup.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] container [HTMLElement or String] element or ID
    //param  [opt] callback: [function] onclick callback function
    initialize: function (container, options) {
        this.container = RUI.$(container);
        this.options = options;
        this.radioButtons = RUI.$A(
                this.container.getElementsByTagName('input')
            ).findAll(function (input) {
            return (input.type === 'radio');
        });

        // Register the custom onclick events
        if (this.options.callback) {
            this._registerCallbacks(this.options.callback);
        }
    },

    //method getValue
    //desc   Checkes the selected value from radio button group
    //       and returns it otherwise return null.
    getValue: function () {
        return this.radioButtons.find(function (radioButton) {
            return radioButton.checked;
        }).value || null;
    },

    //method [private] _registerCallbacks
    //desc   Register the callback function with the onclick event of each
    //       radio button.
    _registerCallbacks: function () {
        this.radioButtons.each(function (radioButton) {
            RUI.Event.observe(
                radioButton,
                'click',
                this.options.callback.bind(this)
            );
        }.bind(this));
    },

    //method invokeCallback
    //desc   Calls the callback function.
    invokeCallback: function () {
        this.options.callback.call(this);
    }
};


//desc   Represents a group of checkbox elements.
RUI.CheckboxGroup = RUI.Class.create();

RUI.CheckboxGroup.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] container [String] or [HTMLElement] container ID or object
    //param  [opt] selectAllBox: [String] or [HTMLElement] 'select all' checkbox
    //       ID or object
    //param  [opt] callback: [function] onclick callback function
    initialize: function (container, options) {
        this.container = RUI.$(container);

        this._setOptions(options);

        this.options.selectAllBox = RUI.$(this.options.selectAllBox);

        // Grab the checkbox inputs from the container element
        this.checkboxes = [];
        RUI.$A(
            this.container.getElementsByTagName('input')
        ).each(function (input) {
            if (input.type === 'checkbox'
                && input !== this.options.selectAllBox) {
                this.checkboxes.push(input);
            }
        }.bind(this));

        // Register the custom onclick events
        if (this.options.callback) {
            this._registerCallbacks(this.options.callback);
        }

        // A checkbox that will control the selected status of the group
        if (this.options.selectAllBox) {
            // Update the group of checkboxes when 'select all' is clicked
            this.options.selectAllBox.observe(
                'click',
                this.updateGroup.bind(this),
                false
            );

            // Uncheck the 'select all' box when any of the group are unchecked
            this.checkboxes.each(function (checkbox) {
                RUI.Element.extend(checkbox); // Add Prototype's methods
                checkbox.observe(
                    'click',
                    this.updateSelectAll.bind(this),
                    false
                );
            }.bind(this));
        }
    },

    //method [private] _setOptions
    //desc   Sets default options and assigns named constructor arguments to
    //       this.options.
    //param  [opt] options [Object] See initialize method for details.
    _setOptions: function (options) {
        this.options = {};

        RUI.Object.extend(this.options, options || {});
    },

    //method [private] _registerCallbacks
    //desc   Register the callback function with the onclick event of each
    //       checkbox.
    _registerCallbacks: function () {
        this.checkboxes.each(function (checkbox) {
            RUI.Event.observe(
                checkbox,
                'click',
                this.options.callback.bind(this)
            );
        }.bind(this));
    },

    //method invokeCallback
    //desc   Calls the callback function.
    invokeCallback: function () {
        this.options.callback.call(this);
    },

    //method updateGroup
    //desc   Updates the selected status of the checkboxes in the group
    //       depending on the value of the 'select all' checkbox.
    updateGroup: function () {
        var checked = this.options.selectAllBox.checked;
        this.checkboxes.each(function (checkbox) {
            checkbox.checked = checked;
        });
    },

    //method updateSelectAll
    //desc   Updates the sleected status of the 'select all' checkbox depending
    //       on the checked status of the group.
    updateSelectAll: function () {
        var checked = true;
        this.checkboxes.each(function (input) {
            if (!input.checked) {
                checked = false;
                return; // Break the each loop
            }
        });
        this.options.selectAllBox.checked = checked;
    },

    //method isChecked
    //desc   Returns true if at least one of the checkboxes in the group holds
    //       this value and is checked.
    //param  [req] value [String]
    //return [Boolean] at least one is checked
    isChecked: function (value) {
        var checked = false;

        this.checkboxes.each(function (checkbox) {
            if (checkbox.checked && checkbox.value === value) {
                checked = true;
                return; // Break the each loop
            }
        });

        return checked;
    }
};

//desc   Category and associated subcategory select lists.
RUI.RCA.CategorySelect = RUI.Class.create();

// This the string we will expect when the user chooses 'All categories' option
RUI.Object.extend(RUI.RCA.CategorySelect, {
    ALL_CATEGORIES_TEXT : 'All categories'
});

RUI.RCA.CategorySelect.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] categoryObject [Object] RUI.RCA.Categories object
    //param  [req] catSelect [String] or [HTMLElement] category select element
    //       ID or object
    //param  [req] subSelect [String] or [HTMLElement] sub-category select
    //       element ID or object
    initialize: function (categoryObject, catSelect, subSelect) {
        this.categoryObject = categoryObject;
        this.catSelect = RUI.$(catSelect); // Category select element
        this.subSelect = RUI.$(subSelect); // Sub-category select element

        // Update the sub-categories now
        this.update();

        // Ensure sub-categories are updated when category is changed
        this.catSelect.observe('change', this.update.bind(this), false);
    },

    //method update
    //desc   Updates the subcategories select box based on the selected main
    //       category.
    update: function () {
        // Multiple options may be selected so loop over them and check
        // their 'selected' attribute
        var cats = RUI.$A(this.catSelect.getElementsByTagName('option'));

        var firstOption, name;

        // Check if All catgories is chosen
        var allCategoriesSelected = false;
        cats.each(function (cat) {
            if (
                cat.text === RUI.RCA.CategorySelect.ALL_CATEGORIES_TEXT
                && cat.selected
            ) {
                allCategoriesSelected = true;
                throw $break; // Proptotype way of breaking from the each
            }
        });

        var selectedCats = [];
        cats.each(function (cat) {
            if (cat.selected || allCategoriesSelected) {
                selectedCats.push(cat.value);
            }
        }.bind(this));

        var subcats = this.categoryObject.getSubcats(selectedCats);

        // Add a prompting option unless this is a multiple select
        if (!this.subSelect.multiple) {
            firstOption   = {};
            firstOption.name  = 'Select a sub-category';
            firstOption.value = '';
            subcats.unshift(firstOption);
        }

        // get existing selected subcats
        var selectedSubCats = [];
        var oldSubcats = RUI.$A(this.subSelect.getElementsByTagName('option'));
        oldSubcats.each(function (subcat) {
            if (subcat.selected) {
                selectedSubCats.push(subcat);
            }
        });

        // Remove any option elements inside the sub-category select
        RUI.$A(this.subSelect.childNodes).each(function (option) {
            RUI.Element.remove(option);
        });

        // Create the new option elements
        subcats.each(function (subcat) {
            var option = document.createElement('option');
            option.value = subcat.value;
            selectedSubCats.each(function (selectedSubcat) {
                if (option.value === selectedSubcat.value) {
                    option = selectedSubcat;
                }
            });
            if (!option.selected) {
                name = document.createTextNode(subcat.name);
                option.appendChild(name);
            }
            this.subSelect.appendChild(option);
        }.bind(this));
    }
};

//desc   Choose subcategories for a category.
RUI.RCA.Categories = RUI.Class.create();

RUI.RCA.Categories.prototype = {
    //method initialize
    //desc   Constructor.
    //param  [req] categories [Object]
    //       categories is a JSON object of the form
    //       { <catId>: [{name: <subcatName>, value: <subcatValue>}, ...],
    //         ...
    //       }
    initialize: function (categories) {
        this.categories = categories;
    },

    //method getSubcats
    //desc   Given an array of categories, gets the associated subcategories
    //param  [req] cats [array] Category IDs that we're getting subcategories
    //       for.
    //return [array] The appropriate subcategories.
    getSubcats: function (cats) {
        var subcats = [];
        cats.each(function (cat) {
            var theseSubcats = this.categories[cat];
            if (theseSubcats) {
                theseSubcats.each(function (subcat) {
                    subcats.push(subcat);
                });
            }
        }.bind(this));

        // Sort the new array of subcats.
        subcats.sort(function (a, b) {
            if (a.name < b.name) {
                return -1;
            }

            if (a.name > b.name) {
                return 1;
            }

            return 0;
        });

        // The following piece of code combines duplicate category names
        // into one with multiple values separated by comma
        var subcatsUnique = [];
        var lastSubcatSeen;

        subcats.each(function (subcat) {
            if (lastSubcatSeen) {
                if (lastSubcatSeen.name === subcat.name) {
                    lastSubcatSeen.value += ',' + subcat.value;
                }
                else {
                    subcatsUnique.push(lastSubcatSeen);
                    lastSubcatSeen = subcat;
                }
            }
            else {
                lastSubcatSeen = subcat;
            }
        });

        if (lastSubcatSeen) {
            subcatsUnique.push(lastSubcatSeen);
        }

        // Copy over the unique subcategories
        return subcatsUnique;
    }
};

//desc   A class to represent the listing photo tab page.
RUI.RCA.ListingPhotos = RUI.Class.create();

RUI.Object.extend(RUI.RCA.ListingPhotos, {
    ID: {
        NAVIGATION: 'photoNavigation',
        PREVIOUS:   'previousPhoto',
        NEXT:       'nextPhoto',
        PRINT:      'printPhoto'
    }
});

RUI.RCA.ListingPhotos.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        RUI.$(RUI.RCA.ListingPhotos.ID.PRINT).observe('click', function () {
            window.print();
        });
    }
};

//todo   Refactor from a class to a function
//desc   A class to represent the listing details page.
RUI.RCA.ListingDetails = RUI.Class.create();

RUI.Object.extend(RUI.RCA.ListingDetails, {
    ID: {
        LISTING_DETAILS  : 'listingDetails',
        CONTACT_AGENT    : 'ua_contact',
        PRINT_BROCHURE   : 'ua_print',
        ADD_TO_SHORTLIST : 'ua_add',
        EMAIL_FRIEND     : 'ua_email',
        EMAIL_ME         : 'ua_emailMe',
        PHOTO            : 'la_photos',
        VIRTUAL_TOUR     : 'la_tour',
        MORE             : 'la_documents',
        MAP              : 'la_map',
        CONTACT_CONTAINER: 'emailFormAgents',
        MORE_CONTAINER   : 'listingDetailsTools',
        PHOTO_CONTAINER  : 'photoPreview'
    },

    LOG_URL: '/cgi-bin/rsearch',

    //method [static] closeAgentForms
    //desc   Collapses the contact agent forms.
    //param  [req] triggers [Array] array of HTMLElement anchors
    closeAgentForms: function (triggers) {
        triggers.each(function (trigger) {
            RUI.RCA.ListingDetails.hideTriggerContent(trigger);
        });
    }
});

RUI.RCA.ListingDetails.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        // Event handlers to make images appear in a new window
        var moreInfoContainer = RUI.$(RUI.RCA.ListingDetails.ID.MORE_CONTAINER);

        var moreInfoAnchors, moreInforWindow, triggers, multipleAgents,
            contactForms;

        if (moreInfoContainer) {
            moreInfoAnchors = RUI.$A(
                moreInfoContainer.getElementsByTagName('a')
            );

            moreInfoAnchors.each(function (anchor) {
                if ( anchor.id === RUI.RCA.ListingDetails.ID.PRINT_BROCHURE ) {
                    RUI.Element.extend(anchor); // Add Prototype's methods
                    anchor.observe('click', function (e) {
                        moreInfoWindow = window.open(
                            anchor.href,
                            'PrintBrochure',
                            'menubar=yes'  +
                            ',toolbar=yes' +
                            ',status=yes'  +
                            ',location=yes'  +
                            ',resizable=yes,' +
                            'scrollbars=yes,width=830,height=650'
                        );
                        moreInfoWindow.focus();
                        RUI.Event.stop(e);
                    }, false);
                }
            });
        }

        // Contact form display toggling
        var contactContainer = RUI.$(
            RUI.RCA.ListingDetails.ID.CONTACT_CONTAINER
        );
        if (contactContainer) {
            triggers = document.getElementsByClassName(
                RUI.ContentSwitch.CLASS.TRIGGER,
                contactContainer
            );
            multipleAgents = (triggers.toArray().length > 2) ? true : false;

            triggers.each(function (trigger) {
                var contentTriggers = [];
                var parser = new RUI.URLParser(trigger.href);
                var anchorLink = parser.getAnchor();
                var disableOnShow = true;
                var callback, formId, nameField, emailField, phoneField;

                if (anchorLink.match(/emailAgent_/)) {
                    // Email form will toggle open/close
                    disableOnShow = false;
                }
                else if (anchorLink.match(/phoneAgent_/)) {
                    // Log phone clicks
                    callback = function () {
                        (new RUI.Ajax.Request(RUI.RCA.ListingDetails.LOG_URL, {
                            parameters: 'a=event&o=Click_' + anchorLink
                        }));
                    };
                }

                contentTriggers.push(
                    (new RUI.ContentSwitch(
                        trigger,
                        { disableOnShow: disableOnShow, callback: callback }
                    ))
                );

                // Close all contact forms except the one that's being opened
                if (multipleAgents) {
                    trigger.observe('click', function (e) {
                        contentTriggers.each(function (control) {
                            if (
                                control.getContent().visible()
                                && control.getTrigger() !== trigger
                            ) {
                                control.hide();
                            }
                        });
                        RUI.Event.stop(e);
                    }.bind(this));
                }
            });

            // Contact form validation
            contactForms = RUI.$A(
                contactContainer.getElementsByTagName('form')
            );
            contactForms.each(function (form) {
                // Expects input ID to be the same as the form ID follwed by an
                // underscore and then by the input name
                // NB: Don't use getAttribute('id'), it doesn't work in IE.
                formId = form.attributes.id.nodeValue;

                nameField = new RUI.ValidatedField(
                    formId + '_Name',
                    [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
                );
                emailField = new RUI.ValidatedField(
                    formId + '_Email',
                    [(new RUI.ValidationRule(RUI.ValidationRule.EMAIL))]
                );
                phoneField = new RUI.ValidatedField(
                    formId + '_Phone',
                    [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
                );

                // Create the validator, which will attach itself the the submit
                // event of the form.
                (new RUI.FormValidator(
                    form,
                    [nameField, emailField, phoneField]
                ));
            });
        }
    }
};

//todo   Refactor from a class to a function
//desc   Behaviour layer for the basic search form.
RUI.RCA.BasicSearchForm = RUI.Class.create();

RUI.Object.extend(RUI.RCA.BasicSearchForm, {
    ID: {
        BASIC_SEARCH        : 'basicSearchWidget',
        CATEGORY            : 'categories',
        TOTAL_AREA_MIN      : 'totalAreaMin',
        TOTAL_AREA_MAX      : 'totalAreaMax',
        LUK                 : 'lukField',
        ACTIVATE_SEARCH_TEXT: 'activateSearchText',
        FORM                : 'n'
    },

    PARAM: {
        NAME: {
            LAND: {
                SIZE_MIN: 'minlandsize',
                SIZE_MAX: 'maxlandsize'
            }
        },
        VALUE: {
            LAND: {
                CATEGORY_ID: '2'
            }
        }
    },

    TOTAL_AREA_MIN_DEFAULT: 'Min',
    TOTAL_AREA_MAX_DEFAULT: 'Max',
    // if you change LUK_DEFAULT, you will also need to change
    // LUK_INSTRUCTIONS in lib/REA/RSearch/Request.pm
    // TODO: Refactor this so that the default value is taken from the input
    // object, rather than this constant.
    LUK_DEFAULT:            'or enter PropertyLook LUK here'
});

RUI.RCA.BasicSearchForm.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        this._searchPending = 0;
        var totalAreaMin = RUI.RCA.BasicSearchForm.ID.TOTAL_AREA_MIN;
        if (RUI.$(totalAreaMin)) {
            (new RUI.ClearingField(
                totalAreaMin,
                RUI.RCA.BasicSearchForm.TOTAL_AREA_MIN_DEFAULT
            ));
        }

        var totalAreaMax = RUI.RCA.BasicSearchForm.ID.TOTAL_AREA_MAX;
        if (RUI.$(totalAreaMax)) {
            (new RUI.ClearingField(
                totalAreaMax,
                RUI.RCA.BasicSearchForm.TOTAL_AREA_MAX_DEFAULT
            ));
        }

        var lookupKey = RUI.RCA.BasicSearchForm.ID.LUK;
        if (RUI.$(lookupKey)) {
            (new RUI.ClearingField(
                lookupKey,
                RUI.RCA.BasicSearchForm.LUK_DEFAULT
            ));
        }

        var basicSearchForm = RUI.$(RUI.RCA.BasicSearchForm.ID.FORM);
        if (basicSearchForm) {
            basicSearchForm.observe(
                'keypress',
                this._processReturnKeyPress.bindAsEventListener(this)
            );
        }

        //refactor this to a submit event and add a button to the html in the
        //appropriate place
        var searchButtonDiv = RUI.$(
            RUI.RCA.BasicSearchForm.ID.ACTIVATE_SEARCH_TEXT
        );
        if (searchButtonDiv) {
            searchButtonDiv.observe(
                'click',
                this._searchOnce.bindAsEventListener(this)
            );
            searchButtonDiv.observe(
                'mouseover',
                this._cursorToPointerOnMouseOver.bindAsEventListener(this)
            );
            searchButtonDiv.observe(
                'mouseout',
                this._cursorToPointerOnMouseOut.bindAsEventListener(this)
            );
        }
    },

    //method [private] _processReturnKeyPress
    //desc   Process a press of the return/enter key.
    _processReturnKeyPress: function (e) {
        if (e.keyCode === RUI.Event.KEY_RETURN) {
            this._searchOnce();
        }
    },

    //method [private] _cursorToPonterOnMouseOver
    //desc   Change cursor shape to pointer when element is moused over.
    _cursorToPointerOnMouseOver: function (myEvent) {
        RUI.Event.element(myEvent).style.cursor = 'pointer';
    },

    //method [private] _cursorToDefaultOnMouseOut
    //desc   Change cursor shape to default when mouse leaves element.
    _cursorToPointerOnMouseOut: function (myEvent) {
        RUI.Event.element(myEvent).style.cursor = 'default';
    },

    //method [private] _searchOnce
    //desc   Submit the search, checking that another search request has not
    //       already been submitted.
    //note   Can this and related functions be removed and replaced with
    //       form.submit()?
    _searchOnce: function () {
        var form, totalAreaMin, totalAreaMax, category, ID;
        var landParam, landValue;

        if (!this._searchPending) {
            this._searchPending = 10;

            ID = RUI.RCA.BasicSearchForm.ID;
            form = RUI.$(ID.FORM);

            if (form) {
                // if the category selected is land/site development,
                // the Total Area is Land Size, not Floor/Building size.
                totalAreaMin = RUI.$(ID.TOTAL_AREA_MIN);
                totalAreaMax = RUI.$(ID.TOTAL_AREA_MAX);
                category     = RUI.$(ID.CATEGORY);
                landParam    = RUI.RCA.BasicSearchForm.PARAM.NAME.LAND;
                landValue    = RUI.RCA.BasicSearchForm.PARAM.VALUE.LAND;
                if (category && category.value && category.value === landValue.CATEGORY_ID) {
                    totalAreaMin.name = landParam.SIZE_MIN;
                    totalAreaMax.name = landParam.SIZE_MAX;
                }
                $J('#' + ID.FORM).submit();
            }

            this._reduceTime();
        }
        else {
            alert('Your search is still in progress.\n'
                + 'Please wait a few seconds before clicking search again');
        }
    },

    //method [private] _reduceTime
    //desc   Reduce the time to wait on a pending search before another search
    //       can be submitted.
    //note   Can this and related functions be removed and replaced with
    //       form.submit()?
    _reduceTime: function () {
        if (this._searchPending) {
            --this._searchPending;
            setTimeout(this._reduceTime, 1000);
        }
    }
};

//todo   Refactor from a class to a function
//desc   Behaviour layer for the advanced search form.
RUI.RCA.AdvancedSearchForm = RUI.Class.create();

RUI.Object.extend(RUI.RCA.AdvancedSearchForm, {
    ID: {
        ADVANCED_SEARCH            : 'searchFrame',
        REFINE_SEARCH              : 'refineSearch',
        CATEGORY_CHECKBOX_CONTAINER: 'categoryCheckboxGroup',
        TYPE_CHECKBOX_CONTAINER    : 'typeCheckboxGroup',
        TENURE_CONTAINER           : 'tenureContainer',
        INVESTMENT_CONTAINER       : 'returnContainer',
        INVESTMENT_INPUT           : 'return',
        CATEGORY_SELECT_ALL_BOX    : 'categorySelectAll',
        PRICE_RANGE_SALE           : 'salePriceRange',
        PRICE_RANGE_LEASE          : 'leasePriceRange',
        SUBMIT_BUTTON              : 'submitAdvancedSearch',
        FORM                       : 'n'
    },

    // Form parameter names and values
    PARAM: {
        TYPE_BUY_VALUE       : 'buy',
        TYPE_SOLD_VALUE      : 'sold',
        TYPE_LEASE_VALUE     : 'lease',
        TENURE_VACANT_VALUE  : 'Vacant',
        TENURE_TENANTED_VALUE: 'Tenanted'
    },

    //desc   Sets the disabled status of the '%Return (p.a.)' field.
    //method [private static] _setInvestmentDisabledStatus
    _setInvestmentDisabledStatus: function () {
        var typeCheckboxGroup = new RUI.CheckboxGroup(
            RUI.RCA.AdvancedSearchForm.ID.TYPE_CHECKBOX_CONTAINER
        );

        var tenureCheckboxGroup = new RUI.CheckboxGroup(
            RUI.RCA.AdvancedSearchForm.ID.TENURE_CONTAINER
        );

        var disabled = true;

        if (typeCheckboxGroup.isChecked(
                RUI.RCA.AdvancedSearchForm.PARAM.TYPE_BUY_VALUE
            )
            && tenureCheckboxGroup.isChecked(
                RUI.RCA.AdvancedSearchForm.PARAM.TENURE_TENANTED_VALUE
            )
        ) {
            disabled = false;
        }

        RUI.$(
            RUI.RCA.AdvancedSearchForm.ID.INVESTMENT_INPUT
        ).disabled = disabled;
    },

    //method [private static] _typeCallback
    //desc    The callback function for the type (buy/lease/sold) checkboxes.
    _typeCallback: function () {
        var showSalePrice  = false;
        var showLeasePrice = false;
        var showTenure     = false;

        this.checkboxes.each(function (checkbox) {
            if (checkbox.checked) {
                if (checkbox.value
                    === RUI.RCA.AdvancedSearchForm.PARAM.TYPE_BUY_VALUE
                    || checkbox.value
                    === RUI.RCA.AdvancedSearchForm.PARAM.TYPE_SOLD_VALUE) {
                    showSalePrice = true;
                    showTenure    = true;
                }
                else if (checkbox.value
                    === RUI.RCA.AdvancedSearchForm.PARAM.TYPE_LEASE_VALUE) {
                    showLeasePrice = true;
                }
            }
        });

        if (showSalePrice) {
            RUI.Element.show(RUI.RCA.AdvancedSearchForm.ID.PRICE_RANGE_SALE);
        }
        else {
            RUI.Element.hide(RUI.RCA.AdvancedSearchForm.ID.PRICE_RANGE_SALE);
        }

        if (showLeasePrice) {
            RUI.Element.show(RUI.RCA.AdvancedSearchForm.ID.PRICE_RANGE_LEASE);
        }
        else {
            RUI.Element.hide(RUI.RCA.AdvancedSearchForm.ID.PRICE_RANGE_LEASE);
        }

        if (showTenure) {
            [
                RUI.RCA.AdvancedSearchForm.ID.TENURE_CONTAINER,
                RUI.RCA.AdvancedSearchForm.ID.INVESTMENT_CONTAINER
            ].each(RUI.Element.show);
        }
        else {
            [
                RUI.RCA.AdvancedSearchForm.ID.TENURE_CONTAINER,
                RUI.RCA.AdvancedSearchForm.ID.INVESTMENT_CONTAINER
            ].each(RUI.Element.hide);
        }

        RUI.RCA.AdvancedSearchForm._setInvestmentDisabledStatus();
    }
});

RUI.RCA.AdvancedSearchForm.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        this._searchPending = 0;
        // Set price range visibility based on 'type' checkboxes
        var typeContainer = RUI.$(
            RUI.RCA.AdvancedSearchForm.ID.TYPE_CHECKBOX_CONTAINER
        );

        var typeCheckboxGroup;

        if (typeContainer) {
            typeCheckboxGroup = new RUI.CheckboxGroup(
                typeContainer,
                { callback: RUI.RCA.AdvancedSearchForm._typeCallback }
            );

            // Initialise the price range input visibility
            typeCheckboxGroup.invokeCallback();
        }

        // Categories on advanced search
        var categoryContainer = RUI.$(
            RUI.RCA.AdvancedSearchForm.ID.CATEGORY_CHECKBOX_CONTAINER
        );
        if (categoryContainer) {
            (new RUI.CheckboxGroup(
                categoryContainer,
                { selectAllBox:
                      RUI.RCA.AdvancedSearchForm.ID.CATEGORY_SELECT_ALL_BOX
                }
            ));
        }

        // Set disabled status of '%Return (p.a.)' based on type and tenure
        var tenureContainer = RUI.$(
            RUI.RCA.AdvancedSearchForm.ID.TENURE_CONTAINER
        );
        if (tenureContainer) {
            (new RUI.CheckboxGroup(
                tenureContainer,
                { callback:
                      RUI.RCA.AdvancedSearchForm._setInvestmentDisabledStatus
                }
            ));
        }

        var searchButton = RUI.$(
            RUI.RCA.AdvancedSearchForm.ID.SUBMIT_BUTTON
        );
        if (searchButton) {
            searchButton.observe(
                'click',
                this._searchOnce.bindAsEventListener(this)
            );
        }

    },

    _searchOnce: function () {
        var form, ID;

        if (!this._searchPending) {
            this._searchPending = 10;

            ID = RUI.RCA.BasicSearchForm.ID;
            form = RUI.$(ID.FORM);

            if (form) {
                this._checkQuerySize();
                $J('#' + ID.FORM).submit();
            }

            this._reduceTime();
        }
        else {
            alert('Your search is still in progress.\n'
                + 'Please wait a few seconds before clicking search again');
        }
    },

    //method [private] _reduceTime
    //desc   Reduce the time to wait on a pending search before another search
    //       can be submitted.
    //note   Can this and related functions be removed and replaced with
    //       form.submit()?
    _reduceTime: function () {
        if (this._searchPending) {
            --this._searchPending;
            setTimeout(this._reduceTime, 1000);
        }
    },

    //method [private] _checkQuerySize
    //desc   If the form method is get and the query is considered to be large
    //       the form method is changed to post
    //       This works around a limitation of the get query string size in IE
    //       TODO - This a very basic function that only checks the number of
    //       option elements selected. Should be extended to total up the size
    //       of all the selected form input names and values + the URL to get
    //       the actual query string length
    _checkQuerySize: function () {

        var form, ID, selectedOptions;

        ID = RUI.RCA.AdvancedSearchForm.ID;
        form = RUI.$(ID.FORM);

        if (form && form.method === 'get') {
            selectedOptions = this._getSelectedOptionsIn(
                RUI.$(RUI.RCA.AdvancedSearchForm.ID.FORM)
            );
            if (selectedOptions > 10) {
                form.method = 'post';
            }

        }
    },

    //method [private] _getSelectedOptionsIn
    //desc   Returns all of the selected option elements inside the container
    //       element
    // param containerEl: [Element] Any element that contains option elements
    //       regardless of depth
    _getSelectedOptionsIn: function (containerEl) {
        var options;
        var selectedOptions = 0;
        options = RUI.$(containerEl).getElementsBySelector(
            'option'
        ).each(function (option) {
            if (option.selected === true) {
                selectedOptions++;
            }
        });
        return selectedOptions;
    }

};

//desc   A class to show/hide content
RUI.ContentSwitch = RUI.Class.create();

RUI.Object.extend(RUI.ContentSwitch, {
    CLASS: {
        TRIGGER : 'trigger',

        MINUS   : 'minus',
        PLUS    : 'plus',
        DISABLED: 'disabled'
    }
});

RUI.ContentSwitch.prototype = {
    //method initialize
    //desc   Constructor.
    //note   The content can be defined either in the anchor portion of
    //       the trigger href, or explicitly as an argument.
    //eg     Either:
    //
    //       <a href="/path/with/an/anchor#theContent" id="theTrigger">
    //          The Trigger
    //       </a>
    //       <div id="theContent">
    //          <a name="theContent"></a>
    //          The content to control
    //       </div>
    //       <script type="text/javascript">//<![CDATA[
    //         (new RUI.ContentSwitch('theTrigger'));
    //       //]]></script>
    //
    //       or:
    //
    //       <a href="/path/without/an/anchor" id="theTrigger">The Trigger</a>
    //       <div id="theContent">The content to control</div>
    //       <script type="text/javascript">//<![CDATA[
    //         (new RUI.ContentSwitch('theTrigger', {
    //             content: 'theContent'
    //         }));
    //       //]]></script>
    //param  [req] trigger [String] or [HTMLElement] trigger anchor ID or object
    //param  [opt] content: [String] or [HTMLElement] Content ID or object.
    //       This is not required if the content ID is embedded in the trigger
    //       href as an anchor.
    //param  [opt] disableOnShow: [Boolean] true if the trigger should
    //       be disabled once the content has been shown.
    //param  [opt] callback: [function] callback to be triggered on show/hide
    initialize: function (trigger, options) {
        this.trigger = RUI.$(trigger);

        this._setOptions(options);

        var parser;

        if (this.options.content) {
            this.content = RUI.$(this.options.content);
        }
        else {
            parser = new RUI.URLParser(this.trigger.href);
            this.content = RUI.$(parser.getAnchor());
        }

        // Bind the this keyword now so that stopObserving can be passed the
        // function reference later on (it won't work otherwise)
        this.toggle = this.toggle.bindAsEventListener(this);

        this.trigger.observe('click', this.toggle);
    },

    //method [private] _setOptions
    //desc   Sets default options and assigns named constructor arguments to
    //       this.options.
    //param  [opt] options [Object] See initialize method for details.
    _setOptions: function (options) {
        this.options = {
            disableOnShow: false
        };

        RUI.Object.extend(this.options, options || {});
    },

    //method show
    //desc   Shows the content.
    //param  [opt] event [Event]
    show: function (e) {
        this.trigger.removeClassName(RUI.ContentSwitch.CLASS.PLUS);
        if (this.options.disableOnShow) {
            this.trigger.addClassName(RUI.ContentSwitch.CLASS.DISABLED);
            this.trigger.stopObserving('click', this.toggle);
            // Disable the onclick
            this.trigger.observe('click', function (e) {
                RUI.Event.stop(e);
            });
        }
        else {
            this.trigger.addClassName(RUI.ContentSwitch.CLASS.MINUS);
        }
        this.content.show();
        this.invokeCallback();
        if (e) {
            RUI.Event.stop(e);
        }
    },

    //method hide
    //desc   Hides the content.
    //param  [opt] event [Event]
    hide: function (e) {
        // Don't hide disabled triggers
        if (this.trigger.hasClassName(RUI.ContentSwitch.CLASS.DISABLED)) {
            return;
        }

        this.trigger.removeClassName(RUI.ContentSwitch.CLASS.MINUS);
        this.trigger.addClassName(RUI.ContentSwitch.CLASS.PLUS);
        this.content.hide();
        this.invokeCallback();

        if (e) {
            RUI.Event.stop(e);
        }
    },

    //method toggle
    //desc   Toggles the display of the content.
    //param  [opt] event [Event]
    toggle: function (e) {
        if (this.content.visible()) {
            this.hide(e);
        }
        else {
            this.show(e);
        }
    },

    //method getContent
    //desc   Returns the content element.
    //return [HTMLElement] content
    getContent: function () {
        return this.content;
    },

    //method getTrigger
    //desc   Returns the trigger element.
    //return [HTMLElement] trigger
    getTrigger: function () {
        return this.trigger;
    },

    //method invokeCallback
    //desc   Calls the callback function.
    invokeCallback: function () {
        if (this.options.callback) {
            this.options.callback.call(this);
        }
    }
};

// TODO: Add READoc
// TODO: Refactor from a class to a function
RUI.RCA.SEODirectories = RUI.Class.create();

RUI.Object.extend(RUI.RCA.SEODirectories, {
    DIRECTORY_ID: 'dirLinks'
});

RUI.RCA.SEODirectories.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {
        // Directory items display toggling show and hide
        var triggers = document.getElementsByClassName(
            RUI.ContentSwitch.CLASS.TRIGGER,
            RUI.$(RUI.RCA.SEODirectories.DIRECTORY_ID)
        );

        triggers.each(function (trigger) {
            (new RUI.ContentSwitch(trigger));
        });
    }
};

// TODO: Add READoc
// TODO: Refactor from a class to a function
RUI.RCA.Homepage = RUI.Class.create();

RUI.Object.extend(RUI.RCA.Homepage, {
    ID: {
        HOMEPAGE   : 'homepage',
        QUICK_LINKS: 'quickLinks'
    }
});

RUI.RCA.Homepage.prototype = {
    initialize: function () {
        // Add toggling to quick links
        var triggers = document.getElementsByClassName(
            RUI.ContentSwitch.CLASS.TRIGGER,
            RUI.$(RUI.RCA.Homepage.ID.QUICK_LINKS)
        );

        triggers.each(function (trigger) {
            (new RUI.ContentSwitch(trigger));
        });
    }
};

//desc   A class to handling the realcommercial auction booking
//       form functionality calls validation of the form fields
//       also show/hide particular form sections based on user
//       selection.
//todo   Current validation is limited to required and email
//       further validation to be added in the future for each
//       specific business rule or extract/refactor the validation
//       logic to a separate business class.
RUI.RCA.AuctionBooking = RUI.Class.create();

RUI.Object.extend(RUI.RCA.AuctionBooking, {
    //HTML id attributes
    ID: {
        FORM               : 'auctionBooking',
        LOOKUP_KEY_OPTION  : 'lookupKeyOptions',
        LISTING_DETAILS_BOX: 'listingDetailsBox',
        LOOKUP_KEY_BOX     : 'lookupKeyBox',
        USE_LOOKUP_KEY     : 'useLookupKey'
    },

    //HTML form value constant
    VALUE: {
        LISTED_RCA_YES: 'Yes'
    },

    //HTML form field ids
    FIELD: {
        AGENCY_NAME         : 'AgencyName',
        AGENCY_ADDRESS      : 'AgencyAddress',
        AGENCY_TOWN_SUBURB  : 'AgencyTownSuburb',
        AGENCY_POSTCODE     : 'AgencyPostcode',
        AGENT_CONTACT_NAME  : 'ContactName',
        AGENT_CONTACT_PHONE : 'ContactPhone',
        AGENT_CONTACT_MOBILE: 'ContactMobile',
        AGENT_CONTATC_EMAIL : 'ContactEmail',
        USE_LOOKUP_KEY_YES  : 'UseLookupKeyYes',
        USE_LOOKUP_KEY_NO   : 'UseLookupKeyNo',
        LOOKUP_KEY          : 'LookupKey',
        PROPERTY_TYPE       : 'PropertyType',
        PROPERTY_ADDRESS    : 'PropertyAddress',
        PROPERTY_TOWN_SUBURB: 'PropertyTownSuburb',
        AUCTION_DAY_ID      : 'AuctionDay',
        AUCTIONEER_ID       : 'Auctioneer'
    }
});

RUI.RCA.AuctionBooking.prototype = {
    //method initialize
    //desc   Constructor.
    initialize: function () {

        var ID = RUI.RCA.AuctionBooking.ID;
        var VALUE = RUI.RCA.AuctionBooking.VALUE;
        var FIELD = RUI.RCA.AuctionBooking.FIELD;

        //auction booking form validation
        var auctionBookingForm = RUI.$(ID.FORM);

        // Auction booking form validation
        var agencyName = new RUI.ValidatedField(
            FIELD.AGENCY_NAME,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var agencyAddress = new RUI.ValidatedField(
            FIELD.AGENCY_ADDRESS,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var agencyTownSuburb = new RUI.ValidatedField(
            FIELD.AGENCY_TOWN_SUBURB,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var agencyPostcode = new RUI.ValidatedField(
            FIELD.AGENCY_POSTCODE,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var contactName = new RUI.ValidatedField(
            FIELD.AGENT_CONTACT_NAME,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var contactPhone = new RUI.ValidatedField(
            FIELD.AGENT_CONTACT_PHONE,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var contactMobile = new RUI.ValidatedField(
            FIELD.AGENT_CONTACT_MOBILE,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var contactEmail = new RUI.ValidatedField(
            FIELD.AGENT_CONTATC_EMAIL,
            [(new RUI.ValidationRule(RUI.ValidationRule.EMAIL))]
        );

        var lookupKey = new RUI.ValidatedField(
            FIELD.LOOKUP_KEY,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var propertyType = new RUI.ValidatedField(
            FIELD.PROPERTY_TYPE,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var propertyAddress = new RUI.ValidatedField(
            FIELD.PROPERTY_ADDRESS,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var propertyTownSuburb = new RUI.ValidatedField(
            FIELD.PROPERTY_TOWN_SUBURB,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var auctionDayID = new RUI.ValidatedField(
            FIELD.AUCTION_DAY_ID,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        var auctioneerID = new RUI.ValidatedField(
            FIELD.AUCTIONEER_ID,
            [(new RUI.ValidationRule(RUI.ValidationRule.REQUIRED))]
        );

        // Create the validator, which will attach itself the the submit
        // event of the form.

        // On initialize load all the validatiors
        var auctionValidator = new RUI.FormValidator(auctionBookingForm,
            [agencyName,
             agencyAddress,
             agencyTownSuburb,
             agencyPostcode,
             contactName,
             contactPhone,
             contactMobile,
             contactEmail,
             lookupKey,
             propertyType,
             propertyAddress,
             propertyTownSuburb,
             auctionDayID,
             auctioneerID]
        );

        var typeContainer = RUI.$(ID.LOOKUP_KEY_OPTION);
        var typeRadioGroup = new RUI.RadioButtonGroup(typeContainer, {
            callback: function () {
                var value = this.getValue();
                if (value === VALUE.LISTED_RCA_YES) {
                    RUI.$(ID.LOOKUP_KEY_BOX).show();
                    RUI.$(ID.LISTING_DETAILS_BOX).hide();
                    //add and remove any validation fields
                    auctionValidator.addValidatedField(lookupKey);
                    auctionValidator.removeValidatedField(
                        propertyType
                    );
                    auctionValidator.removeValidatedField(
                        propertyAddress
                    );
                    auctionValidator.removeValidatedField(
                        propertyTownSuburb
                    );
                }
                else {
                    RUI.$(ID.LOOKUP_KEY_BOX).hide();
                    RUI.$(ID.LISTING_DETAILS_BOX).show();
                    //add and remove any validation fields
                    auctionValidator.removeValidatedField(
                        lookupKey
                    );
                    auctionValidator.addValidatedField(
                        propertyType
                    );
                    auctionValidator.addValidatedField(
                        propertyAddress
                    );
                    auctionValidator.addValidatedField(
                        propertyTownSuburb
                    );
                }
            }
        });

        // Initialise the the callback function
        typeRadioGroup.invokeCallback();
    }
};

//desc   Behaviours for RCA Google maps.
// TODO: Move to the RUI.RCA namespace
RUI.RCA.GoogleMap = {

    // Element id's used to discriminate between different map behaviours
    ID: {
        PROPERTY_DETAILS : 'propertyLocation',
        PRINT_DETAILS    : 'printableMap',
        VIEW_ON_MAP      : 'searchResults'
    },

    //method [static] initialize
    //desc   Initializes behaviours for RCA Google maps
    initialize: function () {
        // Bail early if there is no Google map on the page
        if (!RUI.GoogleMap || !RUI.GoogleMap.map) {
            return;
        }

        var path               = '/im/rca/maps/gmap/';
        var singleImage        = path + 'pin.png';
        var singlePrintImage   = path + 'pin.gif';
        var multipleImage      = path + 'multiplePin.png';
        var multiplePrintImage = path + 'multiplePin.gif';

        var defaultMarkerOptions = {
            image        : singleImage,
            printImage   : singlePrintImage,
            mozPrintImage: singleImage
        };

        var smallControls = (
            RUI.$(RUI.RCA.GoogleMap.ID.PROPERTY_DETAILS) ? true : false
        );

        RUI.GoogleMap.load({
            showGoogleBar           : false,
            smallControls           : smallControls,
            singleMarkerOptions     : defaultMarkerOptions,
            innaccurateMarkerOptions: defaultMarkerOptions,
            multipleMarkerOptions   : {
                image        : multipleImage,
                printImage   : multiplePrintImage,
                mozPrintImage: multipleImage
            }
        });
    }
};

//desc   The main RCA stats namespace.
RUI.RCA.Stats = {};

RUI.RCA.Stats.trackClickThrough = function (products) {
    var rhauCheckAvailabilityLinks = $J('.showAgentPhone') || [];
    var rhauCheckAvailability = function (products) {
        RUI.StatsExtras.onClickTracking.register({
            targets: rhauCheckAvailabilityLinks,
            callback: function () {
                if (!s_account) {
                    return;
                }
                var s = s_gi(s_account);
                s.linkTrackVars = 'events,products';
                s.linkTrackEvents='purchase,event17';
                s.events='purchase,event17';
                s.products=products;
                s.tl(this,'o','agent-phonecall');
            }
        });
    };
    if (rhauCheckAvailabilityLinks.length > 0) {
        rhauCheckAvailability(products);
    }
};

