import { interpolateKeyframes } from '../keyframes.js';
import { combineAnimatedValue } from '../lfo.js';

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

function audioSegmentId(trackId, index) {
  return `audios_${trackId}_${index}`;
}

function volumeAtTime({
  segment,
  segmentId,
  localTime,
  keyframeData,
  curveTypes,
  lfoData,
  bpm,
}) {
  const keyframes = keyframeData?.[segmentId]?.audio_volume;
  const lfo = lfoData?.[segmentId]?.audio_volume;
  const curveType = curveTypes?.[segmentId]?.audio_volume || 'linear';
  const keyframeValue = interpolateKeyframes(keyframes, localTime, curveType);
  const bounds = segment.sliderBounds?.audio_volume || { min: 0, max: 1 };
  const combined = combineAnimatedValue(
    keyframeValue,
    lfo,
    localTime,
    {
      min: Number.isFinite(Number(bounds.min)) ? Number(bounds.min) : 0,
      max: Number.isFinite(Number(bounds.max)) ? Number(bounds.max) : 1,
    },
    bpm,
  );
  const fallback = Number(segment.volume ?? 1);
  return clamp(Number.isFinite(combined) ? combined : fallback, 0, 1);
}

export function buildAudioMixPlan({
  tracks = [],
  rangeStart = 0,
  rangeEnd = 0,
  keyframeData = {},
  curveTypes = {},
  lfoData = {},
  bpm = 129.2,
  automationRate = 60,
} = {}) {
  const start = Math.max(0, Number(rangeStart) || 0);
  const end = Math.max(start, Number(rangeEnd) || start);
  const clips = [];

  for (const track of tracks) {
    if (track.type !== 'audio' || track.visible === false) continue;
    const segments = track.segments || [];
    for (let index = 0; index < segments.length; index += 1) {
      const segment = segments[index];
      if (!segment?.audioFile || segment.muted) continue;
      const clipStart = Number(segment.start) || 0;
      const clipEnd = Math.max(clipStart, Number(segment.end) || clipStart);
      const overlapStart = Math.max(start, clipStart);
      const overlapEnd = Math.min(end, clipEnd);
      if (overlapEnd <= overlapStart) continue;

      const sourceOffset = Math.max(0, Number(segment.sourceOffset) || 0)
        + overlapStart - clipStart;
      const sourceDuration = Number(segment.sourceDuration);
      const availableSource = Number.isFinite(sourceDuration)
        ? Math.max(0, sourceDuration - sourceOffset)
        : Infinity;
      const duration = Math.min(overlapEnd - overlapStart, availableSource);
      if (!(duration > 0)) continue;

      const segmentId = audioSegmentId(track.id, index);
      const hasKeyframes = Boolean(keyframeData?.[segmentId]?.audio_volume?.length);
      const hasLfo = Boolean(lfoData?.[segmentId]?.audio_volume?.enabled);
      let volumeCurve = null;
      if (hasKeyframes || hasLfo) {
        const pointCount = Math.max(2, Math.ceil(duration * automationRate) + 1);
        volumeCurve = new Float32Array(pointCount);
        for (let point = 0; point < pointCount; point += 1) {
          const relativeTime = (point / (pointCount - 1)) * duration;
          volumeCurve[point] = volumeAtTime({
            segment,
            segmentId,
            localTime: overlapStart + relativeTime - clipStart,
            keyframeData,
            curveTypes,
            lfoData,
            bpm,
          });
        }
      }

      clips.push({
        segmentId,
        trackId: track.id,
        audioFile: segment.audioFile,
        destinationStart: overlapStart - start,
        sourceOffset,
        duration,
        volume: clamp(Number(segment.volume ?? 1), 0, 1),
        volumeCurve,
      });
    }
  }

  return {
    start,
    end,
    duration: end - start,
    clips,
  };
}

