// A set of helper functions for working with the `termios` and `termios2` structs
use cfg_if::cfg_if;

use crate::{DataBits, FlowControl, Parity, Result, StopBits};
use nix::libc;

use std::os::unix::prelude::*;

cfg_if! {
    if #[cfg(any(
        target_os = "dragonfly",
        target_os = "freebsd",
        target_os = "ios",
        target_os = "macos",
        target_os = "netbsd",
        target_os = "openbsd",
        all(
            target_os = "linux",
            any(
                target_env = "musl",
                target_arch = "powerpc",
                target_arch = "powerpc64"
            )
        )
    ))] {
        pub(crate) type Termios = libc::termios;
    } else if #[cfg(any(
        target_os = "android",
        all(
            target_os = "linux",
            not(any(
                target_env = "musl",
                target_arch = "powerpc",
                target_arch = "powerpc64"
            ))
        )
    ))] {
        pub(crate) type Termios = libc::termios2;
    } else {
        compile_error!("Unsupported platform. See crate documentation for supported platforms");
    }
}

// The termios struct isn't used for storing the baud rate, but it can be affected by other
// calls in this lib to the IOSSIOSPEED ioctl. So whenever we get this struct, make sure to
// reset the input & output baud rates to a safe default. This is accounted for by the
// corresponding set_termios that is mac-specific and always calls IOSSIOSPEED.
#[cfg(any(target_os = "ios", target_os = "macos",))]
pub(crate) fn get_termios(fd: RawFd) -> Result<Termios> {
    use std::mem::MaybeUninit;

    let mut termios = MaybeUninit::uninit();
    let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) };
    nix::errno::Errno::result(res)?;
    let mut termios = unsafe { termios.assume_init() };
    termios.c_ispeed = self::libc::B9600;
    termios.c_ospeed = self::libc::B9600;
    Ok(termios)
}

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    all(
        target_os = "linux",
        any(
            target_env = "musl",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        )
    )
))]
pub(crate) fn get_termios(fd: RawFd) -> Result<Termios> {
    use std::mem::MaybeUninit;

    let mut termios = MaybeUninit::uninit();
    let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) };
    nix::errno::Errno::result(res)?;
    unsafe { Ok(termios.assume_init()) }
}

#[cfg(any(
    target_os = "android",
    all(
        target_os = "linux",
        not(any(
            target_env = "musl",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        ))
    )
))]
pub(crate) fn get_termios(fd: RawFd) -> Result<Termios> {
    crate::posix::ioctl::tcgets2(fd)
}

#[cfg(any(target_os = "ios", target_os = "macos",))]
pub(crate) fn set_termios(fd: RawFd, termios: &libc::termios, baud_rate: u32) -> Result<()> {
    let res = unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) };
    nix::errno::Errno::result(res)?;

    // Note: attempting to set the baud rate on a pseudo terminal via this ioctl call will fail
    // with the `ENOTTY` error.
    if baud_rate > 0 {
        crate::posix::ioctl::iossiospeed(fd, &(baud_rate as libc::speed_t))?;
    }

    Ok(())
}

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    all(
        target_os = "linux",
        any(
            target_env = "musl",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        )
    )
))]
pub(crate) fn set_termios(fd: RawFd, termios: &libc::termios) -> Result<()> {
    let res = unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) };
    nix::errno::Errno::result(res)?;
    Ok(())
}

#[cfg(any(
    target_os = "android",
    all(
        target_os = "linux",
        not(any(
            target_env = "musl",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        ))
    )
))]
pub(crate) fn set_termios(fd: RawFd, termios: &Termios) -> Result<()> {
    crate::posix::ioctl::tcsets2(fd, termios)
}

pub(crate) fn set_parity(termios: &mut Termios, parity: Parity) {
    match parity {
        Parity::None => {
            termios.c_cflag &= !(libc::PARENB | libc::PARODD);
            termios.c_iflag &= !libc::INPCK;
            termios.c_iflag |= libc::IGNPAR;
        }
        Parity::Odd => {
            termios.c_cflag |= libc::PARENB | libc::PARODD;
            termios.c_iflag |= libc::INPCK;
            termios.c_iflag &= !libc::IGNPAR;
        }
        Parity::Even => {
            termios.c_cflag &= !libc::PARODD;
            termios.c_cflag |= libc::PARENB;
            termios.c_iflag |= libc::INPCK;
            termios.c_iflag &= !libc::IGNPAR;
        }
    };
}

