use std::io::Write;

use crossterm::{
  cursor::MoveTo,
  queue,
  style::{Color, Print, ResetColor, SetForegroundColor},
};
use rand::Rng;

use crate::box_chrome;
use crate::select_option::SelectOption;
use crate::{Component, Renderer};

use super::layout::{
  MAX_VISIBLE_OPTIONS, SelectorLayout, render_partial_border, render_selector_items,
  selector_border,
};

/// Don't start chrome animation until the question is at least this far revealed.
const QUESTION_REVEAL_GATE: f64 = 0.5;

/// Vertical list navigation (↑/↓); map from the host wizard's up/down actions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ListNavigate {
  Up,
  Down,
}

/// Select list with scroll, open/reveal animation progress, and bordered layout.
#[derive(Debug, Clone)]
pub struct SelectorComponent {
  pub options: Vec<SelectOption>,
  pub selected: usize,
  pub scroll_offset: usize,
  pub visible: bool,
  pub open: f64,
  pub reveal: f64,
}

impl SelectorComponent {
  pub fn hidden() -> Self {
    Self {
      options: Vec::new(),
      selected: 0,
      scroll_offset: 0,
      visible: false,
      open: 0.0,
      reveal: 0.0,
    }
  }

  pub fn from_options(options: Vec<SelectOption>, default_index: usize) -> Self {
    Self {
      options,
      selected: default_index,
      scroll_offset: 0,
      visible: true,
      open: 0.0,
      reveal: 0.0,
    }
  }

  pub fn is_fully_open(&self) -> bool {
    self.open >= 1.0
  }

  pub fn is_fully_closed(&self) -> bool {
    self.open <= 0.0
  }

  pub fn is_fully_revealed(&self) -> bool {
    self.reveal >= 1.0
  }

  pub fn is_fully_hidden(&self) -> bool {
    self.reveal <= 0.0
  }

  /// How many option rows are visible for a list of this length.
  pub fn viewport_row_count(option_count: usize) -> usize {
    option_count.min(MAX_VISIBLE_OPTIONS)
  }

  pub fn navigate(&mut self, direction: ListNavigate) {
    let len = self.options.len();
    if len == 0 {
      return;
    }

    match direction {
      ListNavigate::Up => {
        self.selected = if self.selected == 0 {
          len - 1
        } else {
          self.selected - 1
        };
      }
      ListNavigate::Down => {
        self.selected = (self.selected + 1) % len;
      }
    }

    let max_visible = Self::viewport_row_count(len);
    if self.selected < self.scroll_offset {
      self.scroll_offset = self.selected;
    } else if self.selected >= self.scroll_offset + max_visible {
      self.scroll_offset = self.selected + 1 - max_visible;
    }

    if self.selected == 0 {
      self.scroll_offset = 0;
    } else if self.selected == len - 1 {
      self.scroll_offset = len.saturating_sub(max_visible);
    }
  }

  pub fn tick_decode(&mut self, question_reveal: f64, box_open_speed: f64, reveal_speed: f64) {
    if !self.visible || question_reveal < QUESTION_REVEAL_GATE {
      return;
    }
    self.open = (self.open + box_open_speed).min(1.0);
    if self.open >= 1.0 {
      self.reveal = (self.reveal + reveal_speed).min(1.0);
    }
  }

  pub fn tick_transition_encode(
    &mut self,
    next_has_selector: bool,
    encode_speed: f64,
    morph_speed: f64,
  ) {
    if !self.visible {
      return;
    }
    self.reveal = (self.reveal - encode_speed).max(0.0);
    if !next_has_selector && self.is_fully_hidden() {
      self.open = (self.open - morph_speed).max(0.0);
    }
  }

  pub fn tick_transition_decode(&mut self, encode_speed: f64, morph_speed: f64) {
    if !self.visible {
      return;
    }
    if self.open < 1.0 {
      self.open = (self.open + morph_speed).min(1.0);
    } else {
      self.reveal = (self.reveal + encode_speed).min(1.0);
    }
  }

