use std::io::Write;
use std::time::Duration;

use crossterm::{
  cursor::{Hide, MoveTo},
  queue,
  style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
  terminal::{Clear, ClearType},
};
use rand::rngs::ThreadRng;

use crate::animation::{bobbing_frame, bobbing_origin};
use crate::input_buffer::{self, InputBuffer};
use crate::layout::{self, Size, centered_block_origin, centered_column, top_two_thirds};
use crate::mascot::{self, MascotFrames, queue_frame_owned};
use crate::mascot_fx::{self, DissolveState, DistanceMap};
use crate::prompt::{self, InputBox, InputBoxLayout, input_box, input_box_layout};

use super::ui_controller::{
  BoxElement, HintElement, MascotElement, SelectorElement, TextElement, UiController,
};

// ── Constants ──────────────────────────────────────────────────────────────────

const SELECTOR_PREFIX_ACTIVE: &str = "\u{25b8} ";
const SELECTOR_PREFIX_INACTIVE: &str = "  ";
const SELECTOR_ARROW_UP: char = '\u{25b2}';
const SELECTOR_ARROW_DOWN: char = '\u{25bc}';
const SELECTOR_ELLIPSIS: &str = " \u{00b7}\u{00b7}\u{00b7} ";
const BORDER_VERTICAL: &str = "\u{2502}";
const BORDER_HORIZONTAL: &str = "\u{2500}";
const CORNER_TOP_LEFT: &str = "\u{256d}";
const CORNER_TOP_RIGHT: &str = "\u{256e}";
const CORNER_BOTTOM_LEFT: &str = "\u{2570}";
const CORNER_BOTTOM_RIGHT: &str = "\u{256f}";
const BORDER_TOP_OFFSET: u16 = 1;

pub(crate) const MAX_VISIBLE_OPTIONS: usize = 8;

// ── Render context ─────────────────────────────────────────────────────────────

pub struct RenderContext {
  pub mascot: MascotFrames,
  pub terminal_size: Size,
  pub elapsed: Duration,
  pub distance_map: DistanceMap,
}

struct SelectorLayout {
  tw: u16,
  inner_width: u16,
  outer_width: u16,
  col: u16,
  max_visible: usize,
}

impl SelectorLayout {
  fn compute(sel: &SelectorElement, tw: u16) -> Self {
    let total = sel.options.len();
    let max_visible = total.min(MAX_VISIBLE_OPTIONS);
    let inner_width = selector_inner_width(sel, tw);
    let outer_width = inner_width + 4;
    let col = centered_column(tw, outer_width);
    Self {
      tw,
      inner_width,
      outer_width,
      col,
      max_visible,
    }
  }
}

// ── Top-level render ───────────────────────────────────────────────────────────

#[cfg(not(tarpaulin_include))]
#[allow(clippy::too_many_arguments)]
pub fn render<W: Write>(
  writer: &mut W,
  ui: &UiController,
  ctx: &RenderContext,
  rng: &mut ThreadRng,
  dissolve_state: Option<&DissolveState>,
  entrance_tick: usize,
  dissolve_tick: usize,
  input_buf: &InputBuffer,
) -> std::io::Result<()> {
  queue!(writer, Hide, Clear(ClearType::All))?;

  render_mascot(
    writer,
    &ui.mascot,
    ctx,
    rng,
    dissolve_state,
    entrance_tick,
    dissolve_tick,
  )?;
  render_prompt_layer(writer, ui, ctx.terminal_size, rng, input_buf)?;

  Ok(())
}

#[cfg(not(tarpaulin_include))]
#[allow(clippy::too_many_arguments)]
fn render_mascot<W: Write>(
  writer: &mut W,
  mascot: &MascotElement,
  ctx: &RenderContext,
  rng: &mut ThreadRng,
  dissolve_state: Option<&DissolveState>,
  entrance_tick: usize,
  dissolve_tick: usize,
) -> std::io::Result<()> {
  if mascot.entrance <= 0.0 && mascot.dissolve <= 0.0 {
    return Ok(());
  }

  if mascot.dissolve >= 1.0 {
    return Ok(());
  }

  let region = top_two_thirds(ctx.terminal_size);
  let origin = if mascot.floating {
    let frame = bobbing_frame(ctx.elapsed);
    bobbing_origin(region, ctx.mascot.size(), frame.row_offset)
  } else {
    centered_block_origin(region, ctx.mascot.size())
  };

  let lines = ctx.mascot.lines();
  if mascot.dissolve > 0.0
    && let Some(state) = dissolve_state
  {
    let frame = mascot_fx::dissolve_frame(lines, state, dissolve_tick, rng);
    return queue_frame_owned(writer, origin, &frame);
  }

  if mascot.entrance < 1.0 {
    let frame = mascot_fx::entrance_frame(lines, &ctx.distance_map, entrance_tick, rng);
    return queue_frame_owned(writer, origin, &frame);
  }

  mascot::queue_frame(writer, origin, lines)
}

