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

const TRACK_TYPE_NAMES = {
  audio: 'Audio',
  scene: 'Scene',
  shader: 'Shader',
  media: 'Media',
  postfx: 'Post FX',
  text: 'Text',
  lut: 'LUT',
};

export function nextTrackIdentity(trackIdCounter) {
  const nextCounter = trackIdCounter + 1;
  return { id: `track_${nextCounter}`, trackIdCounter: nextCounter };
}

export function trackName(type, tracks) {
  const existingCount = tracks.filter(track => track.type === type).length;
  return `${TRACK_TYPE_NAMES[type] || type} ${existingCount + 1}`;
}

export function createDefaultTimelineTracks(trackIdCounter = 0) {
  const definitions = [
    ['audio', 'Audio 1'],
    ['scene', 'Scene 1'],
    ['shader', 'Shader 1'],
    ['media', 'Media 1'],
    ['postfx', 'Post FX 1'],
  ];
  const tracks = [];
  for (const [type, name] of definitions) {
    const identity = nextTrackIdentity(trackIdCounter);
    trackIdCounter = identity.trackIdCounter;
    tracks.push({
      id: identity.id,
      type,
      name,
      order: tracks.length,
      visible: true,
      segments: [],
    });
  }
  return { tracks, trackIdCounter };
}

export function appendTimelineTrack(tracks, type, trackIdCounter) {
  const identity = nextTrackIdentity(trackIdCounter);
  const maxOrder = Math.max(-1, ...tracks.map(track => track.order));
  const track = {
    id: identity.id,
    type,
    name: trackName(type, tracks),
    order: maxOrder + 1,
    visible: true,
    segments: [],
  };
  return { tracks: [...tracks, track], track, trackIdCounter: identity.trackIdCounter };
}

export function findTrack(tracks, trackId) {
  return tracks.find(track => track.id === trackId);
}

export function trackIsVisible(track) {
  return track?.visible !== false;
}

export function replaceMediaSegmentSource(segment, imageFile, isVideo) {
  const path = String(imageFile || '').trim();
  if (!segment || !path) {
    return { segment, changed: false, reason: 'invalid-source' };
  }
  const nextIsVideo = Boolean(isVideo);
  if (segment.imageFile === path && Boolean(segment.isVideo) === nextIsVideo) {
    return { segment, changed: false, reason: 'unchanged' };
  }
  return {
    segment: {
      ...segment,
      imageFile: path,
      isVideo: nextIsVideo,
    },
    changed: true,
  };
}

export function replaceLibrarySegmentPreset(segment, presetDefaults) {
  if (!segment || !presetDefaults) {
    return { segment, changed: false, reason: 'invalid-preset' };
  }
  return {
    segment: {
      ...presetDefaults,
      id: segment.id ?? presetDefaults.id,
      start: segment.start,
      end: segment.end,
    },
    changed: true,
  };
}

export function renameTimelineTrack(tracks, trackId, requestedName) {
  const name = String(requestedName ?? '').trim();
  if (!name) return { tracks, changed: false, reason: 'empty-name' };
  const track = findTrack(tracks, trackId);
  if (!track) return { tracks, changed: false, reason: 'missing-track' };
  if (track.name === name) return { tracks, changed: false, reason: 'unchanged' };
  return {
    tracks: tracks.map(item => (
      item.id === trackId ? { ...item, name } : item
    )),
    changed: true,
    name,
  };
}

export function setTimelineTrackVisibility(tracks, trackId, visible) {
  const track = findTrack(tracks, trackId);
  if (!track) return { tracks, changed: false, reason: 'missing-track' };
  const nextVisible = Boolean(visible);
  if (trackIsVisible(track) === nextVisible) {
    return { tracks, changed: false, reason: 'unchanged', visible: nextVisible };
  }
  return {
    tracks: tracks.map(item => (
      item.id === trackId ? { ...item, visible: nextVisible } : item
    )),
    changed: true,
    visible: nextVisible,
  };
}

export function tracksOfType(tracks, type) {
  return tracks
    .filter(track => track.type === type)
    .sort((left, right) => left.order - right.order);
}

export function sortedTracks(tracks) {
  return [...tracks].sort((left, right) => left.order - right.order);
}

export function removeTimelineTrack(tracks, trackId) {
  return sortedTracks(tracks)
    .filter(track => track.id !== trackId)
    .map((track, order) => ({ ...track, order }));
}

export function reorderTimelineTracks(tracks, trackId, newOrder) {
  const ordered = sortedTracks(tracks);
  const oldIndex = ordered.findIndex(track => track.id === trackId);
  if (oldIndex < 0) return ordered;
  const targetIndex = clamp(newOrder, 0, ordered.length - 1);
  const [moved] = ordered.splice(oldIndex, 1);
  ordered.splice(targetIndex, 0, moved);
  return ordered.map((track, order) => ({ ...track, order }));
}

export function snapTimeToGrid(time, { enabled = true, bpm = 129.2, grid = 0.25 } = {}) {
  if (!enabled) return time;
  const safeBpm = Number.parseFloat(bpm) || 129.2;
  const safeGrid = Number.parseFloat(grid) || 0.25;
  const step = (60 / safeBpm) * safeGrid;
  return Math.round(time / step) * step;
}

