/**
 * 気の抜けたボサノバ調BGM。全てWebAudioで手続き生成する。
 *
 * 16小節を A - A' - B - B' の形式で回し、
 * ナイロン弦風のコンピング / ウッドベース / シェイカー / リムショット(クラーベ) /
 * 気だるいメロディ の5レイヤーを重ねる。
 * 稼働中(Do)は全部鳴らし、計画中は薄くしてUIの邪魔をしないようにする。
 */
import { getBuses, ensureAudio } from './engine';

const BPM = 126;
/** 16分音符1つぶんの長さ (秒)。ボサノバのシンコペーションを扱うため16分グリッドで持つ。 */
const STEP = 60 / BPM / 4;
const STEPS_PER_BAR = 16;
const LOOKAHEAD = 0.35;
const TICK_MS = 90;

type ChordQuality = 'm7' | '7' | 'maj7' | '7b9' | 'm7b5';

/** コードトーン (ルートからの半音) */
const QUALITY: Record<ChordQuality, number[]> = {
  m7: [0, 3, 7, 10],
  '7': [0, 4, 7, 10],
  maj7: [0, 4, 7, 11],
  '7b9': [0, 4, 10, 13],
  m7b5: [0, 3, 6, 10],
};

/** メロディで使ってよい音 (ルートからの半音)。ここから外れないので破綻しない。 */
const SCALE: Record<ChordQuality, number[]> = {
  m7: [0, 2, 3, 5, 7, 9, 10],
  '7': [0, 2, 4, 5, 7, 9, 10],
  maj7: [0, 2, 4, 7, 9, 11],
  '7b9': [0, 1, 4, 7, 10],
  m7b5: [0, 3, 5, 6, 10],
};

interface Bar {
  /** ルートのMIDIノート番号 */
  root: number;
  quality: ChordQuality;
  /** メロディを乗せる小節か */
  lead: boolean;
}

/**
 * 形式: A(ii-V-I-VI7) → A'(同じだがトニックで落ち着く) → B(変ホ長調へ転調) → B'(ターンアラウンド)
 * 16小節ひと回りなので、聴いていて同じところに戻ってくる感じが出る。
 */
const FORM: Bar[] = [
  // A
  { root: 62, quality: 'm7', lead: false },   // Dm7
  { root: 55, quality: '7', lead: false },    // G7
  { root: 60, quality: 'maj7', lead: false }, // Cmaj7
  { root: 57, quality: '7b9', lead: false },  // A7b9
  // A'
  { root: 62, quality: 'm7', lead: true },
  { root: 55, quality: '7', lead: true },
  { root: 60, quality: 'maj7', lead: true },
  { root: 60, quality: 'maj7', lead: true },
  // B — 変ホ長調へ
  { root: 65, quality: 'm7', lead: true },    // Fm7
  { root: 58, quality: '7', lead: true },     // Bb7
  { root: 63, quality: 'maj7', lead: true },  // Ebmaj7
  { root: 63, quality: 'maj7', lead: false },
  // B' — 元の調へ戻るターンアラウンド
  { root: 62, quality: 'm7b5', lead: true },  // Dm7b5
  { root: 55, quality: '7b9', lead: true },   // G7b9
  { root: 60, quality: 'maj7', lead: true },  // Cmaj7
  { root: 57, quality: '7b9', lead: false },  // A7b9
];

/** ボサノバのコンピング。2小節で1周する裏拍のパターン。 */
const COMP: number[][] = [
  [0, 6, 11],
  [3, 6, 11, 14],
];

/** ウッドベース。1拍目・2拍裏・3拍目・4拍裏 の跳ねる形。 */
const BASS: Array<{ s: number; deg: 'root' | 'fifth' | 'approach' }> = [
  { s: 0, deg: 'root' },
  { s: 6, deg: 'root' },
  { s: 8, deg: 'fifth' },
  { s: 14, deg: 'approach' },
];

/** クラーベ(2-3)。2小節で1周。 */
const CLAVE: number[][] = [
  [0, 6, 12],
  [4, 10],
];

