/**

* Flot plugin for adding 'events' to the plot.
*
* Events are small icons drawn onto the graph that represent something happening at that time.
*
* This plugin adds the following options to flot:
*
* options = {
*      events: {
*          levels: int   // number of hierarchy levels
*          data: [],     // array of event objects
*          types: []     // array of icons
*          xaxis: int    // the x axis to attach events to
*      }
*  };
*
*
* An event is a javascript object in the following form:
*
* {
*      min: startTime,
*      max: endTime,
*      eventType: "type",
*      title: "event title",
*      description: "event description"
* }
*
* Types is an array of javascript objects in the following form:
*
* types: [
*     {
*         eventType: "eventType",
*         level: hierarchicalLevel,
*         icon: {
              image: "eventImage1.png",
*             width: 10,
*             height: 10
*         }
*     }
*  ]
*
* @author Joel Oughton
*/

(function($){

function init(plot){
    var DEFAULT_ICON = {
        icon: "icon-caret-up",
        size: 20,
        width: 19,
        height: 10
    };

    var _events = [], _types, _eventsEnabled = false, lastRange;

    plot.getEvents = function(){
        return _events;
    };

    plot.hideEvents = function(levelRange){

        $.each(_events, function(index, event){
            if (_withinHierarchy(event.level(), levelRange)) {
                event.visual().getObject().hide();
            }
        });

    };

    plot.showEvents = function(levelRange){
        plot.hideEvents();

        $.each(_events, function(index, event){
            if (!_withinHierarchy(event.level(), levelRange)) {
                event.hide();
            }
        });

        _drawEvents();
    };

    plot.hooks.processOptions.push(function(plot, options){
        // enable the plugin
        if (options.events.data != null) {
            _eventsEnabled = true;
        }
    });

    plot.hooks.draw.push(function(plot, canvascontext){
        var options = plot.getOptions();
        var xaxis = plot.getXAxes()[options.events.xaxis - 1];

        if (_eventsEnabled) {

            // check for first run
            if (_events.length < 1) {

                _lastRange = xaxis.max - xaxis.min;

                // check for clustering
                if (options.events.clustering) {
                    var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min);
                    _types = ed.types;
                    _setupEvents(ed.data);
                } else {
                    _types = options.events.types;
                    _setupEvents(options.events.data);
                }

            } else {
                if (options.events.clustering) {
                    _clearEvents();
                    var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min);
                    _types = ed.types;
                    _setupEvents(ed.data);
                }
                _updateEvents();
            }
        }

        _drawEvents();
    });

    var _drawEvents = function() {
        var o = plot.getPlotOffset();
        var pleft = o.left, pright = plot.width() - o.right;

        $.each(_events, function(index, event){

            // check event is inside the graph range and inside the hierarchy level
            if (_insidePlot(event.getOptions().min) &&
                !event.isHidden()) {
                event.visual().draw();
            }  else {
                event.visual().getObject().hide();
            }
        });

        _identicalStarts();
        _overlaps();
    };

    var _withinHierarchy = function(level, levelRange){
        var range = {};

        if (!levelRange) {
            range.start = 0;
            range.end = _events.length - 1;
        } else {
            range.start = (levelRange.min == undefined) ? 0 : levelRange.min;
            range.end = (levelRange.max == undefined) ? _events.length - 1 : levelRange.max;
        }

        if (level >= range.start && level <= range.end) {
            return true;
        }
        return false;
    };

    var _clearEvents = function(){
        $.each(_events, function(index, val) {
            val.visual().clear();
        });

        _events = [];
    };

    var _updateEvents = function() {
        var o = plot.getPlotOffset(), left, top;
        var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];

        $.each(_events, function(index, event) {
            top = o.top + plot.height() - event.visual().height();
            left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;

            event.visual().moveTo({ top: top, left: left });
        });
    };

    var _showTooltip = function(x, y, event){
        $('#tooltip').remove();

        // @rashidkpc - hack to work with our normal tooltip placer
        var $tooltip = $('<div id="tooltip">');
        if (event) {
            $tooltip
                .html(event.description)
                .place_tt(x, y, {
                    offset: 10
                });
        } else {
            $tooltip.remove();
        }
    };

    var _setupEvents = function(events){

        $.each(events, function(index, event){
            var level = (plot.getOptions().events.levels == null || !_types || !_types[event.eventType]) ? 0 : _types[event.eventType].level;

            if (level > plot.getOptions().events.levels) {
                throw "A type's level has exceeded the maximum. Level=" +
                level +
                ", Max levels:" +
                (plot.getOptions().events.levels);
            }

            _events.push(new VisualEvent(event, _buildDiv(event), level));
        });

        _events.sort(compareEvents);
    };

    var _identicalStarts = function() {
        var ranges = [], range = {}, event, prev, offset = 0;

        $.each(_events, function(index, val) {

            if (prev) {
                if (val.getOptions().min == prev.getOptions().min) {

                    if (!range.min) {
                        range.min = index;
                    }
                    range.max = index;
                } else {
                    if (range.min) {
                        ranges.push(range);
                        range = {};
                    }
                }
            }

            prev = val;
        });

        if (range.min) {
            ranges.push(range);
        }

        $.each(ranges, function(index, val) {
            var removed = _events.splice(val.min - offset, val.max - val.min + 1);

            $.each(removed, function(index, val) {
                val.visual().clear();
            });

            offset += val.max - val.min + 1;
        });
    };

    var _overlaps = function() {
        var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];
        var range, diff, cmid, pmid, left = 0, right = -1;
        pright = plot.width() - plot.getPlotOffset().right;

        // coverts a clump of events into a single vertical line
        var processClump = function() {
            // find the middle x value
            pmid = _events[right].getOptions().min -
                (_events[right].getOptions().min - _events[left].getOptions().min) / 2;

            cmid = xaxis.p2c(pmid);

            // hide the events between the discovered range
            while (left <= right) {
                _events[left++].visual().getObject().hide();
            }

            // draw a vertical line in the middle of where they are
            if (_insidePlot(pmid)) {
                _drawLine('#000', 1, { x: cmid, y: 0 }, { x: cmid, y: plot.height() });

            }
        };

        if (xaxis.min && xaxis.max) {
            range = xaxis.max - xaxis.min;

            for (var i = 1; i < _events.length; i++) {
                diff = _events[i].getOptions().min - _events[i - 1].getOptions().min;

                if (diff / range > 0.007) {  //enough variance
                    // has a clump has been found
                    if (right != -1) {
                        //processClump();
                    }
                    right = -1;
                    left = i;
                } else {    // not enough variance
                    right = i;
                    // handle to final case
                    if (i == _events.length - 1) {
                        //processClump();
                    }
                }
            }
        }
    };

    var _buildDiv = function(event){
        //var po = plot.pointOffset({ x: 450, y: 1});
        var container = plot.getPlaceholder(), o = plot.getPlotOffset(), yaxis,
        xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1], axes = plot.getAxes();
        var top, left, div, icon, level, drawableEvent, eventType;

        // determine the y axis used
        if (axes.yaxis && axes.yaxis.used) yaxis = axes.yaxis;
        if (axes.yaxis2 && axes.yaxis2.used) yaxis = axes.yaxis2;

        if(event.eventType.split(',')[1] === 'cluster') {
            eventType = event.eventType.split(',')[0]
        } else {
            eventType = event.eventType;
        }

        // use the default icon and level
        if (_types == null || !_types[eventType] || !_types[eventType].icon) {
            icon = DEFAULT_ICON;
            level = 0;
        } else {
            icon = _types[eventType].icon;
            level = _types[eventType].level;
        }

        div = $('<i style="position:absolute" class="'+icon.icon+'"></i>').appendTo(container);

        var width = icon.size || icon.width;
        var height = icon.size || icon.height;

        top = o.top + plot.height() - height + 1;
        left = xaxis.p2c(event.min) + o.left - width / 2;

        // Positions the marker
        var cssOptions = {
            left: left + 'px',
            top: top
        };

        if (icon.outline) cssOptions['text-shadow'] = "1px 1px "+icon.outline+", -1px -1px "+icon.outline+", -1px 1px "+icon.outline+", 1px -1px "+icon.outline;
        if (icon.size) cssOptions['font-size'] = icon['size']+'px';
        if (icon.color) cssOptions.color = icon.color;

        div.css(cssOptions);
        div.hide();
        div.data({
            "event": event
        });
        div.hover(
        // mouseenter
        function(){
            var pos = $(this).offset();
            _showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event"));
        },
        // mouseleave
        function(){
            //$(this).data("bouncing", false);
            $('#tooltip').remove();
            plot.clearSelection();
        });

        drawableEvent = new DrawableEvent(
            div,
            function(obj){
                obj.show();
            },
            function(obj){
                obj.remove();
            },
            function(obj, position){
                obj.css({
                    top: position.top,
                    left: position.left
                });
            },
            left, top, div.width(), div.height());

        return drawableEvent;
    };

    var _getEventsAtPos = function(x, y){
        var found = [], left, top, width, height;

        $.each(_events, function(index, val){

            left = val.div.offset().left;
            top = val.div.offset().top;
            width = val.div.width();
            height = val.div.height();

            if (x >= left && x <= left + width && y >= top && y <= top + height) {
                found.push(val);
            }

            return found;
        });
    };

    var _insidePlot = function(x) {
        var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];
        var xc = xaxis.p2c(x);

        return xc > 0 && xc < xaxis.p2c(xaxis.max);
    };

    var _drawLine = function(color, lineWidth, from, to) {
        var ctx = plot.getCanvas().getContext("2d");
        var plotOffset = plot.getPlotOffset();

        ctx.save();
        ctx.translate(plotOffset.left, plotOffset.top);

        ctx.beginPath();
        ctx.strokeStyle = color;
        ctx.lineWidth = lineWidth;
        ctx.moveTo(from.x, from.y);
        ctx.lineTo(to.x, to.y);
        ctx.stroke();

        ctx.restore();
    };

    /**
     * Runs over the given 2d array of event objects and returns an object
     * containing:
     *
     * {
     *      types {},   // An array containing all the different event types
     *      data [],    // An array of the clustered events
     * }
     *
     * @param {Object} types
     *          an object containing event types
     * @param {Object} events
     *          an array of event to cluster
     * @param {Object} range
     *          the current graph range
     */
    var _clusterEvents = function(types, events, range) {
        //TODO: support custom types
        var groups, clusters = [], newEvents = [];

        // split into same evenType groups
        groups = _groupEvents(events);

        $.each(groups.eventTypes, function(index, val) {
            clusters.push(_varianceAlgorithm(groups.groupedEvents[val], 1, range));
        });

        // summarise clusters
        $.each(clusters, function(index, eventType) {

            // each cluser of each event type
            $.each(eventType, function(index, cluster) {

                var description = "<strong>"+(cluster.length>5?"Top 5 of ":"") + cluster.length + " events</strong>";
                $.each(cluster,function(i,c) {
                    if(i > 5) {
                        return;
                    }
                    description += '<div style="'+(i%2?'background-color:#444;':'')+
                        '" style="padding-bottom:0px">'+c.description + "</div>";
                });

                var newEvent = {
                    min: cluster[0].min,
                    max: cluster[cluster.length - 1].min,    //TODO: needs to be max of end event if it exists
                    eventType: cluster[0].eventType + ",cluster",
                    title: "Cluster of: " + cluster[0].title,
                    description: description //+ ", Number of events in the cluster: " + cluster.length
                };

                newEvents.push(newEvent);
            });
        });

        return { types: types, data: newEvents };
    };

    /**
     * Runs over the given 2d array of event objects and returns an object
     * containing:
     *
     * {
     *      eventTypes [],      // An array containing all the different event types
     *      groupedEvents {},   // An object containing all the grouped events
     * }
     *
     * @param {Object} events
     *          an array of event objects
     */
    var _groupEvents = function(events) {
        var eventTypes = [], groupedEvents = {};

        $.each(events, function(index, val) {
            if (!groupedEvents[val.eventType]) {
                groupedEvents[val.eventType] = [];
                eventTypes.push(val.eventType);
            }

            groupedEvents[val.eventType].push(val);
        });

        return { eventTypes: eventTypes, groupedEvents: groupedEvents };
    };

    /**
     * Runs over the given 2d array of event objects and returns a 3d array of
     * the same events,but clustered into groups with similar x deltas.
     *
     * This function assumes that the events are related. So it must be run on
     * each set of related events.
     *
     * @param {Object} events
     *          an array of event objects
     * @param {Object} sens
     *          a measure of the level of grouping tolerance
     * @param {Object} space
     *          the size of the space we have to place clusters within
     */
    var _varianceAlgorithm = function(events, sens, space) {
        var cluster, clusters = [], sum = 0, avg, density;

        events.sort(sortEvents);

        // find the average x delta
        for (var i = 1; i < events.length - 1; i++) {
            sum += events[i].min - events[i-1].min;
        }
        avg = sum / (events.length - 2);

        // first point
        cluster = [ events[0] ];

        // middle points
        for (var i = 1; i < events.length; i++) {
            var leftDiff = events[i - 1].min - events[i].min;

            density = leftDiff / space;

            var avgSens = avg * sens

            if (leftDiff > avgSens && density > 0.05) {
                clusters.push(cluster);
                cluster = [ events[i] ];
            } else {
                cluster.push(events[i]);
            }
        }

        clusters.push(cluster);

        return clusters;
    };
}

