// Copyright (c) 2020 Huawei Technologies Co.,Ltd. All rights reserved.
//
// StratoVirt is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan
// PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//         http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.

use std::fs::{File, OpenOptions};
use std::io::{Read, Result as IoResult, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::sync::Arc;

use anyhow::{anyhow, bail, Context, Result};
use log::error;
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use vmm_sys_util::ioctl::{ioctl_with_mut_ref, ioctl_with_ref, ioctl_with_val};
use vmm_sys_util::{ioctl_ioc_nr, ioctl_ior_nr, ioctl_iow_nr};

const IFF_ATTACH_QUEUE: u16 = 0x0200;
const IFF_DETACH_QUEUE: u16 = 0x0400;

pub const TUN_F_CSUM: u32 = 1;
pub const TUN_F_TSO4: u32 = 2;
pub const TUN_F_TSO6: u32 = 4;
pub const TUN_F_TSO_ECN: u32 = 8;
pub const TUN_F_UFO: u32 = 16;
pub const TUN_F_VIRTIO: u32 = TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6 | TUN_F_UFO;

const IFF_TAP: u16 = 0x02;
pub const IFF_MULTI_QUEUE: u16 = 0x100;
const IFF_NO_PI: u16 = 0x1000;
const IFF_VNET_HDR: u16 = 0x4000;
const TUNTAP_PATH: &str = "/dev/net/tun";
const IFNAME_SIZE: usize = 16;

ioctl_iow_nr!(TUNSETIFF, 84, 202, ::std::os::raw::c_int);
ioctl_ior_nr!(TUNGETFEATURES, 84, 207, ::std::os::raw::c_uint);
ioctl_iow_nr!(TUNSETOFFLOAD, 84, 208, ::std::os::raw::c_int);
ioctl_iow_nr!(TUNSETVNETHDRSZ, 84, 216, ::std::os::raw::c_int);
ioctl_iow_nr!(TUNSETQUEUE, 84, 217, ::std::os::raw::c_int);

#[repr(C)]
pub struct IfReq {
    ifr_name: [u8; IFNAME_SIZE],
    ifr_flags: u16,
}

#[derive(Clone)]
pub struct Tap {
    pub file: Arc<File>,
    pub enabled: bool,
}

impl Tap {
    pub fn new(name: Option<&str>, fd: Option<RawFd>, queue_pairs: u16) -> Result<Self> {
        let file;

        if let Some(name) = name {
            if name.len() > IFNAME_SIZE - 1 {
                return Err(anyhow!("Open tap {} failed, name too long.", name));
            }

            let mut ifr_name = [0_u8; IFNAME_SIZE];
            let (left, _) = ifr_name.split_at_mut(name.len());
            left.copy_from_slice(name.as_bytes());

            let mut if_req = IfReq {
                ifr_name,
                ifr_flags: IFF_TAP | IFF_NO_PI | IFF_VNET_HDR,
            };

            if queue_pairs > 1 {
                if_req.ifr_flags |= IFF_MULTI_QUEUE;
            }

            let file_ = OpenOptions::new()
                .read(true)
                .write(true)
                .custom_flags(libc::O_CLOEXEC | libc::O_NONBLOCK)
                .open(TUNTAP_PATH)
                .with_context(|| format!("Open {} failed.", TUNTAP_PATH))?;

            // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
            let ret = unsafe { ioctl_with_mut_ref(&file_, TUNSETIFF(), &mut if_req) };
            if ret < 0 {
                return Err(anyhow!(
                    "Failed to set tap ifr flags, error is {}",
                    std::io::Error::last_os_error()
                ));
            }

            file = file_;
        } else if let Some(fd) = fd {
            let fcnt_arg = FcntlArg::F_SETFL(OFlag::from_bits(libc::O_NONBLOCK).unwrap());
            fcntl(fd, fcnt_arg)?;
            // SAFETY: The fd has been verified.
            file = unsafe { File::from_raw_fd(fd) };
        } else {
            return Err(anyhow!(
                "Open tap failed, unsupported operation, error is {}",
                std::io::Error::last_os_error()
            ));
        }

        let mut features = 0;
        // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
        let ret = unsafe { ioctl_with_mut_ref(&file, TUNGETFEATURES(), &mut features) };
        if ret < 0 {
            return Err(anyhow!(
                "Failed to get tap features, error is {}.",
                std::io::Error::last_os_error()
            ));
        }

        if (features & IFF_MULTI_QUEUE == 0) && queue_pairs > 1 {
            bail!("Needs multiqueue, but no kernel support for IFF_MULTI_QUEUE available");
        }

        Ok(Tap {
            file: Arc::new(file),
            enabled: true,
        })
    }

    pub fn set_offload(&self, flags: u32) -> Result<()> {
        let ret =
            // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
            unsafe { ioctl_with_val(self.file.as_ref(), TUNSETOFFLOAD(), flags as libc::c_ulong) };
        if ret < 0 {
            return Err(anyhow!("ioctl TUNSETOFFLOAD failed.".to_string()));
        }

        Ok(())
    }

    pub fn set_hdr_size(&self, len: u32) -> Result<()> {
        // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
        let ret = unsafe { ioctl_with_ref(self.file.as_ref(), TUNSETVNETHDRSZ(), &len) };
        if ret < 0 {
            return Err(anyhow!("ioctl TUNSETVNETHDRSZ failed.".to_string()));
        }

        Ok(())
    }

    pub fn has_ufo(&self) -> bool {
        let flags = TUN_F_CSUM | TUN_F_UFO;
        (
            // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
            unsafe { ioctl_with_val(self.file.as_ref(), TUNSETOFFLOAD(), flags as libc::c_ulong) }
        ) >= 0
    }

    pub fn set_queue(&mut self, enable: bool) -> i32 {
        if enable == self.enabled {
            return 0;
        }
        let ifr_flags = if enable {
            IFF_ATTACH_QUEUE
        } else {
            IFF_DETACH_QUEUE
        };
        let mut if_req = IfReq {
            ifr_name: [0_u8; IFNAME_SIZE],
            ifr_flags,
        };

        // SAFETY: The parameter of file can be guaranteed to be legal, and other parameters are constant.
        let ret = unsafe { ioctl_with_mut_ref(self.file.as_ref(), TUNSETQUEUE(), &mut if_req) };
        if ret == 0 {
            self.enabled = enable;
        } else {
            error!(
                "Failed to set queue, flags is {}, error is {}",
                ifr_flags,
                std::io::Error::last_os_error()
            );
        }
        ret
    }

    pub fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
        self.file.as_ref().read(buf)
    }

    pub fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
        self.file.as_ref().write(buf)
    }

    pub fn as_raw_fd(&self) -> RawFd {
        self.file.as_raw_fd()
    }
}