use std::{
    fmt::{self, Write},
    io,
    process::ExitCode,
};

use clap::Parser;
use libterm::{Clear, Color, Term, TermKey};
use sysutils::Input;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("I/O error: {0}")]
    IoError(#[from] io::Error),
    #[error("Terminal output error: {0}")]
    FmtError(#[from] fmt::Error),
}

pub struct View {
    term: Term,
    source: Input,
    eof: bool,
    show_bar: bool,

    cursor: usize,
    buffer: Vec<String>,
}

impl View {
    pub fn open(src: &str, show_bar: bool) -> Result<Self, Error> {
        let term = Term::open()?;
        let source = Input::open_str(src)?;

        Ok(Self {
            term,
            source,
            cursor: 0,
            show_bar,
            buffer: vec![],
            eof: false,
        })
    }

    fn fetch_more(&mut self, height: usize) -> Result<(), Error> {
        if self.eof || self.cursor + height < self.buffer.len() {
            return Ok(());
        }

        let need_lines = self.cursor + height + 1 - self.buffer.len();
        for _ in 0..need_lines {
            let mut line = String::new();
            let len = self.source.read_line(&mut line)?;
            if len == 0 {
                self.eof = true;
                break;
            }
            self.buffer.push(line.trim_matches('\n').to_owned());
        }

        Ok(())
    }

    fn display(&mut self, width: usize, height: usize) -> Result<(), Error> {
        // TODO don't redraw every time
        self.term.clear(Clear::All)?;
        self.term.set_cursor_visible(false)?;

        let bar_height = self.show_bar as usize;

        for y in 0..height - bar_height {
            let i = y + self.cursor;
            let Some(line) = self.buffer.get(i) else {
                break;
            };
            let limit = std::cmp::min(line.len(), width);

            self.term.set_cursor_position(y, 0)?;
            write!(self.term, "{}", &line[..limit])?;
        }

        if self.show_bar {
            let line_end = std::cmp::min(self.cursor + height, self.buffer.len());

            self.term.set_cursor_position(height - 1, 0)?;

            if self.eof {
                self.term.set_background(Color::Green)?;
            } else {
                self.term.set_background(Color::Yellow)?;
            }
            self.term.set_foreground(Color::Black)?;
            self.term.set_bright(true)?;

            self.term.clear(Clear::LineToEnd)?;

            if self.eof {
                let p = line_end * 100 / self.buffer.len();
                write!(
                    self.term,
                    "[{}..{}/{}] ({}%)",
                    self.cursor,
                    line_end,
                    self.buffer.len(),
                    p
                )?;
            } else {
                write!(self.term, "[{}..{}] ...", self.cursor, line_end)?;
            }
        }

        self.term.reset_style()?;
        self.term.flush()?;

        Ok(())
    }

    pub fn run(mut self) -> Result<(), Error> {
        loop {
            let (width, height) = self.term.size()?;

            self.fetch_more(height)?;

            self.display(width, height)?;

            let key = self.term.read_key()?;

            match key {
                TermKey::Char('q') => break,
                TermKey::Char('j') => {
                    if !self.eof || self.cursor + height < self.buffer.len() {
                        self.cursor += 1;
                    }
                }
                TermKey::Char('k') => {
                    if self.cursor != 0 {
                        self.cursor -= 1;
                    }
                }
                // TODO page down, page up
                _ => (),
            }
        }

        Ok(())
    }
}

impl Drop for View {
    fn drop(&mut self) {
        self.term.set_cursor_visible(true).ok();
    }
}

#[derive(Parser)]
struct Args {
    #[arg(short, long)]
    no_bar: bool,

    filename: String,
}

fn main() -> ExitCode {
    logsink::setup_logging(true);
    // TODO check if running in a terminal
    let args = Args::parse();
    let view = View::open(&args.filename, !args.no_bar).unwrap();
    match view.run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            log::error!("view: {error}");
            ExitCode::FAILURE
        }
    }
}
