// ui/botonera/ventana_botonera.rs — GR Botonera de Efectos
//
// 5 pestañas renombrables con doble clic
// FILAS x COLS botones por pestaña
// Drag & drop de audios
// Clic: reproducir / detener con mpv
// Paleta de colores con contraste garantizado

use gtk4 as gtk;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Box as GtkBox, Button, Entry,
    Label, Notebook, Orientation, ScrolledWindow, Grid,
    DropTarget, GestureClick, PopoverMenu,
    FileChooserDialog, FileFilter,
};
use gtk::gdk;
use glib;
use std::cell::RefCell;
use std::rc::Rc;
use gstreamer as gst;
use gst::prelude::*;

use crate::storage_botonera::{
    self, Boton, TabBotonera, FILAS, COLS, NUM_TABS, nombre_desde_ruta,
};
use crate::ui::comun;
use crate::skin;

const ICONO_BOTONERA: &[u8] = include_bytes!("../../../assets/botonera48x48.png");

// ─── Paleta de colores ─────────────────────────────────────────────────────

const PALETA: &[&str] = &[
    "#1565c0", "#2e7d32", "#6a1b9a", "#c62828", "#e65100",
    "#00695c", "#ad1457", "#4527a0", "#558b2f", "#00838f",
    "#f57f17", "#283593", "#37474f", "#4e342e", "#00600f",
];

fn color_texto(hex_bg: &str) -> &'static str {
    let hex = hex_bg.trim_start_matches('#');
    if hex.len() < 6 { return "white"; }
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32;
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32;
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32;
    let lum = 0.2126*(r/255.0) + 0.7152*(g/255.0) + 0.0722*(b/255.0);
    if lum > 0.45 { "black" } else { "white" }
}

// ─── Estado ────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, PartialEq)]
struct Reproduciendo {
    tab: usize, fila: usize, col: usize,
}

struct Estado {
    reproduciendo: Option<Reproduciendo>,
    css_provider: gtk::CssProvider,
    pipeline: Option<gst::Pipeline>,
}

impl Estado {
    fn nuevo() -> Self {
        Estado {
            reproduciendo: None,
            css_provider: gtk::CssProvider::new(),
            pipeline: None,
        }
    }
}

fn path_to_uri(path: &str) -> String {
    #[cfg(target_os = "windows")]
    {
        let fwd = path.replace('\\', "/");
        let encoded: String = fwd.bytes().map(|b| match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
            | b'-' | b'_' | b'.' | b'~' | b'/' | b':' => (b as char).to_string(),
            _ => format!("%{:02X}", b),
        }).collect();
        if encoded.len() >= 2 && encoded.as_bytes()[1] == b':' {
            return format!("file:///{}", encoded);
        }
        return format!("file://{}", encoded);
    }
    #[cfg(not(target_os = "windows"))]
    {
        let encoded: String = path.chars().map(|c| match c {
            ' '  => "%20".to_string(),
            '#'  => "%23".to_string(),
            '?'  => "%3F".to_string(),
            '%'  => "%25".to_string(),
            _    => c.to_string(),
        }).collect();
        format!("file://{}", encoded)
    }
}

// ─── Ventana ───────────────────────────────────────────────────────────────

