use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
    error::Error,
    fmt::{Display, Write},
};

/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke {
    /// the state of the modifier keys at the time the keystroke was generated
    pub modifiers: Modifiers,

    /// key is the character printed on the key that was pressed
    /// e.g. for option-s, key is "s"
    pub key: String,

    /// key_char is the character that could have been typed when
    /// this binding was pressed.
    /// e.g. for s this is "s", for option-s "ß", and cmd-s None
    pub key_char: Option<String>,
}

/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
/// markdown to display it.
#[derive(Debug)]
pub struct InvalidKeystrokeError {
    /// The invalid keystroke.
    pub keystroke: String,
}

impl Error for InvalidKeystrokeError {}

impl Display for InvalidKeystrokeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Invalid keystroke \"{}\". {}",
            self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
        )
    }
}

/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
    (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
    followed by a key, separated by `-`.";

impl Keystroke {
    /// When matching a key we cannot know whether the user intended to type
    /// the key_char or the key itself. On some non-US keyboards keys we use in our
    /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
    /// and on some keyboards the IME handler converts a sequence of keys into a
    /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
    ///
    /// This method assumes that `self` was typed and `target' is in the keymap, and checks
    /// both possibilities for self against the target.
    pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
        if let Some(key_char) = self
            .key_char
            .as_ref()
            .filter(|key_char| key_char != &&self.key)
        {
            let ime_modifiers = Modifiers {
                control: self.modifiers.control,
                platform: self.modifiers.platform,
                ..Default::default()
            };

            if &target.key == key_char && target.modifiers == ime_modifiers {
                return true;
            }
        }

        target.modifiers == self.modifiers && target.key == self.key
    }

    /// key syntax is:
    /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
    /// key_char syntax is only used for generating test events,
    /// secondary means "cmd" on macOS and "ctrl" on other platforms
    /// when matching a key with an key_char set will be matched without it.
    pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
        let mut control = false;
        let mut alt = false;
        let mut shift = false;
        let mut platform = false;
        let mut function = false;
        let mut key = None;
        let mut key_char = None;

        let mut components = source.split('-').peekable();
        while let Some(component) = components.next() {
            if component.eq_ignore_ascii_case("ctrl") {
                control = true;
                continue;
            }
            if component.eq_ignore_ascii_case("alt") {
                alt = true;
                continue;
            }
            if component.eq_ignore_ascii_case("shift") {
                shift = true;
                continue;
            }
            if component.eq_ignore_ascii_case("fn") {
                function = true;
                continue;
            }
            if component.eq_ignore_ascii_case("secondary") {
                if cfg!(target_os = "macos") {
                    platform = true;
                } else {
                    control = true;
                };
                continue;
            }

            let is_platform = component.eq_ignore_ascii_case("cmd")
                || component.eq_ignore_ascii_case("super")
                || component.eq_ignore_ascii_case("win");

            if is_platform {
                platform = true;
                continue;
            }

            let mut key_str = component.to_string();

            if let Some(next) = components.peek() {
                if next.is_empty() && source.ends_with('-') {
                    key = Some(String::from("-"));
                    break;
                } else if next.len() > 1 && next.starts_with('>') {
                    key = Some(key_str);
                    key_char = Some(String::from(&next[1..]));
                    components.next();
                } else {
                    return Err(InvalidKeystrokeError {
                        keystroke: source.to_owned(),
                    });
                }
                continue;
            }

            if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
                // Convert to shift + lowercase char
                shift = true;
                key_str.make_ascii_lowercase();
            } else {
                // convert ascii chars to lowercase so that named keys like "tab" and "enter"
                // are accepted case insensitively and stored how we expect so they are matched properly
                key_str.make_ascii_lowercase()
            }
            key = Some(key_str);
        }

        // Allow for the user to specify a keystroke modifier as the key itself
        // This sets the `key` to the modifier, and disables the modifier
        if key.is_none() {
            if shift {
                key = Some("shift".to_string());
                shift = false;
            } else if control {
                key = Some("control".to_string());
                control = false;
            } else if alt {
                key = Some("alt".to_string());
                alt = false;
            } else if platform {
                key = Some("platform".to_string());
                platform = false;
            } else if function {
                key = Some("function".to_string());
                function = false;
            }
        }

        let key = key.ok_or_else(|| InvalidKeystrokeError {
            keystroke: source.to_owned(),
        })?;

        Ok(Keystroke {
            modifiers: Modifiers {
                control,
                alt,
                shift,
                platform,
                function,
            },
            key,
            key_char,
        })
    }

    /// Produces a representation of this key that Parse can understand.
    pub fn unparse(&self) -> String {
        let mut str = String::new();
        if self.modifiers.function {
            str.push_str("fn-");
        }
        if self.modifiers.control {
            str.push_str("ctrl-");
        }
        if self.modifiers.alt {
            str.push_str("alt-");
        }
        if self.modifiers.platform {
            #[cfg(target_os = "macos")]
            str.push_str("cmd-");

            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
            str.push_str("super-");

            #[cfg(target_os = "windows")]
            str.push_str("win-");
        }
        if self.modifiers.shift {
            str.push_str("shift-");
        }
        str.push_str(&self.key);
        str
    }

    /// Returns true if this keystroke left
    /// the ime system in an incomplete state.
    pub fn is_ime_in_progress(&self) -> bool {
        self.key_char.is_none()
            && (is_printable_key(&self.key) || self.key.is_empty())
            && !(self.modifiers.platform
                || self.modifiers.control
                || self.modifiers.function
                || self.modifiers.alt)
    }

    /// Returns a new keystroke with the key_char filled.
    /// This is used for dispatch_keystroke where we want users to
    /// be able to simulate typing "space", etc.
    pub fn with_simulated_ime(mut self) -> Self {
        if self.key_char.is_none()
            && !self.modifiers.platform
            && !self.modifiers.control
            && !self.modifiers.function
            && !self.modifiers.alt
        {
            self.key_char = match self.key.as_str() {
                "space" => Some(" ".into()),
                "tab" => Some("\t".into()),
                "enter" => Some("\n".into()),
                key if !is_printable_key(key) || key.is_empty() => None,
                key => {
                    if self.modifiers.shift {
                        Some(key.to_uppercase())
                    } else {
                        Some(key.into())
                    }
                }
            }
        }
        self
    }
}

