/*
* 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.
*/


/**
 * AUTO-GENERATED FILE. DO NOT MODIFY.
 */

/*
* 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.
*/
// Universal transitions that can animate between any shapes(series) and any properties in any amounts.
import { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series.js';
import { createHashMap, each, map, filter, isArray, extend } from 'zrender/lib/core/util.js';
import { applyMorphAnimation, getPathList } from './morphTransitionHelper.js';
import Path from 'zrender/lib/graphic/Path.js';
import { initProps } from '../util/graphic.js';
import DataDiffer from '../data/DataDiffer.js';
import { makeInner, normalizeToArray } from '../util/model.js';
import { warn } from '../util/log.js';
import { getAnimationConfig, getOldStyle } from './basicTransition.js';
import Displayable from 'zrender/lib/graphic/Displayable.js';
var DATA_COUNT_THRESHOLD = 1e4;
;
var getUniversalTransitionGlobalStore = makeInner();

function getGroupIdDimension(data) {
  var dimensions = data.dimensions;

  for (var i = 0; i < dimensions.length; i++) {
    var dimInfo = data.getDimensionInfo(dimensions[i]);

    if (dimInfo && dimInfo.otherDims.itemGroupId === 0) {
      return dimensions[i];
    }
  }
}

function flattenDataDiffItems(list) {
  var items = [];
  each(list, function (seriesInfo) {
    var data = seriesInfo.data;

    if (data.count() > DATA_COUNT_THRESHOLD) {
      if (process.env.NODE_ENV !== 'production') {
        warn('Universal transition is disabled on large data > 10k.');
      }

      return;
    }

    var indices = data.getIndices();
    var groupDim = getGroupIdDimension(data);

    for (var dataIndex = 0; dataIndex < indices.length; dataIndex++) {
      items.push({
        dataGroupId: seriesInfo.dataGroupId,
        data: data,
        dim: seriesInfo.dim || groupDim,
        divide: seriesInfo.divide,
        dataIndex: dataIndex
      });
    }
  });
  return items;
}

function fadeInElement(newEl, newSeries, newIndex) {
  newEl.traverse(function (el) {
    if (el instanceof Path) {
      // TODO use fade in animation for target element.
      initProps(el, {
        style: {
          opacity: 0
        }
      }, newSeries, {
        dataIndex: newIndex,
        isFrom: true
      });
    }
  });
}

function removeEl(el) {
  if (el.parent) {
    // Bake parent transform to element.
    // So it can still have proper transform to transition after it's removed.
    var computedTransform = el.getComputedTransform();
    el.setLocalTransform(computedTransform);
    el.parent.remove(el);
  }
}

function stopAnimation(el) {
  el.stopAnimation();

  if (el.isGroup) {
    el.traverse(function (child) {
      child.stopAnimation();
    });
  }
}

function animateElementStyles(el, dataIndex, seriesModel) {
  var animationConfig = getAnimationConfig('update', seriesModel, dataIndex);
  animationConfig && el.traverse(function (child) {
    if (child instanceof Displayable) {
      var oldStyle = getOldStyle(child);

      if (oldStyle) {
        child.animateFrom({
          style: oldStyle
        }, animationConfig);
      }
    }
  });
}

function isAllIdSame(oldDiffItems, newDiffItems) {
  var len = oldDiffItems.length;

  if (len !== newDiffItems.length) {
    return false;
  }

  for (var i = 0; i < len; i++) {
    var oldItem = oldDiffItems[i];
    var newItem = newDiffItems[i];

    if (oldItem.data.getId(oldItem.dataIndex) !== newItem.data.getId(newItem.dataIndex)) {
      return false;
    }
  }

  return true;
}

function transitionBetween(oldList, newList, api) {
  var oldDiffItems = flattenDataDiffItems(oldList);
  var newDiffItems = flattenDataDiffItems(newList);

  function updateMorphingPathProps(from, to, rawFrom, rawTo, animationCfg) {
    if (rawFrom || from) {
      to.animateFrom({
        style: rawFrom && rawFrom !== from ? // dividingMethod like clone may override the style(opacity)
        // So extend it to raw style.
        extend(extend({}, rawFrom.style), from.style) : from.style
      }, animationCfg);
    }
  }

  function findKeyDim(items) {
    for (var i = 0; i < items.length; i++) {
      if (items[i].dim) {
        return items[i].dim;
      }
    }
  }

  var oldKeyDim = findKeyDim(oldDiffItems);
  var newKeyDim = findKeyDim(newDiffItems);
  var hasMorphAnimation = false;

  function createKeyGetter(isOld, onlyGetId) {
    return function (diffItem) {
      var data = diffItem.data;
      var dataIndex = diffItem.dataIndex; // TODO if specified dim

      if (onlyGetId) {
        return data.getId(dataIndex);
      } // Use group id as transition key by default.
      // So we can achieve multiple to multiple animation like drilldown / up naturally.
      // If group id not exits. Use id instead. If so, only one to one transition will be applied.


      var dataGroupId = diffItem.dataGroupId; // If specified key dimension(itemGroupId by default). Use this same dimension from other data.
      // PENDING: If only use key dimension of newData.

      var keyDim = isOld ? oldKeyDim || newKeyDim : newKeyDim || oldKeyDim;
      var dimInfo = keyDim && data.getDimensionInfo(keyDim);
      var dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta;

      if (dimInfo) {
        // Get from encode.itemGroupId.
        var key = data.get(dimInfo.name, dataIndex);

        if (dimOrdinalMeta) {
          return dimOrdinalMeta.categories[key] || key + '';
        }

        return key + '';
      } // Get groupId from raw item. { groupId: '' }


      var itemVal = data.getRawDataItem(dataIndex);

      if (itemVal && itemVal.groupId) {
        return itemVal.groupId + '';
      }

      return dataGroupId || data.getId(dataIndex);
    };
  } // Use id if it's very likely to be an one to one animation
  // It's more robust than groupId
  // TODO Check if key dimension is specified.


  var useId = isAllIdSame(oldDiffItems, newDiffItems);
  var isElementStillInChart = {};

  if (!useId) {
    // We may have different diff strategy with basicTransition if we use other dimension as key.
    // If so, we can't simply check if oldEl is same with newEl. We need a map to check if oldEl is still being used in the new chart.
    // We can't use the elements that already being morphed. Let it keep it's original basic transition.
    for (var i = 0; i < newDiffItems.length; i++) {
      var newItem = newDiffItems[i];
      var el = newItem.data.getItemGraphicEl(newItem.dataIndex);

      if (el) {
        isElementStillInChart[el.id] = true;
      }
    }
  }

  function updateOneToOne(newIndex, oldIndex) {
    var oldItem = oldDiffItems[oldIndex];
    var newItem = newDiffItems[newIndex];
    var newSeries = newItem.data.hostModel; // TODO Mark this elements is morphed and don't morph them anymore

    var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex);
    var newEl = newItem.data.getItemGraphicEl(newItem.dataIndex); // Can't handle same elements.

    if (oldEl === newEl) {
      newEl && animateElementStyles(newEl, newItem.dataIndex, newSeries);
      return;
    }

    if ( // We can't use the elements that already being morphed
    oldEl && isElementStillInChart[oldEl.id]) {
      return;
    }

    if (newEl) {
      // TODO: If keep animating the group in case
      // some of the elements don't want to be morphed.
      // TODO Label?
      stopAnimation(newEl);

      if (oldEl) {
        stopAnimation(oldEl); // If old element is doing leaving animation. stop it and remove it immediately.

        removeEl(oldEl);
        hasMorphAnimation = true;
        applyMorphAnimation(getPathList(oldEl), getPathList(newEl), newItem.divide, newSeries, newIndex, updateMorphingPathProps);
      } else {
        fadeInElement(newEl, newSeries, newIndex);
      }
    } // else keep oldEl leaving animation.

  }

  new DataDiffer(oldDiffItems, newDiffItems, createKeyGetter(true, useId), createKeyGetter(false, useId), null, 'multiple').update(updateOneToOne).updateManyToOne(function (newIndex, oldIndices) {
    var newItem = newDiffItems[newIndex];
    var newData = newItem.data;
    var newSeries = newData.hostModel;
    var newEl = newData.getItemGraphicEl(newItem.dataIndex);
    var oldElsList = filter(map(oldIndices, function (idx) {
      return oldDiffItems[idx].data.getItemGraphicEl(oldDiffItems[idx].dataIndex);
    }), function (oldEl) {
      return oldEl && oldEl !== newEl && !isElementStillInChart[oldEl.id];
    });

    if (newEl) {
      stopAnimation(newEl);

      if (oldElsList.length) {
        // If old element is doing leaving animation. stop it and remove it immediately.
        each(oldElsList, function (oldEl) {
          stopAnimation(oldEl);
          removeEl(oldEl);
        });
        hasMorphAnimation = true;
        applyMorphAnimation(getPathList(oldElsList), getPathList(newEl), newItem.divide, newSeries, newIndex, updateMorphingPathProps);
      } else {
        fadeInElement(newEl, newSeries, newItem.dataIndex);
      }
    } // else keep oldEl leaving animation.

  }).updateOneToMany(function (newIndices, oldIndex) {
    var oldItem = oldDiffItems[oldIndex];
    var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); // We can't use the elements that already being morphed

    if (oldEl && isElementStillInChart[oldEl.id]) {
      return;
    }

    var newElsList = filter(map(newIndices, function (idx) {
      return newDiffItems[idx].data.getItemGraphicEl(newDiffItems[idx].dataIndex);
    }), function (el) {
      return el && el !== oldEl;
    });
    var newSeris = newDiffItems[newIndices[0]].data.hostModel;

    if (newElsList.length) {
      each(newElsList, function (newEl) {
        return stopAnimation(newEl);
      });

      if (oldEl) {
        stopAnimation(oldEl); // If old element is doing leaving animation. stop it and remove it immediately.

        removeEl(oldEl);
        hasMorphAnimation = true;
        applyMorphAnimation(getPathList(oldEl), getPathList(newElsList), oldItem.divide, // Use divide on old.
        newSeris, newIndices[0], updateMorphingPathProps);
      } else {
        each(newElsList, function (newEl) {
          return fadeInElement(newEl, newSeris, newIndices[0]);
        });
      }
    } // else keep oldEl leaving animation.

  }).updateManyToMany(function (newIndices, oldIndices) {
    // If two data are same and both have groupId.
    // Normally they should be diff by id.
    new DataDiffer(oldIndices, newIndices, function (rawIdx) {
      return oldDiffItems[rawIdx].data.getId(oldDiffItems[rawIdx].dataIndex);
    }, function (rawIdx) {
      return newDiffItems[rawIdx].data.getId(newDiffItems[rawIdx].dataIndex);
    }).update(function (newIndex, oldIndex) {
      // Use the original index
      updateOneToOne(newIndices[newIndex], oldIndices[oldIndex]);
    }).execute();
  }).execute();

  if (hasMorphAnimation) {
    each(newList, function (_a) {
      var data = _a.data;
      var seriesModel = data.hostModel;
      var view = seriesModel && api.getViewOfSeriesModel(seriesModel);
      var animationCfg = getAnimationConfig('update', seriesModel, 0); // use 0 index.

      if (view && seriesModel.isAnimationEnabled() && animationCfg && animationCfg.duration > 0) {
        view.group.traverse(function (el) {
          if (el instanceof Path && !el.animators.length) {
            // We can't accept there still exists element that has no animation
            // if universalTransition is enabled
            el.animateFrom({
              style: {
                opacity: 0
              }
            }, animationCfg);
          }
        });
      }
    });
  }
}