pub fn construir_ventana_botonera(app: &Application) {
    storage_botonera::crear_directorios();

    let tabs_data = Rc::new(RefCell::new(storage_botonera::cargar()));
    let estado = Rc::new(RefCell::new(Estado::nuevo()));

    let ventana = ApplicationWindow::builder()
        .application(app)
        .title("GR_Botonera")
        .default_width(860)
        .default_height(640)
        .build();

    // CSS base
    let css_base = gtk::CssProvider::new();
    css_base.load_from_data(concat!(
        "button.fx-btn { border-radius:4px; border:1px solid rgba(255,255,255,0.2);",
        "  font-size:10pt; font-weight:bold; min-height:60px; }",
        "button.fx-vacio { background:#2a2a2a; color:#555;",
        "  border:1px dashed #444; border-radius:4px; min-height:60px; }",
        "button.fx-vacio:hover { background:#333; border-color:#666; }",
        "button.fx-play { border:3px solid #ffffff !important;",
        "  border-radius:4px; min-height:60px; font-weight:bold; }",
        "label.tab-label { font-weight:bold; padding:2px 8px; }",
    ));
    gtk::style_context_add_provider_for_display(
        &gtk4::prelude::WidgetExt::display(&ventana),
        &css_base,
        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );

    // Provider dinámico para colores de botones (se actualiza al cambiar estado)
    {
        let e = estado.borrow();
        gtk::style_context_add_provider_for_display(
            &gtk4::prelude::WidgetExt::display(&ventana),
            &e.css_provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
        );
    }
    skin::aplicar_css_extra(&gtk4::prelude::WidgetExt::display(&ventana));

    let vbox = GtkBox::new(Orientation::Vertical, 0);

    let notebook = Notebook::new();
    notebook.set_vexpand(true);

    for tab_idx in 0..NUM_TABS {
        let (tab_widget, tab_label) = construir_tab(
            tab_idx, tabs_data.clone(), estado.clone(),
        );
        notebook.append_page(&tab_widget, Some(&tab_label));
    }

    vbox.append(&notebook);
    ventana.set_child(Some(&vbox));

    // Render inicial
    actualizar_css_colores(0, &tabs_data, &estado);

    comun::aplicar_icono_ventana(&ventana, &skin::icono("botonera48x48.png", ICONO_BOTONERA));
    comun::aplicar_tamanio_ventana(&ventana, 900, 680);
    ventana.present();
}

// ─── Tab ───────────────────────────────────────────────────────────────────

fn construir_tab(
    tab_idx: usize,
    tabs_data: Rc<RefCell<Vec<TabBotonera>>>,
    estado: Rc<RefCell<Estado>>,
) -> (gtk::Widget, gtk::Widget) {
    let scroll = ScrolledWindow::new();
    scroll.set_hexpand(true);
    scroll.set_vexpand(true);

    let grid = Grid::new();
    grid.set_column_spacing(4);
    grid.set_row_spacing(4);
    grid.set_column_homogeneous(true);
    grid.set_row_homogeneous(true);
    grid.set_margin_top(8);
    grid.set_margin_bottom(8);
    grid.set_margin_start(8);
    grid.set_margin_end(8);

    let btns: Rc<Vec<Button>> = Rc::new(
        (0..FILAS * COLS).map(|_| Button::new()).collect()
    );

    // Poblar grid y aplicar CSS inicial
    poblar_botones(tab_idx, &btns, &grid, &tabs_data, &estado);

    // Conectar botones
    for fila in 0..FILAS {
        for col in 0..COLS {
            let idx = TabBotonera::idx(fila, col);
            let btn = &btns[idx];

            // Clic izquierdo → reproducir/detener
            {
                let td = tabs_data.clone();
                let est = estado.clone();
                let btns_c = btns.clone();
                let grid_c = grid.clone();
                btn.connect_clicked(move |_| {
                    manejar_clic(tab_idx, fila, col, &td, &est, &btns_c, &grid_c);
                });
            }

            // Clic derecho → menú contextual
            {
                let td = tabs_data.clone();
                let est = estado.clone();
                let btns_c = btns.clone();
                let grid_c = grid.clone();
                let btn_c = btn.clone();

                let gesture_right = GestureClick::new();
                gesture_right.set_button(3); // botón derecho
                gesture_right.connect_released(move |_, _, x, y| {
                    let tiene_audio = td.borrow()[tab_idx]
                        .botones[TabBotonera::idx(fila, col)].is_some();
                    mostrar_menu_contextual(
                        &btn_c, x, y,
                        tab_idx, fila, col,
                        tiene_audio,
                        &td, &est, &btns_c, &grid_c,
                    );
                });
                btn.add_controller(gesture_right);
            }

            // Drop → asignar audio
            // DropTarget para STRING (drag interno)
            let drop_str = DropTarget::new(glib::Type::STRING, gdk::DragAction::COPY);
            {
                let td = tabs_data.clone();
                let est = estado.clone();
                let btns_c = btns.clone();
                let grid_c = grid.clone();
                drop_str.connect_drop(move |_, value, _, _| {
                    let raw = value.get::<String>().unwrap_or_default();
                    let ruta = extraer_ruta(&raw);
                    if ruta.is_empty() { return false; }
                    asignar_audio(tab_idx, fila, col, ruta, &td, &est, &btns_c, &grid_c);
                    true
                });
            }
            btn.add_controller(drop_str);

            // DropTarget para text/uri-list (Thunar, Nautilus, etc.)
            // Usamos gtk::gio::File via FileList
            let drop_uri = DropTarget::new(
                gtk::gio::File::static_type(),
                gdk::DragAction::COPY
            );
            {
                let td = tabs_data.clone();
                let est = estado.clone();
                let btns_c = btns.clone();
                let grid_c = grid.clone();
                drop_uri.connect_drop(move |_, value, _, _| {
                    // Intentar como gio::File (un solo archivo)
                    if let Ok(file) = value.get::<gtk::gio::File>() {
                        if let Some(path) = file.path() {
                            let ruta = path.to_string_lossy().to_string();
                            asignar_audio(tab_idx, fila, col, ruta, &td, &est, &btns_c, &grid_c);
                            return true;
                        }
                    }
                    false
                });
            }
            btn.add_controller(drop_uri);
        }
    }

    scroll.set_child(Some(&grid));

    // Etiqueta renombrable de la pestaña
    let nombre = tabs_data.borrow()[tab_idx].nombre.clone();
    let tab_lbl = construir_label_tab(tab_idx, nombre, tabs_data.clone());

    (scroll.upcast::<gtk::Widget>(), tab_lbl)
}

