use std::{
    fs::{File, OpenOptions},
    io::{self, IsTerminal, Read, Stdin},
    mem::MaybeUninit,
    os::fd::{AsRawFd, RawFd},
};

use crate::{io::TerminalOptionsImpl, sys::RawStdin};

enum Inner {
    Stdin(Stdin),
    DevTty(File),
}

pub struct RawStdinImpl {
    inner: Inner,
    saved: libc::termios,
}

impl Inner {
    fn open() -> io::Result<Self> {
        let stdin = io::stdin();
        if stdin.is_terminal() {
            Ok(Self::Stdin(stdin))
        } else {
            let dev_tty = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
            Ok(Self::DevTty(dev_tty))
        }
    }

    fn update_options<F: FnOnce(&mut libc::termios)>(&mut self, f: F) -> io::Result<libc::termios> {
        let fd = self.as_raw_fd();
        let mut old = MaybeUninit::<libc::termios>::uninit();
        if unsafe { libc::tcgetattr(fd, old.as_mut_ptr()) } != 0 {
            return Err(io::Error::last_os_error());
        }
        let old = unsafe { old.assume_init() };
        let mut new = old;
        f(&mut new);
        if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw const new) } != 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(old)
    }
}

impl Read for Inner {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Self::Stdin(stdin) => stdin.read(buf),
            Self::DevTty(dev_tty) => dev_tty.read(buf),
        }
    }
}

impl AsRawFd for Inner {
    fn as_raw_fd(&self) -> RawFd {
        match self {
            Self::Stdin(stdin) => stdin.as_raw_fd(),
            Self::DevTty(dev_tty) => dev_tty.as_raw_fd(),
        }
    }
}

impl RawStdin for RawStdinImpl {
    fn open() -> io::Result<Self> {
        let mut inner = Inner::open()?;
        let saved = inner.update_options(|options| {
            unsafe {
                libc::cfmakeraw(options);
            };
        })?;
        Ok(Self { inner, saved })
    }

    fn set_options(&mut self, options: TerminalOptionsImpl) -> io::Result<TerminalOptionsImpl> {
        self.inner.update_options(|t| {
            *t = options;
        })
    }
}

impl io::Read for RawStdinImpl {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.inner.read(buf)
    }
}

impl AsRawFd for RawStdinImpl {
    fn as_raw_fd(&self) -> RawFd {
        self.inner.as_raw_fd()
    }
}

impl Drop for RawStdinImpl {
    fn drop(&mut self) {
        self.inner
            .update_options(|options| *options = self.saved)
            .ok();
    }
}