function getSeriesTransitionKey(series) {
  var seriesKey = series.getModel('universalTransition').get('seriesKey');

  if (!seriesKey) {
    // Use series id by default.
    return series.id;
  }

  return seriesKey;
}

function convertArraySeriesKeyToString(seriesKey) {
  if (isArray(seriesKey)) {
    // Order independent.
    return seriesKey.sort().join(',');
  }

  return seriesKey;
}

function getDivideShapeFromData(data) {
  if (data.hostModel) {
    return data.hostModel.getModel('universalTransition').get('divideShape');
  }
}

function findTransitionSeriesBatches(globalStore, params) {
  var updateBatches = createHashMap();
  var oldDataMap = createHashMap(); // Map that only store key in array seriesKey.
  // Which is used to query the old data when transition from one to multiple series.

  var oldDataMapForSplit = createHashMap();
  each(globalStore.oldSeries, function (series, idx) {
    var oldDataGroupId = globalStore.oldDataGroupIds[idx];
    var oldData = globalStore.oldData[idx];
    var transitionKey = getSeriesTransitionKey(series);
    var transitionKeyStr = convertArraySeriesKeyToString(transitionKey);
    oldDataMap.set(transitionKeyStr, {
      dataGroupId: oldDataGroupId,
      data: oldData
    });

    if (isArray(transitionKey)) {
      // Same key can't in different array seriesKey.
      each(transitionKey, function (key) {
        oldDataMapForSplit.set(key, {
          key: transitionKeyStr,
          dataGroupId: oldDataGroupId,
          data: oldData
        });
      });
    }
  });

  function checkTransitionSeriesKeyDuplicated(transitionKeyStr) {
    if (updateBatches.get(transitionKeyStr)) {
      warn("Duplicated seriesKey in universalTransition " + transitionKeyStr);
    }
  }

  each(params.updatedSeries, function (series) {
    if (series.isUniversalTransitionEnabled() && series.isAnimationEnabled()) {
      var newDataGroupId = series.get('dataGroupId');
      var newData = series.getData();
      var transitionKey = getSeriesTransitionKey(series);
      var transitionKeyStr = convertArraySeriesKeyToString(transitionKey); // Only transition between series with same id.

      var oldData = oldDataMap.get(transitionKeyStr); // string transition key is the best match.

      if (oldData) {
        if (process.env.NODE_ENV !== 'production') {
          checkTransitionSeriesKeyDuplicated(transitionKeyStr);
        } // TODO check if data is same?


        updateBatches.set(transitionKeyStr, {
          oldSeries: [{
            dataGroupId: oldData.dataGroupId,
            divide: getDivideShapeFromData(oldData.data),
            data: oldData.data
          }],
          newSeries: [{
            dataGroupId: newDataGroupId,
            divide: getDivideShapeFromData(newData),
            data: newData
          }]
        });
      } else {
        // Transition from multiple series.
        if (isArray(transitionKey)) {
          if (process.env.NODE_ENV !== 'production') {
            checkTransitionSeriesKeyDuplicated(transitionKeyStr);
          }

          var oldSeries_1 = [];
          each(transitionKey, function (key) {
            var oldData = oldDataMap.get(key);

            if (oldData.data) {
              oldSeries_1.push({
                dataGroupId: oldData.dataGroupId,
                divide: getDivideShapeFromData(oldData.data),
                data: oldData.data
              });
            }
          });

          if (oldSeries_1.length) {
            updateBatches.set(transitionKeyStr, {
              oldSeries: oldSeries_1,
              newSeries: [{
                dataGroupId: newDataGroupId,
                data: newData,
                divide: getDivideShapeFromData(newData)
              }]
            });
          }
        } else {
          // Try transition to multiple series.
          var oldData_1 = oldDataMapForSplit.get(transitionKey);

          if (oldData_1) {
            var batch = updateBatches.get(oldData_1.key);

            if (!batch) {
              batch = {
                oldSeries: [{
                  dataGroupId: oldData_1.dataGroupId,
                  data: oldData_1.data,
                  divide: getDivideShapeFromData(oldData_1.data)
                }],
                newSeries: []
              };
              updateBatches.set(oldData_1.key, batch);
            }

            batch.newSeries.push({
              dataGroupId: newDataGroupId,
              data: newData,
              divide: getDivideShapeFromData(newData)
            });
          }
        }
      }
    }
  });
  return updateBatches;
}

