/**
 * Cartesian sprite.
 */
Ext.define('Ext.chart.series.sprite.Cartesian', {
    extend: 'Ext.chart.series.sprite.Series',
 
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [selectionTolerance=20]
                 * The distance from the event position to the sprite's data points to trigger interactions (used for 'iteminfo', etc).
                 */
                selectionTolerance: 'number',
 
                /**
                 * @cfg {Boolean} flipXY If flipXY is 'true', the series is flipped.
                 */
                flipXY: 'bool',
 
                renderer: 'default',
 
                // Visible range of data (pan/zoom) information.
                visibleMinX: 'number',
                visibleMinY: 'number',
                visibleMaxX: 'number',
                visibleMaxY: 'number',
                innerWidth: 'number',
                innerHeight: 'number'
            },
            defaults: {
                selectionTolerance: 20,
                flipXY: false,
                renderer: null,
                transformFillStroke: false,
 
                visibleMinX: 0,
                visibleMinY: 0,
                visibleMaxX: 1,
                visibleMaxY: 1,
                innerWidth: 1,
                innerHeight: 1
            },
            triggers: {
                dataX: 'dataX,bbox',
                dataY: 'dataY,bbox',
                visibleMinX: 'panzoom',
                visibleMinY: 'panzoom',
                visibleMaxX: 'panzoom',
                visibleMaxY: 'panzoom',
                innerWidth: 'panzoom',
                innerHeight: 'panzoom'
            },
            updaters: {
                dataX: function (attr) {
                    this.processDataX();
                    this.scheduleUpdater(attr, 'dataY', ['dataY']);
                },
 
                dataY: function () {
                    this.processDataY();
                },
 
                panzoom: function (attr) {
                    // dx, dy are deltas between min & max of coordinated data values.
                    var dx = attr.visibleMaxX - attr.visibleMinX,
                        dy = attr.visibleMaxY - attr.visibleMinY,
                        innerWidth = attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        innerHeight = !attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        surface = this.getSurface(),
                        isRtl = surface ? surface.getInherited().rtl : false;
 
                    attr.scalingCenterX = 0;
                    attr.scalingCenterY = 0;
                    attr.scalingX = innerWidth / dx;
                    attr.scalingY = innerHeight / dy;
                    // (attr.visibleMinY * attr.scalingY) will be the vertical position of
                    // our minimum data points, which we want to be at zero, so we offset
                    // by this amount.
                    attr.translationX = -(attr.visibleMinX * attr.scalingX);
                    attr.translationY = -(attr.visibleMinY * attr.scalingY);
 
                    if (isRtl && !attr.flipXY) {
                        attr.scalingX *= -1;
                        attr.translationX *= -1;
                        attr.translationX += innerWidth;
                    }
 
                    this.applyTransformations(true);
                }
            }
        }
    },
 
    processDataY: Ext.emptyFn,
 
    processDataX: Ext.emptyFn,
 
    updatePlainBBox: function (plain) {
        var attr = this.attr;
 
        plain.x = attr.dataMinX;
        plain.y = attr.dataMinY;
        plain.width = attr.dataMaxX - attr.dataMinX;
        plain.height = attr.dataMaxY - attr.dataMinY;
    },
 
    /**
     * Does a binary search of the data on the x-axis using the given key.
     * @param {String} key 
     * @return {*} 
     */
    binarySearch: function (key) {
        var dx = this.attr.dataX,
            start = 0,
            end = dx.length;
 
        if (key <= dx[0]) {
            return start;
        }
        if (key >= dx[end - 1]) {
            return end - 1;
        }
 
        while (start + 1 < end) {
            var mid = (start + end) >> 1,
                val = dx[mid];
            if (val === key) {
                return mid;
            } else if (val < key) {
                start = mid;
            } else {
                end = mid;
            }
        }
 
        return start;
    },
 
    render: function (surface, ctx, surfaceClipRect) {
        var me = this,
            attr = me.attr,
            margin = 1, // TODO: why do we need it?
            inverseMatrix = attr.inverseMatrix.clone();
 
        // The sprite's `attr.matrix` is stretching/shrinking data coordinates
        // to surface coordinates.
        // This matrix is set (indirectly) by the 'panzoom' updater.
        // The sprite's `attr.inverseMatrix` does the opposite.
        //
        // The `surface.matrix` of the 'series' surface of a cartesian chart flips the
        // surface content vertically, so that y=0 is at the bottom (look for
        // `surface.matrix.set` call in the CartesianChart.performLayout method).
        // This matrix is set in the 'performLayout' of the CartesianChart.
        // The `surface.inverseMatrix` flips the content back.
        //
        // By combining the inverse matrices of the series surface and the series sprite,
        // we essentially get a transformation that allows us to go from surface coordinates
        // in a final flipped drawing back to data points.
        //
        // For example
        //
        //     inverseMatrix.transformPoint([ 0, rect[3] ])
        //     inverseMatrix.transformPoint([ rect[2], 0 ])
        //
        // will return
        //
        //     [attr.dataMinX, attr.dataMinY]
        //     [attr.dataMaxX, attr.dataMaxY]
        //
        // because left/bottom and top/right of the series surface is where the first smallest
        // and last largest data points would be (given no pan/zoom), respectively.
        //
        // So the `dataClipRect` passed to the `renderClipped` call below is effectively
        // the visible rect in data (not surface!) coordinates.
 
        // It is important to note, that the all the scaling and translation is defined
        // by the sprite's matrix, the 'series' surface matrix does not contain scaling
        // or translation components, except for the vertical flipping.
 
        // This is important because there is a common pattern in chart series sprites
        // (MarkerHolders) - instead of using transform attributes for their Markers
        // (e.g. instances of a 'rect' sprite in case of 'bar' series), the attributes
        // that would position a sprite with no transformations are transformed.
 
        // For example, to draw a rect with coordinates TL(10, 10), BR(20, 40),
        // we could use the folling 'rect' sprite attributes:
        //
        //     {
        //         x: 0,
        //         y: 0
        //         width: 10,
        //         height: 30
        //
        //         translationX: 10,
        //         translationY: 10
        //
        // But the correct thing to do here is
        //
        //    {
        //        x: 10,
        //        y: 10,
        //        width: 10,
        //        height: 30
        //    }
        //
        // Similarly, if the sprite was scaled, the 'x', 'y', 'width', 'height' attributes
        // would have to account for that as well.
        //
        // This is done, so that the attribute values a marker gets by the time it renders,
        // are the final values, and are not affected later by other transforms, such as
        // surface matrix scaling, which could ruin the visual result, if the attributes
        // values are doctored to make lines align to the pixel grid (which is typically
        // the case).
 
        inverseMatrix.appendMatrix(surface.inverseMatrix);
 
        if (attr.dataX === null || attr.dataX === undefined) {
            return;
        }
        if (attr.dataY === null || attr.dataY === undefined) {
            return;
        }
        if (inverseMatrix.getXX() * inverseMatrix.getYX() || inverseMatrix.getXY() * inverseMatrix.getYY()) {
            Ext.Logger.warn('Cartesian Series sprite does not support rotation/sheering');
            return;
        }
 
        var dataClipRect = inverseMatrix.transformList([
            [surfaceClipRect[0] - margin, surfaceClipRect[3] + margin],  // (left, height)
            [surfaceClipRect[0] + surfaceClipRect[2] + margin, -margin]  // (width, top)
        ]);
 
        dataClipRect = dataClipRect[0].concat(dataClipRect[1]);
 
        // TODO: RTL improvements:
        // TODO: produce such a dataClipRect here, so that we don't have to do:
        // TODO: min = Math.min(dataClipRect[0], dataClipRect[2])
        // TODO: max = Math.max(dataClipRect[0], dataClipRect[2])
        // TODO: inside each 'renderClipped' call
 
        me.renderClipped(surface, ctx, dataClipRect, surfaceClipRect);
    },
 
    /**
     * Render the given visible clip range.
     * @param {Ext.draw.Surface} surface A draw container surface.
     * @param {CanvasRenderingContext2D} ctx A context object that is API compatible with the native
     * [CanvasRenderingContext2D](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D).
     * @param {Number[]} dataClipRect The clip rect in data coordinates, roughly equivalent to
     * [attr.dataMinX, attr.dataMinY, attr.dataMaxX, attr.dataMaxY] for an untranslated/unscaled surface/sprite.
     * @param {Number[]} surfaceClipRect The clip rect in surface coordinates: [left, top, width, height].
     * @method
     */
    renderClipped: Ext.emptyFn,
 
    /**
     * Get the nearest item index from point (x, y). -1 as not found.
     * @param {Number} x 
     * @param {Number} y 
     * @return {Number} The index
     * @deprecated 6.5.2 Use {@link #getNearestDataPoint} instead.
     */
    getIndexNearPoint: function (x, y) {
        var result = this.getNearestDataPoint(x, y);
        return result ? result.index : -1;
    },
 
    /**
     * Given a point in 'series' surface element coordinates, returns the `index` of the
     * sprite's data point that is nearest to that point, along with the `distance`
     * between points.
     * If the `selectionTolerance` attribute of the sprite is not zero, only the data points
     * that are within that pixel distance from the given point will be checked.
     * In the event no such data points exist or the data is empty, `null` is returned.
     *
     * Notes:
     * 1) given a mouse/pointer event object, the surface coordinates of the event can be
     *    obtained with the `getEventXY` method of the chart;
     * 2) using `selectionTolerance` of zero is useful for series with no visible markers,
     *    such as the Area series, where this attribute becomes meaningless.
     *
     * @param {Number} x 
     * @param {Number} y 
     * @return {Object} 
     */
    getNearestDataPoint: function (x, y) {
        var me = this,
            attr = me.attr,
            series = me.getSeries(),
            surface = me.getSurface(),
            items = me.boundMarkers.items,
            matrix = attr.matrix,
            dataX = attr.dataX,
            dataY = attr.dataY,
            selectionTolerance = attr.selectionTolerance,
            minDistance = Infinity,
            index = -1,
            result = null,
            distance, dx, dy,
            xy, i, ln, end, inc;
 
        // Notes:
        // Instead of converting the given point from surface coordinates to data coordinates
        // and then measuring the distances between it and the data points, we have to
        // convert all the data points to surface coordinates and measure the distances
        // between them and the given point. This is because the data coordinates can use
        // different scales, which makes distance measurement impossible.
        // For example, if the x-axis is a `category` axis, the categories will be assigned
        // indexes starting from 0, that's what the `attr.dataX` array will contain;
        // and if the y-axis is a `numeric` axis, the `attr.dataY` array will simply contain
        // the original values.
        //
        // Either 'items' or 'markers' will be highlighted. If a sprite has both (for example,
        // 'bar' series with the 'marker' config, where the bars are 'items' and marker instances
        // are 'markers'), only the 'items' (bars) will be highlighted.
 
        if (items) {
            ln = dataX.length;
            if (series.reversedSpriteZOrder) {
                i = ln - 1;
                end = -1;
                inc = -1;
            } else {
                i = 0;
                end = ln;
                inc = 1;
            }
            for (; i !== end; i += inc) {
                var bbox = me.getMarkerBBox('items', i);
                // Transform the given surface element coordinates to logical coordinates
                // of the surface (the ones the bbox uses).
                xy = surface.inverseMatrix.transformPoint([x, y]);
                if (Ext.draw.Draw.isPointInBBox(xy[0], xy[1], bbox)) {
                    index = i;
                    minDistance = 0;
                    // Return the first item that contains our touch point.
                    break;
                }
            }
        } else { // markers
            for (= 0, ln = dataX.length; i < ln; i++) {
                // Convert from data coordinates to coordinates within inner size rectangle.
                // See `panzoom` method for more details.
                xy = matrix.transformPoint([dataX[i], dataY[i]]);
                // Flip back vertically and padding adjust (see `render` method comments).
                xy = surface.matrix.transformPoint(xy);
                // Essentially sprites go through the same two transformations when they render
                // data points.
 
                dx = x - xy[0];
                dy = y - xy[1];
 
                distance = Math.sqrt(dx * dx + dy * dy);
 
                if (selectionTolerance && distance > selectionTolerance) {
                    continue;
                }
 
                if (distance < minDistance) {
                    minDistance = distance;
                    index = i;
                    // Keep looking for the nearest marker.
                }
            }
        }
 
        if (index > -1) {
            result = {
                index: index,
                distance: minDistance
            };
        }
 
        return result;
    }
 
});