//! The controller "Add Host" screen — three field rows (name / address / port) plus the
//! Add action. A on a field opens the on-screen keyboard in a bottom tray (on a Steam
//! Deck it opens SDL text input instead — Steam's own keyboard types, ours never
//! draws), B peels one layer: keyboard first, then the screen. Field edits are live;
//! hardware keyboards type straight into the focused field through SDL text input.

use crate::glyphs::{Hint, HintKey};
use crate::model::{ConsoleCmd, HostRow};
use crate::pointer::Pointer;
use crate::screens::{Ctx, Outbox};
use crate::theme::{fg, Fonts, EDGE_INSET, W};
use crate::widgets::{permits, Charset, KeyMsg, Keyboard, ListMsg, MenuList, RowSpec, ROW_MAX_W};
use pf_client_core::menu_nav::{MenuEvent, MenuPulse};
use skia_safe::{Canvas, Rect};

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Field {
    Name,
    Address,
    Port,
}

const FIELDS: [Field; 3] = [Field::Name, Field::Address, Field::Port];

pub(crate) struct AddHostScreen {
    list: MenuList,
    keyboard: Keyboard,
    name: String,
    address: String,
    port: String,
    editing: Option<Field>,
    /// `Some(host key)` = editing a saved host rather than adding one. The same three
    /// fields either way — what changes is the verb, and that the write must UPDATE the
    /// stored host instead of appending a second one beside it.
    edits: Option<String>,
}

impl AddHostScreen {
    pub(crate) fn new() -> AddHostScreen {
        AddHostScreen {
            list: MenuList::new(),
            keyboard: Keyboard::new(),
            name: String::new(),
            address: String::new(),
            port: "9777".into(),
            editing: None,
            edits: None,
        }
    }

    /// The same screen, prefilled, saving over a host instead of adding one.
    pub(crate) fn edit(host: &HostRow) -> AddHostScreen {
        AddHostScreen {
            name: host.name.clone(),
            address: host.addr.clone(),
            port: host.port.to_string(),
            // A pinned card's key carries its profile past a NUL; the HOST is what's edited.
            edits: Some(host.key.split('\0').next().unwrap_or(&host.key).to_string()),
            ..AddHostScreen::new()
        }
    }

    pub(crate) fn title(&self) -> String {
        if self.edits.is_some() {
            "Edit Host".into()
        } else {
            "Add Host".into()
        }
    }

