/**
 * @class Ext.chart.Legend
 *
 * Defines a legend for a chart's series.
 * The 'chart' member must be set prior to rendering.
 * The legend class displays a list of legend items each of them related with a
 * series being rendered. In order to render the legend item of the proper series
 * the series configuration object must have {@link Ext.chart.Series#showInLegend showInLegend}
 * set to true.
 *
 * The legend configuration object accepts a {@link #position} as parameter, which allows
 * control over where the legend appears in relation to the chart. The position can be
 * confiured with different values for portrait vs. landscape orientations. Also, the {@link #dock}
 * config can be used to hide the legend in a sheet docked to one of the sides.
 *
 * Full example:
    <pre><code>
    var store = new Ext.data.JsonStore({
        fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
        data: [
            {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
            {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
            {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
            {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
            {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
        ]
    });

    new Ext.chart.Chart({
        renderTo: Ext.getBody(),
        width: 500,
        height: 300,
        animate: true,
        store: store,
        shadow: true,
        theme: 'Category1',
        legend: {
            position: 'top'
        },
         axes: [{
                type: 'Numeric',
                grid: true,
                position: 'left',
                fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
                title: 'Sample Values',
                grid: {
                    odd: {
                        opacity: 1,
                        fill: '#ddd',
                        stroke: '#bbb',
                        'stroke-width': 1
                    }
                },
                minimum: 0,
                adjustMinimumByMajorUnit: 0
            }, {
                type: 'Category',
                position: 'bottom',
                fields: ['name'],
                title: 'Sample Metrics',
                grid: true,
                label: {
                    rotate: {
                        degrees: 315
                    }
                }
        }],
        series: [{
            type: 'area',
            highlight: false,
            axis: 'left',
            xField: 'name',
            yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
            style: {
                opacity: 0.93
            }
        }]
    });
    </code></pre>
 *
 */
