use crate::{
    geometry::PointU16,
    utils::{BoxError, Utils},
};
use crossterm::{QueueableCommand, terminal::SetTitle};
use ratatui::{Frame, Terminal as BaseRatatuiTerminal, TerminalOptions, Viewport, backend::CrosstermBackend};
use std::{
    fmt::Display,
    io::{Error as IoError, Write},
};

type RatatuiTerminal = BaseRatatuiTerminal<CrosstermBackend<Vec<u8>>>;

pub struct Terminal {
    ratatui_terminal: RatatuiTerminal,
    bytes: Vec<u8>,
}

impl Terminal {
    pub fn new(size: PointU16) -> Result<Self, IoError> {
        let ratatui_terminal = Self::ratatui_terminal(size)?;
        let bytes = Vec::new();
        let terminal = Self {
            ratatui_terminal,
            bytes,
        };

        terminal.ok()
    }

    fn ratatui_terminal(size: PointU16) -> Result<RatatuiTerminal, IoError> {
        let backend = CrosstermBackend::new(Vec::new());
        let rect = size.ratatui_rect();
        let viewport = Viewport::Fixed(rect);
        let options = TerminalOptions { viewport };
        let mut ratatui_terminal = RatatuiTerminal::with_options(backend, options)?;

        ratatui_terminal.resize(rect)?;

        ratatui_terminal.ok()
    }

    pub fn size(&self) -> Result<PointU16, IoError> {
        self.ratatui_terminal.size()?.convert::<PointU16>().ok()
    }

    pub fn resize(&mut self, num_cols: u16, num_rows: u16) -> Result<(), IoError> {
        let size = num_cols.pair(num_rows).convert::<PointU16>();

        self.ratatui_terminal.resize(size.ratatui_rect())?;

        ().ok()
    }

    // NOTE: no way to set the terminal title using ratatui, so must fallback to
    // crossterm methods
    pub fn set_title<T: Display>(&mut self, title: T) -> Result<&mut Self, IoError> {
        let set_title = SetTitle(title);

        self.bytes.queue(set_title)?.flush()?;

        self.ok()
    }

    pub fn draw<E: Into<BoxError>, F: FnOnce(&mut Frame) -> Result<(), E>>(
        &mut self,
        draw_fn: F,
    ) -> Result<&mut Self, IoError> {
        self.ratatui_terminal.try_draw(|frame| draw_fn(frame).io_result())?;
        self.ratatui_terminal
            .backend_mut()
            .writer_mut()
            .split_off(0)
            .push_all_to(&mut self.bytes);

        self.ok()
    }

    pub fn take_bytes(&mut self) -> Vec<u8> {
        self.bytes.mem_take()
    }
}
