# -*- coding: utf-8 -*-
"""
Super Reproductor Accesible v0.3.0 - wxPython + basskit completo.
Usa TODOS los 25 efectos, Analizador, LectorTags, Tempo/Pitch, EQ, Reverse.
Interfaz 100% accesible con lectores de pantalla.
Requisitos: pip install wxPython
"""
import wx
import sys, os, ctypes, json
from pathlib import Path

from basskit.dlls.bass import bass
from basskit.dlls.bass_fx import bass_fx
from basskit.constantes.bass import (
	BASS_UNICODE, BASS_STREAM_DECODE, BASS_ATTRIB_VOL, BASS_ATTRIB_PAN,
	BASS_POS_BYTE, BASS_ACTIVE_PLAYING, BASS_ACTIVE_PAUSED,
	BASS_ACTIVE_STOPPED, BASS_SAMPLE_LOOP, BASS_SAMPLE_FLOAT,
)
from basskit.constantes.bass_fx import (
	BASS_FX_FREESOURCE, BASS_ATTRIB_TEMPO, BASS_ATTRIB_TEMPO_PITCH,
	BASS_ATTRIB_REVERSE_DIR, BASS_FX_RVS_FORWARD, BASS_FX_RVS_REVERSE,
	BASS_FX_BFX_PEAKEQ,
)
from basskit.estructuras.bass import BASS_CHANNELINFO
from basskit.estructuras.addons import BASS_BFX_PEAKEQ
from basskit.alto_nivel.lector_tags import LectorTags
from basskit.alto_nivel.efectos import CadenaEfectos, TODOS_LOS_EFECTOS
from basskit.alto_nivel.analizador import Analizador

FORMATOS = (
	"Audio|*.mp3;*.wav;*.ogg;*.flac;*.opus;*.wma;*.aac;*.m4a;*.wv;"
	"*.ape;*.mpc;*.tta;*.ac3;*.mid;*.midi;*.mod;*.xm;*.it;*.s3m|"
	"Todos|*.*"
)
EQ_BANDAS = [60, 170, 400, 1000, 2400, 6000, 12000, 15000]
EQ_NOMBRES = ["60", "170", "400", "1k", "2.4k", "6k", "12k", "15k"]

# Efectos con nombres legibles para la UI
EFECTOS_UI = [
	("Efectos DX8", [
		("chorus", "Chorus DX8"), ("compressor_dx8", "Compresor DX8"),
		("distortion_dx8", "Distorsion DX8"), ("echo", "Echo DX8"),
		("flanger_dx8", "Flanger DX8"), ("gargle", "Gargle DX8"),
		("i3dl2reverb", "I3DL2 Reverb"), ("parameq", "ParamEQ DX8"),
		("reverb", "Reverb DX8"), ("volume_dsp", "Volume DSP"),
	]),
	("Efectos BASS_FX", [
		("bfx_echo", "Echo BFX"), ("bfx_flanger", "Flanger BFX"),
		("bfx_reverb", "Reverb BFX"), ("bfx_lpf", "Filtro Paso Bajo"),
		("bfx_damp", "Amplificacion Dinamica"), ("bfx_autowah", "AutoWah"),
		("bfx_echo2", "Echo 2 (avanzado)"), ("bfx_phaser", "Phaser"),
		("bfx_echo3", "Echo 3 (stereo)"), ("bfx_chorus", "Chorus BFX"),
		("bfx_apf", "Filtro Pasa-Todo"), ("bfx_compressor", "Compresor BFX"),
		("bfx_distortion", "Distorsion BFX"), ("bfx_compressor2", "Compresor 2"),
		("bfx_bqf", "Filtro BiQuad"),
	]),
]