  pub fn tick_exit_close(&mut self, encode_speed: f64, box_close_speed: f64) {
    if !self.visible {
      return;
    }
    self.reveal = (self.reveal - encode_speed).max(0.0);
    if self.reveal <= 0.0 {
      self.open = (self.open - box_close_speed).max(0.0);
    }
  }
}

#[cfg(not(tarpaulin_include))]
impl SelectorComponent {
  fn render_fully_open<W: Write, R: Rng>(
    &self,
    writer: &mut W,
    layout: &SelectorLayout,
    start_row: u16,
    rng: &mut R,
  ) -> std::io::Result<u16> {
    let has_scroll_up = self.scroll_offset > 0;
    let has_scroll_down = self.scroll_offset + layout.max_visible < self.options.len();
    let mut row = start_row;

    let top = selector_border(layout.inner_width, has_scroll_up, true);
    queue!(
      writer,
      MoveTo(layout.col, row),
      SetForegroundColor(Color::DarkGrey),
      Print(&top),
      ResetColor
    )?;
    row += 1;

    render_selector_items(
      writer,
      self,
      layout,
      row,
      has_scroll_up,
      has_scroll_down,
      rng,
    )?;
    row += layout.max_visible as u16;

    let bottom = selector_border(layout.inner_width, has_scroll_down, false);
    queue!(
      writer,
      MoveTo(layout.col, row),
      SetForegroundColor(Color::DarkGrey),
      Print(&bottom),
      ResetColor
    )?;

    Ok((layout.max_visible as u16).saturating_add(box_chrome::BORDER_ROWS))
  }
}

#[cfg(not(tarpaulin_include))]
impl Component for SelectorComponent {
  fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
    if self.options.is_empty() {
      return Ok(0);
    }
    renderer.with_panel(|writer, panel, rng| {
      let layout = SelectorLayout::compute(self, panel.width);

      if self.open < 1.0 {
        return render_partial_border(writer, self.open, &layout, panel.row).map(|n| n as u16);
      }

      self.render_fully_open(writer, &layout, panel.row, rng)
    })
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::test_helpers::select_option as option;

  #[test]
  fn hidden_is_invisible_with_empty_options() {
    let s = SelectorComponent::hidden();
    assert!(!s.visible);
    assert!(s.options.is_empty());
    assert_eq!(s.open, 0.0);
    assert_eq!(s.reveal, 0.0);
  }

  #[test]
  fn tick_decode_noop_when_invisible() {
    let mut s = SelectorComponent::hidden();
    s.tick_decode(1.0, 0.1, 0.1);
    assert_eq!(s.open, 0.0);
  }