Ext.chart.Legend = Ext.extend(Ext.util.Observable, {

    /**
     * @cfg {Boolean} visible
     * Whether or not the legend should be displayed.
     */
    visible: true,

    /**
     * @cfg {String} position
     * The position of the legend in relation to the chart. Can be one of:
     *
     * -  "top" - positions the legend centered at the top of the chart
     * -  "bottom" - positions the legend centered at the bottom of the chart
     * -  "left" - positions the legend centered on the left side of the chart
     * -  "right" - positions the legend centered on the right side of the chart
     * -  an Object with numeric properties `x` and `y`, and boolean property `vertical` - displays the legend
     *    floating on top of the chart at the given x/y coordinates. If `vertical:true` the legend items will
     *    be arranged stacked vertically, otherwise they will be arranged side-by-side. If {@link #dock} is
     *    set to `true` then this position config will be ignored and will dock to the bottom.
     *
     * In addition, you can specify different legend alignments based on the orientation of the browser viewport,
     * for instance you might want to put the legend on the right in landscape orientation but on the bottom in
     * portrait orientation. To achieve this, you can set the `position` config to an Object with `portrait` and
     * `landscape` properties, and set the value of those properties to one of the recognized value types described
     * above. For example, the following config will put the legend on the right in landscape but float it on top
     * of the chart at position 10,10 in portrait:
     *
     *     legend: {
     *         position: {
     *             landscape: 'right',
     *             portrait: {
     *                 x: 10,
     *                 y: 10,
     *                 vertical: true
     *             }
     *         }
     *     }
     */
    position: 'bottom',

    /**
     * @cfg {Boolean} dock
     * If set to `true`, then rather than rendering within the chart area the legend will be docked to the
     * {@link #position configured edge position} within a {@link Ext.Sheet}. The sheet will be initially
     * hidden and can be opened by tapping on a tab along the configured edge. This prevents screen real
     * estate from being taken up by the legend, which is especially important on small screen devices.
     *
     * Defaults to `true` for phone-sized screens, `false` for larger screens.
     */
    dock: Ext.is.Phone,

    /**
     * @cfg {Number} doubleTapThreshold
     * The duration in milliseconds in which two consecutive taps will be considered a doubletap.
     * Defaults to `250`.
     */
    doubleTapThreshold: 250,

    /**
     * @constructor
     * @param {Object} config
     */
    constructor: function(config) {
        var me = this,
            chart = config.chart,
            chartEl = chart.el,
            button, sheet, view, transitions, sheetAnim;

        me.addEvents(
            /**
             * @event combine
             * Fired when two legend items are combined together via drag-drop.
             * @param {Ext.chart.Legend} legend
             * @param {Ext.chart.series.Series} series The series owning the items being combined
             * @param {Number} index1 The index of the first legend item
             * @param {Number} index2 The index of the second legend item
             */
            'combine',

            /**
             * @event split
             * Fired when a previously-combined legend item is split into its original constituent items.
             * @param {Ext.chart.Legend} legend
             * @param {Ext.chart.series.Series} series The series owning the item being split
             * @param {Number} index The index of the legend item being split
             */
            'split'
        );

        Ext.chart.Legend.superclass.constructor.call(me, config);

        view = me.getView();
        if (me.dock) {
            // Legend is docked; create the sheet and trigger button
            button = me.button = chart.getToolbar().add({
                showAnimation: 'fade',
                cls: Ext.baseCSSPrefix + 'legend-button',
                iconCls: Ext.baseCSSPrefix + 'legend-button-icon',
                iconMask: true,
                handler: function() {
                    me.sheet.show();
                }
            });
            button.show();

            transitions = {
                bottom : 'up',
                top    : 'down',
                right  : 'left',
                left   : 'right'
            };

            sheetAnim = {
                type: 'slide',
                duration: 150,
                direction: transitions[me.getPosition()]
            };

            sheet = me.sheet = new Ext.Sheet({
                enter: me.getPosition(),
                stretchY: true,
                stretchX: true,
                ui: 'legend',
                hideOnMaskTap: true,
                enterAnimation: sheetAnim,
                exitAnimation: sheetAnim,
                width: 200,
                height: 260,
                renderTo: chartEl,
                layout: 'fit',
                items: view,
                listeners: {
                    // If user swipes in direction sheet came from, close it
                    // Only works for side-positioned labels (otherwise could just be scrolling legend list)
                    swipe: {
                        element: 'el',
                        fn: function(gesture){
                            if (gesture.direction == me.getPosition()) {
                                me.sheet.hide();
                            }
                        }
                    }
                }
            });
        } else {
            // Not docked; render view directly into chart container
            view.render(chartEl);
        }

        if (me.isDisplayed()) {
            me.show();
        }
    },

    /**
     * Retrieves the view component for this legend, creating it first if needed.
     * @return {Ext.chart.Legend.View}
     */
    getView: function() {
        var me = this;
        return me.view || (me.view = new Ext.chart.Legend.View({
            legend: me,
            floating: !me.dock
        }));
    },

    destroy: function() {
        var me = this;
        if (me.view) {
            me.view.destroy();
        }
    },

    /**
     * @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config,
     * and also the 'showInLegend' config for each of the series.
     * @return {Boolean}
     */
    isDisplayed: function() {
        return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1;
    },

    /**
     * Returns whether the legend is configured with orientation-specific positions.
     * @return {Boolean}
     */
    isOrientationSpecific: function() {
        var position = this.position;
        return (Ext.isObject(position) && 'portrait' in position);
    },

    /**
     * Get the target position of the legend, after resolving any orientation-specific configs.
     * In most cases this method should be used rather than reading the `position` property directly.
     * @return {String/Object} The position config value
     */
    getPosition: function() {
        var me = this,
            position = me.position;
        // Grab orientation-specific config if specified
        if (me.isOrientationSpecific()) {
            position = position[Ext.getOrientation()];
        }
        // If legend is docked, default non-String values to 'bottom'
        if (me.dock && !Ext.isString(position)) {
            position = 'bottom';
        }
        return position;
    },

    /**
     * Returns whether the orientation of the legend items is vertical.
     * @return {Boolean} `true` if the legend items are to be arranged stacked vertically, `false` if they
     * are to be arranged side-by-side.
     */
    isVertical: function() {
        var position = this.getPosition();
        return this.dock || (Ext.isObject(position) ? position.vertical : "left|right|float".indexOf('' + position) !== -1);
    },

    /**
     * Update the legend component to match the current viewport orientation.
     */
    orient: function() {
        var me = this,
            sheet = me.sheet,
            position = me.getPosition(),
            orientation = Ext.getOrientation(),
            auto = 'auto';

        me.getView().orient();

        if (me.lastOrientation !== orientation) {
            if (sheet) {
                sheet.hide();
                sheet.enter = sheet.exit = position;
                sheet.setSize(null, null);
                sheet.orient();
            }

            me.lastOrientation = orientation;
        }
    },

    /**
     * @private Update the position of the legend if it is displayed and not docked.
     */
    updatePosition: function() {
        if (!this.dock) {
            var me = this,
                chart = me.chart,
                chartBBox = chart.chartBBox,
                insets = chart.insetPadding,
                isObject = Ext.isObject(insets),
                insetLeft = (isObject ? insets.left : insets) || 0,
                insetRight = (isObject ? insets.right : insets) || 0,
                insetBottom = (isObject ? insets.bottom : insets) || 0,
                insetTop = (isObject ? insets.top : insets) || 0,
                chartWidth = chart.curWidth,
                chartHeight = chart.curHeight,
                seriesWidth = chartBBox.width - (insetLeft + insetRight),
                seriesHeight = chartBBox.height - (insetTop + insetBottom),
                chartX = chartBBox.x + insetLeft,
                chartY = chartBBox.y + insetTop,
                isVertical = me.isVertical(),
                view = me.getView(),
                math = Math,
                mfloor = math.floor,
                mmin = math.min,
                mmax = math.max,
                x, y, legendWidth, legendHeight, maxWidth, maxHeight, position, undef;

            if (me.sheet) {
                return; //only set position if view is directly floated
            }

            if (me.isDisplayed()) {
                // Calculate the natural size
                view.show();
                view.setCalculatedSize(isVertical ? undef : null, isVertical ? null : undef); //clear fixed scroller length
                legendWidth = view.getWidth();
                legendHeight = view.getHeight();

                position = me.getPosition();
                if (Ext.isObject(position)) {
                    // Object with x/y properties: use them directly
                    x = position.x;
                    y = position.y;
                } else {
                    // Named positions - calculate x/y based on chart dimensions
                    switch(position) {
                        case "left":
                            x = insetLeft;
                            y = mfloor(chartY + seriesHeight / 2 - legendHeight / 2);
                            break;
                        case "right":
                            x = mfloor(chartWidth - legendWidth) - insetRight;
                            y = mfloor(chartY + seriesHeight / 2 - legendHeight / 2);
                            break;
                        case "top":
                            x = mfloor(chartX + seriesWidth / 2 - legendWidth / 2);
                            y = insetTop;
                            break;
                        default:
                            x = mfloor(chartX + seriesWidth / 2 - legendWidth / 2);
                            y = mfloor(chartHeight - legendHeight) - insetBottom;
                    }
                    x = mmax(x, insetLeft);
                    y = mmax(y, insetTop);
                }

                maxWidth = chartWidth - x - insetRight;
                maxHeight = chartHeight - y - insetBottom;

                view.setPosition(x, y);
                if (legendWidth > maxWidth || legendHeight > maxHeight) {
                    view.setCalculatedSize(mmin(legendWidth, maxWidth), mmin(legendHeight, maxHeight));
                }
            } else {
                view.hide();
            }
        }
    },

    /**
     * Calculate and return the number of pixels that should be reserved for the legend along
     * its edge. Only returns a non-zero value if the legend is positioned to one of the four
     * named edges, and if it is not {@link #dock docked}.
     */
    getInsetSize: function() {
        var me = this,
            pos = me.getPosition(),
            chartPadding = me.chart.insets,
            left = chartPadding.left,
            bottom = chartPadding.bottom,
            top = chartPadding.top,
            right = chartPadding.right,
            size = 0,
            view;

        if (!me.dock && me.isDisplayed()) {
            view = me.getView();
            view.show();
            if (pos === 'left' || pos === 'right') {
                size = view.getWidth() + left;
            }
            else if (pos === 'top' || pos === 'bottom') {
                size = view.getHeight() + top;
            }
        }
        return size;
    },

    /**
     * Shows the legend if it is currently hidden.
     */
    show: function() {
        (this.sheet || this.getView()).show();
    },

    /**
     * Hides the legend if it is currently shown.
     */
    hide: function() {
        (this.sheet || this.getView()).hide();
    },

    /**
     * @protected Fired when two legend items are combined via drag-drop in the legend view.
     * @param {Ext.chart.series.Series} series The series for the combined items
     * @param {Ext.chart.series.Series} index1 The series for the combined items
     * @param {Ext.chart.series.Series} index2 The series for the combined items
     */
    onCombine: function(series, index1, index2) {
        var me = this;
        series.combine(index1, index2);
        me.getView().updateStore();
        me.fireEvent('combine', me, series, index1, index2);
    },

    onSplit: function(series, index) {
        var me = this;
        series.split(index);
        me.getView().updateStore();
        me.fireEvent('split', me, series, index);
    },

    /**
     * Reset the legend back to its initial state before any user interactions.
     */
    reset: function() {
        this.getView().reset();
    }
});