/** メロディの動機。転調しても同じ形が出てくるようにルートからの度数で持つ。 */
const MOTIFS: Array<Array<{ s: number; n: number; len: number }>> = [
  [{ s: 2, n: 19, len: 3 }, { s: 6, n: 21, len: 3 }, { s: 10, n: 24, len: 5 }, { s: 14, n: 22, len: 2 }],
  [{ s: 0, n: 24, len: 4 }, { s: 5, n: 22, len: 3 }, { s: 9, n: 21, len: 3 }, { s: 13, n: 19, len: 3 }],
  [{ s: 3, n: 21, len: 4 }, { s: 8, n: 19, len: 3 }, { s: 12, n: 17, len: 4 }],
  [{ s: 1, n: 26, len: 3 }, { s: 6, n: 24, len: 4 }, { s: 11, n: 21, len: 5 }],
];

let timer = 0;
let step = 0;
let nextTime = 0;
let playing = false;
/** 0 = 薄く(計画中) / 1 = 全部鳴らす(稼働中) */
let intensity = 0;

const midiToHz = (m: number): number => 440 * Math.pow(2, (m - 69) / 12);

/** メロディ音をコードのスケールに収める。半音のぶつかりを防ぐ。 */
function snapToScale(semi: number, quality: ChordQuality): number {
  const pool = SCALE[quality];
  const oct = Math.floor(semi / 12) * 12;
  const pc = semi - oct;
  let best = pool[0];
  let bestD = 99;
  for (const p of pool) {
    const d = Math.abs(p - pc);
    if (d < bestD) {
      bestD = d;
      best = p;
    }
  }
  return oct + best;
}

// ------------------------------------------------------------------ 音色

function pluck(freq: number, at: number, gain: number, decay: number): void {
  const b = getBuses();
  if (!b) return;
  const osc = b.ctx.createOscillator();
  osc.type = 'triangle';
  osc.frequency.value = freq;

  const lp = b.ctx.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.setValueAtTime(2600, at);
  lp.frequency.exponentialRampToValueAtTime(700, at + decay);

  const env = b.ctx.createGain();
  env.gain.setValueAtTime(0.0001, at);
  env.gain.exponentialRampToValueAtTime(gain, at + 0.012);
  env.gain.exponentialRampToValueAtTime(0.0001, at + decay);

  osc.connect(lp);
  lp.connect(env);
  env.connect(b.music);
  osc.start(at);
  osc.stop(at + decay + 0.03);
}

function bassNote(freq: number, at: number, gain: number): void {
  const b = getBuses();
  if (!b) return;
  const osc = b.ctx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(freq * 1.01, at);
  osc.frequency.exponentialRampToValueAtTime(freq, at + 0.05);

  const env = b.ctx.createGain();
  env.gain.setValueAtTime(0.0001, at);
  env.gain.exponentialRampToValueAtTime(gain, at + 0.02);
  env.gain.exponentialRampToValueAtTime(0.0001, at + 0.42);

  osc.connect(env);
  env.connect(b.music);
  osc.start(at);
  osc.stop(at + 0.45);
}

/** 気だるいリード。少し遅れて入り、軽くビブラートをかける。 */
function lead(freq: number, at: number, dur: number): void {
  const b = getBuses();
  if (!b) return;
  const osc = b.ctx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(freq * 0.995, at);
  osc.frequency.exponentialRampToValueAtTime(freq, at + 0.06);

  const vib = b.ctx.createOscillator();
  vib.frequency.value = 5.2;
  const vibDepth = b.ctx.createGain();
  vibDepth.gain.value = 3.5;
  vib.connect(vibDepth);
  vibDepth.connect(osc.frequency);

  const lp = b.ctx.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.value = 1800;

  const env = b.ctx.createGain();
  env.gain.setValueAtTime(0.0001, at);
  env.gain.exponentialRampToValueAtTime(0.10, at + 0.07);
  env.gain.setValueAtTime(0.10, at + dur * 0.6);
  env.gain.exponentialRampToValueAtTime(0.0001, at + dur);

  osc.connect(lp);
  lp.connect(env);
  env.connect(b.music);
  vib.start(at);
  osc.start(at);
  vib.stop(at + dur + 0.05);
  osc.stop(at + dur + 0.05);
}

