
/*
* 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 zrUtil = require("zrender/lib/core/util");

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

var VisualMapping = require("../../visual/VisualMapping");

var visualDefault = require("../../visual/visualDefault");

var _number = require("../../util/number");

var reformIntervals = _number.reformIntervals;

/*
* 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 PiecewiseModel = VisualMapModel.extend({
  type: 'visualMap.piecewise',

  /**
   * Order Rule:
   *
   * option.categories / option.pieces / option.text / option.selected:
   *     If !option.inverse,
   *     Order when vertical: ['top', ..., 'bottom'].
   *     Order when horizontal: ['left', ..., 'right'].
   *     If option.inverse, the meaning of
   *     the order should be reversed.
   *
   * this._pieceList:
   *     The order is always [low, ..., high].
   *
   * Mapping from location to low-high:
   *     If !option.inverse
   *     When vertical, top is high.
   *     When horizontal, right is high.
   *     If option.inverse, reverse.
   */

  /**
   * @protected
   */
  defaultOption: {
    selected: null,
    // Object. If not specified, means selected.
    // When pieces and splitNumber: {'0': true, '5': true}
    // When categories: {'cate1': false, 'cate3': true}
    // When selected === false, means all unselected.
    minOpen: false,
    // Whether include values that smaller than `min`.
    maxOpen: false,
    // Whether include values that bigger than `max`.
    align: 'auto',
    // 'auto', 'left', 'right'
    itemWidth: 20,
    // When put the controller vertically, it is the length of
    // horizontal side of each item. Otherwise, vertical side.
    itemHeight: 14,
    // When put the controller vertically, it is the length of
    // vertical side of each item. Otherwise, horizontal side.
    itemSymbol: 'roundRect',
    pieceList: null,
    // Each item is Object, with some of those attrs:
    // {min, max, lt, gt, lte, gte, value,
    // color, colorSaturation, colorAlpha, opacity,
    // symbol, symbolSize}, which customize the range or visual
    // coding of the certain piece. Besides, see "Order Rule".
    categories: null,
    // category names, like: ['some1', 'some2', 'some3'].
    // Attr min/max are ignored when categories set. See "Order Rule"
    splitNumber: 5,
    // If set to 5, auto split five pieces equally.
    // If set to 0 and component type not set, component type will be
    // determined as "continuous". (It is less reasonable but for ec2
    // compatibility, see echarts/component/visualMap/typeDefaulter)
    selectedMode: 'multiple',
    // Can be 'multiple' or 'single'.
    itemGap: 10,
    // The gap between two items, in px.
    hoverLink: true,
    // Enable hover highlight.
    showLabel: null // By default, when text is used, label will hide (the logic
    // is remained for compatibility reason)

  },

  /**
   * @override
   */
  optionUpdated: function (newOption, isInit) {
    PiecewiseModel.superApply(this, 'optionUpdated', arguments);
    /**
     * The order is always [low, ..., high].
     * [{text: string, interval: Array.<number>}, ...]
     * @private
     * @type {Array.<Object>}
     */

    this._pieceList = [];
    this.resetExtent();
    /**
     * 'pieces', 'categories', 'splitNumber'
     * @type {string}
     */

    var mode = this._mode = this._determineMode();

    resetMethods[this._mode].call(this);

    this._resetSelected(newOption, isInit);

    var categories = this.option.categories;
    this.resetVisual(function (mappingOption, state) {
      if (mode === 'categories') {
        mappingOption.mappingMethod = 'category';
        mappingOption.categories = zrUtil.clone(categories);
      } else {
        mappingOption.dataExtent = this.getExtent();
        mappingOption.mappingMethod = 'piecewise';
        mappingOption.pieceList = zrUtil.map(this._pieceList, function (piece) {
          var piece = zrUtil.clone(piece);

          if (state !== 'inRange') {
            // FIXME
            // outOfRange do not support special visual in pieces.
            piece.visual = null;
          }

          return piece;
        });
      }
    });
  },

  /**
   * @protected
   * @override
   */
  completeVisualOption: function () {
    // Consider this case:
    // visualMap: {
    //      pieces: [{symbol: 'circle', lt: 0}, {symbol: 'rect', gte: 0}]
    // }
    // where no inRange/outOfRange set but only pieces. So we should make
    // default inRange/outOfRange for this case, otherwise visuals that only
    // appear in `pieces` will not be taken into account in visual encoding.
    var option = this.option;
    var visualTypesInPieces = {};
    var visualTypes = VisualMapping.listVisualTypes();
    var isCategory = this.isCategory();
    zrUtil.each(option.pieces, function (piece) {
      zrUtil.each(visualTypes, function (visualType) {
        if (piece.hasOwnProperty(visualType)) {
          visualTypesInPieces[visualType] = 1;
        }
      });
    });
    zrUtil.each(visualTypesInPieces, function (v, visualType) {
      var exists = 0;
      zrUtil.each(this.stateList, function (state) {
        exists |= has(option, state, visualType) || has(option.target, state, visualType);
      }, this);
      !exists && zrUtil.each(this.stateList, function (state) {
        (option[state] || (option[state] = {}))[visualType] = visualDefault.get(visualType, state === 'inRange' ? 'active' : 'inactive', isCategory);
      });
    }, this);

    function has(obj, state, visualType) {
      return obj && obj[state] && (zrUtil.isObject(obj[state]) ? obj[state].hasOwnProperty(visualType) : obj[state] === visualType // e.g., inRange: 'symbol'
      );
    }

    VisualMapModel.prototype.completeVisualOption.apply(this, arguments);
  },
  _resetSelected: function (newOption, isInit) {
    var thisOption = this.option;
    var pieceList = this._pieceList; // Selected do not merge but all override.

    var selected = (isInit ? thisOption : newOption).selected || {};
    thisOption.selected = selected; // Consider 'not specified' means true.

    zrUtil.each(pieceList, function (piece, index) {
      var key = this.getSelectedMapKey(piece);

      if (!selected.hasOwnProperty(key)) {
        selected[key] = true;
      }
    }, this);

    if (thisOption.selectedMode === 'single') {
      // Ensure there is only one selected.
      var hasSel = false;
      zrUtil.each(pieceList, function (piece, index) {
        var key = this.getSelectedMapKey(piece);

        if (selected[key]) {
          hasSel ? selected[key] = false : hasSel = true;
        }
      }, this);
    } // thisOption.selectedMode === 'multiple', default: all selected.

  },

  /**
   * @public
   */
  getSelectedMapKey: function (piece) {
    return this._mode === 'categories' ? piece.value + '' : piece.index + '';
  },

  /**
   * @public
   */
  getPieceList: function () {
    return this._pieceList;
  },

  /**
   * @private
   * @return {string}
   */
  _determineMode: function () {
    var option = this.option;
    return option.pieces && option.pieces.length > 0 ? 'pieces' : this.option.categories ? 'categories' : 'splitNumber';
  },

  /**
   * @public
   * @override
   */
  setSelected: function (selected) {
    this.option.selected = zrUtil.clone(selected);
  },

  /**
   * @public
   * @override
   */
  getValueState: function (value) {
    var index = VisualMapping.findPieceIndex(value, this._pieceList);
    return index != null ? this.option.selected[this.getSelectedMapKey(this._pieceList[index])] ? 'inRange' : 'outOfRange' : 'outOfRange';
  },

  /**
   * @public
   * @params {number} pieceIndex piece index in visualMapModel.getPieceList()
   * @return {Array.<Object>} [{seriesId, dataIndex: <Array.<number>>}, ...]
   */
  findTargetDataIndices: function (pieceIndex) {
    var result = [];
    this.eachTargetSeries(function (seriesModel) {
      var dataIndices = [];
      var data = seriesModel.getData();
      data.each(this.getDataDimension(data), function (value, dataIndex) {
        // Should always base on model pieceList, because it is order sensitive.
        var pIdx = VisualMapping.findPieceIndex(value, this._pieceList);
        pIdx === pieceIndex && dataIndices.push(dataIndex);
      }, this);
      result.push({
        seriesId: seriesModel.id,
        dataIndex: dataIndices
      });
    }, this);
    return result;
  },

  /**
   * @private
   * @param {Object} piece piece.value or piece.interval is required.
   * @return {number} Can be Infinity or -Infinity
   */
  getRepresentValue: function (piece) {
    var representValue;

    if (this.isCategory()) {
      representValue = piece.value;
    } else {
      if (piece.value != null) {
        representValue = piece.value;
      } else {
        var pieceInterval = piece.interval || [];
        representValue = pieceInterval[0] === -Infinity && pieceInterval[1] === Infinity ? 0 : (pieceInterval[0] + pieceInterval[1]) / 2;
      }
    }

    return representValue;
  },
  getVisualMeta: function (getColorVisual) {
    // Do not support category. (category axis is ordinal, numerical)
    if (this.isCategory()) {
      return;
    }

    var stops = [];
    var outerColors = [];
    var visualMapModel = this;

    function setStop(interval, valueState) {
      var representValue = visualMapModel.getRepresentValue({
        interval: interval
      });

      if (!valueState) {
        valueState = visualMapModel.getValueState(representValue);
      }

      var color = getColorVisual(representValue, valueState);

      if (interval[0] === -Infinity) {
        outerColors[0] = color;
      } else if (interval[1] === Infinity) {
        outerColors[1] = color;
      } else {
        stops.push({
          value: interval[0],
          color: color
        }, {
          value: interval[1],
          color: color
        });
      }
    } // Suplement


    var pieceList = this._pieceList.slice();

    if (!pieceList.length) {
      pieceList.push({
        interval: [-Infinity, Infinity]
      });
    } else {
      var edge = pieceList[0].interval[0];
      edge !== -Infinity && pieceList.unshift({
        interval: [-Infinity, edge]
      });
      edge = pieceList[pieceList.length - 1].interval[1];
      edge !== Infinity && pieceList.push({
        interval: [edge, Infinity]
      });
    }

    var curr = -Infinity;
    zrUtil.each(pieceList, function (piece) {
      var interval = piece.interval;

      if (interval) {
        // Fulfill gap.
        interval[0] > curr && setStop([curr, interval[0]], 'outOfRange');
        setStop(interval.slice());
        curr = interval[1];
      }
    }, this);
    return {
      stops: stops,
      outerColors: outerColors
    };
  }
});
/**
 * Key is this._mode
 * @type {Object}
 * @this {module:echarts/component/viusalMap/PiecewiseMode}
 */

