/**
 * musicSynth — процедурный синтезатор GD-музыки на Web Audio API.
 *
 * Используется как fallback пока нет реальных mp3-файлов.
 * Принимает параметры из musicCatalog.fallbackSynth и играет:
 *   - басовую партию (4/4 кик + бас по тонике)
 *   - арпеджио лида по выбранной шкале
 *   - хайхэт (шум) для ритма
 *
 * Это НЕ замена настоящей музыке — это инструмент для проверки темпа.
 * Когда МИН закачает реальные треки, синтез автоматически отключается.
 */

import { SCALES } from './musicCatalog';

const MOOD_PARAMS = {
  calm:       { leadGain: 0.10, bassGain: 0.18, density: 0.5, drumGain: 0.20 },
  cold:       { leadGain: 0.12, bassGain: 0.20, density: 0.6, drumGain: 0.22 },
  dark:       { leadGain: 0.10, bassGain: 0.22, density: 0.6, drumGain: 0.24 },
  energetic:  { leadGain: 0.13, bassGain: 0.22, density: 0.8, drumGain: 0.26 },
  mystical:   { leadGain: 0.11, bassGain: 0.18, density: 0.7, drumGain: 0.22 },
  chill:      { leadGain: 0.10, bassGain: 0.18, density: 0.5, drumGain: 0.20 },
  aggressive: { leadGain: 0.14, bassGain: 0.25, density: 0.9, drumGain: 0.30 },
  space:      { leadGain: 0.10, bassGain: 0.18, density: 0.6, drumGain: 0.22 },
  intense:    { leadGain: 0.14, bassGain: 0.24, density: 0.9, drumGain: 0.28 },
  epic:       { leadGain: 0.13, bassGain: 0.24, density: 0.85, drumGain: 0.28 },
  final:      { leadGain: 0.15, bassGain: 0.26, density: 0.95, drumGain: 0.30 },
};

/**
 * Преобразовать степень шкалы в частоту относительно root.
 * step — 0..N в пределах октавы, octaveOffset — сдвиг октавы (целое).
 */
function noteHz(rootHz, scale, step, octaveOffset = 0) {
  const semis = scale[step % scale.length] + 12 * Math.floor(step / scale.length);
  return rootHz * Math.pow(2, (semis + octaveOffset * 12) / 12);
}

/**
 * Класс для управления одной играющей синтез-партией.
 * Создаёт AudioContext, расписывает ноты, может остановиться.
 */
export class SynthPlayer {
  constructor(synthParams, bpm) {
    this.synthParams = synthParams;
    this.bpm = bpm;
    this.ctx = null;
    this.masterGain = null;
    this.scheduledStop = [];
    this.schedulerInterval = null;
    this.startTime = 0;
    this.nextNoteTime = 0;
    this.beat = 0;
    this.isPlaying = false;
  }

  async start() {
    if (this.isPlaying) return;
    const Ctor = window.AudioContext || window.webkitAudioContext;
    if (!Ctor) {
      console.warn('[SynthPlayer] Web Audio API не поддерживается в этом браузере');
      return;
    }
    this.ctx = new Ctor();

    // Браузер мог создать context в состоянии 'suspended' (autoplay policy).
    // resume() работает только если вызван внутри user-gesture цепочки — здесь
    // это так, потому что start() вызывается из onClick. На всякий случай ждём.
    if (this.ctx.state === 'suspended') {
      try { await this.ctx.resume(); }
      catch (e) { console.warn('[SynthPlayer] resume() failed:', e); }
    }
    console.log('[SynthPlayer] AudioContext state:', this.ctx.state,
                'sampleRate:', this.ctx.sampleRate, 'bpm:', this.bpm,
                'synth:', this.synthParams);

    this.masterGain = this.ctx.createGain();
    this.masterGain.gain.value = 0.7;
    this.masterGain.connect(this.ctx.destination);

    this.startTime = this.ctx.currentTime;
    this.nextNoteTime = this.startTime + 0.05;
    this.beat = 0;
    this.isPlaying = true;

    // Планировщик нот — каждые 25мс заглядывает на 0.1с вперёд и ставит ноты
    this.schedulerInterval = setInterval(() => this._scheduler(), 25);
  }