#[cfg(not(tarpaulin_include))]
fn render_prompt_layer<W: Write>(
  writer: &mut W,
  ui: &UiController,
  terminal_size: Size,
  rng: &mut ThreadRng,
  input_buf: &InputBuffer,
) -> std::io::Result<()> {
  let tw = terminal_size.width;
  let ib = input_box(&ui.input_box.label, &ui.input_box.content, tw);
  let layout = input_box_layout(&ib, terminal_size);

  render_question(writer, &ui.question, tw, layout.question_row, rng)?;

  if ui.selector.visible {
    let selector_start = layout.box_top_row;
    let visible_count = render_selector(writer, &ui.selector, tw, selector_start, rng)?;
    let hint_row = selector_start
      .saturating_add(visible_count as u16)
      .saturating_add(1);
    render_hint(writer, &ui.hint, tw, hint_row)?;
    return Ok(());
  }

  let cursor_col = if ui.input_box.visible && ui.input_box.open > 0.0 {
    Some(render_box(
      writer,
      &ui.input_box,
      &layout,
      tw,
      input_buf,
      rng,
    )?)
  } else {
    None
  };

  render_hint(writer, &ui.hint, tw, layout.hint_row)?;

  let box_fully_ready = ui.input_box.visible
    && ui.input_box.open >= 1.0
    && ui.input_box.label_reveal >= 1.0
    && ui.input_box.content_reveal >= 1.0;

  if box_fully_ready && let Some(col) = cursor_col {
    prompt::queue_cursor(writer, &layout, col)?;
  }

  Ok(())
}

