use std::env;
use std::fmt::Display;
use std::fs;
use std::io;
use std::io::{BufRead, BufReader};
use std::mem;
use std::os::unix::io::AsRawFd;
use std::ptr;
use std::str;

use crate::kb::Key;
use crate::term::Term;

pub use crate::common_term::*;

pub const DEFAULT_WIDTH: u16 = 80;

#[inline]
pub fn is_a_terminal(out: &Term) -> bool {
    unsafe { libc::isatty(out.as_raw_fd()) != 0 }
}

pub fn is_a_color_terminal(out: &Term) -> bool {
    if !is_a_terminal(out) {
        return false;
    }

    if env::var("NO_COLOR").is_ok() {
        return false;
    }

    match env::var("TERM") {
        Ok(term) => term != "dumb",
        Err(_) => false,
    }
}

pub fn c_result<F: FnOnce() -> libc::c_int>(f: F) -> io::Result<()> {
    let res = f();
    if res != 0 {
        Err(io::Error::last_os_error())
    } else {
        Ok(())
    }
}

pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {
    unsafe {
        if libc::isatty(libc::STDOUT_FILENO) != 1 {
            return None;
        }

        let mut winsize: libc::winsize = std::mem::zeroed();

        // FIXME: ".into()" used as a temporary fix for a libc bug
        // https://github.com/rust-lang/libc/pull/704
        #[allow(clippy::useless_conversion)]
        libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize);
        if winsize.ws_row > 0 && winsize.ws_col > 0 {
            Some((winsize.ws_row as u16, winsize.ws_col as u16))
        } else {
            None
        }
    }
}

pub fn read_secure() -> io::Result<String> {
    let f_tty;
    let fd = unsafe {
        if libc::isatty(libc::STDIN_FILENO) == 1 {
            f_tty = None;
            libc::STDIN_FILENO
        } else {
            let f = fs::OpenOptions::new()
                .read(true)
                .write(true)
                .open("/dev/tty")?;
            let fd = f.as_raw_fd();
            f_tty = Some(BufReader::new(f));
            fd
        }
    };

    let mut termios = core::mem::MaybeUninit::uninit();
    c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?;
    let mut termios = unsafe { termios.assume_init() };
    let original = termios;
    termios.c_lflag &= !libc::ECHO;
    c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) })?;
    let mut rv = String::new();

    let read_rv = if let Some(mut f) = f_tty {
        f.read_line(&mut rv)
    } else {
        io::stdin().read_line(&mut rv)
    };

    c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &original) })?;

    read_rv.map(|_| {
        let len = rv.trim_end_matches(&['\r', '\n'][..]).len();
        rv.truncate(len);
        rv
    })
}

fn poll_fd(fd: i32, timeout: i32) -> io::Result<bool> {
    let mut pollfd = libc::pollfd {
        fd,
        events: libc::POLLIN,
        revents: 0,
    };
    let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) };
    if ret < 0 {
        Err(io::Error::last_os_error())
    } else {
        Ok(pollfd.revents & libc::POLLIN != 0)
    }
}

#[cfg(target_os = "macos")]
fn select_fd(fd: i32, timeout: i32) -> io::Result<bool> {
    unsafe {
        let mut read_fd_set: libc::fd_set = mem::zeroed();

        let mut timeout_val;
        let timeout = if timeout < 0 {
            ptr::null_mut()
        } else {
            timeout_val = libc::timeval {
                tv_sec: (timeout / 1000) as _,
                tv_usec: (timeout * 1000) as _,
            };
            &mut timeout_val
        };

        libc::FD_ZERO(&mut read_fd_set);
        libc::FD_SET(fd, &mut read_fd_set);
        let ret = libc::select(
            fd + 1,
            &mut read_fd_set,
            ptr::null_mut(),
            ptr::null_mut(),
            timeout,
        );
        if ret < 0 {
            Err(io::Error::last_os_error())
        } else {
            Ok(libc::FD_ISSET(fd, &read_fd_set))
        }
    }
}

fn select_or_poll_term_fd(fd: i32, timeout: i32) -> io::Result<bool> {
    // There is a bug on macos that ttys cannot be polled, only select()
    // works.  However given how problematic select is in general, we
    // normally want to use poll there too.
    #[cfg(target_os = "macos")]
    {
        if unsafe { libc::isatty(fd) == 1 } {
            return select_fd(fd, timeout);
        }
    }
    poll_fd(fd, timeout)
}

fn read_single_char(fd: i32) -> io::Result<Option<char>> {
    // timeout of zero means that it will not block
    let is_ready = select_or_poll_term_fd(fd, 0)?;

    if is_ready {
        // if there is something to be read, take 1 byte from it
        let mut buf: [u8; 1] = [0];

        read_bytes(fd, &mut buf, 1)?;
        Ok(Some(buf[0] as char))
    } else {
        //there is nothing to be read
        Ok(None)
    }
}

// Similar to libc::read. Read count bytes into slice buf from descriptor fd.
// If successful, return the number of bytes read.
// Will return an error if nothing was read, i.e when called at end of file.
fn read_bytes(fd: i32, buf: &mut [u8], count: u8) -> io::Result<u8> {
    let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) };
    if read < 0 {
        Err(io::Error::last_os_error())
    } else if read == 0 {
        Err(io::Error::new(
            io::ErrorKind::UnexpectedEof,
            "Reached end of file",
        ))
    } else if buf[0] == b'\x03' {
        Err(io::Error::new(
            io::ErrorKind::Interrupted,
            "read interrupted",
        ))
    } else {
        Ok(read as u8)
    }
}