/**
 * @class Ext.chart.Legend.View
 * @extends Ext.DataView
 *
 * A DataView specialized for displaying the legend items for a chart. This class is only
 * used internally by {@link Ext.chart.Legend} and should not need to be instantiated directly.
 */
Ext.chart.Legend.View = Ext.extend(Ext.DataView, {
    tpl: [
        '<ul class="' + Ext.baseCSSPrefix + 'legend-items">',
            '<tpl for=".">',
                '<li class="' + Ext.baseCSSPrefix + 'legend-item <tpl if="disabled">' + Ext.baseCSSPrefix + 'legend-inactive' + '</tpl>">',
                    '<span class="' + Ext.baseCSSPrefix + 'legend-item-marker" style="background-color:{markerColor};"></span>{label}',
                '</li>',
            '</tpl>',
        '</ul>'
    ],

    disableSelection: true,
    componentCls: Ext.baseCSSPrefix + 'legend',
    horizontalCls: Ext.baseCSSPrefix + 'legend-horizontal',
    inactiveItemCls: Ext.baseCSSPrefix + 'legend-inactive',
    itemSelector: '.' + Ext.baseCSSPrefix + 'legend-item',
    hideOnMaskTap: false,
    triggerEvent: 'tap',

    initComponent: function() {
        var me = this;
        me.createStore();
        Ext.chart.Legend.View.superclass.initComponent.call(me);
        me.on('refresh', me.updateDroppables, me);
    },

    initEvents: function() {
        var me = this;
        Ext.chart.Legend.View.superclass.initEvents.call(me);
        me.el.on('taphold', me.onTapHold, me, {delegate: me.itemSelector});
},

    /**
     * @private Fired when a legend item is tap-held. Initializes a draggable for the
     * held item.
     */
    onTapHold: function(e, target) {
        var me = this,
            draggable, record, seriesId, combinable;

        if (!Ext.fly(target).hasCls(me.inactiveItemCls)) {
            record = me.getRecord(target);
            seriesId = record.get('seriesId');
            combinable = me.store.findBy(function(record2) {
                return record2 !== record && record2.get('seriesId') === seriesId;
            });
            if (combinable > -1) {
                draggable = new Ext.util.Draggable(target, {
                    threshold: 0,
                    revert: true,
                    direction: me.legend.isVertical() ? 'vertical' : 'horizontal',
                    group: seriesId
                });

                draggable.on('dragend', me.onDragEnd, me);

                if (!draggable.dragging) {
                    draggable.onStart(e);
                }
            }
        }
    },

    /**
     * @private Updates the droppable objects for each list item. Should be called whenever
     * the list view is re-rendered.
     */
    updateDroppables: function() {
        var me = this,
            droppables = me.droppables,
            droppable;

        Ext.destroy(droppables);

        droppables = me.droppables = [];
        me.store.each(function(record) {
            droppable = new Ext.chart.Legend.Droppable(me.getNode(record), {
                group: record.get('seriesId'),
                disabled: record.get('disabled')
            });

            droppable.on('drop', me.onDrop, me);

            droppables.push(droppable);
        });
    },

    /**
     * @private Handles dropping one legend item on another.
     */
    onDrop: function(droppable, draggable) {
        var me = this,
            dragRecord = me.getRecord(draggable.el.dom),
            dropRecord = me.getRecord(droppable.el.dom);
        me.legend.onCombine(dragRecord.get('series'), dragRecord.get('index'), dropRecord.get('index'));
    },

    onDragEnd : function(draggable, e) {
        draggable.destroy();
    },

    /**
     * @private Create the internal data store for the view
     */
    createStore: function() {
        var me = this;

        me.store = new Ext.data.Store({
            fields: ['markerColor', 'label', 'series', 'seriesId', 'index', 'disabled'],
            data: me.getStoreData()
        });

        me.legend.chart.series.each(function(series) {
            series.on('titlechange', me.updateStore, me);
        });
    },

    /**
     * @private Create and return the JSON data for the legend's internal data store
     */
    getStoreData: function() {
        var data = [];

        this.legend.chart.series.each(function(series) {
            if (series.showInLegend) {
                Ext.each(series.getLegendLabels(), function(label, i) {
                    data.push({
                        label: label,
                        markerColor: series.getLegendColor(i),
                        series: series,
                        seriesId: Ext.id(series, 'legend-series-'),
                        index: i,
                        disabled: !series.visibleInLegend(i)
                    });
                });
            }
        });

        return data;
    },

    /**
     * Updates the internal store to match the current legend info supplied by all the series.
     */
    updateStore: function() {
        var store = this.store;
        store.suspendEvents(true);
        store.removeAll();
        store.add(this.getStoreData());
        store.resumeEvents();
    },

    /**
     * Update the legend component to match its current vertical/horizontal orientation
     */
    orient: function() {
        var me = this,
            legend = me.legend,
            horizontalCls = me.horizontalCls,
            isVertical = legend.isVertical(),
            orientation = Ext.getOrientation();

        if (isVertical) {
            me.removeCls(horizontalCls);
        } else {
            me.addCls(horizontalCls);
        }

        if (me.lastOrientation !== orientation) {
            me.setCalculatedSize(null, null);

            // Clean up things set by previous scroller -- Component#setScrollable should be fixed to do this
            me.scrollEl.setStyle({
                width: '',
                height: '',
                minWidth: '',
                minHeight: ''
            });
            Ext.iterate(me.scroller.scrollView.indicators, function(axis, indicator) {
                clearTimeout(indicator.hideTimer);
                Ext.destroy(indicator.el);
                delete indicator.el;
            }, this);
            me.scroller.destroy();

            // Re-init scrolling in the correct direction
            me.setScrollable(isVertical ? 'vertical' : 'horizontal');

            if (isVertical) {
                // Fix to the initial natural width so it doesn't expand when items are combined
                me.setCalculatedSize(me.getWidth());
            }
            if (me.scroller) {
                me.scroller.scrollTo({x: 0, y: 0});
            }
            me.lastOrientation = orientation;
        }
    },

    afterComponentLayout: function() {
        var me = this,
            scroller = me.scroller,
            innerSize, outerSize;

        Ext.chart.Legend.View.superclass.afterComponentLayout.apply(me, arguments);

        // Enable or disable scrolling depending on if the legend needs to be scrollable
        if (scroller) {
            innerSize = scroller.size;
            outerSize = scroller.containerBox;
            if (innerSize.width > outerSize.width || innerSize.height > outerSize.height) {
                scroller.enable();
            } else {
                scroller.disable();
            }
        }
    },

    refresh: function() {
        Ext.chart.Legend.View.superclass.refresh.apply(this, arguments);

        // Refresh may decrease the size of the scrollable content; we need to clear minWidth/Height
        // on the scrollEl so it doesn't force the floated view el to keep its old size.
        this.scrollEl.setStyle({
            minWidth: '',
            minHeight: ''
        });
    },

    onItemTap: function(item, i, e) {
        Ext.chart.Legend.View.superclass.onItemTap.apply(this, arguments);

        var me = this,
            record = me.store.getAt(i),
            series = record.get('series'),
            index = record.get('index'),
            threshold = me.legend.doubleTapThreshold,
            tapTask = me.tapTask || (me.tapTask = new Ext.util.DelayedTask()),
            now = +new Date();
        tapTask.cancel();

        // If the tapped item is a combined item, we need to distinguish between single and
        // double taps by waiting a bit; otherwise trigger the single tap handler immediately.
        if (series.isCombinedItem(index)) {
            if (now - (me.lastTapTime || 0) < threshold) {
                me.doItemDoubleTap(item, i);
            }
            else {
                tapTask.delay(threshold, me.doItemTap, me, [item, i]);
            }
            me.lastTapTime = now;
        } else {
            me.doItemTap(item, i);
        }
    },

    /**
     * @private
     * Handle single taps on legend items; toggles the corresponding series items on and off.
     */
    doItemTap: function(item, i) {
        var me = this,
            record = me.store.getAt(i),
            series = record.get('series'),
            index = record.get('index'),
            active = series.visibleInLegend(index),
            droppable = me.droppables[i],
            inactiveCls = me.inactiveItemCls;

        // Set the _index property on the series, this is used by the hideAll and
        // showAll methods for some series to know which legend item to hide/show.
        // This would be cleaner if it were just a passed argument.
        series._index = index;
        if (active) {
            series.hideAll();
            Ext.fly(item).addCls(inactiveCls);
            droppable.disable();
        } else {
            series.showAll();
            Ext.fly(item).removeCls(inactiveCls);
            droppable.enable();
        }

        // Flush rendering of affected surfaces
        series.getSurface().renderFrame();
        series.getOverlaySurface().renderFrame();
        me.legend.chart.axes.each(function(axis) {
            axis.renderFrame();
        });
    },

    /**
     * @private
     * Handle double-taps on legend items; splits items that are a result of item combination
     */
    doItemDoubleTap: function(item, i) {
        var me = this,
            record = me.getRecord(item);
        if (record) {
            me.legend.onSplit(record.get('series'), record.get('index'));
        }
    },

    /**
     * Reset the legend view back to its initial state before any user interactions.
     */
    reset: function() {
        var me = this;
        me.store.each(function(record) {
            var series = record.get('series');
            series._index = record.get('index');
            series.showAll();
            Ext.fly(me.getNode(record)).removeCls(me.inactiveItemCls);
            series.clearCombinations();
        });

        me.updateStore();
    }

});


/**
 * @private
 * @class Ext.chart.Legend.Droppable
 * @extends Ext.util.Droppable
 * Custom Droppable implementation for legend items. Only lets one legend item be active as a
 * drop target at once, using the center point of the draggable.
 */
Ext.chart.Legend.Droppable = Ext.extend(Ext.util.Droppable, {
    isDragOver : function(draggable) {
        var draggableRegion = draggable.region,
            round = Math.round,
            draggableCenter = {
                x: round((draggableRegion.right - draggableRegion.left) / 2 + draggableRegion.left) + 0.5,
                y: round((draggableRegion.bottom - draggableRegion.top) / 2 + draggableRegion.top) + 0.5
            };

        return draggable.el !== this.el && !this.region.isOutOfBound(draggableCenter);
    }
});