fn is_printable_key(key: &str) -> bool {
    !matches!(
        key,
        "f1" | "f2"
            | "f3"
            | "f4"
            | "f5"
            | "f6"
            | "f7"
            | "f8"
            | "f9"
            | "f10"
            | "f11"
            | "f12"
            | "f13"
            | "f14"
            | "f15"
            | "f16"
            | "f17"
            | "f18"
            | "f19"
            | "f20"
            | "f21"
            | "f22"
            | "f23"
            | "f24"
            | "f25"
            | "f26"
            | "f27"
            | "f28"
            | "f29"
            | "f30"
            | "f31"
            | "f32"
            | "f33"
            | "f34"
            | "f35"
            | "backspace"
            | "delete"
            | "left"
            | "right"
            | "up"
            | "down"
            | "pageup"
            | "pagedown"
            | "insert"
            | "home"
            | "end"
            | "back"
            | "forward"
            | "escape"
    )
}

impl std::fmt::Display for Keystroke {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.modifiers.control {
            if cfg!(target_os = "macos") {
                f.write_char('^')?;
            } else {
                write!(f, "ctrl-")?;
            }
        }
        if self.modifiers.alt {
            if cfg!(target_os = "macos") {
                f.write_char('⌥')?;
            } else {
                write!(f, "alt-")?;
            }
        }
        if self.modifiers.platform {
            #[cfg(target_os = "macos")]
            f.write_char('⌘')?;

            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
            f.write_char('❖')?;

            #[cfg(target_os = "windows")]
            f.write_char('⊞')?;
        }
        if self.modifiers.shift {
            if cfg!(target_os = "macos") {
                f.write_char('⇧')?;
            } else {
                write!(f, "shift-")?;
            }
        }
        let key = match self.key.as_str() {
            "backspace" if cfg!(target_os = "macos") => '⌫',
            "up" if cfg!(target_os = "macos") => '↑',
            "down" if cfg!(target_os = "macos") => '↓',
            "left" if cfg!(target_os = "macos") => '←',
            "right" if cfg!(target_os = "macos") => '→',
            "tab" if cfg!(target_os = "macos") => '⇥',
            "escape" if cfg!(target_os = "macos") => '⎋',
            "shift" if cfg!(target_os = "macos") => '⇧',
            "control" if cfg!(target_os = "macos") => '⌃',
            "alt" if cfg!(target_os = "macos") => '⌥',
            "platform" if cfg!(target_os = "macos") => '⌘',
            key => {
                if key.len() == 1 {
                    key.chars().next().unwrap().to_ascii_uppercase()
                } else {
                    return f.write_str(key);
                }
            }
        };
        f.write_char(key)
    }
}

