import _defineProperty from '@babel/runtime/helpers/esm/defineProperty';
import { reactive } from 'vue';

var DIRECTION_TYPE = /*#__PURE__*/function (DIRECTION_TYPE) {
  DIRECTION_TYPE["FRONT"] = "FRONT";
  // scroll up or left
  DIRECTION_TYPE["BEHIND"] = "BEHIND"; // scroll down or right
  return DIRECTION_TYPE;
}(DIRECTION_TYPE || {});
var CALC_TYPE = /*#__PURE__*/function (CALC_TYPE) {
  CALC_TYPE["INIT"] = "INIT";
  CALC_TYPE["FIXED"] = "FIXED";
  CALC_TYPE["DYNAMIC"] = "DYNAMIC";
  return CALC_TYPE;
}(CALC_TYPE || {});
const LEADING_BUFFER = 0;
class Virtual {
  constructor(param, callUpdate) {
    _defineProperty(this, "_scrollRAF", null);
    // 缓存滚动位置计算结果
    _defineProperty(this, "_scrollPositionCache", new Map());
    this.init(param, callUpdate);
  }
  init() {
    let param = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    let callUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
    this.param = param;
    this.callUpdate = callUpdate;

    // size data
    this.sizes = reactive(new Map());
    this.firstRangeTotalSize = 0;
    this.firstRangeAverageSize = 0;
    this.fixedSizeValue = 0;
    this.calcType = CALC_TYPE.INIT;

    // scroll data
    this.offset = 0;

    // range data
    this.range = Object.create(null);

    // 清空所有缓存
    this._scrollPositionCache = new Map();

    // 清除滚动动画帧引用
    if (this._scrollRAF) {
      cancelAnimationFrame(this._scrollRAF);
      this._scrollRAF = null;
    }
    if (this.param && this.callUpdate) {
      this.checkRange(0, this.param.keeps - 1);
    }
  }
  destroy() {
    // 重置内部状态数据
    this.init();
  }

  // return current render range
  getRange() {
    const range = Object.create(null);
    range.start = this.range.start;
    range.end = this.range.end;
    range.padFront = this.range.padFront;
    range.padBehind = this.range.padBehind;
    return range;
  }
  isBehind() {
    return this.direction === DIRECTION_TYPE.BEHIND;
  }
  isFront() {
    return this.direction === DIRECTION_TYPE.FRONT;
  }

  // return start index offset
  getOffset(start) {
    return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize;
  }
  updateParam(key, value) {
    if (this.param && Object.keys(this.param).includes(key)) {
      // if uniqueIds change, find out deleted id and remove from size map
      if (key === 'uniqueIds') {
        this.sizes.forEach((v, k) => {
          if (!value.includes(k)) {
            this.sizes.delete(k);
          }
        });
      }
      this.param[key] = value;
    }
  }

  // save each size map by id
  saveSize(id, size) {
    this.sizes.set(id, size);
    // we assume size type is fixed at the beginning and remember first size value
    // if there is no size value different from this at next comming saving
    // we think it's a fixed size list, otherwise is dynamic size list
    if (this.calcType === CALC_TYPE.INIT) {
      this.fixedSizeValue = size;
      this.calcType = CALC_TYPE.FIXED;
    } else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) {
      this.calcType = CALC_TYPE.DYNAMIC;
      // it's no use at all
      delete this.fixedSizeValue;
    }

