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 = {}; 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 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); // 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(); } catch (_) { _presetPrincipal = PresetEcualizador.flat; _presetActual = PresetEcualizador.flat; _activo = true; _eqMultiDeviceEnabled = false; _presetsEmisoraMap.clear(); _presetsDispositivo.clear(); _presetsMatriz.clear(); } } /// 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); } } 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(); } }