function shaker(at: number, gain: number): void {
  const b = getBuses();
  if (!b) return;
  const src = b.ctx.createBufferSource();
  src.buffer = b.noise;
  src.loop = true;

  const hp = b.ctx.createBiquadFilter();
  hp.type = 'highpass';
  hp.frequency.value = 6500;

  const env = b.ctx.createGain();
  env.gain.setValueAtTime(0.0001, at);
  env.gain.exponentialRampToValueAtTime(gain, at + 0.005);
  env.gain.exponentialRampToValueAtTime(0.0001, at + 0.06);

  src.connect(hp);
  hp.connect(env);
  env.connect(b.music);
  src.start(at);
  src.stop(at + 0.08);
}

function rim(at: number, gain: number): void {
  const b = getBuses();
  if (!b) return;
  const osc = b.ctx.createOscillator();
  osc.type = 'triangle';
  osc.frequency.setValueAtTime(1750, at);
  osc.frequency.exponentialRampToValueAtTime(900, at + 0.04);

  const env = b.ctx.createGain();
  env.gain.setValueAtTime(0.0001, at);
  env.gain.exponentialRampToValueAtTime(gain, at + 0.004);
  env.gain.exponentialRampToValueAtTime(0.0001, at + 0.07);

  osc.connect(env);
  env.connect(b.music);
  osc.start(at);
  osc.stop(at + 0.09);
}

// ------------------------------------------------------------------ スケジューラ

function scheduleStep(globalStep: number, at: number): void {
  const barIndex = Math.floor(globalStep / STEPS_PER_BAR) % FORM.length;
  const inBar = globalStep % STEPS_PER_BAR;
  const bar = FORM[barIndex];
  const nextBar = FORM[(barIndex + 1) % FORM.length];
  const twoBar = Math.floor(globalStep / STEPS_PER_BAR) % 2;

  // --- コンピング ---
  if (COMP[twoBar].includes(inBar)) {
    const notes = QUALITY[bar.quality];
    const g = 0.055 + intensity * 0.03;
    notes.forEach((semi, i) => {
      pluck(midiToHz(bar.root + semi + 12), at + i * 0.012, g, 0.75);
    });
  }

  // --- ベース ---
  for (const b of BASS) {
    if (b.s !== inBar) continue;
    let semi = 0;
    if (b.deg === 'fifth') semi = 7;
    // 次のコードへ半音下から寄せる
    if (b.deg === 'approach') semi = nextBar.root + 1 - bar.root;
    bassNote(midiToHz(bar.root + semi - 12), at, 0.22 + intensity * 0.05);
  }

  // --- パーカッション (稼働中だけ厚くする) ---
  if (inBar % 2 === 0) shaker(at, (inBar % 4 === 2 ? 0.05 : 0.03) * (0.5 + intensity * 0.5));
  if (intensity > 0.3 && CLAVE[twoBar].includes(inBar)) rim(at, 0.07);

  // --- メロディ ---
  if (bar.lead && intensity > 0.5) {
    const motif = MOTIFS[barIndex % MOTIFS.length];
    for (const n of motif) {
      if (n.s !== inBar) continue;
      lead(midiToHz(bar.root + snapToScale(n.n, bar.quality)), at, n.len * STEP);
    }
  }
}

function scheduler(): void {
  const b = getBuses();
  if (!b || !playing) return;
  while (nextTime < b.ctx.currentTime + LOOKAHEAD) {
    scheduleStep(step, nextTime);
    nextTime += STEP;
    step++;
  }
}

export function startMusic(): void {
  const b = ensureAudio();
  if (!b || playing) return;
  playing = true;
  step = 0;
  nextTime = b.ctx.currentTime + 0.15;
  scheduler();
  timer = window.setInterval(scheduler, TICK_MS);
}

export function stopMusic(): void {
  playing = false;
  window.clearInterval(timer);
  timer = 0;
}

export function isMusicPlaying(): boolean {
  return playing;
}

/**
 * 曲の厚みを変える。
 * @param level 0 = 薄く(計画・評価フェーズ) / 1 = フル(稼働中)
 */
export function setMusicIntensity(level: number): void {
  intensity = Math.max(0, Math.min(1, level));
}

/** デバッグ用: いま何小節目のどのセクションを鳴らしているか */
export function musicPosition(): { bar: number; chordRoot: number; quality: string } {
  const bar = Math.floor(step / STEPS_PER_BAR) % FORM.length;
  return { bar, chordRoot: FORM[bar].root, quality: FORM[bar].quality };
}
