/**
 * framework.bi.js (BI Framework)
 *
 * @author John Napier
 *
 * @desc JavaScript engine for AutoTrader Classics Business Intelligence metrics reporting
 *
 * @note All methods preceded by a double underscore ("__") are considered to be "magic keywords" and should never be
 *       called explicitly. They are there to be referenced implicitly through the normal flow of the program whenever
 *       they are needed, and can double as a potential soft reset for each object if data gets corrupted.
 *
 * @requires Prototype v1.6 or later...*sigh*
 *
 * @version 1.2
 */

/**
 * Core BI data logging ajax request
 *
 * @param src String URL representation of tag to log
 * @param queryString String optional query string to be appended to src
 *
 * @note While this will always request the provided src (plus optional query string), and in theory, will
 *       always log to the access logs, any additional URL entries must be matched in BIFilter to ensure
 *       a 200 status is returned. A 200 status is critical for the log entry to be parsed and dumped into
 *       the dataset
 */
var BITag = Class.create({
    initialize : function (src, queryString) {
        this.url = !!(queryString && queryString != null)
            ? src+"?"+queryString
                  : src;
        this.requestObject = null;
    },

    /**
     * Create and store a new Ajax request
     */
    register : function (properties) {
        var defaultProperties =
            {
                method : "GET",
                requestHeaders :
                {
                    "cache-control" : "no-cache"
                }
            };

        return this.requestObject = new Ajax.Request(this.url, Object.extend(defaultProperties, properties.toObject()));
    }
});

/**
 * Class for custom ATX BI event nomenclature
 *
 * @param event String event value for mandatorily logged "event" key
 * @param params Hash of params to be logged. Params are logged in the same key/value pair format that they are set in
 *        the hash, with custom parsing behavior being applied for special types. See notes for special types.
 * @param predicate Function optional conditional predicate (which comes from the concept in set comprehensions) which,
 *        based off it's returned boolean interpretation, decides whether the event should be fired or not.
 *
 * @note Preferably, this class should be referenced instead of an explicit call to BITag, as it doubles as a factory
 *       method for constructing BITag instances with custom domain-specific key/value syntax.
 *
 * @note Special types:
 *       Hash -> Hash objects, when set as a param value, are doubly parsed and converted into a domain-specific
 *               readable and uniquely identifiable syntax for the server logs. Hash format is denoted by a preceeding
 *               "{" and succeeding "}", with comma delimited "key:value" pairs, where the key is a String literal, and
 *               the value is a safe-URL encoded String. Please note that this functionality does NOT support nested
 *               hashes. That is, when a hash, set as a key value, contains key/value pairs where one of the values is
 *               also a Hash instance, that will NOT show up as expected. Instead, the Object representation of the
 *               Hash (denoted as "[Object object]") will be safe-URL encoded and passed instead. Beware of this
 *               entirely intentional caveat!
 */
var BIEvent = Class.create({
    initialize : function (event, params, predicate, ajaxProperties) {
        this.event = (Object.isString(event)) ? event : "load";
        this.params = (Object.isHash(params)) ? params : $H();
        this.predicate = (Object.isFunction(predicate)) ? predicate : Prototype.emptyFunction;
        this.ajax_properties = (Object.isHash(ajaxProperties)? ajaxProperties : new Hash());
    },

    addParam : function (key, value) {
        return this.params.set(key, value);
    },

    removeParam : function (key) {
        return (this.params.get(key) != 'undefined')
            ? this.params.unset(key)
                : false;
    },

    setEvent : function (event) {
        return Object.isString(event)
            ? this.event = event
               : false;
    },

    getEvent : function () {
        return this.event;
    },

    setPredicate : function (predicate) {
        return (Object.isFunction(predicate))
            ? this.predicate = predicate
               : false;
    },

    getPredicate : function () {
        return this.predicate;
    },

    addAjaxProperty : function (key, value) {
        return this.ajax_properties.set(key, value);
    },

    removeAjaxProperty : function (key) {
        return (this.ajax_properties.get(key) != 'undefined')
            ? this.ajax_properties.unset(key)
                : false;
    },

    /**
     * Iterate through params and format a query string adhearing to domain-specific syntax, then return it
     */
    asQueryParam : function (key, value) {
        var k = key,
            v = (Object.isHash(value))
                ? '{'+Object.toQueryString(value.toObject()).replace(/\&|=/g,
                        function($1){
                            return ($1 == '&') ? ',' : ':';
                        })+'}'
                    : encodeURIComponent(value);

        return k + '=' + v;
    },

    canBeBound : function (node) {
        return !!(node && (Object.isElement(node) || node.nodeType === 9 || node == window));
    },

    fire : function (e) {
        var i,
            iterations = [],
            ln,
            multipart = Math.ceil(Math.random()*10000000000),
            properties,
            qstring = "event="+this.event,
            result = [];

        if (this.predicate(e) === false){
            return false;
        }

        this.params.each(function (pair){
            var param = this.asQueryParam(pair.key,  pair.value);
            if ((qstring+param).length < 1500)
            {
                qstring += "&amp;"+param;
            }
            else
            {
                iterations.push(qstring+"&amp;api_key=atx");
                qstring = "event="+this.event+"&amp;"+this.asQueryParam("type", "continued")+"&amp;"+param;
            }
        }.bind(this));
        iterations.push(qstring+"&amp;api_key=atx");

        for (i = 0, ln = iterations.length; i < ln; i++)
        {
            iterations[i] += (ln > 1) ? "&amp;multipart="+multipart : "";
            properties = (i+1 == ln) ? this.ajax_properties : new Hash();
            result.push( new BITag("/log.bi", iterations[i]).register( properties ) );
        }

        return result;
    },

    /**
     *
     * @param node Object element to be observed for event logging
     *
     * @note Preferred over fire, as this directly calls fire upon a registered event matching the set event listener
     */
    bindTo : function (node) {
        return (this.canBeBound(node))
            ? Event.observe(node, this.event, this.fire.bindAsEventListener(this))
               : false;
    }
});