// ─── Label de pestaña renombrable ─────────────────────────────────────────

fn construir_label_tab(
    tab_idx: usize,
    nombre: String,
    tabs_data: Rc<RefCell<Vec<TabBotonera>>>,
) -> gtk::Widget {
    let stack = gtk::Stack::new();
    stack.set_transition_type(gtk::StackTransitionType::None);

    let lbl = Label::new(Some(&nombre));
    lbl.add_css_class("tab-label");
    stack.add_named(&lbl, Some("label"));

    let entry = Entry::new();
    entry.set_width_chars(7);
    entry.set_text(&nombre);
    stack.add_named(&entry, Some("entry"));

    stack.set_visible_child_name("label");

    // Doble clic → editar
    let gesture = GestureClick::new();
    gesture.set_button(1);
    {
        let sc = stack.clone();
        let ec = entry.clone();
        let lc = lbl.clone();
        gesture.connect_released(move |_, n, _, _| {
            if n == 2 {
                ec.set_text(&lc.text());
                sc.set_visible_child_name("entry");
                ec.grab_focus();
            }
        });
    }
    lbl.add_controller(gesture);

    // Confirmar con Enter
    {
        let sc = stack.clone();
        let lc = lbl.clone();
        let td = tabs_data.clone();
        entry.connect_activate(move |e| {
            let nuevo = e.text().to_string();
            lc.set_text(&nuevo);
            td.borrow_mut()[tab_idx].nombre = nuevo;
            storage_botonera::guardar(&td.borrow());
            sc.set_visible_child_name("label");
        });
    }

    // Confirmar al perder foco
    {
        let sc = stack.clone();
        let lc = lbl.clone();
        let td = tabs_data.clone();
        let ec2 = entry.clone();
        let focus_ctrl = gtk::EventControllerFocus::new();
        focus_ctrl.connect_leave(move |_| {
            let nuevo = ec2.text().to_string();
            lc.set_text(&nuevo);
            td.borrow_mut()[tab_idx].nombre = nuevo;
            storage_botonera::guardar(&td.borrow());
            sc.set_visible_child_name("label");
        });
        entry.add_controller(focus_ctrl);
    }

    stack.upcast::<gtk::Widget>()
}

// ─── Poblar botones del grid ───────────────────────────────────────────────

