(function () {
    /**
     * @param {String} tagName
     * @returns {HTMLElement}
     */
    var ce = function (tagName) {
        return document.createElement(tagName);
    };
    
    /**
     * @param {Node} Parent node
     * @param {Node} Child node
     */
    var ac = function (parent, child) {
        return parent.appendChild(child);
    };
    
    /**
     * Creates and appends new child element.
     *
     * @param {Node} Node to append to
     * @param {String} tagName New child element's tagName
     * @returns {HTMLElement} New child element
     */
    var cc = function (node, tagName) {
        return ac(node, ce(tagName));
    };
    
    var _package = pkg('wx.chart.markings');
    
    var _abstract;
    
    /**
     * @class
     * @abstract
     */
    _package.BarModel = Class.extend({
        /**
         * @abstract
         * @returns {String} Label HTML
         */
        getLabel: function () { _abstract },
        
        /**
         * @abstract
         * @returns {Marking[]} Markings
         */
        getMarkings: function() { _abstract },
        
        /**
         * @abstract
         * @returns {String} Extra HTML
         */
        getExtra: function () { _abstract },
        
        /**
         * @abstract
         * @returns {BarModel[]} Child bars
         */
        getChildren: function() { _abstract }
    });
    
    /**
     * @class
     * @abstract
     */
    _package.Bar = Class.extend({
        /**
         * @abstract
         * @param {HTMLElement} label Label container
         * @param {HTMLElement} markings Markings container
         * @param {HTMLElement} extra Extra container
         */
        render: function (label, markings, extra) { _abstract },
        
        /**
         * @abstract
         */
        initialize: function () { _abstract },
        
        /**
         * @returns {Marking[]}
         */
        getGraphMarkings: function () { _abstract }
    });
    
    /**
     * @class
     * @abstract
     */
    _package.BarPanel = Class.extend({
        /**
         * @abstract
         * @param {BarModel} model Bar model
         */
        add: function (model) { _abstract },
        
        /**
         * @abstract
         * @param {Bar} bar Bar
         */
        remove: function (bar) { _abstract },
        
        /**
         * @abstract
         * @returns {Marking[]}
         */
        getGraphMarkings: function () { _abstract }
    });
    
    /**
     * @class
     */
    _package.QuerySetBarModel = _package.BarModel.extend({
        /**
         * @constructor
         * @param {Object} data Log query result set
         */
        init: function (data) {
            this.querySet = data.query;
            this.children = [];
            
            // init global combatLog
            var renderer = new wol.cl.Renderer(data);
            
            // merge data
            this._merge(data);
            
            // re-init renderer
            wol.cl.Renderer.call(renderer, data);
            
            // create children
            for (var i = 0; i < data.logQuery.results.length; i++)
                this.children.push(
                        new _package.QueryBarModel(
                                data.query[i],
                                map(data.logQuery.results[i],
                                        function (i) {
                                            return data.logQuery.entries[i];
                                        }),
                                renderer));
        },
        
        /**
         * @param {Report} report Report
         * @param {Object} query Query
         * @param {Function} callback Callback
         * @returns {QuerySetBarModel}
         */
        create: function (report, query, callback) {
            $.getJSON(
                    ['/reports/', '/d/query/'].join(report.sid),
                    $.extend(
                            { query: query },
                            report.timeInfo ),
                    function (data) { callback(new _package.QuerySetBarModel(data)) });
        },
        
        /**
         * @param {Report} report Report
         * @param {Object} query Query
         * @param {Function} callback Callback
         * @returns {SimpleBar}
         */
        createBar: function (report, query, callback) {
            _package.QuerySetBarModel.create(
                    report,
                    query,
                    function (barModel) { callback(new _package.SimpleBar(barModel)) });
        },
        
        /**
         * @param {Object} data Log query result set
         */
        _merge: function (data) {
            this._mergeActors(data.actors);
            this._mergeFlaggedActors(data.flaggedActors);
            this._mergeSpells(data.spells);
            this._mergeEvents(data.events);
            this._mergeEntries(data.logQuery.entries);
        },
        
        /**
         * @param {Actor[]} actors Actors
         */
        _mergeActors: function (actors) {
            for (var i = 0; i < actors.length; i++)
                combatLog.uids[actors[i].uid]
                        = combatLog.actors[actors[i].id]
                        = actors[i];
        },
        
        /**
         * @param {FlaggedActor[]} flaggedActors
         */
        _mergeFlaggedActors: function (flaggedActors) {
            for (var i = 0; i < flaggedActors.length; i++)
                combatLog.flaggedActors[flaggedActors[i].id] = flaggedActors[i];
        },
        
        /**
         * @param {Spell[]} spells Spells
         */
        _mergeSpells: function (spells) {
            for (var i = 0; i < spells.length; i++)
                combatLog.spells[spells[i].id] = spells[i];
        },
        
        /**
         * @param {Event[]} events Events
         */
        _mergeEvents: function (events) {
            for (var i = 0; i < events.length; i++)
                combatLog.events[events[i].id] = events[i];
        },
        
        /**
         * @param {Entry[]} entries Entries
         */
        _mergeEntries: function (entries) {
            for (var i = 0; i < entries.length; i++)
                combatLog.entries[entries[i][0]] = entries[i];
        },
        
        getLabel: function () {
            return $.toJSON(this.querySet);
        },
        
        getMarkings: function () {
            var markings = [];
            
            for (var i = 0; i < this.children.length; i++)
                markings.push.apply(markings, this.children[i].getMarkings());
            
            return markings;
        },
        
        getExtra: function () {
            return '';
        },
        
        getChildren: function () {
            return this.children;
        }
    });
    
    /**
     * @class
     */
    _package.QueryBarModel = _package.BarModel.extend({
        /**
         * @constructor
         * @param {Object} query Log query
         * @param {Entry[]} entries Entries
         * @param {wol.cl.Renderer} renderer Renderer
         */
        init: function (query, entries, renderer) {
            this.query = query;
            this.entries = entries;
            this.markings = [];
            
            for (var i = 0; i < this.entries.length; i++)
                this.markings.push(new _package.EntryMarking(this.entries[i], renderer));
        },
        
        getLabel: function () {
            return $.toJSON(this.query);
        },
        
        getMarkings: function () {
            return this.markings;
        },
        
        getExtra: function () {
            return '';
        },
        
        getChildren: function () {
            return [];
        }
    });
    
    /**
     * @class
     */
    _package.EntryMarking = Class.extend({
        /**
         * @constructor
         * @param {Entry} entry Entry
         * @param {wol.cl.Renderer} renderer Renderer
         */
        init: function (entry, renderer) {
            var self = this;
            
            this.time = entry[1];
            this.xaxis = {};
            this.xaxis.from
                    = this.xaxis.to
                    = report.startTime + this.time;
            this.color = 'rgba(0, 255, 255, .5)';
            this.lineWidth = 1;
            
            this.renderer = function () {
                return renderer.formatLine(
                        entry[1] * 1000,
                        entry[2],
                        entry[3],
                        entry[4]);
            };
        }
    });
    
    /**
     * @class
     */
    _package.SimpleBar = _package.Bar.extend({
        /**
         * @constructor
         * @param {BarModel} model Bar model
         * @param {Flot.Axis} axis Flot axis instance
         */
        init: function (model, axis) {
            var self = this;
            
            this.model = model;
            this.axis = axis;
            this.children = [];
            
            this._childShownOnGraphChanged = function () {
                if (!self.isShownOnGraph())
                    $(self).trigger('shownOnGraphChanged');
            };
            
            var children = this.model.getChildren();
            
            for (var i = 0, child; i < children.length; i++) {
                child = new wx.chart.markings.SimpleBar(children[i], axis);
                $(child).bind('shownOnGraphChanged', this._childShownOnGraphChanged);
                this.children.push(child);
            }
        },
        
        /**
         * @returns {bool}
         */
        isExpanded: function () {
            return this.expanded;
        },
        
        /**
         * @returns {bool}
         */
        isShownOnGraph: function () {
            return this.shownOnGraph;
        },
        
        /**
         * @returns {Marking[]}
         */
        getGraphMarkings: function () {
            if (this.isShownOnGraph())
                return this.model.getMarkings();
            else {
                var markings = [];
                
                for (var i = 0; i < this.children.length; i++)
                    markings.push.apply(markings, this.children[i].getGraphMarkings());
                
                return markings;
            }
        },
        
        /**
         * @param {HTMLElement} label Label container
         * @param {HTMLElement} markings Markings container
         * @param {HTMLElement} extra Extra container
         */
        render: function (label, markings, extra) {
            // render self
            this.label = this.renderLabel(label);
            this.markings = this.renderMarkings(markings);
            this.extra = this.renderExtra(extra);
            
            // set bar height
            $([this.label.myContainer,
                    this.markings.myContainer,
                    this.extra.myContainer])
                    .css({
                            height          : '18px' });
            
            // render children
            for (var i = 0; i < this.children.length; i++)
                this.children[i].render(
                        this.label.childContainer,
                        this.markings.childContainer,
                        this.extra.childContainer);
        },
        
        /**
         * @param {HTMLElement} container Container
         * @returns {Object} Properties
         */
        renderLabel: function (container) {
            var self = this;
            
            // create containers
            var myContainer = cc(container, 'div');
            var childContainer = cc(container, 'div');
            $(childContainer)
                    .css({
                            marginLeft      : '12px' });
            
            // create 'show on graph' checkbox
            // delay DOM insertion; IE does not allow input.type assignment on associated nodes
            var showOnGraph = ce('input');
            $(showOnGraph)
                    .attr({
                            type            : 'checkbox',
                            checked         : this.isShownOnGraph() })
                    .css({
                            marginRight     : '6px',
                            verticalAlign   : 'bottom' })
                    .click(
                            function (event) {
                                self.setShownOnGraph(this.checked);
                            })
                    .appendTo(myContainer);
            
            // create 'expand/collapse' clickable
            var expand = cc(myContainer, 'img');
            $(expand)
                    .css({
                            cursor          : 'pointer',
                            marginRight     : '3px',
                            visibility      : this.model.getChildren().length > 0
                                    ? 'visible'
                                    : 'hidden' })
                    .click(
                            function (event) {
                                self.setExpanded(!self.isExpanded());
                            });
            
            // create label
            var label = cc(myContainer, 'span');
            $(label)
                    .html(this.model.getLabel());
            
            // return properties
            return {
                    container       : container,
                    myContainer     : myContainer,
                    childContainer  : childContainer,
                    
                    showOnGraph     : showOnGraph,
                    expand          : expand,
                    label           : label };
        },
        
        /**
         * @param {HTMLElement} container Container
         * @returns {Object} Properties
         */
        renderMarkings: function (container) {
            var self = this;
            
            // create containers
            var myContainer = cc(container, 'div');
            var childContainer = cc(container, 'div');
            $(myContainer)
                    .css({
                            position        : 'relative' });
            
            // create canvas
            // delay DOM insertion; canvas default size would mess up container.clientWidth
            var canvas = ce('canvas');
            $(canvas)
                    .css({
                            width           : 0,
                            height          : 0 })
                    .appendTo(myContainer);
            
            // proxy for canvas mouse events
            var proxy = cc(myContainer, 'div');
            $(proxy)
                    .css({
                            position        : 'absolute' })
                    .mousemove(
                            function (event) {
                                self._onCanvasHover(event);
                            })
                    .mouseout(
                            function (event) {
                                wx.TT();
                            });
            
            // return properties
            return {
                    container       : container,
                    myContainer     : myContainer,
                    childContainer  : childContainer,
                    
                    canvas          : canvas,
                    proxy           : proxy };
        },
        
        /**
         * @param {HTMLElement} container Container
         * @returns {Object} Properties
         */
        renderExtra: function (container) {
            // create containers
            var myContainer = cc(container, 'div');
            var childContainer = cc(container, 'div');
            
            // create extra
            var extra = cc(myContainer, 'span');
            $(extra)
                    .html(this.model.getExtra());
            
            // return properties
            return {
                    container       : container,
                    myContainer     : myContainer,
                    childContainer  : childContainer,
                    
                    extra           : extra };
        },
        
        /**
         * 
         */
        initialize: function () {
            // set markings child container width; IE fails to set clientWidth
            $(this.markings.childContainer)
                    .css({
                            width           : this.markings.container.clientWidth });
            
            // initialize children
            for (var i = 0; i < this.children.length; i++)
                this.children[i].initialize();
            
            // initialize label.showOnGraph
            this.setShownOnGraph(false);
            
            // intialize label.expand
            this.setExpanded(false);
            
            // initialize canvas dimensions
            $(this.markings.canvas)
                    .css({
                            width           : this.markings.canvas.width = this.markings.container.clientWidth,
                            height          : this.markings.canvas.height = 16,
                            margin          : '1px 0' });
            
            // initialize canvas proxy dimensions
            $(this.markings.proxy)
                    .css({
                            top             : this.markings.canvas.offsetTop,
                            left            : this.markings.canvas.offsetLeft,
                            width           : this.markings.canvas.clientWidth,
                            height          : this.markings.canvas.clientHeight });
            
            // initialize excanvas
            if ($.browser.msie)
                G_vmlCanvasManager.initElement(this.markings.canvas);
            
            // paint markings
            this._paintMarkings();
        },
        
        /**
         * @private
         */
        _paintMarkings: function () {
            var p2c = this.axis.p2c;
            var context = this.markings.canvas.getContext('2d');
            
            var markings = this.model.getMarkings();
            
            context.fillStyle = "transparent";
            context.fillRect(0, 0, context.canvas.width, context.canvas.height);
            
            for (var i = 0, x, w; i < markings.length; i++) {
                var marking = markings[i];
                
                x = p2c(marking.xaxis.from);
                
                // if (line marking) stroke with lineWidth; else draw rect;
                if (marking.xaxis.from == marking.xaxis.to) {
                    w = marking.lineWidth || 1;
                    x -= w / 2;
                } else {
                    w = p2c(marking.xaxis.to) - x;
                }
                
                context.fillStyle = marking.color;
                context.fillRect(x, 0, w, context.canvas.height);
            }
        },
        
        /**
         * @param {Boolean} shownOnGraph
         */
        setShownOnGraph: function (shownOnGraph) {
            if (this.shownOnGraph === shownOnGraph)
                return;
            
            this.shownOnGraph = shownOnGraph;
            
            // update label.showOnGraph.checked
            $(this.label.showOnGraph)
                    .attr({
                            checked         : shownOnGraph });
            
            $(this).trigger('shownOnGraphChanged');
        },
        
        /**
         * @param {Boolean} expanded
         */
        setExpanded: function (expanded) {
            if (this.expanded === expanded)
                return;
            
            this.expanded = expanded;
            
            // update label.expand.src
            $(this.label.expand)
                    .attr({
                            src         : '/media/images/arrows/9x9-ccc-' + (expanded ? 'down' : 'right') + '.png' });
            
            // set childContainer visibilities
            $([this.label.childContainer,
                    this.markings.childContainer,
                    this.extra.childContainer])
                    .css({
                            display     : expanded ? 'block' : 'none' });
        },
        
        /**
         * @private
         * @param {int} x Canvas X coordinate
         * @returns {Marking[]} Near markings
         */
        _getMarkingsNear: function (x) {
            var c2p = this.axis.c2p;
            
            var from = c2p(x - 2);
            var to = c2p(x + 2);
            
            var rs = [];
            var markings = this.model.getMarkings();
            
            for (var i = 0; i < markings.length; i++)
                if (to >= markings[i].xaxis.from && from <= markings[i].xaxis.to)
                    rs.push(markings[i]);
            
            return rs;
        },
        
        /**
         * @private
         * @param {jQuery.Event} event Event
         */
        _onCanvasHover: function (event) {
            var offsets = $(event.target).offset();
            var canvasX = event.pageX - offsets.left;
            
            var markings = this._getMarkingsNear(canvasX);
            
            if (markings.length > 0) {
                var html = [];
                
                for (var i = 0; i < markings.length; i++)
                    html.push(['<div>', '</div>'].join(markings[i].renderer()));
                
                wx.TT(event, html.join('\n'));
            } else
                wx.TT();
        }
    });
    
    /**
     * @abstract
     * @class
     */
    _package.AbstractBarPanel = _package.BarPanel.extend({
        /**
         * @constructor
         * @param {HTMLElement} container Container
         * @param {Flot} plot Plot
         * @param {Object} options_ Options [offsetLeft]
         */
        init: function (container, plot, options_) {
            var self = this;
            
            this.container = $(container).get(0);
            this.plot = plot;
            
            this.bars = [];
            this.rows = [];
            
            setOptions(this, options_, {
                    offsetLeft      : 0 });
            
            this._graphMarkingsChanged = function () {
                $(self).trigger('graphMarkingsChanged');
            };
        },
        
        /**
         * @param {BarModel} model Bar model
         */
        add: function (model) {
            // create bar and render target
            var bar = new wx.chart.markings.SimpleBar(model, this.plot.getAxes().xaxis);
            var target = this._createRow(bar);
            
            this.bars.push(bar);
            this.rows.push(target.row);
            
            // setup events
            $(bar).bind('shownOnGraphChanged', this._graphMarkingsChanged);
            
            // render bar
            bar.render(target.left, target.middle, target.right);
            ac(this.container, target.row);
            bar.initialize();
        },
        
        /**
         * @param {Bar} bar Bar
         */
        remove: function (bar) {
            var i;
            
            // find bar index
            for (var i = 0; i < this.bars.length; i++)
                if (this.bars[i] === bar)
                    break;
            
            // bail if not found
            if (i === undefined)
                return null;
            
            var bar = this.bars[i];
            var row = this.rows[i];
            
            // remove from DOM
            $(row).remove();
            
            // unbind event handlers
            $(bar).unbind();
            
            // remove from internals
            this.bars.splice(i, 1);
            this.rows.splice(i, 1);
            
            this._graphMarkingsChanged();
            
            return bar;
        },
        
        /**
         * @returns {Marking[]}
         */
        getGraphMarkings: function () {
            var markings = [];
            
            for (var i = 0; i < this.bars.length; i++)
                markings.push.apply(markings, this.bars[i].getGraphMarkings());
            
            return markings;
        },
        
        /**
         * @abstract
         * @param {Bar} bar Bar
         * @returns {Object} Row properties
         */
        _createRow: function () { _abstract }
    });
    
    /**
     * @class
     */
    _package.TabularBarPanel = _package.AbstractBarPanel.extend({
        /**
         * @param {Bar} bar Bar
         * @returns {Object} Row properties
         */
        _createRow: function (bar) {
            var self = this;
            
            // create row, cells and containers
            var row = ce('tr');
            var cells = [];
            var containers = [];
            
            for (var i = 0; i < 4; i++) {
                cells[i] = cc(row, 'td');
                containers[i] = cc(cells[i], 'div');
            }
            
            // set cell and container widths
            var width = this.plot.getCanvas().width;
            var offsets = this.plot.getPlotOffset();
            
            $([cells[0], containers[0]]).css('width', this.offsetLeft + offsets.left);
            $([cells[1], containers[1]]).css('width', width - offsets.left - offsets.right);
            $([cells[3], containers[3]]).css('width', '20px');
            
            // prevent cells and containers from overflowing
            for (var i = 0; i < cells.length; i++) {
                $(cells[i])
                        .css({
                                padding         : 0,
                                verticalAlign   : 'top' });
                
                $(containers[i])
                        .css({
                                overflow        : 'hidden',
                                whiteSpace      : 'nowrap' });
            }
            
            // miscellaneous styles
            $(cells[3])
                    .css({
                            textAlign       : 'right' });
            
            // create remove button
            var remove = cc(containers[3], 'img');
            $(remove)
                    .attr({
                            src         : '/media/images/icons/cross.png' })
                    .click(
                            function (event) {
                                self.remove(bar);
                            });
            
            // return properties
            return {
                    row             : row,
                    
                    left            : containers[0],
                    middle          : containers[1],
                    right           : containers[2] };
        }
    });
    
    /**
     * @class
     */
    _package.FloatingBarPanel = _package.AbstractBarPanel.extend({
        /**
         * @param {Bar} bar Bar
         * @returns {Object} Row properties
         */
        _createRow: function (bar) {
            var self = this;
            
            // create row and containers
            var row = ce('div');
            var containers = [];
            
            for (var i = 0; i < 4; i++)
                containers[i] = cc(row, 'div');
            
            // set container widths
            var width = this.plot.getCanvas().width;
            var offsets = this.plot.getPlotOffset();
            
            $(containers[0])
                    .css({
                            width           : this.offsetLeft + offsets.left,
                            'float'         : 'left' });
            $(containers[1])
                    .css({
                            width           : width - offsets.left - offsets.right,
                            'float'         : 'left' });
            $(containers[2])
                    .css({
                            width           : 20,
                            'float'         : 'right' });
            
            // prevent containers from overflowing
            for (var i = 0; i < containers.length; i++)
                $(containers[i])
                        .css({
                                overflow        : 'hidden',
                                whiteSpace      : 'nowrap' });
            
            
            // allow containers to overflow when mouse is over them
            $([containers[0], containers[3]])
                    .mouseover(
                            function (event) {
                                $(this).css('overflow', 'visible');
                            })
                    .mouseout(
                            function (event) {
                                $(this).css('overflow', 'hidden');
                    });
            
            // create remove button
            var remove = cc(containers[2], 'img');
            $(remove)
                    .attr({
                            src         : '/media/images/icons/cross.png' })
                    .click(
                            function (event) {
                                self.remove(bar);
                            });
            
            // return properties
            return {
                    row             : row,
                    
                    left            : containers[0],
                    middle          : containers[1],
                    right           : containers[3] };
        }
    });
})();