var options = {
    events: {
        levels: null,
        data: null,
        types: null,
        xaxis: 1,
        clustering: false
    }
};

$.plot.plugins.push({
    init: init,
    options: options,
    name: "events",
    version: "0.20"
});

/**
 * A class that allows for the drawing an remove of some object
 *
 * @param {Object} object
 *          the drawable object
 * @param {Object} drawFunc
 *          the draw function
 * @param {Object} clearFunc
 *          the clear function
 */
function DrawableEvent(object, drawFunc, clearFunc, moveFunc, left, top, width, height){
    var _object = object, _drawFunc = drawFunc, _clearFunc = clearFunc, _moveFunc = moveFunc,
    _position = { left: left, top: top }, _width = width, _height = height;

    this.width = function() { return _width; };
    this.height = function() { return _height };
    this.position = function() { return _position; };
    this.draw = function() { _drawFunc(_object); };
    this.clear = function() { _clearFunc(_object); };
    this.getObject = function() { return _object; };
    this.moveTo = function(position) {
        _position = position;
        _moveFunc(_object, _position);
    };
}

/**
 * Event class that stores options (eventType, min, max, title, description) and the object to draw.
 *
 * @param {Object} options
 * @param {Object} drawableEvent
 */
function VisualEvent(options, drawableEvent, level){
    var _parent, _options = options, _drawableEvent = drawableEvent,
        _level = level, _hidden = false;

    this.visual = function() { return _drawableEvent; }
    this.level = function() { return _level; };
    this.getOptions = function() { return _options; };
    this.getParent = function() { return _parent; };

    this.isHidden = function() { return _hidden; };
    this.hide = function() { _hidden = true; };
    this.unhide = function() { _hidden = false; };
}

function compareEvents(a, b) {
    var ao = a.getOptions(), bo = b.getOptions();

    if (ao.min > bo.min) return 1;
    if (ao.min < bo.min) return -1;
    return 0;
};

function sortEvents(a,b) {
    if (a.min < b.min) return 1;
    if (a.min > b.min) return -1;
    return 0;
};

})(jQuery);