text: import {valueOrDefault} from 'chart.js/helpers';
  import {getState} from './state';
  
  function zoomDelta(scale, zoom, center) {
    const range = scale.max - scale.min;
    const newRange = range * (zoom - 1);
  
    const centerPoint = scale.isHorizontal() ? center.x : center.y;
    const minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range || 0;
    const maxPercent = 1 - minPercent;
  
    return {
      min: newRange * minPercent,
      max: newRange * maxPercent
    };
  }
  
  function getLimit(state, scale, scaleLimits, prop, fallback) {
    let limit = scaleLimits[prop];
    if (limit === 'original') {
      const original = state.originalScaleLimits[scale.id][prop];
      limit = valueOrDefault(original.options, original.scale);
    }
    return valueOrDefault(limit, fallback);
  }
  
  export function updateRange(scale, {min, max}, limits, zoom = false) {
    const state = getState(scale.chart);
    const {id, axis, options: scaleOpts} = scale;
  
    const scaleLimits = limits && (limits[id] || limits[axis]) || {};
    const {minRange = 0} = scaleLimits;
    const minLimit = getLimit(state, scale, scaleLimits, 'min', -Infinity);
    const maxLimit = getLimit(state, scale, scaleLimits, 'max', Infinity);
  
    const cmin = Math.max(min, minLimit);
    const cmax = Math.min(max, maxLimit);
    const range = zoom ? Math.max(cmax - cmin, minRange) : scale.max - scale.min;
    if (cmax - cmin !== range) {
      if (minLimit > cmax - range) {
        min = cmin;
        max = cmin + range;
      } else if (maxLimit < cmin + range) {
        max = cmax;
        min = cmax - range;
      } else {
        const offset = (range - cmax + cmin) / 2;
        min = cmin - offset;
        max = cmax + offset;
      }
    } else {
      min = cmin;
      max = cmax;
    }
    scaleOpts.min = min;
    scaleOpts.max = max;
  
    state.updatedScaleLimits[scale.id] = {min, max};
  
    // return true if the scale range is changed
    return scale.parse(min) !== scale.min || scale.parse(max) !== scale.max;
  }
  
  function zoomNumericalScale(scale, zoom, center, limits) {
    const delta = zoomDelta(scale, zoom, center);
    const newRange = {min: scale.min + delta.min, max: scale.max - delta.max};
    return updateRange(scale, newRange, limits, true);
  }
  
  const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);
  
  function existCategoryFromMaxZoom(scale) {
    const labels = scale.getLabels();
    const maxIndex = labels.length - 1;
  
    if (scale.min > 0) {
      scale.min -= 1;
    }
    if (scale.max < maxIndex) {
      scale.max += 1;
    }
  }
  
  function zoomCategoryScale(scale, zoom, center, limits) {
    const delta = zoomDelta(scale, zoom, center);
    if (scale.min === scale.max && zoom < 1) {
      existCategoryFromMaxZoom(scale);
    }
    const newRange = {min: scale.min + integerChange(delta.min), max: scale.max - integerChange(delta.max)};
    return updateRange(scale, newRange, limits, true);
  }
  
  function scaleLength(scale) {
    return scale.isHorizontal() ? scale.width : scale.height;
  }
  
  function panCategoryScale(scale, delta, limits) {
    const labels = scale.getLabels();
    const lastLabelIndex = labels.length - 1;
    let {min, max} = scale;
    // The visible range. Ticks can be skipped, and thus not reliable.
    const range = Math.max(max - min, 1);
    // How many pixels of delta is required before making a step. stepSize, but limited to max 1/10 of the scale length.
    const stepDelta = Math.round(scaleLength(scale) / Math.max(range, 10));
    const stepSize = Math.round(Math.abs(delta / stepDelta));
    let applied;
    if (delta < -stepDelta) {
      max = Math.min(max + stepSize, lastLabelIndex);
      min = range === 1 ? max : max - range;
      applied = max === lastLabelIndex;
    } else if (delta > stepDelta) {
      min = Math.max(0, min - stepSize);
      max = range === 1 ? min : min + range;
      applied = min === 0;
    }
  
    return updateRange(scale, {min, max}, limits) || applied;
  }
  
  const OFFSETS = {
    second: 500, // 500 ms
    minute: 30 * 1000, // 30 s
    hour: 30 * 60 * 1000, // 30 m
    day: 12 * 60 * 60 * 1000, // 12 h
    week: 3.5 * 24 * 60 * 60 * 1000, // 3.5 d
    month: 15 * 24 * 60 * 60 * 1000, // 15 d
    quarter: 60 * 24 * 60 * 60 * 1000, // 60 d
    year: 182 * 24 * 60 * 60 * 1000 // 182 d
  };
  
  function panNumericalScale(scale, delta, limits, canZoom = false) {
    const {min: prevStart, max: prevEnd, options} = scale;
    const round = options.time && options.time.round;
    const offset = OFFSETS[round] || 0;
    const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart + offset) - delta);
    const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd + offset) - delta);
    const {min: minLimit = -Infinity, max: maxLimit = Infinity} = canZoom && limits && limits[scale.axis] || {};
    if (isNaN(newMin) || isNaN(newMax) || newMin < minLimit || newMax > maxLimit) {
      // At limit: No change but return true to indicate no need to store the delta.
      // NaN can happen for 0-dimension scales (either because they were configured
      // with min === max or because the chart has 0 plottable area).
      return true;
    }
    return updateRange(scale, {min: newMin, max: newMax}, limits, canZoom);
  }
  
  function panNonLinearScale(scale, delta, limits) {
    return panNumericalScale(scale, delta, limits, true);
  }
  
  export const zoomFunctions = {
    category: zoomCategoryScale,
    default: zoomNumericalScale,
  };
  
  export const panFunctions = {
    category: panCategoryScale,
    default: panNumericalScale,
    logarithmic: panNonLinearScale,
    timeseries: panNonLinearScale,
  };
  