/**
 * The Navigator component is used to visually set the visible range of the x-axis
 * of a cartesian chart.
 *
 * This component is meant to be used with the Navigator Container
 * via its {@link Ext.chart.navigator.Container#navigator} config.
 *
 * IMPORTANT: even though the Navigator component is a kind of chart, it should not be
 * treated as such. Correct behavior is not guaranteed when using hidden/private configs.
 */
Ext.define('Ext.chart.navigator.Navigator', {
    extend: 'Ext.chart.navigator.NavigatorBase',
 
    isNavigator: true,
 
    requires: [
        'Ext.chart.navigator.sprite.RangeMask'
    ],
 
    config: {
        /**
         * @cfg {'bottom'/'top'} [docked='bottom']
         */
        docked: 'bottom',
 
        /**
         * @cfg {'series'/'chart'} [span='series']
         * Whether the navigator should span the 'series' (default) or the whole 'chart'.
         */
        span: 'series',
 
        insetPadding: 0,
        innerPadding: 0,
 
        /**
         * @cfg {Ext.chart.navigator.Container} navigatorContainer
         * 'parent' is reserved in Modern, 'container' is reserved in Classic,
         * so we use 'navigatorContainer' as a config name.
         * @private
         */
        navigatorContainer: null,
 
        /**
         * @cfg {String} axis (required)
         * The ID of the {@link #chart chart's} axis to link to.
         * The axis should be positioned to 'bottom' or 'top' in the chart.
         */
        axis: null,
 
        /**
         * @cfg {Number} [tolerance=20]
         * The maximum horizontal delta between the pointer/finger and the center of a navigator
         * thumb. Used for hit testing.
         */
        tolerance: 20,
 
        /**
         * @cfg {Number} [minimum=0.8]
         * The start of the visible range, where the visible range is a [0, 1] interval.
         */
        minimum: 0.8,
 
        /**
         * @cfg {Number} [maximum=1]
         * The end of the visible range, where the visible range is a [0, 1] interval.
         */
        maximum: 1,
 
        /**
         * @cfg {Number} [thumbGap=30]
         * Minimum gap between navigator thumbs in pixels.
         */
        thumbGap: 30,
 
        autoHideThumbs: true,
 
        width: '100%',
 
        /**
         * @cfg {Number} [height=75]
         * The height of the navigator component.
         */
        height: 75
 
        /**
         * @cfg flipXY
         * @hide
         */
 
        /**
         * @cfg series
         * @hide
         */
 
        /**
         * @cfg axes
         * @hide
         */
 
        /**
         * @cfg store
         * @hide
         */
 
        /**
         * @cfg legend
         * @hide
         */
 
        /**
         * @cfg interactions
         * @hide
         */
 
        /**
         * @cfg highlightItem
         * @hide
         */
 
        /**
         * @cfg theme
         * @hide
         */
 
        /**
         * @cfg innerPadding
         * @hide
         */
 
        /**
         * @cfg insetPadding
         * @hide
         */
    },
 
    dragType: null,
 
    constructor: function(config) {
        var me = this,
            visibleRange, overlay;
 
        config = config || {};
        visibleRange = [
            config.minimum || 0.8,
            config.maximum || 1
        ];
 
        me.callParent([config]);
 
        overlay = me.overlaySurface;
        overlay.element.setStyle({
            zIndex: 100
        });
 
        me.rangeMask = overlay.add({
            type: 'rangemask',
            min: visibleRange[0],
            max: visibleRange[1],
            fillStyle: 'rgba(0, 0, 0, .25)'
        });
 
        me.onDragEnd(); // Set 'thumbOpacity' of the range mask sprite to 0, if needed,
        // and apply animation modifier changes after that, so that the attribute is set
        // instantly.
        me.rangeMask.setAnimation({
            duration: 500,
            customDurations: {
                min: 0,
                max: 0,
                translationX: 0,
                translationY: 0,
                scalingX: 0,
                scalingY: 0,
                scalingCenterX: 0,
                scalingCenterY: 0,
                fillStyle: 0,
                strokeStyle: 0
            }
        });
 
        me.setVisibleRange(visibleRange);
    },
 
    createSurface: function(id) {
        var surface = this.callParent([id]);
 
        if (id === 'overlay') {
            this.overlaySurface = surface;
        }
 
        return surface;
    },
 
    // Note: 'applyDock' and 'updateDock' won't ever be called in Classic.
    // See Classic NavigatorBase.
 
    applyAxis: function(axis) {
        return this.getNavigatorContainer().getChart().getAxis(axis);
    },
 
    updateAxis: function(axis, oldAxis) {
        var me = this,
            eventName = 'visiblerangechange',
            eventHandler = 'onAxisVisibleRangeChange';
 
        if (oldAxis) {
            oldAxis.un(eventName, eventHandler, me);
        }
 
        if (axis) {
            axis.on(eventName, eventHandler, me);
        }
 
        me.axis = axis;
    },
 
    getAxis: function() {
        // The superclass doesn't have the 'axis' config, but it has the same method,
        // which we override here to act as a getter for the config. The user is not
        // expected to use the original method in this subclass anyway.
        return this.axis;
    },
 
    onAxisVisibleRangeChange: function(axis, visibleRange) {
        this.setVisibleRange(visibleRange);
    },
 
    updateNavigatorContainer: function(navigatorContainer) {
        var me = this,
            oldChart = me.chart,
            chart = me.chart = navigatorContainer && navigatorContainer.getChart(),
            chartSeriesList = chart && chart.getSeries(),
            // 'legendStore' already exists in the base class.
            chartLegendStore = me.chartLegendStore,
            navigatorSeriesList = [],
            storeEventName = 'update',
            // 'onLegendStoreUpdate' already exists in the base class.
            storeEventHandler = 'onChartLegendStoreUpdate',
            chartSeries, navigatorSeries,
            seriesConfig, i;
 
        if (oldChart) {
            oldChart.un('layout', 'afterBoundChartLayout', me);
            oldChart.un('themechange', 'onChartThemeChange', me);
            oldChart.un('storechange', 'onChartStoreChange', me);
        }
 
        chart.on('layout', 'afterBoundChartLayout', me);
 
        for (= 0; i < chartSeriesList.length; i++) {
            chartSeries = chartSeriesList[i];
            seriesConfig = me.getSeriesConfig(chartSeries);
            navigatorSeries = Ext.create('series.' + seriesConfig.type, seriesConfig);
            navigatorSeries.parentSeries = chartSeries;
            chartSeries.navigatorSeries = navigatorSeries;
            navigatorSeriesList.push(navigatorSeries);
        }
 
        if (chartLegendStore) {
            chartLegendStore.un(storeEventName, storeEventHandler, me);
            me.chartLegendStore = null;
        }
 
        if (chart) {
            me.setStore(chart.getStore());
            me.chartLegendStore = chartLegendStore = chart.getLegendStore();
 
            if (chartLegendStore) {
                chartLegendStore.on(storeEventName, storeEventHandler, me);
            }
 
            chart.on('themechange', 'onChartThemeChange', me);
            chart.on('storechange', 'onChartStoreChange', me);
            me.onChartThemeChange(chart, chart.getTheme());
        }
 
        me.setSeries(navigatorSeriesList);
    },
 
    onChartThemeChange: function(chart, theme) {
        this.setTheme(theme);
    },
 
    onChartStoreChange: function(chart, store) {
        this.setStore(store);
    },
 
    addCustomStyle: function(config, style, subStyle) {
        var fillStyle, strokeStyle;
 
        style = style || {};
        subStyle = subStyle || {};
 
        config.style = config.style || {};
        config.subStyle = config.subStyle || {};
 
        fillStyle = style && (style.fillStyle || style.fill);
        strokeStyle = style && (style.strokeStyle || style.stroke);
 
        if (fillStyle) {
            config.style.fillStyle = fillStyle;
        }
 
        if (strokeStyle) {
            config.style.strokeStyle = strokeStyle;
        }
 
        fillStyle = subStyle && (subStyle.fillStyle || subStyle.fill);
        strokeStyle = subStyle && (subStyle.strokeStyle || subStyle.stroke);
 
        if (fillStyle) {
            config.subStyle.fillStyle = fillStyle;
        }
 
        if (strokeStyle) {
            config.subStyle.strokeStyle = strokeStyle;
        }
 
        return config;
    },
 
    getSeriesConfig: function(chartSeries) {
        var me = this,
            style = chartSeries.getStyle(),
            config;
 
        if (chartSeries.isLine) {
            config = me.addCustomStyle({
                type: 'line',
                fill: true,
                xField: chartSeries.getXField(),
                yField: chartSeries.getYField(),
                smooth: chartSeries.getSmooth()
            }, style);
        }
        else if (chartSeries.isCandleStick) {
            config = me.addCustomStyle({
                type: 'line',
                fill: true,
                xField: chartSeries.getXField(),
                yField: chartSeries.getCloseField()
            }, style.raiseStyle);
        }
        else if (chartSeries.isArea || chartSeries.isBar) {
            config = me.addCustomStyle({
                type: 'area',
                xField: chartSeries.getXField(),
                yField: chartSeries.getYField()
            }, style, chartSeries.getSubStyle());
        }
        else {
            Ext.raise("Navigator only works with 'line', 'bar', 'candlestick' and 'area' series.");
        }
 
        config.style.fillOpacity = 0.2;
 
        return config;
    },
 
    onChartLegendStoreUpdate: function(store, record) {
        var me = this,
            chart = me.chart,
            series;
 
        if (chart && record) {
            series = chart.getSeries().map[record.get('series')];
 
            if (series && series.navigatorSeries) {
                series.navigatorSeries.setHiddenByIndex(record.get('index'),
                                                        record.get('disabled'));
                me.redraw();
            }
        }
    },
 
    setupEvents: function() {
        // Called from NavigatorBase classes.
        var me = this,
            overlayEl = me.overlaySurface.element;
 
        overlayEl.on({
            scope: me,
            drag: 'onDrag',
            dragstart: 'onDragStart',
            dragend: 'onDragEnd',
            dragcancel: 'onDragEnd',
            mousemove: 'onMouseMove'
        });
    },
 
    onMouseMove: function(e) {
        var me = this,
            overlayEl = me.overlaySurface.element,
            style = overlayEl.dom.style,
            dragType = me.getDragType(e.pageX - overlayEl.getXY()[0]);
 
        switch (dragType) {
            case 'min':
            case 'max':
                style.cursor = 'ew-resize';
                break;
 
            case 'pan':
                style.cursor = 'move';
                break;
 
            default:
                style.cursor = 'default';
        }
    },
 
    getDragType: function(x) {
        var me = this,
            t = me.getTolerance(),
            width = me.overlaySurface.element.getSize().width,
            rangeMask = me.rangeMask,
            min = width * rangeMask.attr.min,
            max = width * rangeMask.attr.max,
            dragType;
 
        if (> min + t && x < max - t) {
            dragType = 'pan';
        }
        else if (<= min + t && x > min - t) {
            dragType = 'min';
        }
        else if (>= max - t && x < max + t) {
            dragType = 'max';
        }
 
        return dragType;
    },
 
    onDragStart: function(e) {
        var me = this,
            x, dragType;
 
        // Limit drags to single touch.
        if (me.dragType || e && e.touches && e.touches.length > 1) {
            return;
        }
 
        x = e.touches[0].pageX - me.overlaySurface.element.getXY()[0];
        dragType = me.getDragType(x);
 
        me.rangeMask.attr.thumbOpacity = 1;
 
        if (dragType) {
            me.dragType = dragType;
            me.touchId = e.touches[0].identifier;
            me.dragX = x;
        }
    },
 
    onDrag: function(e) {
        if (e.touch.identifier !== this.touchId) {
            return;
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            overlayEl = me.overlaySurface.element,
            width = overlayEl.getSize().width,
            x = e.touches[0].pageX - overlayEl.getXY()[0],
            thumbGap = me.getThumbGap() / width,
            rangeMask = me.rangeMask,
            min = rangeMask.attr.min,
            max = rangeMask.attr.max,
            delta = max - min,
            dragType = me.dragType,
            drag = me.dragX,
            dx = (- drag) / width;
 
        if (dragType === 'pan') {
            min += dx;
            max += dx;
 
            if (min < 0) {
                min = 0;
                max = delta;
            }
 
            if (max > 1) {
                max = 1;
                min = max - delta;
            }
        }
        else if (dragType === 'min') {
            min += dx;
 
            if (min < 0) {
                min = 0;
            }
 
            if (min > max - thumbGap) {
                min = max - thumbGap;
            }
        }
        else if (dragType === 'max') {
            max += dx;
 
            if (max > 1) {
                max = 1;
            }
 
            if (max < min + thumbGap) {
                max = min + thumbGap;
            }
        }
        else {
            return;
        }
 
        me.dragX = x;
        me.setVisibleRange([min, max]);
    },
 
    onDragEnd: function() {
        var me = this,
            autoHideThumbs = me.getAutoHideThumbs();
 
        me.dragType = null;
 
        if (autoHideThumbs) {
            me.rangeMask.setAttributes({
                thumbOpacity: 0
            });
        }
    },
 
    updateMinimum: function(mininum) {
        if (!this.isConfiguring) {
            this.setVisibleRange([mininum, this.getMaximum()]);
        }
    },
 
    updateMaximum: function(maximum) {
        if (!this.isConfiguring) {
            this.setVisibleRange([this.getMinimum(), maximum]);
        }
    },
 
    getMinimum: function() {
        return this.rangeMask.attr.min;
    },
 
    getMaximum: function() {
        return this.rangeMask.attr.max;
    },
 
    setVisibleRange: function(visibleRange) {
        var me = this,
            chart = me.chart;
 
        me.axis.setVisibleRange(visibleRange);
        me.rangeMask.setAttributes({
            min: visibleRange[0],
            max: visibleRange[1]
        });
        me.getSurface('overlay').renderFrame();
 
        chart.suspendAnimation();
        chart.redraw();
        chart.resumeAnimation();
    },
 
    afterBoundChartLayout: function() {
        var me = this,
            spanSeries = me.getSpan() === 'series',
            mainRect = me.chart.getMainRect(),
            size = me.element.getSize();
 
        if (mainRect && spanSeries) {
            me.setInsetPadding({
                left: mainRect[0],
                right: size.width - mainRect[2] - mainRect[0],
                top: 0,
                bottom: 0
            });
            me.performLayout();
        }
    },
 
    afterChartLayout: function() {
        var me = this,
            size = me.overlaySurface.element.getSize();
 
        me.rangeMask.setAttributes({
            scalingCenterX: 0,
            scalingCenterY: 0,
            scalingX: size.width,
            scalingY: size.height
        });
    },
 
    doDestroy: function() {
        var chart = this.chart;
 
        if (chart && !chart.destroyed) {
            chart.un('layout', 'afterBoundChartLayout', this);
        }
 
        this.callParent();
    }
});