use std::borrow::Cow;
use std::io::Write;

use crate::box_chrome;
use crate::decode;
use crate::input_buffer::{self, InputBuffer};
use crate::layout::{Size, centered_column};
use crate::prompt::{
  TextEntry, TextEntryLayout, bottom_border, queue_box_border, queue_box_border_plain,
  queue_box_content, queue_box_content_with_cursor, text_entry, text_entry_layout, visible_window,
};
use crate::{Component, Renderer};

const MIN_PARTIAL_WIDTH: u16 = 2;
/// Don't start chrome animation until the question is at least this far revealed.
const QUESTION_REVEAL_GATE: f64 = 0.5;
/// Rows consumed by a fully rendered text entry box (top border + content + bottom border).
const BOX_ROW_COUNT: u16 = 3;

#[derive(Debug, Clone)]
pub struct TextEntryComponent {
  pub label: String,
  pub content: String,
  pub open: f64,
  pub label_reveal: f64,
  pub content_reveal: f64,
  pub visible: bool,
  input_buf: InputBuffer,
  cached_layout: Option<TextEntryLayout>,
}

impl TextEntryComponent {
  /// Text entry chrome for an env-var style prompt (`label` = variable name, `content` = default).
  pub fn prompt(label: impl Into<String>, default_value: impl Into<String>) -> Self {
    Self {
      label: label.into(),
      content: default_value.into(),
      open: 0.0,
      label_reveal: 0.0,
      content_reveal: 0.0,
      visible: true,
      input_buf: input_buffer::new(),
      cached_layout: None,
    }
  }

  /// Invisible shell when the step is dialog-only or selector-only (no editable field).
  pub fn hidden() -> Self {
    Self {
      label: String::new(),
      content: String::new(),
      open: 0.0,
      label_reveal: 0.0,
      content_reveal: 0.0,
      visible: false,
      input_buf: input_buffer::new(),
      cached_layout: None,
    }
  }

  pub fn input_buf(&self) -> &InputBuffer {
    &self.input_buf
  }

  pub fn input_buf_mut(&mut self) -> &mut InputBuffer {
    &mut self.input_buf
  }

  pub fn reset_input_buf(&mut self) {
    self.input_buf = input_buffer::new();
  }

  /// Compute and cache the text-entry layout for the given terminal size.
  pub fn compute_layout(&mut self, terminal_size: Size) {
    let dims = text_entry(&self.label, &self.content, terminal_size.width);
    self.cached_layout = Some(text_entry_layout(&dims, terminal_size));
  }

  pub fn layout(&self) -> Option<&TextEntryLayout> {
    self.cached_layout.as_ref()
  }

  /// Compute cursor column for the active input buffer, or `None` if the
  /// box isn't fully open yet.
  pub fn cursor_column(&self, terminal_width: u16) -> Option<u16> {
    if !self.visible || self.open < 1.0 {
      return None;
    }
    let frame = text_entry(&self.label, &self.content, terminal_width);
    let buf_text = input_buffer::text(&self.input_buf);
    let cursor_pos = input_buffer::cursor(&self.input_buf);
    let selection = input_buffer::selection_range(&self.input_buf);
    let window = visible_window(&frame, buf_text, cursor_pos, selection);
    Some(window.cursor_col as u16)
  }

  /// Wizard decode phase: grows the chrome and reveals labels after the question is half-shown.
  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.label_reveal = (self.label_reveal + reveal_speed).min(1.0);
      self.content_reveal = (self.content_reveal + reveal_speed).min(1.0);
    }
  }

  /// Transition encode: scrub label/content; optionally shrink the border when leaving prompt steps.
  pub fn tick_transition_encode(
    &mut self,
    next_has_prompt_box: bool,
    encode_speed: f64,
    morph_speed: f64,
  ) {
    if !self.visible {
      return;
    }
    self.label_reveal = (self.label_reveal - encode_speed).max(0.0);
    self.content_reveal = (self.content_reveal - encode_speed).max(0.0);
    let text_hidden = self.label_reveal <= 0.0 && self.content_reveal <= 0.0;
    if !next_has_prompt_box && text_hidden {
      self.open = (self.open - morph_speed).max(0.0);
    }
  }

  /// Transition decode after `swap_to`: open chrome then fade labels/content in.
  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.label_reveal = (self.label_reveal + encode_speed).min(1.0);
      self.content_reveal = (self.content_reveal + encode_speed).min(1.0);
    }
  }

  /// True when the chrome is fully open and all text is fully revealed.
  pub fn is_fully_ready(&self) -> bool {
    self.visible && self.open >= 1.0 && self.label_reveal >= 1.0 && self.content_reveal >= 1.0
  }

  /// Exit phase: encode away text then close chrome.
  pub fn tick_exit_close(&mut self, encode_speed: f64, box_close_speed: f64) {
    if !self.visible {
      return;
    }
    self.label_reveal = (self.label_reveal - encode_speed).max(0.0);
    self.content_reveal = (self.content_reveal - encode_speed).max(0.0);
    if self.label_reveal <= 0.0 && self.content_reveal <= 0.0 {
      self.open = (self.open - box_close_speed).max(0.0);
    }
  }

  #[cfg(not(tarpaulin_include))]
  fn render_partial_open<W: Write>(
    &self,
    writer: &mut W,
    full_width: u16,
    top_row: u16,
    terminal_width: u16,
  ) -> std::io::Result<u16> {
    let raw = ((full_width as f64) * self.open).round() as u16;
    let current_width = box_chrome::snap_even(raw, MIN_PARTIAL_WIDTH).min(full_width);

    if current_width == 0 {
      return Ok(0);
    }

    let outer_width = current_width + box_chrome::CHROME_WIDTH;
    let col = centered_column(terminal_width, outer_width);
    let empty_content = " ".repeat(current_width as usize);
    let bottom = bottom_border(current_width);

    queue_box_border(writer, "", current_width, 0, col, top_row)?;
    queue_box_content(writer, &empty_content, false, col, top_row + 1)?;
    queue_box_border_plain(writer, &bottom, col, top_row + 2)?;
    Ok(BOX_ROW_COUNT)
  }
}