/**
 * Custom BIEvent wrapper for page loading events
 *
 * @param predicate Function
 *
 * @see BIEvent
 */
var PageLoadEvent = Class.create(BIEvent, {
    initialize : function ($super, predicate, ajaxProperties) {
        $super("load", $H({type:"page", url:window.top.location.href}), predicate, ajaxProperties);
    }
});

/**
 * Custom BIEvent wrapper for anchor clicks
 *
 * @param predicate Function
 *
 * @see BIEvent
 */
var LinkClickEvent = Class.create(BIEvent, {
    initialize : function ($super, predicate, ajaxProperties) {
        $super("click", $H({type:"link", orig_uri:window.top.location.href}), predicate, ajaxProperties);
    },

    bindTo : function (node) {
        var i,
            img,
            images = new Hash,
            text = null;

        if (this.canBeBound(node) === false && node.tagName.toLowerCase() === "a"){
            return false;
        }

        text = node.innerHTML.replace(/<[^>]*>(.*)<\/[^>]>|<[^>]*\/>|<img[^>]*>/gi, "$1")
            .replace(/(^\s+[\n]?|[\n]?\s+$|\n)/gi, function($1){
                return ($1 == "\n")
                    ? "\\n"
                       : "";
            }).replace(/\s+/gi, " ");

        var getAltText = false;
        if(text.length > 0){
            this.addParam("text", text);
        } else {
            getAltText = true;
        }

        if ((img = node.getElementsByTagName("img")).length > 0){
            for (i = 0; i < img.length; i++){
                this.addParam("image_"+(i+1), img[i].src);
                if(getAltText)
                    text += img[i].getAttribute("alt");
                    text += " ";
            }
            if(text.length > 0){
                this.addParam("text", text);
            }
        }

        this.addParam("dest_uri", node.getAttribute("href"));
        return Event.observe(node, this.event, this.fire.bindAsEventListener(this));
    },

    fire : function($super, e)
    {
        if (Prototype.Browser.WebKit && !e.__registered__)
        {
            Event.stop(e);
            /* Determine if we're a commandLink rather than a regular link */
            if(!e.element().up().hasClassName("action-button")){
                this.addAjaxProperty("onComplete", this.resume.bind(this, e));
            }


            return $super(e);
        }

        return (Prototype.Browser.WebKit)
            ? null
               : $super(e);
    },

    resume : function(e)
    {
        var mEvent;

        try
        {
            mEvent = document.createEvent("MouseEvents");
            mEvent.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, false, false, false, false);
            mEvent.__registered__ = true;
            Event.element(e).dispatchEvent(mEvent);
        }
        catch (ex)
        {
            throw "This BI Engine is not fully compatible with Safari 2.x";
        }
        finally
        {
            mEvent = null;
        }

        return mEvent;
    }
});

/**
 * Custom BIEvent wrapper for form submissions
 *
 * @param predicate Function
 *
 * @note This event fires on the custom ":submit" event, even though it logs as a true-to-form "submit" event type
 *       (no colon). This is because the event fired isn't truly a submit event and can't currently be spoofed as
 *       a true submit event.
 *
 * @see BIEvent
 */