fn poblar_botones(
    tab_idx: usize,
    btns: &Rc<Vec<Button>>,
    grid: &Grid,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
) {
    let td = tabs_data.borrow();
    let est = estado.borrow();

    for fila in 0..FILAS {
        for col in 0..COLS {
            let idx = TabBotonera::idx(fila, col);
            let btn = &btns[idx];

            // Asegurar que está en el grid
            if btn.parent().is_none() {
                grid.attach(btn, col as i32, fila as i32, 1, 1);
            }

            btn.remove_css_class("fx-btn");
            btn.remove_css_class("fx-vacio");
            btn.remove_css_class("fx-play");

            let reproduciendo = est.reproduciendo.as_ref()
                .map(|r| r.tab == tab_idx && r.fila == fila && r.col == col)
                .unwrap_or(false);

            match &td[tab_idx].botones[idx] {
                None => {
                    btn.set_label("");
                    btn.set_widget_name("");
                    btn.add_css_class("fx-vacio");
                }
                Some(boton) => {
                    let wname = format!("fx{}{}{}", tab_idx, fila, col);
                    btn.set_widget_name(&wname);
                    btn.set_label(&boton.nombre);
                    if reproduciendo {
                        btn.add_css_class("fx-play");
                    } else {
                        btn.add_css_class("fx-btn");
                    }
                }
            }
        }
    }
    drop(td);
    drop(est);
    actualizar_css_colores(tab_idx, tabs_data, estado);
}

// ─── CSS dinámico de colores ───────────────────────────────────────────────

fn actualizar_css_colores(
    tab_idx: usize,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
) {
    let td = tabs_data.borrow();
    let est = estado.borrow();
    let mut css = String::new();

    for fila in 0..FILAS {
        for col in 0..COLS {
            let idx = TabBotonera::idx(fila, col);
            if td[tab_idx].botones[idx].is_none() { continue; }

            let color = PALETA[idx % PALETA.len()];
            let texto = color_texto(color);
            let wname = format!("fx{}{}{}", tab_idx, fila, col);
            let reproduciendo = est.reproduciendo.as_ref()
                .map(|r| r.tab == tab_idx && r.fila == fila && r.col == col)
                .unwrap_or(false);

            if reproduciendo {
                css.push_str(&format!(
                    "#{} {{ background:{}; color:{}; }}\n",
                    wname, color, texto
                ));
            } else {
                css.push_str(&format!(
                    "#{} {{ background:{}; color:{}; }}\n",
                    wname, color, texto
                ));
            }
        }
    }

    est.css_provider.load_from_data(&css);
}

// ─── Reproducción ─────────────────────────────────────────────────────────

fn manejar_clic(
    tab_idx: usize, fila: usize, col: usize,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
    btns: &Rc<Vec<Button>>,
    grid: &Grid,
) {
    let idx = TabBotonera::idx(fila, col);
    let tiene_audio = tabs_data.borrow()[tab_idx].botones[idx].is_some();
    if !tiene_audio { return; }

    let ya_reproduciendo = estado.borrow().reproduciendo
        .as_ref()
        .map(|r| r.tab == tab_idx && r.fila == fila && r.col == col)
        .unwrap_or(false);

    if ya_reproduciendo {
        detener_reproduccion(estado);
    } else {
        detener_reproduccion(estado);
        let ruta = tabs_data.borrow()[tab_idx].botones[idx]
            .as_ref().map(|b| b.ruta.clone());
        if let Some(ruta) = ruta {
            estado.borrow_mut().reproduciendo = Some(Reproduciendo { tab: tab_idx, fila, col });
            let uri = path_to_uri(&ruta);
            if let Ok(elem) = gst::ElementFactory::make("playbin")
                .property("uri", &uri)
                .build()
            {
                if let Ok(pipeline) = elem.downcast::<gst::Pipeline>() {
                    // Bus watch: reset toggle cuando termina el audio
                    if let Some(bus) = pipeline.bus() {
                        let estado_w = estado.clone();
                        let tabs_w = tabs_data.clone();
                        let btns_w = btns.clone();
                        let grid_w = grid.clone();
                        let watch = bus.add_watch_local(move |_, msg| {
                            match msg.view() {
                                gst::MessageView::Eos(_) | gst::MessageView::Error(_) => {
                                    if let Some(p) = estado_w.borrow_mut().pipeline.take() {
                                        let _ = p.set_state(gst::State::Null);
                                    }
                                    estado_w.borrow_mut().reproduciendo = None;
                                    poblar_botones(tab_idx, &btns_w, &grid_w, &tabs_w, &estado_w);
                                    return glib::ControlFlow::Break;
                                }
                                _ => {}
                            }
                            glib::ControlFlow::Continue
                        });
                        if let Ok(guard) = watch { std::mem::forget(guard); }
                    }
                    let _ = pipeline.set_state(gst::State::Playing);
                    estado.borrow_mut().pipeline = Some(pipeline);
                }
            }
        }
    }
    poblar_botones(tab_idx, btns, grid, tabs_data, estado);
}

