use std::fmt::Write;

use anyhow::Result;

use super::{
  key::{Key, KeyCode, KeyMods, MediaKeyCode, ModKeyCode},
  mouse::{MouseButton, MouseEvent, MouseEventKind},
};

pub const CSI: &str = "\x1b[";
pub const SS3: &str = "\x1bO";

/// Specifies terminal modes/configuration that can influence how a KeyCode
/// is encoded when being sent to and application via the pty.
#[derive(Debug, Clone, Copy)]
pub struct KeyCodeEncodeModes {
  pub enable_csi_u_key_encoding: bool,
  pub application_cursor_keys: bool,
  pub newline_mode: bool,
}

#[allow(clippy::derivable_impls)]
impl Default for KeyCodeEncodeModes {
  fn default() -> Self {
    KeyCodeEncodeModes {
      enable_csi_u_key_encoding: false,
      application_cursor_keys: false,
      newline_mode: false,
    }
  }
}

/// Returns the xterm compatible byte sequence that represents this KeyCode
/// and Modifier combination.
pub fn encode_key(key: &Key, modes: KeyCodeEncodeModes) -> Result<String> {
  use KeyCode::*;

  #[cfg(windows)]
  if let Some(encoded) = encode_key_win32(key, modes) {
    return Ok(encoded);
  }

  let code = key.code();
  let mods = key.mods();

  let mut buf = String::new();

  let code = normalize_shift_to_upper_case(code, &mods);
  // Normalize the modifier state for Char's that are uppercase; remove
  // the SHIFT modifier so that reduce ambiguity below
  let mods = match code {
    Char(c)
      if (c.is_ascii_punctuation() || c.is_ascii_uppercase())
        && mods.contains(KeyMods::SHIFT) =>
    {
      mods.difference(KeyMods::SHIFT)
    }
    _ => mods,
  };

  // Normalize Backspace and Delete
  let code = match code {
    Char('\x7f') => KeyCode::Backspace,
    Char('\x08') => KeyCode::Delete,
    c => c,
  };

  // TODO: also respect self.application_keypad

  match code {
    Char(c)
      if is_ambiguous_ascii_ctrl(c)
        && mods.contains(KeyMods::CONTROL)
        && modes.enable_csi_u_key_encoding =>
    {
      csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
    }
    Char(c) if c.is_ascii_uppercase() && mods.contains(KeyMods::CONTROL) => {
      csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
    }

    Char(c) if mods.contains(KeyMods::CONTROL) && ctrl_mapping(c).is_some() => {
      let c = ctrl_mapping(c).unwrap();
      if mods.contains(KeyMods::ALT) {
        buf.push(0x1b as char);
      }
      buf.push(c);
    }

    // When alt is pressed, send escape first to indicate to the peer that
    // ALT is pressed.  We do this only for ascii alnum characters because
    // eg: on macOS generates altgr style glyphs and keeps the ALT key
    // in the modifier set.  This confuses eg: zsh which then just displays
    // <fffffffff> as the input, so we want to avoid that.
    Char(c)
      if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
        && mods.contains(KeyMods::ALT) =>
    {
      buf.push(0x1b as char);
      buf.push(c);
    }

    Enter | Esc | Backspace => {
      let c = match code {
        Enter => '\r',
        Esc => '\x1b',
        // Backspace sends the default VERASE which is confusingly
        // the DEL ascii codepoint
        Backspace => '\x7f',
        _ => unreachable!(),
      };
      if mods.contains(KeyMods::SHIFT) || mods.contains(KeyMods::CONTROL) {
        csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
      } else {
        if mods.contains(KeyMods::ALT) {
          buf.push(0x1b as char);
        }
        buf.push(c);
        if modes.newline_mode && code == Enter {
          buf.push(0x0a as char);
        }
      }
    }

    Tab => {
      if mods.contains(KeyMods::ALT) {
        buf.push(0x1b as char);
      }
      let mods = mods & !KeyMods::ALT;
      if mods == KeyMods::CONTROL {
        buf.push_str("\x1b[9;5u");
      } else if mods == KeyMods::CONTROL | KeyMods::SHIFT {
        buf.push_str("\x1b[1;5Z");
      } else if mods == KeyMods::SHIFT {
        buf.push_str("\x1b[Z");
      } else {
        buf.push('\t');
      }
    }

    Char(c) => {
      if mods.is_empty() {
        buf.push(c);
      } else {
        csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
      }
    }

    Home | End | Up | Down | Right | Left => {
      let (force_app, c) = match code {
        Up => (false, 'A'),
        Down => (false, 'B'),
        Right => (false, 'C'),
        Left => (false, 'D'),
        Home => (false, 'H'),
        End => (false, 'F'),
        _ => unreachable!(),
      };

      let csi_or_ss3 = if force_app
        || (
          modes.application_cursor_keys
          // Strict reading of DECCKM suggests that application_cursor_keys
          // only applies when DECANM and DECKPAM are active, but that seems
          // to break unmodified cursor keys in vim
          /* && self.dec_ansi_mode && self.application_keypad */
        ) {
        // Use SS3 in application mode
        SS3
      } else {
        // otherwise use regular CSI
        CSI
      };

      if mods.contains(KeyMods::ALT)
        || mods.contains(KeyMods::SHIFT)
        || mods.contains(KeyMods::CONTROL)
      {
        write!(buf, "{}1;{}{}", CSI, 1 + encode_modifiers(mods), c)?;
      } else {
        write!(buf, "{}{}", csi_or_ss3, c)?;
      }
    }

    PageUp | PageDown | Insert | Delete => {
      let c = match code {
        Insert => 2,
        Delete => 3,
        PageUp => 5,
        PageDown => 6,
        _ => unreachable!(),
      };

      if mods.contains(KeyMods::ALT)
        || mods.contains(KeyMods::SHIFT)
        || mods.contains(KeyMods::CONTROL)
      {
        write!(buf, "\x1b[{};{}~", c, 1 + encode_modifiers(mods))?;
      } else {
        write!(buf, "\x1b[{}~", c)?;
      }
    }

    F(n) => {
      if mods.is_empty() && n < 5 {
        // F1-F4 are encoded using SS3 if there are no modifiers
        let s = match n {
          1 => "\x1bOP",
          2 => "\x1bOQ",
          3 => "\x1bOR",
          4 => "\x1bOS",
          _ => unreachable!("wat?"),
        };
        write!(buf, "{}", s)?;
      } else {
        // Higher numbered F-keys plus modified F-keys are encoded
        // using CSI instead of SS3.
        let intro = match n {
          1 => "\x1b[11",
          2 => "\x1b[12",
          3 => "\x1b[13",
          4 => "\x1b[14",
          5 => "\x1b[15",
          6 => "\x1b[17",
          7 => "\x1b[18",
          8 => "\x1b[19",
          9 => "\x1b[20",
          10 => "\x1b[21",
          11 => "\x1b[23",
          12 => "\x1b[24",
          _ => panic!("unhandled fkey number {}", n),
        };
        let encoded_mods = encode_modifiers(mods);
        if encoded_mods == 0 {
          // If no modifiers are held, don't send the modifier
          // sequence, as the modifier encoding is a CSI-u extension.
          write!(buf, "{}~", intro)?;
        } else {
          write!(buf, "{};{}~", intro, 1 + encoded_mods)?;
        }
      }
    }

    Null => (),
    CapsLock => (),
    ScrollLock => (),
    NumLock => (),
    PrintScreen => (),
    Pause => (),
    Menu => (),
    KeypadBegin => (),
    Media(_) => (),
    Modifier(_) => (),
  };

  Ok(buf)
}

