//! Swipe and gesture handling on `Forge`: the top-bar and bottom-edge swipes,
//! the app-switcher dwell, active-corner actions and user gesture dispatch.
//! Operates on `Forge`.

use super::*;

impl Forge {
    pub(super) fn emit_gesture( event: &forge_ipc::gesture::GestureEvent )
    {
        if let Ok( json ) = serde_json::to_string( event )
        {
            Self::send_to_crustace( &json );
        }
    }

    pub(super) fn handle_gesture_action( &mut self, ev: &forge_ipc::gesture::GestureEvent )
    {
        use forge_ipc::gesture::*;

        if ev.phase != GesturePhase::End { return; }

        match ( ev.gesture_type, ev.edge, ev.direction )
        {
            // Edge back: left edge swipe right → send Back key to focused app
            ( GestureType::EdgePull | GestureType::Swipe, Edge::Left, Direction::Right ) =>
            {
                self.send_virtual_key(
                    smithay::input::keyboard::keysyms::KEY_XF86Back,
                    forge_ipc::Modifiers::default(),
                );
                info!( "Gesture: edge back" );
            }

            // Bottom edge swipe up is owned by the progressive `BottomSwipe`
            // state machine (launcher / go-home / app switcher), which
            // consumes the motion before the recogniser sees it.

            // Top edge swipe down → quick settings (crustace handles the UI)
            ( GestureType::EdgePull | GestureType::Swipe, Edge::Top, Direction::Down ) =>
            {
                info!( "Gesture: top edge pull (crustace handles)" );
            }

            // Right edge swipe left → forward navigation (if applicable)
            ( GestureType::EdgePull | GestureType::Swipe, Edge::Right, Direction::Left ) =>
            {
                info!( "Gesture: right edge swipe (reserved)" );
            }

            _ => {}
        }
    }

