# -*- coding: utf-8 -*-
"""
Ejemplo 26: Grabador de audio a MP3 con wxPython.

Permite seleccionar uno o dos dispositivos (micro + loopback),
grabar en tiempo real a MP3, pausar y detener.

Si se seleccionan 2 fuentes, cada una se graba por separado
y luego se mezclan con ffmpeg en un solo MP3.
"""

import wx
import ctypes
import time
import subprocess
import os
from pathlib import Path

from basskit.dlls.bass import bass
from basskit.dlls.bassenc import bassenc
from basskit.dlls.addons import bassenc_mp3
from basskit.callbacks.bass import RECORDPROC
from basskit.constantes.bass import (
	BASS_SAMPLE_FLOAT, BASS_DEVICE_LOOPBACK, BASS_DEVICE_DEFAULT,
)
from basskit.estructuras.bass import BASS_DEVICEINFO, BASS_RECORDINFO

_refs = []

@RECORDPROC
def _rec_proc(handle, buffer, length, user):
	return 1

_refs.append(_rec_proc)
_rec_proc_ptr = ctypes.cast(_rec_proc, ctypes.c_void_p)

SCRIPT_DIR = Path(__file__).parent
FFMPEG = SCRIPT_DIR / "ffmpeg.exe"


def listar_dispositivos():
	info = BASS_DEVICEINFO()
	i = 0
	devs = []
	while bass.BASS_RecordGetDeviceInfo(i, ctypes.byref(info)):
		nombre = info.name.decode("utf-8", errors="replace") if info.name else "?"
		es_loopback = bool(info.flags & BASS_DEVICE_LOOPBACK)
		es_defecto = bool(info.flags & BASS_DEVICE_DEFAULT)
		habilitado = bool(info.flags & 1)
		if habilitado:
			devs.append({
				"indice": i, "nombre": nombre,
				"loopback": es_loopback, "defecto": es_defecto,
				"etiqueta": f"{nombre} ({'Loopback' if es_loopback else 'Micro'})",
			})
		i += 1
	return devs


def obtener_freq_dispositivo():
	ri = BASS_RECORDINFO()
	if bass.BASS_RecordGetInfo(ctypes.byref(ri)) and ri.freq > 0:
		return ri.freq
	return 48000


def mezclar_con_ffmpeg(archivo1: str, archivo2: str, salida: str) -> bool:
	"""Mezcla dos MP3 en uno usando ffmpeg."""
	if not FFMPEG.exists():
		return False
	try:
		cmd = [
			str(FFMPEG), "-y",
			"-i", archivo1,
			"-i", archivo2,
			"-filter_complex", "amix=inputs=2:duration=longest",
			"-b:a", "192k",
			salida,
		]
		r = subprocess.run(cmd, capture_output=True, timeout=120,
			creationflags=subprocess.CREATE_NO_WINDOW)
		return r.returncode == 0
	except Exception:
		return False


