#[allow(unused_macros)]
macro_rules! itry {
    ($value:expr, $error:expr) => {{
        use ::eyre::WrapErr;
        match $value {
            Ok(x) => x,
            Err(e) => return Some(Err(e).wrap_err($error)),
        }
    }};
}

cfg_if::cfg_if! {
    if #[cfg(target_os = "macos")] {
        mod macos;
        pub(crate) use self::macos::*;
    } else if #[cfg(unix)] {
        mod unix;
        pub(crate) use self::unix::*;
    } else if #[cfg(windows)] {
        // visible for doctests
        pub mod windows;
        pub(crate) use self::windows::*;
    } else {
        mod empty;
        pub(crate) use self::empty::*;
    }
}

use std::ffi::OsStr;
use std::net::SocketAddr;
use std::num::NonZeroU16;

pub trait Resolvers: Sized + Iterator<Item = eyre::Result<SocketAddr>> {
    fn new(port: PortChooser<NonZeroU16>) -> eyre::Result<Self>;
    fn show(&self, _sink: &mut impl std::io::Write) -> eyre::Result<()> {
        Ok(())
    }
}

#[derive(Clone, Copy)]
pub struct PortChooser<D> {
    override_port: Option<NonZeroU16>,
    default_port: D,
}

pub trait ChoosePort<D> {
    fn choose(self, resolver_port: Option<NonZeroU16>) -> D;
    fn choose_or_zero(self, resolver_port: Option<NonZeroU16>) -> u16;
}

impl<D> PortChooser<D> {
    pub fn new(override_port: Option<NonZeroU16>, default_port: D) -> Self {
        Self {
            override_port,
            default_port,
        }
    }
}

impl ChoosePort<NonZeroU16> for PortChooser<NonZeroU16> {
    fn choose(self, resolver_port: Option<NonZeroU16>) -> NonZeroU16 {
        self.override_port
            .or(resolver_port)
            .unwrap_or(self.default_port)
    }
    fn choose_or_zero(self, resolver_port: Option<NonZeroU16>) -> u16 {
        self.choose(resolver_port).into()
    }
}

impl ChoosePort<Option<NonZeroU16>> for PortChooser<Option<NonZeroU16>> {
    fn choose(self, resolver_port: Option<NonZeroU16>) -> Option<NonZeroU16> {
        self.override_port.or(resolver_port).or(self.default_port)
    }
    fn choose_or_zero(self, resolver_port: Option<NonZeroU16>) -> u16 {
        self.choose(resolver_port).map(|x| x.get()).unwrap_or(0)
    }
}

pub struct SockaddrDisplay(pub SocketAddr);

impl std::fmt::Display for SockaddrDisplay {
    fn fmt(&self, sink: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        match self.0 {
            SocketAddr::V4(address) => {
                write!(sink, "{};", address.ip())?;

                if address.port() != 0 {
                    write!(sink, " port {};", address.port())?;
                } else {
                    write!(sink, " no port;")?;
                }
            }
            SocketAddr::V6(address) => {
                write!(sink, "{};", address.ip())?;

                if address.port() != 0 {
                    write!(sink, " port {};", address.port())?;
                } else {
                    write!(sink, " no port;")?;
                }

                if address.flowinfo() != 0 {
                    write!(sink, " flowinfo {};", address.flowinfo())?;
                } else {
                    write!(sink, " no flowinfo;")?;
                }

                if address.scope_id() != 0 {
                    write!(sink, " scope_id {};", address.scope_id())?;
                } else {
                    write!(sink, " no scope_id;")?;
                }
            }
        }

        Ok(())
    }
}

/// Allow SIGPIPE to terminate the process.
#[allow(unsafe_code)]
pub fn sigpipe_default() {
    // FIXME non-POSIX platforms?
    #[cfg(unix)]
    unsafe {
        // https://github.com/rust-lang/rust/issues/46016#issuecomment-428106774
        // https://github.com/rust-lang/rust/issues/46016#issuecomment-605624865
        use nix::sys::signal::{signal, SigHandler::SigDfl, Signal::SIGPIPE};
        signal(SIGPIPE, SigDfl).expect("signal(2) failed");
    }
}

/// Enable ANSI escape codes on Windows.
pub fn enable_virtual_terminal_processing() {
    #[cfg(windows)]
    if atty::is(atty::Stream::Stdout) {
        winapi_util::console::Console::stdout()
            .expect("GetConsoleScreenBufferInfo failed on stdout")
            .set_virtual_terminal_processing(true)
            .expect("GetConsoleMode or SetConsoleMode failed on stdout");
    }
    #[cfg(windows)]
    if atty::is(atty::Stream::Stderr) {
        winapi_util::console::Console::stderr()
            .expect("GetConsoleScreenBufferInfo failed on stderr")
            .set_virtual_terminal_processing(true)
            .expect("GetConsoleMode or SetConsoleMode failed on stderr");
    }
}

pub(crate) fn expect_pledge(result: Result<(), pledge::Error>) {
    result
        .or_else(pledge::Error::ignore_platform)
        .expect("pledge(2) failed");
}

#[cfg(not(unix))]
pub(crate) fn expect_unveil(_: impl AsRef<OsStr>, _: &str) {}

#[cfg(unix)]
pub(crate) fn expect_unveil(path: impl AsRef<OsStr>, permissions: &str) {
    use std::os::unix::ffi::OsStrExt;
    match unveil::unveil(path.as_ref().as_bytes(), permissions) {
        Err(unveil::Error::NotSupported) => Ok(()),
        x => x,
    }
    .expect("unveil(2) failed");
}

#[cfg(not(unix))]
pub(crate) async fn spawn_man() -> bool {
    false
}

#[cfg(unix)]
#[allow(unsafe_code)]
pub(crate) async fn spawn_man() -> bool {
    use std::fs::File;
    use std::io::Write;
    use std::os::unix::io::FromRawFd;

    use eyre::{bail, WrapErr};
    use nix::unistd::{mkstemp, unlink};
    use tokio::process::Command;

    async fn real() -> eyre::Result<()> {
        let (fd, path) =
            mkstemp("/tmp/bore.XXXXXX").wrap_err("failed to create temporary file for manual")?;

        let mut man = unsafe { File::from_raw_fd(fd) };

        man.write_all(include_bytes!("../doc/bore.1"))
            .wrap_err("failed to write temporary file for manual")?;
        man.sync_all()
            .wrap_err("failed to sync temporary file for manual")?;

        let status = Command::new("man")
            .arg(&path)
            .status()
            .await
            .wrap_err("failed to spawn man(1)")?;

        if !status.success() {
            // FIXME exposing Debug here lacks polish
            bail!("man(1) spawned but failed: {:?}", status);
        }

        let unlink = unlink(&path);

        // don’t treat unlink(2) failure as failure to view manual page
        if let Err(e) = unlink {
            if let Some(path) = path.to_str() {
                eprintln!("Warning: failed to delete temporary file {}: {}", path, e);
                eprintln!();
            } else {
                unreachable!();
            }
        }

        Ok(())
    }

    if let Err(e) = real().await {
        eprintln!("Warning: {}", e);
        eprintln!();
        return false;
    }

    true
}