var FormSubmitEvent = Class.create(BIEvent, {
    initialize : function ($super, predicate, ajaxProperties) {
        $super("submit", $H({type:"form",orig_uri:window.top.location.href}), predicate, ajaxProperties);
    },

    bindTo : function (node) {
        if (node.tagName.toLowerCase() != "form"){
            return false;
        }

        this.addParam("dest_uri", node.getAttribute("action"));

        Event.observe(node, "form:submit", this.fire.bindAsEventListener(this));
        Event.observe(node, "submit", this.fire.bindAsEventListener(this));
        return true;
    },

    parseFormElements : function (node)
    {
        return Form.getElements(node).reject(function(item){
            var e;
            try
            {
                return ((item.tagName.toLowerCase() === "input" && item.readAttribute("type").toLowerCase() == "password")
                        || item.readAttribute("name").indexOf("javax.faces.ViewState") != -1
                        || (Prototype.Browser.IE && item.className.indexOf("BI-IGNORE") != -1)
                        || (!Prototype.Browser.IE && item.readAttribute("class") != null && item.readAttribute("class").indexOf("BI-IGNORE") != -1));
            }
            catch (e)
            {
                return true;
            }
        });
    },

    fire : function($super, e)
    {
        if (Event.element(e).__submitted__){
            return window.jsfcljs_proxied(e);
        }

        this.addParam("form", $(Event.element(e)).readAttribute("id"));
        this.addAjaxProperty("onComplete", window.jsfcljs_proxied.bind(window, e));

        $H(Form.serializeElements(this.parseFormElements(Event.element(e)), {hash:true})).each(
                function (pair) {
                    return this.addParam(pair[0].substr(pair[0].lastIndexOf(":")+1), pair[1]);
                }.bind(this)
        );

        return $super(e);
    }
});

/**
 * Iterate through the DOM, instance BIEvents based on parent classes, add appropriate event listeners to captured
 * HTMLElement objects with said BIEvent instances, and optionally execute callback upon completion
 *
 */
var Runner = Class.create({
    initialize: function (elements, events) {
        this.pattern = {};
        this.setPattern(elements, events);
    },

    setPattern : function (elements, events) {
        this.pattern.elements = (!Object.isUndefined(elements)) ? elements : [];
        this.pattern.events = [];

        if (Object.isArray(events)){
            this.pattern.events = events;
        }
        else {
            if (!Object.isUndefined(events)){
                this.pattern.events.push(events);
            }
        }
    },

    /**
     * Execute the Runner
     *
     * @param callback Function to execute when the Runner completes it's DOM traversal/Event registration
     */
    go : function (callback) {
        return this.__exec(this.pattern.elements, callback, false);
    },

    __exec : function (elements, callback, secondPass) {
        var i, length;

        if (Object.isUndefined(elements) || elements == null){
            return false;
        }

        if (Object.isArray(elements))
        {
            for (i = 0, length = elements.length; i < length; i++)
                this.__exec(elements[i]);
        }
        /**
         * All in all we're just like you...
         */
        else if (elements instanceof (window.HTMLCollection || Prototype.emptyFunction))
        {
            for (i = 0, length = elements.length; i < length; i++)
                this.__exec(elements[i]);
        }
        /**
         * ...we love the all of you
         */
        else if (elements instanceof (window.NodeList  || Prototype.emptyFunction))
        {
            for (i = 0, length = elements.length; i < length; i++)
                this.__exec(elements[i]);
        }
        else if (elements === window || /^1|9$/.test(elements.nodeType || 0))
        {
            this.pattern.events.each(function(eventBuilder){
                var e = (eventBuilder instanceof BI.Utility.EventBuilder)
                    ? eventBuilder.buildEvent()
                        : null;
                return (e instanceof BIEvent)
                    ? e.bindTo(elements)
                       : false;
            });
        }
        /**
         * You disappoint me...
         */
        else if (Prototype.Browser.IE && secondPass !== true)
        {
            for (var property in elements)
                if (property != "length"){
                    this.__exec(elements[property], Prototype.emptyFunction, true);
                }
        }
        /**
         * ...maybe you're better off this way
         */
        else
        {
            return false;
        }

        /**
         * Run the callback
         */
        if (Object.isFunction(callback)){
            callback();
        }

        /**
         * Whew!
         */
        return null;
    }
});

