use base::{error, libc, warn};
use libc::{
    POLLIN, SFD_CLOEXEC, SIG_BLOCK, SIGWINCH, STDIN_FILENO, STDOUT_FILENO, TCSADRAIN, TCSAFLUSH,
    TIOCGWINSZ, TIOCSWINSZ, cfmakeraw, close, pipe, poll, pollfd, raise, read, sigaddset,
    sigemptyset, signalfd, signalfd_siginfo, sigprocmask, sigset_t, splice, tcgetattr, tcsetattr,
    termios, winsize,
};
use std::{ffi::c_int, mem::MaybeUninit, ptr::null_mut};

static mut OLD_STDIN: Option<termios> = None;
const TIOCGPTN: u32 = 0x80045430;

unsafe extern "C" {
    // Don't use the declaration from the libc crate as request should be u32 not i32
    fn ioctl(fd: c_int, request: u32, ...) -> i32;
}

pub fn get_pty_num(fd: i32) -> i32 {
    let mut pty_num = -1i32;
    if unsafe { ioctl(fd, TIOCGPTN, &mut pty_num) } != 0 {
        warn!("Failed to get pty number");
    }
    pty_num
}

fn set_stdin_raw() -> bool {
    unsafe {
        let mut termios: termios = std::mem::zeroed();

        if tcgetattr(STDIN_FILENO, &mut termios) < 0 {
            return false;
        }

        let old_c_oflag = termios.c_oflag;
        OLD_STDIN = Some(termios);

        cfmakeraw(&mut termios);

        // don't modify output flags, since we are not setting stdout raw
        termios.c_oflag = old_c_oflag;

        if tcsetattr(STDIN_FILENO, TCSAFLUSH, &termios) < 0
            && tcsetattr(STDIN_FILENO, TCSADRAIN, &termios) < 0
        {
            warn!("Failed to set terminal attributes");
            return false;
        }
    }

    true
}

pub fn restore_stdin() -> bool {
    unsafe {
        if let Some(ref termios) = OLD_STDIN {
            if tcsetattr(STDIN_FILENO, TCSAFLUSH, termios) < 0
                && tcsetattr(STDIN_FILENO, TCSADRAIN, termios) < 0
            {
                warn!("Failed to restore terminal attributes");
                return false;
            }
        }
        OLD_STDIN = None;
        true
    }
}

fn resize_pty(outfd: i32) {
    let mut ws: winsize = unsafe { std::mem::zeroed() };
    if unsafe { ioctl(STDIN_FILENO, TIOCGWINSZ as u32, &mut ws) } >= 0 {
        unsafe { ioctl(outfd, TIOCSWINSZ as u32, &ws) };
    }
}

fn pump_via_pipe(infd: i32, outfd: i32, pipe: &[c_int; 2]) -> bool {
    // usize::MAX will EINVAL in some kernels, use i32::MAX in case
    let s = unsafe { splice(infd, null_mut(), pipe[1], null_mut(), i32::MAX as _, 0) };
    if s < 0 {
        error!("splice error");
        return false;
    }
    if s == 0 {
        return true;
    }
    let s = unsafe { splice(pipe[0], null_mut(), outfd, null_mut(), s as usize, 0) };
    if s < 0 {
        error!("splice error");
        return false;
    }
    true
}

pub fn pump_tty(infd: i32, outfd: i32) {
    set_stdin_raw();

    let sfd = unsafe {
        let mut mask: sigset_t = std::mem::zeroed();
        sigemptyset(&mut mask);
        sigaddset(&mut mask, SIGWINCH);
        if sigprocmask(SIG_BLOCK, &mask, null_mut()) < 0 {
            error!("sigprocmask");
        }
        signalfd(-1, &mask, SFD_CLOEXEC)
    };

    resize_pty(outfd);

    let mut pfds = [
        pollfd {
            fd: if outfd > 0 { STDIN_FILENO } else { -1 },
            events: POLLIN,
            revents: 0,
        },
        pollfd {
            fd: infd,
            events: POLLIN,
            revents: 0,
        },
        pollfd {
            fd: sfd,
            events: POLLIN,
            revents: 0,
        },
    ];

    let mut p: [c_int; 2] = [0; 2];
    if unsafe { pipe(&mut p as *mut c_int) } < 0 {
        error!("pipe error");
        return;
    }
    'poll: loop {
        let ready = unsafe { poll(pfds.as_mut_ptr(), pfds.len() as _, -1) };

        if ready < 0 {
            error!("poll error");
            break;
        }

        for pfd in &pfds {
            if pfd.revents & POLLIN != 0 {
                let res = if pfd.fd == STDIN_FILENO {
                    pump_via_pipe(pfd.fd, outfd, &p)
                } else if pfd.fd == infd {
                    pump_via_pipe(pfd.fd, STDOUT_FILENO, &p)
                } else if pfd.fd == sfd {
                    resize_pty(outfd);
                    let mut buf: [MaybeUninit<u8>; size_of::<signalfd_siginfo>()] =
                        MaybeUninit::uninit_array();
                    if unsafe { read(pfd.fd, buf.as_mut_ptr() as *mut _, buf.len()) } < 0 {
                        error!("read error");
                        false
                    } else {
                        true
                    }
                } else {
                    false
                };
                if !res {
                    break 'poll;
                }
            } else if pfd.revents != 0 && pfd.fd == infd {
                unsafe { close(pfd.fd) };
                break 'poll;
            }
        }
    }

    restore_stdin();
    unsafe { raise(SIGWINCH) };
}