fn encode_modifiers(mods: KeyMods) -> u8 {
  let mut number = 0;
  if mods.contains(KeyMods::SHIFT) {
    number |= 1;
  }
  if mods.contains(KeyMods::ALT) {
    number |= 2;
  }
  if mods.contains(KeyMods::CONTROL) {
    number |= 4;
  }
  number
}

/// characters that when masked for CTRL could be an ascii control character
/// or could be a key that a user legitimately wants to process in their
/// terminal application
fn is_ambiguous_ascii_ctrl(c: char) -> bool {
  match c {
    'i' | 'I' | 'm' | 'M' | '[' | '{' | '@' => true,
    _ => false,
  }
}

/// Map c to its Ctrl equivalent.
/// In theory, this mapping is simply translating alpha characters
/// to upper case and then masking them by 0x1f, but xterm inherits
/// some built-in translation from legacy X11 so that are some
/// aliased mappings and a couple that might be technically tied
/// to US keyboard layout (particularly the punctuation characters
/// produced in combination with SHIFT) that may not be 100%
/// the right thing to do here for users with non-US layouts.
fn ctrl_mapping(c: char) -> Option<char> {
  Some(match c {
    '@' | '`' | ' ' | '2' => '\x00',
    'A' | 'a' => '\x01',
    'B' | 'b' => '\x02',
    'C' | 'c' => '\x03',
    'D' | 'd' => '\x04',
    'E' | 'e' => '\x05',
    'F' | 'f' => '\x06',
    'G' | 'g' => '\x07',
    'H' | 'h' => '\x08',
    'I' | 'i' => '\x09',
    'J' | 'j' => '\x0a',
    'K' | 'k' => '\x0b',
    'L' | 'l' => '\x0c',
    'M' | 'm' => '\x0d',
    'N' | 'n' => '\x0e',
    'O' | 'o' => '\x0f',
    'P' | 'p' => '\x10',
    'Q' | 'q' => '\x11',
    'R' | 'r' => '\x12',
    'S' | 's' => '\x13',
    'T' | 't' => '\x14',
    'U' | 'u' => '\x15',
    'V' | 'v' => '\x16',
    'W' | 'w' => '\x17',
    'X' | 'x' => '\x18',
    'Y' | 'y' => '\x19',
    'Z' | 'z' => '\x1a',
    '[' | '3' | '{' => '\x1b',
    '\\' | '4' | '|' => '\x1c',
    ']' | '5' | '}' => '\x1d',
    '^' | '6' | '~' => '\x1e',
    '_' | '7' | '/' => '\x1f',
    '8' | '?' => '\x7f', // `Delete`
    _ => return None,
  })
}