function querySeries(series, finder) {
  for (var i = 0; i < series.length; i++) {
    var found = finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex || finder.seriesId != null && finder.seriesId === series[i].id;

    if (found) {
      return i;
    }
  }
}

function transitionSeriesFromOpt(transitionOpt, globalStore, params, api) {
  var from = [];
  var to = [];
  each(normalizeToArray(transitionOpt.from), function (finder) {
    var idx = querySeries(globalStore.oldSeries, finder);

    if (idx >= 0) {
      from.push({
        dataGroupId: globalStore.oldDataGroupIds[idx],
        data: globalStore.oldData[idx],
        // TODO can specify divideShape in transition.
        divide: getDivideShapeFromData(globalStore.oldData[idx]),
        dim: finder.dimension
      });
    }
  });
  each(normalizeToArray(transitionOpt.to), function (finder) {
    var idx = querySeries(params.updatedSeries, finder);

    if (idx >= 0) {
      var data = params.updatedSeries[idx].getData();
      to.push({
        dataGroupId: globalStore.oldDataGroupIds[idx],
        data: data,
        divide: getDivideShapeFromData(data),
        dim: finder.dimension
      });
    }
  });

  if (from.length > 0 && to.length > 0) {
    transitionBetween(from, to, api);
  }
}

