//! On-screen keyboard and physical-keyboard presence on `Forge`: deciding when
//! the OSK applies, syncing squeekboard's visibility to text-input focus, and
//! the keyboard-layout cycle. Operates on `Forge`.

use super::*;

impl Forge {
    /// OSK applies on mobile (incl. forced `FORGE_MOBILE`) or with no keyboard.
    fn osk_enabled(&self) -> bool {
        self.windows.canvas().is_mobile() || !self.session.has_keyboard
    }

    /// A real keyboard has >40 keys; power/lid/volume report EV_KEY with few.
    pub(super) fn has_physical_keyboard() -> bool {
        let Ok(entries) = std::fs::read_dir("/sys/class/input") else {
            return true;
        };
        for entry in entries.flatten() {
            if !entry.file_name().to_string_lossy().starts_with("event") {
                continue;
            }
            let path = entry.path().join("device/capabilities/key");
            let Ok(content) = std::fs::read_to_string(&path) else {
                continue;
            };
            let keys: u32 = content
                .split_whitespace()
                .filter_map(|w| u64::from_str_radix(w, 16).ok())
                .map(u64::count_ones)
                .sum();
            if keys > 40 {
                return true;
            }
        }
        false
    }

    /// Re-evaluate keyboard presence after a hotplug and reconcile the OSK.
    pub fn refresh_keyboard_presence(&mut self) {
        if !self.init_done {
            return;
        }
        let had = self.session.has_keyboard;
        self.session.has_keyboard = Self::has_physical_keyboard();
        if self.session.has_keyboard == had {
            return;
        }
        if self.osk_enabled() {
            self.ensure_greeter_osk();
        } else {
            self.session.last_ti_active = false;
            self.session.osk_visible = false;
            Self::set_osk_visible(false);
        }
    }

    /// Mirror squeekboard's visibility to whether any client has an active
    /// text-input, so the OSK follows focus for any text-input-v3 app.
    pub(super) fn sync_osk(&mut self) {
        if !self.osk_enabled() {
            return;
        }
        // The app switcher — even while still being formed by the swipe —
        // takes over the screen, so keep the OSK down regardless of the app's
        // text-input state. If the swipe cancels back to the app, the next
        // sync brings it up again from the (still active) text-input.
        let active = if self.shell.app_switcher.is_active() {
            false
        } else {
            let mut active = false;
            self.seat.text_input().with_active_text_input(|_, _| active = true);
            active
        };
        if active != self.session.last_ti_active {
            self.session.last_ti_active = active;
            self.session.osk_visible = active;
            Self::set_osk_visible(active);
        }
    }

    /// Force the on-screen keyboard down now (app switcher opened, went home),
    /// keeping `sync_osk`'s bookkeeping consistent so it re-shows correctly
    /// once a text field is focused again.
    pub(crate) fn hide_osk(&mut self) {
        if !self.session.osk_visible && !self.session.last_ti_active {
            return;
        }
        self.session.last_ti_active = false;
        self.session.osk_visible = false;
        Self::set_osk_visible(false);
    }

    pub fn cycle_keyboard_layout(&mut self) {
        if self.input.xkb_layouts.len() <= 1 {
            return;
        }
        self.input.current_layout = (self.input.current_layout + 1) % self.input.xkb_layouts.len();
        let layout = self.input.xkb_layouts[self.input.current_layout].clone();
        self.set_xkb_config(XkbConfig {
            layout: &layout,
            ..Default::default()
        });
        self.windows.set_dirty();
    }

    /// Start squeekboard, but only in the greeter — the one bare session
    /// forge owns that has no shell to start it. The desktop (crustace's
    /// session-init) and the installer kiosk start their own, and forge
    /// only drives visibility there; a second instance would fight the
    /// session's over sm.puri.OSK0 and the OSK would flicker. A pgrep
    /// guard keeps a re-evaluation from stacking a duplicate.
    pub(super) fn ensure_greeter_osk(&self) {
        if !self.session.greeter_mode || !self.osk_enabled() {
            return;
        }
        let running = std::process::Command::new("pgrep")
            .arg("-x")
            .arg("squeekboard")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false);
        if running {
            return;
        }
        let mut cmd = std::process::Command::new("squeekboard");
        cmd.stdin(std::process::Stdio::null());
        crate::spawn::detached(cmd);
    }

    pub(super) fn set_osk_visible(visible: bool) {
        std::thread::spawn(move || {
            let conn = match zbus::blocking::Connection::session() {
                Ok(c) => c,
                Err(e) => {
                    tracing::warn!("OSK D-Bus connect failed: {e}");
                    return;
                }
            };
            if let Err(e) = conn.call_method(
                Some("sm.puri.OSK0"),
                "/sm/puri/OSK0",
                Some("sm.puri.OSK0"),
                "SetVisible",
                &(visible,),
            ) {
                tracing::warn!("OSK SetVisible({visible}) failed: {e}");
            }
        });
    }

}
