﻿// ------------------------------------------
// Copyright © OPONEO.PL 2010
// Data utworzenia: 2010-12-01 12:05:26
// ------------------------------------------

(function ($)
{
    $.trackEvents = function (options)
    {
        /// <summary>
        /// Uruchamia zapisywanie zdarzeń.
        /// </summary>
        /// <param name="options">
        /// Dostępne opcje:
        /// - dom - elementy DOM, do których będzie podpięta detekcja zdarzeń / domyślnie cały dokument
        /// - useRelativePosition - czy używać pozycji względnej (względem obiektu DOM) / domyślnie true
        /// - events - wykrywane eventy / domyślnie ["click", "mousemove", "change"]
        /// - eventBufferCount - rozmiar bufora, w którym są rejestrowane eventy (liczba rejestrowanych eventów) / domyślnie 1
        /// - eventCallback - metoda wywoływana po zarejestrowaniu eventu / domyślnie null
        /// - finishedCallback - metoda wywoływana po zakończeniu rejestracji eventów / domyślnie null
        /// - delay - gdy jest zdefiniowane to uruchomienie następuje z zadaną wartością / domyślnie 80 milisekund
        /// </param>

        return (new EventTracker(options)).init();
    }

    $.playbackEvents = function (data, options)
    {
        /// <summary>
        /// Uruchamia odtwarzanie sesji użytkownika.
        /// </summary>
        /// <param name="data">Lista klatek z sesji</param>
        /// <param name="options">
        /// Dostępne opcje:
        /// - dom - elementy DOM, do których będą podpięte zdarzenia / domyślnie całe body
        /// - triggeringEnabled - czy event-y mają być wywoływane przy odtwarzaniu / domyślnie true
        /// - timeRatio - współczynnik przyspieszenia/opóźnienia animacji / domyslnie 1.0
        /// </param>
        return (new EventPlayer(data, options)).init();
    }

    // --------------------------
    // CONSTRUCTOR - EventTracker
    // --------------------------

    function EventTracker(options)
    {
        /// <summary>
        /// Tworzy instancję klasy EventTracker.
        /// </summary>
        /// <param name="options">
        /// Dostępne opcje:
        /// - dom - elementy DOM, do których będzie podpięta detekcja zdarzeń / domyślnie cały dokument
        /// - events - wykrywane eventy / domyślnie ["click", "mousemove", "change"]
        /// - eventBufferCount - rozmiar bufora, w którym są rejestrowane eventy (liczba rejestrowanych eventów) / domyślnie 1
        /// - eventCallback - metoda wywoływana po zarejestrowaniu eventu / domyślnie null
        /// - finishedCallback - metoda wywoływana po zakończeniu rejestracji eventów / domyślnie null
        /// - delay - gdy jest zdefiniowane to uruchomienie następuje z zadaną wartością / domyślnie 80 milisekund
        /// </param>

        this._frames = [];
        this._options = $.extend({
            dom: $(document),
            useRelativePosition: true,
            events: ["click", "mousemove", "change"],
            eventBufferCount: 1,
            delay: 80
        }, options || {});
    }

    // ------------------
    // NON-STATIC MEMBERS
    // ------------------

    EventTracker.prototype = {

        // ----------
        // ATTRIBUTES
        // ----------

        _frames: null,
        _options: null,
        _frameIx: null,
        _lastTimePoint: null,

        // -------
        // METHODS
        // -------

        createFrame: function (evt)
        {
            /// <summary>
            /// Buduje ramkę na podstwie wychwyconego event-u
            /// </summary>
            /// <param name="evt">Event</param>

            var offX = 0;
            var offY = 0;

            if (this._options.useRelativePosition)
            {
                offX = parseInt(this._options.dom.offset().left);
                offY = parseInt(this._options.dom.offset().top);
            }

            return {
                x: evt.pageX - offX,
                y: evt.pageY - offY,
                type: evt.type,
                target: this.getElementXPath(evt.target),
                value: evt.type == "change" ? $(evt.target).val() : null,
                timeSpan: 0
            }
        },

        getElementXPath: function (elm)
        {
            /// <summary>
            /// Zwaraca ścieżkę dla elementu
            /// Uwaga: metoda przeniesiona z plugin-u jquery.event-playback.js
            /// </summary>
            /// <param name="elm">Element DOM</param>

            for (segs = []; elm && elm.nodeType == 1; elm = elm.parentNode)
            {
                if (elm.hasAttribute("id"))
                {
                    segs.unshift('id("' + elm.getAttribute("id") + '")')
                    return segs.join("/")
                }
                else if (elm.hasAttribute("class"))
                    segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute("class") + '"]')
                else
                {
                    for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling)
                        if (sib.localName == elm.localName) i++
                    segs.unshift(elm.localName.toLowerCase() + "[" + i + "]")
                }
            }
            return segs.length ? "/" + segs.join("/") : null;
        },

        startTracking: function ()
        {
            /// <summary>
            /// Rozpoczyna rejestrowanie - przypina eventy
            /// </summary>

            var inst = this

            $.each(this._options.events, function (i, type)
            {
                inst._options.dom.bind(type, function (e)
                {
                    if (inst._frameIx < inst._options.eventBufferCount)
                    {
                        var prevFrame = (inst._frames.length > 0 ? inst._frames[inst._frameIx - 1] : null);
                        if (!e.pageX)
                        {
                            // może mieć miejsce w przypadku event-u "change"
                            if (prevFrame) { e.pageX = prevFrame.x; e.pageY = prevFrame.y; }
                        }

                        // wyliczenie czasu poprzedniej klatki (od poprzedniej do bieżącej)
                        var ms = (new Date()).valueOf();
                        var diff = ms - this._lastTimePoint;
                        this._lastTimePoint = ms;

                        if (prevFrame)
                        {
                            prevFrame.timeSpan = diff;
                            inst._frames[inst._frameIx - 1] = prevFrame;
                        }

                        var currFrame = inst.createFrame(e);
                        inst._frames.push(currFrame);
                        ++inst._frameIx;

                        inst.onEvent(currFrame);
                    }
                    else
                    {
                        inst.onFinished();
                    }
                })
            })
            return this;
        },

        onEvent: function (frame)
        {
            /// <summary>
            /// Metoda wywoływana podczas detekcji zdarzenia
            /// </summary>
            /// <param name="frame">Klatka utworzona z okazji zdarzenia</param>

            if (this._options.eventCallback instanceof Function)
                this._options.eventCallback(this, frame);
            return this;
        },

        onFinished: function ()
        {
            /// <summary>
            /// Metoda wywoływana podczas zakończenia detekcji zdarzeń
            /// </summary>

            this._options.dom.unbind();

            if (this._options.finishedCallback instanceof Function)
                this._options.finishedCallback(this);
            return this;
        },

        init: function ()
        {
            /// <summary>
            /// Inicjuje działanie pluginu
            /// </summary>

            var inst = this;

            setTimeout(function ()
            {
                inst.frameIx = 0;
                this._lastTimePoint = (new Date()).valueOf();
                inst.startTracking();
            }, this._options.delay);
            return this;
        },

        stop: function ()
        {
            /// <summary>
            /// Zatrzymuje działanie pluginu
            /// </summary>

            this.onFinished();
        }
    }

    // -------------------------
    // CONSTRUCTOR - EventPlayer
    // -------------------------

    function EventPlayer(data, options)
    {
        /// <summary>
        /// Tworzy instancje EventPlayer
        /// </summary>
        /// <param name="data">Lista klatek z sesji</param>
        /// <param name="options">
        /// Dostępne opcje:
        /// - dom - elementy DOM, do których będą podpięte zdarzenia / domyślnie cały body
        /// - triggeringEnabled - czy event-y mają być wywoływane przy odtwarzaniu / domyślnie true
        /// - timeRatio - współczynnik przyspieszenia/opóźnienia animacji / domyslnie 1.0
        /// </param>

        this._data = data;
        this._options = $.extend({
            dom: $("body"),
            triggeringEnabled: true,
            timeRatio: 1
        }, options || {});
    }

    // ------------------
    // NON-STATIC MEMBERS
    // ------------------

    EventPlayer.prototype = {

        // ----------
        // ATTRIBUTES
        // ----------

        _isRunning: null,
        _lastPath: null,
        _trigger: null,
        _cursor: null,
        _data: null,
        _dom: null,

        // -------
        // METHODS
        // -------

        prepareFrame: function (frame)
        {
            /// <summary>
            /// Przygotowuje klatke do wyświetlenia
            /// </summary>
            /// <param name="frame">Klatka</summary>

            frame.xpath = frame.target;
            frame.target = $(this.getElementByXPath(frame.target));
            return frame;
        },

        getElementByXPath: function (path)
        {
            /// <summary>
            /// Pobiera ścieżkę elementu.
            /// </summary>
            /// <param name="">Ścieżka</param>

            result = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
            return result.singleNodeValue;
        },

        playFrame: function (frame)
        {
            /// <summary>
            /// Odtwarza klatkę
            /// </summary>
            /// <param name="frame">Klatka</summary>

            var inst = this;
            isNewPath = this.lastPath != frame.xpath;

            // Position

            this._cursor.css({
                left: frame.x,
                top: frame.y
            });

            // Utilities

            trigger = function (type)
            {
                inst.triggerEvent(type, frame.target);
            }

            triggerLast = function (type)
            {
                inst.triggerEvent(type, $(inst.getElementByXPath(inst.lastPath)));
            }

            // Event emulation

            switch (frame.type)
            {
                case "click":
                    this._cursor.addClass("clicking");
                    this.showDisc();
                    trigger("click");
                    break;

                case "mousemove":
                    trigger("mouseover");
                    if (isNewPath) triggerLast("mouseout");
                    break;

                case "change":
                    trigger("change");
                    frame.target.val(frame.value);
                    break;
            }

            // GC

            if (this._cursor.hasClass("clicking"))
            {
                this._cursor.removeClass("clicking");
                this.hideDisc();
            }

            this._lastPath = frame.xpath;
            return this;
        },

        triggerEvent: function (type, target)
        {
            /// <summary>
            /// Wywołuje akcję użytkownika
            /// </summary>
            /// <param name="">Typ event-u</param>
            /// <param name="">Obiekt event-u</param>

            if (this._options.triggeringEnabled && target) target.trigger(type);
            return this;
        },

        showDisc: function (color)
        {
            /// <summary>
            /// Pokazuje ikonkę akcji
            /// </summary>
            /// <param name="color">Kolor</param>

            this._disc.addClass(color || "blue").fadeIn(90);
            return this;
        },

        hideDisc: function ()
        {
            /// <summary>
            /// Ukrywa ikonkę akcji
            /// </summary>

            this._disc.fadeOut(80, function ()
            {
                $(this).removeClass("yellow").removeClass("blue").removeClass("red");
            });
            return this;
        },

        initPlayFrame: function ()
        {
            /// <summary>
            /// Inicjalizuje odtwarzanie klatki
            /// </summary>
            /// <param name="timeout">Czas trwania klatki</param>

            var inst = this;

            var frame;
            if (frame = inst._data.shift())
            {
                inst.playFrame(inst.prepareFrame(frame));
                setTimeout(function () { inst.initPlayFrame(); }, frame.timeSpan);
            }
            else
            {
                inst.stop();
            }
            return this;
        },

        displayCursor: function ()
        {
            /// <summary>
            /// Tworzy i pokazuje kursor
            /// </summary>

            this._cursor = $('<div id="event-playback-cursor"><div class="disc"></div></div>');
            this._disc = $(".disc", this._cursor);
            this._options.dom.prepend(this._cursor);
            this._cursor.fadeIn("slow");
            return this;
        },

        removeCursor: function ()
        {
            /// <summary>
            /// Usuwa kursor
            /// </summary>

            $("#event-playback-cursor", this._dom).fadeOut("slow", function ()
            {
                $(this).remove();
            })
            return this;
        },

        stop: function ()
        {
            /// <summary>
            /// Zatrzymuje działanie pluginu odtwarzającego sesję użytkownika
            /// </summary>

            this._isRunning = false;
            clearInterval(this.intervalID);
            this.removeCursor();
            if (this._options.finished instanceof Function)
                this._options.finished.call(this);
            return this;
        },

        init: function ()
        {
            /// <summary>
            /// Inicjalizuje odtwarzanie nagranej sesji
            /// </summary>

            var inst = this;
            this._isRunning = true;
            this.displayCursor();
            this.initPlayFrame();
            if (this._options.duration)
                setTimeout(function () { inst.stop() }, this._options.duration);
            return this;
        }
    }

})(jQuery)

