use std::{
    collections::{BTreeMap, VecDeque},
    io::{Read, Write},
    sync::RwLock,
};

pub mod colors;

fn read1() -> Option<u8> {
    let mut buf = [0];
    match std::io::stdin().lock().read_exact(&mut buf) {
        Ok(_) => Some(buf[0]),
        Err(_) => None,
    }
}

pub struct TerminalInput {
    pub debug: bool,
    buf: VecDeque<KbInput<'static>>,
}

impl TerminalInput {
    pub fn new() -> Self {
        Self {
            debug: false,
            buf: VecDeque::new(),
        }
    }

    fn read_internal(&mut self) {
        let mut reader = EscapingStdinReader::new(et());
        loop {
            let Some(b) = read1() else {
                // EOF
                return;
            };
            match reader.process_byte(b) {
                ByteProcessingResult::Done(kb) => {
                    if self.debug {
                        if let KbInput::Escape(e) = &kb {
                            for k in e.keys.iter() {
                                print!("{k} ")
                            }
                        }
                        println!("{}\r", kb.as_bytes().escape_ascii());
                    }

                    if let KbInput::InvalidEscape(x) = &kb
                        && x.len() == 1
                    {
                        self.buf.push_back(KbInput::Key([x[0]]));
                    } else {
                        self.buf.push_back(kb);
                    }
                    return;
                }
                ByteProcessingResult::Continue(r) => reader = r,
            }
        }
    }

    pub fn read(&mut self) -> Option<KbInput<'static>> {
        if self.buf.is_empty() {
            self.read_internal();
        }

        self.buf.pop_front()
    }

    pub fn query_cursor_position(&mut self) -> Option<CursorPos> {
        // query to request cursor position
        let mut stdout = std::io::stdout().lock();
        stdout.write_all(b"\x1b[6n").ok()?;
        stdout.flush().ok()?;
        drop(stdout);

        self.read_internal();

        while self
            .buf
            .back()
            .map(|b| !b.as_bytes().ends_with(b"R"))
            .unwrap_or(true)
        {
            let buf_len = self.buf.len();
            self.read_internal();

            // no new input, give up
            if self.buf.len() <= buf_len {
                break;
            }
        }

        if self.buf.is_empty() {
            return None;
        }

        if !self.buf.back().unwrap().as_bytes().ends_with(b"R") {
            return None;
        }

        let i = self
            .buf
            .iter()
            .enumerate()
            .rev()
            .find(|(_, x)| x.as_bytes().starts_with(b"\x1b"))
            .map(|x| x.0)?;

        let input = self.buf.iter().skip(i).fold(BString::new(), |mut acc, x| {
            acc.push_all(x.as_bytes());
            acc
        });

        let pos = try_parse_cursor_position_message(&input)?;
        self.buf.truncate(i);

        Some(pos)
    }
}

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

// expecting a string of the form `\x1b[n;mR`
fn try_parse_cursor_position_message(mut msg: &[u8]) -> Option<CursorPos> {
    if !msg.starts_with(b"\x1b[") || !msg.ends_with(b"R") {
        return None;
    }

    msg = &msg[2..msg.len() - 1];

    let (first, second) = msg.split_once(|x| *x == b';')?;

    let first = str::from_utf8(first).ok()?;
    let second = str::from_utf8(second).ok()?;

    let row = first.parse::<usize>().ok()?;
    let col = second.parse::<usize>().ok()?;

    Some(CursorPos { row, col })
}

struct EscapingStdinReader<'a> {
    buf: Vec<u8>,
    trie: &'a EscapeTrie,
}

enum ByteProcessingResult<'a> {
    Done(KbInput<'a>),
    Continue(EscapingStdinReader<'a>),
}

impl<'a> EscapingStdinReader<'a> {
    pub fn new(trie: &'a EscapeTrie) -> Self {
        Self {
            buf: Vec::new(),
            trie,
        }
    }

    pub fn process_byte(mut self, byte: u8) -> ByteProcessingResult<'a> {
        match self.trie {
            EscapeTrie::Done(_) => ByteProcessingResult::Done(KbInput::Key([byte])),
            EscapeTrie::More(trie) => {
                self.buf.push(byte);
                match trie.get(&byte) {
                    Some(EscapeTrie::Done(keys)) => {
                        ByteProcessingResult::Done(KbInput::Escape(Escape {
                            keys: &keys[..],
                            value: self.buf,
                        }))
                    }
                    Some(trie) => {
                        self.trie = trie;
                        ByteProcessingResult::Continue(self)
                    }
                    None => ByteProcessingResult::Done(KbInput::InvalidEscape(self.buf)),
                }
            }
        }
    }
}

