//! User input facilities for the application.
//!
//! Modified from the [`User Input`](https://ratatui.rs/examples/apps/user_input/) application example.

use std::borrow::Cow;

use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEventKind};
use ratatui::layout::{Constraint, Layout, Position};
use ratatui::prelude::{Buffer, Rect};
use ratatui::style::{Color, Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::{Block, Paragraph, Widget};
use ratatui::{DefaultTerminal, Frame};
use zeroize::Zeroize;

use crate::{App, AppEvent, Event, EventHandler, Result, field_access};

mod state;

pub use state::{InputMode, InputState, InputVisibility};

/// Represents a user input field.
#[derive(Clone, Debug, Eq, PartialEq, Zeroize)]
pub struct InputField {
    input: String,
    character_index: usize,
    state: InputState,
    title: String,
}

impl InputField {
    /// Creates a new [InputField].
    pub const fn new() -> Self {
        Self {
            input: String::new(),
            character_index: 0,
            state: InputState::new(),
            title: String::new(),
        }
    }

    /// Creates a new hidden [InputField].
    pub fn new_hidden() -> Self {
        Self {
            input: String::new(),
            character_index: 0,
            state: InputState::new().with_visibility(InputVisibility::Hide),
            title: String::new(),
        }
    }

    /// Creates a  new [InputField] with the provided title.
    pub fn create<'c, C: Into<Cow<'c, str>>>(title: C) -> Self {
        Self {
            input: String::new(),
            character_index: 0,
            state: InputState::new(),
            title: C::into(title).into(),
        }
    }

    /// Creates a  new [InputField] with the provided title.
    pub fn create_hidden<'c, C: Into<Cow<'c, str>>>(title: C) -> Self {
        Self {
            input: String::new(),
            character_index: 0,
            state: InputState::new().with_visibility(InputVisibility::Hide),
            title: C::into(title).into(),
        }
    }

    /// Gets a mutable reference to the input state.
    pub fn state_mut(&mut self) -> &mut InputState {
        &mut self.state
    }

    pub(crate) fn move_cursor_left(&mut self) {
        let cursor_moved_left = self.character_index.saturating_sub(1);
        self.character_index = self.clamp_cursor(cursor_moved_left);
    }

    pub(crate) fn move_cursor_right(&mut self) {
        let cursor_moved_right = self.character_index.saturating_add(1);
        self.character_index = self.clamp_cursor(cursor_moved_right);
    }

    pub(crate) fn enter_char(&mut self, new_char: char) {
        let index = self.byte_index();
        self.input.insert(index, new_char);
        self.move_cursor_right();
    }

    /// Returns the byte index based on the character position.
    ///
    /// Since each character in a string can be contain multiple bytes, it's necessary to calculate
    /// the byte index based on the index of the character.
    fn byte_index(&self) -> usize {
        self.input
            .char_indices()
            .map(|(i, _)| i)
            .nth(self.character_index)
            .unwrap_or(self.input.len())
    }

    pub(crate) fn delete_char(&mut self) {
        let is_not_cursor_leftmost = self.character_index != 0;
        if is_not_cursor_leftmost {
            let current_index = self.character_index;
            let from_left_to_current_index = current_index - 1;

            // Getting all characters before the selected character.
            let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
            // Getting all characters after selected character.
            let after_char_to_delete = self.input.chars().skip(current_index);

            // Put all characters together except the selected one.
            // By leaving the selected one out, it is forgotten and therefore deleted.
            self.input = before_char_to_delete.chain(after_char_to_delete).collect();
            self.move_cursor_left();
        }
    }

    fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
        new_cursor_pos.clamp(0, self.input.chars().count())
    }

    fn reset_cursor(&mut self) {
        self.character_index = 0;
    }

    /// Gets the cursor position of the input field, based on it's index in a list.
    pub fn cursor_position(&self, index: usize) -> Position {
        Position::new(
            self.character_index.saturating_add(1) as u16,
            index
                .saturating_add(4)
                .saturating_add(index.saturating_mul(2)) as u16,
        )
    }

    pub(crate) fn submit_message(&mut self) -> String {
        self.reset_cursor();
        self.input.drain(..).collect()
    }

    /// Runs an event loop for the [InputField].
    pub async fn run(
        &mut self,
        terminal: &mut DefaultTerminal,
        events: &mut EventHandler,
    ) -> Result<String> {
        loop {
            terminal.draw(|frame| self.draw(frame))?;

            match events.next().await? {
                Event::Crossterm(CrosstermEvent::Key(key)) => match self.state.mode() {
                    InputMode::Normal => match key.code {
                        KeyCode::Enter => return Ok(self.submit_message()),
                        KeyCode::Char('e') | KeyCode::Char('i') => {
                            self.state.set_mode(InputMode::Editing);
                        }
                        KeyCode::Char('r') => {
                            self.state.toggle_visibility();
                        }
                        KeyCode::Esc | KeyCode::Char('q') => {
                            return Ok(String::new());
                        }
                        _ => {}
                    },
                    InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
                        KeyCode::Enter => return Ok(self.submit_message()),
                        KeyCode::Char(to_insert) => self.enter_char(to_insert),
                        KeyCode::Backspace => self.delete_char(),
                        KeyCode::Left => self.move_cursor_left(),
                        KeyCode::Right => self.move_cursor_right(),
                        KeyCode::Esc => self.state.set_mode(InputMode::Normal),
                        _ => {}
                    },
                    InputMode::Editing => {}
                },
                Event::Crossterm(CrosstermEvent::Paste(paste)) => {
                    if matches!(self.state.mode(), InputMode::Editing) {
                        paste.chars().for_each(|c| self.enter_char(c));
                    }
                }
                Event::App(AppEvent::ClearClipboard) => App::clear_clipboard().await?,
                _ => {}
            }
        }
    }

    /// Converts the [InputField] into a TUI widget.
    pub fn as_widget(&self) -> Paragraph<'_> {
        let input = match self.state.visibility() {
            InputVisibility::Show => self.input.clone(),
            InputVisibility::Hide => self.input.chars().map(|_| '*').collect::<String>(),
        };
        Paragraph::new(input)
            .style(match self.state.mode() {
                InputMode::Normal => Style::default(),
                InputMode::Editing => Style::default().fg(Color::Yellow),
            })
            .block(Block::bordered().title(self.title()))
    }

    /// Gets the layout constraint required to render the [InputField].
    pub fn constraint(&self) -> Constraint {
        Constraint::Length(3)
    }

    fn draw(&self, frame: &mut Frame) {
        let (msg, style) = match self.state.mode() {
            InputMode::Normal => (
                vec![
                    "Press ".into(),
                    "Esc".bold(),
                    " or ".into(),
                    "q".bold(),
                    " to exit | ".into(),
                    "i".bold(),
                    " or ".into(),
                    "e".bold(),
                    " to start editing | ".into(),
                    "r".bold(),
                    " to show/hide".into(),
                ],
                Style::default(),
            ),
            InputMode::Editing => (
                vec![
                    "Press ".into(),
                    "Esc".bold(),
                    " to stop editing, ".into(),
                    "Enter".bold(),
                    " to record the message".into(),
                ],
                Style::default(),
            ),
        };

        let text = Text::from(Line::from(msg)).patch_style(style);
        let help_message = Paragraph::new(text);

        let vertical = Layout::vertical([Constraint::Length(1), Constraint::Length(3)]);
        let [help_area, input_area] = vertical.areas(frame.area());

        frame.render_widget(help_message, help_area);
        frame.render_widget(Text::from(self.input.as_str()), input_area);

        if self.state.mode().is_editing() {
            frame.set_cursor_position(Position::new(
                input_area.x + self.character_index as u16 + 1,
                input_area.y + 1,
            ));
        }
    }
}

field_access! {
    InputField {
        input: cow { str },
        title: cow { str },
    }
}

field_access! {
    InputField {
        state: as_ref { InputState },
    }
}

impl Widget for &InputField {
    fn render(self, area: Rect, buf: &mut Buffer) {
        self.as_widget().render(area, buf);
    }
}

impl Default for InputField {
    fn default() -> Self {
        Self::new()
    }
}
