use std::collections::BTreeMap;
#[cfg(feature = "seccomp_trace")]
use base::debug;
use base::Event;
use devices::serial_device::SerialHardware;
use devices::serial_device::SerialParameters;
use devices::serial_device::SerialType;
use devices::Bus;
use devices::Serial;
use hypervisor::ProtectionType;
#[cfg(feature = "seccomp_trace")]
use jail::read_jail_addr;
#[cfg(windows)]
use jail::FakeMinijailStub as Minijail;
#[cfg(any(target_os = "android", target_os = "linux"))]
use minijail::Minijail;
use remain::sorted;
use thiserror::Error as ThisError;
use crate::DeviceRegistrationError;
mod sys;
pub fn set_default_serial_parameters(
    serial_parameters: &mut BTreeMap<(SerialHardware, u8), SerialParameters>,
    is_vhost_user_console_enabled: bool,
) {
    let default_console = (SerialHardware::Serial, 1);
    if !serial_parameters.iter().any(|(_, p)| p.console) && !is_vhost_user_console_enabled {
        serial_parameters
            .entry(default_console)
            .or_insert(SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::Serial,
                name: None,
                path: None,
                input: None,
                num: 1,
                console: true,
                earlycon: false,
                stdin: true,
                out_timestamp: false,
                ..Default::default()
            });
    }
    for num in 1..=4 {
        let key = (SerialHardware::Serial, num);
        serial_parameters.entry(key).or_insert(SerialParameters {
            type_: SerialType::Sink,
            hardware: SerialHardware::Serial,
            name: None,
            path: None,
            input: None,
            num,
            console: false,
            earlycon: false,
            stdin: false,
            out_timestamp: false,
            ..Default::default()
        });
    }
}
pub const SERIAL_ADDR: [u64; 4] = [0x3f8, 0x2f8, 0x3e8, 0x2e8];
pub struct SerialDeviceInfo {
    pub address: u64,
    pub size: u64,
    pub irq: u32,
}
pub fn add_serial_devices(
    protection_type: ProtectionType,
    io_bus: &Bus,
    com_evt_1_3: (u32, &Event),
    com_evt_2_4: (u32, &Event),
    serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>,
    #[cfg_attr(windows, allow(unused_variables))] serial_jail: Option<Minijail>,
    #[cfg(feature = "swap")] swap_controller: &mut Option<swap::SwapController>,
) -> std::result::Result<Vec<SerialDeviceInfo>, DeviceRegistrationError> {
    let mut devices = Vec::new();
    for com_num in 0..=3 {
        let com_evt = match com_num {
            0 => &com_evt_1_3,
            1 => &com_evt_2_4,
            2 => &com_evt_1_3,
            3 => &com_evt_2_4,
            _ => &com_evt_1_3,
        };
        let (irq, com_evt) = (com_evt.0, com_evt.1);
        let param = serial_parameters
            .get(&(SerialHardware::Serial, com_num + 1))
            .ok_or(DeviceRegistrationError::MissingRequiredSerialDevice(
                com_num + 1,
            ))?;
        let mut preserved_descriptors = Vec::new();
        let com = param
            .create_serial_device::<Serial>(protection_type, com_evt, &mut preserved_descriptors)
            .map_err(DeviceRegistrationError::CreateSerialDevice)?;
        #[cfg(any(target_os = "android", target_os = "linux"))]
        let serial_jail = if let Some(serial_jail) = serial_jail.as_ref() {
            let jail_clone = serial_jail
                .try_clone()
                .map_err(DeviceRegistrationError::CloneJail)?;
            #[cfg(feature = "seccomp_trace")]
            debug!(
                    "seccomp_trace {{\"event\": \"minijail_clone\", \"src_jail_addr\": \"0x{:x}\", \"dst_jail_addr\": \"0x{:x}\"}}",
                    read_jail_addr(serial_jail),
                    read_jail_addr(&jail_clone)
                );
            Some(jail_clone)
        } else {
            None
        };
        #[cfg(windows)]
        let serial_jail = None;
        let com = sys::add_serial_device(
            com,
            param,
            serial_jail,
            preserved_descriptors,
            #[cfg(feature = "swap")]
            swap_controller,
        )?;
        let address = SERIAL_ADDR[usize::from(com_num)];
        let size = 0x8; io_bus.insert(com, address, size).unwrap();
        devices.push(SerialDeviceInfo { address, size, irq })
    }
    Ok(devices)
}
#[sorted]
#[derive(ThisError, Debug)]
pub enum GetSerialCmdlineError {
    #[error("Error appending to cmdline: {0}")]
    KernelCmdline(kernel_cmdline::Error),
    #[error("Hardware {0} not supported as earlycon")]
    UnsupportedEarlyconHardware(SerialHardware),
}
pub type GetSerialCmdlineResult<T> = std::result::Result<T, GetSerialCmdlineError>;
pub fn get_serial_cmdline(
    cmdline: &mut kernel_cmdline::Cmdline,
    serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>,
    serial_io_type: &str,
    serial_devices: &[SerialDeviceInfo],
) -> GetSerialCmdlineResult<()> {
    for serial_parameter in serial_parameters
        .iter()
        .filter(|(_, p)| p.console)
        .map(|(k, _)| k)
    {
        match serial_parameter {
            (SerialHardware::Serial, num) => {
                cmdline
                    .insert("console", &format!("ttyS{}", num - 1))
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
            (SerialHardware::VirtioConsole, num) => {
                cmdline
                    .insert("console", &format!("hvc{}", num - 1))
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
            (SerialHardware::Debugcon, _) => {}
        }
    }
    match serial_parameters
        .iter()
        .filter(|(_, p)| p.earlycon)
        .map(|(k, _)| k)
        .next()
    {
        Some((SerialHardware::Serial, num)) => {
            if let Some(serial_device) = serial_devices.get(*num as usize - 1) {
                cmdline
                    .insert(
                        "earlycon",
                        &format!("uart8250,{},0x{:x}", serial_io_type, serial_device.address),
                    )
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
        }
        Some((hw, _num)) => {
            return Err(GetSerialCmdlineError::UnsupportedEarlyconHardware(*hw));
        }
        None => {}
    }
    Ok(())
}
#[cfg(test)]
mod tests {
    use devices::BusType;
    use kernel_cmdline::Cmdline;
    use super::*;
    #[test]
    fn get_serial_cmdline_default() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();
        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");
        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=ttyS0"));
    }
    #[test]
    fn get_serial_cmdline_virtio_console() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                console: true,
                stdin: true,
                ..Default::default()
            },
        );
        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");
        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=hvc0"));
    }
    #[test]
    fn get_serial_cmdline_virtio_console_serial_earlycon() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                console: true,
                stdin: true,
                ..Default::default()
            },
        );
        serial_parameters.insert(
            (SerialHardware::Serial, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::Serial,
                num: 1,
                earlycon: true,
                ..Default::default()
            },
        );
        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");
        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=hvc0"));
        assert!(cmdline_str.contains("earlycon=uart8250,io,0x3f8"));
    }
    #[test]
    fn get_serial_cmdline_virtio_console_invalid_earlycon() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                earlycon: true,
                stdin: true,
                ..Default::default()
            },
        );
        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect_err("get_serial_cmdline succeeded");
    }
}