export function rangesOverlap(start, end, otherStart, otherEnd) {
  return start < otherEnd && end > otherStart;
}

export function findAvailableSegmentStart(segments, desiredStart, length) {
  let start = Math.max(0, desiredStart);
  const safeLength = Math.max(0, length);
  const ordered = [...segments].sort((left, right) => left.start - right.start);
  for (const segment of ordered) {
    if (segment.end <= start) continue;
    if (start + safeLength <= segment.start) return start;
    start = Math.max(start, segment.end);
  }
  return start;
}

export function findNearestAvailableSegmentRange(
  segments,
  desiredStart,
  length,
  timelineDuration,
) {
  const preferredStart = Math.max(0, Number(desiredStart) || 0);
  const safeLength = Math.max(0, Number(length) || 0);
  const safeTimelineDuration = Math.max(0, Number(timelineDuration) || 0);
  if (safeLength === 0) {
    return {
      start: preferredStart,
      end: preferredStart,
      extended: preferredStart > safeTimelineDuration,
    };
  }

  const ordered = [...(segments || [])]
    .filter(segment => (
      Number.isFinite(segment?.start)
      && Number.isFinite(segment?.end)
      && segment.end > segment.start
    ))
    .sort((left, right) => left.start - right.start);
  const gaps = [];
  let cursor = 0;
  for (const segment of ordered) {
    const occupiedStart = Math.max(0, segment.start);
    const occupiedEnd = Math.max(occupiedStart, segment.end);
    if (occupiedStart > cursor) {
      gaps.push({ start: cursor, end: Math.min(occupiedStart, safeTimelineDuration) });
    }
    cursor = Math.max(cursor, occupiedEnd);
    if (cursor >= safeTimelineDuration) break;
  }
  if (cursor < safeTimelineDuration) {
    gaps.push({ start: cursor, end: safeTimelineDuration });
  }

  let bestStart = null;
  let bestDistance = Infinity;
  for (const gap of gaps) {
    if (gap.end - gap.start < safeLength) continue;
    const candidate = clamp(preferredStart, gap.start, gap.end - safeLength);
    const distance = Math.abs(candidate - preferredStart);
    const candidateIsForward = candidate >= preferredStart;
    const bestIsForward = bestStart !== null && bestStart >= preferredStart;
    if (
      distance < bestDistance
      || (
        distance === bestDistance
        && (
          bestStart === null
          || (candidateIsForward && !bestIsForward)
          || (candidateIsForward === bestIsForward && candidate < bestStart)
        )
      )
    ) {
      bestStart = candidate;
      bestDistance = distance;
    }
  }

  if (bestStart !== null) {
    return {
      start: bestStart,
      end: bestStart + safeLength,
      extended: false,
    };
  }

  const contentEnd = ordered.reduce(
    (end, segment) => Math.max(end, segment.end),
    safeTimelineDuration,
  );
  const extendedStart = Math.max(preferredStart, contentEnd);
  return {
    start: extendedStart,
    end: extendedStart + safeLength,
    extended: true,
  };
}

export function timelineContentEnd(tracks, minimum = 0) {
  let end = minimum;
  for (const track of tracks) {
    for (const segment of track.segments || []) {
      if (Number.isFinite(segment.end)) end = Math.max(end, segment.end);
    }
  }
  return end;
}

export function normalizeProjectDuration(value, tracks = [], fallback = 60) {
  const requested = Number(value);
  const fallbackDuration = Number(fallback);
  const baseDuration = Number.isFinite(requested) && requested > 0
    ? requested
    : Number.isFinite(fallbackDuration) && fallbackDuration > 0
      ? fallbackDuration
      : 60;
  return Math.max(1, baseDuration, timelineContentEnd(tracks));
}

export function moveSegmentStart({
  candidateStart,
  length,
  duration,
  segments,
  excludeIndex = null,
  originalStart,
}) {
  let start = clamp(candidateStart, 0, duration - length);
  for (let index = 0; index < segments.length; index += 1) {
    if (index === excludeIndex) continue;
    const other = segments[index];
    if (rangesOverlap(start, start + length, other.start, other.end)) {
      start = originalStart < other.start ? other.start - length : other.end;
    }
  }
  return clamp(start, 0, duration - length);
}

export function resizeSegmentStart({
  candidateStart,
  end,
  segments,
  excludeIndex,
  minimumLength = 0.25,
}) {
  let start = clamp(candidateStart, 0, end - minimumLength);
  for (let index = 0; index < segments.length; index += 1) {
    if (index === excludeIndex) continue;
    const other = segments[index];
    if (other.end <= end && other.end > start) start = other.end;
  }
  return start;
}

export function resizeSegmentEnd({
  candidateEnd,
  start,
  duration,
  segments,
  excludeIndex,
  minimumLength = 0.25,
}) {
  let end = clamp(candidateEnd, start + minimumLength, duration);
  for (let index = 0; index < segments.length; index += 1) {
    if (index === excludeIndex) continue;
    const other = segments[index];
    if (other.start >= start && other.start < end) end = other.start;
  }
  return end;
}
