import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';

import 'package:wolf_3d_dart/src/engine/audio/debug_music_player.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_synth.dart';

class NativeSubprocessAudio implements EngineAudio, DebugMusicPlayer {
  NativeSubprocessAudio({this.maxConcurrentSfx = 8});

  static bool get supportsCurrentPlatform =>
      Platform.isLinux || Platform.isMacOS || Platform.isWindows;

  final int maxConcurrentSfx;

  @override
  WolfensteinData? activeGame;

  bool _initialized = false;
  bool _isSupported = false;
  bool _disposed = false;
  _AudioBackend _backend = _AudioBackend.none;
  String _windowsShellCommand = 'powershell';

  Process? _musicProcess;
  int _musicLoopToken = 0;
  String? _musicTempFilePath;

  final List<Process> _sfxProcesses = <Process>[];

  @override
  Future<void> init() async {
    if (_initialized || _disposed) {
      return;
    }

    _backend = await _detectBackend();
    _isSupported = _backend != _AudioBackend.none;
    _initialized = true;

    if (_isSupported) {
      log('[NATIVE AUDIO] Subprocess backend enabled: ${_backend.name}');
    } else {
      log('[NATIVE AUDIO] No supported audio backend found; running silent.');
    }
  }

  @override
  void dispose() {
    if (_disposed) {
      return;
    }
    _disposed = true;

    _musicLoopToken++;
    _musicProcess?.kill();
    _musicProcess = null;

    for (final process in List<Process>.from(_sfxProcesses)) {
      process.kill();
    }
    _sfxProcesses.clear();

    final path = _musicTempFilePath;
    _musicTempFilePath = null;
    if (path != null) {
      unawaited(_cleanupTempWav(path));
    }
  }