fn detener_reproduccion(estado: &Rc<RefCell<Estado>>) {
    if estado.borrow().reproduciendo.is_some() {
        estado.borrow_mut().reproduciendo = None;
        if let Some(p) = estado.borrow_mut().pipeline.take() {
            let _ = p.set_state(gst::State::Null);
        }
    }
}

// ─── Menú contextual (botón derecho) ──────────────────────────────────────

fn mostrar_menu_contextual(
    btn: &Button,
    x: f64, y: f64,
    tab_idx: usize, fila: usize, col: usize,
    tiene_audio: bool,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
    btns: &Rc<Vec<Button>>,
    grid: &Grid,
) {
    let menu = gtk::gio::Menu::new();
    menu.append(Some(crate::i18n::tr("boto.menu_asignar")), Some("btn.asignar"));
    if tiene_audio {
        menu.append(Some(crate::i18n::tr("boto.menu_quitar")), Some("btn.quitar"));
    }

    let popover = PopoverMenu::from_model(Some(&menu));
    popover.set_parent(btn);
    popover.set_has_arrow(false);
    let rect = gtk::gdk::Rectangle::new(x as i32, y as i32, 1, 1);
    popover.set_pointing_to(Some(&rect));

    let action_group = gtk::gio::SimpleActionGroup::new();

    // ── Acción: Asignar audio ──
    let asignar_action = gtk::gio::SimpleAction::new("asignar", None);
    {
        let td = tabs_data.clone();
        let est = estado.clone();
        let btns_c = btns.clone();
        let grid_c = grid.clone();
        let btn_c = btn.clone();
        let popover_c = popover.clone();
        asignar_action.connect_activate(move |_, _| {
            popover_c.popdown();
            // Obtener la ventana raíz del botón
            let ventana = btn_c.root()
                .and_then(|r| r.downcast::<gtk::Window>().ok());
            abrir_selector_audio(
                ventana.as_ref(),
                tab_idx, fila, col,
                &td, &est, &btns_c, &grid_c,
            );
        });
    }
    action_group.add_action(&asignar_action);

    // ── Acción: Quitar audio ──
    if tiene_audio {
        let quitar_action = gtk::gio::SimpleAction::new("quitar", None);
        let td = tabs_data.clone();
        let est = estado.clone();
        let btns_c = btns.clone();
        let grid_c = grid.clone();
        let popover_c = popover.clone();
        quitar_action.connect_activate(move |_, _| {
            {
                let mut td_mut = td.borrow_mut();
                td_mut[tab_idx].botones[TabBotonera::idx(fila, col)] = None;
                storage_botonera::guardar(&td_mut);
            }
            if estado_reproduce(&est, tab_idx, fila, col) {
                est.borrow_mut().reproduciendo = None;
                if let Some(p) = est.borrow_mut().pipeline.take() {
                    let _ = p.set_state(gst::State::Null);
                }
            }
            poblar_botones(tab_idx, &btns_c, &grid_c, &td, &est);
            popover_c.popdown();
        });
        action_group.add_action(&quitar_action);
    }

    btn.insert_action_group("btn", Some(&action_group));
    popover.popup();
}

// ─── Selector de archivo ───────────────────────────────────────────────────

