use std::os::unix::net::UnixStream;

use reis::eis;
use smithay::reexports::reis;

use smithay::backend::libei::{EiInput, EiInputEvent, EiRegion};
use smithay::input::keyboard::Keysym;
use smithay::reexports::calloop;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::text_input::TextInputSeat;

use crate::config::xkb_config_to_wl;
use crate::input::InputBackendId;
use crate::state::{BackendData, State};
use crate::utils::{geometry::RectGlobalExt, prelude::OutputExt};

// Requested device types for an EI connection, mirroring the XDG RemoteDesktop portal `DeviceType` bitmask
const DEVICE_TYPE_KEYBOARD: u32 = 1;
const DEVICE_TYPE_POINTER: u32 = 2;
const DEVICE_TYPE_TOUCHSCREEN: u32 = 4;

// Name of the EI absolute-pointer device. Shared so the connect path and the
// re-advertise-on-output-change path recreate the same device.
const ABSOLUTE_POINTER_NAME: &str = "virtual absolute pointer";

pub type EiRequest = (UnixStream, u32);

/// Build the regions advertised on the EI absolute devices (absolute pointer and touch) for the
/// output layout: one region per output, each at its **global logical** offset with
/// its logical size and scale.
pub fn absolute_regions(state: &State) -> Vec<EiRegion> {
    let shell = state.common.shell.read();
    shell
        .outputs()
        .map(|output| {
            let scale = output.current_scale().fractional_scale();
            EiRegion {
                // Keep the signed logical rect
                // the u32 clamp happens at the `ei_device.region` in smithay.
                rect: output.geometry().as_logical(),
                scale: scale as f32,
                // Tie the region to its output so a client can correlate it with the
                // matching screencast stream (the portal advertises the same name).
                mapping_id: Some(output.name()),
            }
        })
        .collect()
}

/// Re-advertise the coordinate regions on every active EI seat's absolute devices.
pub fn refresh_absolute_pointer_regions(state: &State) {
    if state.common.ei_seats.is_empty() {
        return;
    }
    let regions = absolute_regions(state);
    for seat in state.common.ei_seats.values() {
        seat.update_regions(&regions);
    }
}

