function decimalPlaces(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return 0;
  const text = String(numeric).toLowerCase();
  if (text.includes('e-')) {
    const [coefficient, exponentText] = text.split('e-');
    const coefficientPlaces = coefficient.split('.')[1]?.length || 0;
    return Number(exponentText) + coefficientPlaces;
  }
  return text.split('.')[1]?.length || 0;
}

export function keyframeValuePrecision({
  step,
  min = 0,
  max = 1,
} = {}) {
  const numericStep = Number(step);
  if (Number.isFinite(numericStep) && numericStep > 0) {
    return Math.min(6, Math.max(2, decimalPlaces(numericStep)));
  }
  const span = Math.abs(Number(max) - Number(min));
  if (span < 0.01) return 5;
  if (span < 0.1) return 4;
  if (span < 1) return 3;
  return 2;
}

export function formatKeyframeValue(value, valueSpec = {}) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return '—';
  return numeric.toFixed(keyframeValuePrecision(valueSpec));
}

export function parseKeyframeValue(input, valueSpec = {}) {
  const {
    min = Number.NEGATIVE_INFINITY,
    max = Number.POSITIVE_INFINITY,
  } = valueSpec;
  const normalized = String(input ?? '').trim().replace(',', '.');
  const value = Number(normalized);
  if (!normalized || !Number.isFinite(value)) {
    return { valid: false, error: 'Enter a valid number.' };
  }
  if (value < min || value > max) {
    return {
      valid: false,
      error: `Enter a value between ${formatKeyframeValue(min, valueSpec)} and ${formatKeyframeValue(max, valueSpec)}.`,
    };
  }
  return { valid: true, value };
}

export function keyframeDragActivated({
  startX,
  startY,
  clientX,
  clientY,
  threshold = 2,
}) {
  return Math.hypot(clientX - startX, clientY - startY) >= threshold;
}

export function initialKeyframeAtPlayhead({
  playheadTime,
  segmentStart,
  segmentEnd,
  value,
  min = Number.NEGATIVE_INFINITY,
  max = Number.POSITIVE_INFINITY,
}) {
  const numericPlayhead = Number(playheadTime);
  const numericStart = Number(segmentStart);
  const numericEnd = Number(segmentEnd);
  const numericValue = Number(value);
  if (
    !Number.isFinite(numericPlayhead)
    || !Number.isFinite(numericStart)
    || !Number.isFinite(numericEnd)
    || numericEnd < numericStart
    || !Number.isFinite(numericValue)
  ) {
    return null;
  }

  const boundedPlayhead = Math.max(numericStart, Math.min(numericEnd, numericPlayhead));
  const boundedValue = Math.max(Number(min), Math.min(Number(max), numericValue));
  return {
    time: boundedPlayhead - numericStart,
    value: boundedValue,
    easing: 'linear',
  };
}

export function createKeyframeEditorController({
  waveCanvas,
  rulerCanvas,
  viewport,
  snapButton,
  gridSelect,
  windowTarget,
  documentTarget,
  getMetrics,
  isKeyframeMode,
  hasSelection,
  getSnapEnabled,
  setSnapEnabled,
  onSeek,
  onRedraw,
  labelWidth = 100,
}) {
  const removers = [];
  let scrubbing = false;
  const listen = (target, type, handler) => {
    target?.addEventListener(type, handler);
    if (target) removers.push(() => target.removeEventListener(type, handler));
  };
  const redraw = () => { if (hasSelection()) onRedraw(); };
  const toggleSnap = () => {
    const enabled = !getSnapEnabled();
    setSnapEnabled(enabled);
    snapButton?.classList.toggle('active', enabled);
    redraw();
  };
  const seek = event => {
    const rect = waveCanvas.getBoundingClientRect();
    const metrics = getMetrics();
    const local = (event.clientX - rect.left - labelWidth + (viewport?.scrollLeft || 0)) / metrics.pixelsPerSecond;
    const bounded = Math.max(0, Math.min(metrics.duration, local));
    onSeek(metrics.start + bounded);
  };
  const startScrub = event => {
    const x = event.clientX - waveCanvas.getBoundingClientRect().left;
    if (x < labelWidth) return;
    scrubbing = true;
    seek(event);
    event.preventDefault();
  };

  listen(snapButton, 'click', toggleSnap);
  listen(gridSelect, 'change', redraw);
  listen(waveCanvas, 'mousedown', startScrub);
  listen(rulerCanvas, 'mousedown', startScrub);
  listen(windowTarget, 'mousemove', event => { if (scrubbing) seek(event); });
  listen(windowTarget, 'mouseup', () => { scrubbing = false; });
  listen(documentTarget, 'keydown', event => {
    if (!isKeyframeMode() || ['INPUT', 'TEXTAREA'].includes(event.target?.tagName) || event.code !== 'KeyS') return;
    toggleSnap();
    event.preventDefault();
  });

  return {
    destroy() {
      for (const remove of removers.splice(0)) remove();
      scrubbing = false;
    },
  };
}