fn csi_u_encode(
  buf: &mut String,
  c: char,
  mods: KeyMods,
  enable_csi_u_key_encoding: bool,
) -> Result<()> {
  if enable_csi_u_key_encoding {
    write!(buf, "\x1b[{};{}u", c as u32, 1 + encode_modifiers(mods))?;
  } else {
    let c = if mods.contains(KeyMods::CONTROL) && ctrl_mapping(c).is_some() {
      ctrl_mapping(c).unwrap()
    } else {
      c
    };
    if mods.contains(KeyMods::ALT) {
      buf.push(0x1b as char);
    }
    write!(buf, "{}", c)?;
  }
  Ok(())
}

/// if SHIFT is held and we have KeyCode::Char('c') we want to normalize
/// that keycode to KeyCode::Char('C'); that is what this function does.
/// In theory we should give the same treatment to keys like `[` -> `{`
/// but that assumes something about the keyboard layout and is probably
/// better done in the gui frontend rather than this layer.
/// In fact, this function might be better off if it lived elsewhere.
pub fn normalize_shift_to_upper_case(
  code: KeyCode,
  modifiers: &KeyMods,
) -> KeyCode {
  if modifiers.contains(KeyMods::SHIFT) {
    match code {
      KeyCode::Char(c) if c.is_ascii_lowercase() => KeyCode::Char(c),
      _ => code,
    }
  } else {
    code
  }
}