    /// Dispatch an active-corner action.
    pub(super) fn trigger_corner_action(
        &mut self,
        corner: crate::corner_action::Corner,
        action: crate::corner_action::CornerAction,
    ) {
        use crate::corner_action::CornerAction::*;
        // A locked session swallows the active corners: the launcher,
        // homescreen and app switcher must not surface over the lock screen.
        // A kiosk session swallows them too — it owns the whole screen and has
        // nothing to switch to.
        if self.windows.is_locked() || self.session.kiosk {
            return;
        }
        tracing::info!("Active corner triggered: {:?} → {:?}", corner, action);
        match action {
            Overview      => {
                Self::send_to_crustace(r#"{"action":"toggle_overview"}"#);
            },
            ShowDesktop   => {
                Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
            },
            Lock          => {
                // Close crustace overlays first so they don't sit on top of
                // the lock screen, then lock the compositor.
                Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
                self.lock_session();
                self.request_redraw();
            }
            AppSwitcher   => {
                // In-process: open the Alt+Tab overlay without advancing the
                // selection (step=0). The gesture is a "show me what's open"
                // signal — the user picks the target with the pointer or
                // with Tab afterwards; advancing on the gesture itself would
                // mean a flick of the cursor reorders the focus chain.
                //
                // If the switcher is already visible (re-trigger before the
                // user picked anything), refresh the entries snapshot so the
                // panel reflects any windows opened/closed in the interim —
                // a no-op would leave it stale and make repeat triggers
                // look like "sometimes works, sometimes doesn't" from the
                // user's perspective.
                let entries = self.windows.app_switcher_entries();
                tracing::info!(
                    "corner→AppSwitcher: {} entries, app_switcher.visible={} (will {})",
                    entries.len(),
                    self.shell.app_switcher.is_active(),
                    if self.shell.app_switcher.is_active() { "refresh" } else { "open" },
                );
                let focused = self.windows.focused_object_id();
                if !entries.is_empty() {
                    self.shell.app_switcher.begin(entries, 0, focused);
                    self.hide_osk();
                    self.request_redraw();
                } else {
                    // Nothing to switch to — the only useful action is
                    // launching something, so surface the app menu instead.
                    Self::send_to_crustace(r#"{"action":"toggle_launcher"}"#);
                }
            }
            Custom(_id)   => (),
        }
    }

    /// Try launching a program from a list of candidates (first success wins).
    /// Send a JSON message to crustace's IPC socket.
    /// Feed a motion event to the topbar-swipe state machine and emit
    /// any IPC messages it produces. Returns true if the swipe consumed
    /// the event (caller should skip normal input handling).
    pub(super) fn update_topbar_swipe(&mut self, pos: Point<f64, Logical>) -> bool {
        use crate::forge::TopbarSwipeMsg;

        let Some(swipe) = self.input.topbar_swipe.as_mut() else { return false };
        let last = swipe.last_sent_progress;
        let (consumed, start, wiping) = swipe.on_motion(pos.y);
        let new_last = swipe.last_sent_progress;

        if matches!(start, Some(TopbarSwipeMsg::Start)) {
            let dy = pos.y - swipe.start_y;
            tracing::info!("Topbar swipe: activated (dy={:.1})", dy);
            Self::send_to_crustace(r#"{"action":"start_wipe_down"}"#);
        }

        if let Some(TopbarSwipeMsg::Wiping(progress)) = wiping {
            let msg = format!(r#"{{"action":"wiping","progress":{:.3}}}"#, progress);
            Self::send_to_crustace(&msg);
            let step_now  = (new_last * 10.0).floor() as i32;
            let step_last = (last.max(0.0) * 10.0).floor() as i32;
            if step_now != step_last {
                tracing::info!("Topbar swipe: wiping progress={:.2}", progress);
            }
        }

        consumed
    }

    /// End a topbar swipe, emitting the wipe_done IPC if the swipe had
    /// become active. Returns true if the caller should skip the normal
    /// up-path.
    pub(super) fn end_topbar_swipe(&mut self, pos: Point<f64, Logical>) -> bool {
        use crate::forge::TopbarSwipeMsg;

        let Some(swipe) = self.input.topbar_swipe.take() else { return false };
        let Some(done) = swipe.on_release(pos.y) else { return false };
        if let TopbarSwipeMsg::Done { progress, committed } = done {
            tracing::info!("Topbar swipe: done (progress={:.2}, committed={})", progress, committed);
            let msg = format!(
                r#"{{"action":"wipe_done","committed":{},"progress":{:.3}}}"#,
                committed, progress,
            );
            Self::send_to_crustace(&msg);
        }
        true
    }

    /// Escalate a pending bottom press into an active swipe-up, then drive
    /// either the launcher reveal (crustace) or the app switcher's
    /// finger-following entry from the swipe progress. Returns true once the
    /// gesture owns the event so normal input handling is skipped.
    pub(super) fn update_bottom_swipe(&mut self, pos: Point<f64, Logical>) -> bool {
        use crate::forge::SwipeContext;

        let (consumed, activated, wiping) = {
            let Some(swipe) = self.input.bottom_swipe.as_mut() else { return false };
            swipe.on_motion(pos.x, pos.y)
        };
        if !consumed {
            return false;
        }

        if activated {
            // The press may already have reached a client surface; cancel it
            // so the app sees no dangling touch while forge drives the slide.
            self.input.touch_state.input_surface = None;
            if let Some(touch) = self.seat.get_touch() {
                touch.cancel(self);
            }

            // A crustace panel (Quick Settings, calendar, picker…) is open:
            // this swipe-up dismisses it rather than revealing the launcher
            // or shrinking an app. Resolve inertly so the release is a no-op.
            if self.windows.has_dismissable_panel() {
                Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
                if let Some(s) = self.input.bottom_swipe.as_mut() { s.resolved = true; }
                return true;
            }

            // Launcher only on the homescreen and only when the swipe starts
            // at the dock's top edge or higher; the very bottom (and an empty
            // homescreen with nothing to switch to) forms the switcher.
            let home = self.windows.homescreen_active();
            let canvas_h = self.windows.canvas().size().h as f64;
            let dock_h = self.shell.app_switcher.dock_height() as f64;
            let start_y = self.input.bottom_swipe.as_ref().map_or(pos.y, |s| s.start_y);
            let switcher_zone = if dock_h > 0.0 {
                start_y > canvas_h - dock_h
            } else {
                start_y >= canvas_h - BOTTOM_ARM_BAND
            };
            let ctx = if home && (!switcher_zone || !self.windows.has_windows()) {
                SwipeContext::Launcher
            } else {
                SwipeContext::Switcher
            };
            let over_app = !home;
            if let Some(s) = self.input.bottom_swipe.as_mut() {
                s.context = Some(ctx);
                s.over_app = over_app;
            }
            match ctx {
                SwipeContext::Launcher => {
                    Self::send_to_crustace(r#"{"action":"start_launcher_wipe"}"#);
                },
                SwipeContext::Switcher => {
                    let entries = self.windows.app_switcher_entries();
                    let focused = self.windows.focused_object_id();
                    self.shell.app_switcher.begin_swipe(entries, focused);
                },
            }
        }

        if self.input.bottom_swipe.as_ref().is_some_and(|s| s.resolved) {
            return true;
        }

        match self.input.bottom_swipe.as_ref().and_then(|s| s.context) {
            Some(SwipeContext::Launcher) => {
                if let Some(progress) = wiping {
                    let msg = format!(
                        r#"{{"action":"launcher_wiping","progress":{:.3}}}"#,
                        progress,
                    );
                    Self::send_to_crustace(&msg);
                }
            },
            Some(SwipeContext::Switcher) => {
                let progress = self.input.bottom_swipe.as_ref().map_or(0.0, |s| s.progress(pos.y));
                self.shell.app_switcher.set_swipe_progress(progress);
                self.request_redraw();
            },
            None => {}
        }

        self.arm_dwell();
        true
    }

    /// (Re)arm or cancel the dwell-to-switcher timer. The countdown is only
    /// restarted when the finger drifts past [`DWELL_TOLERANCE`] from where it
    /// began, so it need not be perfectly still — only roughly settled past
    /// [`BottomSwipe::DWELL_ARM`] — to fire and commit.
    fn arm_dwell(&mut self) {
        let Some(swipe) = self.input.bottom_swipe.as_ref() else { return };
        if swipe.resolved { return; }
        let progress = swipe.last_sent_progress;

        if progress < crate::forge::BottomSwipe::DWELL_ARM {
            if let Some(token) = self.input.dwell_timer.take() {
                self.event_loop.remove(token);
            }
            self.input.dwell_anchor = None;
            return;
        }

        let drifted = self
            .input
            .dwell_anchor
            .map_or(true, |a| (progress - a).abs() > DWELL_TOLERANCE);
        if self.input.dwell_timer.is_none() || drifted {
            if let Some(token) = self.input.dwell_timer.take() {
                self.event_loop.remove(token);
            }
            self.input.dwell_anchor = Some(progress);
            let timer = Timer::from_duration(std::time::Duration::from_millis(DWELL_MS));
            let token = self
                .event_loop
                .insert_source(timer, |_, _, forge| {
                    forge.on_dwell_fired();
                    TimeoutAction::Drop
                })
                .expect("dwell timer");
            self.input.dwell_timer = Some(token);
        }
    }

    /// Dwell elapsed with the finger still down past the arm fraction:
    /// commit the app switcher. Only the switcher context dwells — a
    /// launcher swipe settles on release.
    fn on_dwell_fired(&mut self) {
        self.input.dwell_timer = None;
        self.input.dwell_anchor = None;
        let Some(swipe) = self.input.bottom_swipe.as_mut() else { return };
        if !swipe.active || swipe.resolved { return; }
        if swipe.last_sent_progress < crate::forge::BottomSwipe::DWELL_ARM { return; }
        if !matches!(swipe.context, Some(crate::forge::SwipeContext::Switcher)) { return; }
        swipe.resolved = true;

        self.open_swipe_switcher();
        // The switcher now owns the touch (carousel drag); drop the swipe so
        // a later release doesn't double-handle it — on mobile the carousel
        // up-path returns before end_bottom_swipe.
        self.input.bottom_swipe = None;
        self.unstall();
        self.request_redraw();
    }

    /// Freeze the swipe-driven app switcher fully open and interactive.
    fn open_swipe_switcher(&mut self) {
        self.shell.app_switcher.commit_swipe();
        self.hide_osk();
        Self::send_to_crustace(r#"{"action":"hide_homescreen"}"#);
    }

    /// Close the switcher and fall back to whatever is underneath: the
    /// foreground app, or — when every window is minimised — the crustace
    /// homescreen, which was hidden when the switcher opened.
    pub(super) fn dismiss_app_switcher(&mut self) {
        self.shell.app_switcher.cancel();
        if self.windows.homescreen_active() {
            Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
        }
    }

    /// End a bottom swipe-up: settle the launcher, or resolve the switcher
    /// to go-home / commit / cancel. Returns true if the caller should skip
    /// the normal up-path.
    pub(super) fn end_bottom_swipe(&mut self, pos: Point<f64, Logical>) -> bool {
        use crate::forge::{ BottomSwipe, SwipeContext };

        let Some(swipe) = self.input.bottom_swipe.take() else { return false };
        if let Some(token) = self.input.dwell_timer.take() {
            self.event_loop.remove(token);
        }
        self.input.dwell_anchor = None;
        if !swipe.active {
            return false;
        }
        if swipe.resolved {
            return true;
        }

        let progress = swipe.progress(pos.y);
        match swipe.context {
            Some(SwipeContext::Launcher) => {
                let committed = progress >= BottomSwipe::LAUNCHER_COMMIT;
                let msg = format!(
                    r#"{{"action":"launcher_wipe_done","committed":{},"progress":{:.3}}}"#,
                    committed, progress,
                );
                Self::send_to_crustace(&msg);
            },
            Some(SwipeContext::Switcher) => {
                // Over an app: a big swipe (past the home threshold) minimises
                // it and lands home; a deliberate swipe past the arm point but
                // short of home opens the switcher; a tiny one returns to the
                // app. On the homescreen there is no app to return to, so any
                // release past the arm point keeps the switcher. (Holding to
                // dwell also commits, but that path is unreliable on slow
                // touch hardware, so the release decides too.)
                if swipe.over_app && progress >= BottomSwipe::HOME_COMMIT {
                    self.shell.app_switcher.cancel_swipe();
                    self.windows.minimize_all();
                    self.hide_osk();
                    Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
                } else if progress >= BottomSwipe::DWELL_ARM {
                    self.open_swipe_switcher();
                } else {
                    self.shell.app_switcher.cancel_swipe();
                }
            },
            None => {}
        }
        self.request_redraw();
        true
    }

    /// Returns whether a shell was there to receive it.
    pub( crate ) fn send_to_crustace(msg: &str) -> bool {
        use std::io::Write;
        use std::os::unix::net::UnixStream;

        let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into());
        let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".into());
        let socket_path = format!("{runtime_dir}/crustace-{wayland_display}.sock");

        match UnixStream::connect(&socket_path) {
            Ok(mut stream) => {
                let _ = writeln!(stream, "{msg}");
                let _ = stream.flush();
                true
            }
            Err(e) => {
                tracing::debug!("Could not connect to crustace socket: {e}");
                false
            }
        }
    }
}