fn read_single_key_impl(fd: i32) -> Result<Key, io::Error> {
    loop {
        match read_single_char(fd)? {
            Some('\x1b') => {
                // Escape was read, keep reading in case we find a familiar key
                break if let Some(c1) = read_single_char(fd)? {
                    if c1 == '[' {
                        if let Some(c2) = read_single_char(fd)? {
                            match c2 {
                                'A' => Ok(Key::ArrowUp),
                                'B' => Ok(Key::ArrowDown),
                                'C' => Ok(Key::ArrowRight),
                                'D' => Ok(Key::ArrowLeft),
                                'H' => Ok(Key::Home),
                                'F' => Ok(Key::End),
                                'Z' => Ok(Key::BackTab),
                                _ => {
                                    let c3 = read_single_char(fd)?;
                                    if let Some(c3) = c3 {
                                        if c3 == '~' {
                                            match c2 {
                                                '1' => Ok(Key::Home), // tmux
                                                '2' => Ok(Key::Insert),
                                                '3' => Ok(Key::Del),
                                                '4' => Ok(Key::End), // tmux
                                                '5' => Ok(Key::PageUp),
                                                '6' => Ok(Key::PageDown),
                                                '7' => Ok(Key::Home), // xrvt
                                                '8' => Ok(Key::End),  // xrvt
                                                _ => Ok(Key::UnknownEscSeq(vec![c1, c2, c3])),
                                            }
                                        } else {
                                            Ok(Key::UnknownEscSeq(vec![c1, c2, c3]))
                                        }
                                    } else {
                                        // \x1b[ and 1 more char
                                        Ok(Key::UnknownEscSeq(vec![c1, c2]))
                                    }
                                }
                            }
                        } else {
                            // \x1b[ and no more input
                            Ok(Key::UnknownEscSeq(vec![c1]))
                        }
                    } else {
                        // char after escape is not [
                        Ok(Key::UnknownEscSeq(vec![c1]))
                    }
                } else {
                    //nothing after escape
                    Ok(Key::Escape)
                };
            }
            Some(c) => {
                let byte = c as u8;
                let mut buf: [u8; 4] = [byte, 0, 0, 0];

                break if byte & 224u8 == 192u8 {
                    // a two byte unicode character
                    read_bytes(fd, &mut buf[1..], 1)?;
                    Ok(key_from_utf8(&buf[..2]))
                } else if byte & 240u8 == 224u8 {
                    // a three byte unicode character
                    read_bytes(fd, &mut buf[1..], 2)?;
                    Ok(key_from_utf8(&buf[..3]))
                } else if byte & 248u8 == 240u8 {
                    // a four byte unicode character
                    read_bytes(fd, &mut buf[1..], 3)?;
                    Ok(key_from_utf8(&buf[..4]))
                } else {
                    Ok(match c {
                        '\n' | '\r' => Key::Enter,
                        '\x7f' => Key::Backspace,
                        '\t' => Key::Tab,
                        '\x01' => Key::Home,      // Control-A (home)
                        '\x05' => Key::End,       // Control-E (end)
                        '\x08' => Key::Backspace, // Control-H (8) (Identical to '\b')
                        _ => Key::Char(c),
                    })
                };
            }
            None => {
                // there is no subsequent byte ready to be read, block and wait for input
                // negative timeout means that it will block indefinitely
                match select_or_poll_term_fd(fd, -1) {
                    Ok(_) => continue,
                    Err(_) => break Err(io::Error::last_os_error()),
                }
            }
        }
    }
}

pub fn read_single_key() -> io::Result<Key> {
    let tty_f;
    let fd = unsafe {
        if libc::isatty(libc::STDIN_FILENO) == 1 {
            libc::STDIN_FILENO
        } else {
            tty_f = fs::OpenOptions::new()
                .read(true)
                .write(true)
                .open("/dev/tty")?;
            tty_f.as_raw_fd()
        }
    };
    let mut termios = core::mem::MaybeUninit::uninit();
    c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?;
    let mut termios = unsafe { termios.assume_init() };
    let original = termios;
    unsafe { libc::cfmakeraw(&mut termios) };
    termios.c_oflag = original.c_oflag;
    c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?;
    let rv: io::Result<Key> = read_single_key_impl(fd);
    c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &original) })?;

    // if the user hit ^C we want to signal SIGINT to outselves.
    if let Err(ref err) = rv {
        if err.kind() == io::ErrorKind::Interrupted {
            unsafe {
                libc::raise(libc::SIGINT);
            }
        }
    }

    rv
}

pub fn key_from_utf8(buf: &[u8]) -> Key {
    if let Ok(s) = str::from_utf8(buf) {
        if let Some(c) = s.chars().next() {
            return Key::Char(c);
        }
    }
    Key::Unknown
}

#[cfg(not(target_os = "macos"))]
lazy_static::lazy_static! {
    static ref IS_LANG_UTF8: bool = match std::env::var("LANG") {
        Ok(lang) => lang.to_uppercase().ends_with("UTF-8"),
        _ => false,
    };
}

#[cfg(target_os = "macos")]
pub fn wants_emoji() -> bool {
    true
}

#[cfg(not(target_os = "macos"))]
pub fn wants_emoji() -> bool {
    *IS_LANG_UTF8
}

pub fn set_title<T: Display>(title: T) {
    print!("\x1b]0;{}\x07", title);
}