class GrabadorMP3:
	def __init__(self):
		self._recs = []
		self._encoders = []
		self._archivos = []
		self._grabando = False
		self._pausado = False
		self._inicio = 0.0
		self._tiempo_pausado = 0.0
		self._pausa_inicio = 0.0
		self._dispositivos_init = []
		self._freq = 0
		self._multifuente = False

	def iniciar(self, indices: list[int], archivo: str) -> str | None:
		self._limpiar()
		self._multifuente = len(indices) > 1
		self._archivos.clear()

		for i, idx in enumerate(indices):
			if not bass.BASS_RecordInit(idx):
				return f"Error RecordInit({idx}): {bass.BASS_ErrorGetCode()}"
			self._dispositivos_init.append(idx)
			bass.BASS_RecordSetDevice(idx)

			freq = obtener_freq_dispositivo()
			self._freq = freq

			rec = bass.BASS_RecordStart(freq, 2, BASS_SAMPLE_FLOAT, _rec_proc_ptr, None)
			if rec == 0:
				rec = bass.BASS_RecordStart(freq, 1, BASS_SAMPLE_FLOAT, _rec_proc_ptr, None)
			if rec == 0:
				return f"Error RecordStart({idx}): {bass.BASS_ErrorGetCode()}"
			self._recs.append(rec)

			# Nombre de archivo para esta fuente
			if self._multifuente:
				base = Path(archivo)
				nombre_tmp = str(base.with_stem(f"{base.stem}_tmp{i+1}"))
			else:
				nombre_tmp = archivo
			self._archivos.append(nombre_tmp)

			enc = bassenc_mp3.BASS_Encode_MP3_StartFile(
				rec, b"--preset standard", 0, nombre_tmp.encode("utf-8"))
			if enc == 0:
				return f"Error Encoder: {bass.BASS_ErrorGetCode()}"
			self._encoders.append(enc)

		self._archivo_final = archivo
		self._grabando = True
		self._pausado = False
		self._inicio = time.time()
		self._tiempo_pausado = 0.0
		return None

	def pausar(self):
		if not self._grabando:
			return
		if self._pausado:
			self._tiempo_pausado += time.time() - self._pausa_inicio
			self._pausado = False
			for rec in self._recs:
				bass.BASS_ChannelPlay(rec, 0)
		else:
			self._pausado = True
			self._pausa_inicio = time.time()
			for rec in self._recs:
				bass.BASS_ChannelPause(rec)

	@property
	def esta_pausado(self):
		return self._pausado

	def detener(self) -> tuple[float, str]:
		"""Detiene y retorna (duracion, mensaje)."""
		if not self._grabando:
			return 0.0, ""
		self._grabando = False
		duracion = time.time() - self._inicio - self._tiempo_pausado
		if self._pausado:
			duracion -= (time.time() - self._pausa_inicio)

		for enc in self._encoders:
			bassenc.BASS_Encode_Stop(enc)
		self._encoders.clear()
		self._limpiar_canales()

		# Si multifuente, mezclar con ffmpeg
		msg = ""
		if self._multifuente and len(self._archivos) == 2:
			if not FFMPEG.exists():
				msg = (f"ffmpeg.exe no encontrado en {SCRIPT_DIR}.\n"
					   f"Los archivos separados se mantienen:\n"
					   + "\n".join(self._archivos))
			else:
				ok = mezclar_con_ffmpeg(
					self._archivos[0], self._archivos[1], self._archivo_final)
				if ok:
					# Borrar temporales
					for f in self._archivos:
						try:
							os.remove(f)
						except Exception:
							pass
					msg = f"Mezclado en: {self._archivo_final}"
				else:
					msg = ("Error al mezclar con ffmpeg.\n"
						   f"Archivos separados:\n"
						   + "\n".join(self._archivos))
		else:
			msg = f"Guardado: {self._archivo_final}"

		return max(0, duracion), msg

	def cancelar(self):
		self._grabando = False
		for enc in self._encoders:
			bassenc.BASS_Encode_Stop(enc)
		self._encoders.clear()
		self._limpiar_canales()
		# Borrar archivos parciales
		for f in self._archivos:
			try:
				os.remove(f)
			except Exception:
				pass

	def tiempo_transcurrido(self) -> float:
		if not self._grabando:
			return 0.0
		t = time.time() - self._inicio - self._tiempo_pausado
		if self._pausado:
			t -= (time.time() - self._pausa_inicio)
		return max(0, t)

	@property
	def activo(self):
		return self._grabando

	def _limpiar_canales(self):
		for rec in self._recs:
			bass.BASS_ChannelStop(rec)
			bass.BASS_StreamFree(rec)
		self._recs.clear()
		for idx in self._dispositivos_init:
			bass.BASS_RecordSetDevice(idx)
			bass.BASS_RecordFree()
		self._dispositivos_init.clear()

	def _limpiar(self):
		if self._grabando:
			self.cancelar()
		self._limpiar_canales()