    fn commit_label(&self) -> &'static str {
        if self.edits.is_some() {
            "Save changes"
        } else {
            "Add host"
        }
    }

    /// Mouse/touch. A raised keyboard is modal: it takes anything landing on it, and a
    /// press outside closes it rather than reaching the row underneath — which is what a
    /// tap outside a keyboard means everywhere else on a touchscreen.
    pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool {
        if self.editing.is_some() && !ctx.deck {
            if !self.keyboard.covers(p) {
                if p.press() {
                    self.editing = None;
                    return true;
                }
                return false;
            }
            let (msg, _) = self.keyboard.pointer(p);
            match msg {
                KeyMsg::Type(c) => {
                    self.type_char(c);
                }
                KeyMsg::Backspace => {
                    self.backspace();
                }
                KeyMsg::Done => self.editing = None,
                KeyMsg::None => {}
            }
            return true;
        }
        let (msg, pulse) = self.list.pointer(p, FIELDS.len() + 1);
        if matches!(msg, ListMsg::None) && pulse.is_none() {
            return false;
        }
        self.activate(msg, fx);
        true
    }

    pub(crate) fn editing(&self) -> bool {
        self.editing.is_some()
    }

    fn can_add(&self) -> bool {
        !self.address.trim().is_empty() && self.port.parse::<u16>().is_ok_and(|p| p > 0)
    }

    fn field_mut(&mut self, f: Field) -> &mut String {
        match f {
            Field::Name => &mut self.name,
            Field::Address => &mut self.address,
            Field::Port => &mut self.port,
        }
    }

    fn charset(f: Field) -> Charset {
        match f {
            Field::Name => Charset::Free,
            Field::Address => Charset::Hostname,
            Field::Port => Charset::Digits,
        }
    }

    /// Apply one typed character to the editing field (charset + the port's 5-digit
    /// cap). Returns false when refused — the boundary thud.
    fn type_char(&mut self, ch: char) -> bool {
        let Some(f) = self.editing else { return false };
        if !permits(Self::charset(f), ch) {
            return false;
        }
        if f == Field::Port && self.field_mut(f).chars().count() >= 5 {
            return false;
        }
        self.field_mut(f).push(ch);
        true
    }

    fn backspace(&mut self) -> bool {
        let Some(f) = self.editing else { return false };
        self.field_mut(f).pop().is_some()
    }

    pub(crate) fn text_input(&mut self, text: &str) {
        for ch in text.chars() {
            self.type_char(ch);
        }
    }

    pub(crate) fn edit_key(&mut self, key: crate::input::Key) -> bool {
        use crate::input::Key as K;
        if self.editing.is_none() {
            return false;
        }
        match key {
            K::Backspace => {
                self.backspace();
                true
            }
            K::Return | K::Escape => {
                self.editing = None;
                true
            }
            _ => false,
        }
    }

    pub(crate) fn menu(
        &mut self,
        ev: MenuEvent,
        ctx: &mut Ctx,
        fx: &mut Outbox,
    ) -> Option<MenuPulse> {
        if let Some(_field) = self.editing {
            if ctx.deck {
                // Steam's keyboard owns typing; the pad only closes the field.
                return match ev {
                    MenuEvent::Back | MenuEvent::Confirm => {
                        self.editing = None;
                        Some(MenuPulse::Confirm)
                    }
                    _ => None,
                };
            }
            let (msg, pulse) = self.keyboard.menu(ev);
            return match msg {
                KeyMsg::Type(c) => {
                    if self.type_char(c) {
                        Some(MenuPulse::Move)
                    } else {
                        Some(MenuPulse::Boundary)
                    }
                }
                KeyMsg::Backspace => {
                    if self.backspace() {
                        Some(MenuPulse::Move)
                    } else {
                        Some(MenuPulse::Boundary)
                    }
                }
                KeyMsg::Done => {
                    self.editing = None;
                    Some(MenuPulse::Confirm)
                }
                KeyMsg::None => pulse,
            };
        }

        if ev == MenuEvent::Back {
            fx.pop();
            return None;
        }
        let (msg, pulse) = self.list.menu(ev, FIELDS.len() + 1);
        match msg {
            ListMsg::Activate => {
                self.activate(msg, fx);
                pulse
            }
            _ => pulse,
        }
    }

    /// The commit row's behaviour, shared by the pad/keyboard path and the pointer's.
    fn activate(&mut self, msg: ListMsg, fx: &mut Outbox) {
        if !matches!(msg, ListMsg::Activate) {
            return;
        }
        if self.list.cursor < FIELDS.len() {
            self.editing = Some(FIELDS[self.list.cursor]);
            return;
        }
        if !self.can_add() {
            // Not commitable yet — jump to what's missing instead of a dead press.
            self.list.cursor = 1; // the address row
            self.editing = Some(Field::Address);
            return;
        }
        let (name, addr) = (
            self.name.trim().to_string(),
            self.address.trim().to_string(),
        );
        let port = self.port.parse().unwrap_or(9777);
        match &self.edits {
            Some(key) => {
                // Name it by its nickname if it has one, else by the address — the same
                // fallback the store applies to an unnamed host.
                let label = if name.is_empty() {
                    addr.clone()
                } else {
                    name.clone()
                };
                fx.cmds.push(ConsoleCmd::UpdateHost {
                    key: key.clone(),
                    name,
                    addr,
                    port,
                });
                fx.toast = Some(format!("Saved {label}"));
            }
            None => {
                fx.toast = Some(format!("Added {addr}"));
                fx.cmds.push(ConsoleCmd::SaveHost { name, addr, port });
            }
        }
        fx.pop();
    }

    pub(crate) fn hints(&self, ctx: &Ctx) -> Vec<Hint> {
        if self.editing.is_some() {
            if ctx.deck {
                return vec![
                    Hint::new(HintKey::Key("STEAM + X"), "Keyboard"),
                    Hint::new(HintKey::Confirm, "Done"),
                    Hint::new(HintKey::Back, "Done"),
                ];
            }
            return vec![
                Hint::new(HintKey::Confirm, "Type"),
                Hint::new(HintKey::Tertiary, "Delete"),
                Hint::new(HintKey::Back, "Done"),
            ];
        }
        vec![
            Hint::new(HintKey::Confirm, "Select"),
            Hint::new(HintKey::Back, "Cancel"),
        ]
    }

    pub(crate) fn render(
        &mut self,
        canvas: &Canvas,
        rect: Rect,
        k: f64,
        dt: f64,
        fonts: &Fonts,
        ctx: &mut Ctx,
    ) {
        // Half of the heading's block, so it sits on the heading's column — a centred
        // sub-line under a leading title reads as belonging to the rows instead. Its width
        // is capped against the ROW column, not the screen: measured off the full width it
        // would run all the way under the controller chip.
        fonts.leading(
            canvas,
            "Hosts on this network appear automatically — add one by address for everything else.",
            W::Regular,
            13.0 * k,
            fg(0.55),
            f64::from(rect.left) + EDGE_INSET * k,
            f64::from(rect.top) + 2.0 * k,
            ROW_MAX_W * 0.72 * k,
        );

        // While the keyboard tray is up (never on Deck) the rows squeeze above it.
        let seat = self.keyboard.seat(self.editing.is_some() && !ctx.deck, dt);
        let tray_h = if seat > 0.0 {
            (Keyboard::tray_height() + 12.0) * k * seat
        } else {
            0.0
        };
        let list_rect = Rect::from_ltrb(
            rect.left,
            rect.top + (34.0 * k) as f32,
            rect.right,
            rect.bottom - tray_h as f32,
        );
        let rows = self.rows();
        self.list.render(
            canvas,
            list_rect,
            &rows,
            fonts,
            k,
            dt,
            self.editing.is_none(),
        );
        if seat > 0.0 {
            self.keyboard.render(
                canvas,
                fonts,
                f64::from(rect.width()),
                f64::from(rect.bottom),
                seat,
                k,
            );
        }
    }

    fn rows(&self) -> Vec<RowSpec> {
        let field_row = |label: &str, value: &str, placeholder: &str, f: Field| {
            let mut row = RowSpec::field(label, value.to_string(), placeholder);
            row.caret = self.editing == Some(f);
            row
        };
        vec![
            field_row(
                "Name",
                &self.name,
                "Optional — e.g. Living Room",
                Field::Name,
            ),
            field_row("Address", &self.address, "IP or hostname", Field::Address),
            field_row("Port", &self.port, "9777", Field::Port),
            RowSpec::action(self.commit_label(), self.can_add()),
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::screens::Nav;
    use pf_client_core::trust::Settings;

    fn ctx<'a>(
        settings: &'a mut Settings,
        pads: &'a [pf_client_core::menu_nav::PadInfo],
        library: &'a crate::library::LibraryShared,
        deck: bool,
    ) -> Ctx<'a> {
        Ctx {
            hosts: &[],
            library,
            settings,
            store: crate::store::file_store(),
            platform: crate::platform::Platform::Desktop,
            pads,
            deck,
            device_name: "t",
            t: 0.0,
        }
    }

    #[test]
    fn end_to_end_add_flow() {
        let mut settings = Settings::default();
        let library = crate::library::LibraryShared::default();
        let mut c = ctx(&mut settings, &[], &library, false);
        let mut s = AddHostScreen::new();
        let mut fx = Outbox::default();

        // A on "Add Host" while the address is empty jumps into the address field.
        s.list.cursor = 3;
        s.menu(MenuEvent::Confirm, &mut c, &mut fx);
        assert_eq!(s.editing, Some(Field::Address));
        assert_eq!(s.list.cursor, 1);

        // Hardware keys / Steam keyboard commit text; spaces are refused for addresses.
        s.text_input("deck tower.local");
        assert_eq!(s.address, "decktower.local");
        s.edit_key(crate::input::Key::Backspace);
        assert_eq!(s.address, "decktower.loca");
        s.text_input("l");
        s.edit_key(crate::input::Key::Return);
        assert!(s.editing.is_none());

        // Now Add saves, toasts, and pops.
        s.list.cursor = 3;
        let mut fx = Outbox::default();
        s.menu(MenuEvent::Confirm, &mut c, &mut fx);
        assert!(matches!(
            fx.cmds.first(),
            Some(ConsoleCmd::SaveHost { addr, port: 9777, .. }) if addr == "decktower.local"
        ));
        assert!(matches!(fx.nav, Some(Nav::Pop)));
    }

    #[test]
    fn port_caps_at_five_digits() {
        let mut s = AddHostScreen::new();
        s.editing = Some(Field::Port);
        s.port.clear();
        s.text_input("123456789");
        assert_eq!(s.port, "12345");
        assert!(!s.type_char('x'), "digits only");
    }

    #[test]
    fn deck_mode_never_uses_the_grid() {
        let mut settings = Settings::default();
        let library = crate::library::LibraryShared::default();
        let mut c = ctx(&mut settings, &[], &library, true);
        let mut s = AddHostScreen::new();
        let mut fx = Outbox::default();
        s.list.cursor = 1;
        s.menu(MenuEvent::Confirm, &mut c, &mut fx); // open the address field
        assert!(s.editing());
        // Moves don't drive a key grid on Deck; B closes the field.
        assert!(s
            .menu(
                MenuEvent::Move(pf_client_core::menu_nav::MenuDir::Right),
                &mut c,
                &mut fx
            )
            .is_none());
        s.menu(MenuEvent::Back, &mut c, &mut fx);
        assert!(!s.editing());
        assert!(fx.nav.is_none(), "B closed the field, not the screen");
    }
}
