
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

var _config = require("../../config");

var __DEV__ = _config.__DEV__;

var echarts = require("../../echarts");

var zrUtil = require("zrender/lib/core/util");

var env = require("zrender/lib/core/env");

var modelUtil = require("../../util/model");

var helper = require("./helper");

var AxisProxy = require("./AxisProxy");

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var each = zrUtil.each;
var eachAxisDim = helper.eachAxisDim;
var DataZoomModel = echarts.extendComponentModel({
  type: 'dataZoom',
  dependencies: ['xAxis', 'yAxis', 'zAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series'],

  /**
   * @protected
   */
  defaultOption: {
    zlevel: 0,
    z: 4,
    // Higher than normal component (z: 2).
    orient: null,
    // Default auto by axisIndex. Possible value: 'horizontal', 'vertical'.
    xAxisIndex: null,
    // Default the first horizontal category axis.
    yAxisIndex: null,
    // Default the first vertical category axis.
    filterMode: 'filter',
    // Possible values: 'filter' or 'empty' or 'weakFilter'.
    // 'filter': data items which are out of window will be removed. This option is
    //          applicable when filtering outliers. For each data item, it will be
    //          filtered if one of the relevant dimensions is out of the window.
    // 'weakFilter': data items which are out of window will be removed. This option
    //          is applicable when filtering outliers. For each data item, it will be
    //          filtered only if all  of the relevant dimensions are out of the same
    //          side of the window.
    // 'empty': data items which are out of window will be set to empty.
    //          This option is applicable when user should not neglect
    //          that there are some data items out of window.
    // 'none': Do not filter.
    // Taking line chart as an example, line will be broken in
    // the filtered points when filterModel is set to 'empty', but
    // be connected when set to 'filter'.
    throttle: null,
    // Dispatch action by the fixed rate, avoid frequency.
    // default 100. Do not throttle when use null/undefined.
    // If animation === true and animationDurationUpdate > 0,
    // default value is 100, otherwise 20.
    start: 0,
    // Start percent. 0 ~ 100
    end: 100,
    // End percent. 0 ~ 100
    startValue: null,
    // Start value. If startValue specified, start is ignored.
    endValue: null,
    // End value. If endValue specified, end is ignored.
    minSpan: null,
    // 0 ~ 100
    maxSpan: null,
    // 0 ~ 100
    minValueSpan: null,
    // The range of dataZoom can not be smaller than that.
    maxValueSpan: null,
    // The range of dataZoom can not be larger than that.
    rangeMode: null // Array, can be 'value' or 'percent'.

  },

  /**
   * @override
   */
  init: function (option, parentModel, ecModel) {
    /**
     * key like x_0, y_1
     * @private
     * @type {Object}
     */
    this._dataIntervalByAxis = {};
    /**
     * @private
     */

    this._dataInfo = {};
    /**
     * key like x_0, y_1
     * @private
     */

    this._axisProxies = {};
    /**
     * @readOnly
     */

    this.textStyleModel;
    /**
     * @private
     */

    this._autoThrottle = true;
    /**
     * It is `[rangeModeForMin, rangeModeForMax]`.
     * The optional values for `rangeMode`:
     * + `'value'` mode: the axis extent will always be determined by
     *     `dataZoom.startValue` and `dataZoom.endValue`, despite
     *     how data like and how `axis.min` and `axis.max` are.
     * + `'percent'` mode: `100` represents 100% of the `[dMin, dMax]`,
     *     where `dMin` is `axis.min` if `axis.min` specified, otherwise `data.extent[0]`,
     *     and `dMax` is `axis.max` if `axis.max` specified, otherwise `data.extent[1]`.
     *     Axis extent will be determined by the result of the percent of `[dMin, dMax]`.
     *
     * For example, when users are using dynamic data (update data periodically via `setOption`),
     * if in `'value`' mode, the window will be kept in a fixed value range despite how
     * data are appended, while if in `'percent'` mode, whe window range will be changed alone with
     * the appended data (suppose `axis.min` and `axis.max` are not specified).
     *
     * @private
     */

    this._rangePropMode = ['percent', 'percent'];
    var inputRawOption = retrieveRawOption(option);
    /**
     * Suppose a "main process" start at the point that model prepared (that is,
     * model initialized or merged or method called in `action`).
     * We should keep the `main process` idempotent, that is, given a set of values
     * on `option`, we get the same result.
     *
     * But sometimes, values on `option` will be updated for providing users
     * a "final calculated value" (`dataZoomProcessor` will do that). Those value
     * should not be the base/input of the `main process`.
     *
     * So in that case we should save and keep the input of the `main process`
     * separately, called `settledOption`.
     *
     * For example, consider the case:
     * (Step_1) brush zoom the grid by `toolbox.dataZoom`,
     *     where the original input `option.startValue`, `option.endValue` are earsed by
     *     calculated value.
     * (Step)2) click the legend to hide and show a series,
     *     where the new range is calculated by the earsed `startValue` and `endValue`,
     *     which brings incorrect result.
     *
     * @readOnly
     */

    this.settledOption = inputRawOption;
    this.mergeDefaultAndTheme(option, ecModel);
    this.doInit(inputRawOption);
  },

  /**
   * @override
   */
  mergeOption: function (newOption) {
    var inputRawOption = retrieveRawOption(newOption); //FIX #2591

    zrUtil.merge(this.option, newOption, true);
    zrUtil.merge(this.settledOption, inputRawOption, true);
    this.doInit(inputRawOption);
  },

  /**
   * @protected
   */
  doInit: function (inputRawOption) {
    var thisOption = this.option; // Disable realtime view update if canvas is not supported.

    if (!env.canvasSupported) {
      thisOption.realtime = false;
    }

    this._setDefaultThrottle(inputRawOption);

    updateRangeUse(this, inputRawOption);
    var settledOption = this.settledOption;
    each([['start', 'startValue'], ['end', 'endValue']], function (names, index) {
      // start/end has higher priority over startValue/endValue if they
      // both set, but we should make chart.setOption({endValue: 1000})
      // effective, rather than chart.setOption({endValue: 1000, end: null}).
      if (this._rangePropMode[index] === 'value') {
        thisOption[names[0]] = settledOption[names[0]] = null;
      } // Otherwise do nothing and use the merge result.

    }, this);
    this.textStyleModel = this.getModel('textStyle');

    this._resetTarget();

    this._giveAxisProxies();
  },

  /**
   * @private
   */
  _giveAxisProxies: function () {
    var axisProxies = this._axisProxies;
    this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) {
      var axisModel = this.dependentModels[dimNames.axis][axisIndex]; // If exists, share axisProxy with other dataZoomModels.

      var axisProxy = axisModel.__dzAxisProxy || ( // Use the first dataZoomModel as the main model of axisProxy.
      axisModel.__dzAxisProxy = new AxisProxy(dimNames.name, axisIndex, this, ecModel)); // FIXME
      // dispose __dzAxisProxy

      axisProxies[dimNames.name + '_' + axisIndex] = axisProxy;
    }, this);
  },

  /**
   * @private
   */
  _resetTarget: function () {
    var thisOption = this.option;

    var autoMode = this._judgeAutoMode();

    eachAxisDim(function (dimNames) {
      var axisIndexName = dimNames.axisIndex;
      thisOption[axisIndexName] = modelUtil.normalizeToArray(thisOption[axisIndexName]);
    }, this);

    if (autoMode === 'axisIndex') {
      this._autoSetAxisIndex();
    } else if (autoMode === 'orient') {
      this._autoSetOrient();
    }
  },

  /**
   * @private
   */
  _judgeAutoMode: function () {
    // Auto set only works for setOption at the first time.
    // The following is user's reponsibility. So using merged
    // option is OK.
    var thisOption = this.option;
    var hasIndexSpecified = false;
    eachAxisDim(function (dimNames) {
      // When user set axisIndex as a empty array, we think that user specify axisIndex
      // but do not want use auto mode. Because empty array may be encountered when
      // some error occured.
      if (thisOption[dimNames.axisIndex] != null) {
        hasIndexSpecified = true;
      }
    }, this);
    var orient = thisOption.orient;

    if (orient == null && hasIndexSpecified) {
      return 'orient';
    } else if (!hasIndexSpecified) {
      if (orient == null) {
        thisOption.orient = 'horizontal';
      }

      return 'axisIndex';
    }
  },

  /**
   * @private
   */
  _autoSetAxisIndex: function () {
    var autoAxisIndex = true;
    var orient = this.get('orient', true);
    var thisOption = this.option;
    var dependentModels = this.dependentModels;

    if (autoAxisIndex) {
      // Find axis that parallel to dataZoom as default.
      var dimName = orient === 'vertical' ? 'y' : 'x';

      if (dependentModels[dimName + 'Axis'].length) {
        thisOption[dimName + 'AxisIndex'] = [0];
        autoAxisIndex = false;
      } else {
        each(dependentModels.singleAxis, function (singleAxisModel) {
          if (autoAxisIndex && singleAxisModel.get('orient', true) === orient) {
            thisOption.singleAxisIndex = [singleAxisModel.componentIndex];
            autoAxisIndex = false;
          }
        });
      }
    }

    if (autoAxisIndex) {
      // Find the first category axis as default. (consider polar)
      eachAxisDim(function (dimNames) {
        if (!autoAxisIndex) {
          return;
        }

        var axisIndices = [];
        var axisModels = this.dependentModels[dimNames.axis];

        if (axisModels.length && !axisIndices.length) {
          for (var i = 0, len = axisModels.length; i < len; i++) {
            if (axisModels[i].get('type') === 'category') {
              axisIndices.push(i);
            }
          }
        }

        thisOption[dimNames.axisIndex] = axisIndices;

        if (axisIndices.length) {
          autoAxisIndex = false;
        }
      }, this);
    }

    if (autoAxisIndex) {
      // FIXME
      // 这里是兼容ec2的写法（没指定xAxisIndex和yAxisIndex时把scatter和双数值轴折柱纳入dataZoom控制），
      // 但是实际是否需要Grid.js#getScaleByOption来判断（考虑time，log等axis type）？
      // If both dataZoom.xAxisIndex and dataZoom.yAxisIndex is not specified,
      // dataZoom component auto adopts series that reference to
      // both xAxis and yAxis which type is 'value'.
      this.ecModel.eachSeries(function (seriesModel) {
        if (this._isSeriesHasAllAxesTypeOf(seriesModel, 'value')) {
          eachAxisDim(function (dimNames) {
            var axisIndices = thisOption[dimNames.axisIndex];
            var axisIndex = seriesModel.get(dimNames.axisIndex);
            var axisId = seriesModel.get(dimNames.axisId);
            var axisModel = seriesModel.ecModel.queryComponents({
              mainType: dimNames.axis,
              index: axisIndex,
              id: axisId
            })[0];
            axisIndex = axisModel.componentIndex;

            if (zrUtil.indexOf(axisIndices, axisIndex) < 0) {
              axisIndices.push(axisIndex);
            }
          });
        }
      }, this);
    }
  },

  /**
   * @private
   */
  _autoSetOrient: function () {
    var dim; // Find the first axis

    this.eachTargetAxis(function (dimNames) {
      !dim && (dim = dimNames.name);
    }, this);
    this.option.orient = dim === 'y' ? 'vertical' : 'horizontal';
  },

  /**
   * @private
   */
  _isSeriesHasAllAxesTypeOf: function (seriesModel, axisType) {
    // FIXME
    // 需要series的xAxisIndex和yAxisIndex都首先自动设置上。
    // 例如series.type === scatter时。
    var is = true;
    eachAxisDim(function (dimNames) {
      var seriesAxisIndex = seriesModel.get(dimNames.axisIndex);
      var axisModel = this.dependentModels[dimNames.axis][seriesAxisIndex];

      if (!axisModel || axisModel.get('type') !== axisType) {
        is = false;
      }
    }, this);
    return is;
  },

  /**
   * @private
   */
  _setDefaultThrottle: function (inputRawOption) {
    // When first time user set throttle, auto throttle ends.
    if (inputRawOption.hasOwnProperty('throttle')) {
      this._autoThrottle = false;
    }

    if (this._autoThrottle) {
      var globalOption = this.ecModel.option;
      this.option.throttle = globalOption.animation && globalOption.animationDurationUpdate > 0 ? 100 : 20;
    }
  },

  /**
   * @public
   */
  getFirstTargetAxisModel: function () {
    var firstAxisModel;
    eachAxisDim(function (dimNames) {
      if (firstAxisModel == null) {
        var indices = this.get(dimNames.axisIndex);

        if (indices.length) {
          firstAxisModel = this.dependentModels[dimNames.axis][indices[0]];
        }
      }
    }, this);
    return firstAxisModel;
  },

  /**
   * @public
   * @param {Function} callback param: axisModel, dimNames, axisIndex, dataZoomModel, ecModel
   */
  eachTargetAxis: function (callback, context) {
    var ecModel = this.ecModel;
    eachAxisDim(function (dimNames) {
      each(this.get(dimNames.axisIndex), function (axisIndex) {
        callback.call(context, dimNames, axisIndex, this, ecModel);
      }, this);
    }, this);
  },

  /**
   * @param {string} dimName
   * @param {number} axisIndex
   * @return {module:echarts/component/dataZoom/AxisProxy} If not found, return null/undefined.
   */
  getAxisProxy: function (dimName, axisIndex) {
    return this._axisProxies[dimName + '_' + axisIndex];
  },

  /**
   * @param {string} dimName
   * @param {number} axisIndex
   * @return {module:echarts/model/Model} If not found, return null/undefined.
   */
  getAxisModel: function (dimName, axisIndex) {
    var axisProxy = this.getAxisProxy(dimName, axisIndex);
    return axisProxy && axisProxy.getAxisModel();
  },

  /**
   * If not specified, set to undefined.
   *
   * @public
   * @param {Object} opt
   * @param {number} [opt.start]
   * @param {number} [opt.end]
   * @param {number} [opt.startValue]
   * @param {number} [opt.endValue]
   */
  setRawRange: function (opt) {
    var thisOption = this.option;
    var settledOption = this.settledOption;
    each([['start', 'startValue'], ['end', 'endValue']], function (names) {
      // Consider the pair <start, startValue>:
      // If one has value and the other one is `null/undefined`, we both set them
      // to `settledOption`. This strategy enables the feature to clear the original
      // value in `settledOption` to `null/undefined`.
      // But if both of them are `null/undefined`, we do not set them to `settledOption`
      // and keep `settledOption` with the original value. This strategy enables users to
      // only set <end or endValue> but not set <start or startValue> when calling
      // `dispatchAction`.
      // The pair <end, endValue> is treated in the same way.
      if (opt[names[0]] != null || opt[names[1]] != null) {
        thisOption[names[0]] = settledOption[names[0]] = opt[names[0]];
        thisOption[names[1]] = settledOption[names[1]] = opt[names[1]];
      }
    }, this);
    updateRangeUse(this, opt);
  },

  /**
   * @public
   * @param {Object} opt
   * @param {number} [opt.start]
   * @param {number} [opt.end]
   * @param {number} [opt.startValue]
   * @param {number} [opt.endValue]
   */
  setCalculatedRange: function (opt) {
    var option = this.option;
    each(['start', 'startValue', 'end', 'endValue'], function (name) {
      option[name] = opt[name];
    });
  },

  /**
   * @public
   * @return {Array.<number>} [startPercent, endPercent]
   */
  getPercentRange: function () {
    var axisProxy = this.findRepresentativeAxisProxy();

    if (axisProxy) {
      return axisProxy.getDataPercentWindow();
    }
  },

  /**
   * @public
   * For example, chart.getModel().getComponent('dataZoom').getValueRange('y', 0);
   *
   * @param {string} [axisDimName]
   * @param {number} [axisIndex]
   * @return {Array.<number>} [startValue, endValue] value can only be '-' or finite number.
   */
  getValueRange: function (axisDimName, axisIndex) {
    if (axisDimName == null && axisIndex == null) {
      var axisProxy = this.findRepresentativeAxisProxy();

      if (axisProxy) {
        return axisProxy.getDataValueWindow();
      }
    } else {
      return this.getAxisProxy(axisDimName, axisIndex).getDataValueWindow();
    }
  },

  /**
   * @public
   * @param {module:echarts/model/Model} [axisModel] If axisModel given, find axisProxy
   *      corresponding to the axisModel
   * @return {module:echarts/component/dataZoom/AxisProxy}
   */
  findRepresentativeAxisProxy: function (axisModel) {
    if (axisModel) {
      return axisModel.__dzAxisProxy;
    } // Find the first hosted axisProxy


    var axisProxies = this._axisProxies;

    for (var key in axisProxies) {
      if (axisProxies.hasOwnProperty(key) && axisProxies[key].hostedBy(this)) {
        return axisProxies[key];
      }
    } // If no hosted axis find not hosted axisProxy.
    // Consider this case: dataZoomModel1 and dataZoomModel2 control the same axis,
    // and the option.start or option.end settings are different. The percentRange
    // should follow axisProxy.
    // (We encounter this problem in toolbox data zoom.)


    for (var key in axisProxies) {
      if (axisProxies.hasOwnProperty(key) && !axisProxies[key].hostedBy(this)) {
        return axisProxies[key];
      }
    }
  },

  /**
   * @return {Array.<string>}
   */
  getRangePropMode: function () {
    return this._rangePropMode.slice();
  }
});
/**
 * Retrieve the those raw params from option, which will be cached separately.
 * becasue they will be overwritten by normalized/calculated values in the main
 * process.
 */

function retrieveRawOption(option) {
  var ret = {};
  each(['start', 'end', 'startValue', 'endValue', 'throttle'], function (name) {
    option.hasOwnProperty(name) && (ret[name] = option[name]);
  });
  return ret;
}

function updateRangeUse(dataZoomModel, inputRawOption) {
  var rangePropMode = dataZoomModel._rangePropMode;
  var rangeModeInOption = dataZoomModel.get('rangeMode');
  each([['start', 'startValue'], ['end', 'endValue']], function (names, index) {
    var percentSpecified = inputRawOption[names[0]] != null;
    var valueSpecified = inputRawOption[names[1]] != null;

    if (percentSpecified && !valueSpecified) {
      rangePropMode[index] = 'percent';
    } else if (!percentSpecified && valueSpecified) {
      rangePropMode[index] = 'value';
    } else if (rangeModeInOption) {
      rangePropMode[index] = rangeModeInOption[index];
    } else if (percentSpecified) {
      // percentSpecified && valueSpecified
      rangePropMode[index] = 'percent';
    } // else remain its original setting.

  });
}

var _default = DataZoomModel;
module.exports = _default;