/**
 * Factory method for creating new instances of BIEvent on the fly
 *
 * @param biEventClass Function BIEvent class to build
 *
 * @note biEventClass should NOT be an instance of the event to build, but rather, the parent class itself
 */
var BIEventBuilder = Class.create({
    initialize : function (biEventClass) {
        this.biEventClass = biEventClass;
        this.biEventArguments = null;
    },

    /**
     * Set arguments to pass directly into the specified BIEvent's constructor
     */
    setArguments : function () {
        this.biEventArguments = arguments || null;
        return this;
    },

    /**
     * Create the instance
     */
    buildEvent : function () {
        var args = (this.biEventArguments == null || this.biEventArguments.length == 0)
            ? ""
               : (function(){
                    var i, ln, args = "";
                    for (i = 0, ln = this.biEventArguments.length-1; i < this.biEventArguments.length; i++)
                        args += (i < ln)
                            ? this.biEventArguments[i]+","
                                : this.biEventArguments[i];
                    return args;
                  }).apply(this);

        return eval("new this.biEventClass("+args+")");
    }
});

/**
 * Containing object for all BI related functions
 */
var BI =
{
    Engine :
    {
        /**
         * Default runtime code for all ATX pages
         */
        startup : function ()
        {
            new BI.EventWrapper.PageLoadEvent().fire();
            Event.observe(window, "load", function(){
                new BI.Utility.Runner(document.getElementsByTagName("a"), new BI.Utility.EventBuilder(BI.EventWrapper.LinkClickEvent)).go();
                new BI.Utility.Runner($A(document.forms), new BI.Utility.EventBuilder(BI.EventWrapper.FormSubmitEvent)).go(
                    function ()
                    {
                        /**
                         * Overwriting the native JSF function jsfcljs for BI injection purposes
                         *
                         * @param f form object
                         * @param pvp additional parameters
                         * @param t optional target
                         *
                         * @see FormSubmitEvent#fire
                         */
                        window.jsfcljs = function (f, pvp, t)
                        {
                            $(f).fire("form:submit", {pvp:pvp, t:t});
                            $(f).__submitted__ = true;
                        };

                        /**
                         * Re-interpretation of jsfcljs
                         *
                         * @param e event
                         *
                         * @see FormSubmitEvent#fire
                         */
                        window.jsfcljs_proxied = function(e)
                        {
                            var f = Event.element(e);
                            apf(f, e.memo.pvp);
                            var ft = f.target;
                            if (e.memo.t)
                            {
                                f.target = e.memo.t;
                            }
                            f.submit();
                            f.target = ft;
                            dpf(f);
                        };
                    }
                );
            });
        }
    },
    Event : BIEvent,
    Tag : BITag,
    EventWrapper :
    {
        LinkClickEvent : LinkClickEvent,
        PageLoadEvent : PageLoadEvent,
        FormSubmitEvent : FormSubmitEvent
    },
    Utility :
    {
        EventBuilder : BIEventBuilder,
        Runner : Runner
    }
};

/**
 * Start your engines...
 */
BI.Engine.startup();






/**
 * custom.bi.js (Custom BI Event Tagging)
 *
 * @author John Napier
 *
 * @desc Custom event tagging for AutoTrader Classics Business Intelligence metrics reporting
 *
 * @requires Prototype v1.6 or later
 * @requires BI Framework v1.0 or later
 */
