/* Locally bundled Japanese management announcement; no network or system voices at runtime. */
(function (root) {
  'use strict';
  const D = root.Danchi = root.Danchi || {};
  const INTRO_SEGMENTS = [
    { text: '団地の皆さん、こんにちは。', start: 0, end: 2.6562 },
    { text: '怪物をクリックして撃退してください。', start: 2.6562, end: 5.6268 },
    { text: '住民に当てると、請求額が増えます。', start: 5.6268, end: 9.0788 },
    { text: '武器は数字キーで切り替え。', start: 9.0788, end: 11.2501 },
    { text: '弾切れも、時間制限もありません。', start: 11.2501, end: 14.3338 },
    { text: 'では、よい午後を。', start: 14.3338, end: 16.4386 }
  ];
  const scriptUrl = root.document?.currentScript?.src;
  const assetUrl = (file) => scriptUrl ? new URL('../assets/narration/' + file, scriptUrl).href : 'assets/narration/' + file;
  const clip = (filename, duration, segments) => Object.freeze({ url: assetUrl(filename), duration, segments: Object.freeze(segments.map(Object.freeze)) });
  const CLIPS = Object.freeze({
    introduction: clip('introduction-ja.wav', 16.4386, INTRO_SEGMENTS),
    bossWarning: clip('boss-warning-ja.wav', 6.1337, [
      { text: 'あの爆弾にあてると多額の請求が来るかもしれません、', start: 0, end: 3.9268 },
      { text: '本体を狙ってください。', start: 3.9268, end: 6.1337 }
    ]),
    structureWarning: clip('structure-warning-ja.wav', 8.8004, [
      { text: '建物の耐久値が半分になりました。', start: 0, end: 3.1501 },
      { text: 'このまま撃ち続けると全壊します。再建費は、あなた持ちです。', start: 3.1501, end: 8.8004 }
    ])
  });
  const now = () => root.performance?.now() ?? Date.now();
  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));

  class Narrator {
    constructor(onChange) {
      this.onChange = typeof onChange === 'function' ? onChange : onChange?.onChange;
      this.active = false;
      this.text = '';
      this.status = 'stopped';
      this.clipId = 'introduction';
      this.fullText = INTRO_SEGMENTS.map(s => s.text).join('');
      this.segments = CLIPS.introduction.segments;
      this.duration = CLIPS.introduction.duration;
      this._audio = null;
      this._muted = false;
      this._volume = .65;
      this._paused = false;
      this._elapsed = 0;
      this._generation = 0;
      this._timer = null;
      this._source = 'subtitles';
      this._lastNotice = '';
      this._hide = () => { if (root.document?.hidden) this.setPaused(true); };
      this._leave = () => this.stop();
      root.document?.addEventListener('visibilitychange', this._hide);
      root.addEventListener?.('pagehide', this._leave);
    }

    get audio() { return this._audio; }
    get currentTime() { return this._elapsed; }
    get muted() { return this._muted; }
    get volume() { return this._volume; }
    get paused() { return this._paused; }

    start(clipId = 'introduction') {
      if (clipId === 'intro') clipId = 'introduction';
      if (!Object.prototype.hasOwnProperty.call(CLIPS, clipId)) return false;
      const selected = CLIPS[clipId];
      if (!selected) return false;
      this.stop();
      this._generation++;
      this.clipId = clipId;
      this.duration = selected.duration;
      this.segments = selected.segments;
      this.fullText = this.segments.map(s => s.text).join('');
      this._createAudio(selected.url);
      this.active = true;
      this._paused = false;
      this._elapsed = 0;
      this._source = this._audio ? 'audio' : 'subtitles';
      this._audioStarted = false;
      this._lastTick = this._lastAdvance = now();
      this.status = this._audio ? 'loading' : 'subtitles';
      this.text = this.segments[0].text;
      this._notify();
      if (this._audio) {
        try { this._audio.currentTime = 0; } catch (_) { /* A not-yet-decoded local file starts at zero. */ }
        this._audio.muted = this._muted;
        this._audio.volume = this._volume;
        // This call stays in the original start-button gesture to satisfy autoplay policies.
        this._play();
      }
      this._timer = root.setInterval(() => this._tick(), 80);
      return this.active;
    }

    _createAudio(url) {
      // A new element per run also isolates queued ended/error events from an older clip.
      this._audio = null;
      try {
        if (typeof root.Audio !== 'function') return;
        const audio = this._audio = new root.Audio(url);
        audio.preload = 'auto';
        audio.volume = this._volume;
        audio.addEventListener('ended', () => {
          if (audio === this._audio && this.active && this._source === 'audio' && !this._paused) this._finish();
        });
        audio.addEventListener('error', () => { if (audio === this._audio) this._fallback(); });
      } catch (_) { this._audio = null; }
    }

    _play() {
      const generation = this._generation;
      try {
        const result = this._audio.play();
        const started = () => {
          if (generation !== this._generation || !this.active || this._source !== 'audio') return;
          this._audioStarted = true;
          this._lastAdvance = now();
          if (this._paused) { this._audio.pause(); return; }
          this.status = 'playing';
          this._notify();
        };
        if (result?.then) result.then(started, () => {
          if (generation === this._generation) this._fallback();
        });
        else started();
      } catch (_) { this._fallback(); }
    }

    _tick() {
      const time = now(), delta = Math.max(0, (time - this._lastTick) / 1000);
      this._lastTick = time;
      if (!this.active || this._paused) return;
      if (this._source === 'audio') {
        const position = Number(this._audio.currentTime) || 0;
        if (position > this._elapsed + .005) this._lastAdvance = time;
        this._elapsed = Math.max(this._elapsed, position);
        // Missing/unavailable media must never leave a permanent caption or muted game mix.
        if (time - this._lastAdvance > (this._audioStarted ? 2200 : 1400)) this._fallback();
      } else this._elapsed += delta;
      if (this._elapsed >= this.duration + .1) { this._finish(); return; }
      const segment = this.segments.find(s => this._elapsed >= s.start && this._elapsed < s.end) || this.segments[this.segments.length - 1];
      this.text = segment.text;
      this._notify();
    }

    _fallback() {
      if (!this.active || this._source === 'subtitles') return;
      this._source = 'subtitles';
      try { this._audio?.pause(); } catch (_) {}
      this._lastTick = now();
      this.status = this._paused ? 'paused' : 'subtitles';
      this._notify();
    }

    setMuted(value) {
      this._muted = Boolean(value);
      if (this._audio) this._audio.muted = this._muted;
      // Muting preserves synchronized subtitles; it never restarts the announcement.
    }

    setVolume(value) {
      const number = Number(value);
      this._volume = clamp(Number.isFinite(number) ? number : 0, 0, 1);
      if (this._audio) this._audio.volume = this._volume;
    }

    setPaused(value) {
      const paused = Boolean(value);
      if (!this.active || paused === this._paused) return;
      if (paused) this._tick();
      if (!this.active) return;
      this._paused = paused;
      this._lastTick = this._lastAdvance = now();
      if (paused) {
        this._generation++; // Ignore AbortError from a play promise interrupted by pause.
        try { this._audio?.pause(); } catch (_) {}
        this.status = 'paused';
        this._notify();
      } else if (this._source === 'audio') {
        this.status = 'loading';
        this._notify();
        this._play();
      } else {
        this.status = 'subtitles';
        this._notify();
      }
    }

    stop() {
      this._generation++;
      if (this._timer !== null) root.clearInterval(this._timer);
      this._timer = null;
      try {
        this._audio?.pause();
        if (this._audio) this._audio.currentTime = 0;
      } catch (_) {}
      this.active = false;
      this._paused = false;
      this._elapsed = 0;
      this.text = '';
      this.status = 'stopped';
      this._notify();
    }

    _finish() {
      this.stop();
      this._elapsed = this.duration;
      this.status = 'ended';
      this._notify();
    }

    _notify() {
      const key = [this.clipId, this.active, this.text, this.status].join('|');
      if (key === this._lastNotice) return;
      this._lastNotice = key;
      try {
        this.onChange?.({ clipId: this.clipId, text: this.text, active: this.active, status: this.status, currentTime: this._elapsed, duration: this.duration });
      } catch (_) { /* Presentation callbacks cannot interrupt play. */ }
    }

    dispose() {
      this.stop();
      root.document?.removeEventListener('visibilitychange', this._hide);
      root.removeEventListener?.('pagehide', this._leave);
      if (this._audio) this._audio.removeAttribute('src');
    }
  }
  Narrator.assetUrl = CLIPS.introduction.url;
  Narrator.clips = CLIPS;
  D.Narrator = Narrator;
})(globalThis);