#[cfg(not(tarpaulin_include))]
impl TextEntryComponent {
  fn decode_text<'a, R: rand::Rng>(text: &'a str, reveal: f64, rng: &mut R) -> Cow<'a, str> {
    if reveal >= 1.0 {
      Cow::Borrowed(text)
    } else {
      let revealed = (text.chars().count() as f64 * reveal).round() as usize;
      Cow::Owned(decode::decode_frame(text, revealed, rng))
    }
  }

  fn render_fully_open<W: Write, R: rand::Rng>(
    &self,
    writer: &mut W,
    panel: crate::LayoutPanel,
    rng: &mut R,
  ) -> std::io::Result<u16> {
    let label_display = Self::decode_text(&self.label, self.label_reveal, rng);
    let content_display = Self::decode_text(&self.content, self.content_reveal, rng);

    let frame = TextEntry {
      label: String::new(),
      hint: content_display.into_owned(),
      inner_width: panel.width,
    };
    let label_width = label_display.chars().count() as u16;
    let buf_text = input_buffer::text(&self.input_buf);
    let cursor_pos = input_buffer::cursor(&self.input_buf);
    let selection = input_buffer::selection_range(&self.input_buf);
    let window = visible_window(&frame, buf_text, cursor_pos, selection);
    let is_hint = buf_text.is_empty();
    let bottom = bottom_border(panel.width);

    queue_box_border(
      writer,
      &label_display,
      panel.width,
      label_width,
      panel.column,
      panel.row,
    )?;
    queue_box_content_with_cursor(writer, &window, is_hint, panel.column, panel.row + 1)?;
    queue_box_border_plain(writer, &bottom, panel.column, panel.row + 2)?;
    Ok(BOX_ROW_COUNT)
  }
}

#[cfg(not(tarpaulin_include))]
impl Component for TextEntryComponent {
  fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
    if !self.visible || self.open <= 0.0 {
      return Ok(0);
    }
    let terminal_width = renderer.ctx().terminal_size.width;