  @override
  Future<void> debugSoundTest() async {
    for (int i = 0; i < 50; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 500));
      await playSoundEffectId(i);
    }
  }

  @override
  Future<void> playMenuMusic() async {
    final data = activeGame;
    final trackIndex = data == null
        ? null
        : Music.menuTheme.trackIndexFor(data.version);
    if (data == null || trackIndex == null || trackIndex >= data.music.length) {
      return;
    }

    await playMusic(data.music[trackIndex]);
  }

  @override
  Future<void> playLevelMusic(Music music) async {
    final data = activeGame;
    if (data == null || data.music.isEmpty) {
      return;
    }

    final index = music.trackIndexFor(data.version) ?? 0;
    if (index < 0 || index >= data.music.length) {
      return;
    }

    await playMusic(data.music[index]);
  }

  @override
  Future<void> playMusic(ImfMusic track, {bool looping = true}) async {
    if (!_isSupported) {
      return;
    }

    final pcmSamples = ImfRenderer.render(track);
    final wavBytes = ImfRenderer.createWavFile(pcmSamples);

    if (looping) {
      await _startLoopingMusic(wavBytes);
      return;
    }

    await stopMusic();
    final process = await _startPlaybackProcess(
      wavBytes: wavBytes,
      role: _PlaybackRole.music,
    );
    if (process != null) {
      _musicProcess = process;
    }
  }

  Future<void> _startLoopingMusic(Uint8List wavBytes) async {
    await stopMusic();

    final int token = ++_musicLoopToken;

    while (_musicLoopToken == token) {
      final process = await _startPlaybackProcess(
        wavBytes: wavBytes,
        role: _PlaybackRole.music,
      );
      if (process == null) {
        break;
      }

      _musicProcess = process;
      await process.exitCode;

      if (_musicLoopToken != token) {
        break;
      }
    }
  }

  @override
  Future<void> stopMusic() async {
    _musicLoopToken++;
    _musicProcess?.kill();
    _musicProcess = null;

    final path = _musicTempFilePath;
    _musicTempFilePath = null;
    if (path != null) {
      await _cleanupTempWav(path);
    }
  }

  @override
  Future<void> stopAllAudio() async {
    await stopMusic();

    for (final process in List<Process>.from(_sfxProcesses)) {
      process.kill();
    }
    _sfxProcesses.clear();
  }

  @override
  Future<void> playSoundEffect(SoundEffect effect) async {
    final data = activeGame;
    if (data == null) {
      return;
    }

    final resolved = data.registry.sfx.resolve(effect);
    if (resolved != null) {
      await playSoundEffectId(resolved.slotIndex);
      return;
    }

    if (data.version == GameVersion.spearOfDestinyDemo) {
      return;
    }

    await playSoundEffectId(effect.idFor(data.version));
  }

  @override
  Future<void> playSoundEffectId(int sfxId) async {
    if (!_isSupported) {
      return;
    }

    final data = activeGame;
    if (data == null) {
      return;
    }

    final soundsList = data.sounds;
    if (sfxId < 0 || sfxId >= soundsList.length) {
      return;
    }

    final raw8bitBytes = soundsList[sfxId].bytes;
    if (raw8bitBytes.isEmpty) {
      return;
    }

    final Int16List converted16bit = Int16List(raw8bitBytes.length);
    for (int i = 0; i < raw8bitBytes.length; i++) {
      converted16bit[i] = (raw8bitBytes[i] - 128) * 256;
    }

    final wavBytes = ImfRenderer.createWavFile(
      converted16bit,
      sampleRate: 7000,
    );

    if (_sfxProcesses.length >= maxConcurrentSfx) {
      final oldest = _sfxProcesses.removeAt(0);
      oldest.kill();
    }

    final process = await _startPlaybackProcess(
      wavBytes: wavBytes,
      role: _PlaybackRole.sfx,
    );
    if (process == null) {
      return;
    }

    _sfxProcesses.add(process);
    unawaited(
      process.exitCode.then((_) {
        _sfxProcesses.remove(process);
      }),
    );
  }

  Future<Process?> _startPlaybackProcess({
    required Uint8List wavBytes,
    required _PlaybackRole role,
  }) async {
    try {
      switch (_backend) {
        case _AudioBackend.linuxPipeWire:
          return _startFilePlaybackProcess('pw-play', const [], wavBytes, role);
        case _AudioBackend.linuxPulseAudio:
          return _startFilePlaybackProcess('paplay', const [], wavBytes, role);
        case _AudioBackend.linuxAplay:
          return _startFilePlaybackProcess(
            'aplay',
            const ['-q'],
            wavBytes,
            role,
          );
        case _AudioBackend.macosAfplay:
          return _startFilePlaybackProcess('afplay', const [], wavBytes, role);
        case _AudioBackend.windowsPowerShell:
          return _startWindowsPlaybackProcess(wavBytes, role: role);
        case _AudioBackend.none:
          return null;
      }
    } catch (error) {
      log('[NATIVE AUDIO] Failed to start playback process: $error');
      return null;
    }
  }

  Future<Process> _startFilePlaybackProcess(
    String executable,
    List<String> baseArguments,
    Uint8List wavBytes,
    _PlaybackRole role,
  ) async {
    final path = await _writeTempWav(wavBytes, prefix: 'wolf3d_native_audio_');

    if (role == _PlaybackRole.music) {
      final existing = _musicTempFilePath;
      _musicTempFilePath = path;
      if (existing != null && existing != path) {
        await _cleanupTempWav(existing);
      }
    }

    final process = await Process.start(executable, <String>[
      ...baseArguments,
      path,
    ]);

    unawaited(
      process.exitCode.then((code) async {
        if (code != 0) {
          log('[NATIVE AUDIO] Player exited with code $code: $executable');
        }
        await _cleanupTempWav(path);
      }),
    );

    return process;
  }

  Future<Process> _startWindowsPlaybackProcess(
    Uint8List wavBytes, {
    required _PlaybackRole role,
  }) async {
    final path = await _writeWindowsTempWav(wavBytes, role: role);

    if (role == _PlaybackRole.music) {
      final existing = _musicTempFilePath;
      _musicTempFilePath = path;
      if (existing != null && existing != path) {
        await _cleanupTempWav(existing);
      }
    }

    final escapedPath = path.replaceAll("'", "''");
    final script = "(New-Object Media.SoundPlayer '$escapedPath').PlaySync()";

    final process = await Process.start(_windowsShellCommand, <String>[
      '-NoProfile',
      '-NonInteractive',
      '-Command',
      script,
    ]);

    unawaited(
      process.exitCode.then((_) async {
        await _cleanupTempWav(path);
      }),
    );

    return process;
  }

  Future<String> _writeWindowsTempWav(
    Uint8List wavBytes, {
    required _PlaybackRole role,
  }) async {
    final tempDir = await Directory.systemTemp.createTemp(
      'wolf3d_native_audio_',
    );
    final suffix = role == _PlaybackRole.music
        ? 'music_${DateTime.now().microsecondsSinceEpoch}.wav'
        : 'sfx_${DateTime.now().microsecondsSinceEpoch}.wav';
    final path = '${tempDir.path}${Platform.pathSeparator}$suffix';
    await File(path).writeAsBytes(wavBytes, flush: true);

    return path;
  }

  Future<String> _writeTempWav(
    Uint8List wavBytes, {
    required String prefix,
  }) async {
    final tempDir = await Directory.systemTemp.createTemp(prefix);
    final path =
        '${tempDir.path}${Platform.pathSeparator}audio_${DateTime.now().microsecondsSinceEpoch}.wav';
    await File(path).writeAsBytes(wavBytes, flush: true);

    return path;
  }

  Future<_AudioBackend> _detectBackend() async {
    if (Platform.isLinux) {
      final hasPwPlay = await _commandExists('pw-play');
      if (hasPwPlay) {
        return _AudioBackend.linuxPipeWire;
      }

      final hasPaplay = await _commandExists('paplay');
      if (hasPaplay) {
        return _AudioBackend.linuxPulseAudio;
      }

      final hasAplay = await _commandExists('aplay');
      if (hasAplay) {
        return _AudioBackend.linuxAplay;
      }
    }

    if (Platform.isMacOS) {
      final hasAfplay = await _commandExists('afplay');
      if (hasAfplay) {
        return _AudioBackend.macosAfplay;
      }
    }

    if (Platform.isWindows) {
      final hasPowerShell = await _commandExists('powershell');
      if (hasPowerShell) {
        _windowsShellCommand = 'powershell';
        return _AudioBackend.windowsPowerShell;
      }

      final hasPwsh = await _commandExists('pwsh');
      if (hasPwsh) {
        _windowsShellCommand = 'pwsh';
        return _AudioBackend.windowsPowerShell;
      }
    }

    return _AudioBackend.none;
  }

  Future<bool> _commandExists(String command) async {
    final probe = Platform.isWindows
        ? await Process.run('where', <String>[command])
        : await Process.run('which', <String>[command]);
    return probe.exitCode == 0;
  }

  Future<void> _cleanupTempWav(String path) async {
    try {
      final file = File(path);
      if (await file.exists()) {
        await file.delete();
      }

      final directory = file.parent;
      if (await directory.exists()) {
        await directory.delete();
      }
    } catch (error) {
      log('[NATIVE AUDIO] Temp WAV cleanup failed: $error');
    }
  }
}

enum _AudioBackend {
  none,
  linuxPipeWire,
  linuxPulseAudio,
  linuxAplay,
  macosAfplay,
  windowsPowerShell,
}

enum _PlaybackRole { music, sfx }