pub(crate) fn set_flow_control(termios: &mut Termios, flow_control: FlowControl) {
    match flow_control {
        FlowControl::None => {
            termios.c_iflag &= !(libc::IXON | libc::IXOFF);
            termios.c_cflag &= !libc::CRTSCTS;
        }
        FlowControl::Software => {
            termios.c_iflag |= libc::IXON | libc::IXOFF;
            termios.c_cflag &= !libc::CRTSCTS;
        }
        FlowControl::Hardware => {
            termios.c_iflag &= !(libc::IXON | libc::IXOFF);
            termios.c_cflag |= libc::CRTSCTS;
        }
    };
}

pub(crate) fn set_data_bits(termios: &mut Termios, data_bits: DataBits) {
    let size = match data_bits {
        DataBits::Five => libc::CS5,
        DataBits::Six => libc::CS6,
        DataBits::Seven => libc::CS7,
        DataBits::Eight => libc::CS8,
    };

    termios.c_cflag &= !libc::CSIZE;
    termios.c_cflag |= size;
}

pub(crate) fn set_stop_bits(termios: &mut Termios, stop_bits: StopBits) {
    match stop_bits {
        StopBits::One => termios.c_cflag &= !libc::CSTOPB,
        StopBits::Two => termios.c_cflag |= libc::CSTOPB,
    };
}

#[cfg(any(
    target_os = "android",
    all(
        target_os = "linux",
        not(any(
            target_env = "musl",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        ))
    )
))]
pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> {
    termios.c_cflag &= !nix::libc::CBAUD;
    termios.c_cflag |= nix::libc::BOTHER;
    termios.c_ispeed = baud_rate;
    termios.c_ospeed = baud_rate;
    Ok(())
}

// BSDs use the baud rate as the constant value so there's no translation necessary
#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd"
))]
pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> {
    let res = unsafe { libc::cfsetspeed(termios, baud_rate.into()) };
    nix::errno::Errno::result(res)?;
    Ok(())
}

#[cfg(all(
    target_os = "linux",
    any(
        target_env = "musl",
        target_arch = "powerpc",
        target_arch = "powerpc64"
    )
))]
pub(crate) fn set_baud_rate(termios: &mut Termios, baud_rate: u32) -> Result<()> {
    use crate::{Error, ErrorKind};

    use self::libc::{
        B1000000, B1152000, B1500000, B2000000, B2500000, B3000000, B3500000, B4000000, B460800,
        B500000, B576000, B921600,
    };
    use self::libc::{
        B110, B115200, B1200, B134, B150, B1800, B19200, B200, B230400, B2400, B300, B38400, B4800,
        B50, B57600, B600, B75, B9600,
    };

    let baud_rate = match baud_rate {
        50 => B50,
        75 => B75,
        110 => B110,
        134 => B134,
        150 => B150,
        200 => B200,
        300 => B300,
        600 => B600,
        1200 => B1200,
        1800 => B1800,
        2400 => B2400,
        4800 => B4800,
        9600 => B9600,
        19_200 => B19200,
        38_400 => B38400,
        57_600 => B57600,
        115_200 => B115200,
        230_400 => B230400,
        460_800 => B460800,
        500_000 => B500000,
        576_000 => B576000,
        921_600 => B921600,
        1_000_000 => B1000000,
        1_152_000 => B1152000,
        1_500_000 => B1500000,
        2_000_000 => B2000000,
        2_500_000 => B2500000,
        3_000_000 => B3000000,
        3_500_000 => B3500000,
        4_000_000 => B4000000,
        _ => return Err(Error::new(ErrorKind::InvalidInput, "Unsupported baud rate")),
    };
    let res = unsafe { libc::cfsetspeed(termios, baud_rate) };
    nix::errno::Errno::result(res)?;
    Ok(())
}