BI.Custom =
{
    A4JEnter : false,
    A4JSubmit : null,
    A4JSubmitProxy : function (containerId, form, evt, options)
    {
        if(typeof evt == 'undefined') return false;

        var bievent,
            el = Event.element(evt),
            elements,
            target;

        if ((el != null && Object.isElement(el) && (el.tagName.toLowerCase() == "a" || (el.tagName.toLowerCase() == "input" && el.getAttribute("type") == "button"))) || BI.Custom.A4JEnter === true)
        {
            target = evt.srcElement ? evt.srcElement : evt.target;

            bievent =
                new BI.Event("submit", $H({
                    type : "form",
                    form : $(form).getAttribute("id"),
                    orig_uri : window.top.location.href,
                    dest_uri : $(form).getAttribute("action") || window.top.location.href
                }), null, $H({onComplete : BI.Custom.A4JSubmit.bind(A4J.AJAX, containerId, form, {srcElement:target, target:target}, options)}));

            elements =
                Form.getElements($(form)).reject(function(item){
                    var e;
                    try
                    {
                        return ((item.tagName.toLowerCase() === "input" && item.readAttribute("type").toLowerCase() == "password")
                                || item.readAttribute("name").indexOf("javax.faces.ViewState") != -1
                                || (Prototype.Browser.IE && item.className.indexOf("BI-IGNORE") != -1)
                                || (!Prototype.Browser.IE && item.readAttribute("class") != null && item.readAttribute("class").indexOf("BI-IGNORE") != -1));
                    }
                    catch (e)
                    {
                        return true;
                    }
                });

            $H(Form.serializeElements(elements, {hash:true})).each(
                function (pair) {
                    return bievent.addParam(pair[0].substr(pair[0].lastIndexOf(":")+1), pair[1]);
                }
            );

            BI.Custom.A4JEnter = false;
            bievent.fire();
        }
        else
        {
            return BI.Custom.A4JSubmit.bind(A4J.AJAX, containerId, form, evt, options)();
        }
    },

    Startup : function ()
    {
        Event.observe(window, "load",
            function ()
            {
                if (typeof window.A4J !== 'undefined')
                {
                    BI.Custom.A4JSubmit = A4J.AJAX.Submit;
                    A4J.AJAX.Submit = BI.Custom.A4JSubmitProxy;
                }
            }
        );
    },

    captureFeatureAdsLoad : function()
    {
        var parse = function (transport)
        {
            var i,
                json = transport.responseText.evalJSON(),
                ln,
                tag = new BI.Event("load", $H({type: "featuredCars", url : window.top.location.href}));

            for (i = 0, ln = json.featuredAds.length; i < ln; i++)
                tag.addParam("ad_"+(i+1), json.featuredAds[i].entityId);

            if (json.featuredAds.length > 0) {
                tag.addAjaxProperty("onComplete", function(){
                    swfobject.embedSWF(
                        "/swf/components/featuredcarflow/featuredCarFlow.swf",
                        "featuredCarFlow",
                        "591",
                        "280",
                        "8.0.0",
                        false,
                        json,
                        {wmode: "transparent"}
                    );
                });
            }

            tag.fire();
        };

        new Ajax.Request("/servlets/featuredVehicles.xhtml", {method:"Get", requestHeaders:{"cache-control":"no-cache"}, onSuccess:parse});
    },

    captureCommunityTopPicksLoad : function()
    {
        var parse = function (transport)
        {
            var i,
                json = transport.responseText.evalJSON(),
                ln,
                tag = new BI.Event("load", $H({type: "featuredMemberCars", url : window.top.location.href}));

            for (i = 0, ln = json.featuredAds.length; i < ln; i++)
                tag.addParam("ad_"+(i+1), json.featuredAds[i].entityId);

            if (json.featuredAds.length > 0) {
                tag.addAjaxProperty("onComplete", function(){
                    swfobject.embedSWF(
                        "/swf/components/membercarflow/memberCarFlow.swf",
                        "memberCarFlow",
                        "591",
                        "280",
                        "8.0.0",
                        false,
                        false,
                        {wmode: "transparent"}
                    );
                });
            }

            tag.fire();
        };

        new Ajax.Request("/servlets/featuredMemberVehicles.xhtml", {method:"Get", requestHeaders:{"cache-control":"no-cache"}, onSuccess:parse});
    },

    SRL :
    {
        count : 0,
        listings : new BI.Event("load", $H({type:"listings", url:window.top.location.href})),

        log : function ()
        {
            return this.listings.fire();
        }
    },

    Ad :
    {
        count : 0,
        ads : new BI.Event("load", $H({type:"featuredAds", url:window.top.location.href})),

        log : function ()
        {
            return this.ads.fire();
        }
    },

    Compare :
    {
        count : 0,
        items : new BI.Event("load", $H({type:"compare", url:window.top.location.href})),

        log : function ()
        {
            return this.items.fire();
        }
    },

    DP :
    {
        count : 0,
        images: new BI.Event("load", $H({type:"dpimages", url:window.top.location.href, data:this.images})),

        log : function ()
        {
            return this.images.fire();
        }
    },

    MapClick : function (element)
    {
        var el = $(element),
            event = new BI.EventWrapper.LinkClickEvent();
        if (el != null && Object.isElement(el))
        {
            event.bindTo(el);
            event.fire();
        }
        return false;

    },

    Articles :
    {
        count : 0,
        articles : new BI.Event("load", $H({type:"listing", url:window.top.location.href})),

        log : function ()
        {
            return this.articles.fire();
        }
    },

    Resources :
    {
        count : 0,
        resources : new BI.Event("load", $H({type:"listing", url:window.top.location.href})),

        log : function ()
        {
            return this.resources.fire();
        }
    }

};

BI.Custom.Startup();