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

use runtime::abi::io::TerminalOptions;

use crate::sys::RawStdin;

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

pub struct RawStdinImpl {
    inner: Inner,
    saved: TerminalOptions,
}

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(TerminalOptions) -> TerminalOptions>(
        &mut self,
        f: F,
    ) -> io::Result<TerminalOptions> {
        let fd = self.as_raw_fd();
        let old = runtime::rt::io::terminal::update_terminal_options(fd, f)?;
        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(|_| TerminalOptions::raw_input())?;
        Ok(Self { inner, saved })
    }

    fn set_options(&mut self, options: TerminalOptions) -> io::Result<TerminalOptions> {
        self.inner.update_options(|_| 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(|_| self.saved).ok();
    }
}