fn abrir_selector_audio(
    ventana: Option<&gtk::Window>,
    tab_idx: usize, fila: usize, col: usize,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
    btns: &Rc<Vec<Button>>,
    grid: &Grid,
) {
    let dialog = gtk::FileChooserDialog::new(
        Some(crate::i18n::tr("boto.sel_audio")),
        ventana,
        gtk::FileChooserAction::Open,
        &[
            (crate::i18n::tr("btn.cancel"), gtk::ResponseType::Cancel),
            (crate::i18n::tr("btn.open"),   gtk::ResponseType::Accept),
        ],
    );
    dialog.set_modal(true);

    // Filtro de audio
    let filtro = gtk::FileFilter::new();
    filtro.set_name(Some(crate::i18n::tr("boto.filtro_audio")));
    for ext in &["*.mp3", "*.wav", "*.ogg", "*.flac", "*.aac", "*.m4a", "*.gradio"] {
        filtro.add_pattern(ext);
    }
    filtro.add_mime_type("audio/*");
    dialog.add_filter(&filtro);

    let filtro_todo = gtk::FileFilter::new();
    filtro_todo.set_name(Some(crate::i18n::tr("boto.filtro_todos")));
    filtro_todo.add_pattern("*");
    dialog.add_filter(&filtro_todo);

    // Abrir en el directorio de inicio de G Radio si existe
    // Abrir en $HOME sin restricciones
    let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
    let _ = dialog.set_current_folder(Some(
        &gtk::gio::File::for_path(&home)
    ));

    let td = tabs_data.clone();
    let est = estado.clone();
    let btns_c = btns.clone();
    let grid_c = grid.clone();

    dialog.connect_response(move |d, resp| {
        if resp == gtk::ResponseType::Accept {
            if let Some(file) = d.file() {
                if let Some(path) = file.path() {
                    let ruta = path.to_string_lossy().to_string();
                    asignar_audio(tab_idx, fila, col, ruta, &td, &est, &btns_c, &grid_c);
                }
            }
        }
        d.close();
    });

    dialog.present();
}

fn estado_reproduce(estado: &Rc<RefCell<Estado>>, tab: usize, fila: usize, col: usize) -> bool {
    estado.borrow().reproduciendo
        .as_ref()
        .map(|r| r.tab == tab && r.fila == fila && r.col == col)
        .unwrap_or(false)
}


// ─── Helpers de drop ──────────────────────────────────────────────────────

/// Extrae la primera ruta válida de un string (puede ser uri-list o ruta directa)
fn extraer_ruta(raw: &str) -> String {
    raw.lines()
        .map(|l| l.trim())
        .filter(|l| !l.starts_with('#') && !l.is_empty())
        .map(|l| {
            if l.starts_with("file://") {
                uri_decode(&l[7..])
            } else {
                l.trim_end_matches('\r').to_string()
            }
        })
        .filter(|r| !r.is_empty())
        .next()
        .unwrap_or_default()
}

fn asignar_audio(
    tab_idx: usize, fila: usize, col: usize,
    ruta: String,
    tabs_data: &Rc<RefCell<Vec<TabBotonera>>>,
    estado: &Rc<RefCell<Estado>>,
    btns: &Rc<Vec<Button>>,
    grid: &Grid,
) {
    let nombre = nombre_desde_ruta(&ruta);
    {
        let mut td_mut = tabs_data.borrow_mut();
        td_mut[tab_idx].botones[TabBotonera::idx(fila, col)] =
            Some(Boton { ruta, nombre });
        storage_botonera::guardar(&td_mut);
    }
    poblar_botones(tab_idx, btns, grid, tabs_data, estado);
}


// ─── Utilidades ────────────────────────────────────────────────────────────

/// Decodifica %XX de una URI path (ej: %20 → espacio)
fn uri_decode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'%' && i + 2 < bytes.len() {
            if let (Ok(h), Ok(l)) = (
                std::str::from_utf8(&bytes[i+1..i+2]),
                std::str::from_utf8(&bytes[i+2..i+3]),
            ) {
                if let Ok(byte) = u8::from_str_radix(&format!("{}{}", h, l), 16) {
                    out.push(byte as char);
                    i += 3;
                    continue;
                }
            }
        }
        out.push(bytes[i] as char);
        i += 1;
    }
    // Quitar \r al final si viene de uri-list
    out.trim_end_matches('\r').to_string()
}
