import 'dart:async'; import 'package:flutter/foundation.dart'; import '../modelos/dispositivo_audio.dart'; import '../modelos/preset_ecualizador.dart'; import '../servicios/servicio_audio.dart'; import '../servicios/servicio_dispositivo_audio.dart'; import '../servicios/servicio_ecualizador.dart'; /// Equalizer state extracted from `EstadoRadio` (S4-R1). /// /// Owns the main preset, the per-station preset map, the current (applied) /// preset and the enabled flag, plus their persistence through /// [ServicioEcualizador] and their application through [ServicioAudio]. /// Notifies ONLY its own listeners — EQ changes must not rebuild /// `EstadoRadio` consumers (S4-R1-A, S4-R5). /// /// Multi-device EQ extension (Phase 5): when [eqMultiDeviceEnabled] is true, /// resolves the active preset through a 4-level hierarchy: /// 1. presetsMatriz["stationUuid:deviceId"] /// 2. presetsEmisoraMap[stationUuid] /// 3. presetsDispositivo[deviceId] /// 4. presetPrincipal /// /// When the toggle is false, resolution falls back to the original 2-level /// hierarchy (station → global) — zero behavioral change vs. prior releases. class EstadoEcualizador extends ChangeNotifier { EstadoEcualizador({ required this.audio, ServicioEcualizador? servicio, ServicioDispositivoAudio? dispositivoAudio, String? Function()? emisoraActualUuid, }) : servicio = servicio ?? ServicioEcualizador(), _dispositivoAudio = dispositivoAudio, _emisoraActualUuid = emisoraActualUuid ?? (() => null); final ServicioAudio audio; final ServicioEcualizador servicio; final ServicioDispositivoAudio? _dispositivoAudio; /// Callback into the owner (EstadoRadio) for the currently playing station; /// keeps this notifier free of any station-list coupling. final String? Function() _emisoraActualUuid; final Map _presetsEmisoraMap = {}; /// Per-device presets: deviceId → PresetEcualizador. final Map _presetsDispositivo = {}; /// Matrix presets: "stationUuid:deviceId" → PresetEcualizador. final Map _presetsMatriz = {}; /// Custom display names for devices: deviceId → custom name. final Map _nombresDispositivos = {}; PresetEcualizador _presetPrincipal = PresetEcualizador.flat; PresetEcualizador _presetActual = PresetEcualizador.flat; bool _activo = true; bool _eqMultiDeviceEnabled = false; String? _dispositivoActualId; StreamSubscription? _deviceSub; PresetEcualizador get presetActual => _presetActual; PresetEcualizador get presetPrincipal => _presetPrincipal; bool get activo => _activo; bool get disponible => audio.ecualizadorDisponible; bool get eqMultiDeviceEnabled => _eqMultiDeviceEnabled; String? get dispositivoActualId => _dispositivoActualId; Map get nombresDispositivos => Map.unmodifiable(_nombresDispositivos); Map get presetsPorEmisora => Map.unmodifiable(_presetsEmisoraMap); Map get presetsDispositivo => Map.unmodifiable(_presetsDispositivo); Map get presetsMatriz => Map.unmodifiable(_presetsMatriz); bool get emisoraActualTienePresetPropio { final uuid = _emisoraActualUuid(); if (uuid == null) return false; return tienePresetPorEmisora(uuid); } bool tienePresetPorEmisora(String uuid) => _presetsEmisoraMap.containsKey(uuid); PresetEcualizador? presetPorEmisora(String uuid) => _presetsEmisoraMap[uuid]; PresetEcualizador presetParaEmisora(String uuid) => _presetsEmisoraMap[uuid] ?? _presetPrincipal; /// 4-level resolution hierarchy (ADR-4, spec requirement). /// /// When [eqMultiDeviceEnabled] is false, falls back to 2-level (station → global). PresetEcualizador presetEfectivo({ required String stationUuid, required String deviceId, }) { if (!_eqMultiDeviceEnabled) { // Original 2-level behavior: station → global. return _presetsEmisoraMap[stationUuid] ?? _presetPrincipal; } // Level 1: station × device matrix. final matrizKey = '$stationUuid:$deviceId'; if (_presetsMatriz.containsKey(matrizKey)) { return _presetsMatriz[matrizKey]!; } // Level 2: station-only. if (_presetsEmisoraMap.containsKey(stationUuid)) { return _presetsEmisoraMap[stationUuid]!; } // Level 3: device-only. if (_presetsDispositivo.containsKey(deviceId)) { return _presetsDispositivo[deviceId]!; } // Level 4: global fallback. return _presetPrincipal; } /// Resolves the effective preset for the current station and device. PresetEcualizador _resolverPresetActivo() { final uuid = _emisoraActualUuid(); if (!_eqMultiDeviceEnabled) { return uuid != null ? (_presetsEmisoraMap[uuid] ?? _presetPrincipal) : _presetPrincipal; } final deviceId = _dispositivoActualId; if (uuid == null || deviceId == null) { return _presetPrincipal; } return presetEfectivo(stationUuid: uuid, deviceId: deviceId); } /// Loads the persisted EQ configuration and applies it to the audio engine. Future cargarPersistido() async { try { final config = await servicio.cargar(); _presetPrincipal = config.principal; _activo = config.activo; _eqMultiDeviceEnabled = config.eqMultiDeviceEnabled; _presetsEmisoraMap ..clear() ..addAll(config.porEmisora); _presetsDispositivo ..clear() ..addAll(config.presetsDispositivo); _presetsMatriz ..clear() ..addAll(config.presetsMatriz); _nombresDispositivos ..clear() ..addAll(config.nombresDispositivos); // Resolve active preset and apply it. _presetActual = _resolverPresetActivo(); await audio.setEcualizadorActivo(_activo); await audio.aplicarPreset(_presetActual); // Subscribe to device changes only when toggle is on. _configurarSuscripcionDispositivo(); // Seed current device immediately so presets resolve before first event. if (_eqMultiDeviceEnabled) { await _sembrarDispositivoActual(); } } catch (_) { _presetPrincipal = PresetEcualizador.flat; _presetActual = PresetEcualizador.flat; _activo = true; _eqMultiDeviceEnabled = false; _presetsEmisoraMap.clear(); _presetsDispositivo.clear(); _presetsMatriz.clear(); _nombresDispositivos.clear(); } } /// Queries the current device and seeds [_dispositivoActualId] without /// waiting for a stream event. Falls back to `'builtin_speaker'` on error. Future _sembrarDispositivoActual() async { final svc = _dispositivoAudio; if (svc == null) return; try { final dispositivo = await svc.obtenerDispositivoActual(); await _onDispositivoCambiado(dispositivo); } catch (_) { _dispositivoActualId = 'builtin_speaker'; } } /// Subscribes to the device change stream if multi-device is enabled. void _configurarSuscripcionDispositivo() { _deviceSub?.cancel(); _deviceSub = null; final svc = _dispositivoAudio; if (!_eqMultiDeviceEnabled || svc == null) return; _deviceSub = svc.onDispositivoCambiado.listen(_onDispositivoCambiado); } /// Called when a device change event arrives. Future _onDispositivoCambiado(DispositivoAudio dispositivo) async { if (!_eqMultiDeviceEnabled) return; _dispositivoActualId = dispositivo.id; // First-seen device: copy the current resolved preset as its starting point. if (!_presetsDispositivo.containsKey(dispositivo.id)) { final presetBase = _resolverPresetActivo(); _presetsDispositivo[dispositivo.id] = presetBase; await servicio.guardarPresetDispositivo(dispositivo.id, presetBase); } // Re-resolve and apply the preset for the new device. final resuelto = _resolverPresetActivo(); _presetActual = resuelto; await audio.aplicarPreset(resuelto); notifyListeners(); } /// Applies [preset] to the audio engine and tracks it as current /// WITHOUT persisting it (used when switching stations). Future aplicarPresetActivo(PresetEcualizador preset) async { _presetActual = preset; await audio.aplicarPreset(preset); } /// Enables or disables the multi-device EQ feature toggle. /// /// When disabled, the device stream subscription is cancelled and resolution /// immediately collapses to 2-level (station → global). Future cambiarMultiDeviceEnabled( bool habilitado, { bool notificar = true, }) async { _eqMultiDeviceEnabled = habilitado; await servicio.guardarToggleMultiDispositivo(habilitado); _configurarSuscripcionDispositivo(); if (notificar) notifyListeners(); } Future cambiarPresetPrincipal( PresetEcualizador preset, { bool notificar = true, }) async { _presetPrincipal = preset; await servicio.guardarPrincipal(preset); final uuid = _emisoraActualUuid(); final puedeAplicarAhora = uuid == null || !_presetsEmisoraMap.containsKey(uuid); if (puedeAplicarAhora) { await aplicarPresetActivo(preset); } if (notificar) notifyListeners(); } Future guardarPresetPorEmisora( String uuid, PresetEcualizador preset, { bool notificar = true, }) async { _presetsEmisoraMap[uuid] = preset; await servicio.guardarPorEmisora(uuid, preset); if (_emisoraActualUuid() == uuid) { await aplicarPresetActivo(preset); } if (notificar) notifyListeners(); } Future habilitarPresetPorEmisora( String uuid, { PresetEcualizador? base, bool notificar = true, }) async { final presetBase = base ?? _presetsEmisoraMap[uuid] ?? _presetPrincipal; await guardarPresetPorEmisora(uuid, presetBase, notificar: notificar); } Future deshabilitarPresetPorEmisora( String uuid, { bool notificar = true, }) async { _presetsEmisoraMap.remove(uuid); await servicio.eliminarPorEmisora(uuid); if (_emisoraActualUuid() == uuid) { await aplicarPresetActivo(_presetPrincipal); } if (notificar) notifyListeners(); } Future cambiarModoEmisoraActual({required bool usarPropio}) async { final uuid = _emisoraActualUuid(); if (uuid == null) return; if (usarPropio) { await habilitarPresetPorEmisora(uuid); } else { await deshabilitarPresetPorEmisora(uuid); } } /// Persists a custom display name for [deviceId]. /// /// Empty names are silently ignored so the existing name is preserved. /// No-op when the multi-device toggle is off. Future renombrarDispositivo(String deviceId, String nombre) async { if (!_eqMultiDeviceEnabled) return; final nombreTrimmed = nombre.trim(); if (nombreTrimmed.isEmpty) return; _nombresDispositivos[deviceId] = nombreTrimmed; await servicio.guardarNombresDispositivos( Map.unmodifiable(_nombresDispositivos), ); notifyListeners(); } /// Persists a per-device EQ preset for [deviceId]. /// /// Guards on [_eqMultiDeviceEnabled]: no-op when toggle is off. /// If [deviceId] is the currently active device, re-resolves and /// applies the effective preset immediately. Future guardarPresetDispositivo( String deviceId, PresetEcualizador preset, ) async { if (!_eqMultiDeviceEnabled) return; _presetsDispositivo[deviceId] = preset; await servicio.guardarPresetDispositivo(deviceId, preset); if (_dispositivoActualId == deviceId) { // Re-resolve using the full hierarchy (station/matrix may override). final resuelto = _resolverPresetActivo(); _presetActual = resuelto; await audio.aplicarPreset(resuelto); } notifyListeners(); } /// Returns the stored custom name for [deviceId], or an empty string if none. String obtenerNombreDispositivo(String deviceId) => _nombresDispositivos[deviceId] ?? ''; /// Resolves the display name for [deviceId] using the fallback chain: /// custom name → [platformName] → raw [deviceId]. String nombreVisible(String deviceId, String platformName) { final custom = _nombresDispositivos[deviceId]; if (custom != null && custom.isNotEmpty) return custom; if (platformName.isNotEmpty) return platformName; return deviceId; } Future cambiarActivo(bool activo) async { _activo = activo; await servicio.guardarActivo(activo); await audio.setEcualizadorActivo(activo); if (activo) { await audio.aplicarPreset(_presetActual); } notifyListeners(); } Future cambiarPreset( PresetEcualizador preset, { bool guardarPorEmisora = true, }) async { final uuid = _emisoraActualUuid(); final usarPresetPropio = guardarPorEmisora && uuid != null && _presetsEmisoraMap.containsKey(uuid); if (usarPresetPropio) { await guardarPresetPorEmisora(uuid, preset); return; } await cambiarPresetPrincipal(preset); } Future cambiarBanda(int index, double db) async { final bandas = List.from(_presetActual.bandas); if (index < 0 || index >= bandas.length) return; bandas[index] = db; final modificado = PresetEcualizador( nombre: 'Personalizado', bandas: bandas, ); await cambiarPreset(modificado); } /// Replaces the whole EQ configuration (backup import path): persists it, /// re-applies the preset effective for the current station and notifies. Future importarConfiguracion({ required PresetEcualizador principal, required Map porEmisora, Map? presetsDispositivo, Map? presetsMatriz, bool? eqMultiDeviceEnabled, }) async { _presetPrincipal = principal; _presetsEmisoraMap ..clear() ..addAll(porEmisora); if (presetsDispositivo != null) { _presetsDispositivo ..clear() ..addAll(presetsDispositivo); } if (presetsMatriz != null) { _presetsMatriz ..clear() ..addAll(presetsMatriz); } if (eqMultiDeviceEnabled != null) { _eqMultiDeviceEnabled = eqMultiDeviceEnabled; } await servicio.guardarConfiguracion( ConfiguracionEcualizador( principal: _presetPrincipal, porEmisora: _presetsEmisoraMap, activo: _activo, eqMultiDeviceEnabled: _eqMultiDeviceEnabled, presetsDispositivo: _presetsDispositivo, presetsMatriz: _presetsMatriz, ), ); final uuid = _emisoraActualUuid(); final presetEfectivoActual = uuid == null ? _presetPrincipal : _resolverPresetActivo(); await aplicarPresetActivo(presetEfectivoActual); notifyListeners(); } @override void dispose() { _deviceSub?.cancel(); super.dispose(); } }