  #[test]
  fn tick_decode_noop_below_question_gate() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.tick_decode(0.49, 0.1, 0.1);
    assert_eq!(s.open, 0.0);
  }

  #[test]
  fn tick_decode_opens_then_reveals() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);

    s.tick_decode(0.6, 0.5, 0.3);
    assert!(s.open > 0.0);
    assert_eq!(s.reveal, 0.0);

    s.open = 1.0;
    s.tick_decode(0.6, 0.5, 0.3);
    assert!(s.reveal > 0.0);
  }

  #[test]
  fn tick_decode_clamps_at_one() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 1.0;
    s.tick_decode(1.0, 1.0, 2.0);
    assert_eq!(s.open, 1.0);
    assert_eq!(s.reveal, 1.0);
  }

  #[test]
  fn tick_transition_encode_noop_when_invisible() {
    let mut s = SelectorComponent::hidden();
    s.tick_transition_encode(true, 0.1, 0.1);
    assert_eq!(s.reveal, 0.0);
  }

  #[test]
  fn tick_transition_encode_fades_reveal_then_closes() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 1.0;
    s.reveal = 1.0;

    s.tick_transition_encode(false, 0.5, 0.0);
    assert!(s.reveal < 1.0);

    s.reveal = 0.0;
    s.tick_transition_encode(false, 0.5, 0.4);
    assert!(s.open < 1.0);
  }

  #[test]
  fn tick_transition_encode_keeps_box_when_next_has_selector() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 1.0;
    s.reveal = 0.0;

    s.tick_transition_encode(true, 0.5, 0.5);
    assert_eq!(s.open, 1.0);
  }

  #[test]
  fn tick_transition_decode_noop_when_invisible() {
    let mut s = SelectorComponent::hidden();
    s.tick_transition_decode(0.1, 0.1);
    assert_eq!(s.open, 0.0);
  }

  #[test]
  fn tick_transition_decode_opens_then_reveals() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 0.0;

    s.tick_transition_decode(0.3, 0.5);
    assert!(s.open > 0.0);
    assert_eq!(s.reveal, 0.0);

    s.open = 1.0;
    s.tick_transition_decode(0.3, 0.5);
    assert!(s.reveal > 0.0);
  }

  #[test]
  fn tick_exit_close_noop_when_invisible() {
    let mut s = SelectorComponent::hidden();
    s.tick_exit_close(0.1, 0.1);
    assert_eq!(s.open, 0.0);
  }

  #[test]
  fn tick_exit_close_fades_reveal_then_closes_box() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 1.0;
    s.reveal = 1.0;

    s.tick_exit_close(0.5, 0.5);
    assert!(s.reveal < 1.0);
    assert_eq!(s.open, 1.0);

    s.reveal = 0.0;
    s.tick_exit_close(0.5, 0.5);
    assert!(s.open < 1.0);
  }

  #[test]
  fn viewport_row_count_matches_render_cap() {
    assert_eq!(SelectorComponent::viewport_row_count(0), 0);
    assert_eq!(SelectorComponent::viewport_row_count(5), 5);
    assert_eq!(SelectorComponent::viewport_row_count(8), 8);
    assert_eq!(
      SelectorComponent::viewport_row_count(200),
      MAX_VISIBLE_OPTIONS
    );
  }

  #[test]
  fn is_fully_thresholds() {
    let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
    s.open = 1.0;
    assert!(s.is_fully_open());
    s.open = 0.999;
    assert!(!s.is_fully_open());
    s.open = 0.0;
    assert!(s.is_fully_closed());
    s.open = -0.1;
    assert!(s.is_fully_closed());

    s.reveal = 1.0;
    assert!(s.is_fully_revealed());
    s.reveal = 0.999;
    assert!(!s.is_fully_revealed());
    s.reveal = 0.0;
    assert!(s.is_fully_hidden());
    s.reveal = 0.01;
    assert!(!s.is_fully_hidden());
  }

  #[test]
  fn navigate_down_increments_selection() {
    let mut sel = SelectorComponent::from_options(
      vec![option("A", "a"), option("B", "b"), option("C", "c")],
      0,
    );
    sel.navigate(ListNavigate::Down);
    assert_eq!(sel.selected, 1);
  }

  #[test]
  fn navigate_down_wraps_to_first() {
    let mut sel = SelectorComponent::from_options(vec![option("A", "a"), option("B", "b")], 1);
    sel.navigate(ListNavigate::Down);
    assert_eq!(sel.selected, 0);
  }

  #[test]
  fn navigate_up_decrements_selection() {
    let mut sel = SelectorComponent::from_options(
      vec![option("A", "a"), option("B", "b"), option("C", "c")],
      2,
    );
    sel.navigate(ListNavigate::Up);
    assert_eq!(sel.selected, 1);
  }

  #[test]
  fn navigate_up_wraps_to_last() {
    let mut sel = SelectorComponent::from_options(
      vec![option("A", "a"), option("B", "b"), option("C", "c")],
      0,
    );
    sel.navigate(ListNavigate::Up);
    assert_eq!(sel.selected, 2);
  }

  #[test]
  fn navigate_empty_options_is_noop() {
    let mut sel = SelectorComponent::hidden();
    sel.navigate(ListNavigate::Down);
    assert_eq!(sel.selected, 0);
  }
}