    renderer.with_panel(|writer, panel, rng| {
      if self.open < 1.0 {
        return self.render_partial_open(writer, panel.width, panel.row, terminal_width);
      }
      self.render_fully_open(writer, panel, rng)
    })
  }
}

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

  #[test]
  fn hidden_is_invisible_with_zero_progress() {
    let te = TextEntryComponent::hidden();
    assert!(!te.visible);
    assert_eq!(te.open, 0.0);
    assert_eq!(te.label_reveal, 0.0);
    assert_eq!(te.content_reveal, 0.0);
    assert!(te.label.is_empty());
    assert!(te.content.is_empty());
  }

  #[test]
  fn prompt_starts_visible_with_zero_progress() {
    let te = TextEntryComponent::prompt("NAME", "default");
    assert!(te.visible);
    assert_eq!(te.open, 0.0);
    assert_eq!(te.label, "NAME");
    assert_eq!(te.content, "default");
  }

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

  #[test]
  fn tick_decode_noop_below_question_gate() {
    let mut te = TextEntryComponent::prompt("X", "");
    te.tick_decode(0.49, 0.1, 0.1);
    assert_eq!(te.open, 0.0);
  }

  #[test]
  fn tick_decode_opens_then_reveals() {
    let mut te = TextEntryComponent::prompt("X", "val");
    // Open phase
    te.tick_decode(0.6, 0.5, 0.3);
    assert!(te.open > 0.0);
    assert_eq!(
      te.label_reveal, 0.0,
      "label shouldn't reveal until fully open"
    );

    // Finish opening
    te.open = 1.0;
    te.tick_decode(0.6, 0.5, 0.3);
    assert!(te.label_reveal > 0.0);
    assert!(te.content_reveal > 0.0);
  }

  #[test]
  fn tick_decode_clamps_at_one() {
    let mut te = TextEntryComponent::prompt("X", "");
    te.open = 1.0;
    te.tick_decode(1.0, 1.0, 2.0);
    assert_eq!(te.open, 1.0);
    assert_eq!(te.label_reveal, 1.0);
    assert_eq!(te.content_reveal, 1.0);
  }

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

  #[test]
  fn tick_transition_encode_fades_text_then_closes() {
    let mut te = TextEntryComponent::prompt("X", "v");
    te.open = 1.0;
    te.label_reveal = 1.0;
    te.content_reveal = 1.0;

    // Encode: text fades
    te.tick_transition_encode(false, 0.5, 0.0);
    assert!(te.label_reveal < 1.0);
    assert!(te.content_reveal < 1.0);

    // Finish fading text
    te.label_reveal = 0.0;
    te.content_reveal = 0.0;

    // Now box closes since next step has no prompt box
    te.tick_transition_encode(false, 0.5, 0.4);
    assert!(te.open < 1.0);
  }

  #[test]
  fn tick_transition_encode_keeps_box_when_next_has_prompt() {
    let mut te = TextEntryComponent::prompt("X", "");
    te.open = 1.0;
    te.label_reveal = 0.0;
    te.content_reveal = 0.0;

    te.tick_transition_encode(true, 0.5, 0.5);
    assert_eq!(
      te.open, 1.0,
      "box stays open when next step has a prompt box"
    );
  }

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

  #[test]
  fn tick_transition_decode_opens_then_reveals() {
    let mut te = TextEntryComponent::prompt("X", "v");
    te.open = 0.0;

    // Opens first
    te.tick_transition_decode(0.3, 0.5);
    assert!(te.open > 0.0);
    assert_eq!(te.label_reveal, 0.0);

    // Finish opening
    te.open = 1.0;
    te.tick_transition_decode(0.3, 0.5);
    assert!(te.label_reveal > 0.0);
    assert!(te.content_reveal > 0.0);
  }

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

  #[test]
  fn tick_exit_close_fades_text_then_closes_box() {
    let mut te = TextEntryComponent::prompt("X", "v");
    te.open = 1.0;
    te.label_reveal = 1.0;
    te.content_reveal = 1.0;

    te.tick_exit_close(0.5, 0.5);
    assert!(te.label_reveal < 1.0);
    assert_eq!(te.open, 1.0, "box stays while text is visible");

    // Finish fading
    te.label_reveal = 0.0;
    te.content_reveal = 0.0;
    te.tick_exit_close(0.5, 0.5);
    assert!(te.open < 1.0);
  }

  #[test]
  fn is_fully_ready_requires_all_conditions() {
    let mut te = TextEntryComponent::prompt("X", "v");
    assert!(!te.is_fully_ready());

    te.open = 1.0;
    te.label_reveal = 1.0;
    te.content_reveal = 1.0;
    assert!(te.is_fully_ready());

    te.visible = false;
    assert!(!te.is_fully_ready());
  }

  #[test]
  fn cursor_column_none_when_not_ready() {
    let te = TextEntryComponent::prompt("X", "v");
    assert!(te.cursor_column(80).is_none());
  }

  #[test]
  fn cursor_column_some_when_fully_ready() {
    let mut te = TextEntryComponent::prompt("X", "v");
    te.open = 1.0;
    te.label_reveal = 1.0;
    te.content_reveal = 1.0;
    assert!(te.cursor_column(80).is_some());
  }

  #[test]
  fn compute_layout_caches_and_layout_retrieves() {
    let mut te = TextEntryComponent::prompt("X", "v");
    assert!(te.layout().is_none());

    let size = Size {
      width: 80,
      height: 24,
    };
    te.compute_layout(size);
    let layout = te.layout().expect("layout should be cached");
    assert!(layout.inner_width > 0);
  }

  #[test]
  fn input_buf_accessors() {
    let mut te = TextEntryComponent::prompt("X", "");
    assert!(input_buffer::text(te.input_buf()).is_empty());
    input_buffer::insert_char(te.input_buf_mut(), 'a');
    assert_eq!(input_buffer::text(te.input_buf()), "a");
    te.reset_input_buf();
    assert!(input_buffer::text(te.input_buf()).is_empty());
  }
}
