//! Shared HID mapping helpers used by both the client and server crates.

pub type KeyboardHidReport = [u8; 8];

/// Map a printable character to a USB HID usage plus modifier byte.
///
/// Inputs: a Unicode scalar value that should be typed through the HID gadget.
/// Outputs: `Some((usage, modifiers))` for supported ASCII characters, or
/// `None` when the character cannot be represented by the current keyboard map.
/// Why: server-side paste injection and client-side keymap tests must agree on
/// the exact HID encoding so they do not drift apart over time.
#[must_use]
pub fn char_to_usage(c: char) -> Option<(u8, u8)> {
    let shift = 0x02; // left shift in HID modifier byte
    match c {
        'a'..='z' => Some((0x04 + (c as u8 - b'a'), 0)),
        'A'..='Z' => Some((0x04 + (c as u8 - b'A'), shift)),
        '1'..='9' => Some((0x1E + (c as u8 - b'1'), 0)),
        '0' => Some((0x27, 0)),
        '!' => Some((0x1E, shift)),
        '@' => Some((0x1F, shift)),
        '#' => Some((0x20, shift)),
        '$' => Some((0x21, shift)),
        '%' => Some((0x22, shift)),
        '^' => Some((0x23, shift)),
        '&' => Some((0x24, shift)),
        '*' => Some((0x25, shift)),
        '(' => Some((0x26, shift)),
        ')' => Some((0x27, shift)),
        '-' => Some((0x2D, 0)),
        '_' => Some((0x2D, shift)),
        '=' => Some((0x2E, 0)),
        '+' => Some((0x2E, shift)),
        '[' => Some((0x2F, 0)),
        '{' => Some((0x2F, shift)),
        ']' => Some((0x30, 0)),
        '}' => Some((0x30, shift)),
        '\\' => Some((0x31, 0)),
        '|' => Some((0x31, shift)),
        ';' => Some((0x33, 0)),
        ':' => Some((0x33, shift)),
        '\'' => Some((0x34, 0)),
        '"' => Some((0x34, shift)),
        '`' => Some((0x35, 0)),
        '~' => Some((0x35, shift)),
        ',' => Some((0x36, 0)),
        '<' => Some((0x36, shift)),
        '.' => Some((0x37, 0)),
        '>' => Some((0x37, shift)),
        '/' => Some((0x38, 0)),
        '?' => Some((0x38, shift)),
        ' ' => Some((0x2C, 0)),
        '\n' | '\r' => Some((0x28, 0)),
        '\t' => Some((0x2B, 0)),
        _ => None,
    }
}

/// Append the HID report sequence needed to type a character.
///
/// Inputs: an output buffer plus the character that should be typed.
/// Outputs: `true` when the character is supported and reports were appended.
/// Why: some targets miss uppercase or shifted characters when they arrive as a
/// single modifier+usage pulse, so we emit a more human-like modifier press,
/// key press, key release, and modifier release sequence.
pub fn append_char_reports(out: &mut Vec<KeyboardHidReport>, c: char) -> bool {
    let Some((usage, mods)) = char_to_usage(c) else {
        return false;
    };

    if mods != 0 {
        out.push([mods, 0, 0, 0, 0, 0, 0, 0]);
        out.push([mods, 0, usage, 0, 0, 0, 0, 0]);
        out.push([mods, 0, 0, 0, 0, 0, 0, 0]);
        out.push([0; 8]);
    } else {
        out.push([0, 0, usage, 0, 0, 0, 0, 0]);
        out.push([0; 8]);
    }

    true
}

#[cfg(test)]
mod tests {
    use super::{append_char_reports, char_to_usage};

    #[test]
    fn char_to_usage_maps_letters_numbers_and_shifted_symbols() {
        assert_eq!(char_to_usage('a'), Some((0x04, 0)));
        assert_eq!(char_to_usage('Z'), Some((0x1D, 0x02)));
        assert_eq!(char_to_usage('0'), Some((0x27, 0)));
        assert_eq!(char_to_usage('9'), Some((0x26, 0)));
        assert_eq!(char_to_usage(' '), Some((0x2C, 0)));
        assert_eq!(char_to_usage('\n'), Some((0x28, 0)));
        assert_eq!(char_to_usage('\t'), Some((0x2B, 0)));
        assert_eq!(char_to_usage('{'), Some((0x2F, 0x02)));
        assert_eq!(char_to_usage('~'), Some((0x35, 0x02)));
        assert_eq!(char_to_usage('?'), Some((0x38, 0x02)));
    }

    #[test]
    fn char_to_usage_rejects_unsupported_chars() {
        assert_eq!(char_to_usage('é'), None);
        assert_eq!(char_to_usage('\u{2603}'), None);
    }

    #[test]
    fn append_char_reports_expands_shifted_chars_into_four_steps() {
        let mut reports = Vec::new();
        assert!(append_char_reports(&mut reports, 'A'));
        assert_eq!(reports.len(), 4);
        assert_eq!(reports[0], [0x02, 0, 0, 0, 0, 0, 0, 0]);
        assert_eq!(reports[1], [0x02, 0, 0x04, 0, 0, 0, 0, 0]);
        assert_eq!(reports[2], [0x02, 0, 0, 0, 0, 0, 0, 0]);
        assert_eq!(reports[3], [0; 8]);
    }

    #[test]
    fn append_char_reports_keeps_unshifted_chars_as_press_and_release() {
        let mut reports = Vec::new();
        assert!(append_char_reports(&mut reports, 'a'));
        assert_eq!(reports.len(), 2);
        assert_eq!(reports[0], [0, 0, 0x04, 0, 0, 0, 0, 0]);
        assert_eq!(reports[1], [0; 8]);
    }

    #[test]
    fn append_char_reports_rejects_unsupported_chars() {
        let mut reports = Vec::new();
        assert!(!append_char_reports(&mut reports, '🙂'));
        assert!(reports.is_empty());
    }
}
