use std::fmt;
use std::fmt::Display;
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::io::stdin;
use std::io::stdout;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use base::error;
use base::open_file_or_duplicate;
use base::syslog;
#[cfg(windows)]
use base::windows::Console as WinConsole;
use base::AsRawDescriptor;
use base::Event;
use base::FileSync;
use base::RawDescriptor;
use base::ReadNotifier;
use hypervisor::ProtectionType;
use remain::sorted;
use serde::Deserialize;
use serde::Serialize;
use serde_keyvalue::FromKeyValues;
use thiserror::Error as ThisError;
pub use crate::sys::serial_device::SerialDevice;
use crate::sys::serial_device::*;
use crate::PciAddress;
#[sorted]
#[derive(ThisError, Debug)]
pub enum Error {
    #[error("Unable to clone an Event: {0}")]
    CloneEvent(base::Error),
    #[error("Unable to clone a Unix Stream: {0}")]
    CloneUnixStream(std::io::Error),
    #[error("Unable to clone file: {0}")]
    FileClone(std::io::Error),
    #[error("Unable to create file '{1}': {0}")]
    FileCreate(std::io::Error, PathBuf),
    #[error("Unable to open file '{1}': {0}")]
    FileOpen(std::io::Error, PathBuf),
    #[error("Invalid serial config specified: {0}")]
    InvalidConfig(String),
    #[error("Serial device path '{0} is invalid")]
    InvalidPath(PathBuf),
    #[error("Invalid serial hardware: {0}")]
    InvalidSerialHardware(String),
    #[error("Invalid serial type: {0}")]
    InvalidSerialType(String),
    #[error("Serial device type file requires a path")]
    PathRequired,
    #[error("Failed to connect to socket: {0}")]
    SocketConnect(std::io::Error),
    #[error("Failed to create unbound socket: {0}")]
    SocketCreate(std::io::Error),
    #[error("Unable to open system type serial: {0}")]
    SystemTypeError(std::io::Error),
    #[error("Serial device type {0} not implemented")]
    Unimplemented(SerialType),
}
pub trait SerialInput: io::Read + ReadNotifier + Send {}
impl SerialInput for File {}
#[cfg(unix)]
impl SerialInput for UnixStream {}
#[cfg(windows)]
impl SerialInput for WinConsole {}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SerialType {
    File,
    Stdout,
    Sink,
    Syslog,
    #[cfg_attr(unix, serde(rename = "unix"))]
    #[cfg_attr(windows, serde(rename = "namedpipe"))]
    SystemSerialType,
    #[cfg(unix)]
    UnixStream,
}
impl Default for SerialType {
    fn default() -> Self {
        Self::Sink
    }
}
impl Display for SerialType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = match &self {
            SerialType::File => "File".to_string(),
            SerialType::Stdout => "Stdout".to_string(),
            SerialType::Sink => "Sink".to_string(),
            SerialType::Syslog => "Syslog".to_string(),
            SerialType::SystemSerialType => SYSTEM_SERIAL_TYPE_NAME.to_string(),
            #[cfg(unix)]
            SerialType::UnixStream => "UnixStream".to_string(),
        };
        write!(f, "{}", s)
    }
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SerialHardware {
    Serial,
    #[serde(alias = "legacy-virtio-console")]
    VirtioConsole,
    Debugcon,
}
impl Default for SerialHardware {
    fn default() -> Self {
        Self::Serial
    }
}
impl Display for SerialHardware {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = match &self {
            SerialHardware::Serial => "serial".to_string(),
            SerialHardware::VirtioConsole => "virtio-console".to_string(),
            SerialHardware::Debugcon => "debugcon".to_string(),
        };
        write!(f, "{}", s)
    }
}
fn serial_parameters_default_num() -> u8 {
    1
}
fn serial_parameters_default_debugcon_port() -> u16 {
    0x402
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, FromKeyValues)]
#[serde(deny_unknown_fields, rename_all = "kebab-case", default)]
pub struct SerialParameters {
    #[serde(rename = "type")]
    pub type_: SerialType,
    pub hardware: SerialHardware,
    pub name: Option<String>,
    pub path: Option<PathBuf>,
    pub input: Option<PathBuf>,
    #[cfg(unix)]
    pub input_unix_stream: bool,
    #[serde(default = "serial_parameters_default_num")]
    pub num: u8,
    pub console: bool,
    pub earlycon: bool,
    pub stdin: bool,
    #[serde(alias = "out_timestamp")]
    pub out_timestamp: bool,
    #[serde(
        alias = "debugcon_port",
        default = "serial_parameters_default_debugcon_port"
    )]
    pub debugcon_port: u16,
    pub pci_address: Option<PciAddress>,
    pub max_queue_sizes: Option<Vec<u16>>,
}
#[derive(Default)]
pub struct SerialOptions {
    pub name: Option<String>,
    pub out_timestamp: bool,
    pub console: bool,
    pub pci_address: Option<PciAddress>,
    pub max_queue_sizes: Option<Vec<u16>>,
}
impl SerialParameters {
    pub fn create_serial_device<T: SerialDevice>(
        &self,
        protection_type: ProtectionType,
        evt: &Event,
        keep_rds: &mut Vec<RawDescriptor>,
    ) -> std::result::Result<T, Error> {
        let evt = evt.try_clone().map_err(Error::CloneEvent)?;
        keep_rds.push(evt.as_raw_descriptor());
        cros_tracing::push_descriptors!(keep_rds);
        metrics::push_descriptors(keep_rds);
        #[cfg(unix)]
        if self.input_unix_stream {
            if self.input.is_some() {
                return Err(Error::InvalidConfig(
                    "input-unix-stream can't be passed when input is specified".to_string(),
                ));
            }
            if self.type_ != SerialType::UnixStream {
                return Err(Error::InvalidConfig(
                    "input-unix-stream must be used with type=unix-stream".to_string(),
                ));
            }
            return create_unix_stream_serial_device(self, protection_type, evt, keep_rds);
        }
        let input: Option<Box<dyn SerialInput>> = if let Some(input_path) = &self.input {
            let input_path = input_path.as_path();
            let input_file = open_file_or_duplicate(input_path, OpenOptions::new().read(true))
                .map_err(|e| Error::FileOpen(e.into(), input_path.into()))?;
            keep_rds.push(input_file.as_raw_descriptor());
            Some(Box::new(input_file))
        } else if self.stdin {
            keep_rds.push(stdin().as_raw_descriptor());
            Some(Box::new(ConsoleInput::new()))
        } else {
            None
        };
        let (output, sync): (
            Option<Box<dyn io::Write + Send>>,
            Option<Box<dyn FileSync + Send>>,
        ) = match self.type_ {
            SerialType::Stdout => {
                keep_rds.push(stdout().as_raw_descriptor());
                (Some(Box::new(stdout())), None)
            }
            SerialType::Sink => (None, None),
            SerialType::Syslog => {
                syslog::push_descriptors(keep_rds);
                (
                    Some(Box::new(syslog::Syslogger::new(base::syslog::Level::Info))),
                    None,
                )
            }
            SerialType::File => match &self.path {
                Some(path) => {
                    let file =
                        open_file_or_duplicate(path, OpenOptions::new().append(true).create(true))
                            .map_err(|e| Error::FileCreate(e.into(), path.clone()))?;
                    let sync = file.try_clone().map_err(Error::FileClone)?;
                    keep_rds.push(file.as_raw_descriptor());
                    keep_rds.push(sync.as_raw_descriptor());
                    (Some(Box::new(file)), Some(Box::new(sync)))
                }
                None => return Err(Error::PathRequired),
            },
            SerialType::SystemSerialType => {
                return create_system_type_serial_device(
                    self,
                    protection_type,
                    evt,
                    input,
                    keep_rds,
                );
            }
            #[cfg(unix)]
            SerialType::UnixStream => {
                let path = self.path.as_ref().ok_or(Error::PathRequired)?;
                let output = UnixStream::connect(path).map_err(Error::SocketConnect)?;
                keep_rds.push(output.as_raw_descriptor());
                (Some(Box::new(output)), None)
            }
        };
        Ok(T::new(
            protection_type,
            evt,
            input,
            output,
            sync,
            SerialOptions {
                name: self.name.clone(),
                out_timestamp: self.out_timestamp,
                console: self.console,
                pci_address: self.pci_address,
                max_queue_sizes: self.max_queue_sizes.clone(),
            },
            keep_rds.to_vec(),
        ))
    }
}
#[cfg(test)]
mod tests {
    use serde_keyvalue::*;
    use super::*;
    fn from_serial_arg(options: &str) -> Result<SerialParameters, ParseError> {
        from_key_values(options)
    }
    #[test]
    fn params_from_key_values() {
        let params = from_serial_arg("").unwrap();
        assert_eq!(
            params,
            SerialParameters {
                type_: SerialType::Sink,
                hardware: SerialHardware::Serial,
                name: None,
                path: None,
                input: None,
                #[cfg(unix)]
                input_unix_stream: false,
                num: 1,
                console: false,
                earlycon: false,
                stdin: false,
                out_timestamp: false,
                debugcon_port: 0x402,
                pci_address: None,
                max_queue_sizes: None,
            }
        );
        let params = from_serial_arg("type=file").unwrap();
        assert_eq!(params.type_, SerialType::File);
        let params = from_serial_arg("type=stdout").unwrap();
        assert_eq!(params.type_, SerialType::Stdout);
        let params = from_serial_arg("type=sink").unwrap();
        assert_eq!(params.type_, SerialType::Sink);
        let params = from_serial_arg("type=syslog").unwrap();
        assert_eq!(params.type_, SerialType::Syslog);
        #[cfg(any(target_os = "android", target_os = "linux"))]
        let opt = "type=unix";
        #[cfg(windows)]
        let opt = "type=namedpipe";
        let params = from_serial_arg(opt).unwrap();
        assert_eq!(params.type_, SerialType::SystemSerialType);
        #[cfg(unix)]
        {
            let params = from_serial_arg("type=unix-stream").unwrap();
            assert_eq!(params.type_, SerialType::UnixStream);
        }
        let params = from_serial_arg("type=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("hardware=serial").unwrap();
        assert_eq!(params.hardware, SerialHardware::Serial);
        let params = from_serial_arg("hardware=virtio-console").unwrap();
        assert_eq!(params.hardware, SerialHardware::VirtioConsole);
        let params = from_serial_arg("hardware=debugcon").unwrap();
        assert_eq!(params.hardware, SerialHardware::Debugcon);
        let params = from_serial_arg("hardware=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("path=/test/path").unwrap();
        assert_eq!(params.path, Some("/test/path".into()));
        let params = from_serial_arg("path");
        assert!(params.is_err());
        let params = from_serial_arg("input=/path/to/input").unwrap();
        assert_eq!(params.input, Some("/path/to/input".into()));
        let params = from_serial_arg("input");
        assert!(params.is_err());
        #[cfg(unix)]
        {
            let params = from_serial_arg("input-unix-stream").unwrap();
            assert!(params.input_unix_stream);
            let params = from_serial_arg("input-unix-stream=true").unwrap();
            assert!(params.input_unix_stream);
            let params = from_serial_arg("input-unix-stream=false").unwrap();
            assert!(!params.input_unix_stream);
            let params = from_serial_arg("input-unix-stream=foobar");
            assert!(params.is_err());
        }
        let params = from_serial_arg("console").unwrap();
        assert!(params.console);
        let params = from_serial_arg("console=true").unwrap();
        assert!(params.console);
        let params = from_serial_arg("console=false").unwrap();
        assert!(!params.console);
        let params = from_serial_arg("console=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("earlycon").unwrap();
        assert!(params.earlycon);
        let params = from_serial_arg("earlycon=true").unwrap();
        assert!(params.earlycon);
        let params = from_serial_arg("earlycon=false").unwrap();
        assert!(!params.earlycon);
        let params = from_serial_arg("earlycon=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("stdin").unwrap();
        assert!(params.stdin);
        let params = from_serial_arg("stdin=true").unwrap();
        assert!(params.stdin);
        let params = from_serial_arg("stdin=false").unwrap();
        assert!(!params.stdin);
        let params = from_serial_arg("stdin=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("out-timestamp").unwrap();
        assert!(params.out_timestamp);
        let params = from_serial_arg("out-timestamp=true").unwrap();
        assert!(params.out_timestamp);
        let params = from_serial_arg("out-timestamp=false").unwrap();
        assert!(!params.out_timestamp);
        let params = from_serial_arg("out-timestamp=foobar");
        assert!(params.is_err());
        let params = from_serial_arg("out_timestamp=true").unwrap();
        assert!(params.out_timestamp);
        let params = from_serial_arg("debugcon-port=1026").unwrap();
        assert_eq!(params.debugcon_port, 1026);
        let params = from_serial_arg("debugcon_port=1026").unwrap();
        assert_eq!(params.debugcon_port, 1026);
        let params = from_serial_arg("type=stdout,path=/some/path,hardware=virtio-console,num=5,earlycon,console,stdin,input=/some/input,out_timestamp,debugcon_port=12,pci-address=00:0e.0,max-queue-sizes=[1,2]").unwrap();
        assert_eq!(
            params,
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                name: None,
                path: Some("/some/path".into()),
                input: Some("/some/input".into()),
                #[cfg(unix)]
                input_unix_stream: false,
                num: 5,
                console: true,
                earlycon: true,
                stdin: true,
                out_timestamp: true,
                debugcon_port: 12,
                pci_address: Some(PciAddress {
                    bus: 0,
                    dev: 14,
                    func: 0
                }),
                max_queue_sizes: Some(vec![1, 2]),
            }
        );
        let params = from_serial_arg("type=stdout,foo=bar");
        assert!(params.is_err());
    }
}