    // calculate the average size only in the first range
    if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== 'undefined') {
      if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) {
        this.firstRangeTotalSize = [...this.sizes.values()].reduce((acc, val) => acc + val, 0);
        this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size);
      } else {
        // it's done using
        delete this.firstRangeTotalSize;
      }
    }
  }

  // in some special situation (e.g. length change) we need to update in a row
  // try goiong to render next range by a leading buffer according to current direction
  handleDataSourcesChange() {
    let start = this.range.start;
    if (this.isFront()) {
      start = start - LEADING_BUFFER;
    } else if (this.isBehind()) {
      start = start + LEADING_BUFFER;
    }
    start = Math.max(start, 0);
    this.updateRange(this.range.start, this.getEndByStart(start));
  }

  // when slot size change, we also need force update
  handleSlotSizeChange() {
    this.handleDataSourcesChange();
  }

  // calculating range on scroll
  handleScroll(offset) {
    this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND;
    this.offset = offset;
    if (!this.param) {
      return;
    }

    // 使用requestAnimationFrame优化滚动性能
    if (this._scrollRAF) {
      cancelAnimationFrame(this._scrollRAF);
    }
    this._scrollRAF = requestAnimationFrame(() => {
      if (this.direction === DIRECTION_TYPE.FRONT) {
        this.handleFront();
      } else if (this.direction === DIRECTION_TYPE.BEHIND) {
        this.handleBehind();
      }
    });
  }
  handleFront() {
    const overs = this.getScrollOvers();
    // should not change range if start doesn't exceed overs
    if (overs > this.range.start) {
      return;
    }

    // move up start by a buffer length, and make sure its safety
    const start = Math.max(overs - this.param.buffer, 0);
    this.checkRange(start, this.getEndByStart(start));
  }
  handleBehind() {
    const overs = this.getScrollOvers();
    // range should not change if scroll overs within buffer
    if (overs < this.range.start + this.param.buffer) {
      return;
    }
    this.checkRange(overs, this.getEndByStart(overs));
  }

  // return the pass overs according to current scroll offset
  getScrollOvers() {
    // if slot header exist, we need subtract its size
    const offset = this.offset - this.param.slotHeaderSize;
    if (offset <= 0) {
      return 0;
    }
    // if is fixed type, that can be easily
    if (this.isFixedType()) {
      return Math.floor(offset / this.fixedSizeValue);
    }
    let low = 0;
    let middle = 0;
    let middleOffset = 0;
    let high = this.param.uniqueIds.length;
    while (low <= high) {
      middle = low + Math.floor((high - low) / 2);
      middleOffset = this.getIndexOffset(middle);
      if (middleOffset === offset) {
        return middle;
      }
      if (middleOffset < offset) {
        low = middle + 1;
      } else if (middleOffset > offset) {
        high = middle - 1;
      }
    }
    return low > 0 ? --low : 0;
  }

  // 滚动位置计算方法
  getIndexOffset(givenIndex) {
    if (!givenIndex) {
      return 0;
    }

    // 检查缓存
    const cachedOffset = this._scrollPositionCache.get(givenIndex);
    if (cachedOffset !== undefined) {
      return cachedOffset;
    }
    let offset = 0;
    // 直接计算从0到目标索引的偏移量
    for (let index = 0; index < givenIndex; index++) {
      const indexSize = this.sizes.get(this.param.uniqueIds[index]);
      offset += typeof indexSize === 'number' ? indexSize : this.getEstimateSize();
    }

    // 缓存结果
    this._scrollPositionCache.set(givenIndex, offset);

    // 如果缓存过大，清理一半的缓存
    if (this._scrollPositionCache.size > 1000) {
      const indices = Array.from(this._scrollPositionCache.keys());
      indices.slice(0, indices.length / 2).forEach(index => {
        this._scrollPositionCache.delete(index);
      });
    }
    return offset;
  }

  // is fixed size type
  isFixedType() {
    return this.calcType === CALC_TYPE.FIXED;
  }

  // return the real last index
  getLastIndex() {
    return this.param.uniqueIds.length - 1;
  }

  // in some conditions range is broke, we need correct it
  // and then decide whether need update to next range
  checkRange(start, end) {
    const keeps = this.param.keeps;
    const total = this.param.uniqueIds.length;
    // datas less than keeps, render all
    if (total <= keeps) {
      start = 0;
      end = this.getLastIndex();
    } else if (end - start < keeps - 1) {
      // if range length is less than keeps, corrent it base on end
      start = end - keeps + 1;
    }
    if (this.range.start !== start) {
      this.updateRange(start, end);
    }
  }

  // setting to a new range and rerender
  updateRange(start, end) {
    this.range.start = start;
    this.range.end = end;
    this.range.padFront = this.getPadFront();
    this.range.padBehind = this.getPadBehind();
    this.callUpdate(this.getRange());
  }

  // return end base on start
  getEndByStart(start) {
    const theoryEnd = start + this.param.keeps - 1;
    const truelyEnd = Math.min(theoryEnd, this.getLastIndex());
    return truelyEnd;
  }

  // return total front offset
  getPadFront() {
    if (this.isFixedType()) {
      return this.fixedSizeValue * this.range.start;
    }
    return this.getIndexOffset(this.range.start);
  }

  // return total behind offset
  getPadBehind() {
    const end = this.range.end;
    const lastIndex = this.getLastIndex();
    if (this.isFixedType()) {
      return (lastIndex - end) * this.fixedSizeValue;
    }

    // if not, use a estimated value
    return (lastIndex - end) * this.getEstimateSize();
  }

  // get the item estimate size
  getEstimateSize() {
    return this.isFixedType() ? this.fixedSizeValue : this.firstRangeAverageSize || this.param.estimateSize;
  }
  getTotalSize() {
    return [...this.sizes.values()].reduce((acc, val) => acc + val, 0);
  }
}

export { Virtual as default };