/// <https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md>
#[cfg(windows)]
pub fn encode_key_win32(
  key: &Key,
  _modes: KeyCodeEncodeModes,
) -> Option<String> {
  // <https://docs.microsoft.com/en-us/windows/console/key-event-record-str>
  // defines the dwControlKeyState values
  let mut control_key_state = 0;

  if key.mods().contains(KeyMods::SHIFT) {
    control_key_state |= windows::Win32::System::Console::SHIFT_PRESSED;
  }
  if key.mods().contains(KeyMods::ALT) {
    control_key_state |= windows::Win32::System::Console::LEFT_ALT_PRESSED;
  }
  if key.mods().contains(KeyMods::CONTROL) {
    control_key_state |= windows::Win32::System::Console::LEFT_CTRL_PRESSED;
  }

  let vkey = virtual_key_code(&key.code())?;
  let uni = match key.code() {
    KeyCode::Char(c) => {
      let c = match c {
        // Delete key is transmitted as 0x0
        '\x7f' => '\x00',
        // Backspace key is transmitted as 0x8, 0x7f or 0x0
        '\x08' => {
          if key.mods().contains(KeyMods::CONTROL) {
            if key.mods().contains(KeyMods::ALT)
              || key.mods().contains(KeyMods::SHIFT)
            {
              '\x00'
            } else {
              '\x7f'
            }
          } else {
            '\x08'
          }
        }
        _ => c,
      };

      let c = if key.mods().contains(KeyMods::CONTROL) {
        // Ensure that we rewrite the unicode value to the ASCII CTRL
        // equivalent value.
        // <https://github.com/microsoft/terminal/issues/13134>
        ctrl_mapping(c).unwrap_or(c)
      } else {
        c
      };
      c as u32
    }
    KeyCode::Backspace => 0x8,
    KeyCode::Enter => 0xd,
    KeyCode::Left => 0,
    KeyCode::Right => 0,
    KeyCode::Up => 0,
    KeyCode::Down => 0,
    KeyCode::Home => 0,
    KeyCode::End => 0,
    KeyCode::PageUp => 0,
    KeyCode::PageDown => 0,
    KeyCode::Tab => 0x9,
    KeyCode::Delete => 0x7f,
    KeyCode::Insert => 0,
    KeyCode::F(_) => 0,
    KeyCode::Null => 0,
    KeyCode::Esc => 0,
    KeyCode::CapsLock => 0,
    KeyCode::ScrollLock => 0,
    KeyCode::NumLock => 0,
    KeyCode::PrintScreen => 0,
    KeyCode::Pause => 0,
    KeyCode::Menu => 0,
    KeyCode::KeypadBegin => 0,
    KeyCode::Media(_) => 0,
    KeyCode::Modifier(_) => 0,
  };

  let scan_code = 0;
  let key_down = 1;
  let repeat_count = 1;
  Some(format!(
    "\u{1b}[{};{};{};{};{};{}_",
    vkey.0, scan_code, uni, key_down, control_key_state, repeat_count
  ))
}

