/**
 * PanZoomインタラクションを使用すると、ユーザーは、パンおよび/または拡大縮小によって1つ以上のチャート軸のデータをナビゲーションできるようになります。ナビゲーションは特定の軸に制限することができます。拡大縮小は、チャートまたは軸のエリア上のピンチによって実行され、パンは、シングルタッチのドラッグによって実行されます。
 *
 * マルチタッチイベントをサポートしないデバイスでは、拡大縮小はピンチ動作から実行できません。このような場合、インタラクションによって、ユーザーは、同じシングルタッチのドラッグ動作を使用して、拡大縮小とパンの両方を実行できるようになります。{@link #modeToggleButton}は、2つのモードを示し、トグルするボタンを提供します。
 *
 *     @example
 *     Ext.create('Ext.Container', {
 *         renderTo: Ext.getBody(),
 *         width: 600,
 *         height: 400,
 *         layout: 'fit',
 *         items: {
 *             xtype: 'cartesian',
 *             interactions: [{
 *                 type: 'panzoom',
 *                 zoomOnPanGesture: true
 *             }],
 *             store: {
 *               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}
 *               ]
 *             },
 *             axes: [{
 *                 type: 'numeric',
 *                 position: 'left',
 *                 fields: ['data1'],
 *                 title: {
 *                     text: 'Sample Values',
 *                     fontSize: 15
 *                 },
 *                 grid: true,
 *                 minimum: 0
 *             }, {
 *                 type: 'category',
 *                 position: 'bottom',
 *                 fields: ['name'],
 *                 title: {
 *                     text: 'Sample Values',
 *                     fontSize: 15
 *                 }
 *             }],
 *             series: [{
 *                 type: 'line',
 *                 highlight: {
 *                     size: 7,
 *                     radius: 7
 *                 },
 *                 style: {
 *                     stroke: 'rgb(143,203,203)'
 *                 },
 *                 xField: 'name',
 *                 yField: 'data1',
 *                 marker: {
 *                     type: 'path',
 *                     path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
 *                     stroke: 'blue',
 *                     lineWidth: 0
 *                 }
 *             }, {
 *                 type: 'line',
 *                 highlight: {
 *                     size: 7,
 *                     radius: 7
 *                 },
 *                 fill: true,
 *                 xField: 'name',
 *                 yField: 'data3',
 *                 marker: {
 *                     type: 'circle',
 *                     radius: 4,
 *                     lineWidth: 0
 *                 }
 *             }]
 *         }
 *     });
 *
 * `panzoom`インタラクションタイプのコンフィグオブジェクトは、`axes`コンフィグによってナビゲーション可能にする軸を指定することが必要です。可能なフォーマットの詳細については、{@link #axes}コンフィグマニュアルを参照してください。`axes`コンフィグが指定されていない場合、デフォルトで、デフォルトの軸オプションを使用して軸全てをナビゲーション可能にします。
 *
 */