pub fn setup_ei(
    handle: &calloop::LoopHandle<'static, State>,
) -> calloop::channel::Sender<EiRequest> {
    let (sender, channel) = calloop::channel::channel::<EiRequest>();
    let handle_clone = handle.clone();
    handle
        .insert_source(channel, move |event, _, _| {
            let calloop::channel::Event::Msg((stream, device_types)) = event else {
                return;
            };
            let context = match eis::Context::new(stream) {
                Ok(context) => context,
                Err(err) => {
                    tracing::error!("Failed to create EI context: {}", err);
                    return;
                }
            };
            let source = EiInput::new(context);
            if let Err(err) =
                handle_clone.insert_source(source, move |event, connection, data| match event {
                    EiInputEvent::Connected => {
                        let conn = connection.eis_connection().clone();
                        let seat = connection.add_seat("default");
                        let wants_keyboard = device_types & DEVICE_TYPE_KEYBOARD != 0;
                        if wants_keyboard {
                            let conf = data.common.config.xkb_config();
                            // The ei_keyboard device is given the compositor keymap; its key
                            // events feed the shared seat like any other keyboard.
                            let _ = seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&conf));
                            // The text device lets clients inject keysyms/utf8 directly, delivered
                            seat.add_text("virtual text");
                        }
                        if device_types & DEVICE_TYPE_POINTER != 0 {
                            seat.add_pointer("virtual pointer");
                            let regions = absolute_regions(data);
                            seat.add_pointer_absolute(ABSOLUTE_POINTER_NAME, &regions);
                        }
                        if device_types & DEVICE_TYPE_TOUCHSCREEN != 0 {
                            let regions = absolute_regions(data);
                            seat.add_touch("virtual touch", &regions);
                        }
                        // Kb-capable connections get a shared-seat source so their `ei_keyboard`
                        // key events feed the seat keyboard tracking.
                        if wants_keyboard {
                            data.common.ei_keyboard_source.insert(
                                conn.clone(),
                                smithay::input::keyboard::KeyboardSource::new_auxiliary(),
                            );
                        }
                        // Track the seat for every connection
                        data.common.ei_seats.insert(conn, seat);
                        data.update_ei_input_method();
                    }
                    EiInputEvent::Disconnected => {
                        let conn = connection.eis_connection().clone();
                        let backend_id = InputBackendId::Ei(conn.clone());
                        // Release any keys/modifiers and pointer buttons this remote still holds
                        data.release_ei_keyboard(&conn);
                        data.release_ei_pointer(&conn);
                        data.clear_input_source_state(&backend_id);
                        data.common.ei_seats.remove(&conn);
                        data.common.ei_keyboard_source.remove(&conn);
                        data.common.ei_pointer_buttons.remove(&conn);
                        data.update_ei_input_method();
                        // Notify the remaining libei clients of the now-cleared modifier state
                        let seat = data.common.shell.read().seats.last_active().clone();
                        data.broadcast_ei_keyboard_modifiers(&seat);
                    }
                    EiInputEvent::Event(event) => {
                        use smithay::backend::input::{InputEvent, KeyboardKeyEvent};
                        match event {
                            InputEvent::Keyboard { event } => {
                                data.inject_ei_key(
                                    connection.eis_connection(),
                                    event.key_code(),
                                    event.state(),
                                );
                            }
                            other => {
                                let backend_id =
                                    InputBackendId::Ei(connection.eis_connection().clone());
                                data.process_input_event(other, backend_id);
                                if matches!(data.backend, BackendData::Kms(_)) {
                                    for output in data.common.shell.read().outputs() {
                                        data.backend.kms().schedule_render(output);
                                    }
                                }
                            }
                        }
                    }
                    EiInputEvent::TextKeysym { keysym, state } => {
                        data.inject_ei_text_keysym(connection.eis_connection(), keysym, state);
                    }
                    EiInputEvent::TextUtf8 { text } => {
                        data.inject_ei_text(&text);
                    }
                })
            {
                tracing::error!("Failed to insert EI input source: {}", err);
            }
        })
        .expect("Failed to insert EI channel source into the event loop");

    sender
}

impl State {
    /// Act as the input method for text injection while any text-capable EI connection is
    /// active, so `ei_text` UTF-8 can be committed into the focused app even without a real
    /// IME, but only when none is bound (a real IME always wins)
    pub(crate) fn update_ei_input_method(&mut self) {
        let active = !self.common.ei_keyboard_source.is_empty();
        let seats = self
            .common
            .shell
            .read()
            .seats
            .iter()
            .cloned()
            .collect::<Vec<_>>();
        for seat in seats {
            let has_ime = seat.input_method().has_instance();
            let text_input = seat.text_input();
            if active {
                if !has_ime {
                    text_input.set_compositor_input_method(true);
                }
            } else {
                text_input.set_compositor_input_method(false);
            }
        }
    }

    /// Inject UTF-8 text (from an EI `ei_text` device) into the focused client.
    pub fn inject_ei_text(&mut self, text: &str) {
        let seat = self.common.shell.read().seats.last_active().clone();
        // Only commit through text-input when we're the active input method (no real IME)
        if !seat.input_method().has_instance() {
            let text_input = seat.text_input();
            let mut injected = false;
            text_input.with_active_text_input(|ti, _surface| {
                ti.commit_string(Some(text.to_owned()));
                injected = true;
            });
            if injected {
                text_input.done(false);
                return;
            }
        }

        // Bind the whole chunk to spare keycodes in one temporary keymap per batch (delivered
        // to just the focused client), so we change the keymap ~once per chunk instead of once
        // per character. Leaves the seat's own keyboard state untouched.
        let keysyms: Vec<Keysym> = text
            .chars()
            .map(Keysym::from_char)
            .filter(|keysym| keysym.raw() != 0)
            .collect();
        let Some(keyboard) = seat.get_keyboard() else {
            return;
        };
        // At most ~247 keysyms fit one spare keymap (keycodes 9..=255); leave margin.
        const BATCH: usize = 240;
        for batch in keysyms.chunks(BATCH) {
            keyboard.inject_text_keysyms(self, batch);
        }
    }
}