/// <https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes>
#[cfg(windows)]
fn virtual_key_code(
  code: &KeyCode,
) -> Option<windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY> {
  use windows::Win32::UI::Input::KeyboardAndMouse::*;

  let code = match code {
    KeyCode::Char(c) => match c {
      '0'..='9' => VIRTUAL_KEY(*c as u16),
      'a'..='z' => VIRTUAL_KEY(c.to_ascii_uppercase() as u16),
      ' ' => VK_SPACE,
      '*' => VK_MULTIPLY,
      '+' => VK_ADD,
      ',' => VK_SEPARATOR,
      '-' => VK_SUBTRACT,
      '.' => VK_DECIMAL,
      '/' => VK_DIVIDE,
      _ => return None,
    },
    KeyCode::Backspace => VK_BACK,
    KeyCode::Enter => VK_RETURN,
    KeyCode::Left => VK_LEFT,
    KeyCode::Right => VK_RIGHT,
    KeyCode::Up => VK_UP,
    KeyCode::Down => VK_DOWN,
    KeyCode::Home => VK_HOME,
    KeyCode::End => VK_END,
    KeyCode::PageUp => VK_PRIOR,
    KeyCode::PageDown => VK_NEXT,
    KeyCode::Tab => VK_TAB,
    KeyCode::Delete => VK_DELETE,
    KeyCode::Insert => VK_INSERT,
    KeyCode::F(n) => match n {
      1..=24 => VIRTUAL_KEY(VK_F1.0 - 1 + *n as u16),
      _ => return None,
    },
    KeyCode::Null => VIRTUAL_KEY(0),
    KeyCode::Esc => VK_ESCAPE,
    KeyCode::CapsLock => VIRTUAL_KEY(0),
    KeyCode::ScrollLock => VIRTUAL_KEY(0),
    KeyCode::NumLock => VIRTUAL_KEY(0),
    KeyCode::PrintScreen => VIRTUAL_KEY(0),
    KeyCode::Pause => VIRTUAL_KEY(0),
    KeyCode::Menu => VIRTUAL_KEY(0),
    KeyCode::KeypadBegin => VIRTUAL_KEY(0),
    KeyCode::Media(_) => VIRTUAL_KEY(0),
    KeyCode::Modifier(_) => VIRTUAL_KEY(0),
  };

  Some(code)
}

pub fn print_key(key: &Key) -> String {
  let mut buf = String::new();

  if key.mods().contains(KeyMods::CONTROL) {
    buf.push_str("C-");
  }
  if key.mods().contains(KeyMods::SHIFT) {
    buf.push_str("S-");
  }
  if key.mods().contains(KeyMods::ALT) {
    buf.push_str("M-");
  }

  match key.code() {
    KeyCode::Backspace => buf.push_str("BS"),
    KeyCode::Enter => buf.push_str("Enter"),
    KeyCode::Left => buf.push_str("Left"),
    KeyCode::Right => buf.push_str("Right"),
    KeyCode::Up => buf.push_str("Up"),
    KeyCode::Down => buf.push_str("Down"),
    KeyCode::Home => buf.push_str("Home"),
    KeyCode::End => buf.push_str("End"),
    KeyCode::PageUp => buf.push_str("PageUp"),
    KeyCode::PageDown => buf.push_str("PageDown"),
    KeyCode::Tab => buf.push_str("Tab"),
    KeyCode::Delete => buf.push_str("Del"),
    KeyCode::Insert => buf.push_str("Insert"),
    KeyCode::F(n) => buf.push_str(&format!("F{}", n)),
    KeyCode::Char(ch) => buf.push(ch),
    KeyCode::Null => buf.push_str("Nul"),
    KeyCode::Esc => buf.push_str("Esc"),
    KeyCode::CapsLock => buf.push_str("CapsLock"),
    KeyCode::ScrollLock => buf.push_str("ScrollLock"),
    KeyCode::NumLock => buf.push_str("NumLock"),
    KeyCode::PrintScreen => buf.push_str("PrintScreen"),
    KeyCode::Pause => buf.push_str("Pause"),
    KeyCode::Menu => buf.push_str("Menu"),
    KeyCode::KeypadBegin => buf.push_str("KeypadBegin"),
    KeyCode::Media(code) => buf.push_str(match code {
      MediaKeyCode::Play => "MediaPlay",
      MediaKeyCode::Pause => "MediaPause",
      MediaKeyCode::PlayPause => "MediaPlayPause",
      MediaKeyCode::Reverse => "MediaReverse",
      MediaKeyCode::Stop => "MediaStop",
      MediaKeyCode::FastForward => "MediaFastForward",
      MediaKeyCode::Rewind => "MediaRewind",
      MediaKeyCode::Next => "MediaNext",
      MediaKeyCode::Prev => "MediaPrev",
      MediaKeyCode::Record => "MediaRecord",
      MediaKeyCode::VolumeDown => "VolumeDown",
      MediaKeyCode::VolumeUp => "VolumeUp",
      MediaKeyCode::VolumeMute => "VolumeMute",
    }),
    KeyCode::Modifier(code) => buf.push_str(match code {
      ModKeyCode::LeftShift => "LeftShift",
      ModKeyCode::LeftControl => "LeftControl",
      ModKeyCode::LeftAlt => "LeftAlt",
      ModKeyCode::LeftSuper => "LeftSuper",
      ModKeyCode::LeftHyper => "LeftHyper",
      ModKeyCode::LeftMeta => "LeftMeta",
      ModKeyCode::RightShift => "RightShift",
      ModKeyCode::RightControl => "RightControl",
      ModKeyCode::RightAlt => "RightAlt",
      ModKeyCode::RightSuper => "RightSuper",
      ModKeyCode::RightHyper => "RightHyper",
      ModKeyCode::RightMeta => "RightMeta",
      // ModKeyCode::IsoLevel3Shift => "IsoLevel3Shift",
      // ModKeyCode::IsoLevel5Shift => "IsoLevel5Shift",
    }),
  }

  buf
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn print_key_handles_non_text_keys() {
    assert_eq!(print_key(&Key::from(KeyCode::CapsLock)), "CapsLock");
    assert_eq!(print_key(&Key::from(KeyCode::NumLock)), "NumLock");
    assert_eq!(
      print_key(&Key::from(KeyCode::Media(MediaKeyCode::PlayPause))),
      "MediaPlayPause"
    );
    assert_eq!(
      print_key(&Key::new(
        KeyCode::Modifier(ModKeyCode::LeftSuper),
        KeyMods::SUPER
      )),
      "LeftSuper"
    );
  }

  #[test]
  fn print_key_output_parses_back() {
    let keys = [
      Key::from(KeyCode::Backspace),
      Key::from(KeyCode::PageUp),
      Key::from(KeyCode::PageDown),
      Key::from(KeyCode::Insert),
      Key::from(KeyCode::Null),
      Key::new(KeyCode::Char('a'), KeyMods::CONTROL | KeyMods::ALT),
      Key::from(KeyCode::CapsLock),
      Key::from(KeyCode::Media(MediaKeyCode::PlayPause)),
      Key::from(KeyCode::Modifier(ModKeyCode::LeftSuper)),
    ];

    for key in keys {
      let text = format!("<{}>", print_key(&key));
      assert_eq!(Key::parse(&text).unwrap(), key);
    }
  }
}