var resetMethods = {
  splitNumber: function () {
    var thisOption = this.option;
    var pieceList = this._pieceList;
    var precision = Math.min(thisOption.precision, 20);
    var dataExtent = this.getExtent();
    var splitNumber = thisOption.splitNumber;
    splitNumber = Math.max(parseInt(splitNumber, 10), 1);
    thisOption.splitNumber = splitNumber;
    var splitStep = (dataExtent[1] - dataExtent[0]) / splitNumber; // Precision auto-adaption

    while (+splitStep.toFixed(precision) !== splitStep && precision < 5) {
      precision++;
    }

    thisOption.precision = precision;
    splitStep = +splitStep.toFixed(precision);

    if (thisOption.minOpen) {
      pieceList.push({
        interval: [-Infinity, dataExtent[0]],
        close: [0, 0]
      });
    }

    for (var index = 0, curr = dataExtent[0]; index < splitNumber; curr += splitStep, index++) {
      var max = index === splitNumber - 1 ? dataExtent[1] : curr + splitStep;
      pieceList.push({
        interval: [curr, max],
        close: [1, 1]
      });
    }

    if (thisOption.maxOpen) {
      pieceList.push({
        interval: [dataExtent[1], Infinity],
        close: [0, 0]
      });
    }

    reformIntervals(pieceList);
    zrUtil.each(pieceList, function (piece, index) {
      piece.index = index;
      piece.text = this.formatValueText(piece.interval);
    }, this);
  },
  categories: function () {
    var thisOption = this.option;
    zrUtil.each(thisOption.categories, function (cate) {
      // FIXME category模式也使用pieceList，但在visualMapping中不是使用pieceList。
      // 是否改一致。
      this._pieceList.push({
        text: this.formatValueText(cate, true),
        value: cate
      });
    }, this); // See "Order Rule".

    normalizeReverse(thisOption, this._pieceList);
  },
  pieces: function () {
    var thisOption = this.option;
    var pieceList = this._pieceList;
    zrUtil.each(thisOption.pieces, function (pieceListItem, index) {
      if (!zrUtil.isObject(pieceListItem)) {
        pieceListItem = {
          value: pieceListItem
        };
      }

      var item = {
        text: '',
        index: index
      };

      if (pieceListItem.label != null) {
        item.text = pieceListItem.label;
      }

      if (pieceListItem.hasOwnProperty('value')) {
        var value = item.value = pieceListItem.value;
        item.interval = [value, value];
        item.close = [1, 1];
      } else {
        // `min` `max` is legacy option.
        // `lt` `gt` `lte` `gte` is recommanded.
        var interval = item.interval = [];
        var close = item.close = [0, 0];
        var closeList = [1, 0, 1];
        var infinityList = [-Infinity, Infinity];
        var useMinMax = [];

        for (var lg = 0; lg < 2; lg++) {
          var names = [['gte', 'gt', 'min'], ['lte', 'lt', 'max']][lg];

          for (var i = 0; i < 3 && interval[lg] == null; i++) {
            interval[lg] = pieceListItem[names[i]];
            close[lg] = closeList[i];
            useMinMax[lg] = i === 2;
          }

          interval[lg] == null && (interval[lg] = infinityList[lg]);
        }

        useMinMax[0] && interval[1] === Infinity && (close[0] = 0);
        useMinMax[1] && interval[0] === -Infinity && (close[1] = 0);

        if (interval[0] === interval[1] && close[0] && close[1]) {
          // Consider: [{min: 5, max: 5, visual: {...}}, {min: 0, max: 5}],
          // we use value to lift the priority when min === max
          item.value = interval[0];
        }
      }

      item.visual = VisualMapping.retrieveVisuals(pieceListItem);
      pieceList.push(item);
    }, this); // See "Order Rule".

    normalizeReverse(thisOption, pieceList); // Only pieces

    reformIntervals(pieceList);
    zrUtil.each(pieceList, function (piece) {
      var close = piece.close;
      var edgeSymbols = [['<', '≤'][close[1]], ['>', '≥'][close[0]]];
      piece.text = piece.text || this.formatValueText(piece.value != null ? piece.value : piece.interval, false, edgeSymbols);
    }, this);
  }
};

function normalizeReverse(thisOption, pieceList) {
  var inverse = thisOption.inverse;

  if (thisOption.orient === 'vertical' ? !inverse : inverse) {
    pieceList.reverse();
  }
}

var _default = PiecewiseModel;
module.exports = _default;