Ext.define('Ext.chart.interactions.PanZoom', {

    extend: 'Ext.chart.interactions.Abstract',

    type: 'panzoom',
    alias: 'interaction.panzoom',
    requires: [
        'Ext.draw.Animator'
    ],

    config: {

        /**
         * @cfg {Object/Array} axes
         * ナビゲーション可能にする軸を指定します。コンフィグ値は次のフォーマットが可能です。
         *
         * - ナビゲーション可能になる各軸の{@link Ext.chart.axis.Axis#position 位置}に対応するキーがあるオブジェクト。各キーの値は、各軸のその他のオプションが設定されているか、またはデフォルトのオプションに対して`true`のいずれかが設定されているオブジェクトです。
         *
         *       {
         *           type: 'panzoom',
         *           axes: {
         *               left: {
         *                   maxZoom: 5,
         *                   allowPan: false
         *               },
         *               bottom: true
         *           }
         *       }
         *
         *   完全なオブジェクトフォームを使用する場合、以下のオプションをそれぞれの軸に指定できます。
         *
         *   - minZoom(数値)軸の最小拡大レベル。デフォルトは`1`で、通常のサイズです。
         *   - maxZoom(数値)軸の最大拡大レベル。デフォルトは`10`です。
         *   - startZoom(数値)軸の開始拡大レベル。デフォルトは`1`です。
         *   - allowZoom(論理値)軸の拡大を可能にするかどうか。デフォルトは`true`です。
         *   - allowPan(論理値)軸のパンを可能にするかどうか。デフォルトは`true`です。
         *   - startPan(論理値)軸の開始パンオフセット。デフォルトは`0`です。
         *
         * - 文字列の配列。それぞれの文字列はナビゲーション可能になる軸の{@link Ext.chart.axis.Axis#position 位置}に対応します。それぞれ指定された軸でデフォルトのオプションが使用されます。
         *
         *       {
         *           type: 'panzoom',
         *           axes: ['left', 'bottom']
         *       }
         *
         * `axes`コンフィグが指定されていない場合、デフォルトで、デフォルトの軸オプションを使用して軸全てをナビゲーション可能にします。
         */
        axes: {
            top: {},
            right: {},
            bottom: {},
            left: {}
        },

        minZoom: null,

        maxZoom: null,

        /**
         * @cfg {Boolean} showOverflowArrows
         * `true`に設定すると、軸がオーバーフローしているので、その方向にパンできることを示すために、各軸のいずれかの端に矢印が条件付で表示されます。`false`に設定すると、矢印は表示されなくなります。
         */
        showOverflowArrows: true,

        /**
         * @cfg {Object} overflowArrowOptions
         * オーバーフロー矢印スプライトのオプションのオプションのオーバーライド。{@link #showOverflowArrows}が`true`の場合にのみ有効です。
         */

        /**
         * @cfg {String} panGesture
         * パンを開始するジェスチャーを定義します。
         * @private
         */
        panGesture: 'drag',

        /**
         * @cfg {String} zoomGesture
         * ズームを開始するジェスチャーを定義します。
         * @private
         */
        zoomGesture: 'pinch',

        /**
         * @cfg {Boolean} zoomOnPanGesture
         * `true`に設定すると、パンのジェスチャーによりチャートがズームされます。タッチデバイスでは無視されます。
         */
        zoomOnPanGesture: false,

        modeToggleButton: {
            xtype: 'segmentedbutton',
            width: 200,
            defaults: { ui: 'default-toolbar' },
            items: [
                {
                    text: 'Pan'
                },
                {
                    text: 'Zoom'
                }
            ],
            cls: Ext.baseCSSPrefix + 'panzoom-toggle'
        },

        hideLabelInGesture: false // Ext.os.is.Android
    },

    stopAnimationBeforeSync: true,

    applyAxes: function (axesConfig, oldAxesConfig) {
        return Ext.merge(oldAxesConfig || {}, axesConfig);
    },

    applyZoomOnPanGesture: function (zoomOnPanGesture) {
        this.getChart();
        if (this.isMultiTouch()) {
            return false;
        }
        return zoomOnPanGesture;
    },

    updateZoomOnPanGesture: function (zoomOnPanGesture) {
        var button = this.getModeToggleButton();
        if (!this.isMultiTouch()) {
            button.show();
            if (zoomOnPanGesture) {
                button.setValue(1);
            } else {
                button.setValue(0);
            }
        } else {
            button.hide();
        }
    },

    toggleMode: function () {
        var me = this;
        if (!me.isMultiTouch()) {
            me.setZoomOnPanGesture(!me.getZoomOnPanGesture());
        }
    },

    applyModeToggleButton: function (button, oldButton) {
        var me = this,
            result = Ext.factory(button, 'Ext.button.Segmented', oldButton);
        if (result && !oldButton) {
            result.addListener('toggle', function (segmentedButton) {
                me.setZoomOnPanGesture(segmentedButton.getValue() === 1);
            });
        }
        return result;
    },

    getGestures: function () {
        var me = this,
            gestures = {},
            pan = me.getPanGesture(),
            zoom = me.getZoomGesture(),
            isTouch = Ext.supports.Touch;

        gestures[zoom] = 'onZoomGestureMove';
        gestures[zoom + 'start'] = 'onZoomGestureStart';
        gestures[zoom + 'end'] = 'onZoomGestureEnd';
        gestures[pan] = 'onPanGestureMove';
        gestures[pan + 'start'] = 'onPanGestureStart';
        gestures[pan + 'end'] = 'onPanGestureEnd';
        gestures.doubletap = 'onDoubleTap';
        return gestures;
    },

    onDoubleTap: function (e) {
        var me = this,
            chart = me.getChart(),
            axes = chart.getAxes(),
            axis, i, ln;

        for (i = 0, ln = axes.length; i < ln; i++) {
            axis = axes[i];
            axis.setVisibleRange([0, 1]);
        }
        chart.redraw();
    },

    onPanGestureStart: function (e) {
        if (!e || !e.touches || e.touches.length < 2) { //Limit drags to single touch
            var me = this,
                rect = me.getChart().getInnerRect(),
                xy = me.getChart().element.getXY();
            me.startX = e.getX() - xy[0] - rect[0];
            me.startY = e.getY() - xy[1] - rect[1];
            me.oldVisibleRanges = null;
            me.hideLabels();
            me.getChart().suspendThicknessChanged();
            me.lockEvents(me.getPanGesture());
            return false;
        }
    },

    onPanGestureMove: function (e) {
        var me = this;
        if (me.getLocks()[me.getPanGesture()] === me) { // Limit drags to single touch.
            var rect = me.getChart().getInnerRect(),
                xy = me.getChart().element.getXY();
            if (me.getZoomOnPanGesture()) {
                me.transformAxesBy(me.getZoomableAxes(e), 0, 0, (e.getX() - xy[0] - rect[0]) / me.startX, me.startY / (e.getY() - xy[1] - rect[1]));
            } else {
                me.transformAxesBy(me.getPannableAxes(e), e.getX() - xy[0] - rect[0] - me.startX, e.getY() - xy[1] - rect[1] - me.startY, 1, 1);
            }
            me.sync();
            return false;
        }
    },

    onPanGestureEnd: function (e) {
        var me = this,
            pan = me.getPanGesture();

        if (me.getLocks()[pan] === me) {
            me.getChart().resumeThicknessChanged();
            me.showLabels();
            me.sync();
            me.unlockEvents(pan);
            return false;
        }
    },

    onZoomGestureStart: function (e) {
        if (e.touches && e.touches.length === 2) {
            var me = this,
                xy = me.getChart().element.getXY(),
                rect = me.getChart().getInnerRect(),
                x = xy[0] + rect[0],
                y = xy[1] + rect[1],
                newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
                xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
                yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
            me.getChart().suspendThicknessChanged();
            me.lastZoomDistances = [xDistance, yDistance];
            me.lastPoints = newPoints;
            me.oldVisibleRanges = null;
            me.hideLabels();
            me.lockEvents(me.getZoomGesture());
            return false;
        }
    },

    onZoomGestureMove: function (e) {
        var me = this;
        if (me.getLocks()[me.getZoomGesture()] === me) {
            var rect = me.getChart().getInnerRect(),
                xy = me.getChart().element.getXY(),
                x = xy[0] + rect[0],
                y = xy[1] + rect[1],
                abs = Math.abs,
                lastPoints = me.lastPoints,
                newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
                xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
                yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
                lastDistances = this.lastZoomDistances || [xDistance, yDistance],
                zoomX = xDistance / lastDistances[0],
                zoomY = yDistance / lastDistances[1];

            me.transformAxesBy(me.getZoomableAxes(e),
                rect[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX,
                rect[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY,
                zoomX,
                zoomY);
            me.sync();
            return false;
        }
    },

    onZoomGestureEnd: function (e) {
        var me = this,
            zoom = me.getZoomGesture();

        if (me.getLocks()[zoom] === me) {
            me.getChart().resumeThicknessChanged();
            me.showLabels();
            me.sync();
            me.unlockEvents(zoom);
            return false;
        }
    },

    hideLabels: function () {
        if (this.getHideLabelInGesture()) {
            this.eachInteractiveAxes(function (axis) {
                axis.hideLabels();
            });
        }
    },

    showLabels: function () {
        if (this.getHideLabelInGesture()) {
            this.eachInteractiveAxes(function (axis) {
                axis.showLabels();
            });
        }
    },

    isEventOnAxis: function (e, axis) {
        // TODO: right now this uses the current event position but really we want to only
        // use the gesture's start event. Pinch does not give that to us though.
        var rect = axis.getSurface().getRect();
        return rect[0] <= e.getX() && e.getX() <= rect[0] + rect[2] && rect[1] <= e.getY() && e.getY() <= rect[1] + rect[3];
    },

    getPannableAxes: function (e) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes(),
            i, ln = axes.length,
            result = [], isEventOnAxis = false,
            config;

        if (e) {
            for (i = 0; i < ln; i++) {
                if (this.isEventOnAxis(e, axes[i])) {
                    isEventOnAxis = true;
                    break;
                }
            }
        }

        for (i = 0; i < ln; i++) {
            config = axisConfigs[axes[i].getPosition()];
            if (config && config.allowPan !== false && (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
                result.push(axes[i]);
            }
        }
        return result;
    },

    getZoomableAxes: function (e) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes(),
            result = [],
            i, ln = axes.length, axis,
            isEventOnAxis = false, config;

        if (e) {
            for (i = 0; i < ln; i++) {
                if (this.isEventOnAxis(e, axes[i])) {
                    isEventOnAxis = true;
                    break;
                }
            }
        }

        for (i = 0; i < ln; i++) {
            axis = axes[i];
            config = axisConfigs[axis.getPosition()];
            if (config && config.allowZoom !== false && (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
                result.push(axis);
            }
        }
        return result;
    },

    eachInteractiveAxes: function (fn) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes();
        for (var i = 0; i < axes.length; i++) {
            if (axisConfigs[axes[i].getPosition()]) {
                if (false === fn.call(this, axes[i])) {
                    return;
                }
            }
        }
    },

    transformAxesBy: function (axes, panX, panY, sx, sy) {
        var rect = this.getChart().getInnerRect(),
            axesCfg = this.getAxes(), axisCfg,
            oldVisibleRanges = this.oldVisibleRanges,
            result = false;

        if (!oldVisibleRanges) {
            this.oldVisibleRanges = oldVisibleRanges = {};
            this.eachInteractiveAxes(function (axis) {
                oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
            });
        }

        if (!rect) {
            return;
        }

        for (var i = 0; i < axes.length; i++) {
            axisCfg = axesCfg[axes[i].getPosition()];
            result = this.transformAxisBy(axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy, this.minZoom || axisCfg.minZoom, this.maxZoom || axisCfg.maxZoom) || result;
        }
        return result;
    },

    transformAxisBy: function (axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
        var me = this,
            visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
            visibleRange = axis.getVisibleRange(),
            actualMinZoom =  minZoom || me.getMinZoom() || axis.config.minZoom,
            actualMaxZoom =  maxZoom || me.getMaxZoom() || axis.config.maxZoom,
            rect = me.getChart().getInnerRect(),
            left, right;
        if (!rect) {
            return;
        }

        var isSide = axis.isSide(),
            length = isSide ? rect[3] : rect[2],
            pan = isSide ? -panY : panX;
        visibleLength /= isSide ? sy : sx;
        if (visibleLength < 0) {
            visibleLength = -visibleLength;
        }

        if (visibleLength * actualMinZoom > 1) {
            visibleLength = 1;
        }

        if (visibleLength * actualMaxZoom < 1) {
            visibleLength = 1 / actualMaxZoom;
        }
        left = oldVisibleRange[0];
        right = oldVisibleRange[1];

        visibleRange = visibleRange[1] - visibleRange[0];
        if (visibleLength === visibleRange && visibleRange === 1) {
            return;
        }
        axis.setVisibleRange([
            (oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 - pan / length * visibleLength,
            (oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 - pan / length * visibleLength
        ]);
        return (Math.abs(left - axis.getVisibleRange()[0]) > 1e-10 || Math.abs(right - axis.getVisibleRange()[1]) > 1e-10);
    },

    destroy: function () {
        this.setModeToggleButton(null);
        this.callParent();
    }
});