enum EscapeTrie {
    Done(Vec<&'static str>),
    More(BTreeMap<u8, EscapeTrie>),
}

#[derive(Clone, Debug)]
pub enum KbInput<'a> {
    Key([u8; 1]),
    Escape(Escape<'a>),
    InvalidEscape(Vec<u8>),
}

impl<'a> KbInput<'a> {
    pub fn as_bytes(&'a self) -> &'a [u8] {
        match self {
            KbInput::Key(x) => &x[..],
            KbInput::Escape(e) => &e.value[..],
            KbInput::InvalidEscape(e) => &e[..],
        }
    }
}

#[derive(Clone, Debug)]
pub struct Escape<'a> {
    pub keys: &'a [&'a str],
    pub value: Vec<u8>,
}

use terminfo_lean::parse::Terminfo;

use crate::{BString, PushAll, cursor::CursorPos};

static TERMINFO: RwLock<Option<&'static Terminfo<'static>>> = RwLock::new(None);
static ESCAPE_TRIE: RwLock<Option<&'static EscapeTrie>> = RwLock::new(None);

fn parse_terminfo() -> Result<Terminfo<'static>, ()> {
    let term = std::env::var_os("TERM").unwrap_or_else(|| "xterm".into());
    let terminfo_file_path = terminfo_lean::locate::locate(&term)
        .map_err(|e| println!("failed to locate terminfo file for terminal {term:?}: {e:?}",))?;
    let mut terminfo_file = std::fs::File::open(&terminfo_file_path).map_err(|e| {
        println!("failed to open terminfo file at location {terminfo_file_path:?}: {e:?}")
    })?;
    let mut buf = Vec::new();
    terminfo_file.read_to_end(&mut buf).map_err(|e| {
        println!("failed to read terminfo file at location {terminfo_file_path:?}: {e:?}")
    })?;
    buf.shrink_to_fit();
    let terminfo = terminfo_lean::parse::parse(buf.leak()).map_err(|e| {
        println!("failed to parse terminfo file at location {terminfo_file_path:?}: {e:?}")
    })?;
    Ok(terminfo)
}

fn parse_terminfo_backup() -> Terminfo<'static> {
    todo!("panic-safe backup terminfo")
}

pub fn setup() {
    let ti = parse_terminfo().unwrap_or_else(|_| {
        println!("using backup terminfo (might not be correct for this terminal)");
        parse_terminfo_backup()
    });
    let ti: &'static _ = Box::leak(Box::new(ti));

    TERMINFO.clear_poison();
    *TERMINFO.write().unwrap() = Some(ti);

    let et = Box::leak(Box::new(EscapeTrie::from(ti)));
    ESCAPE_TRIE.clear_poison();
    *ESCAPE_TRIE.write().unwrap() = Some(et);
}

pub fn ti() -> &'static Terminfo<'static> {
    TERMINFO.read().unwrap().unwrap()
}

fn et() -> &'static EscapeTrie {
    ESCAPE_TRIE.read().unwrap().unwrap()
}

fn is_parametrized(x: &[u8]) -> bool {
    let mut pct = false;
    for &b in x {
        if b == b'%' {
            pct = !pct;
        } else if pct {
            return true;
        }
    }
    false
}

fn trie_from_words(words: Vec<(&'static str, &[u8])>) -> EscapeTrie {
    let mut tree = BTreeMap::new();
    let mut all_empty = true;

    for (key, val) in words.iter() {
        if let Some(byte) = val.first() {
            all_empty = false;
            tree.entry(byte)
                .or_insert_with(Vec::new)
                .push((*key, &val[1..]));
        }
    }

    if all_empty {
        EscapeTrie::Done(words.into_iter().map(|x| x.0).collect())
    } else {
        let trie = tree
            .into_iter()
            .map(|(k, v)| (*k, trie_from_words(v)))
            .collect();
        EscapeTrie::More(trie)
    }
}

impl From<&Terminfo<'static>> for EscapeTrie {
    fn from(ti: &Terminfo<'static>) -> Self {
        let w: Vec<(&'static str, &'static [u8])> = ti
            .strings
            .iter()
            .filter(|(k, v)| k.starts_with("k") && !is_parametrized(v))
            .map(|(k, v)| (*k, *v))
            .collect();
        trie_from_words(w)
    }
}