  stop() {
    this.isPlaying = false;
    if (this.schedulerInterval) {
      clearInterval(this.schedulerInterval);
      this.schedulerInterval = null;
    }
    if (this.ctx) {
      try {
        this.masterGain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.05);
        setTimeout(() => {
          try { this.ctx.close(); } catch (_) {}
        }, 100);
      } catch (_) {}
      this.ctx = null;
    }
  }

  _scheduler() {
    if (!this.isPlaying || !this.ctx) return;
    const lookahead = 0.1;  // секунд
    while (this.nextNoteTime < this.ctx.currentTime + lookahead) {
      this._scheduleBeat(this.beat, this.nextNoteTime);
      const beatDur = 60 / this.bpm;
      // играем по 8-м долям (полубиты)
      this.nextNoteTime += beatDur / 2;
      this.beat++;
    }
  }

  _scheduleBeat(beat, time) {
    const sp = this.synthParams;
    const mood = MOOD_PARAMS[sp.mood] || MOOD_PARAMS.calm;
    const scale = SCALES[sp.scale] || SCALES.minor;
    const root = sp.rootHz;

    // Каждый 8-й полу-бит = 4 такта по 4 ноты, итого 16 на цикл
    const pos = beat % 32;

    // === КИК (бас-барабан) — на сильных долях ===
    if (pos % 4 === 0) {
      this._kick(time, mood.drumGain);
    }
    // === СНЭР — на off-beat ===
    if (pos % 8 === 4) {
      this._snare(time, mood.drumGain * 0.8);
    }
    // === ХАЙХЭТ — каждая 8-я ===
    if (pos % 2 === 0) {
      this._hat(time, mood.drumGain * 0.35);
    }
    // === БАС-СИНТ — тоника на сильных, иногда квинта ===
    if (pos % 4 === 0) {
      const bassNote = pos % 16 === 8 ? noteHz(root, scale, 4, -1) : noteHz(root, scale, 0, -1);
      this._tone(time, bassNote, sp.bassWave, mood.bassGain, 0.45);
    }
    // === ЛИД — арпеджио по шкале, не на каждом тике ===
    if (pos % 2 === 0 && Math.random() < mood.density) {
      const arpSteps = [0, 2, 4, 7, 4, 2];
      const step = arpSteps[Math.floor(beat / 2) % arpSteps.length];
      const oct = pos % 8 === 0 ? 1 : 0;
      this._tone(time, noteHz(root, scale, step, oct), sp.leadWave, mood.leadGain, 0.25, true);
    }
    // На эпик-моментах — арпеджио в высокой октаве
    if (sp.mood === 'epic' || sp.mood === 'final') {
      if (pos % 16 === 0) {
        this._tone(time, noteHz(root, scale, 0, 2), sp.leadWave, mood.leadGain * 1.5, 0.8, true);
      }
    }
  }

  _tone(time, freq, waveType, gain, duration, withFilter = false) {
    const osc = this.ctx.createOscillator();
    osc.type = waveType || 'sine';
    osc.frequency.value = freq;

    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0, time);
    g.gain.linearRampToValueAtTime(gain, time + 0.005);
    g.gain.exponentialRampToValueAtTime(0.001, time + duration);

    if (withFilter) {
      const filter = this.ctx.createBiquadFilter();
      filter.type = 'lowpass';
      filter.frequency.value = freq * 4;
      filter.Q.value = 2;
      osc.connect(filter);
      filter.connect(g);
    } else {
      osc.connect(g);
    }
    g.connect(this.masterGain);

    osc.start(time);
    osc.stop(time + duration + 0.05);
  }

  _kick(time, gain) {
    const osc = this.ctx.createOscillator();
    osc.frequency.setValueAtTime(140, time);
    osc.frequency.exponentialRampToValueAtTime(40, time + 0.15);

    const g = this.ctx.createGain();
    g.gain.setValueAtTime(gain * 1.8, time);
    g.gain.exponentialRampToValueAtTime(0.001, time + 0.2);

    osc.connect(g);
    g.connect(this.masterGain);
    osc.start(time);
    osc.stop(time + 0.25);
  }

  _snare(time, gain) {
    // короткий шум
    const noise = this.ctx.createBufferSource();
    const buf = this.ctx.createBuffer(1, 4410, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
    noise.buffer = buf;

    const filter = this.ctx.createBiquadFilter();
    filter.type = 'highpass';
    filter.frequency.value = 1800;

    const g = this.ctx.createGain();
    g.gain.setValueAtTime(gain * 1.5, time);
    g.gain.exponentialRampToValueAtTime(0.001, time + 0.12);

    noise.connect(filter);
    filter.connect(g);
    g.connect(this.masterGain);
    noise.start(time);
    noise.stop(time + 0.15);
  }

  _hat(time, gain) {
    const noise = this.ctx.createBufferSource();
    const buf = this.ctx.createBuffer(1, 2200, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
    noise.buffer = buf;

    const filter = this.ctx.createBiquadFilter();
    filter.type = 'highpass';
    filter.frequency.value = 6000;

    const g = this.ctx.createGain();
    g.gain.setValueAtTime(gain, time);
    g.gain.exponentialRampToValueAtTime(0.001, time + 0.05);

    noise.connect(filter);
    filter.connect(g);
    g.connect(this.masterGain);
    noise.start(time);
    noise.stop(time + 0.07);
  }
}

/**
 * Проверить, есть ли файл трека на сервере (HEAD-запрос).
 *
 * Важно: dev-server React на 404 возвращает index.html с 200 OK
 * (history fallback). Поэтому проверяем не только r.ok, но и
 * Content-Type — должен начинаться с "audio/".
 */
export async function trackFileExists(url) {
  try {
    const r = await fetch(url, { method: 'HEAD', cache: 'no-cache' });
    if (!r.ok) return false;
    const ct = (r.headers.get('content-type') || '').toLowerCase();
    return ct.startsWith('audio/');
  } catch (_) {
    return false;
  }
}