export function audioBufferToWav(audioBuffer) {
  const channels = Math.max(1, Math.min(2, Number(audioBuffer.numberOfChannels) || 1));
  const sampleRate = Number(audioBuffer.sampleRate) || 48000;
  const frameCount = Number(audioBuffer.length) || 0;
  const channelData = Array.from(
    { length: channels },
    (_, channel) => audioBuffer.getChannelData(channel),
  );

  let peak = 0;
  for (const samples of channelData) {
    for (let index = 0; index < frameCount; index += 1) {
      peak = Math.max(peak, Math.abs(samples[index] || 0));
    }
  }
  const gain = peak > 1 ? 0.98 / peak : 1;
  const bytesPerSample = 2;
  const dataSize = frameCount * channels * bytesPerSample;
  const buffer = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buffer);
  const writeAscii = (offset, value) => {
    for (let index = 0; index < value.length; index += 1) {
      view.setUint8(offset + index, value.charCodeAt(index));
    }
  };

  writeAscii(0, 'RIFF');
  view.setUint32(4, 36 + dataSize, true);
  writeAscii(8, 'WAVE');
  writeAscii(12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, channels, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * channels * bytesPerSample, true);
  view.setUint16(32, channels * bytesPerSample, true);
  view.setUint16(34, bytesPerSample * 8, true);
  writeAscii(36, 'data');
  view.setUint32(40, dataSize, true);

  let offset = 44;
  for (let frame = 0; frame < frameCount; frame += 1) {
    for (let channel = 0; channel < channels; channel += 1) {
      const sample = clamp((channelData[channel][frame] || 0) * gain, -1, 1);
      view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
      offset += bytesPerSample;
    }
  }
  return new Uint8Array(buffer);
}

export async function renderAudioMixToWav({
  plan,
  fetchImpl = fetch,
  decodeAudioData,
  createOfflineAudioContext = (channels, length, sampleRate) => (
    new OfflineAudioContext(channels, length, sampleRate)
  ),
  sampleRate = 48000,
  onProgress = () => {},
}) {
  if (!plan?.clips?.length || !(plan.duration > 0)) return null;
  if (typeof decodeAudioData !== 'function') {
    throw new Error('Audio decoder is unavailable');
  }

  const frameCount = Math.max(1, Math.ceil(plan.duration * sampleRate));
  const context = createOfflineAudioContext(2, frameCount, sampleRate);
  const decoded = new Map();
  const uniqueFiles = [...new Set(plan.clips.map(clip => clip.audioFile))];

  for (let index = 0; index < uniqueFiles.length; index += 1) {
    const audioFile = uniqueFiles[index];
    onProgress({ stage: 'decode', index, total: uniqueFiles.length, audioFile });
    const source = audioFile.startsWith('/') ? audioFile : `/${audioFile}`;
    const response = await fetchImpl(source);
    if (!response?.ok) {
      throw new Error(`Could not load audio for export: ${audioFile} (HTTP ${response?.status || 'error'})`);
    }
    try {
      decoded.set(audioFile, await decodeAudioData(await response.arrayBuffer()));
    } catch (error) {
      throw new Error(`Could not decode audio for export: ${audioFile}`, { cause: error });
    }
  }

  for (const clip of plan.clips) {
    const buffer = decoded.get(clip.audioFile);
    const available = Math.max(0, Number(buffer.duration) - clip.sourceOffset);
    const duration = Math.min(clip.duration, available);
    if (!(duration > 0)) continue;

    const source = context.createBufferSource();
    const gain = context.createGain();
    source.buffer = buffer;
    source.connect(gain);
    gain.connect(context.destination);
    if (clip.volumeCurve?.length >= 2) {
      gain.gain.setValueCurveAtTime(
        clip.volumeCurve,
        clip.destinationStart,
        Math.max(duration, 1 / sampleRate),
      );
    } else {
      gain.gain.setValueAtTime(clip.volume, clip.destinationStart);
    }
    source.start(clip.destinationStart, clip.sourceOffset, duration);
  }

  onProgress({ stage: 'render', clips: plan.clips.length });
  const rendered = await context.startRendering();
  onProgress({ stage: 'encode', frames: rendered.length });
  return audioBufferToWav(rendered);
}