#[cfg(not(tarpaulin_include))]
fn render_question<W: Write>(
  writer: &mut W,
  question: &TextElement,
  tw: u16,
  row: u16,
  rng: &mut ThreadRng,
) -> std::io::Result<()> {
  if !question.visible || question.reveal <= 0.0 {
    return Ok(());
  }

  let text = &question.text;
  let display = if question.reveal >= 1.0 {
    text.clone()
  } else {
    let revealed = (text.chars().count() as f64 * question.reveal).round() as usize;
    crate::decode::decode_frame(text, revealed, rng)
  };

  let col = centered_column(tw, text.chars().count() as u16);
  queue!(
    writer,
    MoveTo(col, row),
    SetForegroundColor(Color::White),
    SetAttribute(Attribute::Bold),
    Print(&display),
    SetAttribute(Attribute::Reset),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
fn render_box<W: Write>(
  writer: &mut W,
  box_el: &BoxElement,
  layout: &InputBoxLayout,
  tw: u16,
  input_buf: &InputBuffer,
  rng: &mut ThreadRng,
) -> std::io::Result<u16> {
  if box_el.open < 1.0 {
    render_partial_border(writer, box_el.open, layout, tw)?;
    return Ok(0);
  }

  let label_display = if box_el.label_reveal >= 1.0 {
    box_el.label.clone()
  } else {
    let revealed = (box_el.label.chars().count() as f64 * box_el.label_reveal).round() as usize;
    crate::decode::decode_frame(&box_el.label, revealed, rng)
  };

  let content_display = if box_el.content_reveal >= 1.0 {
    box_el.content.clone()
  } else {
    let revealed = (box_el.content.chars().count() as f64 * box_el.content_reveal).round() as usize;
    crate::decode::decode_frame(&box_el.content, revealed, rng)
  };

  let frame_ib = input_box(&label_display, &content_display, tw);
  let label_width = label_display.chars().count() as u16;
  let buf_text = input_buffer::text(input_buf);
  let cursor_pos = input_buffer::cursor(input_buf);
  let selection = input_buffer::selection_range(input_buf);
  let cursor_col = prompt::queue_input_box_with_cursor(
    writer,
    layout,
    &frame_ib,
    buf_text,
    label_width,
    cursor_pos,
    selection,
  )?;
  Ok(cursor_col)
}

#[cfg(not(tarpaulin_include))]
fn render_partial_border<W: Write>(
  writer: &mut W,
  open_progress: f64,
  layout: &InputBoxLayout,
  tw: u16,
) -> std::io::Result<()> {
  let full_width = layout.inner_width;
  let raw = ((full_width as f64) * open_progress).round() as u16;
  let even = (raw / 2) * 2;
  let current_width = if even == 0 && raw > 0 {
    2
  } else {
    even.min(full_width)
  };

  if current_width == 0 {
    return prompt::queue_clear_prompt(writer, layout);
  }

  let partial_ib = InputBox {
    label: String::new(),
    hint: String::new(),
    inner_width: current_width,
  };
  let partial_layout = input_box_layout(&partial_ib, layout::size(tw, layout.question_row + 10));
  let merged = InputBoxLayout {
    question_row: layout.question_row,
    box_top_row: layout.box_top_row,
    content_row: layout.content_row,
    box_bottom_row: layout.box_bottom_row,
    hint_row: layout.hint_row,
    box_column: partial_layout.box_column,
    inner_width: current_width,
  };
  prompt::queue_input_box(writer, &merged, &partial_ib, "", 0)
}

#[cfg(not(tarpaulin_include))]
fn render_hint<W: Write>(
  writer: &mut W,
  hint: &HintElement,
  tw: u16,
  row: u16,
) -> std::io::Result<()> {
  if hint.fade <= 0.0 {
    return Ok(());
  }

  let color = hint_fade_color(hint.fade);
  let col = centered_column(tw, hint.text.chars().count() as u16);
  queue!(
    writer,
    MoveTo(col, row),
    SetForegroundColor(color),
    Print(hint.text),
    ResetColor
  )
}

// ── Selector rendering ─────────────────────────────────────────────────────────

fn selector_inner_width(sel: &SelectorElement, tw: u16) -> u16 {
  let max_label = sel
    .options
    .iter()
    .map(|o| o.label.chars().count())
    .max()
    .unwrap_or(0);
  let prefix_width = SELECTOR_PREFIX_ACTIVE.chars().count();
  let content_min = (prefix_width + max_label + 2) as u16;
  let input_box_width = prompt::input_box("", "", tw).inner_width;
  content_min.max(input_box_width)
}

fn selector_border(inner_width: u16, has_overflow: bool, top: bool) -> String {
  let fill_total = inner_width as usize + 2;
  let (left_corner, right_corner) = if top {
    (CORNER_TOP_LEFT, CORNER_TOP_RIGHT)
  } else {
    (CORNER_BOTTOM_LEFT, CORNER_BOTTOM_RIGHT)
  };

  if !has_overflow {
    let fill: String = BORDER_HORIZONTAL.repeat(fill_total);
    return format!("{left_corner}{fill}{right_corner}");
  }

  let text_col = SELECTOR_PREFIX_ACTIVE.chars().count() + 1;
  let leading_space = SELECTOR_ELLIPSIS.chars().take_while(|c| *c == ' ').count();
  let prefix_offset = text_col.saturating_sub(leading_space);
  let ellipsis_len = SELECTOR_ELLIPSIS.chars().count();
  let right_fill = fill_total.saturating_sub(prefix_offset + ellipsis_len);
  format!(
    "{left_corner}{}{SELECTOR_ELLIPSIS}{}{}",
    BORDER_HORIZONTAL.repeat(prefix_offset),
    BORDER_HORIZONTAL.repeat(right_fill),
    right_corner,
  )
}

#[cfg(not(tarpaulin_include))]
fn render_selector<W: Write>(
  writer: &mut W,
  sel: &SelectorElement,
  tw: u16,
  start_row: u16,
  rng: &mut ThreadRng,
) -> std::io::Result<usize> {
  if sel.options.is_empty() {
    return Ok(0);
  }

  let layout = SelectorLayout::compute(sel, tw);

  if sel.open < 1.0 {
    return render_selector_partial_border(writer, sel.open, &layout, start_row);
  }

  let scroll_offset = sel.scroll_offset;
  let has_scroll_up = scroll_offset > 0;
  let has_scroll_down = scroll_offset + layout.max_visible < sel.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,
    sel,
    &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 + 2)
}

#[cfg(not(tarpaulin_include))]
fn render_selector_partial_border<W: Write>(
  writer: &mut W,
  open_progress: f64,
  layout: &SelectorLayout,
  start_row: u16,
) -> std::io::Result<usize> {
  let raw = ((layout.inner_width as f64) * open_progress).round() as u16;
  let even = (raw / 2) * 2;
  let current_width = if even == 0 && raw > 0 {
    2
  } else {
    even.min(layout.inner_width)
  };

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

  let partial_outer = current_width + 4;
  let col = centered_column(layout.tw, partial_outer);

  let top = selector_border(current_width, false, true);
  let bottom = selector_border(current_width, false, false);
  let empty_inner: String = " ".repeat(current_width as usize + 2);

  queue!(
    writer,
    MoveTo(col, start_row),
    SetForegroundColor(Color::DarkGrey),
    Print(&top),
    ResetColor
  )?;

  for i in 0..layout.max_visible {
    let row = start_row + BORDER_TOP_OFFSET + i as u16;
    queue!(
      writer,
      MoveTo(col, row),
      SetForegroundColor(Color::DarkGrey),
      Print(BORDER_VERTICAL),
      Print(&empty_inner),
      Print(BORDER_VERTICAL),
      ResetColor
    )?;
  }

  let bottom_row = start_row + BORDER_TOP_OFFSET + layout.max_visible as u16;
  queue!(
    writer,
    MoveTo(col, bottom_row),
    SetForegroundColor(Color::DarkGrey),
    Print(&bottom),
    ResetColor
  )?;

  Ok(layout.max_visible + 2)
}

#[cfg(not(tarpaulin_include))]
fn render_selector_items<W: Write>(
  writer: &mut W,
  sel: &SelectorElement,
  layout: &SelectorLayout,
  start_row: u16,
  has_scroll_up: bool,
  has_scroll_down: bool,
  rng: &mut ThreadRng,
) -> std::io::Result<()> {
  let max_label_width = sel
    .options
    .iter()
    .map(|o| o.label.chars().count())
    .max()
    .unwrap_or(0);
  let pad_width = (layout.inner_width as usize)
    .saturating_sub(SELECTOR_PREFIX_ACTIVE.chars().count() + max_label_width + 2);

  for i in 0..layout.max_visible {
    let opt_idx = sel.scroll_offset + i;
    if opt_idx >= sel.options.len() {
      break;
    }
    render_selector_item(
      writer,
      sel,
      layout,
      start_row + i as u16,
      opt_idx,
      max_label_width,
      pad_width,
      scroll_arrow_for(i, layout.max_visible, has_scroll_up, has_scroll_down),
      rng,
    )?;
  }

  Ok(())
}

#[cfg(not(tarpaulin_include))]
#[allow(clippy::too_many_arguments)]
fn render_selector_item<W: Write>(
  writer: &mut W,
  sel: &SelectorElement,
  layout: &SelectorLayout,
  row: u16,
  opt_idx: usize,
  max_label_width: usize,
  pad_width: usize,
  arrow_char: char,
  rng: &mut ThreadRng,
) -> std::io::Result<()> {
  let opt = &sel.options[opt_idx];
  let is_selected = opt_idx == sel.selected;

  let label = if sel.reveal < 1.0 {
    let revealed = (opt.label.chars().count() as f64 * sel.reveal).round() as usize;
    crate::decode::decode_frame(&opt.label, revealed, rng)
  } else {
    opt.label.clone()
  };

  let prefix = if is_selected {
    SELECTOR_PREFIX_ACTIVE
  } else {
    SELECTOR_PREFIX_INACTIVE
  };

  let padded_label = format!("{:<width$}", label, width = max_label_width);

  queue!(
    writer,
    MoveTo(layout.col, row),
    SetForegroundColor(Color::DarkGrey),
    Print(BORDER_VERTICAL),
    Print(" "),
  )?;

  if is_selected {
    queue!(
      writer,
      SetForegroundColor(Color::White),
      SetAttribute(Attribute::Bold),
      Print(prefix),
      Print(&padded_label),
      SetAttribute(Attribute::Reset),
    )?;
  } else {
    queue!(
      writer,
      SetForegroundColor(Color::DarkGrey),
      Print(prefix),
      Print(&padded_label),
    )?;
  }

  let gap: String = " ".repeat(pad_width + 1);
  queue!(
    writer,
    SetForegroundColor(Color::DarkGrey),
    Print(format!("{gap}{arrow_char}")),
    Print(format!(" {BORDER_VERTICAL}")),
    ResetColor
  )
}

fn scroll_arrow_for(
  i: usize,
  max_visible: usize,
  has_scroll_up: bool,
  has_scroll_down: bool,
) -> char {
  if i == 0 && has_scroll_up {
    SELECTOR_ARROW_UP
  } else if i == max_visible - 1 && has_scroll_down {
    SELECTOR_ARROW_DOWN
  } else {
    ' '
  }
}

// ── Helpers ────────────────────────────────────────────────────────────────────

fn hint_fade_color(fade: f64) -> Color {
  if fade >= 1.0 {
    return Color::DarkGrey;
  }
  Color::AnsiValue(232 + (fade * 15.0).round() as u8)
}