class VentanaGrabador(wx.Frame):
	def __init__(self):
		super().__init__(None, title="Grabador de Audio a MP3 - basskit", size=(520, 430))
		bass.BASS_Init(0, 44100, 0, 0, None)
		self.motor = GrabadorMP3()
		self.devs = listar_dispositivos()
		self.archivo = ""
		self.timer = wx.Timer(self)
		self._crear_ui()
		self.Bind(wx.EVT_CLOSE, self._on_cerrar)
		self.Bind(wx.EVT_TIMER, self._on_timer, self.timer)
		self.Centre()

	def _crear_ui(self):
		panel = wx.Panel(self)
		s = wx.BoxSizer(wx.VERTICAL)

		box1 = wx.StaticBox(panel, label="Dispositivos de grabacion")
		bs1 = wx.StaticBoxSizer(box1, wx.VERTICAL)
		nombres = [d["etiqueta"] for d in self.devs]

		row1 = wx.BoxSizer(wx.HORIZONTAL)
		row1.Add(wx.StaticText(panel, label="Fuente &1:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
		self.choice1 = wx.Choice(panel, choices=["(ninguno)"] + nombres, name="Fuente 1")
		self.choice1.SetSelection(0)
		row1.Add(self.choice1, 1, wx.EXPAND)
		bs1.Add(row1, 0, wx.ALL | wx.EXPAND, 5)

		row2 = wx.BoxSizer(wx.HORIZONTAL)
		row2.Add(wx.StaticText(panel, label="Fuente &2:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
		self.choice2 = wx.Choice(panel, choices=["(ninguno)"] + nombres, name="Fuente 2")
		self.choice2.SetSelection(0)
		row2.Add(self.choice2, 1, wx.EXPAND)
		bs1.Add(row2, 0, wx.ALL | wx.EXPAND, 5)

		s.Add(bs1, 0, wx.ALL | wx.EXPAND, 8)

		box2 = wx.StaticBox(panel, label="Archivo de salida")
		bs2 = wx.StaticBoxSizer(box2, wx.HORIZONTAL)
		self.txt_archivo = wx.TextCtrl(panel, value="grabacion.mp3", name="Archivo de salida")
		self.btn_examinar = wx.Button(panel, label="E&xaminar...")
		bs2.Add(self.txt_archivo, 1, wx.ALL | wx.EXPAND, 5)
		bs2.Add(self.btn_examinar, 0, wx.ALL, 5)
		self.btn_examinar.Bind(wx.EVT_BUTTON, self._on_examinar)
		s.Add(bs2, 0, wx.ALL | wx.EXPAND, 8)

		box3 = wx.StaticBox(panel, label="Estado")
		bs3 = wx.StaticBoxSizer(box3, wx.VERTICAL)
		self.lbl_estado = wx.StaticText(panel, label="Listo para grabar")
		self.lbl_tiempo = wx.StaticText(panel, label="00:00:00")
		font = self.lbl_tiempo.GetFont()
		font.SetPointSize(18)
		font.SetWeight(wx.FONTWEIGHT_BOLD)
		self.lbl_tiempo.SetFont(font)
		bs3.Add(self.lbl_estado, 0, wx.ALL, 5)
		bs3.Add(self.lbl_tiempo, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
		s.Add(bs3, 0, wx.ALL | wx.EXPAND, 8)

		sb = wx.BoxSizer(wx.HORIZONTAL)
		self.btn_grabar = wx.Button(panel, label="&Grabar")
		self.btn_pausar = wx.Button(panel, label="&Pausar")
		self.btn_detener = wx.Button(panel, label="&Detener")
		self.btn_cancelar = wx.Button(panel, label="&Cancelar")
		self.btn_pausar.Disable()
		self.btn_detener.Disable()
		self.btn_cancelar.Disable()
		for b in [self.btn_grabar, self.btn_pausar, self.btn_detener, self.btn_cancelar]:
			sb.Add(b, 1, wx.ALL, 5)
		s.Add(sb, 0, wx.ALL | wx.EXPAND, 8)

		self.btn_grabar.Bind(wx.EVT_BUTTON, self._on_grabar)
		self.btn_pausar.Bind(wx.EVT_BUTTON, self._on_pausar)
		self.btn_detener.Bind(wx.EVT_BUTTON, self._on_detener)
		self.btn_cancelar.Bind(wx.EVT_BUTTON, self._on_cancelar)
		panel.SetSizer(s)

	def _on_examinar(self, e):
		dlg = wx.FileDialog(self, "Guardar como MP3",
			wildcard="MP3|*.mp3", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
			defaultFile="grabacion.mp3")
		if dlg.ShowModal() == wx.ID_OK:
			self.txt_archivo.SetValue(dlg.GetPath())
		dlg.Destroy()

	def _on_grabar(self, e):
		indices = []
		sel1 = self.choice1.GetSelection()
		sel2 = self.choice2.GetSelection()
		if sel1 > 0:
			indices.append(self.devs[sel1 - 1]["indice"])
		if sel2 > 0:
			idx2 = self.devs[sel2 - 1]["indice"]
			if idx2 not in indices:
				indices.append(idx2)
		if not indices:
			wx.MessageBox("Selecciona al menos un dispositivo.", "Error", wx.OK | wx.ICON_WARNING)
			return

		self.archivo = self.txt_archivo.GetValue().strip()
		if not self.archivo:
			wx.MessageBox("Introduce un nombre de archivo.", "Error", wx.OK | wx.ICON_WARNING)
			return
		if not self.archivo.lower().endswith(".mp3"):
			self.archivo += ".mp3"

		err = self.motor.iniciar(indices, self.archivo)
		if err:
			wx.MessageBox(f"Error:\n{err}", "Error", wx.OK | wx.ICON_ERROR)
			return

		fuentes = []
		for idx in indices:
			for d in self.devs:
				if d["indice"] == idx:
					fuentes.append(d["etiqueta"])
		info = f"Grabando ({self.motor._freq} Hz): {', '.join(fuentes)}"
		if len(indices) > 1:
			info += "\n(Se mezclaran con ffmpeg al detener)"
		self.lbl_estado.SetLabel(info)
		self.btn_grabar.Disable()
		self.btn_pausar.Enable()
		self.btn_detener.Enable()
		self.btn_cancelar.Enable()
		self.choice1.Disable()
		self.choice2.Disable()
		self.txt_archivo.Disable()
		self.btn_examinar.Disable()
		self.timer.Start(500)

	def _on_pausar(self, e):
		self.motor.pausar()
		if self.motor.esta_pausado:
			self.btn_pausar.SetLabel("&Reanudar")
			self.lbl_estado.SetLabel("Grabacion en PAUSA")
		else:
			self.btn_pausar.SetLabel("&Pausar")
			self.lbl_estado.SetLabel("Grabando...")

	def _on_detener(self, e):
		self.lbl_estado.SetLabel("Deteniendo y mezclando...")
		wx.Yield()
		duracion, msg = self.motor.detener()
		self.timer.Stop()
		mins = int(duracion) // 60
		secs = int(duracion) % 60
		self.lbl_estado.SetLabel(msg)
		self._restaurar_ui()
		wx.MessageBox(
			f"{msg}\nDuracion: {mins:02d}:{secs:02d}",
			"Completado", wx.OK | wx.ICON_INFORMATION)

	def _on_cancelar(self, e):
		self.motor.cancelar()
		self.timer.Stop()
		self.lbl_estado.SetLabel("Cancelada")
		self.lbl_tiempo.SetLabel("00:00:00")
		self._restaurar_ui()

	def _restaurar_ui(self):
		self.btn_grabar.Enable()
		self.btn_pausar.Disable()
		self.btn_pausar.SetLabel("&Pausar")
		self.btn_detener.Disable()
		self.btn_cancelar.Disable()
		self.choice1.Enable()
		self.choice2.Enable()
		self.txt_archivo.Enable()
		self.btn_examinar.Enable()

	def _on_timer(self, e):
		t = self.motor.tiempo_transcurrido()
		h = int(t) // 3600
		m = (int(t) % 3600) // 60
		s = int(t) % 60
		self.lbl_tiempo.SetLabel(f"{h:02d}:{m:02d}:{s:02d}")

	def _on_cerrar(self, e):
		self.timer.Stop()
		if self.motor.activo:
			self.motor.cancelar()
		bass.BASS_Free()
		self.Destroy()


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


if __name__ == "__main__":
	main()
