use crate::sys::ifreq::ifreq;
use crate::sys::{dummy_socket, ioctls, InterfaceHandle};
use crate::{Error, Interface};
use ipnet::IpNet;
use libc::ARPHRD_ETHER;
use netlink_packet_core::{
    NetlinkHeader, NetlinkMessage, NetlinkPayload, NLM_F_DUMP, NLM_F_REQUEST,
};
use netlink_packet_route::address::{AddressAttribute, AddressMessage};
use netlink_packet_route::{AddressFamily, RouteNetlinkMessage};
use netlink_sys::constants::NETLINK_ROUTE;
use netlink_sys::{Socket, SocketAddr};
use nix::net::if_::InterfaceFlags;
use std::net::IpAddr;
use std::os::unix::io::AsRawFd;

// Public interface (platform extension)
pub trait InterfaceExt {
    fn set_up(&self, v: bool) -> Result<(), Error>;
    fn set_running(&self, v: bool) -> Result<(), Error>;
    fn set_hwaddress(&self, hwaddress: [u8; 6]) -> Result<(), Error>;
    fn set_packet_info(&self, v: bool) -> Result<(), Error>;
}

// Private interface
impl InterfaceHandle {
    pub fn add_address(&self, network: IpNet) -> Result<(), Error> {
        let mut socket = Socket::new(NETLINK_ROUTE)?;
        socket.bind_auto()?;
        socket.connect(&SocketAddr::new(0, 0))?;

        let message = make_address_message(self.index, network);
        let mut nl_hdr = NetlinkHeader::default();
        nl_hdr.flags = NLM_F_DUMP | NLM_F_REQUEST;
        let mut req = NetlinkMessage::new(
            nl_hdr,
            NetlinkPayload::from(RouteNetlinkMessage::NewAddress(message)),
        );

        req.finalize();

        let mut buf = vec![0; req.header.length as _];
        req.serialize(&mut buf);

        socket.send(&buf, 0)?;

        Ok(())
    }

    pub fn remove_address(&self, network: IpNet) -> Result<(), Error> {
        let mut socket = Socket::new(NETLINK_ROUTE)?;
        socket.bind_auto()?;
        socket.connect(&SocketAddr::new(0, 0))?;

        let message = make_address_message(self.index, network);
        let mut nl_hdr = NetlinkHeader::default();
        nl_hdr.flags = NLM_F_REQUEST;
        let mut req = NetlinkMessage::new(
            nl_hdr,
            NetlinkPayload::from(RouteNetlinkMessage::DelAddress(message)),
        );

        req.finalize();

        let mut buf = vec![0; req.header.length as _];
        req.serialize(&mut buf);

        socket.send(&buf, 0)?;

        Ok(())
    }

    pub fn hwaddress(&self) -> Result<[u8; 6], Error> {
        let mut req = ifreq::new(self.name()?)?;
        let socket = dummy_socket()?;

        unsafe { ioctls::siocgifhwaddr(socket.as_raw_fd(), &mut req) }?;
        let mut rs = [0; 6];
        unsafe {
            for (i, b) in req.ifr_ifru.ifru_hwaddr.sa_data.iter().take(6).enumerate() {
                rs[i] = *b as _;
            }
        }
        Ok(rs)
    }
}

impl InterfaceExt for Interface {
    fn set_up(&self, v: bool) -> Result<(), Error> {
        self.0.set_up(v)
    }
    fn set_running(&self, v: bool) -> Result<(), Error> {
        self.0.set_running(v)
    }

    fn set_hwaddress(&self, hwaddress: [u8; 6]) -> Result<(), Error> {
        let mut req = ifreq::new(self.name()?)?;
        req.ifr_ifru.ifru_hwaddr = libc::sockaddr {
            sa_family: ARPHRD_ETHER,
            sa_data: unsafe { std::mem::zeroed() },
        };

        unsafe {
            req.ifr_ifru.ifru_hwaddr.sa_data[0..6]
                .copy_from_slice(hwaddress.map(|c| c as _).as_slice());
        }

        let socket = dummy_socket()?;

        unsafe { ioctls::siocsifhwaddr(socket.as_raw_fd(), &req) }?;
        Ok(())
    }

    fn set_packet_info(&self, v: bool) -> Result<(), Error> {
        let mut flags = self.0.flags()?;
        flags.set(InterfaceFlags::IFF_NO_PI, !v);
        self.0.set_flags(flags)?;
        Ok(())
    }
}

fn make_address_message(index: u32, network: IpNet) -> AddressMessage {
    let mut message = AddressMessage::default();
    message.header.prefix_len = network.prefix_len();
    message.header.index = index;
    let addr = network.addr();
    message.header.family = if addr.is_ipv4() {
        AddressFamily::Inet
    } else {
        AddressFamily::Inet6
    };

    if let IpAddr::V6(addr) = addr {
        if addr.is_multicast() {
            message.attributes.push(AddressAttribute::Multicast(addr));
            return message;
        }
    }

    message
        .attributes
        .push(AddressAttribute::Address(network.addr()));

    if let IpNet::V4(network_v4) = network {
        // for IPv4 the IFA_LOCAL address can be set to the same value as IFA_ADDRESS
        message
            .attributes
            .push(AddressAttribute::Local(network.addr()));
        // set the IFA_BROADCAST address as well (IPv6 does not support broadcast)
        message
            .attributes
            .push(AddressAttribute::Broadcast(network_v4.broadcast()));
    }

    message
}