/*
 * Mouse
 */

pub fn encode_mouse_event(mev: MouseEvent) -> String {
  let mut buf = String::new();
  buf.push_str("\x1b[<");

  match mev.kind {
    MouseEventKind::Down(btn) | MouseEventKind::Up(btn) => match btn {
      MouseButton::Left => buf.push('0'),
      MouseButton::Right => buf.push('1'),
      MouseButton::Middle => buf.push('2'),
    },
    MouseEventKind::Drag(btn) => match btn {
      MouseButton::Left => buf.push_str("32"),
      MouseButton::Right => buf.push_str("33"),
      MouseButton::Middle => buf.push_str("34"),
    },
    MouseEventKind::Moved => buf.push_str("35"),
    MouseEventKind::ScrollUp => buf.push_str("64"),
    MouseEventKind::ScrollDown => buf.push_str("65"),
    MouseEventKind::ScrollLeft => buf.push_str("66"),
    MouseEventKind::ScrollRight => buf.push_str("67"),
  }
  buf.push(';');
  buf.push_str((mev.x + 1).to_string().as_str());
  buf.push(';');
  buf.push_str((mev.y + 1).to_string().as_str());

  buf.push(match mev.kind {
    MouseEventKind::Down(_) => 'M',
    MouseEventKind::Up(_) => 'm',
    MouseEventKind::Drag(_) => 'M',
    MouseEventKind::Moved => 'M',
    MouseEventKind::ScrollDown => 'M',
    MouseEventKind::ScrollUp => 'M',
    MouseEventKind::ScrollLeft => 'M',
    MouseEventKind::ScrollRight => 'M',
  });

  buf
}