/// The state of the modifier keys at some point in time
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
pub struct Modifiers {
    /// The control key
    #[serde(default)]
    pub control: bool,

    /// The alt key
    /// Sometimes also known as the 'meta' key
    #[serde(default)]
    pub alt: bool,

    /// The shift key
    #[serde(default)]
    pub shift: bool,

    /// The command key, on macos
    /// the windows key, on windows
    /// the super key, on linux
    #[serde(default)]
    pub platform: bool,

    /// The function key
    #[serde(default)]
    pub function: bool,
}

impl Modifiers {
    /// Returns whether any modifier key is pressed.
    pub fn modified(&self) -> bool {
        self.control || self.alt || self.shift || self.platform || self.function
    }

    /// Whether the semantically 'secondary' modifier key is pressed.
    ///
    /// On macOS, this is the command key.
    /// On Linux and Windows, this is the control key.
    pub fn secondary(&self) -> bool {
        #[cfg(target_os = "macos")]
        {
            self.platform
        }

        #[cfg(not(target_os = "macos"))]
        {
            self.control
        }
    }

    /// Returns how many modifier keys are pressed.
    pub fn number_of_modifiers(&self) -> u8 {
        self.control as u8
            + self.alt as u8
            + self.shift as u8
            + self.platform as u8
            + self.function as u8
    }

    /// Returns [`Modifiers`] with no modifiers.
    pub fn none() -> Modifiers {
        Default::default()
    }

    /// Returns [`Modifiers`] with just the command key.
    pub fn command() -> Modifiers {
        Modifiers {
            platform: true,
            ..Default::default()
        }
    }

    /// A Returns [`Modifiers`] with just the secondary key pressed.
    pub fn secondary_key() -> Modifiers {
        #[cfg(target_os = "macos")]
        {
            Modifiers {
                platform: true,
                ..Default::default()
            }
        }

        #[cfg(not(target_os = "macos"))]
        {
            Modifiers {
                control: true,
                ..Default::default()
            }
        }
    }

    /// Returns [`Modifiers`] with just the windows key.
    pub fn windows() -> Modifiers {
        Modifiers {
            platform: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with just the super key.
    pub fn super_key() -> Modifiers {
        Modifiers {
            platform: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with just control.
    pub fn control() -> Modifiers {
        Modifiers {
            control: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with just alt.
    pub fn alt() -> Modifiers {
        Modifiers {
            alt: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with just shift.
    pub fn shift() -> Modifiers {
        Modifiers {
            shift: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with command + shift.
    pub fn command_shift() -> Modifiers {
        Modifiers {
            shift: true,
            platform: true,
            ..Default::default()
        }
    }

    /// Returns [`Modifiers`] with command + shift.
    pub fn control_shift() -> Modifiers {
        Modifiers {
            shift: true,
            control: true,
            ..Default::default()
        }
    }

    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
        (other.control || !self.control)
            && (other.alt || !self.alt)
            && (other.shift || !self.shift)
            && (other.platform || !self.platform)
            && (other.function || !self.function)
    }
}