export function installUniversalTransition(registers) {
  registers.registerUpdateLifecycle('series:beforeupdate', function (ecMOdel, api, params) {
    each(normalizeToArray(params.seriesTransition), function (transOpt) {
      each(normalizeToArray(transOpt.to), function (finder) {
        var series = params.updatedSeries;

        for (var i = 0; i < series.length; i++) {
          if (finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex || finder.seriesId != null && finder.seriesId === series[i].id) {
            series[i][SERIES_UNIVERSAL_TRANSITION_PROP] = true;
          }
        }
      });
    });
  });
  registers.registerUpdateLifecycle('series:transition', function (ecModel, api, params) {
    // TODO api provide an namespace that can save stuff per instance
    var globalStore = getUniversalTransitionGlobalStore(api); // TODO multiple to multiple series.

    if (globalStore.oldSeries && params.updatedSeries && params.optionChanged) {
      // Use give transition config if its' give;
      var transitionOpt = params.seriesTransition;

      if (transitionOpt) {
        each(normalizeToArray(transitionOpt), function (opt) {
          transitionSeriesFromOpt(opt, globalStore, params, api);
        });
      } else {
        // Else guess from series based on transition series key.
        var updateBatches_1 = findTransitionSeriesBatches(globalStore, params);
        each(updateBatches_1.keys(), function (key) {
          var batch = updateBatches_1.get(key);
          transitionBetween(batch.oldSeries, batch.newSeries, api);
        });
      } // Reset


      each(params.updatedSeries, function (series) {
        // Reset;
        if (series[SERIES_UNIVERSAL_TRANSITION_PROP]) {
          series[SERIES_UNIVERSAL_TRANSITION_PROP] = false;
        }
      });
    } // Save all series of current update. Not only the updated one.


    var allSeries = ecModel.getSeries();
    var savedSeries = globalStore.oldSeries = [];
    var savedDataGroupIds = globalStore.oldDataGroupIds = [];
    var savedData = globalStore.oldData = [];

    for (var i = 0; i < allSeries.length; i++) {
      var data = allSeries[i].getData(); // Only save the data that can have transition.
      // Avoid large data costing too much extra memory

      if (data.count() < DATA_COUNT_THRESHOLD) {
        savedSeries.push(allSeries[i]);
        savedDataGroupIds.push(allSeries[i].get('dataGroupId'));
        savedData.push(data);
      }
    }
  });
}