class PanelTransporte(wx.Panel):
	"""Controles de transporte y posicion."""
	def __init__(self, parent, app):
		super().__init__(parent)
		self.app = app
		s = wx.BoxSizer(wx.VERTICAL)
		# Info
		self.lbl_titulo = wx.StaticText(self, label="Sin archivo cargado")
		self.lbl_artista = wx.StaticText(self, label="")
		self.lbl_formato = wx.StaticText(self, label="")
		s.Add(self.lbl_titulo, 0, wx.ALL|wx.EXPAND, 3)
		s.Add(self.lbl_artista, 0, wx.ALL|wx.EXPAND, 3)
		s.Add(self.lbl_formato, 0, wx.ALL|wx.EXPAND, 3)
		# Posicion
		sp = wx.BoxSizer(wx.HORIZONTAL)
		self.lbl_pos = wx.StaticText(self, label="00:00 / 00:00")
		self.slider_pos = wx.Slider(self, minValue=0, maxValue=1000, name="Posicion")
		sp.Add(self.lbl_pos, 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
		sp.Add(self.slider_pos, 1, wx.EXPAND)
		s.Add(sp, 0, wx.ALL|wx.EXPAND, 3)
		self.slider_pos.Bind(wx.EVT_SLIDER, self._on_pos)
		# Botones
		sb = wx.BoxSizer(wx.HORIZONTAL)
		self.btn_abrir = wx.Button(self, label="&Abrir")
		self.btn_play = wx.Button(self, label="&Reproducir")
		self.btn_pause = wx.Button(self, label="&Pausar")
		self.btn_stop = wx.Button(self, label="&Detener")
		self.chk_loop = wx.CheckBox(self, label="&Loop")
		self.chk_reverse = wx.CheckBox(self, label="Re&verse")
		for w in [self.btn_abrir, self.btn_play, self.btn_pause, self.btn_stop, self.chk_loop, self.chk_reverse]:
			sb.Add(w, 0, wx.ALL, 2)
		s.Add(sb, 0, wx.ALL|wx.EXPAND, 3)
		self.btn_abrir.Bind(wx.EVT_BUTTON, self._on_abrir)
		self.btn_play.Bind(wx.EVT_BUTTON, lambda e: self.app.play())
		self.btn_pause.Bind(wx.EVT_BUTTON, lambda e: self.app.pause())
		self.btn_stop.Bind(wx.EVT_BUTTON, lambda e: self.app.stop())
		self.chk_loop.Bind(wx.EVT_CHECKBOX, self._on_loop)
		self.chk_reverse.Bind(wx.EVT_CHECKBOX, self._on_reverse)
		self.SetSizer(s)

	def _on_abrir(self, e):
		dlg = wx.FileDialog(self, "Abrir audio", wildcard=FORMATOS, style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
		if dlg.ShowModal() == wx.ID_OK:
			self.app.cargar(dlg.GetPath())
		dlg.Destroy()

	def _on_pos(self, e):
		self.app.seek(self.slider_pos.GetValue() / 1000.0)

	def _on_loop(self, e):
		self.app.set_loop(self.chk_loop.GetValue())

	def _on_reverse(self, e):
		self.app.set_reverse(self.chk_reverse.GetValue())


class PanelVolumen(wx.Panel):
	"""Volumen, pan, tempo, pitch."""
	def __init__(self, parent, app):
		super().__init__(parent)
		self.app = app
		s = wx.BoxSizer(wx.VERTICAL)
		self.sliders = {}
		for nombre, label, mn, mx, val in [
			("vol", "Volumen", 0, 100, 100), ("pan", "Panorama", -100, 100, 0),
			("tempo", "Tempo %", -50, 50, 0), ("pitch", "Pitch st", -12, 12, 0)]:
			row = wx.BoxSizer(wx.HORIZONTAL)
			row.Add(wx.StaticText(self, label=f"{label}:"), 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
			sl = wx.Slider(self, value=val, minValue=mn, maxValue=mx, name=label)
			lbl = wx.StaticText(self, label=str(val), size=(50, -1))
			row.Add(sl, 1, wx.EXPAND)
			row.Add(lbl, 0, wx.LEFT|wx.ALIGN_CENTER_VERTICAL, 5)
			s.Add(row, 0, wx.ALL|wx.EXPAND, 2)
			sl.Bind(wx.EVT_SLIDER, lambda e, n=nombre, l=lbl: self._on_slider(e, n, l))
			self.sliders[nombre] = (sl, lbl)
		btn = wx.Button(self, label="Resetear Tempo/Pitch")
		btn.Bind(wx.EVT_BUTTON, self._on_reset)
		s.Add(btn, 0, wx.ALL, 3)
		self.SetSizer(s)

	def _on_slider(self, e, nombre, lbl):
		v = e.GetEventObject().GetValue()
		lbl.SetLabel(str(v))
		if nombre == "vol": self.app.set_vol(v/100.0)
		elif nombre == "pan": self.app.set_pan(v/100.0)
		elif nombre == "tempo": self.app.set_tempo(float(v))
		elif nombre == "pitch": self.app.set_pitch(float(v))

	def _on_reset(self, e):
		for n in ["tempo", "pitch"]:
			sl, lbl = self.sliders[n]
			sl.SetValue(0); lbl.SetLabel("0")
		self.app.set_tempo(0); self.app.set_pitch(0)


class PanelEQ(wx.Panel):
	"""Ecualizador de 8 bandas."""
	def __init__(self, parent, app):
		super().__init__(parent)
		self.app = app
		s = wx.BoxSizer(wx.HORIZONTAL)
		self.sliders = []
		self.labels = []
		for i, nombre in enumerate(EQ_NOMBRES):
			col = wx.BoxSizer(wx.VERTICAL)
			lbl = wx.StaticText(self, label="0dB", style=wx.ALIGN_CENTRE_HORIZONTAL)
			sl = wx.Slider(self, value=0, minValue=-15, maxValue=15,
				style=wx.SL_VERTICAL|wx.SL_INVERSE, name=f"EQ {nombre} Hz")
			sl.SetMinSize((30, 80))
			nlbl = wx.StaticText(self, label=nombre, style=wx.ALIGN_CENTRE_HORIZONTAL)
			col.Add(lbl, 0, wx.ALIGN_CENTER)
			col.Add(sl, 1, wx.ALIGN_CENTER|wx.ALL, 1)
			col.Add(nlbl, 0, wx.ALIGN_CENTER)
			s.Add(col, 1, wx.ALL|wx.EXPAND, 2)
			sl.Bind(wx.EVT_SLIDER, lambda e, idx=i: self._on_eq(e, idx))
			self.sliders.append(sl); self.labels.append(lbl)
		self.SetSizer(s)

	def _on_eq(self, e, idx):
		v = self.sliders[idx].GetValue()
		self.labels[idx].SetLabel(f"{v:+d}dB")
		self.app.set_eq(idx, float(v))


class PanelEfectos(wx.Panel):
	"""Panel con TODOS los 25 efectos como checkboxes."""
	def __init__(self, parent, app):
		super().__init__(parent)
		self.app = app
		s = wx.BoxSizer(wx.VERTICAL)
		self.checks = {}
		for grupo, efectos in EFECTOS_UI:
			box = wx.StaticBox(self, label=grupo)
			bs = wx.StaticBoxSizer(box, wx.VERTICAL)
			grid = wx.FlexGridSizer(cols=3, hgap=10, vgap=3)
			for clave, nombre in efectos:
				chk = wx.CheckBox(self, label=nombre)
				chk.Bind(wx.EVT_CHECKBOX, lambda e, k=clave: self._on_toggle(e, k))
				grid.Add(chk, 0, wx.ALL, 2)
				self.checks[clave] = chk
			bs.Add(grid, 0, wx.EXPAND|wx.ALL, 3)
			s.Add(bs, 0, wx.ALL|wx.EXPAND, 3)
		btn_off = wx.Button(self, label="Quitar &todos los efectos")
		btn_off.Bind(wx.EVT_BUTTON, self._on_clear)
		s.Add(btn_off, 0, wx.ALL, 5)
		self.SetSizer(s)

	def _on_toggle(self, e, clave):
		if e.IsChecked():
			self.app.efecto_on(clave)
		else:
			self.app.efecto_off(clave)

	def _on_clear(self, e):
		for k, chk in self.checks.items():
			if chk.GetValue():
				chk.SetValue(False)
				self.app.efecto_off(k)


class PanelAnalizador(wx.Panel):
	"""VU meters y info de loudness."""
	def __init__(self, parent, app):
		super().__init__(parent)
		self.app = app
		s = wx.BoxSizer(wx.VERTICAL)
		self.lbl_nivel = wx.StaticText(self, label="Nivel: -- / --")
		self.lbl_lufs = wx.StaticText(self, label="LUFS: --")
		self.lbl_peak = wx.StaticText(self, label="True Peak: --")
		self.gauge_l = wx.Gauge(self, range=100, name="Nivel izquierdo")
		self.gauge_r = wx.Gauge(self, range=100, name="Nivel derecho")
		s.Add(wx.StaticText(self, label="Nivel Izquierdo:"), 0, wx.ALL, 2)
		s.Add(self.gauge_l, 0, wx.ALL|wx.EXPAND, 2)
		s.Add(wx.StaticText(self, label="Nivel Derecho:"), 0, wx.ALL, 2)
		s.Add(self.gauge_r, 0, wx.ALL|wx.EXPAND, 2)
		s.Add(self.lbl_nivel, 0, wx.ALL, 3)
		s.Add(self.lbl_lufs, 0, wx.ALL, 3)
		s.Add(self.lbl_peak, 0, wx.ALL, 3)
		self.SetSizer(s)

	def actualizar(self, izq, der, lufs, peak):
		self.gauge_l.SetValue(min(100, int(izq*100)))
		self.gauge_r.SetValue(min(100, int(der*100)))
		self.lbl_nivel.SetLabel(f"Nivel: L={izq:.2f} R={der:.2f}")
		self.lbl_lufs.SetLabel(f"LUFS integrada: {lufs:.1f}")
		self.lbl_peak.SetLabel(f"True Peak: {peak:.1f} dBFS")


class ReproductorApp:
	"""Logica central del reproductor."""
	def __init__(self):
		self._stream = 0
		self._lector = LectorTags()
		self._cadena = CadenaEfectos()
		self._analizador = Analizador()
		self._eq_fx = []
		self._eq_gains = [0.0]*len(EQ_BANDAS)
		self._loop = False

	def cargar(self, ruta):
		self._limpiar()
		ruta_b = ruta.encode("utf-16-le") + b"\x00\x00"
		flags = BASS_STREAM_DECODE | BASS_UNICODE | BASS_SAMPLE_FLOAT
		decode = bass.BASS_StreamCreateFile(0, ruta_b, 0, 0, flags)
		if decode == 0:
			decode = bass.BASS_MusicLoad(0, ruta_b, 0, 0, flags|BASS_UNICODE, 0)
		if decode == 0:
			return False
		stream = bass_fx.BASS_FX_TempoCreate(decode, BASS_FX_FREESOURCE)
		if stream == 0:
			return False
		self._stream = stream
		self._cadena.canal = stream
		self._analizador.canal = stream
		self._analizador.iniciar_loudness()
		self._aplicar_eq()
		return True

	def info(self, ruta):
		tags = self._lector.leer(ruta)
		titulo = tags.get("TITLE", Path(ruta).stem)
		artista = tags.get("ARTIST", "")
		info = BASS_CHANNELINFO()
		dur = ""
		if self._stream and bass.BASS_ChannelGetInfo(self._stream, ctypes.byref(info)):
			total = bass.BASS_ChannelGetLength(self._stream, BASS_POS_BYTE)
			secs = bass.BASS_ChannelBytes2Seconds(self._stream, total)
			dur = f"{int(secs)//60:02d}:{int(secs)%60:02d}"
			return titulo, artista, f"{info.freq}Hz {info.chans}ch {dur}"
		return titulo, artista, ""

	def play(self):
		if self._stream: bass.BASS_ChannelPlay(self._stream, 0)
	def pause(self):
		if self._stream:
			if bass.BASS_ChannelIsActive(self._stream)==BASS_ACTIVE_PLAYING:
				bass.BASS_ChannelPause(self._stream)
			else:
				bass.BASS_ChannelPlay(self._stream, 0)
	def stop(self):
		if self._stream:
			bass.BASS_ChannelStop(self._stream)
			bass.BASS_ChannelSetPosition(self._stream, 0, BASS_POS_BYTE)
	def seek(self, frac):
		if self._stream:
			total = bass.BASS_ChannelGetLength(self._stream, BASS_POS_BYTE)
			bass.BASS_ChannelSetPosition(self._stream, int(total*frac), BASS_POS_BYTE)
	def set_vol(self, v):
		if self._stream: bass.BASS_ChannelSetAttribute(self._stream, BASS_ATTRIB_VOL, v)
	def set_pan(self, v):
		if self._stream: bass.BASS_ChannelSetAttribute(self._stream, BASS_ATTRIB_PAN, v)
	def set_tempo(self, v):
		if self._stream: bass.BASS_ChannelSetAttribute(self._stream, BASS_ATTRIB_TEMPO, v)
	def set_pitch(self, v):
		if self._stream: bass.BASS_ChannelSetAttribute(self._stream, BASS_ATTRIB_TEMPO_PITCH, v)
	def set_loop(self, on):
		self._loop = on
		if self._stream:
			bass.BASS_ChannelFlags(self._stream, BASS_SAMPLE_LOOP if on else 0, BASS_SAMPLE_LOOP)
	def set_reverse(self, on):
		if self._stream:
			d = float(BASS_FX_RVS_REVERSE if on else BASS_FX_RVS_FORWARD)
			bass.BASS_ChannelSetAttribute(self._stream, BASS_ATTRIB_REVERSE_DIR, d)

	def set_eq(self, idx, gain):
		self._eq_gains[idx] = gain
		if idx < len(self._eq_fx) and self._eq_fx[idx]:
			eq = BASS_BFX_PEAKEQ()
			eq.lBand = idx; eq.fCenter = float(EQ_BANDAS[idx])
			eq.fBandwidth = 2.5; eq.fGain = gain; eq.lChannel = -1
			bass.BASS_FXSetParameters(self._eq_fx[idx], ctypes.byref(eq))

	def _aplicar_eq(self):
		self._eq_fx.clear()
		if not self._stream: return
		for i, freq in enumerate(EQ_BANDAS):
			fx = bass.BASS_ChannelSetFX(self._stream, BASS_FX_BFX_PEAKEQ, 0)
			if fx:
				eq = BASS_BFX_PEAKEQ()
				eq.lBand = i; eq.fCenter = float(freq)
				eq.fBandwidth = 2.5; eq.fGain = self._eq_gains[i]; eq.lChannel = -1
				bass.BASS_FXSetParameters(fx, ctypes.byref(eq))
			self._eq_fx.append(fx)

	def efecto_on(self, nombre):
		self._cadena.agregar(nombre)
	def efecto_off(self, nombre):
		self._cadena.quitar(nombre)

	def pos_info(self):
		if not self._stream: return 0, 0, 0
		total = bass.BASS_ChannelGetLength(self._stream, BASS_POS_BYTE)
		pos = bass.BASS_ChannelGetPosition(self._stream, BASS_POS_BYTE)
		return pos, total, bass.BASS_ChannelIsActive(self._stream)

	def nivel(self):
		return self._analizador.nivel()
	def lufs(self):
		return self._analizador.loudness_integrada()
	def peak(self):
		return self._analizador.true_peak()

	def _limpiar(self):
		self._cadena.limpiar()
		self._analizador.detener_loudness()
		if self._stream: bass.BASS_StreamFree(self._stream)
		self._stream = 0; self._eq_fx.clear()

	def cerrar(self):
		self._limpiar()


class VentanaPrincipal(wx.Frame):
	def __init__(self):
		super().__init__(None, title="Basskit Super Reproductor v0.3.0 - 25 Efectos", size=(750, 700))
		bass.BASS_Init(-1, 44100, 0, 0, None)
		self.app = ReproductorApp()
		self.timer = wx.Timer(self)
		nb = wx.Notebook(self)
		# Pestana Reproductor
		p1 = wx.Panel(nb)
		s1 = wx.BoxSizer(wx.VERTICAL)
		self.transporte = PanelTransporte(p1, self.app)
		self.volumen = PanelVolumen(p1, self.app)
		s1.Add(self.transporte, 0, wx.ALL|wx.EXPAND, 3)
		s1.Add(self.volumen, 0, wx.ALL|wx.EXPAND, 3)
		p1.SetSizer(s1)
		nb.AddPage(p1, "Reproductor")
		# Pestana EQ
		self.eq_panel = PanelEQ(nb, self.app)
		nb.AddPage(self.eq_panel, "Ecualizador")
		# Pestana Efectos
		sp = wx.ScrolledWindow(nb)
		sp.SetScrollRate(0, 20)
		self.efectos_panel = PanelEfectos(sp, self.app)
		ss = wx.BoxSizer(wx.VERTICAL)
		ss.Add(self.efectos_panel, 1, wx.EXPAND)
		sp.SetSizer(ss)
		nb.AddPage(sp, "Efectos (25)")
		# Pestana Analizador
		self.analizador_panel = PanelAnalizador(nb, self.app)
		nb.AddPage(self.analizador_panel, "Analizador")

		# Barra estado
		self.status = self.CreateStatusBar()
		self.status.SetStatusText("Listo. Ctrl+O para abrir.")

		# Atajos
		accel = wx.AcceleratorTable([
			(wx.ACCEL_CTRL, ord('O'), self.transporte.btn_abrir.GetId()),
			(wx.ACCEL_NORMAL, wx.WXK_SPACE, self.transporte.btn_pause.GetId()),
		])
		self.SetAcceleratorTable(accel)

		# Redefinir abrir para actualizar info
		self.transporte.btn_abrir.Bind(wx.EVT_BUTTON, self._on_abrir)
		self.Bind(wx.EVT_TIMER, self._on_timer, self.timer)
		self.Bind(wx.EVT_CLOSE, self._on_cerrar)
		self.Centre()

	def _on_abrir(self, e):
		dlg = wx.FileDialog(self, "Abrir audio", wildcard=FORMATOS, style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
		if dlg.ShowModal() == wx.ID_OK:
			ruta = dlg.GetPath()
			if self.app.cargar(ruta):
				t, a, f = self.app.info(ruta)
				self.transporte.lbl_titulo.SetLabel(f"Titulo: {t}")
				self.transporte.lbl_artista.SetLabel(f"Artista: {a}" if a else "")
				self.transporte.lbl_formato.SetLabel(f"Formato: {f}")
				self.status.SetStatusText(f"Cargado: {Path(ruta).name}")
				self.timer.Start(200)
			else:
				wx.MessageBox(f"Error al abrir archivo.\nCodigo: {bass.BASS_ErrorGetCode()}", "Error", wx.OK|wx.ICON_ERROR)
		dlg.Destroy()

	def _fmt(self, s):
		s = max(0, s); return f"{int(s)//60:02d}:{int(s)%60:02d}"

	def _on_timer(self, e):
		pos, total, estado = self.app.pos_info()
		if total > 0:
			ps = bass.BASS_ChannelBytes2Seconds(self.app._stream, pos)
			ts = bass.BASS_ChannelBytes2Seconds(self.app._stream, total)
			self.transporte.lbl_pos.SetLabel(f"{self._fmt(ps)} / {self._fmt(ts)}")
			self.transporte.slider_pos.SetValue(int(pos*1000/total))
		izq, der = self.app.nivel()
		self.analizador_panel.actualizar(izq, der, self.app.lufs(), self.app.peak())
		if estado == BASS_ACTIVE_STOPPED:
			self.timer.Stop()
			self.status.SetStatusText("Finalizado")

	def _on_cerrar(self, e):
		self.timer.Stop()
		self.app.cerrar()
		bass.BASS_Free()
		self.Destroy()


def main():
	app = wx.App()
	v = VentanaPrincipal()
	v.Show()
	app.MainLoop()

if __name__ == "__main__":
	main()
