import { trimAudioClipStart } from './audio-clips.js';
import { isEditableShortcutTarget } from './audio-playback.js';
import {
  moveSegmentStart,
  resizeSegmentEnd,
  resizeSegmentStart,
} from './timeline-model.js';

const MINIMUM_CLIP_LENGTH = 0.25;
const EPSILON = 0.000001;

function clamp(value, minimum, maximum) {
  return Math.min(maximum, Math.max(minimum, value));
}

function roundedTime(value) {
  return Number(value.toFixed(9));
}

export function normalizeTimelineFrameRate(value, fallback = 30) {
  const requested = Number(value);
  if (Number.isFinite(requested) && requested >= 1 && requested <= 240) return requested;
  const fallbackRate = Number(fallback);
  return Number.isFinite(fallbackRate) && fallbackRate >= 1 && fallbackRate <= 240
    ? fallbackRate
    : 30;
}

export function timelinePlayheadDelta(event, {
  bpm = 129.2,
  frameRate = 60,
} = {}) {
  if (
    (event?.code !== 'ArrowLeft' && event?.code !== 'ArrowRight')
    || event.altKey
    || event.metaKey
    || event.ctrlKey
    || isEditableShortcutTarget(event.target)
  ) {
    return null;
  }

  const direction = event.code === 'ArrowRight' ? 1 : -1;
  if (event.shiftKey) {
    const requestedBpm = Number.parseFloat(bpm);
    const normalizedBpm = Number.isFinite(requestedBpm) && requestedBpm > 0
      ? requestedBpm
      : 129.2;
    return direction * (60 / normalizedBpm);
  }

  return direction / normalizeTimelineFrameRate(frameRate, 60);
}

export function timelineClipShortcut(event) {
  const primaryModifier = Boolean(event.metaKey || event.ctrlKey);
  if (
    event.code === 'KeyR'
    && event.altKey
    && !primaryModifier
    && !event.shiftKey
  ) {
    return { type: 'rename' };
  }

  if (event.code !== 'ArrowLeft' && event.code !== 'ArrowRight') return null;
  const direction = event.code === 'ArrowLeft' ? -1 : 1;

  if (event.altKey && !primaryModifier) {
    return {
      type: direction < 0 ? 'trim-start' : 'trim-end',
      direction,
    };
  }
  if (primaryModifier && !event.altKey) {
    return {
      type: 'nudge',
      direction,
      frames: direction * (event.shiftKey ? 10 : 1),
    };
  }
  return null;
}

export function applyTimelineClipShortcut({
  command,
  segment,
  segments = [],
  index = -1,
  playhead = 0,
  duration = 60,
  frameRate = 30,
  preventOverlap = false,
  minimumLength = MINIMUM_CLIP_LENGTH,
  trackType = '',
}) {
  if (!command || !segment) return { changed: false, reason: 'no-selection' };

  const start = Number(segment.start);
  const end = Number(segment.end);
  if (!Number.isFinite(start) || !Number.isFinite(end) || end - start < minimumLength) {
    return { changed: false, reason: 'invalid-clip' };
  }

  if (command.type === 'trim-start') {
    const boundary = Number(playhead);
    if (
      !Number.isFinite(boundary)
      || boundary < 0
      || boundary > end - minimumLength
    ) {
      return { changed: false, reason: 'playhead-outside' };
    }
    if (Math.abs(boundary - start) <= EPSILON) {
      return { changed: false, reason: 'already-at-playhead' };
    }

    if (boundary < start - EPSILON && trackType === 'audio') {
      const sourceOffset = Math.max(0, Number(segment.sourceOffset || 0));
      if (boundary < Math.max(0, start - sourceOffset) - EPSILON) {
        return { changed: false, reason: 'source-bound' };
      }
    }

    if (boundary < start - EPSILON && preventOverlap) {
      const resized = resizeSegmentStart({
        candidateStart: boundary,
        end,
        segments,
        excludeIndex: index,
        minimumLength,
      });
      if (Math.abs(resized - boundary) > EPSILON) {
        return { changed: false, reason: 'blocked' };
      }
    }

    if (trackType === 'audio') {
      const trimmed = trimAudioClipStart(segment, boundary, minimumLength);
      if (Math.abs(trimmed.start - boundary) > EPSILON) {
        return { changed: false, reason: 'source-bound' };
      }
      return {
        changed: Math.abs(trimmed.start - start) > EPSILON,
        patch: {
          start: roundedTime(trimmed.start),
          sourceOffset: roundedTime(trimmed.sourceOffset),
        },
      };
    }
    return { changed: true, patch: { start: roundedTime(boundary) } };
  }

  if (command.type === 'trim-end') {
    const boundary = Number(playhead);
    if (
      !Number.isFinite(boundary)
      || boundary < start + minimumLength
      || boundary > Number(duration)
    ) {
      return { changed: false, reason: 'playhead-outside' };
    }
    if (Math.abs(boundary - end) <= EPSILON) {
      return { changed: false, reason: 'already-at-playhead' };
    }

    if (boundary > end + EPSILON && trackType === 'audio') {
      const sourceDuration = Number(segment.sourceDuration);
      if (Number.isFinite(sourceDuration)) {
        const sourceOffset = Math.max(0, Number(segment.sourceOffset || 0));
        const latestEnd = start + Math.max(0, sourceDuration - sourceOffset);
        if (boundary > latestEnd + EPSILON) {
          return { changed: false, reason: 'source-bound' };
        }
      }
    }

    if (boundary > end + EPSILON && preventOverlap) {
      const resized = resizeSegmentEnd({
        candidateEnd: boundary,
        start,
        duration,
        segments,
        excludeIndex: index,
        minimumLength,
      });
      if (Math.abs(resized - boundary) > EPSILON) {
        return { changed: false, reason: 'blocked' };
      }
    }

    return { changed: true, patch: { end: roundedTime(boundary) } };
  }

  if (command.type === 'nudge') {
    const fps = normalizeTimelineFrameRate(frameRate);
    const delta = Number(command.frames || command.direction || 0) / fps;
    const length = end - start;
    const maximumStart = Math.max(0, Number(duration) - length);
    const candidateStart = clamp(start + delta, 0, maximumStart);
    let nextStart;
    if (preventOverlap) {
      nextStart = moveSegmentStart({
        candidateStart,
        length,
        duration,
        segments,
        excludeIndex: index,
        originalStart: start,
      });
      if (Math.abs(nextStart - candidateStart) > EPSILON) {
        return { changed: false, reason: 'blocked' };
      }
    } else {
      nextStart = candidateStart;
    }
    nextStart = roundedTime(nextStart);
    if (Math.abs(nextStart - start) <= EPSILON) {
      return { changed: false, reason: 'blocked' };
    }
    return {
      changed: true,
      patch: {
        start: nextStart,
        end: roundedTime(nextStart + length),
      },
    };
  }

  return { changed: false, reason: 'unsupported' };
}
